diff --git a/.gitignore b/.gitignore
index 01684a8..3655f0f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,7 +15,6 @@
# Git worktrees for parallel development
.worktrees/
-
# Claude Code personal workflows (local only)
.claude/
CLAUDE.md
diff --git a/app/controllers/solid_queue_monitor/base_controller.rb b/app/controllers/solid_queue_monitor/base_controller.rb
index ed49147..19fe641 100644
--- a/app/controllers/solid_queue_monitor/base_controller.rb
+++ b/app/controllers/solid_queue_monitor/base_controller.rb
@@ -6,7 +6,7 @@ def paginate(relation)
PaginationService.new(relation, current_page, per_page).paginate
end
- def render_page(title, content)
+ def render_page(title, content, search_query: nil)
# Get flash message from instance variable (set by set_flash_message) or session
message = @flash_message
message_type = @flash_type
@@ -27,7 +27,8 @@ def render_page(title, content)
title: title,
content: content,
message: message,
- message_type: message_type
+ message_type: message_type,
+ search_query: search_query
).generate
render html: html.html_safe
diff --git a/app/controllers/solid_queue_monitor/search_controller.rb b/app/controllers/solid_queue_monitor/search_controller.rb
new file mode 100644
index 0000000..ca0108e
--- /dev/null
+++ b/app/controllers/solid_queue_monitor/search_controller.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module SolidQueueMonitor
+ class SearchController < BaseController
+ def index
+ query = params[:q]
+ results = SearchService.new(query).search
+
+ render_page('Search', SearchResultsPresenter.new(query, results).render, search_query: query)
+ end
+ end
+end
diff --git a/app/presenters/solid_queue_monitor/search_results_presenter.rb b/app/presenters/solid_queue_monitor/search_results_presenter.rb
new file mode 100644
index 0000000..83a3cab
--- /dev/null
+++ b/app/presenters/solid_queue_monitor/search_results_presenter.rb
@@ -0,0 +1,190 @@
+# frozen_string_literal: true
+
+module SolidQueueMonitor
+ class SearchResultsPresenter < BasePresenter
+ def initialize(query, results)
+ @query = query
+ @results = results
+ end
+
+ def render
+ section_wrapper('Search Results', generate_content)
+ end
+
+ private
+
+ def generate_content
+ if @query.blank?
+ generate_empty_query_message
+ elsif total_count.zero?
+ generate_no_results_message
+ else
+ generate_results_summary + generate_all_sections
+ end
+ end
+
+ def generate_empty_query_message
+ <<-HTML
+
+
Enter a search term in the header to find jobs across all categories.
+
+ HTML
+ end
+
+ def generate_no_results_message
+ <<-HTML
+
+
No results found for "#{escape_html(@query)}"
+
0 results
+
+ HTML
+ end
+
+ def generate_results_summary
+ <<-HTML
+
+
Found #{total_count} #{total_count == 1 ? 'result' : 'results'} for "#{escape_html(@query)}"
+
+ HTML
+ end
+
+ def generate_all_sections
+ sections = []
+ sections << generate_ready_section if @results[:ready].any?
+ sections << generate_scheduled_section if @results[:scheduled].any?
+ sections << generate_failed_section if @results[:failed].any?
+ sections << generate_in_progress_section if @results[:in_progress].any?
+ sections << generate_completed_section if @results[:completed].any?
+ sections << generate_recurring_section if @results[:recurring].any?
+ sections.join
+ end
+
+ def generate_ready_section
+ generate_section('Ready Jobs', @results[:ready]) do |execution|
+ generate_job_row(execution.job, execution.queue_name, execution.created_at)
+ end
+ end
+
+ def generate_scheduled_section
+ generate_section('Scheduled Jobs', @results[:scheduled]) do |execution|
+ generate_job_row(execution.job, execution.queue_name, execution.scheduled_at, 'Scheduled for')
+ end
+ end
+
+ def generate_failed_section
+ generate_section('Failed Jobs', @results[:failed]) do |execution|
+ generate_failed_row(execution)
+ end
+ end
+
+ def generate_in_progress_section
+ generate_section('In Progress Jobs', @results[:in_progress]) do |execution|
+ generate_job_row(execution.job, execution.job.queue_name, execution.created_at, 'Started at')
+ end
+ end
+
+ def generate_completed_section
+ generate_section('Completed Jobs', @results[:completed]) do |job|
+ generate_completed_row(job)
+ end
+ end
+
+ def generate_recurring_section
+ generate_section('Recurring Tasks', @results[:recurring]) do |task|
+ generate_recurring_row(task)
+ end
+ end
+
+ def generate_section(title, items, &block)
+ <<-HTML
+
+
#{title} (#{items.size})
+
+
+
+
+ #{section_headers(title)}
+
+
+
+ #{items.map(&block).join}
+
+
+
+
+ HTML
+ end
+
+ def section_headers(title)
+ case title
+ when 'Recurring Tasks'
+ 'Key | Class | Schedule | Queue | '
+ when 'Failed Jobs'
+ 'Job | Queue | Error | Failed At | '
+ when 'Completed Jobs'
+ 'Job | Queue | Arguments | Completed At | '
+ else
+ 'Job | Queue | Arguments | Time | '
+ end
+ end
+
+ def generate_job_row(job, queue_name, time, time_label = 'Created at')
+ <<-HTML
+
+ | #{job.class_name} |
+ #{queue_link(queue_name)} |
+ #{format_arguments(job.arguments)} |
+
+ #{time_label}: #{format_datetime(time)}
+ |
+
+ HTML
+ end
+
+ def generate_failed_row(execution)
+ job = execution.job
+ <<-HTML
+
+ | #{job.class_name} |
+ #{queue_link(job.queue_name)} |
+ #{escape_html(execution.error.to_s.truncate(100))} |
+
+ #{format_datetime(execution.created_at)}
+ |
+
+ HTML
+ end
+
+ def generate_completed_row(job)
+ <<-HTML
+
+ | #{job.class_name} |
+ #{queue_link(job.queue_name)} |
+ #{format_arguments(job.arguments)} |
+
+ #{format_datetime(job.finished_at)}
+ |
+
+ HTML
+ end
+
+ def generate_recurring_row(task)
+ <<-HTML
+
+ | #{task.key} |
+ #{task.class_name || '-'} |
+ #{task.schedule} |
+ #{queue_link(task.queue_name)} |
+
+ HTML
+ end
+
+ def total_count
+ @total_count ||= @results.values.sum(&:size)
+ end
+
+ def escape_html(text)
+ text.to_s.gsub('&', '&').gsub('<', '<').gsub('>', '>').gsub('"', '"')
+ end
+ end
+end
diff --git a/app/services/solid_queue_monitor/html_generator.rb b/app/services/solid_queue_monitor/html_generator.rb
index 51ab10f..3a37d68 100644
--- a/app/services/solid_queue_monitor/html_generator.rb
+++ b/app/services/solid_queue_monitor/html_generator.rb
@@ -5,11 +5,12 @@ class HtmlGenerator
include Rails.application.routes.url_helpers
include SolidQueueMonitor::Engine.routes.url_helpers
- def initialize(title:, content:, message: nil, message_type: nil)
+ def initialize(title:, content:, message: nil, message_type: nil, search_query: nil)
@title = title
@content = content
@message = message
@message_type = message_type
+ @search_query = search_query
end
def generate
@@ -107,7 +108,8 @@ def generate_header
<<-HTML