solid_queue_web 1.5.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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +179 -5
  3. data/app/assets/stylesheets/solid_queue_web/_04_table.css +28 -0
  4. data/app/controllers/solid_queue_web/application_controller.rb +10 -0
  5. data/app/controllers/solid_queue_web/blocked_jobs_controller.rb +2 -2
  6. data/app/controllers/solid_queue_web/failed_jobs/arguments_controller.rb +3 -3
  7. data/app/controllers/solid_queue_web/failed_jobs/selections_controller.rb +4 -4
  8. data/app/controllers/solid_queue_web/failed_jobs_controller.rb +2 -2
  9. data/app/controllers/solid_queue_web/jobs/selections_controller.rb +3 -3
  10. data/app/controllers/solid_queue_web/jobs_controller.rb +4 -3
  11. data/app/controllers/solid_queue_web/queues/jobs_controller.rb +5 -5
  12. data/app/controllers/solid_queue_web/queues/pauses_controller.rb +4 -4
  13. data/app/controllers/solid_queue_web/recurring_tasks/runs_controller.rb +4 -4
  14. data/app/controllers/solid_queue_web/retry_failed_jobs_controller.rb +4 -5
  15. data/app/controllers/solid_queue_web/scheduled_jobs_controller.rb +5 -5
  16. data/app/views/layouts/solid_queue_web/application.html.erb +20 -17
  17. data/app/views/solid_queue_web/audit/index.html.erb +15 -15
  18. data/app/views/solid_queue_web/dashboard/index.html.erb +67 -46
  19. data/app/views/solid_queue_web/failed_jobs/errors/index.html.erb +7 -7
  20. data/app/views/solid_queue_web/failed_jobs/index.html.erb +31 -31
  21. data/app/views/solid_queue_web/history/index.html.erb +14 -14
  22. data/app/views/solid_queue_web/jobs/index.html.erb +42 -42
  23. data/app/views/solid_queue_web/jobs/show.html.erb +20 -20
  24. data/app/views/solid_queue_web/performance/index.html.erb +16 -14
  25. data/app/views/solid_queue_web/processes/index.html.erb +16 -16
  26. data/app/views/solid_queue_web/queues/index.html.erb +16 -16
  27. data/app/views/solid_queue_web/queues/jobs/index.html.erb +21 -21
  28. data/app/views/solid_queue_web/recurring_tasks/index.html.erb +15 -15
  29. data/app/views/solid_queue_web/search/index.html.erb +13 -13
  30. data/config/locales/en.yml +330 -0
  31. data/lib/solid_queue_web/engine.rb +1 -0
  32. data/lib/solid_queue_web/version.rb +1 -1
  33. data/lib/solid_queue_web.rb +13 -1
  34. metadata +2 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0aea2023ccc5983daeb0781f0b56105492179d638822d9d6c4a853e7296f3258
4
- data.tar.gz: 0b430cccb3b56335a562451f0d598390a4a3f81d2abb1ad5122ce3233fa5663c
3
+ metadata.gz: cb357c2f74fd24c1a394d54fe2aa53c0fd62a965c3b08e4e359b086243f66e51
4
+ data.tar.gz: c00154198de848c521c53d02124dcc194fdd54c31e67860a6f70554ad5e055da
5
5
  SHA512:
6
- metadata.gz: f654b581b1bddc2c2559d39fa09cfc83c0bc664268ba085770179476fbed468e672341025f1d34f5e64c8cb9fc9127914d0d1601a073cc7fb19df03ed890c369
7
- data.tar.gz: 5c4794c4ab5e1f5bd5e33aaa4b6630f5de7c4289c55b6506d2f21493c9b63e4e24680b03c2cd1554812426b3b285c5cf640adc079f4b5a9fe93916bcbbd9b3c3
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
@@ -60,6 +104,13 @@ SolidQueueWeb surfaces all of this in a browser UI available at any route you ch
60
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
61
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
62
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
+ ---
63
114
 
64
115
  ## Compatibility
65
116
 
@@ -71,6 +122,10 @@ SolidQueueWeb surfaces all of this in a browser UI available at any route you ch
71
122
 
72
123
  Tested on Ruby 3.3, 3.4, and 4.0.
73
124
 
