solid_queue_web 0.2.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6c49ff8a8bc89fae42038615cbc586d01e108db9549af1184d77623c51c9fb90
4
- data.tar.gz: 5dc479a2e9b27676cb8a00f5d9034f3769af6310d90d6996dd3f5b4bc6569dea
3
+ metadata.gz: 7de0f3d542cd6cd6d162d4069eb5ca373e5ee6fca1738937c2a00ac3f9560956
4
+ data.tar.gz: f8bb5e17912f2e87287aba8a5e6a057eb64adafbea6934aecac8b2559bc17290
5
5
  SHA512:
6
- metadata.gz: a6b5cc22477ff63ce321eeb3947e6200753c6960dc04e510f5e3d08aff750d87511e42668e48e154bc977dd123b9485e85e31eb9ca0c62ee315af3ada929dfd1
7
- data.tar.gz: 59107143c3c2f59e25685e638dc61d2377cd9498cf7ef942dc43f3001b998ea73a218bdc64cbb2318859fb25bc8e8ec691ce688e9aaa7e6b17d76ccd5a746a30
6
+ metadata.gz: ad6b13a2fe53124e78d5365d112eeb1e8811db4a5ba2be56e0e1d5fb4ac7da13c6f503cc0b71c68e4c295119764d3b4211767609a10efeca2ea8abc472dc51ef
7
+ data.tar.gz: 6d046fa5771188fe639ad72960ec4d3ab55916fc4e8d702e68d21b838ee5bdf9b25e6eda641b051ed6d10ecfec69dcdb4720841f170ba02b834d23e2113d516a
data/README.md CHANGED
@@ -2,14 +2,17 @@
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/solid_queue_web.svg)](https://rubygems.org/gems/solid_queue_web)
4
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.
5
+ A Rails engine that mounts a monitoring dashboard for [Solid Queue](https://github.com/rails/solid_queue). View queues, inspect jobs by status, browse failed executions, and take action — all without leaving your app.
6
6
 
7
7
  ## Features
8
8
 
9
9
  - **Dashboard** — stat cards showing counts for ready, scheduled, running, blocked, and failed jobs, plus queues and processes
10
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
11
+ - **Jobs** — filterable by status (ready, scheduled, claimed, blocked, failed) and by queue; discard individual or all jobs
12
+ - **Failed jobs** — list of failed executions with error details; retry or discard individually or in bulk
13
+ - **Job detail** — full arguments, timestamps, and error backtrace; action buttons based on job status
14
+ - **Queue management** — pause and resume individual queues
15
+ - **Processes** — workers, dispatchers, and supervisors with heartbeat health status
13
16
  - No external CSS framework — works out of the box
14
17
 
15
18
  ## Installation
@@ -76,9 +76,21 @@ body {
76
76
  .sqd-page-title {
77
77
  font-size: 20px;
78
78
  font-weight: 600;
79
+ margin-bottom: 0;
80
+ }
81
+
82
+ .sqd-page-header {
83
+ display: flex;
84
+ align-items: center;
85
+ justify-content: space-between;
79
86
  margin-bottom: 1.5rem;
80
87
  }
81
88
 
89
+ .sqd-actions {
90
+ display: flex;
91
+ gap: 0.5rem;
92
+ }
93
+
82
94
  /* Flash notices */
83
95
  .sqd-flash {
84
96
  padding: 0.75rem 1rem;
@@ -198,8 +210,15 @@ tbody tr:hover { background: var(--bg); }
198
210
  .sqd-badge--claimed { background: #cfe2ff; color: #084298; }
199
211
  .sqd-badge--failed { background: #f8d7da; color: #842029; }
200
212
  .sqd-badge--blocked { background: #fff3cd; color: #664d03; }
201
- .sqd-badge--paused { background: #e2e3e5; color: #41464b; }
202
- .sqd-badge--running { background: #d1e7dd; color: #0f5132; }
213
+ .sqd-badge--paused { background: #e2e3e5; color: #41464b; }
214
+ .sqd-badge--running { background: #d1e7dd; color: #0f5132; }
215
+ .sqd-badge--supervisor { background: #e0d7f5; color: #4a2c8a; }
216
+ .sqd-badge--worker { background: #d1e7dd; color: #0f5132; }
217
+ .sqd-badge--dispatcher { background: #cff4fc; color: #055160; }
218
+
219
+ .sqd-process-meta { font-size: 12px; color: var(--muted); }
220
+ .sqd-process-meta span + span::before { content: " · "; }
221
+ .sqd-muted-text { color: var(--muted); font-size: 13px; }
203
222
 
204
223
  /* Buttons */
205
224
  .sqd-btn {
@@ -218,6 +237,10 @@ tbody tr:hover { background: var(--bg); }
218
237
  .sqd-btn--primary { background: var(--primary); color: #fff; border-color: var(--primary); }
219
238
  .sqd-btn--danger { background: var(--danger); color: #fff; border-color: var(--danger); }
220
239
  .sqd-btn--muted { background: var(--surface); color: var(--text); border-color: var(--border); }
240
+ .sqd-btn--sm { padding: 0.2rem 0.55rem; font-size: 11px; }
241
+
242
+ .sqd-row-actions { white-space: nowrap; text-align: right; width: 1%; }
243
+ .sqd-row-actions form { display: inline; margin-left: 0.25rem; }
221
244
 
222
245
  /* Filters */
223
246
  .sqd-filters {
@@ -291,4 +314,71 @@ nav.pagy a {
291
314
  nav.pagy a:hover:not([aria-disabled="true"]) { background: var(--bg); }
292
315
  nav.pagy a[aria-current="page"] { background: var(--primary); color: #fff; border-color: var(--primary); }
293
316
  nav.pagy a[role="separator"],
294
- nav.pagy a[aria-disabled="true"] { color: var(--muted); cursor: default; }
317
+ nav.pagy a[aria-disabled="true"] { color: var(--muted); cursor: default; }
318
+
319
+ /* Job detail page */
320
+ .sqd-breadcrumb {
321
+ font-size: 12px;
322
+ color: var(--muted);
323
+ margin-bottom: 0.25rem;
324
+ }
325
+
326
+ .sqd-breadcrumb a { color: var(--muted); text-decoration: none; }
327
+ .sqd-breadcrumb a:hover { color: var(--text); }
328
+
329
+ .sqd-detail-grid {
330
+ display: grid;
331
+ grid-template-columns: 1fr 1fr;
332
+ gap: 1.5rem;
333
+ }
334
+
335
+ @media (max-width: 768px) {
336
+ .sqd-detail-grid { grid-template-columns: 1fr; }
337
+ }
338
+
339
+ .sqd-detail-section { padding: 1.25rem; }
340
+
341
+ .sqd-section-title {
342
+ font-size: 13px;
343
+ font-weight: 600;
344
+ text-transform: uppercase;
345
+ letter-spacing: 0.05em;
346
+ color: var(--muted);
347
+ margin-bottom: 1rem;
348
+ }
349
+
350
+ .sqd-section-title--danger { color: var(--danger); }
351
+
352
+ .sqd-dl {
353
+ display: grid;
354
+ grid-template-columns: auto 1fr;
355
+ gap: 0.5rem 1.5rem;
356
+ font-size: 13px;
357
+ }
358
+
359
+ .sqd-dl dt { color: var(--muted); white-space: nowrap; }
360
+ .sqd-dl dd { word-break: break-all; }
361
+
362
+ .sqd-pre {
363
+ font-family: monospace;
364
+ font-size: 12px;
365
+ background: var(--bg);
366
+ border: 1px solid var(--border);
367
+ border-radius: 5px;
368
+ padding: 0.75rem;
369
+ overflow-x: auto;
370
+ white-space: pre-wrap;
371
+ word-break: break-word;
372
+ max-height: 400px;
373
+ overflow-y: auto;
374
+ }
375
+
376
+ .sqd-pre--muted { color: var(--muted); }
377
+
378
+ .sqd-error-header {
379
+ font-size: 13px;
380
+ padding: 0.5rem 0.75rem;
381
+ background: #f8d7da;
382
+ color: #842029;
383
+ border-radius: 5px;
384
+ }
@@ -1,5 +1,7 @@
1
1
  module SolidQueueWeb
2
2
  class ApplicationController < ActionController::Base
3
+ include Pagy::Method
4
+
3
5
  before_action :authenticate!
4
6
 
5
7
  private
@@ -1,10 +1,38 @@
1
1
  module SolidQueueWeb
2
2
  class FailedJobsController < ApplicationController
3
3
  def index
4
- @failed_jobs = SolidQueue::FailedExecution
5
- .includes(:job)
6
- .order(created_at: :desc)
7
- .limit(100)
4
+ @pagy, @failed_jobs = pagy(
5
+ SolidQueue::FailedExecution.includes(:job).order(created_at: :desc)
6
+ )
7
+ end
8
+
9
+ def retry
10
+ execution = SolidQueue::FailedExecution.find(params[:id])
11
+ execution.retry
12
+ redirect_to failed_jobs_path, notice: "Job queued for retry."
13
+ rescue => e
14
+ redirect_to failed_jobs_path, alert: "Could not retry job: #{e.message}"
15
+ end
16
+
17
+ def destroy
18
+ execution = SolidQueue::FailedExecution.find(params[:id])
19
+ execution.discard
20
+ redirect_to failed_jobs_path, notice: "Job discarded."
21
+ rescue => e
22
+ redirect_to failed_jobs_path, alert: "Could not discard job: #{e.message}"
23
+ end
24
+
25
+ def retry_all
26
+ executions = SolidQueue::FailedExecution.includes(:job).to_a
27
+ jobs = executions.map(&:job)
28
+ SolidQueue::FailedExecution.retry_all(jobs)
29
+ redirect_to failed_jobs_path, notice: "#{jobs.size} #{"job".pluralize(jobs.size)} queued for retry."
30
+ end
31
+
32
+ def discard_all
33
+ count = SolidQueue::FailedExecution.count
34
+ SolidQueue::FailedExecution.discard_all_in_batches
35
+ redirect_to failed_jobs_path, notice: "#{count} #{"job".pluralize(count)} discarded."
8
36
  end
9
37
  end
10
38
  end
@@ -1,6 +1,7 @@
1
1
  module SolidQueueWeb
2
2
  class JobsController < ApplicationController
3
3
  STATUSES = %w[ready scheduled claimed blocked failed].freeze
4
+ DISCARDABLE = %w[ready scheduled blocked].freeze
4
5
 
5
6
  def index
6
7
  @status = params[:status].presence_in(STATUSES) || "ready"
@@ -15,7 +16,59 @@ module SolidQueueWeb
15
16
  end
16
17
 
17
18
  @jobs = @jobs.where(jobs: { queue_name: @queue }) if @queue.present?
18
- @jobs = @jobs.order(created_at: :desc).limit(100)
19
+ @pagy, @jobs = pagy(@jobs.order(created_at: :desc))
20
+ end
21
+
22
+ def show
23
+ @job = SolidQueue::Job
24
+ .includes(:ready_execution, :scheduled_execution, :claimed_execution, :blocked_execution, :failed_execution)
25
+ .find(params[:id])
26
+ @failed_execution = @job.failed_execution
27
+ @execution_status = derive_status(@job)
28
+ end
29
+
30
+ def destroy
31
+ execution = execution_model_for!(params[:status]).find(params[:id])
32
+ execution.discard
33
+ redirect_to jobs_path(status: params[:status], queue: params[:queue]), notice: "Job discarded."
34
+ rescue ArgumentError => e
35
+ redirect_to jobs_path(status: params[:status], queue: params[:queue]), alert: e.message
36
+ rescue => e
37
+ redirect_to jobs_path(status: params[:status], queue: params[:queue]), alert: "Could not discard job: #{e.message}"
38
+ end
39
+
40
+ def discard_all
41
+ model = execution_model_for!(params[:status])
42
+ scope = model.includes(:job)
43
+ scope = scope.where(jobs: { queue_name: params[:queue] }) if params[:queue].present?
44
+ jobs = scope.map(&:job)
45
+ model.discard_all_from_jobs(jobs)
46
+ redirect_to jobs_path(status: params[:status], queue: params[:queue]),
47
+ notice: "#{jobs.size} #{"job".pluralize(jobs.size)} discarded."
48
+ rescue ArgumentError => e
49
+ redirect_to jobs_path(status: params[:status], queue: params[:queue]), alert: e.message
50
+ rescue => e
51
+ redirect_to jobs_path(status: params[:status], queue: params[:queue]), alert: "Could not discard jobs: #{e.message}"
52
+ end
53
+
54
+ private
55
+
56
+ def derive_status(job)
57
+ return "failed" if job.failed_execution.present?
58
+ return "claimed" if job.claimed_execution.present?
59
+ return "blocked" if job.blocked_execution.present?
60
+ return "ready" if job.ready_execution.present?
61
+ return "scheduled" if job.scheduled_execution.present?
62
+ "finished"
63
+ end
64
+
65
+ def execution_model_for!(status)
66
+ case status
67
+ when "ready" then SolidQueue::ReadyExecution
68
+ when "scheduled" then SolidQueue::ScheduledExecution
69
+ when "blocked" then SolidQueue::BlockedExecution
70
+ else raise ArgumentError, "Cannot discard #{status} jobs from this page."
71
+ end
19
72
  end
20
73
  end
21
74
  end
@@ -0,0 +1,7 @@
1
+ module SolidQueueWeb
2
+ class ProcessesController < ApplicationController
3
+ def index
4
+ @processes = SolidQueue::Process.order(:kind, :name).to_a
5
+ end
6
+ end
7
+ end
@@ -3,5 +3,21 @@ module SolidQueueWeb
3
3
  def index
4
4
  @queues = SolidQueue::Queue.all.sort_by(&:name)
5
5
  end
6
+
7
+ def pause
8
+ queue = SolidQueue::Queue.find_by_name(params[:name])
9
+ queue.pause
10
+ redirect_to queues_path, notice: "Queue \"#{queue.name}\" paused."
11
+ rescue => e
12
+ redirect_to queues_path, alert: "Could not pause queue: #{e.message}"
13
+ end
14
+
15
+ def resume
16
+ queue = SolidQueue::Queue.find_by_name(params[:name])
17
+ queue.resume
18
+ redirect_to queues_path, notice: "Queue \"#{queue.name}\" resumed."
19
+ rescue => e
20
+ redirect_to queues_path, alert: "Could not resume queue: #{e.message}"
21
+ end
6
22
  end
7
23
  end
@@ -18,6 +18,7 @@
18
18
  <li><%= link_to "Queues", queues_path, class: current_page?(queues_path) ? "active" : "" %></li>
19
19
  <li><%= link_to "Jobs", jobs_path, class: current_page?(jobs_path) ? "active" : "" %></li>
20
20
  <li><%= link_to "Failed", failed_jobs_path, class: current_page?(failed_jobs_path) ? "active" : "" %></li>
21
+ <li><%= link_to "Processes", processes_path, class: current_page?(processes_path) ? "active" : "" %></li>
21
22
  </ul>
22
23
  </nav>
23
24
  </header>
@@ -1,4 +1,18 @@
1
- <h1 class="sqd-page-title">Failed Jobs</h1>
1
+ <div class="sqd-page-header">
2
+ <h1 class="sqd-page-title">Failed Jobs</h1>
3
+ <% if @failed_jobs.any? %>
4
+ <div class="sqd-actions">
5
+ <%= button_to "Retry All", retry_all_failed_jobs_path, method: :post, class: "sqd-btn sqd-btn--primary",
6
+ data: { confirm: "Retry all #{@failed_jobs.size} failed jobs?" } %>
7
+ <%= button_to "Discard All", discard_all_failed_jobs_path, method: :post, class: "sqd-btn sqd-btn--danger",
8
+ data: { confirm: "Discard all #{@failed_jobs.size} failed jobs? This cannot be undone." } %>
9
+ </div>
10
+ <% end %>
11
+ </div>
12
+
13
+ <% if @pagy.last > 1 %>
14
+ <%= @pagy.series_nav.html_safe %>
15
+ <% end %>
2
16
 
3
17
  <div class="sqd-card">
4
18
  <% if @failed_jobs.empty? %>
@@ -11,13 +25,14 @@
11
25
  <th>Queue</th>
12
26
  <th>Error</th>
13
27
  <th>Failed At</th>
28
+ <th></th>
14
29
  </tr>
15
30
  </thead>
16
31
  <tbody>
17
32
  <% @failed_jobs.each do |execution| %>
18
33
  <% job = execution.job %>
19
34
  <tr>
20
- <td><%= job.class_name %></td>
35
+ <td><%= link_to job.class_name, job_path(job) %></td>
21
36
  <td class="sqd-mono"><%= job.queue_name %></td>
22
37
  <td>
23
38
  <% if execution.exception_class.present? %>
@@ -25,10 +40,17 @@
25
40
  <strong><%= execution.exception_class %></strong>: <%= execution.message %>
26
41
  </div>
27
42
  <% else %>
28
- <span class="sqd-muted">—</span>
43
+ <span style="color:var(--muted)">—</span>
29
44
  <% end %>
30
45
  </td>
31
46
  <td class="sqd-mono"><%= execution.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
47
+ <td class="sqd-row-actions">
48
+ <%= button_to "Retry", retry_failed_job_path(execution), method: :post,
49
+ class: "sqd-btn sqd-btn--primary sqd-btn--sm" %>
50
+ <%= button_to "Discard", failed_job_path(execution), method: :delete,
51
+ class: "sqd-btn sqd-btn--danger sqd-btn--sm",
52
+ data: { confirm: "Discard this job?" } %>
53
+ </td>
32
54
  </tr>
33
55
  <% end %>
34
56
  </tbody>
@@ -1,4 +1,17 @@
1
- <h1 class="sqd-page-title">Jobs</h1>
1
+ <% discardable = SolidQueueWeb::JobsController::DISCARDABLE.include?(@status) %>
2
+
3
+ <div class="sqd-page-header">
4
+ <h1 class="sqd-page-title">Jobs</h1>
5
+ <% if discardable && @jobs.any? %>
6
+ <div class="sqd-actions">
7
+ <%= button_to "Discard All", discard_all_jobs_path,
8
+ method: :post,
9
+ params: { status: @status, queue: @queue },
10
+ class: "sqd-btn sqd-btn--danger",
11
+ data: { confirm: "Discard all #{@jobs.size} #{@status} jobs? This cannot be undone." } %>
12
+ </div>
13
+ <% end %>
14
+ </div>
2
15
 
3
16
  <div class="sqd-filters">
4
17
  <%= link_to "Ready", jobs_path(status: "ready", queue: @queue), class: @status == "ready" ? "active" : "" %>
@@ -20,6 +33,7 @@
20
33
  <th>Priority</th>
21
34
  <th>Scheduled At</th>
22
35
  <th>Enqueued At</th>
36
+ <% if discardable %><th></th><% end %>
23
37
  </tr>
24
38
  </thead>
25
39
  <tbody>
@@ -28,7 +42,7 @@
28
42
  <tr>
29
43
  <td>
30
44
  <span class="sqd-badge sqd-badge--<%= @status %>"><%= @status %></span>
31
- <span style="margin-left: 0.5rem;"><%= job.class_name %></span>
45
+ <%= link_to job.class_name, job_path(job), style: "margin-left: 0.5rem;" %>
32
46
  </td>
33
47
  <td>
34
48
  <%= link_to job.queue_name, jobs_path(status: @status, queue: job.queue_name),
@@ -39,6 +53,15 @@
39
53
  <%= job.scheduled_at ? job.scheduled_at.strftime("%Y-%m-%d %H:%M:%S") : "—" %>
40
54
  </td>
41
55
  <td class="sqd-mono"><%= job.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
56
+ <% if discardable %>
57
+ <td class="sqd-row-actions">
58
+ <%= button_to "Discard", job_path(execution),
59
+ method: :delete,
60
+ params: { status: @status, queue: @queue },
61
+ class: "sqd-btn sqd-btn--danger sqd-btn--sm",
62
+ data: { confirm: "Discard this job?" } %>
63
+ </td>
64
+ <% end %>
42
65
  </tr>
43
66
  <% end %>
44
67
  </tbody>
@@ -46,9 +69,13 @@
46
69
  <% end %>
47
70
  </div>
48
71
 
72
+ <% if @pagy.last > 1 %>
73
+ <%= @pagy.series_nav.html_safe %>
74
+ <% end %>
75
+
49
76
  <% if @queue.present? %>
50
77
  <p style="margin-top: 0.75rem; font-size: 13px; color: var(--muted);">
51
78
  Filtering by queue: <strong><%= @queue %></strong> &mdash;
52
79
  <%= link_to "Clear filter", jobs_path(status: @status) %>
53
80
  </p>
54
- <% end %>
81
+ <% end %>
@@ -0,0 +1,75 @@
1
+ <div class="sqd-page-header">
2
+ <div>
3
+ <div class="sqd-breadcrumb">
4
+ <%= link_to "Jobs", jobs_path(status: @execution_status) %> &rsaquo; Detail
5
+ </div>
6
+ <h1 class="sqd-page-title"><%= @job.class_name %></h1>
7
+ </div>
8
+
9
+ <div class="sqd-actions">
10
+ <% if @execution_status == "failed" && @failed_execution %>
11
+ <%= button_to "Retry", retry_failed_job_path(@failed_execution), method: :post,
12
+ class: "sqd-btn sqd-btn--primary" %>
13
+ <%= button_to "Discard", failed_job_path(@failed_execution), method: :delete,
14
+ class: "sqd-btn sqd-btn--danger",
15
+ data: { confirm: "Discard this job?" } %>
16
+ <% elsif SolidQueueWeb::JobsController::DISCARDABLE.include?(@execution_status) %>
17
+ <% execution = @job.public_send("#{@execution_status}_execution") %>
18
+ <% if execution %>
19
+ <%= button_to "Discard", job_path(execution),
20
+ method: :delete,
21
+ params: { status: @execution_status },
22
+ class: "sqd-btn sqd-btn--danger",
23
+ data: { confirm: "Discard this job?" } %>
24
+ <% end %>
25
+ <% end %>
26
+ </div>
27
+ </div>
28
+
29
+ <div class="sqd-detail-grid">
30
+ <div class="sqd-card sqd-detail-section">
31
+ <h2 class="sqd-section-title">Details</h2>
32
+ <dl class="sqd-dl">
33
+ <dt>Status</dt>
34
+ <dd><span class="sqd-badge sqd-badge--<%= @execution_status %>"><%= @execution_status %></span></dd>
35
+
36
+ <dt>Queue</dt>
37
+ <dd class="sqd-mono"><%= @job.queue_name %></dd>
38
+
39
+ <dt>Priority</dt>
40
+ <dd><%= @job.priority %></dd>
41
+
42
+ <dt>Active Job ID</dt>
43
+ <dd class="sqd-mono sqd-truncate" title="<%= @job.active_job_id %>"><%= @job.active_job_id %></dd>
44
+
45
+ <dt>Concurrency Key</dt>
46
+ <dd class="sqd-mono"><%= @job.concurrency_key.presence || "—" %></dd>
47
+
48
+ <dt>Enqueued At</dt>
49
+ <dd class="sqd-mono"><%= @job.created_at.strftime("%Y-%m-%d %H:%M:%S %Z") %></dd>
50
+
51
+ <dt>Scheduled At</dt>
52
+ <dd class="sqd-mono"><%= @job.scheduled_at ? @job.scheduled_at.strftime("%Y-%m-%d %H:%M:%S %Z") : "—" %></dd>
53
+
54
+ <dt>Finished At</dt>
55
+ <dd class="sqd-mono"><%= @job.finished_at ? @job.finished_at.strftime("%Y-%m-%d %H:%M:%S %Z") : "—" %></dd>
56
+ </dl>
57
+ </div>
58
+
59
+ <div class="sqd-card sqd-detail-section">
60
+ <h2 class="sqd-section-title">Arguments</h2>
61
+ <pre class="sqd-pre"><%= JSON.pretty_generate(@job.arguments) rescue @job.arguments.inspect %></pre>
62
+ </div>
63
+ </div>
64
+
65
+ <% if @failed_execution %>
66
+ <div class="sqd-card sqd-detail-section" style="margin-top: 1.5rem;">
67
+ <h2 class="sqd-section-title sqd-section-title--danger">Error</h2>
68
+ <p class="sqd-error-header">
69
+ <strong><%= @failed_execution.exception_class %></strong>: <%= @failed_execution.message %>
70
+ </p>
71
+ <% if @failed_execution.backtrace.present? %>
72
+ <pre class="sqd-pre sqd-pre--muted" style="margin-top: 0.75rem;"><%= Array(@failed_execution.backtrace).join("\n") %></pre>
73
+ <% end %>
74
+ </div>
75
+ <% end %>
@@ -0,0 +1,54 @@
1
+ <h1 class="sqd-page-title">Processes</h1>
2
+
3
+ <div class="sqd-card">
4
+ <% if @processes.empty? %>
5
+ <div class="sqd-empty">No processes registered.</div>
6
+ <% else %>
7
+ <table>
8
+ <thead>
9
+ <tr>
10
+ <th>Kind</th>
11
+ <th>Name</th>
12
+ <th>PID</th>
13
+ <th>Host</th>
14
+ <th>Details</th>
15
+ <th>Last Heartbeat</th>
16
+ <th>Status</th>
17
+ </tr>
18
+ </thead>
19
+ <tbody>
20
+ <% @processes.each do |process| %>
21
+ <% stale = process.last_heartbeat_at < SolidQueue.process_alive_threshold.ago %>
22
+ <tr>
23
+ <td><span class="sqd-badge sqd-badge--<%= process.kind.downcase %>"><%= process.kind %></span></td>
24
+ <td class="sqd-mono"><%= process.name %></td>
25
+ <td class="sqd-mono"><%= process.pid %></td>
26
+ <td class="sqd-mono"><%= process.hostname %></td>
27
+ <td class="sqd-process-meta">
28
+ <% meta = process.metadata || {} %>
29
+ <% if meta["queues"].present? %>
30
+ <span title="Queues"><%= Array(meta["queues"]).join(", ") %></span>
31
+ <% end %>
32
+ <% if meta["thread_count"].present? %>
33
+ <span class="sqd-muted-text"><%= meta["thread_count"] %> threads</span>
34
+ <% end %>
35
+ <% if meta["polling_interval"].present? %>
36
+ <span class="sqd-muted-text">every <%= meta["polling_interval"] %>s</span>
37
+ <% end %>
38
+ </td>
39
+ <td class="sqd-mono sqd-muted-text" title="<%= process.last_heartbeat_at %>">
40
+ <%= time_ago_in_words(process.last_heartbeat_at) %> ago
41
+ </td>
42
+ <td>
43
+ <% if stale %>
44
+ <span class="sqd-badge sqd-badge--failed">Stale</span>
45
+ <% else %>
46
+ <span class="sqd-badge sqd-badge--running">Healthy</span>
47
+ <% end %>
48
+ </td>
49
+ </tr>
50
+ <% end %>
51
+ </tbody>
52
+ </table>
53
+ <% end %>
54
+ </div>
@@ -11,6 +11,7 @@
11
11
  <th>Size</th>
12
12
  <th>Latency</th>
13
13
  <th>Status</th>
14
+ <th></th>
14
15
  </tr>
15
16
  </thead>
16
17
  <tbody>
@@ -26,6 +27,16 @@
26
27
  <span class="sqd-badge sqd-badge--running">Running</span>
27
28
  <% end %>
28
29
  </td>
30
+ <td class="sqd-row-actions">
31
+ <% if queue.paused? %>
32
+ <%= button_to "Resume", resume_queue_path(queue.name), method: :post,
33
+ class: "sqd-btn sqd-btn--primary sqd-btn--sm" %>
34
+ <% else %>
35
+ <%= button_to "Pause", pause_queue_path(queue.name), method: :post,
36
+ class: "sqd-btn sqd-btn--muted sqd-btn--sm",
37
+ data: { confirm: "Pause queue \"#{queue.name}\"?" } %>
38
+ <% end %>
39
+ </td>
29
40
  </tr>
30
41
  <% end %>
31
42
  </tbody>
data/config/routes.rb CHANGED
@@ -1,7 +1,25 @@
1
1
  SolidQueueWeb::Engine.routes.draw do
2
2
  root to: "dashboard#index"
3
3
 
4
- resources :queues, only: [ :index ]
5
- resources :jobs, only: [ :index ]
6
- resources :failed_jobs, only: [ :index ]
4
+ resources :processes, only: [ :index ]
5
+ resources :queues, only: [ :index ], param: :name do
6
+ member do
7
+ post :pause
8
+ post :resume
9
+ end
10
+ end
11
+ resources :jobs, path: "list", only: [ :index, :show, :destroy ] do
12
+ collection do
13
+ post :discard_all
14
+ end
15
+ end
16
+ resources :failed_jobs, only: [ :index, :destroy ] do
17
+ collection do
18
+ post :retry_all
19
+ post :discard_all
20
+ end
21
+ member do
22
+ post :retry
23
+ end
24
+ end
7
25
  end
@@ -1,7 +1,15 @@
1
1
  require "solid_queue"
2
+ require "pagy"
3
+ require "pagy/toolbox/paginators/method"
2
4
 
3
5
  module SolidQueueWeb
4
6
  class Engine < ::Rails::Engine
5
7
  isolate_namespace SolidQueueWeb
8
+
9
+ config.i18n.load_path += Gem.find_files("pagy/locales/en.yml")
10
+
11
+ initializer "solid_queue_web.pagy" do
12
+ Pagy::OPTIONS[:limit] = 25
13
+ end
6
14
  end
7
15
  end
@@ -1,3 +1,3 @@
1
1
  module SolidQueueWeb
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solid_queue_web
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuck Smith
@@ -37,8 +37,23 @@ dependencies:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
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.
40
+ - !ruby/object:Gem::Dependency
41
+ name: pagy
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '43.0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '43.0'
54
+ description: Mount SolidQueueWeb in any Rails app using Solid Queue to get a dashboard
55
+ for your queues, jobs by status, failed executions, and job actions (retry, discard)
56
+ — all without leaving your app.
42
57
  email:
43
58
  - eclectic-coding@users.noreply.github.com
44
59
  executables: []
@@ -53,6 +68,7 @@ files:
53
68
  - app/controllers/solid_queue_web/dashboard_controller.rb
54
69
  - app/controllers/solid_queue_web/failed_jobs_controller.rb
55
70
  - app/controllers/solid_queue_web/jobs_controller.rb
71
+ - app/controllers/solid_queue_web/processes_controller.rb
56
72
  - app/controllers/solid_queue_web/queues_controller.rb
57
73
  - app/helpers/solid_queue_web/application_helper.rb
58
74
  - app/jobs/solid_queue_web/application_job.rb
@@ -61,6 +77,8 @@ files:
61
77
  - app/views/solid_queue_web/dashboard/index.html.erb
62
78
  - app/views/solid_queue_web/failed_jobs/index.html.erb
63
79
  - app/views/solid_queue_web/jobs/index.html.erb
80
+ - app/views/solid_queue_web/jobs/show.html.erb
81
+ - app/views/solid_queue_web/processes/index.html.erb
64
82
  - app/views/solid_queue_web/queues/index.html.erb
65
83
  - config/routes.rb
66
84
  - lib/solid_queue_web.rb
@@ -90,5 +108,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
90
108
  requirements: []
91
109
  rubygems_version: 4.0.10
92
110
  specification_version: 4
93
- summary: A read-only Rails engine dashboard for monitoring Solid Queue jobs.
111
+ summary: A Rails engine dashboard for monitoring and managing Solid Queue jobs.
94
112
  test_files: []