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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +220 -4
  3. data/app/assets/stylesheets/solid_queue_web/_04_table.css +28 -0
  4. data/app/controllers/solid_queue_web/application_controller.rb +30 -0
  5. data/app/controllers/solid_queue_web/audit_controller.rb +43 -0
  6. data/app/controllers/solid_queue_web/blocked_jobs_controller.rb +2 -2
  7. data/app/controllers/solid_queue_web/failed_jobs/arguments_controller.rb +3 -3
  8. data/app/controllers/solid_queue_web/failed_jobs/selections_controller.rb +6 -4
  9. data/app/controllers/solid_queue_web/failed_jobs_controller.rb +4 -2
  10. data/app/controllers/solid_queue_web/jobs/selections_controller.rb +4 -3
  11. data/app/controllers/solid_queue_web/jobs_controller.rb +7 -3
  12. data/app/controllers/solid_queue_web/queues/jobs_controller.rb +5 -5
  13. data/app/controllers/solid_queue_web/queues/pauses_controller.rb +6 -4
  14. data/app/controllers/solid_queue_web/recurring_tasks/runs_controller.rb +4 -4
  15. data/app/controllers/solid_queue_web/retry_failed_jobs_controller.rb +6 -5
  16. data/app/controllers/solid_queue_web/scheduled_jobs_controller.rb +5 -5
  17. data/app/models/solid_queue_web/audit_event.rb +17 -0
  18. data/app/views/layouts/solid_queue_web/application.html.erb +20 -16
  19. data/app/views/solid_queue_web/audit/index.html.erb +78 -0
  20. data/app/views/solid_queue_web/dashboard/index.html.erb +67 -46
  21. data/app/views/solid_queue_web/failed_jobs/errors/index.html.erb +7 -7
  22. data/app/views/solid_queue_web/failed_jobs/index.html.erb +31 -31
  23. data/app/views/solid_queue_web/history/index.html.erb +14 -14
  24. data/app/views/solid_queue_web/jobs/index.html.erb +42 -42
  25. data/app/views/solid_queue_web/jobs/show.html.erb +20 -20
  26. data/app/views/solid_queue_web/performance/index.html.erb +16 -14
  27. data/app/views/solid_queue_web/processes/index.html.erb +16 -16
  28. data/app/views/solid_queue_web/queues/index.html.erb +16 -16
  29. data/app/views/solid_queue_web/queues/jobs/index.html.erb +21 -21
  30. data/app/views/solid_queue_web/recurring_tasks/index.html.erb +15 -15
  31. data/app/views/solid_queue_web/search/index.html.erb +13 -13
  32. data/config/locales/en.yml +330 -0
  33. data/config/routes.rb +1 -0
  34. data/db/migrate/01_create_solid_queue_web_audit_events.rb +16 -0
  35. data/lib/generators/solid_queue_web/install/migrations_generator.rb +24 -0
  36. data/lib/generators/solid_queue_web/install/templates/create_solid_queue_web_audit_events.rb.tt +16 -0
  37. data/lib/solid_queue_web/engine.rb +1 -0
  38. data/lib/solid_queue_web/version.rb +1 -1
  39. data/lib/solid_queue_web.rb +18 -1
  40. metadata +8 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c0d9757682265e639856df718934858d99390e8ca06ff14cb575b117db105932
4
- data.tar.gz: a6c0ca7e495f76467cde1aabb0f54674c1439a5489913b4fe9d95837df3acd03
3
+ metadata.gz: cb357c2f74fd24c1a394d54fe2aa53c0fd62a965c3b08e4e359b086243f66e51
4
+ data.tar.gz: c00154198de848c521c53d02124dcc194fdd54c31e67860a6f70554ad5e055da
5
5
  SHA512:
6
- metadata.gz: 41ff634f54cb990c14c57870835f62e091a36130c26e0ecdca0a9fbe07f6ed70b9f128a41d22b59177f03cd21497c3bda9fc17e3044e3fcb843a6031ad9ece3d
7
- data.tar.gz: 15ed48fde349d58b1f2778cd9b0cf5a8e502ccd74f610a23368665f938fe36c3d66041400eeb96b3aa196d03a5dd56c7becfbd6b743b055cba60ee01e4cb6c3b
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
  ![SolidQueueWeb dashboard](docs/solid-queue-web.png)
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
- ## Queue depth alerts
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
- ## Slow job alerts
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
- ## Stale process alerts
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: "#{jobs.size} blocked #{"job".pluralize(jobs.size)} discarded."
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: "Could not discard blocked jobs: #{e.message}"
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: "Job arguments updated and queued for retry."
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: "Invalid JSON: could not parse arguments."
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: "Could not update job: #{e.message}"
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: "#{jobs.size} #{"job".pluralize(jobs.size)} queued for retry."
11
+ notice: t("solid_queue_web.flash.jobs_retried", count: jobs.size)
11
12
  rescue => e
12
- redirect_to failed_jobs_path, alert: "Could not retry jobs: #{e.message}"
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: "#{jobs.size} #{"job".pluralize(jobs.size)} discarded."
23
+ notice: t("solid_queue_web.flash.jobs_discarded", count: jobs.size)
22
24
  rescue => e
23
- redirect_to failed_jobs_path, alert: "Could not discard jobs: #{e.message}"
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: "Could not discard job: #{e.message}"
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: "#{jobs.size} #{"job".pluralize(jobs.size)} discarded."
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, "Cannot discard #{status} jobs." unless Job::DISCARDABLE.include?(status)
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: "#{jobs.size} #{"job".pluralize(jobs.size)} discarded."
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: "Could not discard jobs: #{e.message}"
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: "Job discarded." }
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
- redirect_to jobs_return_path, notice: "#{jobs.size} #{"job".pluralize(jobs.size)} discarded."
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
- redirect_to jobs_return_path, alert: "Could not discard #{params[:id] ? "job" : "jobs"}: #{e.message}"
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: "Job discarded." }
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: "#{jobs.size} #{"job".pluralize(jobs.size)} discarded."
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
- redirect_to queue_jobs_path(queue_name: @queue, status: @status),
36
- alert: "Could not discard #{params[:id] ? "job" : "jobs"}: #{e.message}"
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, "Cannot discard #{status} jobs from this page." unless Job::DISCARDABLE.include?(status)
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