solid_queue_web 0.2.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6c49ff8a8bc89fae42038615cbc586d01e108db9549af1184d77623c51c9fb90
4
+ data.tar.gz: 5dc479a2e9b27676cb8a00f5d9034f3769af6310d90d6996dd3f5b4bc6569dea
5
+ SHA512:
6
+ metadata.gz: a6b5cc22477ff63ce321eeb3947e6200753c6960dc04e510f5e3d08aff750d87511e42668e48e154bc977dd123b9485e85e31eb9ca0c62ee315af3ada929dfd1
7
+ data.tar.gz: 59107143c3c2f59e25685e638dc61d2377cd9498cf7ef942dc43f3001b998ea73a218bdc64cbb2318859fb25bc8e8ec691ce688e9aaa7e6b17d76ccd5a746a30
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright Chuck Smith
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # SolidQueueWeb
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/solid_queue_web.svg)](https://rubygems.org/gems/solid_queue_web)
4
+
5
+ A read-only Rails engine that mounts a monitoring dashboard for [Solid Queue](https://github.com/rails/solid_queue). View queues, inspect jobs by status, and browse failed executions — all without leaving your app.
6
+
7
+ ## Features
8
+
9
+ - **Dashboard** — stat cards showing counts for ready, scheduled, running, blocked, and failed jobs, plus queues and processes
10
+ - **Queues** — all queues sorted by name
11
+ - **Jobs** — filterable by status (ready, scheduled, claimed, blocked, failed) and by queue
12
+ - **Failed jobs** — list of failed executions with error details
13
+ - No external CSS framework — works out of the box
14
+
15
+ ## Installation
16
+
17
+ Add to your application's Gemfile:
18
+
19
+ ```ruby
20
+ gem "solid_queue_web"
21
+ ```
22
+
23
+ Then run:
24
+
25
+ ```bash
26
+ bundle install
27
+ ```
28
+
29
+ ## Mounting the engine
30
+
31
+ Add to your `config/routes.rb`:
32
+
33
+ ```ruby
34
+ mount SolidQueueWeb::Engine, at: "/jobs"
35
+ ```
36
+
37
+ The dashboard will be available at `/jobs`.
38
+
39
+ ## Authentication
40
+
41
+ The engine ships with no authentication by default. Add a block to an initializer (e.g. `config/initializers/solid_queue_web.rb`) to protect the dashboard:
42
+
43
+ ```ruby
44
+ SolidQueueWeb.authenticate do
45
+ # Called in the context of ApplicationController — use any helper available there.
46
+ # Return a truthy value to allow access, falsy to deny (triggers HTTP Basic prompt).
47
+ current_user&.admin?
48
+ end
49
+ ```
50
+
51
+ HTTP Basic authentication is used as a fallback when the block returns falsy.
52
+
53
+ ## Requirements
54
+
55
+ - Ruby >= 3.3
56
+ - Rails >= 8.1.3
57
+ - solid_queue >= 1.0
58
+
59
+ ## Contributing
60
+
61
+ Bug reports and pull requests are welcome on [GitHub](https://github.com/eclectic-coding/solid_queue_web).
62
+
63
+ ## License
64
+
65
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,36 @@
1
+ require "bundler/setup"
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rubocop/rake_task"
5
+ require "rspec/core/rake_task"
6
+
7
+ RuboCop::RakeTask.new
8
+ RSpec::Core::RakeTask.new(:spec)
9
+
10
+ task default: [ :rubocop, :spec ]
11
+
12
+ namespace :dev do
13
+ def dummy_env
14
+ ENV["RAILS_ENV"] = "development"
15
+ require File.expand_path("spec/dummy/config/environment", __dir__)
16
+ end
17
+
18
+ desc "Create and migrate the dummy app development database"
19
+ task :setup do
20
+ dummy_env
21
+ ActiveRecord::Tasks::DatabaseTasks.root = File.expand_path("spec/dummy", __dir__)
22
+ db_config = ActiveRecord::Base.configurations.find_db_config("development")
23
+ ActiveRecord::Tasks::DatabaseTasks.create(db_config)
24
+ load File.expand_path("spec/dummy/db/schema.rb", __dir__)
25
+ puts "Development database ready."
26
+ end
27
+
28
+ desc "Seed the dummy app development database with fake data"
29
+ task :seed do
30
+ dummy_env
31
+ load File.expand_path("spec/dummy/db/seeds.rb", __dir__)
32
+ end
33
+
34
+ desc "Reset and reseed the dummy app development database"
35
+ task reset: [ :setup, :seed ]
36
+ end
@@ -0,0 +1,294 @@
1
+ /*
2
+ *= require_self
3
+ */
4
+
5
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
6
+
7
+ :root {
8
+ --bg: #f8f9fa;
9
+ --surface: #ffffff;
10
+ --border: #dee2e6;
11
+ --text: #212529;
12
+ --muted: #6c757d;
13
+ --primary: #0d6efd;
14
+ --danger: #dc3545;
15
+ --warning: #fd7e14;
16
+ --success: #198754;
17
+ --info: #0dcaf0;
18
+ --purple: #6f42c1;
19
+ }
20
+
21
+ body {
22
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
23
+ font-size: 14px;
24
+ background: var(--bg);
25
+ color: var(--text);
26
+ line-height: 1.5;
27
+ }
28
+
29
+ /* Layout */
30
+ .sqd-header {
31
+ background: var(--surface);
32
+ border-bottom: 1px solid var(--border);
33
+ padding: 0 1.5rem;
34
+ display: flex;
35
+ align-items: center;
36
+ gap: 2rem;
37
+ height: 56px;
38
+ }
39
+
40
+ .sqd-header__title {
41
+ font-size: 16px;
42
+ font-weight: 600;
43
+ color: var(--text);
44
+ text-decoration: none;
45
+ }
46
+
47
+ .sqd-nav {
48
+ display: flex;
49
+ gap: 0.25rem;
50
+ list-style: none;
51
+ }
52
+
53
+ .sqd-nav a {
54
+ display: block;
55
+ padding: 0.35rem 0.75rem;
56
+ border-radius: 6px;
57
+ color: var(--muted);
58
+ text-decoration: none;
59
+ font-size: 13px;
60
+ font-weight: 500;
61
+ transition: background 0.1s, color 0.1s;
62
+ }
63
+
64
+ .sqd-nav a:hover,
65
+ .sqd-nav a.active {
66
+ background: var(--bg);
67
+ color: var(--text);
68
+ }
69
+
70
+ .sqd-main {
71
+ max-width: 1200px;
72
+ margin: 0 auto;
73
+ padding: 2rem 1.5rem;
74
+ }
75
+
76
+ .sqd-page-title {
77
+ font-size: 20px;
78
+ font-weight: 600;
79
+ margin-bottom: 1.5rem;
80
+ }
81
+
82
+ /* Flash notices */
83
+ .sqd-flash {
84
+ padding: 0.75rem 1rem;
85
+ border-radius: 6px;
86
+ margin-bottom: 1rem;
87
+ font-size: 13px;
88
+ }
89
+ .sqd-flash--notice { background: #d1e7dd; color: #0f5132; border: 1px solid #badbcc; }
90
+ .sqd-flash--alert { background: #f8d7da; color: #842029; border: 1px solid #f5c2c7; }
91
+
92
+ /* Stat cards */
93
+ .sqd-stats {
94
+ display: grid;
95
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
96
+ gap: 1rem;
97
+ margin-bottom: 2rem;
98
+ }
99
+
100
+ .sqd-stat {
101
+ background: var(--surface);
102
+ border: 1px solid var(--border);
103
+ border-radius: 8px;
104
+ padding: 1.25rem 1rem;
105
+ text-align: center;
106
+ }
107
+
108
+ .sqd-stat__value {
109
+ font-size: 28px;
110
+ font-weight: 700;
111
+ line-height: 1;
112
+ margin-bottom: 0.25rem;
113
+ }
114
+
115
+ .sqd-stat__label {
116
+ font-size: 12px;
117
+ color: var(--muted);
118
+ text-transform: uppercase;
119
+ letter-spacing: 0.05em;
120
+ }
121
+
122
+ .sqd-stat--ready .sqd-stat__value { color: var(--success); }
123
+ .sqd-stat--scheduled .sqd-stat__value { color: var(--info); }
124
+ .sqd-stat--claimed .sqd-stat__value { color: var(--primary); }
125
+ .sqd-stat--failed .sqd-stat__value { color: var(--danger); }
126
+ .sqd-stat--blocked .sqd-stat__value { color: var(--warning); }
127
+ .sqd-stat--queues .sqd-stat__value { color: var(--purple); }
128
+ .sqd-stat--processes .sqd-stat__value { color: var(--muted); }
129
+
130
+ /* Tables */
131
+ .sqd-card {
132
+ background: var(--surface);
133
+ border: 1px solid var(--border);
134
+ border-radius: 8px;
135
+ overflow: hidden;
136
+ }
137
+
138
+ .sqd-card__header {
139
+ padding: 0.875rem 1rem;
140
+ border-bottom: 1px solid var(--border);
141
+ display: flex;
142
+ align-items: center;
143
+ justify-content: space-between;
144
+ gap: 1rem;
145
+ }
146
+
147
+ .sqd-card__title {
148
+ font-size: 14px;
149
+ font-weight: 600;
150
+ }
151
+
152
+ table {
153
+ width: 100%;
154
+ border-collapse: collapse;
155
+ }
156
+
157
+ th {
158
+ padding: 0.625rem 1rem;
159
+ text-align: left;
160
+ font-size: 12px;
161
+ font-weight: 600;
162
+ color: var(--muted);
163
+ text-transform: uppercase;
164
+ letter-spacing: 0.05em;
165
+ border-bottom: 1px solid var(--border);
166
+ white-space: nowrap;
167
+ }
168
+
169
+ td {
170
+ padding: 0.75rem 1rem;
171
+ border-bottom: 1px solid var(--border);
172
+ vertical-align: middle;
173
+ }
174
+
175
+ tr:last-child td { border-bottom: none; }
176
+ tbody tr:hover { background: var(--bg); }
177
+
178
+ .sqd-empty {
179
+ text-align: center;
180
+ padding: 3rem 1rem;
181
+ color: var(--muted);
182
+ }
183
+
184
+ /* Badges */
185
+ .sqd-badge {
186
+ display: inline-block;
187
+ padding: 0.2em 0.55em;
188
+ border-radius: 4px;
189
+ font-size: 11px;
190
+ font-weight: 600;
191
+ line-height: 1;
192
+ text-transform: uppercase;
193
+ letter-spacing: 0.04em;
194
+ }
195
+
196
+ .sqd-badge--ready { background: #d1e7dd; color: #0f5132; }
197
+ .sqd-badge--scheduled { background: #cff4fc; color: #055160; }
198
+ .sqd-badge--claimed { background: #cfe2ff; color: #084298; }
199
+ .sqd-badge--failed { background: #f8d7da; color: #842029; }
200
+ .sqd-badge--blocked { background: #fff3cd; color: #664d03; }
201
+ .sqd-badge--paused { background: #e2e3e5; color: #41464b; }
202
+ .sqd-badge--running { background: #d1e7dd; color: #0f5132; }
203
+
204
+ /* Buttons */
205
+ .sqd-btn {
206
+ display: inline-flex;
207
+ align-items: center;
208
+ padding: 0.35rem 0.75rem;
209
+ border-radius: 5px;
210
+ font-size: 12px;
211
+ font-weight: 500;
212
+ text-decoration: none;
213
+ border: 1px solid transparent;
214
+ cursor: pointer;
215
+ transition: opacity 0.15s;
216
+ }
217
+ .sqd-btn:hover { opacity: 0.85; }
218
+ .sqd-btn--primary { background: var(--primary); color: #fff; border-color: var(--primary); }
219
+ .sqd-btn--danger { background: var(--danger); color: #fff; border-color: var(--danger); }
220
+ .sqd-btn--muted { background: var(--surface); color: var(--text); border-color: var(--border); }
221
+
222
+ /* Filters */
223
+ .sqd-filters {
224
+ display: flex;
225
+ gap: 0.5rem;
226
+ flex-wrap: wrap;
227
+ margin-bottom: 1rem;
228
+ }
229
+
230
+ .sqd-filters a {
231
+ padding: 0.35rem 0.875rem;
232
+ border-radius: 20px;
233
+ font-size: 12px;
234
+ font-weight: 500;
235
+ text-decoration: none;
236
+ border: 1px solid var(--border);
237
+ color: var(--muted);
238
+ background: var(--surface);
239
+ transition: all 0.1s;
240
+ }
241
+
242
+ .sqd-filters a:hover,
243
+ .sqd-filters a.active {
244
+ background: var(--primary);
245
+ border-color: var(--primary);
246
+ color: #fff;
247
+ }
248
+
249
+ /* Code / monospace */
250
+ .sqd-mono {
251
+ font-family: "SFMono-Regular", Menlo, Monaco, Consolas, monospace;
252
+ font-size: 12px;
253
+ }
254
+
255
+ .sqd-error-msg {
256
+ color: var(--danger);
257
+ font-size: 12px;
258
+ }
259
+
260
+ .sqd-truncate {
261
+ max-width: 320px;
262
+ overflow: hidden;
263
+ text-overflow: ellipsis;
264
+ white-space: nowrap;
265
+ }
266
+
267
+ /* Pagination (pagy v43 series-nav) */
268
+ nav.pagy {
269
+ display: flex;
270
+ justify-content: center;
271
+ gap: 0.25rem;
272
+ padding: 1rem;
273
+ list-style: none;
274
+ }
275
+
276
+ nav.pagy a {
277
+ display: inline-flex;
278
+ align-items: center;
279
+ justify-content: center;
280
+ min-width: 32px;
281
+ height: 32px;
282
+ padding: 0 0.5rem;
283
+ border-radius: 5px;
284
+ font-size: 13px;
285
+ text-decoration: none;
286
+ border: 1px solid var(--border);
287
+ color: var(--text);
288
+ background: var(--surface);
289
+ }
290
+
291
+ nav.pagy a:hover:not([aria-disabled="true"]) { background: var(--bg); }
292
+ nav.pagy a[aria-current="page"] { background: var(--primary); color: #fff; border-color: var(--primary); }
293
+ nav.pagy a[role="separator"],
294
+ nav.pagy a[aria-disabled="true"] { color: var(--muted); cursor: default; }
@@ -0,0 +1,17 @@
1
+ module SolidQueueWeb
2
+ class ApplicationController < ActionController::Base
3
+ before_action :authenticate!
4
+
5
+ private
6
+
7
+ def authenticate!
8
+ return unless (auth = SolidQueueWeb.authenticate)
9
+
10
+ instance_exec(&auth) || request_basic_auth
11
+ end
12
+
13
+ def request_basic_auth
14
+ request_http_basic_authentication("Solid Queue Dashboard")
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ module SolidQueueWeb
2
+ class DashboardController < ApplicationController
3
+ def index
4
+ @stats = {
5
+ ready: SolidQueue::ReadyExecution.count,
6
+ scheduled: SolidQueue::ScheduledExecution.count,
7
+ claimed: SolidQueue::ClaimedExecution.count,
8
+ failed: SolidQueue::FailedExecution.count,
9
+ blocked: SolidQueue::BlockedExecution.count,
10
+ queues: SolidQueue::Job.select(:queue_name).distinct.count,
11
+ processes: SolidQueue::Process.count
12
+ }
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,10 @@
1
+ module SolidQueueWeb
2
+ class FailedJobsController < ApplicationController
3
+ def index
4
+ @failed_jobs = SolidQueue::FailedExecution
5
+ .includes(:job)
6
+ .order(created_at: :desc)
7
+ .limit(100)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,21 @@
1
+ module SolidQueueWeb
2
+ class JobsController < ApplicationController
3
+ STATUSES = %w[ready scheduled claimed blocked failed].freeze
4
+
5
+ def index
6
+ @status = params[:status].presence_in(STATUSES) || "ready"
7
+ @queue = params[:queue].presence
8
+
9
+ @jobs = case @status
10
+ when "ready" then SolidQueue::ReadyExecution.includes(:job)
11
+ when "scheduled" then SolidQueue::ScheduledExecution.includes(:job)
12
+ when "claimed" then SolidQueue::ClaimedExecution.includes(:job)
13
+ when "blocked" then SolidQueue::BlockedExecution.includes(:job)
14
+ when "failed" then SolidQueue::FailedExecution.includes(:job)
15
+ end
16
+
17
+ @jobs = @jobs.where(jobs: { queue_name: @queue }) if @queue.present?
18
+ @jobs = @jobs.order(created_at: :desc).limit(100)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,7 @@
1
+ module SolidQueueWeb
2
+ class QueuesController < ApplicationController
3
+ def index
4
+ @queues = SolidQueue::Queue.all.sort_by(&:name)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,8 @@
1
+ module SolidQueueWeb
2
+ module ApplicationHelper
3
+ def inline_styles
4
+ css = SolidQueueWeb::Engine.root.join("app/assets/stylesheets/solid_queue_web/application.css").read
5
+ content_tag(:style, css.html_safe)
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,4 @@
1
+ module SolidQueueWeb
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,5 @@
1
+ module SolidQueueWeb
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,37 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Solid Queue Dashboard</title>
7
+ <%= csrf_meta_tags %>
8
+ <%= csp_meta_tag %>
9
+ <%= inline_styles %>
10
+ </head>
11
+ <body>
12
+
13
+ <header class="sqd-header">
14
+ <%= link_to "Solid Queue", root_path, class: "sqd-header__title" %>
15
+ <nav>
16
+ <ul class="sqd-nav">
17
+ <li><%= link_to "Dashboard", root_path, class: current_page?(root_path) ? "active" : "" %></li>
18
+ <li><%= link_to "Queues", queues_path, class: current_page?(queues_path) ? "active" : "" %></li>
19
+ <li><%= link_to "Jobs", jobs_path, class: current_page?(jobs_path) ? "active" : "" %></li>
20
+ <li><%= link_to "Failed", failed_jobs_path, class: current_page?(failed_jobs_path) ? "active" : "" %></li>
21
+ </ul>
22
+ </nav>
23
+ </header>
24
+
25
+ <main class="sqd-main">
26
+ <% if notice.present? %>
27
+ <div class="sqd-flash sqd-flash--notice"><%= notice %></div>
28
+ <% end %>
29
+ <% if alert.present? %>
30
+ <div class="sqd-flash sqd-flash--alert"><%= alert %></div>
31
+ <% end %>
32
+
33
+ <%= yield %>
34
+ </main>
35
+
36
+ </body>
37
+ </html>
@@ -0,0 +1,60 @@
1
+ <h1 class="sqd-page-title">Dashboard</h1>
2
+
3
+ <div class="sqd-stats">
4
+ <div class="sqd-stat sqd-stat--ready">
5
+ <div class="sqd-stat__value"><%= @stats[:ready] %></div>
6
+ <div class="sqd-stat__label">Ready</div>
7
+ </div>
8
+ <div class="sqd-stat sqd-stat--scheduled">
9
+ <div class="sqd-stat__value"><%= @stats[:scheduled] %></div>
10
+ <div class="sqd-stat__label">Scheduled</div>
11
+ </div>
12
+ <div class="sqd-stat sqd-stat--claimed">
13
+ <div class="sqd-stat__value"><%= @stats[:claimed] %></div>
14
+ <div class="sqd-stat__label">Running</div>
15
+ </div>
16
+ <div class="sqd-stat sqd-stat--blocked">
17
+ <div class="sqd-stat__value"><%= @stats[:blocked] %></div>
18
+ <div class="sqd-stat__label">Blocked</div>
19
+ </div>
20
+ <div class="sqd-stat sqd-stat--failed">
21
+ <div class="sqd-stat__value"><%= @stats[:failed] %></div>
22
+ <div class="sqd-stat__label">Failed</div>
23
+ </div>
24
+ <div class="sqd-stat sqd-stat--queues">
25
+ <div class="sqd-stat__value"><%= @stats[:queues] %></div>
26
+ <div class="sqd-stat__label">Queues</div>
27
+ </div>
28
+ <div class="sqd-stat sqd-stat--processes">
29
+ <div class="sqd-stat__value"><%= @stats[:processes] %></div>
30
+ <div class="sqd-stat__label">Processes</div>
31
+ </div>
32
+ </div>
33
+
34
+ <div style="display:grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
35
+ <div class="sqd-card">
36
+ <div class="sqd-card__header">
37
+ <span class="sqd-card__title">Quick Links</span>
38
+ </div>
39
+ <div style="padding: 1rem; display: flex; flex-direction: column; gap: 0.5rem;">
40
+ <%= link_to "View all ready jobs", jobs_path(status: "ready"), class: "sqd-btn sqd-btn--muted" %>
41
+ <%= link_to "View scheduled jobs", jobs_path(status: "scheduled"), class: "sqd-btn sqd-btn--muted" %>
42
+ <%= link_to "View failed jobs", failed_jobs_path, class: "sqd-btn sqd-btn--muted" %>
43
+ <%= link_to "Manage queues", queues_path, class: "sqd-btn sqd-btn--muted" %>
44
+ </div>
45
+ </div>
46
+
47
+ <% if @stats[:failed] > 0 %>
48
+ <div class="sqd-card">
49
+ <div class="sqd-card__header">
50
+ <span class="sqd-card__title">Attention Required</span>
51
+ </div>
52
+ <div style="padding: 1rem;">
53
+ <p style="color: var(--danger); margin-bottom: 0.75rem;">
54
+ <%= pluralize(@stats[:failed], "failed job") %> need attention.
55
+ </p>
56
+ <%= link_to "Review failed jobs →", failed_jobs_path, class: "sqd-btn sqd-btn--danger" %>
57
+ </div>
58
+ </div>
59
+ <% end %>
60
+ </div>
@@ -0,0 +1,37 @@
1
+ <h1 class="sqd-page-title">Failed Jobs</h1>
2
+
3
+ <div class="sqd-card">
4
+ <% if @failed_jobs.empty? %>
5
+ <div class="sqd-empty">No failed jobs. All clear!</div>
6
+ <% else %>
7
+ <table>
8
+ <thead>
9
+ <tr>
10
+ <th>Job Class</th>
11
+ <th>Queue</th>
12
+ <th>Error</th>
13
+ <th>Failed At</th>
14
+ </tr>
15
+ </thead>
16
+ <tbody>
17
+ <% @failed_jobs.each do |execution| %>
18
+ <% job = execution.job %>
19
+ <tr>
20
+ <td><%= job.class_name %></td>
21
+ <td class="sqd-mono"><%= job.queue_name %></td>
22
+ <td>
23
+ <% if execution.exception_class.present? %>
24
+ <div class="sqd-error-msg sqd-truncate" title="<%= execution.exception_class %>: <%= execution.message %>">
25
+ <strong><%= execution.exception_class %></strong>: <%= execution.message %>
26
+ </div>
27
+ <% else %>
28
+ <span class="sqd-muted">—</span>
29
+ <% end %>
30
+ </td>
31
+ <td class="sqd-mono"><%= execution.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
32
+ </tr>
33
+ <% end %>
34
+ </tbody>
35
+ </table>
36
+ <% end %>
37
+ </div>
@@ -0,0 +1,54 @@
1
+ <h1 class="sqd-page-title">Jobs</h1>
2
+
3
+ <div class="sqd-filters">
4
+ <%= link_to "Ready", jobs_path(status: "ready", queue: @queue), class: @status == "ready" ? "active" : "" %>
5
+ <%= link_to "Scheduled", jobs_path(status: "scheduled", queue: @queue), class: @status == "scheduled" ? "active" : "" %>
6
+ <%= link_to "Running", jobs_path(status: "claimed", queue: @queue), class: @status == "claimed" ? "active" : "" %>
7
+ <%= link_to "Blocked", jobs_path(status: "blocked", queue: @queue), class: @status == "blocked" ? "active" : "" %>
8
+ <%= link_to "Failed", jobs_path(status: "failed", queue: @queue), class: @status == "failed" ? "active" : "" %>
9
+ </div>
10
+
11
+ <div class="sqd-card">
12
+ <% if @jobs.empty? %>
13
+ <div class="sqd-empty">No <%= @status %> jobs.</div>
14
+ <% else %>
15
+ <table>
16
+ <thead>
17
+ <tr>
18
+ <th>Job Class</th>
19
+ <th>Queue</th>
20
+ <th>Priority</th>
21
+ <th>Scheduled At</th>
22
+ <th>Enqueued At</th>
23
+ </tr>
24
+ </thead>
25
+ <tbody>
26
+ <% @jobs.each do |execution| %>
27
+ <% job = execution.job %>
28
+ <tr>
29
+ <td>
30
+ <span class="sqd-badge sqd-badge--<%= @status %>"><%= @status %></span>
31
+ <span style="margin-left: 0.5rem;"><%= job.class_name %></span>
32
+ </td>
33
+ <td>
34
+ <%= link_to job.queue_name, jobs_path(status: @status, queue: job.queue_name),
35
+ class: "sqd-mono", style: "color: inherit;" %>
36
+ </td>
37
+ <td><%= job.priority %></td>
38
+ <td class="sqd-mono">
39
+ <%= job.scheduled_at ? job.scheduled_at.strftime("%Y-%m-%d %H:%M:%S") : "—" %>
40
+ </td>
41
+ <td class="sqd-mono"><%= job.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
42
+ </tr>
43
+ <% end %>
44
+ </tbody>
45
+ </table>
46
+ <% end %>
47
+ </div>
48
+
49
+ <% if @queue.present? %>
50
+ <p style="margin-top: 0.75rem; font-size: 13px; color: var(--muted);">
51
+ Filtering by queue: <strong><%= @queue %></strong> &mdash;
52
+ <%= link_to "Clear filter", jobs_path(status: @status) %>
53
+ </p>
54
+ <% end %>
@@ -0,0 +1,34 @@
1
+ <h1 class="sqd-page-title">Queues</h1>
2
+
3
+ <div class="sqd-card">
4
+ <% if @queues.empty? %>
5
+ <div class="sqd-empty">No queues found.</div>
6
+ <% else %>
7
+ <table>
8
+ <thead>
9
+ <tr>
10
+ <th>Name</th>
11
+ <th>Size</th>
12
+ <th>Latency</th>
13
+ <th>Status</th>
14
+ </tr>
15
+ </thead>
16
+ <tbody>
17
+ <% @queues.each do |queue| %>
18
+ <tr>
19
+ <td class="sqd-mono"><%= queue.name %></td>
20
+ <td><%= queue.size %></td>
21
+ <td><%= queue.human_latency %></td>
22
+ <td>
23
+ <% if queue.paused? %>
24
+ <span class="sqd-badge sqd-badge--paused">Paused</span>
25
+ <% else %>
26
+ <span class="sqd-badge sqd-badge--running">Running</span>
27
+ <% end %>
28
+ </td>
29
+ </tr>
30
+ <% end %>
31
+ </tbody>
32
+ </table>
33
+ <% end %>
34
+ </div>
data/config/routes.rb ADDED
@@ -0,0 +1,7 @@
1
+ SolidQueueWeb::Engine.routes.draw do
2
+ root to: "dashboard#index"
3
+
4
+ resources :queues, only: [ :index ]
5
+ resources :jobs, only: [ :index ]
6
+ resources :failed_jobs, only: [ :index ]
7
+ end
@@ -0,0 +1,7 @@
1
+ require "solid_queue"
2
+
3
+ module SolidQueueWeb
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace SolidQueueWeb
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ module SolidQueueWeb
2
+ VERSION = "0.2.0"
3
+ end
@@ -0,0 +1,11 @@
1
+ require "solid_queue_web/version"
2
+ require "solid_queue_web/engine"
3
+
4
+ module SolidQueueWeb
5
+ class << self
6
+ def authenticate(&block)
7
+ @authenticate = block if block_given?
8
+ @authenticate
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :solid_queue_web do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,94 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: solid_queue_web
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Chuck Smith
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 8.1.3
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 8.1.3
26
+ - !ruby/object:Gem::Dependency
27
+ name: solid_queue
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '1.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '1.0'
40
+ description: Mount SolidQueueWeb in any Rails app using Solid Queue to get a real-time
41
+ read-only view of your queues, jobs by status, and failed executions.
42
+ email:
43
+ - eclectic-coding@users.noreply.github.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - MIT-LICENSE
49
+ - README.md
50
+ - Rakefile
51
+ - app/assets/stylesheets/solid_queue_web/application.css
52
+ - app/controllers/solid_queue_web/application_controller.rb
53
+ - app/controllers/solid_queue_web/dashboard_controller.rb
54
+ - app/controllers/solid_queue_web/failed_jobs_controller.rb
55
+ - app/controllers/solid_queue_web/jobs_controller.rb
56
+ - app/controllers/solid_queue_web/queues_controller.rb
57
+ - app/helpers/solid_queue_web/application_helper.rb
58
+ - app/jobs/solid_queue_web/application_job.rb
59
+ - app/models/solid_queue_web/application_record.rb
60
+ - app/views/layouts/solid_queue_web/application.html.erb
61
+ - app/views/solid_queue_web/dashboard/index.html.erb
62
+ - app/views/solid_queue_web/failed_jobs/index.html.erb
63
+ - app/views/solid_queue_web/jobs/index.html.erb
64
+ - app/views/solid_queue_web/queues/index.html.erb
65
+ - config/routes.rb
66
+ - lib/solid_queue_web.rb
67
+ - lib/solid_queue_web/engine.rb
68
+ - lib/solid_queue_web/version.rb
69
+ - lib/tasks/solid_queue_web_tasks.rake
70
+ homepage: https://github.com/eclectic-coding/solid_queue_web
71
+ licenses:
72
+ - MIT
73
+ metadata:
74
+ homepage_uri: https://github.com/eclectic-coding/solid_queue_web
75
+ source_code_uri: https://github.com/eclectic-coding/solid_queue_web
76
+ changelog_uri: https://github.com/eclectic-coding/solid_queue_web/blob/main/CHANGELOG.md
77
+ rdoc_options: []
78
+ require_paths:
79
+ - lib
80
+ required_ruby_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: '3.3'
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ requirements: []
91
+ rubygems_version: 4.0.10
92
+ specification_version: 4
93
+ summary: A read-only Rails engine dashboard for monitoring Solid Queue jobs.
94
+ test_files: []