Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
```

Expand Down
4 changes: 1 addition & 3 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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._

---

Expand Down
2 changes: 2 additions & 0 deletions app/javascript/solid_stack_web/application.js
Original file line number Diff line number Diff line change
@@ -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)
52 changes: 52 additions & 0 deletions app/javascript/solid_stack_web/refresh_controller.js
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
3 changes: 3 additions & 0 deletions app/views/solid_stack_web/dashboard/index.html.erb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<%= turbo_frame_tag "sqw-dashboard", target: "_top",
data: { controller: "refresh", refresh_interval_value: SolidStackWeb.dashboard_refresh_interval } do %>
<div class="sqw-page-header">
<h1 class="sqw-page-title">Overview</h1>
</div>
Expand Down Expand Up @@ -102,3 +104,4 @@
</div>
</div>
</div>
<% end %>
3 changes: 3 additions & 0 deletions app/views/solid_stack_web/history/index.html.erb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<%= turbo_frame_tag "sqw-history-table",
data: { controller: "refresh", refresh_interval_value: SolidStackWeb.default_refresh_interval } do %>
<div class="sqw-page-header sqw-page-header--split">
<h1 class="sqw-page-title">Job History</h1>
<div class="sqw-header-actions">
Expand Down Expand Up @@ -70,4 +72,5 @@
<div class="sqw-empty">
<p>No finished jobs found.</p>
</div>
<% end %>
<% end %>
6 changes: 4 additions & 2 deletions app/views/solid_stack_web/jobs/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@
<% end %>
</div>

<turbo-frame id="sqw-jobs-filter" data-turbo-action="advance">
<%= turbo_frame_tag "sqw-jobs-filter",
data: { turbo_action: "advance", controller: "refresh",
refresh_interval_value: SolidStackWeb.default_refresh_interval } do %>
<form class="sqw-filters" action="<%= jobs_path %>" method="get">
<%= hidden_field_tag :status, @status %>
<%= hidden_field_tag :period, @period %>
Expand Down Expand Up @@ -151,4 +153,4 @@
<%= render "empty" %>
<% end %>
</div>
</turbo-frame>
<% end %>
3 changes: 3 additions & 0 deletions app/views/solid_stack_web/processes/index.html.erb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<%= turbo_frame_tag "sqw-processes", target: "_top",
data: { controller: "refresh", refresh_interval_value: SolidStackWeb.default_refresh_interval } do %>
<div class="sqw-page-header">
<h1 class="sqw-page-title">Processes</h1>
</div>
Expand Down Expand Up @@ -30,3 +32,4 @@
<p>No active processes.</p>
</div>
<% end %>
<% end %>
1 change: 1 addition & 0 deletions config/importmap.rb
Original file line number Diff line number Diff line change
@@ -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"
16 changes: 15 additions & 1 deletion lib/solid_stack_web.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading