diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fef07f..df4e318 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Job history view — `GET /history` lists all finished jobs with class name, queue, duration, and finished-at time; filterable by queue, class substring, and time period (1h / 24h / 7d); clicking a queue badge filters the history to that queue; CSV export respects active filters; "History" link added to the queue subnav - Scheduled job management — "Run Now" and offset buttons (+1h / +24h / +7d) on each scheduled job row; Turbo Stream removes the row on run-now and updates the scheduled-at cell on offset reschedule; "Run All Now (N)" header button back-dates all matching scheduled executions; backed by `ScheduledJobsController` using standard CRUD (`update` for single, `create` for bulk via `run_all_now` collection route) ## [0.2.0] - 2026-05-25 diff --git a/README.md b/README.md index cda1c4f..950789f 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ A mountable Rails engine that provides a unified web dashboard for the full [Sol - **Overview dashboard** with live counts across all three Solid Stack components; cards are clickable and link directly to each section - **Solid Queue** — browse jobs by status (ready, scheduled, claimed, blocked) with filtering by job class, queue name, priority, and time period; manage failed jobs (retry / discard / bulk retry / bulk discard), pause/resume queues, and inspect worker processes; **Bulk selection** checkbox-selects individual jobs for discard or retry; **Discard All** bulk-discards every job matching the current filters in one request; **CSV export** downloads jobs or failed jobs as a CSV file respecting active filters +- **Job history view** — paginated list of all finished jobs with class name, queue, duration, and finished-at time; filterable by queue (click a badge), class substring, and time period; CSV export respects active filters - **Scheduled job management** — "Run Now" and offset buttons (+1h / +24h / +7d) per row update the scheduled time inline via Turbo Stream; "Run All Now (N)" in the header back-dates all matching executions at once - **Job detail page** — drill into any job to see full arguments (pretty-printed JSON), queue, priority, enqueued time, Active Job ID, concurrency key, scheduled/blocked-until metadata, and a Discard button - **Failed job detail page** — drill into any failed job to see the full error, backtrace, and an inline JSON argument editor; submit to update arguments and retry in one action diff --git a/app/controllers/solid_stack_web/application_controller.rb b/app/controllers/solid_stack_web/application_controller.rb index 12051f4..57da95a 100644 --- a/app/controllers/solid_stack_web/application_controller.rb +++ b/app/controllers/solid_stack_web/application_controller.rb @@ -15,7 +15,7 @@ class ApplicationController < ActionController::Base def current_section case controller_name - when "jobs", "failed_jobs", "queues", "processes" then :queue + when "jobs", "failed_jobs", "queues", "processes", "history", "scheduled_jobs" then :queue when "cache" then :cache when "cable" then :cable else :overview diff --git a/app/controllers/solid_stack_web/history_controller.rb b/app/controllers/solid_stack_web/history_controller.rb new file mode 100644 index 0000000..1738717 --- /dev/null +++ b/app/controllers/solid_stack_web/history_controller.rb @@ -0,0 +1,42 @@ +module SolidStackWeb + class HistoryController < ApplicationController + before_action :set_filters + + def index + respond_to do |format| + format.html { @pagy, @jobs = pagy(filtered_scope) } + format.csv do + send_data history_csv(filtered_scope), + filename: "job-history-#{Date.today}.csv", + type: "text/csv", disposition: "attachment" + end + end + end + + private + + def set_filters + @queue = params[:queue].presence + @search = params[:q].presence + @period = params[:period].presence_in(PERIOD_DURATIONS.keys) + end + + def filtered_scope + scope = SolidQueue::Job.where.not(finished_at: nil).order(finished_at: :desc) + scope = scope.where(queue_name: @queue) if @queue.present? + scope = scope.where("class_name LIKE ?", "%#{@search}%") if @search.present? + scope = scope.where("finished_at >= ?", PERIOD_DURATIONS[@period].ago) if @period.present? + scope + end + + def history_csv(scope) + CSV.generate(headers: true) do |csv| + csv << %w[id class_name queue_name duration_seconds finished_at] + scope.order(finished_at: :desc).each do |job| + duration = job.finished_at && job.created_at ? (job.finished_at - job.created_at).round : nil + csv << [job.id, job.class_name, job.queue_name, duration, job.finished_at.iso8601] + end + end + end + end +end diff --git a/app/helpers/solid_stack_web/application_helper.rb b/app/helpers/solid_stack_web/application_helper.rb index d22a71b..8ade74f 100644 --- a/app/helpers/solid_stack_web/application_helper.rb +++ b/app/helpers/solid_stack_web/application_helper.rb @@ -1,5 +1,13 @@ module SolidStackWeb module ApplicationHelper + def format_duration(seconds) + s = seconds.to_i + return "#{s}s" if s < 60 + return "#{s / 60}m #{s % 60}s" if s < 3600 + + "#{s / 3600}h #{(s % 3600) / 60}m" + end + def inline_styles dir = SolidStackWeb::Engine.root.join("app/assets/stylesheets/solid_stack_web") css = dir.glob("_*.css").sort.map(&:read).join("\n") diff --git a/app/views/layouts/solid_stack_web/application.html.erb b/app/views/layouts/solid_stack_web/application.html.erb index 3e1f833..40dbdb0 100644 --- a/app/views/layouts/solid_stack_web/application.html.erb +++ b/app/views/layouts/solid_stack_web/application.html.erb @@ -33,6 +33,8 @@ class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "failed_jobs"}" %> <%= link_to "Queues", queues_path, class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "queues"}" %> + <%= link_to "History", history_path, + class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "history"}" %> <%= link_to "Processes", processes_path, class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "processes"}" %> diff --git a/app/views/solid_stack_web/history/index.html.erb b/app/views/solid_stack_web/history/index.html.erb new file mode 100644 index 0000000..d2f4184 --- /dev/null +++ b/app/views/solid_stack_web/history/index.html.erb @@ -0,0 +1,73 @@ +
+ Filtering by queue: <%= @queue %> — + <%= link_to "Clear filter", history_path(q: @search, period: @period) %> +
+<% end %> + +<% if @jobs.any? %> +| Job Class | +Queue | +Duration | +Finished At | +
|---|---|---|---|
| <%= job.class_name %> | ++ <%= link_to job.queue_name, + history_path(queue: job.queue_name, q: @search, period: @period), + class: "sqw-badge sqw-badge--queue" %> + | +<%= format_duration(job.finished_at - job.created_at) %> | +<%= job.finished_at.strftime("%b %d %H:%M:%S") %> | +
No finished jobs found.
+