From 6444a21d17f5471ef7435ad8e9c2b82cf0c9120d Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Tue, 19 May 2026 11:56:15 -0400 Subject: [PATCH 1/5] feat: auto-refresh dashboard, jobs, and processes via Stimulus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a `refresh` Stimulus controller that calls `turbo-frame#reload()` on a configurable interval (default 5 s). The timer resets after each frame load completes and is paused while the browser tab is hidden, so background tabs don't generate unnecessary requests. Wired to: - Dashboard stat grid (5 s) — reflects changing job counts in real time - Jobs index table (10 s) — already inside a turbo-frame; just adds the controller - Processes table (10 s) — wrapped in a new turbo-frame; keeps heartbeat status current Co-Authored-By: Claude Sonnet 4.6 --- app/javascript/solid_queue_web/application.js | 2 + .../solid_queue_web/refresh_controller.js | 41 +++++++++++++++++++ .../solid_queue_web/dashboard/index.html.erb | 4 +- app/views/solid_queue_web/jobs/index.html.erb | 2 +- .../solid_queue_web/processes/index.html.erb | 4 +- config/importmap.rb | 1 + 6 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 app/javascript/solid_queue_web/refresh_controller.js diff --git a/app/javascript/solid_queue_web/application.js b/app/javascript/solid_queue_web/application.js index ad1349d..7e08a27 100644 --- a/app/javascript/solid_queue_web/application.js +++ b/app/javascript/solid_queue_web/application.js @@ -1,6 +1,8 @@ import "@hotwired/turbo" import { Application } from "@hotwired/stimulus" import SearchController from "solid_queue_web/search_controller" +import RefreshController from "solid_queue_web/refresh_controller" const application = Application.start() application.register("search", SearchController) +application.register("refresh", RefreshController) diff --git a/app/javascript/solid_queue_web/refresh_controller.js b/app/javascript/solid_queue_web/refresh_controller.js new file mode 100644 index 0000000..cd98810 --- /dev/null +++ b/app/javascript/solid_queue_web/refresh_controller.js @@ -0,0 +1,41 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static values = { interval: { type: Number, default: 5000 } } + + initialize() { + this._onLoad = this._onLoad.bind(this) + this._onVisibilityChange = this._onVisibilityChange.bind(this) + } + + connect() { + this.element.addEventListener("turbo:frame-load", this._onLoad) + document.addEventListener("visibilitychange", this._onVisibilityChange) + this._schedule() + } + + disconnect() { + clearTimeout(this._timer) + this.element.removeEventListener("turbo:frame-load", this._onLoad) + document.removeEventListener("visibilitychange", this._onVisibilityChange) + } + + _schedule() { + this._timer = setTimeout(() => { + if (!document.hidden) this.element.reload() + }, this.intervalValue) + } + + _onLoad() { + clearTimeout(this._timer) + this._schedule() + } + + _onVisibilityChange() { + if (document.hidden) { + clearTimeout(this._timer) + } else { + this.element.reload() + } + } +} \ No newline at end of file diff --git a/app/views/solid_queue_web/dashboard/index.html.erb b/app/views/solid_queue_web/dashboard/index.html.erb index 56d5a38..0dbd110 100644 --- a/app/views/solid_queue_web/dashboard/index.html.erb +++ b/app/views/solid_queue_web/dashboard/index.html.erb @@ -1,3 +1,4 @@ +<%= turbo_frame_tag "dashboard", data: { controller: "refresh", refresh_interval_value: 5000 } do %>

Dashboard

@@ -62,4 +63,5 @@
<% end %> - \ No newline at end of file + +<% end %> \ No newline at end of file diff --git a/app/views/solid_queue_web/jobs/index.html.erb b/app/views/solid_queue_web/jobs/index.html.erb index 44c64c0..c095a4d 100644 --- a/app/views/solid_queue_web/jobs/index.html.erb +++ b/app/views/solid_queue_web/jobs/index.html.erb @@ -1,6 +1,6 @@

Jobs

-<%= turbo_frame_tag "jobs-table", data: { turbo_action: "advance" } do %> +<%= turbo_frame_tag "jobs-table", data: { turbo_action: "advance", controller: "refresh", refresh_interval_value: 10000 } do %> <% discardable = SolidQueueWeb::Job::DISCARDABLE.include?(@status) %>
diff --git a/app/views/solid_queue_web/processes/index.html.erb b/app/views/solid_queue_web/processes/index.html.erb index 7d1f0ba..6e39f9b 100644 --- a/app/views/solid_queue_web/processes/index.html.erb +++ b/app/views/solid_queue_web/processes/index.html.erb @@ -1,3 +1,4 @@ +<%= turbo_frame_tag "processes", data: { controller: "refresh", refresh_interval_value: 10000 } do %>

Processes

