From 78323480d0367b6507f1815c89e34e3569302406 Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Tue, 26 May 2026 08:20:16 -0400 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20auto-refresh=20via=20Stimulus=20?= =?UTF-8?q?=E2=80=94=20dashboard,=20jobs,=20processes,=20history?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a RefreshController that polls on a configurable interval using fetch + DOMParser to swap turbo-frame content without a full page reload. Pauses when the tab is hidden or any checkbox is checked (preserves selection state). Resumes immediately on tab focus. Frames added: - sqw-dashboard (dashboard_refresh_interval, default 5s) - sqw-jobs-filter (default_refresh_interval, default 10s) - sqw-processes (default_refresh_interval) - sqw-history-table (default_refresh_interval) Configuration additions: dashboard_refresh_interval, default_refresh_interval, search_results_limit (for future search feature, default 25). Co-Authored-By: Claude Sonnet 4.6 --- app/javascript/solid_stack_web/application.js | 2 + .../solid_stack_web/refresh_controller.js | 52 +++++++++++++++++++ .../solid_stack_web/dashboard/index.html.erb | 3 ++ .../solid_stack_web/history/index.html.erb | 3 ++ app/views/solid_stack_web/jobs/index.html.erb | 6 ++- .../solid_stack_web/processes/index.html.erb | 3 ++ config/importmap.rb | 1 + lib/solid_stack_web.rb | 16 +++++- 8 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 app/javascript/solid_stack_web/refresh_controller.js diff --git a/app/javascript/solid_stack_web/application.js b/app/javascript/solid_stack_web/application.js index 44ec66d..050a9d5 100644 --- a/app/javascript/solid_stack_web/application.js +++ b/app/javascript/solid_stack_web/application.js @@ -1,8 +1,10 @@ import "@hotwired/turbo" import { Application } from "@hotwired/stimulus" +import RefreshController from "solid_stack_web/refresh_controller" import SelectionController from "solid_stack_web/selection_controller" import SparklineTooltipController from "solid_stack_web/sparkline_tooltip_controller" const application = Application.start() +application.register("refresh", RefreshController) application.register("selection", SelectionController) application.register("sparkline-tooltip", SparklineTooltipController) \ No newline at end of file diff --git a/app/javascript/solid_stack_web/refresh_controller.js b/app/javascript/solid_stack_web/refresh_controller.js new file mode 100644 index 0000000..7f62b31 --- /dev/null +++ b/app/javascript/solid_stack_web/refresh_controller.js @@ -0,0 +1,52 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static values = { interval: { type: Number, default: 5000 } } + + initialize() { + this._onVisibilityChange = this._onVisibilityChange.bind(this) + } + + connect() { + document.addEventListener("visibilitychange", this._onVisibilityChange) + this._schedule() + } + + disconnect() { + clearTimeout(this._timer) + document.removeEventListener("visibilitychange", this._onVisibilityChange) + } + + _schedule() { + this._timer = setTimeout(() => this._reload(), this.intervalValue) + } + + async _reload() { + clearTimeout(this._timer) + const hasSelection = this.element.querySelector("input[type='checkbox']:checked") + if (!document.hidden && !hasSelection) { + try { + const response = await fetch(window.location.href, { + headers: { "Turbo-Frame": this.element.id, Accept: "text/html" } + }) + if (response.ok) { + const html = await response.text() + const doc = new DOMParser().parseFromString(html, "text/html") + const frame = doc.querySelector(`turbo-frame#${this.element.id}`) + if (frame && this.element.isConnected) this.element.innerHTML = frame.innerHTML + } + } catch { + // network error — skip this tick + } + } + if (this.element.isConnected) this._schedule() + } + + _onVisibilityChange() { + if (document.hidden) { + clearTimeout(this._timer) + } else if (!this.element.querySelector("input[type='checkbox']:checked")) { + this._reload() + } + } +} \ No newline at end of file diff --git a/app/views/solid_stack_web/dashboard/index.html.erb b/app/views/solid_stack_web/dashboard/index.html.erb index bb1789e..f7d2eb6 100644 --- a/app/views/solid_stack_web/dashboard/index.html.erb +++ b/app/views/solid_stack_web/dashboard/index.html.erb @@ -1,3 +1,5 @@ +<%= turbo_frame_tag "sqw-dashboard", target: "_top", + data: { controller: "refresh", refresh_interval_value: SolidStackWeb.dashboard_refresh_interval } do %>

Overview

@@ -102,3 +104,4 @@ +<% end %> \ No newline at end of file diff --git a/app/views/solid_stack_web/history/index.html.erb b/app/views/solid_stack_web/history/index.html.erb index d2f4184..c812096 100644 --- a/app/views/solid_stack_web/history/index.html.erb +++ b/app/views/solid_stack_web/history/index.html.erb @@ -1,3 +1,5 @@ +<%= turbo_frame_tag "sqw-history-table", + data: { controller: "refresh", refresh_interval_value: SolidStackWeb.default_refresh_interval } do %>

Job History

@@ -70,4 +72,5 @@

No finished jobs found.

+<% end %> <% end %> \ No newline at end of file diff --git a/app/views/solid_stack_web/jobs/index.html.erb b/app/views/solid_stack_web/jobs/index.html.erb index ce9876e..b5c1ca8 100644 --- a/app/views/solid_stack_web/jobs/index.html.erb +++ b/app/views/solid_stack_web/jobs/index.html.erb @@ -29,7 +29,9 @@ <% end %>
- +<%= turbo_frame_tag "sqw-jobs-filter", + data: { turbo_action: "advance", controller: "refresh", + refresh_interval_value: SolidStackWeb.default_refresh_interval } do %>
<%= hidden_field_tag :status, @status %> <%= hidden_field_tag :period, @period %> @@ -151,4 +153,4 @@ <%= render "empty" %> <% end %>
- \ No newline at end of file +<% end %> \ No newline at end of file diff --git a/app/views/solid_stack_web/processes/index.html.erb b/app/views/solid_stack_web/processes/index.html.erb index 92cc4de..512e933 100644 --- a/app/views/solid_stack_web/processes/index.html.erb +++ b/app/views/solid_stack_web/processes/index.html.erb @@ -1,3 +1,5 @@ +<%= turbo_frame_tag "sqw-processes", target: "_top", + data: { controller: "refresh", refresh_interval_value: SolidStackWeb.default_refresh_interval } do %>

Processes

@@ -30,3 +32,4 @@

No active processes.

<% end %> +<% end %> diff --git a/config/importmap.rb b/config/importmap.rb index bac4a65..0c67d20 100644 --- a/config/importmap.rb +++ b/config/importmap.rb @@ -1,5 +1,6 @@ pin "@hotwired/turbo", to: "https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.23/dist/turbo.es2017-esm.js" pin "@hotwired/stimulus", to: "https://cdn.jsdelivr.net/npm/@hotwired/stimulus@3.2.2/dist/stimulus.js" pin "solid_stack_web", to: "solid_stack_web/application.js" +pin "solid_stack_web/refresh_controller", to: "solid_stack_web/refresh_controller.js" pin "solid_stack_web/selection_controller", to: "solid_stack_web/selection_controller.js" pin "solid_stack_web/sparkline_tooltip_controller", to: "solid_stack_web/sparkline_tooltip_controller.js" diff --git a/lib/solid_stack_web.rb b/lib/solid_stack_web.rb index b5f73be..d233924 100644 --- a/lib/solid_stack_web.rb +++ b/lib/solid_stack_web.rb @@ -5,7 +5,9 @@ module SolidStackWeb class << self attr_writer :page_size, :connects_to, :slow_job_threshold, :alert_webhook_url, :alert_webhook_cooldown, - :alert_failure_threshold, :alert_queue_thresholds + :alert_failure_threshold, :alert_queue_thresholds, + :dashboard_refresh_interval, :default_refresh_interval, + :search_results_limit def page_size @page_size || 25 @@ -35,6 +37,18 @@ def alert_queue_thresholds @alert_queue_thresholds || {} end + def dashboard_refresh_interval + @dashboard_refresh_interval || 5_000 + end + + def default_refresh_interval + @default_refresh_interval || 10_000 + end + + def search_results_limit + @search_results_limit || 25 + end + def configure yield self end From c66aff8a5fa67acaf6dcd04fcdbaa8ec99ab2b07 Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Tue, 26 May 2026 08:22:38 -0400 Subject: [PATCH 2/2] docs: update CHANGELOG, README, and ROADMAP for auto-refresh Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 1 + README.md | 8 ++++++++ ROADMAP.md | 4 +--- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cb94b9..31fec53 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 +- Auto-refresh — dashboard, jobs, processes, and history views now poll automatically via a Stimulus `RefreshController`; refresh pauses when the browser tab is hidden or a checkbox is checked and resumes immediately on tab focus; intervals are configurable via `dashboard_refresh_interval` (default 5 s) and `default_refresh_interval` (default 10 s); `search_results_limit` (default 25) added as a configuration attribute for the upcoming search feature - Queue depth sparklines — the Queues index gains a "Depth (12h)" column with a compact 12-hour rolling bar chart per queue; each bar is the ready-job depth at an hourly snapshot; hovering a bar shows an instant Stimulus tooltip ("N ready jobs Xh ago"); reuses the `sparkline-tooltip` Stimulus controller - Throughput sparkline — the Solid Queue dashboard card now shows a 12-hour rolling bar chart of completed jobs; bars are hourly buckets rendered as inline SVG with `currentColor` so they respect the card's theme; zero-count buckets render at minimum height with reduced opacity; per-bar Stimulus tooltip gives instant hover feedback with exact counts and time ranges - Alert webhooks — HTTP POST to a configurable URL when the failed-job count meets `alert_failure_threshold` or a queue's ready depth meets a per-queue limit in `alert_queue_thresholds`; a `alert_webhook_cooldown` (default 3600 s) prevents alert storms; delivery failures are swallowed so they never affect request responses; triggered on every `GET /metrics` poll diff --git a/README.md b/README.md index 9f2870b..c2c6784 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ The dashboard will be available at `/solid_stack` (or whatever path you choose). - **Recurring task list** — enumerates all `SolidQueue::RecurringTask` records with cron schedule, job class or command, queue, next-run and last-run times, and a static/dynamic badge; each row has a "Run Now" button - **Performance statistics page** — `GET /stats` aggregates finished jobs by class name with execution count, avg, p50, p95, min, and max duration; click any column header to sort; defaults to p95 descending - **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 +- **Auto-refresh** — dashboard, jobs, processes, and history views poll automatically; pauses when the tab is hidden or a checkbox is checked; intervals configurable via `dashboard_refresh_interval` and `default_refresh_interval` - **Turbo Stream** job discard — removes the row inline without a full page reload ### Configuration @@ -66,6 +67,13 @@ SolidStackWeb.configure do |config| "default" => 500 } config.alert_webhook_cooldown = 3600 # seconds between alerts (default: 3600) + + # Auto-refresh intervals in milliseconds. + config.dashboard_refresh_interval = 5_000 # overview dashboard (default: 5000) + config.default_refresh_interval = 10_000 # jobs, processes, history (default: 10000) + + # Maximum results shown by the search feature (default: 25). + config.search_results_limit = 25 end ``` diff --git a/ROADMAP.md b/ROADMAP.md index 225afd7..fa7af30 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -10,9 +10,7 @@ The path to v1.0.0 is staged: first achieve feature parity with `solid_queue_das > _Give operators the data they need to detect problems before users do._ -### Added -- **Dashboard & table auto-refresh** — configurable polling intervals (`dashboard_refresh_interval`, `default_refresh_interval`) via Stimulus -- **Configuration additions**: `dashboard_refresh_interval`, `default_refresh_interval`, `search_results_limit` +_All items shipped._ ---