solid_queue_web 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +220 -4
- data/app/assets/stylesheets/solid_queue_web/_04_table.css +28 -0
- data/app/controllers/solid_queue_web/application_controller.rb +30 -0
- data/app/controllers/solid_queue_web/audit_controller.rb +43 -0
- data/app/controllers/solid_queue_web/blocked_jobs_controller.rb +2 -2
- data/app/controllers/solid_queue_web/failed_jobs/arguments_controller.rb +3 -3
- data/app/controllers/solid_queue_web/failed_jobs/selections_controller.rb +6 -4
- data/app/controllers/solid_queue_web/failed_jobs_controller.rb +4 -2
- data/app/controllers/solid_queue_web/jobs/selections_controller.rb +4 -3
- data/app/controllers/solid_queue_web/jobs_controller.rb +7 -3
- data/app/controllers/solid_queue_web/queues/jobs_controller.rb +5 -5
- data/app/controllers/solid_queue_web/queues/pauses_controller.rb +6 -4
- data/app/controllers/solid_queue_web/recurring_tasks/runs_controller.rb +4 -4
- data/app/controllers/solid_queue_web/retry_failed_jobs_controller.rb +6 -5
- data/app/controllers/solid_queue_web/scheduled_jobs_controller.rb +5 -5
- data/app/models/solid_queue_web/audit_event.rb +17 -0
- data/app/views/layouts/solid_queue_web/application.html.erb +20 -16
- data/app/views/solid_queue_web/audit/index.html.erb +78 -0
- data/app/views/solid_queue_web/dashboard/index.html.erb +67 -46
- data/app/views/solid_queue_web/failed_jobs/errors/index.html.erb +7 -7
- data/app/views/solid_queue_web/failed_jobs/index.html.erb +31 -31
- data/app/views/solid_queue_web/history/index.html.erb +14 -14
- data/app/views/solid_queue_web/jobs/index.html.erb +42 -42
- data/app/views/solid_queue_web/jobs/show.html.erb +20 -20
- data/app/views/solid_queue_web/performance/index.html.erb +16 -14
- data/app/views/solid_queue_web/processes/index.html.erb +16 -16
- data/app/views/solid_queue_web/queues/index.html.erb +16 -16
- data/app/views/solid_queue_web/queues/jobs/index.html.erb +21 -21
- data/app/views/solid_queue_web/recurring_tasks/index.html.erb +15 -15
- data/app/views/solid_queue_web/search/index.html.erb +13 -13
- data/config/locales/en.yml +330 -0
- data/config/routes.rb +1 -0
- data/db/migrate/01_create_solid_queue_web_audit_events.rb +16 -0
- data/lib/generators/solid_queue_web/install/migrations_generator.rb +24 -0
- data/lib/generators/solid_queue_web/install/templates/create_solid_queue_web_audit_events.rb.tt +16 -0
- data/lib/solid_queue_web/engine.rb +1 -0
- data/lib/solid_queue_web/version.rb +1 -1
- data/lib/solid_queue_web.rb +18 -1
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cb357c2f74fd24c1a394d54fe2aa53c0fd62a965c3b08e4e359b086243f66e51
|
|
4
|
+
data.tar.gz: c00154198de848c521c53d02124dcc194fdd54c31e67860a6f70554ad5e055da
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cb41d51eafd160f58fd7e54f8b0be6c95e9f488d43d8c6c32720c9a21e108643df38d6957d3aed50cc4f675d456425a3907a7aee37ab39d423a3a9705b050a5e
|
|
7
|
+
data.tar.gz: b272217dd76d4fd7f383815b3604acaae7cfdf9ed9bd8c2132539b3c65036398bd9a99431b73b1804fe84d0b47bbe7f0abcc6ad84fb4a6ff770e2630e97c7989
|
data/README.md
CHANGED
|
@@ -12,10 +12,46 @@ A monitoring and management dashboard for [Solid Queue](https://github.com/rails
|
|
|
12
12
|
|
|
13
13
|

|
|
14
14
|
|
|
15
|
+
## Table of Contents
|
|
16
|
+
|
|
17
|
+
- [The problem](#the-problem)
|
|
18
|
+
- [Why SolidQueueWeb?](#why-solidqueueweb)
|
|
19
|
+
- [Real-world use case](#real-world-use-case)
|
|
20
|
+
- [Features](#features)
|
|
21
|
+
- [Compatibility](#compatibility)
|
|
22
|
+
- [Installation](#installation)
|
|
23
|
+
- [Mounting the engine](#mounting-the-engine)
|
|
24
|
+
- [Configuration](#configuration)
|
|
25
|
+
- [Webhook alerts](#webhook-alerts)
|
|
26
|
+
- [Failure threshold alerts](#failure-threshold-alerts)
|
|
27
|
+
- [Queue depth alerts](#queue-depth-alerts)
|
|
28
|
+
- [Slow job alerts](#slow-job-alerts)
|
|
29
|
+
- [Stale process alerts](#stale-process-alerts)
|
|
30
|
+
- [Admin audit log](#admin-audit-log)
|
|
31
|
+
- [Setup](#setup)
|
|
32
|
+
- [Identity](#identity)
|
|
33
|
+
- [Audited actions](#audited-actions)
|
|
34
|
+
- [Metrics endpoint](#metrics-endpoint)
|
|
35
|
+
- [Read replica support](#read-replica-support)
|
|
36
|
+
- [i18n](#i18n)
|
|
37
|
+
- [Adding a custom locale](#adding-a-custom-locale)
|
|
38
|
+
- [Extensibility](#extensibility)
|
|
39
|
+
- [Custom dashboard cards](#custom-dashboard-cards)
|
|
40
|
+
- [Custom nav links](#custom-nav-links)
|
|
41
|
+
- [Roadmap](#roadmap)
|
|
42
|
+
- [Contributing](#contributing)
|
|
43
|
+
- [License](#license)
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
15
47
|
## The problem
|
|
16
48
|
|
|
17
49
|
Solid Queue ships without a web interface. When jobs fail, queues back up, or workers go silent in production, the only options are `rails console` or raw SQL queries. SolidQueueWeb gives your team a real-time dashboard to inspect, retry, and discard jobs without leaving the browser — and without standing up any additional infrastructure.
|
|
18
50
|
|
|
51
|
+
[↑ Back to top](#table-of-contents)
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
19
55
|
## Why SolidQueueWeb?
|
|
20
56
|
|
|
21
57
|
- Purpose-built for Solid Queue — uses its native models directly, no adapters
|
|
@@ -24,6 +60,10 @@ Solid Queue ships without a web interface. When jobs fail, queues back up, or wo
|
|
|
24
60
|
- Built for Rails 8 — Turbo Frames for in-place updates, Stimulus for dynamic search and auto-refresh, Pagy for efficient pagination
|
|
25
61
|
- Inspired by Sidekiq Web UI and the GoodJob dashboard, adapted for the Solid Queue ecosystem
|
|
26
62
|
|
|
63
|
+
[↑ Back to top](#table-of-contents)
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
27
67
|
## Real-world use case
|
|
28
68
|
|
|
29
69
|
A Rails app processes order confirmations, email notifications, and report generation through Solid Queue. An operations team needs to:
|
|
@@ -35,6 +75,10 @@ A Rails app processes order confirmations, email notifications, and report gener
|
|
|
35
75
|
|
|
36
76
|
SolidQueueWeb surfaces all of this in a browser UI available at any route you choose.
|
|
37
77
|
|
|
78
|
+
[↑ Back to top](#table-of-contents)
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
38
82
|
## Features
|
|
39
83
|
|
|
40
84
|
- **Dashboard** — stat cards showing counts for ready, scheduled, running, blocked, and failed jobs, plus queues, recurring tasks, and processes; "Done (1h)" and "Done (24h)" throughput cards; a "Throughput — Last 12 Hours" bar chart (blue) and a "Queue Depth — Last 12 Hours" bar chart (purple) showing hourly snapshots of active job count; pure CSS, no charting library; auto-refreshes every 5 seconds
|
|
@@ -54,11 +98,19 @@ SolidQueueWeb surfaces all of this in a browser UI available at any route you ch
|
|
|
54
98
|
- **CSV export** — "Export CSV" button on the jobs, failed jobs, and history pages downloads all records matching the current filters; columns are tailored per view
|
|
55
99
|
- **Slow job detection** — when `slow_job_threshold` is configured, claimed jobs running longer than the threshold are flagged with an orange row, a "slow" badge, and a "Running For" duration column on the Running tab; a "Slow Jobs" warning card appears on the dashboard with a link to the Running tab
|
|
56
100
|
- **Job wait time** — the Running tab shows a "Wait Time" column with how long each job waited in the queue from enqueue to pickup; also exported as `wait_time_seconds` in the claimed-status CSV
|
|
101
|
+
- **Admin audit log** — every discard, retry, queue pause, and resume is recorded to a `solid_queue_web_audit_events` table and viewable at `/jobs/audit` with action/actor/queue filters and CSV export; actor identity captured via the optional `current_actor` config block; requires running the install generator to create the table
|
|
57
102
|
- **Webhook alerts** — set `alert_webhook_url` and `alert_failure_threshold` to receive a POST request whenever the failed job count meets or exceeds the threshold; set `alert_queue_thresholds` for per-queue depth alerts; set `alert_slow_job_count_threshold` (requires `slow_job_threshold`) for slow-job count alerts; set `alert_stale_process_threshold` for stale-worker alerts; all fire asynchronously with a configurable cooldown (default 1 h) to prevent repeated alerts
|
|
58
103
|
- **Performance analytics** — per-job-class statistics at `/jobs/performance` showing run count, average, p50, p95, p99, standard deviation, min, and max duration; sorted by p95 descending so the slowest classes surface first; high std dev surfaces inconsistent jobs worth investigating; period filter scopes to 1h / 24h / 7d or all time; each class name links to the filtered History view
|
|
59
104
|
- **Failed job trend chart** — a "Failures — Last 12 Hours" bar chart on the dashboard shows failures per hour over the last 12 hours; bars are red, making failure spikes visible before clicking into the failed jobs list
|
|
60
105
|
- **Error frequency report** — `GET /jobs/failed_jobs/errors` groups all failed jobs by error class and message prefix, shows a count per group, and surfaces a sample backtrace in an expandable row; sorted by count descending so the most common errors appear first; accessible via the "Error Summary" button on the Failed Jobs page
|
|
61
106
|
- **Metrics / health endpoint** — `GET /jobs/metrics.json` returns a machine-readable JSON document with job counts, throughput, per-queue depth and pause state, and process health summary; suitable for Prometheus scraping, uptime monitors, or external dashboards; `slow_jobs` count included when `slow_job_threshold` is configured
|
|
107
|
+
- **i18n** — all UI strings (page titles, table headers, buttons, empty states, flash messages) are backed by `config/locales/en.yml`; locale switching via `?locale=` param or session; add a custom locale by supplying a YAML file in your host app and registering it with `config.available_locales`
|
|
108
|
+
- **Custom dashboard cards** — `config.dashboard_cards` accepts an array of `{ title:, stats:, link: }` hashes rendered after the built-in queue stat cards; `stats:` is a lambda returning a `{ label => value }` hash evaluated at render time; `link:` is an optional header link
|
|
109
|
+
- **Custom nav links** — `config.nav_links` accepts an array of `{ label:, url: }` hashes appended to the main navigation bar after the built-in links
|
|
110
|
+
|
|
111
|
+
[↑ Back to top](#table-of-contents)
|
|
112
|
+
|
|
113
|
+
---
|
|
62
114
|
|
|
63
115
|
## Compatibility
|
|
64
116
|
|
|
@@ -70,6 +122,10 @@ SolidQueueWeb surfaces all of this in a browser UI available at any route you ch
|
|
|
70
122
|
|
|
71
123
|
Tested on Ruby 3.3, 3.4, and 4.0.
|
|
72
124
|
|
|
125
|
+
[↑ Back to top](#table-of-contents)
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
73
129
|
## Installation
|
|
74
130
|
|
|
75
131
|
Add to your application's Gemfile:
|
|
@@ -84,6 +140,10 @@ Then run:
|
|
|
84
140
|
bundle install
|
|
85
141
|
```
|
|
86
142
|
|
|
143
|
+
[↑ Back to top](#table-of-contents)
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
87
147
|
## Mounting the engine
|
|
88
148
|
|
|
89
149
|
Add to your `config/routes.rb`:
|
|
@@ -94,6 +154,10 @@ mount SolidQueueWeb::Engine, at: "/jobs"
|
|
|
94
154
|
|
|
95
155
|
The dashboard will be available at `/jobs`.
|
|
96
156
|
|
|
157
|
+
[↑ Back to top](#table-of-contents)
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
97
161
|
## Configuration
|
|
98
162
|
|
|
99
163
|
All settings are optional — the dashboard works with zero configuration. Create `config/initializers/solid_queue_web.rb` to customize behavior:
|
|
@@ -111,8 +175,12 @@ SolidQueueWeb.configure do |config|
|
|
|
111
175
|
config.alert_slow_job_count_threshold = 5 # fire when slow job count >= this (default: nil = disabled)
|
|
112
176
|
config.alert_stale_process_threshold = 1 # fire when stale process count >= this (default: nil = disabled)
|
|
113
177
|
config.alert_webhook_cooldown = 1800 # seconds between repeated alerts per alert type (default: 3600)
|
|
178
|
+
config.current_actor = -> { current_user&.email } # identity for audit log (default: nil)
|
|
114
179
|
config.connects_to = { reading: :reading, writing: :writing } # read replica (default: nil)
|
|
115
180
|
config.time_zone = "America/New_York" # display timezone for all timestamps (default: nil = UTC)
|
|
181
|
+
config.available_locales = [:en, :fr] # locales available for switching (default: [:en])
|
|
182
|
+
config.nav_links = [{ label: "Admin", url: "/admin" }] # extra nav links (default: [])
|
|
183
|
+
config.dashboard_cards = [{ title: "My App", stats: -> { { "Users" => User.count } } }] # custom stat cards (default: [])
|
|
116
184
|
end
|
|
117
185
|
|
|
118
186
|
SolidQueueWeb.authenticate do
|
|
@@ -124,8 +192,16 @@ end
|
|
|
124
192
|
|
|
125
193
|
No authentication is enforced by default. When the `authenticate` block returns falsy, HTTP Basic auth is used as a fallback.
|
|
126
194
|
|
|
195
|
+
[↑ Back to top](#table-of-contents)
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
127
199
|
## Webhook alerts
|
|
128
200
|
|
|
201
|
+
The engine supports four webhook alert types, each firing asynchronously with a configurable cooldown to prevent repeated alerts.
|
|
202
|
+
|
|
203
|
+
### Failure threshold alerts
|
|
204
|
+
|
|
129
205
|
Set `alert_webhook_url` and `alert_failure_threshold` to receive a POST request whenever the failed job count meets or exceeds the threshold. This is useful for paging an on-call team or triggering a Slack notification via an incoming webhook.
|
|
130
206
|
|
|
131
207
|
```ruby
|
|
@@ -160,7 +236,7 @@ The request body is JSON:
|
|
|
160
236
|
|
|
161
237
|
The webhook fires asynchronously in a background thread so dashboard page loads are never delayed. HTTP errors are logged to `Rails.logger` and swallowed. The cooldown window prevents repeated alerts while the count stays elevated — the clock resets on each app restart.
|
|
162
238
|
|
|
163
|
-
|
|
239
|
+
### Queue depth alerts
|
|
164
240
|
|
|
165
241
|
Set `alert_queue_thresholds` to fire a webhook when any queue's ready job count meets or exceeds a per-queue limit:
|
|
166
242
|
|
|
@@ -185,7 +261,7 @@ The same `alert_webhook_url` endpoint(s) receive the payload, with a distinct ev
|
|
|
185
261
|
|
|
186
262
|
Cooldown is tracked independently per queue, so a persistently deep "critical" queue does not suppress alerts for "default". The shared `alert_webhook_cooldown` setting applies to each queue separately.
|
|
187
263
|
|
|
188
|
-
|
|
264
|
+
### Slow job alerts
|
|
189
265
|
|
|
190
266
|
Set `alert_slow_job_count_threshold` to fire a webhook when the number of currently-running slow jobs meets or exceeds a count. This requires `slow_job_threshold` to also be configured — it defines what "slow" means.
|
|
191
267
|
|
|
@@ -211,7 +287,7 @@ The same `alert_webhook_url` endpoint(s) receive the payload with a distinct eve
|
|
|
211
287
|
|
|
212
288
|
The alert fires on every dashboard page load while the condition persists, subject to the cooldown window.
|
|
213
289
|
|
|
214
|
-
|
|
290
|
+
### Stale process alerts
|
|
215
291
|
|
|
216
292
|
Set `alert_stale_process_threshold` to fire a webhook when the number of stale workers meets or exceeds a count. A process is considered stale when its `last_heartbeat_at` has not been updated within `SolidQueue.process_alive_threshold` (default 5 minutes). A stale worker means jobs in its queues have silently stopped processing.
|
|
217
293
|
|
|
@@ -236,6 +312,54 @@ The same `alert_webhook_url` endpoint(s) receive the payload with a distinct eve
|
|
|
236
312
|
|
|
237
313
|
The alert fires on every dashboard page load while the condition persists, subject to the cooldown window.
|
|
238
314
|
|
|
315
|
+
[↑ Back to top](#table-of-contents)
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## Admin audit log
|
|
320
|
+
|
|
321
|
+
Every discard, retry, queue pause, and resume action is recorded to a `solid_queue_web_audit_events` table and viewable at `/jobs/audit`.
|
|
322
|
+
|
|
323
|
+
### Setup
|
|
324
|
+
|
|
325
|
+
The audit log requires an opt-in migration. Run the install generator to copy it to your application:
|
|
326
|
+
|
|
327
|
+
```bash
|
|
328
|
+
rails generate solid_queue_web:install:migrations
|
|
329
|
+
rails db:migrate
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Identity
|
|
333
|
+
|
|
334
|
+
Set `SolidQueueWeb.current_actor` to a block that returns the current user's identity as a string. The block is evaluated in controller context, so you have access to helpers like `current_user`:
|
|
335
|
+
|
|
336
|
+
```ruby
|
|
337
|
+
SolidQueueWeb.configure do |config|
|
|
338
|
+
config.current_actor = -> { current_user&.email }
|
|
339
|
+
end
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
If not configured, the actor column is left `nil`.
|
|
343
|
+
|
|
344
|
+
### Audited actions
|
|
345
|
+
|
|
346
|
+
| Action | Trigger |
|
|
347
|
+
|---|---|
|
|
348
|
+
| `job_discarded` | Single job discarded from the jobs list |
|
|
349
|
+
| `jobs_discarded` | Bulk or selection discard from the jobs list |
|
|
350
|
+
| `failed_job_retried` | Single failed job retried |
|
|
351
|
+
| `failed_jobs_retried` | Bulk or selection retry of failed jobs |
|
|
352
|
+
| `failed_job_discarded` | Single failed job discarded |
|
|
353
|
+
| `failed_jobs_discarded` | Bulk or selection discard of failed jobs |
|
|
354
|
+
| `queue_paused` | Queue paused |
|
|
355
|
+
| `queue_resumed` | Queue resumed |
|
|
356
|
+
|
|
357
|
+
The audit log page at `/jobs/audit` supports filtering by action, actor, and queue name. All records can be exported as CSV.
|
|
358
|
+
|
|
359
|
+
[↑ Back to top](#table-of-contents)
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
239
363
|
## Metrics endpoint
|
|
240
364
|
|
|
241
365
|
`GET /jobs/metrics.json` returns a machine-readable JSON document suitable for Prometheus scraping, uptime monitors, or external dashboards. No configuration is required — the endpoint is available as soon as the engine is mounted.
|
|
@@ -278,6 +402,10 @@ When `slow_job_threshold` is configured, a `slow_jobs` integer is also included
|
|
|
278
402
|
|
|
279
403
|
The endpoint respects the same authentication and `connects_to` settings as the rest of the dashboard. A process is counted as **stale** when its `last_heartbeat_at` is older than `SolidQueue.process_alive_threshold` (default: 5 minutes).
|
|
280
404
|
|
|
405
|
+
[↑ Back to top](#table-of-contents)
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
281
409
|
## Read replica support
|
|
282
410
|
|
|
283
411
|
Set `connects_to` with both `reading:` and `writing:` keys to enable automatic role switching. GET requests are routed to the reading role; POST/DELETE/PATCH requests use the writing role.
|
|
@@ -297,16 +425,104 @@ config.connects_to = { role: :writing }
|
|
|
297
425
|
|
|
298
426
|
When `connects_to` is `nil` (the default), no connection switching occurs and single-database apps are unaffected.
|
|
299
427
|
|
|
428
|
+
[↑ Back to top](#table-of-contents)
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
## i18n
|
|
433
|
+
|
|
434
|
+
All dashboard UI strings — page titles, table headers, button labels, empty states, and flash messages — are backed by `config/locales/en.yml` in the gem. The engine ships with **English (`en`)** only.
|
|
435
|
+
|
|
436
|
+
The selected locale is stored in the session and applied via `I18n.with_locale`, so it persists across requests without touching the host application's locale. The `?locale=` query param takes precedence over the session value, making it easy to deep-link to a specific language.
|
|
437
|
+
|
|
438
|
+
```ruby
|
|
439
|
+
SolidQueueWeb.configure do |config|
|
|
440
|
+
# Locales available for switching (default: [:en]).
|
|
441
|
+
config.available_locales = [:en, :fr]
|
|
442
|
+
end
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### Adding a custom locale
|
|
446
|
+
|
|
447
|
+
1. Create a locale file in your host application under `config/locales/`, e.g. `config/locales/solid_queue_web.fr.yml`.
|
|
448
|
+
2. Nest all keys under `fr > solid_queue_web:` — use [`config/locales/en.yml`](config/locales/en.yml) in the gem as a reference for the full key list.
|
|
449
|
+
3. Register the locale:
|
|
450
|
+
|
|
451
|
+
```ruby
|
|
452
|
+
config.available_locales = [:en, :fr]
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
Rails will pick up the file automatically via its standard `config.i18n.load_path`; no additional configuration is needed.
|
|
456
|
+
|
|
457
|
+
[↑ Back to top](#table-of-contents)
|
|
458
|
+
|
|
459
|
+
---
|
|
460
|
+
|
|
461
|
+
## Extensibility
|
|
462
|
+
|
|
463
|
+
### Custom dashboard cards
|
|
464
|
+
|
|
465
|
+
`config.dashboard_cards` adds custom stat cards to the dashboard after the built-in queue cards. Each card accepts three keys:
|
|
466
|
+
|
|
467
|
+
| Key | Type | Description |
|
|
468
|
+
|-----|------|-------------|
|
|
469
|
+
| `title` | String | Card heading (required) |
|
|
470
|
+
| `link` | `{ label:, url: }` | Optional header link rendered top-right |
|
|
471
|
+
| `stats` | Lambda | Optional — called at render time; must return a `{ label => value }` hash |
|
|
472
|
+
|
|
473
|
+
```ruby
|
|
474
|
+
SolidQueueWeb.configure do |config|
|
|
475
|
+
config.dashboard_cards = [
|
|
476
|
+
{
|
|
477
|
+
title: "My App",
|
|
478
|
+
link: { label: "View Admin", url: "/admin" },
|
|
479
|
+
stats: -> { { "Users" => User.count, "Premium" => User.premium.count } }
|
|
480
|
+
}
|
|
481
|
+
]
|
|
482
|
+
end
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
The `stats` lambda runs on every dashboard render, so keep it fast. Defaults to `[]` — no custom cards appear when unconfigured.
|
|
486
|
+
|
|
487
|
+
### Custom nav links
|
|
488
|
+
|
|
489
|
+
`config.nav_links` appends extra links to the main navigation bar after the built-in links. Use it to link back to your host application's admin pages or related tools.
|
|
490
|
+
|
|
491
|
+
```ruby
|
|
492
|
+
SolidQueueWeb.configure do |config|
|
|
493
|
+
config.nav_links = [
|
|
494
|
+
{ label: "Back to App", url: "/" },
|
|
495
|
+
{ label: "Admin", url: "/admin" }
|
|
496
|
+
]
|
|
497
|
+
end
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
Defaults to `[]` — no extra links appear when unconfigured.
|
|
501
|
+
|
|
502
|
+
[↑ Back to top](#table-of-contents)
|
|
503
|
+
|
|
504
|
+
---
|
|
505
|
+
|
|
300
506
|
## Roadmap
|
|
301
507
|
|
|
302
508
|
See [ROADMAP.md](ROADMAP.md) for the full post-1.0 feature plan, organized by release milestone.
|
|
303
509
|
|
|
304
510
|
Pull requests for any of these are welcome. See [Contributing](#contributing) below.
|
|
305
511
|
|
|
512
|
+
[↑ Back to top](#table-of-contents)
|
|
513
|
+
|
|
514
|
+
---
|
|
515
|
+
|
|
306
516
|
## Contributing
|
|
307
517
|
|
|
308
518
|
Bug reports and pull requests are welcome on [GitHub](https://github.com/eclectic-coding/solid_queue_web).
|
|
309
519
|
|
|
520
|
+
[↑ Back to top](#table-of-contents)
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
310
524
|
## License
|
|
311
525
|
|
|
312
|
-
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
526
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
527
|
+
|
|
528
|
+
[↑ Back to top](#table-of-contents)
|
|
@@ -19,6 +19,34 @@
|
|
|
19
19
|
font-weight: 600;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
.sqd-card__body {
|
|
23
|
+
padding: 0.75rem 1rem;
|
|
24
|
+
display: flex;
|
|
25
|
+
flex-direction: column;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.sqd-custom-stat {
|
|
29
|
+
display: flex;
|
|
30
|
+
justify-content: space-between;
|
|
31
|
+
align-items: baseline;
|
|
32
|
+
font-size: 13px;
|
|
33
|
+
padding: 0.375rem 0;
|
|
34
|
+
border-bottom: 1px solid var(--border);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.sqd-custom-stat:last-child {
|
|
38
|
+
border-bottom: none;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.sqd-custom-stat__label {
|
|
42
|
+
color: var(--muted);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.sqd-custom-stat__value {
|
|
46
|
+
font-weight: 600;
|
|
47
|
+
font-variant-numeric: tabular-nums;
|
|
48
|
+
}
|
|
49
|
+
|
|
22
50
|
table {
|
|
23
51
|
width: 100%;
|
|
24
52
|
border-collapse: collapse;
|
|
@@ -8,10 +8,20 @@ module SolidQueueWeb
|
|
|
8
8
|
STAGGER_INTERVALS = { "5s" => 5.seconds, "10s" => 10.seconds, "30s" => 30.seconds, "1m" => 1.minute }.freeze
|
|
9
9
|
|
|
10
10
|
before_action :authenticate!
|
|
11
|
+
around_action :with_locale
|
|
11
12
|
around_action :with_database_connection
|
|
12
13
|
|
|
13
14
|
private
|
|
14
15
|
|
|
16
|
+
def with_locale
|
|
17
|
+
available = SolidQueueWeb.available_locales.map(&:to_s)
|
|
18
|
+
locale = params[:locale].presence_in(available) ||
|
|
19
|
+
session[:solid_queue_web_locale].presence_in(available) ||
|
|
20
|
+
I18n.default_locale.to_s
|
|
21
|
+
session[:solid_queue_web_locale] = locale
|
|
22
|
+
I18n.with_locale(locale) { yield }
|
|
23
|
+
end
|
|
24
|
+
|
|
15
25
|
def with_database_connection
|
|
16
26
|
config = SolidQueueWeb.connects_to
|
|
17
27
|
return yield unless config
|
|
@@ -37,5 +47,25 @@ module SolidQueueWeb
|
|
|
37
47
|
def request_basic_auth
|
|
38
48
|
request_http_basic_authentication("Solid Queue Dashboard")
|
|
39
49
|
end
|
|
50
|
+
|
|
51
|
+
def record_audit(action, job_class: nil, queue_name: nil, item_count: 1)
|
|
52
|
+
AuditEvent.create!(
|
|
53
|
+
action: action,
|
|
54
|
+
actor: resolve_current_actor,
|
|
55
|
+
job_class: job_class,
|
|
56
|
+
queue_name: queue_name,
|
|
57
|
+
item_count: item_count
|
|
58
|
+
)
|
|
59
|
+
rescue => e
|
|
60
|
+
Rails.logger.error("[SolidQueueWeb] Audit log failed: #{e.message}")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def resolve_current_actor
|
|
64
|
+
block = SolidQueueWeb.current_actor
|
|
65
|
+
instance_exec(&block) if block
|
|
66
|
+
rescue => e
|
|
67
|
+
Rails.logger.error("[SolidQueueWeb] current_actor block failed: #{e.message}")
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
40
70
|
end
|
|
41
71
|
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module SolidQueueWeb
|
|
2
|
+
class AuditController < ApplicationController
|
|
3
|
+
before_action :set_filters
|
|
4
|
+
|
|
5
|
+
def index
|
|
6
|
+
scope = audit_scope
|
|
7
|
+
respond_to do |format|
|
|
8
|
+
format.html { @pagy, @audit_events = pagy(scope) }
|
|
9
|
+
format.csv do
|
|
10
|
+
send_data audit_csv(scope),
|
|
11
|
+
filename: "audit-log-#{Date.today}.csv",
|
|
12
|
+
type: "text/csv", disposition: "attachment"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def set_filters
|
|
20
|
+
@action_filter = params[:action_filter].presence_in(AuditEvent::ACTIONS)
|
|
21
|
+
@actor_filter = params[:actor].presence
|
|
22
|
+
@queue_filter = params[:queue].presence
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def audit_scope
|
|
26
|
+
scope = AuditEvent.recent
|
|
27
|
+
scope = scope.where(action: @action_filter) if @action_filter
|
|
28
|
+
scope = scope.where(actor: @actor_filter) if @actor_filter
|
|
29
|
+
scope = scope.where(queue_name: @queue_filter) if @queue_filter
|
|
30
|
+
scope
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def audit_csv(scope)
|
|
34
|
+
CSV.generate(headers: true) do |csv|
|
|
35
|
+
csv << %w[id action actor job_class queue_name item_count created_at]
|
|
36
|
+
scope.each do |event|
|
|
37
|
+
csv << [event.id, event.action, event.actor, event.job_class,
|
|
38
|
+
event.queue_name, event.item_count, event.created_at.iso8601]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -3,9 +3,9 @@ module SolidQueueWeb
|
|
|
3
3
|
def destroy
|
|
4
4
|
jobs = SolidQueue::BlockedExecution.includes(:job).map(&:job)
|
|
5
5
|
SolidQueue::BlockedExecution.discard_all_from_jobs(jobs)
|
|
6
|
-
redirect_to root_path, notice: "
|
|
6
|
+
redirect_to root_path, notice: t("solid_queue_web.flash.blocked_jobs_discarded", count: jobs.size)
|
|
7
7
|
rescue => e
|
|
8
|
-
redirect_to root_path, alert: "
|
|
8
|
+
redirect_to root_path, alert: t("solid_queue_web.flash.cannot_discard_blocked_jobs", error: e.message)
|
|
9
9
|
end
|
|
10
10
|
end
|
|
11
11
|
end
|
|
@@ -5,11 +5,11 @@ module SolidQueueWeb
|
|
|
5
5
|
new_arguments = JSON.parse(params[:arguments])
|
|
6
6
|
execution.job.update!(arguments: new_arguments)
|
|
7
7
|
execution.retry
|
|
8
|
-
redirect_to failed_jobs_path, notice: "
|
|
8
|
+
redirect_to failed_jobs_path, notice: t("solid_queue_web.flash.arguments_updated")
|
|
9
9
|
rescue JSON::ParserError
|
|
10
|
-
redirect_to job_path(execution.job), alert: "
|
|
10
|
+
redirect_to job_path(execution.job), alert: t("solid_queue_web.flash.invalid_json")
|
|
11
11
|
rescue => e
|
|
12
|
-
redirect_to failed_jobs_path, alert: "
|
|
12
|
+
redirect_to failed_jobs_path, alert: t("solid_queue_web.flash.cannot_update_job", error: e.message)
|
|
13
13
|
end
|
|
14
14
|
end
|
|
15
15
|
end
|
|
@@ -6,10 +6,11 @@ module SolidQueueWeb
|
|
|
6
6
|
executions = SolidQueue::FailedExecution.where(id: ids)
|
|
7
7
|
jobs = executions.includes(:job).map(&:job)
|
|
8
8
|
SolidQueue::FailedExecution.retry_all(jobs)
|
|
9
|
+
record_audit("failed_jobs_retried", item_count: jobs.size)
|
|
9
10
|
redirect_to failed_jobs_path,
|
|
10
|
-
notice: "
|
|
11
|
+
notice: t("solid_queue_web.flash.jobs_retried", count: jobs.size)
|
|
11
12
|
rescue => e
|
|
12
|
-
redirect_to failed_jobs_path, alert: "
|
|
13
|
+
redirect_to failed_jobs_path, alert: t("solid_queue_web.flash.cannot_retry_jobs", error: e.message)
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
def destroy
|
|
@@ -17,10 +18,11 @@ module SolidQueueWeb
|
|
|
17
18
|
executions = SolidQueue::FailedExecution.where(id: ids)
|
|
18
19
|
jobs = executions.includes(:job).map(&:job)
|
|
19
20
|
SolidQueue::FailedExecution.discard_all_from_jobs(jobs)
|
|
21
|
+
record_audit("failed_jobs_discarded", item_count: jobs.size)
|
|
20
22
|
redirect_to failed_jobs_path,
|
|
21
|
-
notice: "
|
|
23
|
+
notice: t("solid_queue_web.flash.jobs_discarded", count: jobs.size)
|
|
22
24
|
rescue => e
|
|
23
|
-
redirect_to failed_jobs_path, alert: "
|
|
25
|
+
redirect_to failed_jobs_path, alert: t("solid_queue_web.flash.cannot_discard_jobs", error: e.message)
|
|
24
26
|
end
|
|
25
27
|
end
|
|
26
28
|
end
|
|
@@ -17,7 +17,7 @@ module SolidQueueWeb
|
|
|
17
17
|
executions = params[:id] ? [SolidQueue::FailedExecution.find(params[:id])] : filtered_scope.to_a
|
|
18
18
|
perform_discard(executions)
|
|
19
19
|
rescue => e
|
|
20
|
-
redirect_to failed_jobs_path, alert: "
|
|
20
|
+
redirect_to failed_jobs_path, alert: t("solid_queue_web.flash.cannot_discard_job", error: e.message)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
private
|
|
@@ -37,9 +37,11 @@ module SolidQueueWeb
|
|
|
37
37
|
|
|
38
38
|
def perform_discard(executions)
|
|
39
39
|
jobs = executions.map(&:job)
|
|
40
|
+
action = params[:id] ? "failed_job_discarded" : "failed_jobs_discarded"
|
|
40
41
|
SolidQueue::FailedExecution.discard_all_from_jobs(jobs)
|
|
42
|
+
record_audit(action, job_class: jobs.first&.class_name, queue_name: jobs.first&.queue_name, item_count: jobs.size)
|
|
41
43
|
redirect_to failed_jobs_path(queue: @queue, q: @search, period: @period),
|
|
42
|
-
notice: "
|
|
44
|
+
notice: t("solid_queue_web.flash.jobs_discarded", count: jobs.size)
|
|
43
45
|
end
|
|
44
46
|
|
|
45
47
|
def sortable_columns
|
|
@@ -4,17 +4,18 @@ module SolidQueueWeb
|
|
|
4
4
|
def destroy
|
|
5
5
|
status = params[:status]
|
|
6
6
|
period = params[:period].presence_in(PERIOD_DURATIONS.keys)
|
|
7
|
-
raise ArgumentError, "
|
|
7
|
+
raise ArgumentError, t("solid_queue_web.flash.cannot_discard", status: status) unless Job::DISCARDABLE.include?(status)
|
|
8
8
|
model = Job::EXECUTION_MODELS[status]
|
|
9
9
|
ids = Array(params[:ids]).map(&:to_i).reject(&:zero?)
|
|
10
10
|
jobs = model.where(id: ids).includes(:job).map(&:job)
|
|
11
11
|
model.discard_all_from_jobs(jobs)
|
|
12
|
+
record_audit("jobs_discarded", item_count: jobs.size)
|
|
12
13
|
redirect_to jobs_path(status: status, period: period),
|
|
13
|
-
notice: "
|
|
14
|
+
notice: t("solid_queue_web.flash.jobs_discarded", count: jobs.size)
|
|
14
15
|
rescue ArgumentError => e
|
|
15
16
|
redirect_to jobs_path(status: status), alert: e.message
|
|
16
17
|
rescue => e
|
|
17
|
-
redirect_to jobs_path(status: status), alert: "
|
|
18
|
+
redirect_to jobs_path(status: status), alert: t("solid_queue_web.flash.cannot_discard_jobs", error: e.message)
|
|
18
19
|
end
|
|
19
20
|
end
|
|
20
21
|
end
|
|
@@ -30,21 +30,25 @@ module SolidQueueWeb
|
|
|
30
30
|
model = Job.execution_model_for!(@status)
|
|
31
31
|
if params[:id]
|
|
32
32
|
@execution = model.find(params[:id])
|
|
33
|
+
discarded_job = @execution.job
|
|
33
34
|
@execution.discard
|
|
35
|
+
record_audit("job_discarded", job_class: discarded_job&.class_name, queue_name: discarded_job&.queue_name)
|
|
34
36
|
@remaining_count = filtered_scope(model).count
|
|
35
37
|
respond_to do |format|
|
|
36
38
|
format.turbo_stream
|
|
37
|
-
format.html { redirect_to jobs_return_path, notice: "
|
|
39
|
+
format.html { redirect_to jobs_return_path, notice: t("solid_queue_web.flash.job_discarded") }
|
|
38
40
|
end
|
|
39
41
|
else
|
|
40
42
|
jobs = filtered_scope(model).map(&:job)
|
|
41
43
|
model.discard_all_from_jobs(jobs)
|
|
42
|
-
|
|
44
|
+
record_audit("jobs_discarded", item_count: jobs.size)
|
|
45
|
+
redirect_to jobs_return_path, notice: t("solid_queue_web.flash.jobs_discarded", count: jobs.size)
|
|
43
46
|
end
|
|
44
47
|
rescue ArgumentError => e
|
|
45
48
|
redirect_to jobs_return_path, alert: e.message
|
|
46
49
|
rescue => e
|
|
47
|
-
|
|
50
|
+
msg = params[:id] ? t("solid_queue_web.flash.cannot_discard_job", error: e.message) : t("solid_queue_web.flash.cannot_discard_jobs", error: e.message)
|
|
51
|
+
redirect_to jobs_return_path, alert: msg
|
|
48
52
|
end
|
|
49
53
|
|
|
50
54
|
private
|
|
@@ -21,19 +21,19 @@ module SolidQueueWeb
|
|
|
21
21
|
@remaining_count = filtered_scope(model).count
|
|
22
22
|
respond_to do |format|
|
|
23
23
|
format.turbo_stream
|
|
24
|
-
format.html { redirect_to queue_jobs_path(queue_name: @queue, status: @status), notice: "
|
|
24
|
+
format.html { redirect_to queue_jobs_path(queue_name: @queue, status: @status), notice: t("solid_queue_web.flash.job_discarded") }
|
|
25
25
|
end
|
|
26
26
|
else
|
|
27
27
|
jobs = filtered_scope(model).map(&:job)
|
|
28
28
|
model.discard_all_from_jobs(jobs)
|
|
29
29
|
redirect_to queue_jobs_path(queue_name: @queue, status: @status),
|
|
30
|
-
notice: "
|
|
30
|
+
notice: t("solid_queue_web.flash.jobs_discarded", count: jobs.size)
|
|
31
31
|
end
|
|
32
32
|
rescue ArgumentError => e
|
|
33
33
|
redirect_to queue_jobs_path(queue_name: @queue, status: @status), alert: e.message
|
|
34
34
|
rescue => e
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
msg = params[:id] ? t("solid_queue_web.flash.cannot_discard_job", error: e.message) : t("solid_queue_web.flash.cannot_discard_jobs", error: e.message)
|
|
36
|
+
redirect_to queue_jobs_path(queue_name: @queue, status: @status), alert: msg
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
private
|
|
@@ -51,7 +51,7 @@ module SolidQueueWeb
|
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
def execution_model_for!(status)
|
|
54
|
-
raise ArgumentError, "
|
|
54
|
+
raise ArgumentError, t("solid_queue_web.flash.cannot_discard_from_queue", status: status) unless Job::DISCARDABLE.include?(status)
|
|
55
55
|
Job::EXECUTION_MODELS[status]
|
|
56
56
|
end
|
|
57
57
|
end
|