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.
- checksums.yaml +4 -4
- data/README.md +69 -51
- data/app/assets/stylesheets/solid_stack_web/_01_base.css +12 -0
- data/app/controllers/solid_stack_web/application_controller.rb +15 -0
- data/app/controllers/solid_stack_web/jobs_controller.rb +5 -2
- data/app/controllers/solid_stack_web/queues_controller.rb +4 -9
- data/app/models/solid_stack_web/cache_size_stats.rb +10 -6
- data/app/views/layouts/solid_stack_web/application.html.erb +6 -5
- data/app/views/solid_stack_web/cable/index.html.erb +3 -3
- data/app/views/solid_stack_web/cable_messages/index.html.erb +4 -4
- data/app/views/solid_stack_web/cache/index.html.erb +5 -5
- data/app/views/solid_stack_web/cache_entries/index.html.erb +3 -3
- data/app/views/solid_stack_web/errors/internal_server_error.html.erb +8 -0
- data/app/views/solid_stack_web/errors/not_found.html.erb +8 -0
- data/app/views/solid_stack_web/failed_jobs/index.html.erb +7 -7
- data/app/views/solid_stack_web/history/index.html.erb +5 -5
- data/app/views/solid_stack_web/jobs/index.html.erb +8 -8
- data/app/views/solid_stack_web/processes/index.html.erb +5 -5
- data/app/views/solid_stack_web/queues/index.html.erb +5 -5
- data/app/views/solid_stack_web/queues/show.html.erb +5 -5
- data/app/views/solid_stack_web/recurring_tasks/index.html.erb +8 -8
- data/app/views/solid_stack_web/stats/index.html.erb +1 -1
- data/lib/generators/solid_stack_web/install/install_generator.rb +19 -0
- data/lib/generators/solid_stack_web/install/templates/initializer.rb +51 -0
- data/lib/solid_stack_web/version.rb +1 -1
- data/lib/solid_stack_web.rb +13 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 85093a248316f7f8921636f68bdc08e2053e496eeb85f17bbbd7cddac57e4ed6
|
|
4
|
+
data.tar.gz: 3321fa62f08ce3615f733740cfac444d60ed65960b9adc9e4894919416482b46
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
9
|
-
|
|
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
|
-
|
|
5
|
-
paused
|
|
4
|
+
counts = ::SolidQueue::ReadyExecution.group(:queue_name).count
|
|
5
|
+
paused = ::SolidQueue::Pause.pluck(:queue_name).to_set
|
|
6
6
|
|
|
7
|
-
@queues =
|
|
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 ||=
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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 ||=
|
|
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>
|
|
@@ -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 — 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 — The record you’re looking for doesn’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
|
data/lib/solid_stack_web.rb
CHANGED
|
@@ -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.
|
|
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
|