solid_stack_web 0.8.0 → 1.0.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 +145 -49
- data/app/assets/stylesheets/solid_stack_web/_01_base.css +12 -0
- data/app/assets/stylesheets/solid_stack_web/_09_detail.css +3 -28
- 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/cache_entries/show.html.erb +11 -14
- 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/failed_jobs/show.html.erb +1 -1
- 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/docs/screenshots/demo.gif +0 -0
- 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/engine.rb +4 -0
- data/lib/solid_stack_web/version.rb +1 -1
- data/lib/solid_stack_web.rb +35 -0
- metadata +14 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e7932ddf2c50cc9ae63a884c0dabdc0fe4553c9dfb823870d3e1b15b4dc2a3a6
|
|
4
|
+
data.tar.gz: 04d6d15b941d21dfae0d1562095e6cdb4cd67a7e1163efa3bc2d33563df2fb92
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 37ac38026dbde5e629d132f18795797f84a7b48b8e19689435b0288f44c57abf73e7dae5aa9e51884021920b4b336160597529b4f87c44cac027e9b88c1293d6
|
|
7
|
+
data.tar.gz: c233686b589092befdcc5b19ca7bfd09a2f10e9f1d95b894f58afff4fb67475ddcae308bf6b65386f9af5479d670256dde1cd02cf3cf2b5466eadcd00a051a2d
|
data/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://www.ruby-lang.org)
|
|
6
6
|
[](https://codecov.io/gh/eclectic-coding/solid_stack_web)
|
|
7
7
|
|
|
8
|
-
A
|
|
8
|
+
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
9
|
|
|
10
10
|
## Installation
|
|
11
11
|
|
|
@@ -29,6 +29,123 @@ 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
|
+
## Screenshots
|
|
45
|
+
|
|
46
|
+

|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Metrics endpoint
|
|
51
|
+
|
|
52
|
+
`GET /metrics` (relative to your mount path) returns a JSON payload suitable for external monitoring tools, uptime checkers, or custom alerting:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"queue": {
|
|
57
|
+
"ready": 4,
|
|
58
|
+
"scheduled": 1,
|
|
59
|
+
"claimed": 2,
|
|
60
|
+
"blocked": 0,
|
|
61
|
+
"failed": 3,
|
|
62
|
+
"done_1h": 45,
|
|
63
|
+
"done_24h": 312,
|
|
64
|
+
"processes_healthy": 2,
|
|
65
|
+
"processes_stale": 0,
|
|
66
|
+
"slow_jobs": 7
|
|
67
|
+
},
|
|
68
|
+
"cache": { "entries": 1024, "byte_size": 2097152, "oldest_entry": "2026-05-20T10:00:00Z" },
|
|
69
|
+
"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 } },
|
|
70
|
+
"generated_at": "2026-05-26T10:00:00Z"
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`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.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## General configuration
|
|
79
|
+
|
|
80
|
+
Create an initializer at `config/initializers/solid_stack_web.rb`:
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
SolidStackWeb.configure do |config|
|
|
84
|
+
# Number of items per paginated page (default: 25)
|
|
85
|
+
config.page_size = 50
|
|
86
|
+
|
|
87
|
+
# Authentication — block runs in controller context.
|
|
88
|
+
# Return a truthy value to allow access; falsy falls back to HTTP Basic.
|
|
89
|
+
config.authenticate do
|
|
90
|
+
current_user&.admin?
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Multi-database — pass a connects_to hash when Solid Queue / Cache / Cable
|
|
94
|
+
# live on a separate database from your primary (default: nil, uses primary).
|
|
95
|
+
config.connects_to = { database: { writing: :queue, reading: :queue } }
|
|
96
|
+
end
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Authentication
|
|
100
|
+
|
|
101
|
+
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.
|
|
102
|
+
|
|
103
|
+
### Linking to the dashboard
|
|
104
|
+
|
|
105
|
+
`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:
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
link_to "Queue Dashboard", SolidStackWeb.mount_path
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Security
|
|
114
|
+
|
|
115
|
+
### Authentication
|
|
116
|
+
|
|
117
|
+
**The dashboard is open to all visitors by default.** Any production deployment must configure an `authenticate` block or the dashboard will be publicly accessible.
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
SolidStackWeb.configure do |config|
|
|
121
|
+
# Devise
|
|
122
|
+
config.authenticate { current_user&.admin? }
|
|
123
|
+
|
|
124
|
+
# HTTP Basic fallback (used when no authenticate block is set, or when
|
|
125
|
+
# the block returns false/nil and you want a browser credential prompt)
|
|
126
|
+
# Configure via HTTP_BASIC_AUTH_NAME / HTTP_BASIC_AUTH_PASSWORD env vars
|
|
127
|
+
# in your host app, or use a reverse proxy.
|
|
128
|
+
end
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
If the `authenticate` block returns `false` or `nil`, the engine falls back to HTTP Basic authentication. If no block is configured at all, the dashboard is open.
|
|
132
|
+
|
|
133
|
+
### Sensitive cache values
|
|
134
|
+
|
|
135
|
+
`allow_value_preview` is `false` by default. Enabling it renders the raw serialised cache value on the entry detail page. Do not enable this if your cache stores session tokens, PII, or other sensitive data.
|
|
136
|
+
|
|
137
|
+
### CSRF protection
|
|
138
|
+
|
|
139
|
+
All state-mutating actions (job discard, retry, queue pause/resume, cache flush) use form POST requests. Turbo handles CSRF tokens automatically for any standard Rails app with `protect_from_forgery`.
|
|
140
|
+
|
|
141
|
+
### Rate limiting and network exposure
|
|
142
|
+
|
|
143
|
+
The dashboard is designed to be mounted behind your application's existing authentication. For additional hardening, consider:
|
|
144
|
+
|
|
145
|
+
- Mounting at a non-guessable path (e.g. `at: "/ops/#{Rails.application.credentials.dashboard_token}"`)
|
|
146
|
+
- Restricting access by IP at the reverse-proxy level
|
|
147
|
+
- Applying [Rack::Attack](https://github.com/rack/rack-attack) rules to the mount path
|
|
148
|
+
|
|
32
149
|
---
|
|
33
150
|
|
|
34
151
|
## Solid Queue
|
|
@@ -128,66 +245,45 @@ Filters are preserved when switching between status tabs (Ready / Scheduled / Ru
|
|
|
128
245
|
|
|
129
246
|
---
|
|
130
247
|
|
|
131
|
-
##
|
|
132
|
-
|
|
133
|
-
`GET /metrics` (relative to your mount path) returns a JSON payload suitable for external monitoring tools, uptime checkers, or custom alerting:
|
|
248
|
+
## Requirements
|
|
134
249
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
```
|
|
250
|
+
- Ruby >= 3.3
|
|
251
|
+
- Rails >= 8.1.3
|
|
252
|
+
- [solid_queue](https://github.com/rails/solid_queue) >= 1.0
|
|
253
|
+
- [solid_cache](https://github.com/rails/solid_cache) >= 1.0
|
|
254
|
+
- [solid_cable](https://github.com/rails/solid_cable) >= 1.0
|
|
255
|
+
- [turbo-rails](https://github.com/hotwired/turbo-rails) >= 2.0
|
|
256
|
+
- [importmap-rails](https://github.com/rails/importmap-rails) >= 1.2
|
|
154
257
|
|
|
155
|
-
|
|
258
|
+
## Versioning
|
|
156
259
|
|
|
157
|
-
|
|
260
|
+
SolidStackWeb follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
158
261
|
|
|
159
|
-
|
|
262
|
+
### Public API
|
|
160
263
|
|
|
161
|
-
|
|
264
|
+
The following are considered stable public API from v1.0.0 onwards — breaking changes to any of these require a major version bump:
|
|
162
265
|
|
|
163
|
-
|
|
164
|
-
SolidStackWeb.
|
|
165
|
-
|
|
166
|
-
|
|
266
|
+
- The `SolidStackWeb.configure` block and all documented configuration keys
|
|
267
|
+
- The `SolidStackWeb.mount_path` helper
|
|
268
|
+
- The `authenticate` block interface
|
|
269
|
+
- The `GET /metrics` JSON payload shape
|
|
270
|
+
- The `SolidStackWeb::Engine` class and its mount interface
|
|
271
|
+
- The `rails generate solid_stack_web:install` generator
|
|
167
272
|
|
|
168
|
-
|
|
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
|
-
```
|
|
273
|
+
### Not part of the public API
|
|
175
274
|
|
|
176
|
-
|
|
275
|
+
The following are internal and may change in any release without notice:
|
|
177
276
|
|
|
178
|
-
|
|
277
|
+
- Internal service classes (`CacheStats`, `QueueStats`, etc.)
|
|
278
|
+
- View templates, partial names, and CSS class names
|
|
279
|
+
- Controller and helper internals
|
|
280
|
+
- Private methods on any class
|
|
179
281
|
|
|
180
|
-
|
|
282
|
+
### Deprecation policy
|
|
181
283
|
|
|
182
|
-
|
|
284
|
+
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.
|
|
183
285
|
|
|
184
|
-
|
|
185
|
-
- Rails >= 8.1.3
|
|
186
|
-
- [solid_queue](https://github.com/rails/solid_queue) >= 1.0
|
|
187
|
-
- [solid_cache](https://github.com/rails/solid_cache) >= 1.0
|
|
188
|
-
- [solid_cable](https://github.com/rails/solid_cable) >= 1.0
|
|
189
|
-
- [turbo-rails](https://github.com/hotwired/turbo-rails) >= 2.0
|
|
190
|
-
- [importmap-rails](https://github.com/rails/importmap-rails) >= 1.2
|
|
286
|
+
---
|
|
191
287
|
|
|
192
288
|
## Contributing
|
|
193
289
|
|
|
@@ -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; }
|
|
@@ -70,20 +70,9 @@
|
|
|
70
70
|
overflow-y: auto;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
.sqw-detail
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
gap: 0.5rem 1.5rem;
|
|
77
|
-
font-size: 13px;
|
|
78
|
-
margin-bottom: 1.5rem;
|
|
79
|
-
}
|
|
80
|
-
.sqw-detail__row {
|
|
81
|
-
display: contents;
|
|
82
|
-
}
|
|
83
|
-
.sqw-detail__row dt { color: var(--muted); white-space: nowrap; align-self: start; padding-top: 0.15rem; }
|
|
84
|
-
.sqw-detail__row dd { word-break: break-all; }
|
|
85
|
-
|
|
86
|
-
.sqw-value-preview { margin-top: 1rem; }
|
|
73
|
+
.sqw-detail-card--mt,
|
|
74
|
+
.sqw-detail-card + .sqw-detail-card,
|
|
75
|
+
.sqw-detail-grid + .sqw-detail-card { margin-top: 1.5rem; }
|
|
87
76
|
|
|
88
77
|
.sqw-value-preview__header {
|
|
89
78
|
display: flex;
|
|
@@ -93,20 +82,6 @@
|
|
|
93
82
|
}
|
|
94
83
|
.sqw-value-preview__header .sqw-section-title { margin-bottom: 0; }
|
|
95
84
|
|
|
96
|
-
.sqw-value-pre {
|
|
97
|
-
font-family: ui-monospace, "SFMono-Regular", Menlo, monospace;
|
|
98
|
-
font-size: 12px;
|
|
99
|
-
background: var(--bg);
|
|
100
|
-
border: 1px solid var(--border);
|
|
101
|
-
border-radius: var(--radius);
|
|
102
|
-
padding: 0.75rem;
|
|
103
|
-
overflow-x: auto;
|
|
104
|
-
white-space: pre-wrap;
|
|
105
|
-
word-break: break-word;
|
|
106
|
-
max-height: 500px;
|
|
107
|
-
overflow-y: auto;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
85
|
.sqw-value-truncated { font-size: 12px; margin-top: 0.5rem; }
|
|
111
86
|
|
|
112
87
|
.sqw-link { color: var(--primary); text-decoration: none; }
|
|
@@ -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? %>
|
|
@@ -10,33 +10,30 @@
|
|
|
10
10
|
</div>
|
|
11
11
|
</div>
|
|
12
12
|
|
|
13
|
-
<
|
|
14
|
-
<
|
|
13
|
+
<div class="sqw-detail-card sqw-detail-section">
|
|
14
|
+
<h2 class="sqw-section-title">Details</h2>
|
|
15
|
+
<dl class="sqw-dl">
|
|
15
16
|
<dt>Key</dt>
|
|
16
17
|
<dd class="sqw-monospace"><%= @entry.key %></dd>
|
|
17
|
-
</div>
|
|
18
|
-
<div class="sqw-detail__row">
|
|
19
18
|
<dt>Size</dt>
|
|
20
19
|
<dd><%= number_to_human_size(@entry.byte_size) %></dd>
|
|
21
|
-
</div>
|
|
22
|
-
<div class="sqw-detail__row">
|
|
23
20
|
<dt>Created</dt>
|
|
24
21
|
<dd class="sqw-muted"><%= local_time(@entry.created_at, format: :long) %></dd>
|
|
25
|
-
</
|
|
26
|
-
</
|
|
22
|
+
</dl>
|
|
23
|
+
</div>
|
|
27
24
|
|
|
28
|
-
<
|
|
25
|
+
<div class="sqw-detail-card sqw-detail-section">
|
|
26
|
+
<% formatted = SolidStackWeb.allow_value_preview ? format_cache_value(@entry.value) : nil %>
|
|
29
27
|
<div class="sqw-value-preview__header">
|
|
30
28
|
<h2 class="sqw-section-title">Value</h2>
|
|
31
|
-
<% if
|
|
32
|
-
<% formatted = format_cache_value(@entry.value) %>
|
|
29
|
+
<% if formatted %>
|
|
33
30
|
<span class="sqw-badge sqw-badge--queue"><%= formatted[:label] %></span>
|
|
34
31
|
<% end %>
|
|
35
32
|
</div>
|
|
36
|
-
<% if
|
|
33
|
+
<% if formatted %>
|
|
37
34
|
<% content = formatted[:content] %>
|
|
38
35
|
<% truncated = content.length > 4096 %>
|
|
39
|
-
<pre class="sqw-
|
|
36
|
+
<pre class="sqw-code-block"><%= truncated ? content[0, 4096] : content %></pre>
|
|
40
37
|
<% if truncated %>
|
|
41
38
|
<p class="sqw-muted sqw-value-truncated">Showing first 4 KB of <%= number_to_human_size(@entry.byte_size) %> total.</p>
|
|
42
39
|
<% end %>
|
|
@@ -45,4 +42,4 @@
|
|
|
45
42
|
<p>Value preview is disabled. Set <code>config.allow_value_preview = true</code> in your initializer to enable.</p>
|
|
46
43
|
</div>
|
|
47
44
|
<% end %>
|
|
48
|
-
</
|
|
45
|
+
</div>
|
|
@@ -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,7 +45,7 @@
|
|
|
45
45
|
</div>
|
|
46
46
|
</div>
|
|
47
47
|
|
|
48
|
-
<div class="sqw-detail-card sqw-detail-section
|
|
48
|
+
<div class="sqw-detail-card sqw-detail-section sqw-detail-card--mt">
|
|
49
49
|
<h2 class="sqw-section-title">Arguments</h2>
|
|
50
50
|
<%= form_with url: failed_job_arguments_path(@execution), method: :patch do |f| %>
|
|
51
51
|
<%= f.text_area :arguments, value: @arguments, rows: 12,
|
|
@@ -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 %>
|
|
Binary file
|
|
@@ -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
|
|
@@ -25,6 +25,10 @@ module SolidStackWeb
|
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
initializer "solid_stack_web.deprecator" do |app|
|
|
29
|
+
app.deprecators[:solid_stack_web] = SolidStackWeb.deprecator
|
|
30
|
+
end
|
|
31
|
+
|
|
28
32
|
initializer "solid_stack_web.pagy" do |app|
|
|
29
33
|
app.config.after_initialize do
|
|
30
34
|
Pagy::OPTIONS[:limit] = SolidStackWeb.page_size
|
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
|
|
@@ -61,5 +74,27 @@ module SolidStackWeb
|
|
|
61
74
|
@authenticate = block if block_given?
|
|
62
75
|
@authenticate
|
|
63
76
|
end
|
|
77
|
+
|
|
78
|
+
def deprecator
|
|
79
|
+
@deprecator ||= ActiveSupport::Deprecation.new("1.0", "SolidStackWeb")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
# Define a deprecated writer for a renamed config key.
|
|
85
|
+
# The old writer issues a deprecation warning and forwards to the new key.
|
|
86
|
+
#
|
|
87
|
+
# Usage (add entries here as keys are renamed before 1.0):
|
|
88
|
+
# deprecated_config :old_key, :new_key
|
|
89
|
+
def deprecated_config(old_key, new_key)
|
|
90
|
+
singleton_class.define_method(:"#{old_key}=") do |value|
|
|
91
|
+
deprecator.warn(
|
|
92
|
+
"config.#{old_key}= is deprecated and will be removed in SolidStackWeb 1.0. " \
|
|
93
|
+
"Use config.#{new_key}= instead.",
|
|
94
|
+
caller_locations
|
|
95
|
+
)
|
|
96
|
+
public_send(:"#{new_key}=", value)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
64
99
|
end
|
|
65
100
|
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: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chuck Smith
|
|
@@ -121,9 +121,14 @@ dependencies:
|
|
|
121
121
|
- - ">="
|
|
122
122
|
- !ruby/object:Gem::Version
|
|
123
123
|
version: '3.0'
|
|
124
|
-
description:
|
|
125
|
-
dashboard
|
|
126
|
-
|
|
124
|
+
description: SolidStackWeb is a mountable Rails engine that provides a production-ready
|
|
125
|
+
operations dashboard for the full Solid Stack. It covers Solid Queue (job browser,
|
|
126
|
+
failed job retry with inline argument editing, queue pause/resume, recurring tasks,
|
|
127
|
+
performance stats, CSV export, and alert webhooks), Solid Cache (entry browser,
|
|
128
|
+
size distribution, 24-hour write timeline, and optional value preview), and Solid
|
|
129
|
+
Cable (channel browser, per-channel message list, and purge controls). Ships with
|
|
130
|
+
dark mode, Turbo Stream responses, a JSON metrics endpoint, and no asset pipeline
|
|
131
|
+
dependency.
|
|
127
132
|
email:
|
|
128
133
|
- eclectic-coding@users.noreply.github.com
|
|
129
134
|
executables: []
|
|
@@ -193,6 +198,8 @@ files:
|
|
|
193
198
|
- app/views/solid_stack_web/cache_entries/index.html.erb
|
|
194
199
|
- app/views/solid_stack_web/cache_entries/show.html.erb
|
|
195
200
|
- app/views/solid_stack_web/dashboard/index.html.erb
|
|
201
|
+
- app/views/solid_stack_web/errors/internal_server_error.html.erb
|
|
202
|
+
- app/views/solid_stack_web/errors/not_found.html.erb
|
|
196
203
|
- app/views/solid_stack_web/failed_jobs/destroy.turbo_stream.erb
|
|
197
204
|
- app/views/solid_stack_web/failed_jobs/index.html.erb
|
|
198
205
|
- app/views/solid_stack_web/failed_jobs/show.html.erb
|
|
@@ -210,6 +217,9 @@ files:
|
|
|
210
217
|
- app/views/solid_stack_web/stats/index.html.erb
|
|
211
218
|
- config/importmap.rb
|
|
212
219
|
- config/routes.rb
|
|
220
|
+
- docs/screenshots/demo.gif
|
|
221
|
+
- lib/generators/solid_stack_web/install/install_generator.rb
|
|
222
|
+
- lib/generators/solid_stack_web/install/templates/initializer.rb
|
|
213
223
|
- lib/solid_stack_web.rb
|
|
214
224
|
- lib/solid_stack_web/engine.rb
|
|
215
225
|
- lib/solid_stack_web/version.rb
|