@@ -51,4 +52,5 @@ <% end %> -
\ No newline at end of file +
+<% end %> \ No newline at end of file diff --git a/config/importmap.rb b/config/importmap.rb index 75bc253..ca0bd21 100644 --- a/config/importmap.rb +++ b/config/importmap.rb @@ -1,2 +1,3 @@ pin "solid_queue_web", to: "solid_queue_web/application.js" pin "solid_queue_web/search_controller", to: "solid_queue_web/search_controller.js" +pin "solid_queue_web/refresh_controller", to: "solid_queue_web/refresh_controller.js" From 3bf963bf72f5929a002274fde5dd76c7a773407c Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Tue, 19 May 2026 12:15:36 -0400 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20refresh=20controller=20=E2=80=94=20u?= =?UTF-8?q?se=20fetch=20to=20reload=20frame=20without=20a=20src=20attribut?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit turbo-frame#reload() is a no-op when the frame has no src attribute, which is the case for all server-rendered frames here. Replace with a direct fetch that sends the Turbo-Frame header and swaps the matching frame's innerHTML. Timer reschedules after each tick regardless of success/failure; pauses while the tab is hidden. Co-Authored-By: Claude Sonnet 4.6 --- .../solid_queue_web/refresh_controller.js | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/app/javascript/solid_queue_web/refresh_controller.js b/app/javascript/solid_queue_web/refresh_controller.js index cd98810..f0eedf6 100644 --- a/app/javascript/solid_queue_web/refresh_controller.js +++ b/app/javascript/solid_queue_web/refresh_controller.js @@ -4,38 +4,48 @@ export default class extends Controller { static values = { interval: { type: Number, default: 5000 } } initialize() { - this._onLoad = this._onLoad.bind(this) this._onVisibilityChange = this._onVisibilityChange.bind(this) } connect() { - this.element.addEventListener("turbo:frame-load", this._onLoad) document.addEventListener("visibilitychange", this._onVisibilityChange) this._schedule() } disconnect() { clearTimeout(this._timer) - this.element.removeEventListener("turbo:frame-load", this._onLoad) document.removeEventListener("visibilitychange", this._onVisibilityChange) } _schedule() { - this._timer = setTimeout(() => { - if (!document.hidden) this.element.reload() - }, this.intervalValue) + this._timer = setTimeout(() => this._reload(), this.intervalValue) } - _onLoad() { + async _reload() { clearTimeout(this._timer) - this._schedule() + if (!document.hidden) { + 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 { - this.element.reload() + this._reload() } } } \ No newline at end of file From 29cce6c536909f50194dcd7820bf0aefa9670eab Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Tue, 19 May 2026 12:17:24 -0400 Subject: [PATCH 3/5] fix: add target="_top" to refresh frames so links navigate the full page Links inside a turbo-frame target the frame by default; without a matching frame on the destination page, Turbo renders "Content missing". Setting target="_top" on the dashboard and processes frames makes all link clicks break out to a full-page navigation while leaving the fetch-based auto-refresh unaffected. Co-Authored-By: Claude Sonnet 4.6 --- app/views/solid_queue_web/dashboard/index.html.erb | 2 +- app/views/solid_queue_web/processes/index.html.erb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/solid_queue_web/dashboard/index.html.erb b/app/views/solid_queue_web/dashboard/index.html.erb index 0dbd110..96d3934 100644 --- a/app/views/solid_queue_web/dashboard/index.html.erb +++ b/app/views/solid_queue_web/dashboard/index.html.erb @@ -1,4 +1,4 @@ -<%= turbo_frame_tag "dashboard", data: { controller: "refresh", refresh_interval_value: 5000 } do %> +<%= turbo_frame_tag "dashboard", target: "_top", data: { controller: "refresh", refresh_interval_value: 5000 } do %>

Dashboard

diff --git a/app/views/solid_queue_web/processes/index.html.erb b/app/views/solid_queue_web/processes/index.html.erb index 6e39f9b..6d5f8e0 100644 --- a/app/views/solid_queue_web/processes/index.html.erb +++ b/app/views/solid_queue_web/processes/index.html.erb @@ -1,4 +1,4 @@ -<%= turbo_frame_tag "processes", data: { controller: "refresh", refresh_interval_value: 10000 } do %> +<%= turbo_frame_tag "processes", target: "_top", data: { controller: "refresh", refresh_interval_value: 10000 } do %>

Processes

From b1c96a34247e842e19e914f6090cc32a168ec6b5 Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Tue, 19 May 2026 12:19:35 -0400 Subject: [PATCH 4/5] chore: add bin/rails to dummy app for local console and server The dummy app had no bin/ directory, making it impossible to run `bin/rails console` or `bin/rails server` from spec/dummy directly. Co-Authored-By: Claude Sonnet 4.6 --- spec/dummy/bin/rails | 4 ++++ 1 file changed, 4 insertions(+) create mode 100755 spec/dummy/bin/rails diff --git a/spec/dummy/bin/rails b/spec/dummy/bin/rails new file mode 100755 index 0000000..fa3c01a --- /dev/null +++ b/spec/dummy/bin/rails @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +APP_PATH = File.expand_path("../config/application", __dir__) +require_relative "../config/boot" +require "rails/commands" \ No newline at end of file From 8f747366c8a08aa72f0caa20dfb0ba30f52d1f3d Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Tue, 19 May 2026 12:21:51 -0400 Subject: [PATCH 5/5] fix: add missing trailing newline to spec/dummy/bin/rails Co-Authored-By: Claude Sonnet 4.6 --- spec/dummy/bin/rails | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/dummy/bin/rails b/spec/dummy/bin/rails index fa3c01a..efc0377 100755 --- a/spec/dummy/bin/rails +++ b/spec/dummy/bin/rails @@ -1,4 +1,4 @@ #!/usr/bin/env ruby APP_PATH = File.expand_path("../config/application", __dir__) require_relative "../config/boot" -require "rails/commands" \ No newline at end of file +require "rails/commands"