125
+ [↑ Back to top](#table-of-contents)
126
+
127
+ ---
128
+
74
129
  ## Installation
75
130
 
76
131
  Add to your application's Gemfile:
@@ -85,6 +140,10 @@ Then run:
85
140
  bundle install
86
141
  ```
87
142
 
143
+ [↑ Back to top](#table-of-contents)
144
+
145
+ ---
146
+
88
147
  ## Mounting the engine
89
148
 
90
149
  Add to your `config/routes.rb`:
@@ -95,6 +154,10 @@ mount SolidQueueWeb::Engine, at: "/jobs"
95
154
 
96
155
  The dashboard will be available at `/jobs`.
97
156
 
157
+ [↑ Back to top](#table-of-contents)
158
+
159
+ ---
160
+
98
161
  ## Configuration
99
162
 
100
163
  All settings are optional — the dashboard works with zero configuration. Create `config/initializers/solid_queue_web.rb` to customize behavior:
@@ -115,6 +178,9 @@ SolidQueueWeb.configure do |config|
115
178
  config.current_actor = -> { current_user&.email } # identity for audit log (default: nil)
116
179
  config.connects_to = { reading: :reading, writing: :writing } # read replica (default: nil)
117
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: [])
118
184
  end
119
185
 
120
186
  SolidQueueWeb.authenticate do
@@ -126,8 +192,16 @@ end
126
192
 
127
193
  No authentication is enforced by default. When the `authenticate` block returns falsy, HTTP Basic auth is used as a fallback.
128
194
 
195
+ [↑ Back to top](#table-of-contents)
196
+
197
+ ---
198
+
129
199
  ## Webhook alerts
130
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
+
131
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.
132
206
 
133
207
  ```ruby
@@ -162,7 +236,7 @@ The request body is JSON:
162
236
 
163
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.
164
238
 
165
- ## Queue depth alerts
239
+ ### Queue depth alerts
166
240
 
167
241
  Set `alert_queue_thresholds` to fire a webhook when any queue's ready job count meets or exceeds a per-queue limit:
168
242
 
@@ -187,7 +261,7 @@ The same `alert_webhook_url` endpoint(s) receive the payload, with a distinct ev
187
261
 
188
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.
189
263
 
190
- ## Slow job alerts
264
+ ### Slow job alerts
191
265
 
192
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.
193
267
 
@@ -213,7 +287,7 @@ The same `alert_webhook_url` endpoint(s) receive the payload with a distinct eve
213
287
 
214
288
  The alert fires on every dashboard page load while the condition persists, subject to the cooldown window.
215
289
 
216
- ## Stale process alerts
290
+ ### Stale process alerts
217
291
 
218
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.
219
293
 
@@ -238,11 +312,15 @@ The same `alert_webhook_url` endpoint(s) receive the payload with a distinct eve
238
312
 
239
313
  The alert fires on every dashboard page load while the condition persists, subject to the cooldown window.
240
314
 
315
+ [↑ Back to top](#table-of-contents)
316
+
317
+ ---
318
+
241
319
  ## Admin audit log
242
320
 
243
321
  Every discard, retry, queue pause, and resume action is recorded to a `solid_queue_web_audit_events` table and viewable at `/jobs/audit`.
244
322
 
245
- ### Installation
323
+ ### Setup
246
324
 
247
325
  The audit log requires an opt-in migration. Run the install generator to copy it to your application:
248
326
 
@@ -278,6 +356,10 @@ If not configured, the actor column is left `nil`.
278
356
 
279
357
  The audit log page at `/jobs/audit` supports filtering by action, actor, and queue name. All records can be exported as CSV.
280
358
 
359
+ [↑ Back to top](#table-of-contents)
360
+
361
+ ---
362
+
281
363
  ## Metrics endpoint
282
364
 
283
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.
@@ -320,6 +402,10 @@ When `slow_job_threshold` is configured, a `slow_jobs` integer is also included
320
402
 
321
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).
322
404
 
405
+ [↑ Back to top](#table-of-contents)
406
+
407
+ ---
408
+
323
409
  ## Read replica support
324
410
 
325
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.
@@ -339,16 +425,104 @@ config.connects_to = { role: :writing }
339
425
 
340
426
  When `connects_to` is `nil` (the default), no connection switching occurs and single-database apps are unaffected.
341
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
+
342
506
  ## Roadmap
343
507
 
344
508
  See [ROADMAP.md](ROADMAP.md) for the full post-1.0 feature plan, organized by release milestone.
345
509
 
346
510
  Pull requests for any of these are welcome. See [Contributing](#contributing) below.
347
511
 
512
+ [↑ Back to top](#table-of-contents)
513
+
514
+ ---
515
+
348
516
  ## Contributing
349
517
 
350
518
  Bug reports and pull requests are welcome on [GitHub](https://github.com/eclectic-coding/solid_queue_web).
351
519
 
520
+ [↑ Back to top](#table-of-contents)
521
+
522
+ ---
523
+
352
524
  ## License
353
525
 
354
- 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
@@ -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
@@ -8,9 +8,9 @@ module SolidQueueWeb
8
8
  SolidQueue::FailedExecution.retry_all(jobs)
9
9
  record_audit("failed_jobs_retried", item_count: jobs.size)
10
10
  redirect_to failed_jobs_path,
11
- notice: "#{jobs.size} #{"job".pluralize(jobs.size)} queued for retry."
11
+ notice: t("solid_queue_web.flash.jobs_retried", count: jobs.size)
12
12
  rescue => e
13
- 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)
14
14
  end
15
15
 
16
16
  def destroy
@@ -20,9 +20,9 @@ module SolidQueueWeb
20
20
  SolidQueue::FailedExecution.discard_all_from_jobs(jobs)
21
21
  record_audit("failed_jobs_discarded", item_count: jobs.size)
22
22
  redirect_to failed_jobs_path,
23
- notice: "#{jobs.size} #{"job".pluralize(jobs.size)} discarded."
23
+ notice: t("solid_queue_web.flash.jobs_discarded", count: jobs.size)
24
24
  rescue => e
25
- 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)
26
26
  end
27
27
  end
28
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
@@ -41,7 +41,7 @@ module SolidQueueWeb
41
41
  SolidQueue::FailedExecution.discard_all_from_jobs(jobs)
42
42
  record_audit(action, job_class: jobs.first&.class_name, queue_name: jobs.first&.queue_name, item_count: jobs.size)
43
43
  redirect_to failed_jobs_path(queue: @queue, q: @search, period: @period),
44
- notice: "#{jobs.size} #{"job".pluralize(jobs.size)} discarded."
44
+ notice: t("solid_queue_web.flash.jobs_discarded", count: jobs.size)
45
45
  end
46
46
 
47
47
  def sortable_columns
@@ -4,18 +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
12
  record_audit("jobs_discarded", item_count: jobs.size)
13
13
  redirect_to jobs_path(status: status, period: period),
14
- notice: "#{jobs.size} #{"job".pluralize(jobs.size)} discarded."
14
+ notice: t("solid_queue_web.flash.jobs_discarded", count: jobs.size)
15
15
  rescue ArgumentError => e
16
16
  redirect_to jobs_path(status: status), alert: e.message
17
17
  rescue => e
18
- 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)
19
19
  end
20
20
  end
21
21
  end
@@ -36,18 +36,19 @@ module SolidQueueWeb
36
36
  @remaining_count = filtered_scope(model).count
37
37
  respond_to do |format|
38
38
  format.turbo_stream
39
- 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") }
40
40
  end
41
41
  else
42
42
  jobs = filtered_scope(model).map(&:job)
43
43
  model.discard_all_from_jobs(jobs)
44
44
  record_audit("jobs_discarded", item_count: jobs.size)
45
- redirect_to jobs_return_path, notice: "#{jobs.size} #{"job".pluralize(jobs.size)} discarded."
45
+ redirect_to jobs_return_path, notice: t("solid_queue_web.flash.jobs_discarded", count: jobs.size)
46
46
  end
47
47
  rescue ArgumentError => e
48
48
  redirect_to jobs_return_path, alert: e.message
49
49
  rescue => e
50
- 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
51
52
  end
52
53
 
53
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
@@ -5,18 +5,18 @@ module SolidQueueWeb
5
5
  queue = SolidQueue::Queue.find_by_name(params[:queue_name])
6
6
  queue.pause
7
7
  record_audit("queue_paused", queue_name: queue.name)
8
- redirect_to queues_path, notice: "Queue \"#{queue.name}\" paused."
8
+ redirect_to queues_path, notice: t("solid_queue_web.flash.queue_paused", name: queue.name)
9
9
  rescue => e
10
- redirect_to queues_path, alert: "Could not pause queue: #{e.message}"
10
+ redirect_to queues_path, alert: t("solid_queue_web.flash.cannot_pause_queue", error: e.message)
11
11
  end
12
12
 
13
13
  def destroy
14
14
  queue = SolidQueue::Queue.find_by_name(params[:queue_name])
15
15
  queue.resume
16
16
  record_audit("queue_resumed", queue_name: queue.name)
17
- redirect_to queues_path, notice: "Queue \"#{queue.name}\" resumed."
17
+ redirect_to queues_path, notice: t("solid_queue_web.flash.queue_resumed", name: queue.name)
18
18
  rescue => e
19
- redirect_to queues_path, alert: "Could not resume queue: #{e.message}"
19
+ redirect_to queues_path, alert: t("solid_queue_web.flash.cannot_resume_queue", error: e.message)
20
20
  end
21
21
  end
22
22
  end
@@ -5,14 +5,14 @@ module SolidQueueWeb
5
5
  result = task.enqueue(at: Time.current)
6
6
 
7
7
  if result
8
- redirect_to recurring_tasks_path, notice: "\"#{task.key}\" queued for immediate execution."
8
+ redirect_to recurring_tasks_path, notice: t("solid_queue_web.flash.task_queued", key: task.key)
9
9
  else
10
- redirect_to recurring_tasks_path, alert: "Could not enqueue \"#{task.key}\" — it may have just run."
10
+ redirect_to recurring_tasks_path, alert: t("solid_queue_web.flash.cannot_enqueue_task", key: task.key)
11
11
  end
12
12
  rescue ActiveRecord::RecordNotFound
13
- redirect_to recurring_tasks_path, alert: "Recurring task not found."
13
+ redirect_to recurring_tasks_path, alert: t("solid_queue_web.flash.task_not_found")
14
14
  rescue => e
15
- redirect_to recurring_tasks_path, alert: "Could not run task: #{e.message}"
15
+ redirect_to recurring_tasks_path, alert: t("solid_queue_web.flash.cannot_run_task", error: e.message)
16
16
  end
17
17
  end
18
18
  end
@@ -8,7 +8,7 @@ module SolidQueueWeb
8
8
 
9
9
  if params[:stagger].present? && executions.size > 1
10
10
  interval = STAGGER_INTERVALS[params[:stagger]]
11
- raise ArgumentError, "Invalid stagger interval." unless interval
11
+ raise ArgumentError, t("solid_queue_web.flash.invalid_stagger") unless interval
12
12
  executions.each_with_index do |execution, i|
13
13
  execution.job.update!(scheduled_at: i.zero? ? nil : Time.current + (i * interval))
14
14
  execution.retry
@@ -23,17 +23,16 @@ module SolidQueueWeb
23
23
  rescue ArgumentError => e
24
24
  redirect_to failed_jobs_path, alert: e.message
25
25
  rescue => e
26
- redirect_to failed_jobs_path, alert: "Could not retry job: #{e.message}"
26
+ redirect_to failed_jobs_path, alert: t("solid_queue_web.flash.cannot_retry_job", error: e.message)
27
27
  end
28
28
 
29
29
  private
30
30
 
31
31
  def retry_notice(count)
32
- label = "#{count} #{"job".pluralize(count)}"
33
32
  if params[:stagger].present? && count > 1
34
- "#{label} queued for retry, staggered #{params[:stagger]} apart."
33
+ t("solid_queue_web.flash.jobs_retried_staggered", count: count, stagger: params[:stagger])
35
34
  else
36
- "#{label} queued for retry."
35
+ t("solid_queue_web.flash.jobs_retried", count: count)
37
36
  end
38
37
  end
39
38
 
@@ -8,10 +8,10 @@ module SolidQueueWeb
8
8
  SolidQueue::Job.where(id: job_ids).update_all(scheduled_at: 1.second.ago)
9
9
 
10
10
  redirect_to jobs_path(status: "scheduled", period: @period),
11
- notice: "#{job_ids.size} #{"job".pluralize(job_ids.size)} scheduled to run immediately."
11
+ notice: t("solid_queue_web.flash.jobs_run_immediately", count: job_ids.size)
12
12
  rescue => e
13
13
  redirect_to jobs_path(status: "scheduled", period: @period),
14
- alert: "Could not run jobs: #{e.message}"
14
+ alert: t("solid_queue_web.flash.cannot_run_jobs", error: e.message)
15
15
  end
16
16
 
17
17
  def update
@@ -26,14 +26,14 @@ module SolidQueueWeb
26
26
  respond_to do |format|
27
27
  format.turbo_stream
28
28
  format.html do
29
- notice = @run_now ? "Job scheduled to run immediately." : "Job rescheduled by +#{params[:offset]}."
29
+ notice = @run_now ? t("solid_queue_web.flash.job_run_immediately") : t("solid_queue_web.flash.job_rescheduled", offset: params[:offset])
30
30
  redirect_to jobs_path(status: "scheduled", period: @period), notice: notice
31
31
  end
32
32
  end
33
33
  rescue ArgumentError => e
34
34
  redirect_to jobs_path(status: "scheduled"), alert: e.message
35
35
  rescue => e
36
- redirect_to jobs_path(status: "scheduled"), alert: "Could not reschedule job: #{e.message}"
36
+ redirect_to jobs_path(status: "scheduled"), alert: t("solid_queue_web.flash.cannot_reschedule_job", error: e.message)
37
37
  end
38
38
 
39
39
  private
@@ -46,7 +46,7 @@ module SolidQueueWeb
46
46
 
47
47
  def resolve_new_time(execution, offset)
48
48
  return 1.second.ago if offset == "now"
49
- raise ArgumentError, "Invalid offset." unless PERIOD_DURATIONS.key?(offset)
49
+ raise ArgumentError, t("solid_queue_web.flash.invalid_offset") unless PERIOD_DURATIONS.key?(offset)
50
50
 
51
51
  execution.scheduled_at + PERIOD_DURATIONS[offset]
52
52
  end