solid_stack_web 0.8.0 → 0.9.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 (27) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +69 -51
  3. data/app/assets/stylesheets/solid_stack_web/_01_base.css +12 -0
  4. data/app/controllers/solid_stack_web/application_controller.rb +15 -0
  5. data/app/controllers/solid_stack_web/jobs_controller.rb +5 -2
  6. data/app/controllers/solid_stack_web/queues_controller.rb +4 -9
  7. data/app/models/solid_stack_web/cache_size_stats.rb +10 -6
  8. data/app/views/layouts/solid_stack_web/application.html.erb +6 -5
  9. data/app/views/solid_stack_web/cable/index.html.erb +3 -3
  10. data/app/views/solid_stack_web/cable_messages/index.html.erb +4 -4
  11. data/app/views/solid_stack_web/cache/index.html.erb +5 -5
  12. data/app/views/solid_stack_web/cache_entries/index.html.erb +3 -3
  13. data/app/views/solid_stack_web/errors/internal_server_error.html.erb +8 -0
  14. data/app/views/solid_stack_web/errors/not_found.html.erb +8 -0
  15. data/app/views/solid_stack_web/failed_jobs/index.html.erb +7 -7
  16. data/app/views/solid_stack_web/history/index.html.erb +5 -5
  17. data/app/views/solid_stack_web/jobs/index.html.erb +8 -8
  18. data/app/views/solid_stack_web/processes/index.html.erb +5 -5
  19. data/app/views/solid_stack_web/queues/index.html.erb +5 -5
  20. data/app/views/solid_stack_web/queues/show.html.erb +5 -5
  21. data/app/views/solid_stack_web/recurring_tasks/index.html.erb +8 -8
  22. data/app/views/solid_stack_web/stats/index.html.erb +1 -1
  23. data/lib/generators/solid_stack_web/install/install_generator.rb +19 -0
  24. data/lib/generators/solid_stack_web/install/templates/initializer.rb +51 -0
  25. data/lib/solid_stack_web/version.rb +1 -1
  26. data/lib/solid_stack_web.rb +13 -0
  27. metadata +5 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a146449291d2d1e6bc46612021b6abf939a08f969fda923afceeb863323bf672
4
- data.tar.gz: 6499cd93a4db8268bbf637bfb5934e3777ec124eea3f44ccea877188c7c83ed1
3
+ metadata.gz: 85093a248316f7f8921636f68bdc08e2053e496eeb85f17bbbd7cddac57e4ed6
4
+ data.tar.gz: 3321fa62f08ce3615f733740cfac444d60ed65960b9adc9e4894919416482b46
5
5
  SHA512:
6
- metadata.gz: be0e30ea4fa72ef666e82cefddd3936a99196bd43f809e546fa37bb59bfed39ed267b07b42268479baed8210303762d0629c49bb15c9cbb3661090b295bad91e
7
- data.tar.gz: 7bc30e8a5a62929ed893bc1189f3b141c52add6489f423f39076a7565219d419a9a60568f5faae9b7dbfb503c8f74144bab1a91f6d7db065b654c9ed2d2e29d7
6
+ metadata.gz: 278a4dbde1baa31bb57f58c72d41ea13ef9ddbb83043839228abb4cb0ab06286c4ca954d63ee0c2f2e6914a1b8c24f946731b73afe8db1176ba28a8c157c6b52
7
+ data.tar.gz: f6b953b896ba256d03e26f802ab594d9e822fbd114ce68f6b7f673d435fceca8009a9ec2c55ea0f45a6dacfcebb34b9b9c23c64564faa212e00a8450292d95c4
data/README.md CHANGED
@@ -29,6 +29,75 @@ mount SolidStackWeb::Engine, at: "/solid_stack"
29
29
 
30
30
  The dashboard will be available at `/solid_stack` (or whatever path you choose).
31
31
 
32
+ ### Install generator
33
+
34
+ Run the install generator to create a documented initializer and wire up the mount point in one step:
35
+
36
+ ```bash
37
+ rails generate solid_stack_web:install
38
+ ```
39
+
40
+ 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`.
41
+
42
+ ---
43
+
44
+ ## Metrics endpoint
45
+
46
+ `GET /metrics` (relative to your mount path) returns a JSON payload suitable for external monitoring tools, uptime checkers, or custom alerting:
47
+
48
+ ```json
49
+ {
50
+ "queue": {
51
+ "ready": 4,
52
+ "scheduled": 1,
53
+ "claimed": 2,
54
+ "blocked": 0,
55
+ "failed": 3,
56
+ "done_1h": 45,
57
+ "done_24h": 312,
58
+ "processes_healthy": 2,
59
+ "processes_stale": 0,
60
+ "slow_jobs": 7
61
+ },
62
+ "cache": { "entries": 1024, "byte_size": 2097152, "oldest_entry": "2026-05-20T10:00:00Z" },
63
+ "cable": { "messages": 50, "channels": 3, "messages_per_hour": 12, "oldest_message": "2026-05-20T10:00:00Z", "top_channels": { "ActionCable::Channel::Base": 30, "ChatChannel": 15, "NotificationsChannel": 5 } },
64
+ "generated_at": "2026-05-26T10:00:00Z"
65
+ }
66
+ ```
67
+
68
+ `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.
69
+
70
+ ---
71
+
72
+ ## General configuration
73
+
74
+ Create an initializer at `config/initializers/solid_stack_web.rb`:
75
+
76
+ ```ruby
77
+ SolidStackWeb.configure do |config|
78
+ # Number of items per paginated page (default: 25)
79
+ config.page_size = 50
80
+
81
+ # Authentication — block runs in controller context.
82
+ # Return a truthy value to allow access; falsy falls back to HTTP Basic.
83
+ config.authenticate do
84
+ current_user&.admin?
85
+ end
86
+ end
87
+ ```
88
+
89
+ ### Authentication
90
+
91
+ The `authenticate` block is evaluated in the context of each request's controller instance, so any helper method available to controllers (e.g. `current_user` from Devise) works directly. If the block returns `false` or `nil`, the engine falls back to HTTP Basic authentication. If no `authenticate` block is configured, the dashboard is open.
92
+
93
+ ### Linking to the dashboard
94
+
95
+ `SolidStackWeb.mount_path` returns the path at which the engine is mounted, derived automatically from your routes. Use it to link to the dashboard from your application layout without hardcoding the path:
96
+
97
+ ```ruby
98
+ link_to "Queue Dashboard", SolidStackWeb.mount_path
99
+ ```
100
+
32
101
  ---
33
102
 
34
103
  ## Solid Queue
