solid_stack_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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +157 -3
  3. data/app/assets/stylesheets/solid_stack_web/_02_layout.css +12 -0
  4. data/app/assets/stylesheets/solid_stack_web/_07_dashboard.css +4 -3
  5. data/app/controllers/solid_stack_web/application_controller.rb +10 -0
  6. data/app/controllers/solid_stack_web/audit_controller.rb +1 -1
  7. data/app/controllers/solid_stack_web/cable/channel_purges_controller.rb +1 -1
  8. data/app/controllers/solid_stack_web/cable/purges_controller.rb +1 -1
  9. data/app/controllers/solid_stack_web/cache/flushes_controller.rb +1 -1
  10. data/app/controllers/solid_stack_web/cache_entries_controller.rb +1 -1
  11. data/app/controllers/solid_stack_web/failed_jobs/arguments_controller.rb +3 -3
  12. data/app/controllers/solid_stack_web/failed_jobs/selections_controller.rb +4 -4
  13. data/app/controllers/solid_stack_web/failed_jobs_controller.rb +2 -2
  14. data/app/controllers/solid_stack_web/jobs/selections_controller.rb +2 -2
  15. data/app/controllers/solid_stack_web/jobs_controller.rb +2 -2
  16. data/app/controllers/solid_stack_web/recurring_tasks/runs_controller.rb +4 -4
  17. data/app/controllers/solid_stack_web/scheduled_jobs_controller.rb +5 -5
  18. data/app/helpers/solid_stack_web/application_helper.rb +30 -17
  19. data/app/views/layouts/solid_stack_web/application.html.erb +28 -24
  20. data/app/views/solid_stack_web/audit/index.html.erb +18 -18
  21. data/app/views/solid_stack_web/cable/index.html.erb +22 -19
  22. data/app/views/solid_stack_web/cable_messages/index.html.erb +15 -14
  23. data/app/views/solid_stack_web/cache/index.html.erb +19 -19
  24. data/app/views/solid_stack_web/cache_entries/index.html.erb +16 -15
  25. data/app/views/solid_stack_web/cache_entries/show.html.erb +11 -11
  26. data/app/views/solid_stack_web/dashboard/index.html.erb +54 -33
  27. data/app/views/solid_stack_web/errors/internal_server_error.html.erb +4 -4
  28. data/app/views/solid_stack_web/errors/not_found.html.erb +4 -4
  29. data/app/views/solid_stack_web/failed_jobs/destroy.turbo_stream.erb +2 -2
  30. data/app/views/solid_stack_web/failed_jobs/errors/index.html.erb +10 -10
  31. data/app/views/solid_stack_web/failed_jobs/index.html.erb +22 -22
  32. data/app/views/solid_stack_web/failed_jobs/show.html.erb +16 -16
  33. data/app/views/solid_stack_web/history/index.html.erb +19 -18
  34. data/app/views/solid_stack_web/jobs/_empty.html.erb +8 -8
  35. data/app/views/solid_stack_web/jobs/index.html.erb +35 -34
  36. data/app/views/solid_stack_web/jobs/show.html.erb +16 -16
  37. data/app/views/solid_stack_web/processes/index.html.erb +8 -8
  38. data/app/views/solid_stack_web/queues/index.html.erb +13 -13
  39. data/app/views/solid_stack_web/queues/show.html.erb +16 -16
  40. data/app/views/solid_stack_web/recurring_tasks/index.html.erb +16 -16
  41. data/app/views/solid_stack_web/shared/_locale_switcher.html.erb +14 -0
  42. data/app/views/solid_stack_web/stats/index.html.erb +13 -13
  43. data/config/locales/en.yml +395 -0
  44. data/config/locales/es.yml +395 -0
  45. data/lib/solid_stack_web/engine.rb +1 -0
  46. data/lib/solid_stack_web/version.rb +1 -1
  47. data/lib/solid_stack_web.rb +14 -1
  48. metadata +4 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f03b011e27e9dfeb227c490906e1fd6c985e9cf6f9f5538fb07b5f24c3a1fcbc
4
- data.tar.gz: 4c97b7832de0a0073e156eb411de5b07d449684a84f9a77025d3bcdb500c31e7
3
+ metadata.gz: db27129d1f850266817f633ff0109fdd3708c7105fdea6d0bcf3035ec1734769
4
+ data.tar.gz: 1549319d21ac55da226b2ce4487a8a8a1340531b13c838643fb2754843620518
5
5
  SHA512:
6
- metadata.gz: cffad880c519141a308b856ea4cb582a14e128f900477d3860de3f1962616a50ea33b4da1c4c5a25f22e452c3dc132dc3881131bcb38cd2cac96b4324eed02ab
7
- data.tar.gz: 455c1bd6a67c02ed6daed933e17c1f903cfdee45d2b57dfc17da255f58fd7b9ed1c1a9bd16900fbf96df70691bedc4014cdb2978bec3fabfd099f968716604cb
6
+ metadata.gz: '02499cb54b94ad58e62204d18f60780cc88e22c62706248f5fb60a4905f36eb822e8243de0351df15a915f740501bde0b973fa9e6a17754ba2741b64f4b6b695'
7
+ data.tar.gz: 627aae0342fa4f93ef0a6e25b89e56764ff6bda2e2c75d7b0315675ee2daacdc2e57ed428698485302b2a6950725bf5d63bb5c5c51a6e566d55f141cbf820c08
data/README.md CHANGED
@@ -1,12 +1,48 @@
1
1
  # SolidStackWeb
2
2
 
3
3
  [![CI](https://github.com/eclectic-coding/solid_stack_web/actions/workflows/ci.yml/badge.svg)](https://github.com/eclectic-coding/solid_stack_web/actions/workflows/ci.yml)
4
- [![Gem Version](https://badge.fury.io/rb/solid_stack_web.svg)](https://badge.fury.io/rb/solid_stack_web)
4
+ [![Gem Version](https://img.shields.io/gem/v/solid_stack_web.svg)](https://rubygems.org/gems/solid_stack_web)
5
5
  [![Downloads](https://img.shields.io/gem/dt/solid_stack_web.svg)](https://rubygems.org/gems/solid_stack_web)
6
6
  [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.3-ruby)](https://www.ruby-lang.org)
7
7
  [![codecov](https://codecov.io/gh/eclectic-coding/solid_stack_web/branch/main/graph/badge.svg)](https://codecov.io/gh/eclectic-coding/solid_stack_web)
8
8
 
9
- A production-ready operations dashboard for the full Rails Solid Stack. Mount one engine to get deep visibility into **Solid Queue** (job browser, failed job retry, queue controls, recurring tasks, performance stats), **Solid Cache** (entry browser, size distribution, write timeline), and **Solid Cable** (channel browser, message list, purge controls) — with dark mode, CSV export, alert webhooks, and a JSON metrics endpoint, all with no asset pipeline dependency.
9
+ A production-ready operations dashboard for the full Rails Solid Stack. Mount one engine to get deep visibility into **Solid Queue** (job browser, failed job retry, queue controls, recurring tasks, performance stats), **Solid Cache** (entry browser, size distribution, write timeline), and **Solid Cable** (channel browser, message list, purge controls) — with dark mode, i18n locale switching, custom nav links, custom dashboard cards, CSV export, alert webhooks, and a JSON metrics endpoint, all with no asset pipeline dependency.
10
+
11
+ ## Table of Contents
12
+
13
+ - [Installation](#installation)
14
+ - [Screenshots](#screenshots)
15
+ - [Metrics endpoint](#metrics-endpoint)
16
+ - [General configuration](#general-configuration)
17
+ - [Authentication](#authentication)
18
+ - [Linking to the dashboard](#linking-to-the-dashboard)
19
+ - [i18n](#i18n)
20
+ - [Adding a custom locale](#adding-a-custom-locale)
21
+ - [Extensibility](#extensibility)
22
+ - [Custom nav links](#custom-nav-links)
23
+ - [Custom dashboard cards](#custom-dashboard-cards)
24
+ - [Security](#security)
25
+ - [Authentication](#authentication-1)
26
+ - [Sensitive cache values](#sensitive-cache-values)
27
+ - [CSRF protection](#csrf-protection)
28
+ - [Rate limiting and network exposure](#rate-limiting-and-network-exposure)
29
+ - [Solid Queue](#solid-queue)
30
+ - [Features](#features)
31
+ - [Configuration](#configuration)
32
+ - [Job Filtering](#job-filtering)
33
+ - [Solid Cache](#solid-cache)
34
+ - [Features](#features-1)
35
+ - [Solid Cable](#solid-cable)
36
+ - [Features](#features-2)
37
+ - [Requirements](#requirements)
38
+ - [Versioning](#versioning)
39
+ - [Public API](#public-api)
40
+ - [Not part of the public API](#not-part-of-the-public-api)
41
+ - [Deprecation policy](#deprecation-policy)
42
+ - [Contributing](#contributing)
43
+ - [License](#license)
44
+
45
+ ---
10
46
 
11
47
  ## Installation
12
48
 
@@ -30,12 +66,16 @@ rails generate solid_stack_web:install
30
66
 
31
67
  This creates `config/initializers/solid_stack_web.rb` with every configuration option commented inline, and injects `mount SolidStackWeb::Engine, at: "/solid_stack"` into `config/routes.rb`. The dashboard will then be available at `/solid_stack` (or whatever path you choose).
32
68
 
69
+ [↑ Back to top](#table-of-contents)
70
+
33
71
  ---
34
72
 
35
73
  ## Screenshots
36
74
 
37
75
  ![SolidStackWeb dashboard](docs/screenshots/demo.gif)
38
76
 
77
+ [↑ Back to top](#table-of-contents)
78
+
39
79
  ---
40
80
 
41
81
  ## Metrics endpoint
@@ -64,6 +104,8 @@ This creates `config/initializers/solid_stack_web.rb` with every configuration o
64
104
 
65
105
  `slow_jobs` is only present when `slow_job_threshold` is configured. The endpoint is protected by the same authentication as the rest of the dashboard.
66
106
 
107
+ [↑ Back to top](#table-of-contents)
108
+
67
109
  ---
68
110
 
69
111
  ## General configuration
@@ -84,6 +126,12 @@ SolidStackWeb.configure do |config|
84
126
  # Multi-database — pass a connects_to hash when Solid Queue / Cache / Cable
85
127
  # live on a separate database from your primary (default: nil, uses primary).
86
128
  config.connects_to = { database: { writing: :queue, reading: :queue } }
129
+
130
+ # Custom nav links — appended to the main navigation bar (default: []).
131
+ config.nav_links = [{ label: "Admin", url: "/admin" }]
132
+
133
+ # Custom dashboard cards — rendered after the built-in cards (default: []).
134
+ config.dashboard_cards = [{ title: "My App", stats: -> { { "Users" => User.count } } }]
87
135
  end
88
136
  ```
89
137
 
@@ -99,6 +147,85 @@ The `authenticate` block is evaluated in the context of each request's controlle
99
147
  link_to "Queue Dashboard", SolidStackWeb.mount_path
100
148
  ```
101
149
 
150
+ [↑ Back to top](#table-of-contents)
151
+
152
+ ---
153
+
154
+ ## i18n
155
+
156
+ All dashboard UI strings — page titles, table headers, button labels, empty states, flash messages, and sparkline tooltips — are backed by locale YAML files. The engine ships with **English (`en`)** and **Spanish (`es`)** built in.
157
+
158
+ A language selector appears in the dashboard header and lets users switch locales at runtime. 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.
159
+
160
+ The switcher is automatically hidden when `config.available_locales` contains only one entry.
161
+
162
+ ```ruby
163
+ SolidStackWeb.configure do |config|
164
+ # Locales shown in the language switcher (default: [:en, :es]).
165
+ # Set to [:en] to hide the switcher entirely.
166
+ config.available_locales = [:en, :es]
167
+ end
168
+ ```
169
+
170
+ ### Adding a custom locale
171
+
172
+ 1. Create a locale file in your host application under `config/locales/`, e.g. `config/locales/solid_stack_web.fr.yml`.
173
+ 2. Nest all keys under `fr > solid_stack_web:` — use `config/locales/en.yml` in the gem as a reference for the full key list.
174
+ 3. Add the locale symbol to `config.available_locales`:
175
+
176
+ ```ruby
177
+ config.available_locales = [:en, :es, :fr]
178
+ ```
179
+
180
+ Rails will pick up the file automatically via its standard `config.i18n.load_path`; no additional configuration is needed.
181
+
182
+ [↑ Back to top](#table-of-contents)
183
+
184
+ ---
185
+
186
+ ## Extensibility
187
+
188
+ ### Custom nav links
189
+
190
+ `config.nav_links` appends extra links to the main navigation bar after the built-in Queue / Cache / Cable links. Use it to link back to your host application's admin pages or related tools without modifying the engine layout.
191
+
192
+ ```ruby
193
+ SolidStackWeb.configure do |config|
194
+ config.nav_links = [
195
+ { label: "Back to App", url: "/" },
196
+ { label: "Admin", url: "/admin" }
197
+ ]
198
+ end
199
+ ```
200
+
201
+ Defaults to `[]` — no extra links appear when unconfigured.
202
+
203
+ ### Custom dashboard cards
204
+
205
+ `config.dashboard_cards` adds custom stat cards to the overview dashboard after the built-in Queue, Cache, and Cable cards. Each card accepts three keys:
206
+
207
+ | Key | Type | Description |
208
+ |-----|------|-------------|
209
+ | `title` | String | Card heading (required) |
210
+ | `link` | `{ label:, url: }` | Optional header link rendered top-right |
211
+ | `stats` | Lambda | Optional — called at render time; must return a `{ label => value }` hash |
212
+
213
+ ```ruby
214
+ SolidStackWeb.configure do |config|
215
+ config.dashboard_cards = [
216
+ {
217
+ title: "My App",
218
+ link: { label: "View Admin", url: "/admin" },
219
+ stats: -> { { "Users" => User.count, "Premium" => User.premium.count } }
220
+ }
221
+ ]
222
+ end
223
+ ```
224
+
225
+ The `stats` lambda runs on every dashboard render, so keep it fast. Defaults to `[]` — no custom cards appear when unconfigured.
226
+
227
+ [↑ Back to top](#table-of-contents)
228
+
102
229
  ---
103
230
 
104
231
  ## Security
@@ -137,6 +264,8 @@ The dashboard is designed to be mounted behind your application's existing authe
137
264
  - Restricting access by IP at the reverse-proxy level
138
265
  - Applying [Rack::Attack](https://github.com/rack/rack-attack) rules to the mount path
139
266
 
267
+ [↑ Back to top](#table-of-contents)
268
+
140
269
  ---
141
270
 
142
271
  ## Solid Queue
@@ -159,6 +288,7 @@ The dashboard is designed to be mounted behind your application's existing authe
159
288
  - **Turbo Stream** job discard — removes the row inline without a full page reload
160
289
  - **Sticky filter preferences** — last-used status, period, and queue filter saved to `localStorage`; a fresh visit to the jobs or history list with no URL params automatically restores the previous selection
161
290
  - **Dark mode** — toggle button in the header switches between light and dark palettes; preference persisted in `localStorage`; respects `prefers-color-scheme` on first visit
291
+ - **i18n / locale switching** — all UI strings backed by locale YAML files; ships with English (`en`) and Spanish (`es`); a language selector in the header lets users switch at runtime; locale is stored in the session and persists across requests; configure which locales appear via `config.available_locales`
162
292
  - **Responsive layout** — stats cards, tables, and two-column grids adapt to narrow viewports; tables scroll horizontally rather than overflow; split page headers stack on small screens
163
293
  - **Empty-state improvements** — all list views show a contextual title and an actionable hint; search empty states include a "Clear search" link; filters-active history view offers "Clear filters"; processes and recurring tasks explain the next step
164
294
  - **Inline notifications** — bulk and single-job actions surface a flash notice; Turbo Stream discard responses inject the message inline without a full page reload; bulk actions report the affected count ("3 jobs discarded")
@@ -192,6 +322,12 @@ SolidStackWeb.configure do |config|
192
322
  # Show the raw serialized value on the cache entry detail page (default: false).
193
323
  # Disable for stores that contain sensitive data.
194
324
  config.allow_value_preview = true
325
+
326
+ # Locales shown in the language switcher (default: [:en, :es]).
327
+ # The switcher is hidden when only one locale is configured.
328
+ # To add a locale, provide a locale YAML file under your app's config/locales/
329
+ # with keys nested under solid_stack_web:, then add the locale symbol here.
330
+ config.available_locales = [:en, :es]
195
331
  end
196
332
  ```
197
333
 
@@ -208,6 +344,8 @@ The jobs list supports four independent filters, all driven by query params:
208
344
 
209
345
  Filters are preserved when switching between status tabs (Ready / Scheduled / Running / Blocked) and when discarding a job. They can be combined freely.
210
346
 
347
+ [↑ Back to top](#table-of-contents)
348
+
211
349
  ---
212
350
 
213
351
  ## Solid Cache
@@ -224,6 +362,8 @@ Filters are preserved when switching between status tabs (Ready / Scheduled / Ru
224
362
  - **Delete entry** — per-row delete button or detail-page button removes a single cache entry
225
363
  - **Flush All** — header button deletes every cache entry with a confirmation prompt
226
364
 
365
+ [↑ Back to top](#table-of-contents)
366
+
227
367
  ---
228
368
 
229
369
  ## Solid Cable
@@ -236,6 +376,8 @@ Filters are preserved when switching between status tabs (Ready / Scheduled / Ru
236
376
  - **Per-channel message list** — `GET /cable/channels/:channel_hash` shows a paginated, reverse-chronological list of that channel's `SolidCable::Message` records; each row shows the message ID, a truncated payload preview (120 chars) with the full payload on hover, and a relative sent time with the exact timestamp on hover; supports `?q=` filtering by payload substring; **Purge Channel** button deletes all messages for the channel
237
377
  - **Message purge** — "Purge Old" form on the channel browser deletes all messages older than 1, 7, or 30 days; confirmation prompt before any destructive action
238
378
 
379
+ [↑ Back to top](#table-of-contents)
380
+
239
381
  ---
240
382
 
241
383
  ## Requirements
@@ -248,6 +390,10 @@ Filters are preserved when switching between status tabs (Ready / Scheduled / Ru
248
390
  - [turbo-rails](https://github.com/hotwired/turbo-rails) >= 2.0
249
391
  - [importmap-rails](https://github.com/rails/importmap-rails) >= 1.2
250
392
 
393
+ [↑ Back to top](#table-of-contents)
394
+
395
+ ---
396
+
251
397
  ## Versioning
252
398
 
253
399
  SolidStackWeb follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
@@ -276,6 +422,8 @@ The following are internal and may change in any release without notice:
276
422
 
277
423
  When a public API item is renamed or removed, the old interface is deprecated in a **minor** release — it continues to work but issues an `ActiveSupport::Deprecation` warning pointing to the replacement. The old interface is removed in the next **major** release. The [UPGRADING.md](UPGRADING.md) file documents every breaking change and the migration steps.
278
424
 
425
+ [↑ Back to top](#table-of-contents)
426
+
279
427
  ---
280
428
 
281
429
  ## Contributing
@@ -287,6 +435,12 @@ When a public API item is renamed or removed, the old interface is deprecated in
287
435
 
288
436
  Bug reports and feature requests are welcome on [GitHub Issues](https://github.com/eclectic-coding/solid_stack_web/issues).
289
437
 
438
+ [↑ Back to top](#table-of-contents)
439
+
440
+ ---
441
+
290
442
  ## License
291
443
 
292
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
444
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
445
+
446
+ [↑ Back to top](#table-of-contents)
@@ -117,3 +117,15 @@
117
117
  }
118
118
  .sqw-flash--notice { background: #d1e7dd; color: #0a3622; border: 1px solid #a3cfbb; }
119
119
  .sqw-flash--alert { background: #f8d7da; color: #58151c; border: 1px solid #f1aeb5; }
120
+
121
+ .sqw-locale-form { display: flex; align-items: center; flex-shrink: 0; }
122
+ .sqw-locale-select {
123
+ font-size: 12px;
124
+ padding: 0.2rem 0.4rem;
125
+ border: 1px solid var(--border);
126
+ border-radius: var(--radius);
127
+ background: var(--surface);
128
+ color: var(--muted);
129
+ cursor: pointer;
130
+ height: 28px;
131
+ }
@@ -16,9 +16,10 @@
16
16
  }
17
17
  .sqw-gem-card:hover { box-shadow: 0 3px 8px rgba(0,0,0,.12); }
18
18
 
19
- .sqw-gem-card--queue { border-top-color: var(--primary); }
20
- .sqw-gem-card--cache { border-top-color: var(--purple); }
21
- .sqw-gem-card--cable { border-top-color: var(--info); }
19
+ .sqw-gem-card--queue { border-top-color: var(--primary); }
20
+ .sqw-gem-card--cache { border-top-color: var(--purple); }
21
+ .sqw-gem-card--cable { border-top-color: var(--info); }
22
+ .sqw-gem-card--custom { border-top-color: var(--muted); }
22
23
 
23
24
  .sqw-gem-card__header {
24
25
  display: flex;
@@ -7,6 +7,7 @@ module SolidStackWeb
7
7
  PERIOD_DURATIONS = { "1h" => 1.hour, "24h" => 24.hours, "7d" => 7.days }.freeze
8
8
 
9
9
  before_action :authenticate!
10
+ around_action :with_locale
10
11
  around_action :with_database_connection
11
12
 
12
13
  rescue_from StandardError do |exception|
@@ -29,6 +30,15 @@ module SolidStackWeb
29
30
  end
30
31
  end
31
32
 
33
+ def with_locale
34
+ available = SolidStackWeb.available_locales.map(&:to_s)
35
+ locale = params[:locale].presence_in(available) ||
36
+ session[:solid_stack_web_locale].presence_in(available) ||
37
+ I18n.default_locale.to_s
38
+ session[:solid_stack_web_locale] = locale
39
+ I18n.with_locale(locale) { yield }
40
+ end
41
+
32
42
  def with_database_connection
33
43
  config = SolidStackWeb.connects_to
34
44
  return yield unless config
@@ -3,7 +3,7 @@ module SolidStackWeb
3
3
  def index
4
4
  unless AuditEvent.table_exists?
5
5
  redirect_to root_path,
6
- alert: "Audit log requires running `rails solid_stack_web:install:migrations && rails db:migrate`."
6
+ alert: t("solid_stack_web.flash.audit_migration_required")
7
7
  return
8
8
  end
9
9
 
@@ -2,7 +2,7 @@ module SolidStackWeb
2
2
  class Cable::ChannelPurgesController < ApplicationController
3
3
  def destroy
4
4
  ::SolidCable::Message.where(channel_hash: params[:channel_hash]).delete_all
5
- redirect_to cable_path, notice: "All messages for this channel have been purged."
5
+ redirect_to cable_path, notice: t("solid_stack_web.flash.channel_purged")
6
6
  end
7
7
  end
8
8
  end
@@ -3,7 +3,7 @@ module SolidStackWeb
3
3
  def destroy
4
4
  days = [params[:older_than].to_i, 1].max
5
5
  ::SolidCable::Message.where("created_at < ?", days.days.ago).delete_all
6
- redirect_to cable_path, notice: "Messages older than #{days} #{days == 1 ? "day" : "days"} purged."
6
+ redirect_to cable_path, notice: t("solid_stack_web.flash.messages_purged", count: days)
7
7
  end
8
8
  end
9
9
  end
@@ -2,7 +2,7 @@ module SolidStackWeb
2
2
  class Cache::FlushesController < ApplicationController
3
3
  def destroy
4
4
  ::SolidCache::Entry.delete_all
5
- redirect_to cache_entries_path, notice: "All cache entries flushed."
5
+ redirect_to cache_entries_path, notice: t("solid_stack_web.flash.cache_flushed")
6
6
  end
7
7
  end
8
8
  end
@@ -18,7 +18,7 @@ module SolidStackWeb
18
18
  def destroy
19
19
  ::SolidCache::Entry.find(params[:id]).destroy
20
20
  redirect_to cache_entries_path(q: params[:q], column: params[:column], direction: params[:direction]),
21
- notice: "Cache entry deleted."
21
+ notice: t("solid_stack_web.flash.cache_entry_deleted")
22
22
  end
23
23
 
24
24
  private
@@ -6,11 +6,11 @@ module SolidStackWeb
6
6
  new_arguments = JSON.parse(params[:arguments])
7
7
  @execution.job.update!(arguments: new_arguments)
8
8
  @execution.retry
9
- redirect_to failed_jobs_path, notice: "Arguments updated and job queued for retry."
9
+ redirect_to failed_jobs_path, notice: t("solid_stack_web.flash.arguments_updated")
10
10
  rescue JSON::ParserError
11
- redirect_to failed_job_path(@execution), alert: "Invalid JSON — arguments were not saved."
11
+ redirect_to failed_job_path(@execution), alert: t("solid_stack_web.flash.invalid_json")
12
12
  rescue => e
13
- redirect_to failed_jobs_path, alert: "Could not update job: #{e.message}"
13
+ redirect_to failed_jobs_path, alert: t("solid_stack_web.flash.cannot_update_job", error: e.message)
14
14
  end
15
15
  end
16
16
  end
@@ -7,18 +7,18 @@ module SolidStackWeb
7
7
  count = @ids.size
8
8
  SolidQueue::FailedExecution.where(id: @ids).each(&:retry)
9
9
  record_audit("failed_jobs_retried", item_count: count)
10
- redirect_to failed_jobs_path, notice: "#{count} #{count == 1 ? "job" : "jobs"} retried."
10
+ redirect_to failed_jobs_path, notice: t("solid_stack_web.flash.jobs_retried", count: count)
11
11
  rescue => e
12
- redirect_to failed_jobs_path, alert: "Could not retry jobs: #{e.message}"
12
+ redirect_to failed_jobs_path, alert: t("solid_stack_web.flash.cannot_retry_jobs", error: e.message)
13
13
  end
14
14
 
15
15
  def destroy
16
16
  job_ids = SolidQueue::FailedExecution.where(id: @ids).pluck(:job_id)
17
17
  count = SolidQueue::Job.where(id: job_ids).destroy_all.size
18
18
  record_audit("failed_jobs_discarded", item_count: count)
19
- redirect_to failed_jobs_path, notice: "#{count} #{count == 1 ? "job" : "jobs"} discarded."
19
+ redirect_to failed_jobs_path, notice: t("solid_stack_web.flash.jobs_discarded", count: count)
20
20
  rescue => e
21
- redirect_to failed_jobs_path, alert: "Could not discard jobs: #{e.message}"
21
+ redirect_to failed_jobs_path, alert: t("solid_stack_web.flash.cannot_discard_jobs", error: e.message)
22
22
  end
23
23
 
24
24
  private
@@ -33,7 +33,7 @@ module SolidStackWeb
33
33
  @execution.job.destroy!
34
34
  record_audit("failed_job_discarded", job_class: job_class, queue_name: queue_name)
35
35
  @executions_remain = ::SolidQueue::FailedExecution.exists?
36
- @notice = "Job discarded."
36
+ @notice = t("solid_stack_web.flash.job_discarded")
37
37
 
38
38
  respond_to do |format|
39
39
  format.html { redirect_to failed_jobs_path }
@@ -45,7 +45,7 @@ module SolidStackWeb
45
45
  execution = ::SolidQueue::FailedExecution.find(params[:id])
46
46
  record_audit("failed_job_retried", job_class: execution.job.class_name, queue_name: execution.job.queue_name)
47
47
  execution.retry
48
- redirect_to failed_jobs_path, notice: "Job retried."
48
+ redirect_to failed_jobs_path, notice: t("solid_stack_web.flash.job_retried")
49
49
  end
50
50
 
51
51
  private
@@ -3,7 +3,7 @@ module SolidStackWeb
3
3
  class SelectionsController < ApplicationController
4
4
  def destroy
5
5
  status = params[:status].presence_in(Job::STATUSES) || "ready"
6
- raise ArgumentError, "Cannot discard #{status} jobs." unless Job::DISCARDABLE.include?(status)
6
+ raise ArgumentError, t("solid_stack_web.flash.cannot_discard", status: status) unless Job::DISCARDABLE.include?(status)
7
7
 
8
8
  ids = Array(params[:job_ids]).map(&:to_i).reject(&:zero?)
9
9
  job_ids = Job::EXECUTION_MODELS[status].where(id: ids).pluck(:job_id)
@@ -18,7 +18,7 @@ module SolidStackWeb
18
18
  priority: params[:priority].presence,
19
19
  sort: params[:sort].presence,
20
20
  direction: params[:direction].presence
21
- ), notice: "#{count} #{count == 1 ? "job" : "jobs"} discarded."
21
+ ), notice: t("solid_stack_web.flash.jobs_discarded", count: count)
22
22
  rescue ArgumentError => e
23
23
  redirect_to jobs_path(status: params[:status]), alert: e.message
24
24
  end
@@ -36,7 +36,7 @@ module SolidStackWeb
36
36
  @execution.job.destroy!
37
37
  record_audit("job_discarded", job_class: job_class, queue_name: queue_name)
38
38
  @executions_remain = Job::EXECUTION_MODELS[@status].exists?
39
- @notice = "Job discarded."
39
+ @notice = t("solid_stack_web.flash.job_discarded")
40
40
 
41
41
  respond_to do |format|
42
42
  format.html { redirect_to jobs_path(status: @status, q: @search, queue: @queue, period: @period, priority: @priority, sort: @sort, direction: @direction) }
@@ -47,7 +47,7 @@ module SolidStackWeb
47
47
  count = SolidQueue::Job.where(id: job_ids).destroy_all.size
48
48
  record_audit("jobs_discarded", item_count: count)
49
49
  redirect_to jobs_path(status: @status, q: @search, queue: @queue, period: @period, priority: @priority, sort: @sort, direction: @direction),
50
- notice: "#{count} #{count == 1 ? "job" : "jobs"} discarded."
50
+ notice: t("solid_stack_web.flash.jobs_discarded", count: count)
51
51
  end
52
52
  end
53
53
 
@@ -5,14 +5,14 @@ module SolidStackWeb
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_stack_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_stack_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_stack_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_stack_web.flash.cannot_run_task", error: e.message)
16
16
  end
17
17
  end
18
18
  end
@@ -6,10 +6,10 @@ module SolidStackWeb
6
6
  SolidQueue::ScheduledExecution.where(job_id: job_ids).update_all(scheduled_at: 1.second.ago)
7
7
  SolidQueue::Job.where(id: job_ids).update_all(scheduled_at: 1.second.ago)
8
8
  redirect_to jobs_path(status: "scheduled", period: @period),
9
- notice: "#{job_ids.size} #{"job".pluralize(job_ids.size)} scheduled to run immediately."
9
+ notice: t("solid_stack_web.flash.jobs_run_immediately", count: job_ids.size)
10
10
  rescue => e
11
11
  redirect_to jobs_path(status: "scheduled", period: @period),
12
- alert: "Could not run jobs: #{e.message}"
12
+ alert: t("solid_stack_web.flash.cannot_run_jobs", error: e.message)
13
13
  end
14
14
 
15
15
  def update
@@ -24,14 +24,14 @@ module SolidStackWeb
24
24
  respond_to do |format|
25
25
  format.turbo_stream
26
26
  format.html do
27
- notice = @run_now ? "Job scheduled to run immediately." : "Job rescheduled by +#{params[:offset]}."
27
+ notice = @run_now ? t("solid_stack_web.flash.job_run_immediately") : t("solid_stack_web.flash.job_rescheduled", offset: params[:offset])
28
28
  redirect_to jobs_path(status: "scheduled", period: @period), notice: notice
29
29
  end
30
30
  end
31
31
  rescue ArgumentError => e
32
32
  redirect_to jobs_path(status: "scheduled"), alert: e.message
33
33
  rescue => e
34
- redirect_to jobs_path(status: "scheduled"), alert: "Could not reschedule job: #{e.message}"
34
+ redirect_to jobs_path(status: "scheduled"), alert: t("solid_stack_web.flash.cannot_reschedule_job", error: e.message)
35
35
  end
36
36
 
37
37
  private
@@ -44,7 +44,7 @@ module SolidStackWeb
44
44
 
45
45
  def resolve_new_time(execution, offset)
46
46
  return 1.second.ago if offset == "now"
47
- raise ArgumentError, "Invalid offset." unless PERIOD_DURATIONS.key?(offset)
47
+ raise ArgumentError, t("solid_stack_web.flash.invalid_offset") unless PERIOD_DURATIONS.key?(offset)
48
48
 
49
49
  execution.scheduled_at + PERIOD_DURATIONS[offset]
50
50
  end
@@ -35,11 +35,14 @@ module SolidStackWeb
35
35
  build_sparkline_svg(
36
36
  Struct.new(:buckets, :max).new(timeline.entry_buckets, timeline.entry_max),
37
37
  css_class: "sqw-sparkline sqw-sparkline--lg",
38
- aria_label: "Cache entries written over the last 24 hours"
38
+ aria_label: t("solid_stack_web.helpers.cache_entry_this_hour", count: 0).gsub(/\d+/, "…")
39
39
  ) do |count, i|
40
40
  hours_ago = CacheTimeline::HOURS - 1 - i
41
- hours_ago.zero? ? "#{count} #{"entry".then { |w| count == 1 ? w : "entries" }} this hour" \
42
- : "#{count} #{"entry".then { |w| count == 1 ? w : "entries" }} #{hours_ago}h ago"
41
+ if hours_ago.zero?
42
+ t("solid_stack_web.helpers.cache_entry_this_hour", count: count)
43
+ else
44
+ t("solid_stack_web.helpers.cache_entry_hours_ago", count: count, hours: hours_ago)
45
+ end
43
46
  end
44
47
  end
45
48
 
@@ -47,11 +50,15 @@ module SolidStackWeb
47
50
  build_sparkline_svg(
48
51
  Struct.new(:buckets, :max).new(timeline.byte_buckets, timeline.byte_max),
49
52
  css_class: "sqw-sparkline sqw-sparkline--lg",
50
- aria_label: "Cache bytes written over the last 24 hours"
53
+ aria_label: t("solid_stack_web.cache.bytes_written")
51
54
  ) do |bytes, i|
52
55
  hours_ago = CacheTimeline::HOURS - 1 - i
53
56
  size = number_to_human_size(bytes)
54
- hours_ago.zero? ? "#{size} written this hour" : "#{size} written #{hours_ago}h ago"
57
+ if hours_ago.zero?
58
+ t("solid_stack_web.helpers.cache_size_this_hour", size: size)
59
+ else
60
+ t("solid_stack_web.helpers.cache_size_hours_ago", size: size, hours: hours_ago)
61
+ end
55
62
  end
56
63
  end
57
64
 
@@ -59,31 +66,37 @@ module SolidStackWeb
59
66
  build_sparkline_svg(
60
67
  Struct.new(:buckets, :max).new(timeline.message_buckets, timeline.message_max),
61
68
  css_class: "sqw-sparkline sqw-sparkline--lg",
62
- aria_label: "Cable messages over the last 24 hours"
69
+ aria_label: t("solid_stack_web.cable.messages_timeline")
63
70
  ) do |count, i|
64
71
  hours_ago = CableTimeline::HOURS - 1 - i
65
- label = count == 1 ? "message" : "messages"
66
- hours_ago.zero? ? "#{count} #{label} this hour" : "#{count} #{label} #{hours_ago}h ago"
72
+ if hours_ago.zero?
73
+ t("solid_stack_web.helpers.cable_message_this_hour", count: count)
74
+ else
75
+ t("solid_stack_web.helpers.cable_message_hours_ago", count: count, hours: hours_ago)
76
+ end
67
77
  end
68
78
  end
69
79
 
70
80
  def throughput_sparkline_svg(sparkline)
71
- build_sparkline_svg(sparkline, aria_label: "Throughput over the last 12 hours") do |count, i|
81
+ build_sparkline_svg(sparkline, aria_label: t("solid_stack_web.dashboard.throughput_label")) do |count, i|
72
82
  hours_ago = SolidStackWeb::ThroughputSparkline::HOURS - i
73
83
  if hours_ago == 1
74
- "#{count} #{count == 1 ? "job" : "jobs"} in the last hour"
84
+ t("solid_stack_web.helpers.throughput_last_hour", count: count)
75
85
  else
76
- "#{count} #{count == 1 ? "job" : "jobs"} (#{hours_ago}h–#{hours_ago - 1}h ago)"
86
+ t("solid_stack_web.helpers.throughput_hours_ago", count: count, from: hours_ago, to: hours_ago - 1)
77
87
  end
78
88
  end
79
89
  end
80
90
 
81
91
  def queue_depth_sparkline_svg(sparkline)
82
92
  build_sparkline_svg(sparkline, css_class: "sqw-sparkline sqw-sparkline--sm",
83
- aria_label: "Queue depth over the last 12 hours") do |count, i|
93
+ aria_label: t("solid_stack_web.queues.col_depth")) do |count, i|
84
94
  hours_ago = SolidStackWeb::QueueDepthSparkline::HOURS - 1 - i
85
- jobs_word = count == 1 ? "job" : "jobs"
86
- hours_ago.zero? ? "#{count} ready #{jobs_word} now" : "#{count} ready #{jobs_word} #{hours_ago}h ago"
95
+ if hours_ago.zero?
96
+ t("solid_stack_web.helpers.queue_depth_now", count: count)
97
+ else
98
+ t("solid_stack_web.helpers.queue_depth_hours_ago", count: count, hours: hours_ago)
99
+ end
87
100
  end
88
101
  end
89
102
 
@@ -110,12 +123,12 @@ module SolidStackWeb
110
123
  end
111
124
 
112
125
  def failed_job_sparkline_svg(sparkline)
113
- build_sparkline_svg(sparkline, aria_label: "Failed jobs over the last 12 hours") do |count, i|
126
+ build_sparkline_svg(sparkline, aria_label: t("solid_stack_web.dashboard.failures_label")) do |count, i|
114
127
  hours_ago = SolidStackWeb::FailedJobSparkline::HOURS - i
115
128
  if hours_ago == 1
116
- "#{count} #{count == 1 ? "failure" : "failures"} in the last hour"
129
+ t("solid_stack_web.helpers.failure_last_hour", count: count)
117
130
  else
118
- "#{count} #{count == 1 ? "failure" : "failures"} (#{hours_ago}h–#{hours_ago - 1}h ago)"
131
+ t("solid_stack_web.helpers.failure_hours_ago", count: count, from: hours_ago, to: hours_ago - 1)
119
132
  end
120
133
  end
121
134
  end