@@ -128,57 +197,6 @@ Filters are preserved when switching between status tabs (Ready / Scheduled / Ru
128
197
 
129
198
  ---
130
199
 
131
- ## Metrics endpoint
132
-
133
- `GET /metrics` (relative to your mount path) returns a JSON payload suitable for external monitoring tools, uptime checkers, or custom alerting:
134
-
135
- ```json
136
- {
137
- "queue": {
138
- "ready": 4,
139
- "scheduled": 1,
140
- "claimed": 2,
141
- "blocked": 0,
142
- "failed": 3,
143
- "done_1h": 45,
144
- "done_24h": 312,
145
- "processes_healthy": 2,
146
- "processes_stale": 0,
147
- "slow_jobs": 7
148
- },
149
- "cache": { "entries": 1024, "byte_size": 2097152, "oldest_entry": "2026-05-20T10:00:00Z" },
150
- "cable": { "messages": 50, "channels": 3, "messages_per_hour": 12, "oldest_message": "2026-05-20T10:00:00Z", "top_channels": { "ActionCable::Channel::Base": 30, "ChatChannel": 15, "NotificationsChannel": 5 } },
151
- "generated_at": "2026-05-26T10:00:00Z"
152
- }
153
- ```
154
-
155
- `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.
156
-
157
- ---
158
-
159
- ## General configuration
160
-
161
- Create an initializer at `config/initializers/solid_stack_web.rb`:
162
-
163
- ```ruby
164
- SolidStackWeb.configure do |config|
165
- # Number of items per paginated page (default: 25)
166
- config.page_size = 50
167
-
168
- # Authentication — block runs in controller context.
169
- # Return a truthy value to allow access; falsy falls back to HTTP Basic.
170
- config.authenticate do
171
- current_user&.admin?
172
- end
173
- end
174
- ```
175
-
176
- ### Authentication
177
-
178
- The `authenticate` block is evaluated in the context of each request's controller instance, so any helper method available to controllers (e.g. `current_user` from Devise) works directly. If the block returns `false` or `nil`, the engine falls back to HTTP Basic authentication. If no `authenticate` block is configured, the dashboard is open.
179
-
180
- ---
181
-
182
200
  ## Requirements
183
201
 
184
202
  - Ruby >= 3.3
@@ -31,3 +31,15 @@ a:hover { text-decoration: underline; }
31
31
  .sqw-monospace { font-family: ui-monospace, "SFMono-Regular", Menlo, monospace; font-size: 13px; }
32
32
  .sqw-muted { color: var(--muted); }
33
33
  .sqw-truncate { max-width: 280px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
34
+
35
+ .sqw-sr-only {
36
+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
37
+ overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
38
+ }
39
+
40
+ .sqw-skip-link {
41
+ position: absolute; top: -100%; left: 0; z-index: 9999;
42
+ padding: 0.5rem 1rem; background: var(--accent); color: #fff;
43
+ font-weight: 600; text-decoration: none; border-radius: 0 0 4px 0;
44
+ }
45
+ .sqw-skip-link:focus { top: 0; }
@@ -9,6 +9,13 @@ module SolidStackWeb
9
9
  before_action :authenticate!
10
10
  around_action :with_database_connection
11
11
 
12
+ rescue_from StandardError do |exception|
13
+ raise exception if Rails.application.config.consider_all_requests_local
14
+ render_internal_server_error
15
+ end
16
+
17
+ rescue_from ActiveRecord::RecordNotFound, with: :render_not_found
18
+
12
19
  helper_method :current_section
13
20
 
14
21
  private
@@ -43,5 +50,13 @@ module SolidStackWeb
43
50
  def request_basic_auth
44
51
  request_http_basic_authentication("Solid Stack Dashboard")
45
52
  end
53
+
54
+ def render_not_found
55
+ render "solid_stack_web/errors/not_found", status: :not_found
56
+ end
57
+
58
+ def render_internal_server_error
59
+ render "solid_stack_web/errors/internal_server_error", status: :internal_server_error
60
+ end
46
61
  end
47
62
  end
@@ -5,8 +5,11 @@ module SolidStackWeb
5
5
  before_action :require_discardable, only: [:destroy]
6
6
 
7
7
  def index
8
- @queue_options = Job::EXECUTION_MODELS[@status].joins(:job).distinct.pluck("solid_queue_jobs.queue_name").sort
9
- @priority_options = Job::EXECUTION_MODELS[@status].joins(:job).distinct.pluck("solid_queue_jobs.priority").sort
8
+ pairs = Job::EXECUTION_MODELS[@status].joins(:job)
9
+ .distinct
10
+ .pluck("solid_queue_jobs.queue_name", "solid_queue_jobs.priority")
11
+ @queue_options = pairs.map(&:first).uniq.sort
12
+ @priority_options = pairs.map(&:last).uniq.sort
10
13
 
11
14
  respond_to do |format|
12
15
  format.html { @pagy, @executions = pagy(filtered_scope) }
@@ -1,16 +1,11 @@
1
1
  module SolidStackWeb
2
2
  class QueuesController < ApplicationController
3
3
  def index
4
- queue_names = ::SolidQueue::ReadyExecution.distinct.pluck(:queue_name)
5
- paused = ::SolidQueue::Pause.pluck(:queue_name).to_set
4
+ counts = ::SolidQueue::ReadyExecution.group(:queue_name).count
5
+ paused = ::SolidQueue::Pause.pluck(:queue_name).to_set
6
6
 
7
- @queues = queue_names.sort.map do |name|
8
- {
9
- name: name,
10
- size: ::SolidQueue::ReadyExecution.where(queue_name: name).count,
11
- paused: paused.include?(name)
12
- }
13
- end
7
+ @queues = counts.map { |name, size| { name:, size:, paused: paused.include?(name) } }
8
+ .sort_by { |q| q[:name] }
14
9
 
15
10
  @sparklines = @queues.each_with_object({}) do |queue, h|
16
11
  h[queue[:name]] = QueueDepthSparkline.new(queue[:name])
@@ -18,16 +18,20 @@ module SolidStackWeb
18
18
  end
19
19
 
20
20
  def buckets
21
- @buckets ||= BUCKETS.map do |b|
22
- scope = ::SolidCache::Entry.all
23
- scope = scope.where("byte_size >= ?", b[:min]) if b[:min] > 0
24
- scope = scope.where("byte_size < ?", b[:max]) if b[:max]
25
- { label: b[:label], count: scope.count }
21
+ @buckets ||= begin
22
+ row = ::SolidCache::Entry.pluck(
23
+ Arel.sql("COALESCE(SUM(CASE WHEN byte_size < 1024 THEN 1 ELSE 0 END), 0)"),
24
+ Arel.sql("COALESCE(SUM(CASE WHEN byte_size >= 1024 AND byte_size < 10240 THEN 1 ELSE 0 END), 0)"),
25
+ Arel.sql("COALESCE(SUM(CASE WHEN byte_size >= 10240 AND byte_size < 102400 THEN 1 ELSE 0 END), 0)"),
26
+ Arel.sql("COALESCE(SUM(CASE WHEN byte_size >= 102400 AND byte_size < 1048576 THEN 1 ELSE 0 END), 0)"),
27
+ Arel.sql("COALESCE(SUM(CASE WHEN byte_size >= 1048576 THEN 1 ELSE 0 END), 0)")
28
+ ).first || Array.new(5, 0)
29
+ BUCKETS.zip(row).map { |b, count| { label: b[:label], count: count.to_i } }
26
30
  end
27
31
  end
28
32
 
29
33
  def total
30
- @total ||= ::SolidCache::Entry.count
34
+ @total ||= buckets.sum { |b| b[:count] }
31
35
  end
32
36
  end
33
37
  end
@@ -11,10 +11,11 @@
11
11
  <%= javascript_importmap_tags "solid_stack_web" %>
12
12
  </head>
13
13
  <body data-controller="theme">
14
+ <a href="#main-content" class="sqw-skip-link">Skip to main content</a>
14
15
  <header class="sqw-header">
15
16
  <div class="sqw-header__inner">
16
17
  <%= link_to "Solid Stack", root_path, class: "sqw-header__logo" %>
17
- <nav class="sqw-nav">
18
+ <nav class="sqw-nav" aria-label="Main navigation">
18
19
  <%= link_to "Queue", jobs_path,
19
20
  class: "sqw-nav__link#{" sqw-nav__link--active" if current_section == :queue}" %>
20
21
  <%= link_to "Cache", cache_path,
@@ -28,7 +29,7 @@
28
29
  </header>
29
30
 
30
31
  <% if current_section == :cache %>
31
- <nav class="sqw-subnav">
32
+ <nav class="sqw-subnav" aria-label="Cache section">
32
33
  <div class="sqw-subnav__inner">
33
34
  <%= link_to "Overview", cache_path,
34
35
  class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "cache"}" %>
@@ -39,7 +40,7 @@
39
40
  <% end %>
40
41
 
41
42
  <% if current_section == :cable %>
42
- <nav class="sqw-subnav">
43
+ <nav class="sqw-subnav" aria-label="Cable section">
43
44
  <div class="sqw-subnav__inner">
44
45
  <%= link_to "Overview", cable_path,
45
46
  class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "cable"}" %>
@@ -48,7 +49,7 @@
48
49
  <% end %>
49
50
 
50
51
  <% if current_section == :queue %>
51
- <nav class="sqw-subnav">
52
+ <nav class="sqw-subnav" aria-label="Queue section">
52
53
  <div class="sqw-subnav__inner">
53
54
  <%= link_to "Jobs", jobs_path,
54
55
  class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "jobs"}" %>
@@ -68,7 +69,7 @@
68
69
  </nav>
69
70
  <% end %>
70
71
 
71
- <main class="sqw-main">
72
+ <main id="main-content" class="sqw-main">
72
73
  <div id="sqw-flash-container">
73
74
  <% flash.each do |type, message| %>
74
75
  <div class="sqw-flash sqw-flash--<%= type %>"><%= message %></div>
@@ -50,9 +50,9 @@
50
50
  <table class="sqw-table">
51
51
  <thead>
52
52
  <tr>
53
- <th>Channel</th>
54
- <th>Messages</th>
55
- <th>Last Message</th>
53
+ <th scope="col">Channel</th>
54
+ <th scope="col">Messages</th>
55
+ <th scope="col">Last Message</th>
56
56
  </tr>
57
57
  </thead>
58
58
  <tbody>
@@ -23,9 +23,9 @@
23
23
  <table class="sqw-table">
24
24
  <thead>
25
25
  <tr>
26
- <th>ID</th>
27
- <th>Payload</th>
28
- <th>Sent</th>
26
+ <th scope="col">ID</th>
27
+ <th scope="col">Payload</th>
28
+ <th scope="col">Sent</th>
29
29
  </tr>
30
30
  </thead>
31
31
  <tbody>
@@ -42,7 +42,7 @@
42
42
  <% end %>
43
43
  </tbody>
44
44
  </table>
45
- <%== @pagy.series_nav if @pagy.pages > 1 %>
45
+ <%== @pagy.series_nav(aria_label: "Pagination") if @pagy.pages > 1 %>
46
46
  <% else %>
47
47
  <div class="sqw-empty">
48
48
  <% if @search.present? %>
@@ -20,9 +20,9 @@
20
20
  <table class="sqw-table">
21
21
  <thead>
22
22
  <tr>
23
- <th>Range</th>
24
- <th>Entries</th>
25
- <th></th>
23
+ <th scope="col">Range</th>
24
+ <th scope="col">Entries</th>
25
+ <th scope="col"><span class="sqw-sr-only">Distribution</span></th>
26
26
  </tr>
27
27
  </thead>
28
28
  <tbody>
@@ -48,8 +48,8 @@
48
48
  <table class="sqw-table">
49
49
  <thead>
50
50
  <tr>
51
- <th>Key</th>
52
- <th>Size</th>
51
+ <th scope="col">Key</th>
52
+ <th scope="col">Size</th>
53
53
  </tr>
54
54
  </thead>
55
55
  <tbody>
@@ -26,7 +26,7 @@
26
26
  <thead>
27
27
  <tr>
28
28
  <% [["key", "Key"], ["byte_size", "Size"], ["created_at", "Created"]].each do |col, label| %>
29
- <th>
29
+ <th scope="col">
30
30
  <% next_dir = (@sort["column"] == col && @sort["direction"] == "desc") ? "asc" : "desc" %>
31
31
  <%= link_to cache_entries_path(q: @search, column: col, direction: next_dir) do %>
32
32
  <%= label %>
@@ -36,7 +36,7 @@
36
36
  <% end %>
37
37
  </th>
38
38
  <% end %>
39
- <th></th>
39
+ <th scope="col"><span class="sqw-sr-only">Actions</span></th>
40
40
  </tr>
41
41
  </thead>
42
42
  <tbody>
@@ -58,7 +58,7 @@
58
58
  <% end %>
59
59
  </tbody>
60
60
  </table>
61
- <%== @pagy.series_nav if @pagy.pages > 1 %>
61
+ <%== @pagy.series_nav(aria_label: "Pagination") if @pagy.pages > 1 %>
62
62
  <% else %>
63
63
  <div class="sqw-empty">
64
64
  <% if @search.present? %>
@@ -0,0 +1,8 @@
1
+ <div class="sqw-page-header">
2
+ <h1 class="sqw-page-title">Something Went Wrong</h1>
3
+ </div>
4
+
5
+ <div class="sqw-empty">
6
+ <p class="sqw-empty__title">500 &mdash; An unexpected error occurred.</p>
7
+ <p class="sqw-empty__hint"><%= link_to "Back to Dashboard", root_path, class: "sqw-btn sqw-btn--secondary" %></p>
8
+ </div>
@@ -0,0 +1,8 @@
1
+ <div class="sqw-page-header">
2
+ <h1 class="sqw-page-title">Not Found</h1>
3
+ </div>
4
+
5
+ <div class="sqw-empty">
6
+ <p class="sqw-empty__title">404 &mdash; The record you&rsquo;re looking for doesn&rsquo;t exist or has been removed.</p>
7
+ <p class="sqw-empty__hint"><%= link_to "Back to Dashboard", root_path, class: "sqw-btn sqw-btn--secondary" %></p>
8
+ </div>
@@ -29,14 +29,14 @@
29
29
  <table class="sqw-table">
30
30
  <thead>
31
31
  <tr>
32
- <th><input type="checkbox" class="sqw-checkbox" aria-label="Select all"
32
+ <th scope="col"><input type="checkbox" class="sqw-checkbox" aria-label="Select all"
33
33
  data-selection-target="selectAll"
34
34
  data-action="change->selection#selectAll"></th>
35
- <th>Job Class</th>
36
- <th>Queue</th>
37
- <th>Error</th>
38
- <th>Failed At</th>
39
- <th></th>
35
+ <th scope="col">Job Class</th>
36
+ <th scope="col">Queue</th>
37
+ <th scope="col">Error</th>
38
+ <th scope="col">Failed At</th>
39
+ <th scope="col"><span class="sqw-sr-only">Actions</span></th>
40
40
  </tr>
41
41
  </thead>
42
42
  <tbody>
@@ -62,7 +62,7 @@
62
62
  <% end %>
63
63
  </tbody>
64
64
  </table>
65
- <%== @pagy.series_nav if @pagy.pages > 1 %>
65
+ <%== @pagy.series_nav(aria_label: "Pagination") if @pagy.pages > 1 %>
66
66
  </div>
67
67
  <% else %>
68
68
  <div class="sqw-empty">
@@ -45,10 +45,10 @@
45
45
  <table class="sqw-table">
46
46
  <thead>
47
47
  <tr>
48
- <th>Job Class</th>
49
- <th>Queue</th>
50
- <th>Duration</th>
51
- <th>Finished At</th>
48
+ <th scope="col">Job Class</th>
49
+ <th scope="col">Queue</th>
50
+ <th scope="col">Duration</th>
51
+ <th scope="col">Finished At</th>
52
52
  </tr>
53
53
  </thead>
54
54
  <tbody>
@@ -66,7 +66,7 @@
66
66
  <% end %>
67
67
  </tbody>
68
68
  </table>
69
- <%== @pagy.series_nav if @pagy.pages > 1 %>
69
+ <%== @pagy.series_nav(aria_label: "Pagination") if @pagy.pages > 1 %>
70
70
  </div>
71
71
  <% else %>
72
72
  <div class="sqw-empty">
@@ -96,16 +96,16 @@
96
96
  <thead>
97
97
  <tr>
98
98
  <% if SolidStackWeb::Job::DISCARDABLE.include?(@status) %>
99
- <th><input type="checkbox" class="sqw-checkbox" aria-label="Select all"
99
+ <th scope="col"><input type="checkbox" class="sqw-checkbox" aria-label="Select all"
100
100
  data-selection-target="selectAll"
101
101
  data-action="change->selection#selectAll"></th>
102
102
  <% end %>
103
- <th>Job Class</th>
104
- <th>Queue</th>
105
- <th>Priority</th>
106
- <th>Enqueued At</th>
107
- <% if @status == "scheduled" %><th>Scheduled At</th><% end %>
108
- <th></th>
103
+ <th scope="col">Job Class</th>
104
+ <th scope="col">Queue</th>
105
+ <th scope="col">Priority</th>
106
+ <th scope="col">Enqueued At</th>
107
+ <% if @status == "scheduled" %><th scope="col">Scheduled At</th><% end %>
108
+ <th scope="col"><span class="sqw-sr-only">Actions</span></th>
109
109
  </tr>
110
110
  </thead>
111
111
  <tbody>
@@ -149,7 +149,7 @@
149
149
  <% end %>
150
150
  </tbody>
151
151
  </table>
152
- <%== @pagy.series_nav if @pagy.pages > 1 %>
152
+ <%== @pagy.series_nav(aria_label: "Pagination") if @pagy.pages > 1 %>
153
153
  </div>
154
154
  <% else %>
155
155
  <%= render "empty" %>
@@ -8,11 +8,11 @@
8
8
  <table class="sqw-table">
9
9
  <thead>
10
10
  <tr>
11
- <th>Kind</th>
12
- <th>Name</th>
13
- <th>PID</th>
14
- <th>Host</th>
15
- <th>Last Heartbeat</th>
11
+ <th scope="col">Kind</th>
12
+ <th scope="col">Name</th>
13
+ <th scope="col">PID</th>
14
+ <th scope="col">Host</th>
15
+ <th scope="col">Last Heartbeat</th>
16
16
  </tr>
17
17
  </thead>
18
18
  <tbody>
@@ -6,11 +6,11 @@
6
6
  <table class="sqw-table">
7
7
  <thead>
8
8
  <tr>
9
- <th>Name</th>
10
- <th>Size</th>
11
- <th>Depth (12h)</th>
12
- <th>Status</th>
13
- <th></th>
9
+ <th scope="col">Name</th>
10
+ <th scope="col">Size</th>
11
+ <th scope="col">Depth (12h)</th>
12
+ <th scope="col">Status</th>
13
+ <th scope="col"><span class="sqw-sr-only">Actions</span></th>
14
14
  </tr>
15
15
  </thead>
16
16
  <tbody>
@@ -35,10 +35,10 @@
35
35
  <table class="sqw-table">
36
36
  <thead>
37
37
  <tr>
38
- <th>Job Class</th>
39
- <th>Priority</th>
40
- <th>Enqueued At</th>
41
- <th></th>
38
+ <th scope="col">Job Class</th>
39
+ <th scope="col">Priority</th>
40
+ <th scope="col">Enqueued At</th>
41
+ <th scope="col"><span class="sqw-sr-only">Actions</span></th>
42
42
  </tr>
43
43
  </thead>
44
44
  <tbody>
@@ -59,7 +59,7 @@
59
59
  <% end %>
60
60
  </tbody>
61
61
  </table>
62
- <%== @pagy.series_nav if @pagy.pages > 1 %>
62
+ <%== @pagy.series_nav(aria_label: "Pagination") if @pagy.pages > 1 %>
63
63
  <% else %>
64
64
  <div class="sqw-empty">
65
65
  <p>No ready jobs in <strong><%= @queue_name %></strong>.</p>
@@ -6,14 +6,14 @@
6
6
  <table class="sqw-table">
7
7
  <thead>
8
8
  <tr>
9
- <th>Key</th>
10
- <th>Schedule</th>
11
- <th>Job / Command</th>
12
- <th>Queue</th>
13
- <th>Next Run</th>
14
- <th>Last Run</th>
15
- <th>Type</th>
16
- <th></th>
9
+ <th scope="col">Key</th>
10
+ <th scope="col">Schedule</th>
11
+ <th scope="col">Job / Command</th>
12
+ <th scope="col">Queue</th>
13
+ <th scope="col">Next Run</th>
14
+ <th scope="col">Last Run</th>
15
+ <th scope="col">Type</th>
16
+ <th scope="col"><span class="sqw-sr-only">Actions</span></th>
17
17
  </tr>
18
18
  </thead>
19
19
  <tbody>
@@ -15,7 +15,7 @@
15
15
  ["min", "Min"],
16
16
  ["max", "Max"]
17
17
  ].each do |col, label| %>
18
- <th>
18
+ <th scope="col" <%= "aria-sort=\"#{@direction == 'asc' ? 'ascending' : 'descending'}\"".html_safe if @sort == col %>>
19
19
  <% next_dir = (@sort == col && @direction == "desc") ? "asc" : "desc" %>
20
20
  <%= link_to stats_path(sort: col, direction: next_dir) do %>
21
21
  <%= label %>
@@ -0,0 +1,19 @@
1
+ require "rails/generators/base"
2
+
3
+ module SolidStackWeb
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ desc "Creates a SolidStackWeb initializer and mounts the engine in routes.rb"
9
+
10
+ def create_initializer
11
+ template "initializer.rb", "config/initializers/solid_stack_web.rb"
12
+ end
13
+
14
+ def mount_engine
15
+ route 'mount SolidStackWeb::Engine, at: "/solid_stack"'
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,51 @@
1
+ SolidStackWeb.configure do |config|
2
+ # Authentication — block runs in controller context.
3
+ # Return a truthy value to allow access; falsy falls back to HTTP Basic auth.
4
+ # If omitted entirely, the dashboard is open to anyone.
5
+ #
6
+ # config.authenticate do
7
+ # current_user&.admin?
8
+ # end
9
+
10
+ # Number of records shown per paginated page (default: 25).
11
+ # config.page_size = 25
12
+
13
+ # Database connection — pass a connects_to hash when Solid Queue / Cache / Cable
14
+ # live on a separate database from your primary.
15
+ #
16
+ # config.connects_to = { database: { writing: :queue, reading: :queue } }
17
+
18
+ # Slow-job threshold in seconds (default: nil — stat hidden).
19
+ # When set, the dashboard shows a "Slow (24h)" count on the overview card
20
+ # for finished jobs whose wall time exceeded this value.
21
+ # config.slow_job_threshold = 30
22
+
23
+ # Auto-refresh intervals in milliseconds.
24
+ # config.dashboard_refresh_interval = 5_000 # overview dashboard
25
+ # config.default_refresh_interval = 10_000 # jobs, processes, history
26
+
27
+ # Maximum number of results returned by the search feature (default: 25).
28
+ # config.search_results_limit = 25
29
+
30
+ # Show the raw serialised value on the cache entry detail page (default: false).
31
+ # Disable for stores that contain sensitive data.
32
+ # config.allow_value_preview = false
33
+
34
+ # Link to the dashboard from anywhere in your app without hardcoding the path:
35
+ #
36
+ # link_to "Queue Dashboard", SolidStackWeb.mount_path
37
+ #
38
+ # SolidStackWeb.mount_path is derived automatically from your routes — no
39
+ # configuration needed.
40
+
41
+ # Alert webhook — POST to this URL when a threshold is breached.
42
+ # Delivery failures are silently swallowed; configure a cooldown to avoid storms.
43
+ #
44
+ # config.alert_webhook_url = "https://hooks.example.com/my-alert"
45
+ # config.alert_failure_threshold = 10 # fire when failed jobs >= this
46
+ # config.alert_queue_thresholds = { # fire when a queue's ready depth >= value
47
+ # "critical" => 50,
48
+ # "default" => 500
49
+ # }
50
+ # config.alert_webhook_cooldown = 3600 # seconds between repeat alerts
51
+ end
@@ -1,3 +1,3 @@
1
1
  module SolidStackWeb
2
- VERSION = "0.8.0"
2
+ VERSION = "0.9.0"
3
3
  end
@@ -53,6 +53,19 @@ module SolidStackWeb
53
53
  @allow_value_preview || false
54
54
  end
55
55
 
56
+ # Returns the path at which the engine is mounted in the host application,
57
+ # derived automatically from the host's routes. Host apps can use this to
58
+ # build links to the dashboard without hardcoding the mount path.
59
+ #
60
+ # link_to "Dashboard", SolidStackWeb.mount_path
61
+ #
62
+ def mount_path
63
+ route = Rails.application.routes.routes.find do |r|
64
+ r.app.respond_to?(:app) && r.app.app == SolidStackWeb::Engine
65
+ end
66
+ route&.path&.spec&.to_s&.sub(%r{\(.*\)\z}, "") || "/"
67
+ end
68
+
56
69
  def configure
57
70
  yield self
58
71
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solid_stack_web
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuck Smith
@@ -193,6 +193,8 @@ files:
193
193
  - app/views/solid_stack_web/cache_entries/index.html.erb
194
194
  - app/views/solid_stack_web/cache_entries/show.html.erb
195
195
  - app/views/solid_stack_web/dashboard/index.html.erb
196
+ - app/views/solid_stack_web/errors/internal_server_error.html.erb
197
+ - app/views/solid_stack_web/errors/not_found.html.erb
196
198
  - app/views/solid_stack_web/failed_jobs/destroy.turbo_stream.erb
197
199
  - app/views/solid_stack_web/failed_jobs/index.html.erb
198
200
  - app/views/solid_stack_web/failed_jobs/show.html.erb
@@ -210,6 +212,8 @@ files:
210
212
  - app/views/solid_stack_web/stats/index.html.erb
211
213
  - config/importmap.rb
212
214
  - config/routes.rb
215
+ - lib/generators/solid_stack_web/install/install_generator.rb
216
+ - lib/generators/solid_stack_web/install/templates/initializer.rb
213
217
  - lib/solid_stack_web.rb
214
218
  - lib/solid_stack_web/engine.rb
215
219
  - lib/solid_stack_web/version.rb