solid_stack_web 1.0.0 → 1.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 +4 -4
- data/README.md +9 -17
- data/app/assets/stylesheets/solid_stack_web/_07_dashboard.css +4 -0
- data/app/assets/stylesheets/solid_stack_web/_09_detail.css +23 -0
- data/app/controllers/solid_stack_web/dashboard_controller.rb +1 -0
- data/app/controllers/solid_stack_web/failed_jobs/errors_controller.rb +9 -0
- data/app/controllers/solid_stack_web/failed_jobs_controller.rb +28 -1
- data/app/controllers/solid_stack_web/history_controller.rb +15 -5
- data/app/controllers/solid_stack_web/jobs/selections_controller.rb +7 -5
- data/app/controllers/solid_stack_web/jobs_controller.rb +27 -11
- data/app/controllers/solid_stack_web/stats_controller.rb +12 -3
- data/app/helpers/solid_stack_web/application_helper.rb +24 -0
- data/app/javascript/solid_stack_web/application.js +2 -0
- data/app/javascript/solid_stack_web/filter_persist_controller.js +28 -0
- data/app/models/solid_stack_web/error_frequency_report.rb +34 -0
- data/app/models/solid_stack_web/failed_job_sparkline.rb +23 -0
- data/app/views/solid_stack_web/dashboard/index.html.erb +12 -0
- data/app/views/solid_stack_web/failed_jobs/errors/index.html.erb +48 -0
- data/app/views/solid_stack_web/failed_jobs/index.html.erb +14 -4
- data/app/views/solid_stack_web/history/index.html.erb +9 -4
- data/app/views/solid_stack_web/jobs/index.html.erb +25 -18
- data/app/views/solid_stack_web/stats/index.html.erb +4 -0
- data/config/routes.rb +2 -0
- data/lib/solid_stack_web/version.rb +1 -1
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f3dbb76a6507f2e2a0480bafe3b9e18ac8f34777563c853911dfaf7b7f549f23
|
|
4
|
+
data.tar.gz: 49f69c487ad4580d704ce122e46024ba96ff05c0c4b25166d64148128a28949d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aa1598c4f1ee1a527ccc5b50240e5c866274e848ad2e7245dfe02ef6f05a6b1b889a7eb88ce80cad46f72f1b7f533a858b47cb1a3d130fd300c8bb32698d9842
|
|
7
|
+
data.tar.gz: 752a9f8176b029fa54f7c0dcef93081d4a6b25bc75dccebab67ae7561854ff3c5d1f9ae96804ac290841afbc38630b7329d0472b45fbaf7cbf95d3f342797b0b
|
data/README.md
CHANGED
|
@@ -21,23 +21,13 @@ Run:
|
|
|
21
21
|
bundle install
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
Mount the engine in `config/routes.rb`:
|
|
25
|
-
|
|
26
|
-
```ruby
|
|
27
|
-
mount SolidStackWeb::Engine, at: "/solid_stack"
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
The dashboard will be available at `/solid_stack` (or whatever path you choose).
|
|
31
|
-
|
|
32
|
-
### Install generator
|
|
33
|
-
|
|
34
24
|
Run the install generator to create a documented initializer and wire up the mount point in one step:
|
|
35
25
|
|
|
36
26
|
```bash
|
|
37
27
|
rails generate solid_stack_web:install
|
|
38
28
|
```
|
|
39
29
|
|
|
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`.
|
|
30
|
+
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`. The dashboard will then be available at `/solid_stack` (or whatever path you choose).
|
|
41
31
|
|
|
42
32
|
---
|
|
43
33
|
|
|
@@ -77,7 +67,7 @@ This creates `config/initializers/solid_stack_web.rb` with every configuration o
|
|
|
77
67
|
|
|
78
68
|
## General configuration
|
|
79
69
|
|
|
80
|
-
|
|
70
|
+
The install generator creates `config/initializers/solid_stack_web.rb` with all options documented inline. The available options are:
|
|
81
71
|
|
|
82
72
|
```ruby
|
|
83
73
|
SolidStackWeb.configure do |config|
|
|
@@ -152,19 +142,21 @@ The dashboard is designed to be mounted behind your application's existing authe
|
|
|
152
142
|
|
|
153
143
|
### Features
|
|
154
144
|
|
|
155
|
-
- **Overview dashboard** — live counts across all queue statuses; done (1h/24h), healthy/stale process counts, and optionally slow jobs (when `slow_job_threshold` is configured); 12-hour throughput sparkline with per-bar hover tooltips
|
|
156
|
-
- **Job browser** — browse jobs by status (ready, scheduled, claimed, blocked) with filtering by job class, queue name, priority, and time period; **Discard All** bulk-discards every job matching the current filters in one request; **CSV export** downloads jobs respecting active filters
|
|
145
|
+
- **Overview dashboard** — live counts across all queue statuses; done (1h/24h), healthy/stale process counts, and optionally slow jobs (when `slow_job_threshold` is configured); 12-hour throughput sparkline and a 12-hour failures sparkline (red bars) with per-bar hover tooltips — failure spikes visible before clicking into the failed jobs list
|
|
146
|
+
- **Job browser** — browse jobs by status (ready, scheduled, claimed, blocked) with filtering by job class, queue name, priority, and time period; sortable by class, queue, priority, and enqueued-at; sort state is preserved across filter and period changes; **Discard All** bulk-discards every job matching the current filters in one request; **CSV export** downloads jobs respecting active filters
|
|
157
147
|
- **Bulk selection** — checkbox-select individual jobs for discard; select-all support
|
|
158
148
|
- **Per-queue browser** — click any queue name or size to drill into its ready jobs with per-row and bulk discard; pause/resume controls on the queue page
|
|
159
149
|
- **Queue depth sparklines** — Queues index shows a 12-hour depth chart per queue; each bar is the ready-job count at an hourly snapshot with an instant hover tooltip
|
|
160
150
|
- **Job detail page** — full arguments (pretty-printed JSON), queue, priority, enqueued time, Active Job ID, concurrency key, scheduled/blocked-until metadata, and a Discard button
|
|
161
|
-
- **Failed jobs** — list with retry / discard / bulk retry / bulk discard; **Failed job detail page** — full error, backtrace, and an inline JSON argument editor; submit to update arguments and retry in one action
|
|
151
|
+
- **Failed jobs** — list with retry / discard / bulk retry / bulk discard; sortable by class, queue, and failed-at; **Failed job detail page** — full error, backtrace, and an inline JSON argument editor; submit to update arguments and retry in one action
|
|
152
|
+
- **Error frequency report** — `GET /failed_jobs/errors` groups all failed jobs by exception class and message prefix with a count and expandable sample backtrace; links through to a filtered list for each error group
|
|
162
153
|
- **Scheduled job management** — "Run Now" and offset buttons (+1h / +24h / +7d) per row update the scheduled time inline via Turbo Stream; "Run All Now (N)" back-dates all matching executions at once
|
|
163
154
|
- **Recurring task list** — enumerates all `SolidQueue::RecurringTask` records with cron schedule, job class or command, queue, next-run and last-run times, and a static/dynamic badge; each row has a "Run Now" button
|
|
164
|
-
- **Performance statistics page** — `GET /stats` aggregates finished jobs by class name with execution count, avg, p50, p95, min, and max duration; click any column header to sort; defaults to p95 descending
|
|
165
|
-
- **Job history view** — paginated list of all finished jobs with class name, queue, duration, and finished-at time; filterable by queue (click a badge), class substring, and time period; CSV export respects active filters
|
|
155
|
+
- **Performance statistics page** — `GET /stats` aggregates finished jobs by class name with execution count, avg, p50, p95, p99, std dev, min, and max duration; click any column header to sort; defaults to p95 descending; high std dev flags inconsistent jobs worth investigating
|
|
156
|
+
- **Job history view** — paginated list of all finished jobs with class name, queue, duration, and finished-at time; filterable by queue (click a badge), class substring, and time period; sortable by class, queue, and finished-at; CSV export respects active filters
|
|
166
157
|
- **Auto-refresh** — dashboard, jobs, processes, and history views poll automatically; pauses when the tab is hidden or a checkbox is checked; intervals configurable via `dashboard_refresh_interval` and `default_refresh_interval`
|
|
167
158
|
- **Turbo Stream** job discard — removes the row inline without a full page reload
|
|
159
|
+
- **Sticky filter preferences** — last-used status, period, and queue filter saved to `localStorage`; a fresh visit to the jobs or history list with no URL params automatically restores the previous selection
|
|
168
160
|
- **Dark mode** — toggle button in the header switches between light and dark palettes; preference persisted in `localStorage`; respects `prefers-color-scheme` on first visit
|
|
169
161
|
- **Responsive layout** — stats cards, tables, and two-column grids adapt to narrow viewports; tables scroll horizontally rather than overflow; split page headers stack on small screens
|
|
170
162
|
- **Empty-state improvements** — all list views show a contextual title and an actionable hint; search empty states include a "Clear search" link; filters-active history view offers "Clear filters"; processes and recurring tasks explain the next step
|
|
@@ -84,6 +84,29 @@
|
|
|
84
84
|
|
|
85
85
|
.sqw-value-truncated { font-size: 12px; margin-top: 0.5rem; }
|
|
86
86
|
|
|
87
|
+
.sqw-error-details > summary {
|
|
88
|
+
cursor: pointer;
|
|
89
|
+
list-style: none;
|
|
90
|
+
display: block;
|
|
91
|
+
max-width: 480px;
|
|
92
|
+
}
|
|
93
|
+
.sqw-error-details > summary::-webkit-details-marker { display: none; }
|
|
94
|
+
.sqw-error-details[open] > summary { margin-bottom: 0.5rem; }
|
|
95
|
+
|
|
96
|
+
.sqw-error-backtrace {
|
|
97
|
+
font-family: ui-monospace, "SFMono-Regular", Menlo, monospace;
|
|
98
|
+
font-size: 11px;
|
|
99
|
+
background: var(--bg);
|
|
100
|
+
border: 1px solid var(--border);
|
|
101
|
+
border-radius: var(--radius);
|
|
102
|
+
padding: 0.5rem 0.75rem;
|
|
103
|
+
overflow-x: auto;
|
|
104
|
+
white-space: pre;
|
|
105
|
+
max-height: 200px;
|
|
106
|
+
overflow-y: auto;
|
|
107
|
+
margin-top: 0.25rem;
|
|
108
|
+
}
|
|
109
|
+
|
|
87
110
|
.sqw-link { color: var(--primary); text-decoration: none; }
|
|
88
111
|
.sqw-link:hover { text-decoration: underline; }
|
|
89
112
|
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
module SolidStackWeb
|
|
2
2
|
class FailedJobsController < ApplicationController
|
|
3
3
|
def index
|
|
4
|
+
@sort = params[:sort].presence_in(sortable_columns) || "created_at"
|
|
5
|
+
@direction = params[:direction] == "asc" ? "asc" : "desc"
|
|
6
|
+
|
|
4
7
|
respond_to do |format|
|
|
5
8
|
format.html do
|
|
6
|
-
scope = ::SolidQueue::FailedExecution.includes(:job).
|
|
9
|
+
scope = ::SolidQueue::FailedExecution.includes(:job).references(:job).order(sort_expression)
|
|
10
|
+
@error_class = params[:error_class].presence
|
|
11
|
+
scope = scope.where(id: ids_for_error_class(@error_class)) if @error_class
|
|
7
12
|
@pagy, @executions = pagy(scope)
|
|
8
13
|
end
|
|
9
14
|
format.csv do
|
|
@@ -41,6 +46,28 @@ module SolidStackWeb
|
|
|
41
46
|
|
|
42
47
|
private
|
|
43
48
|
|
|
49
|
+
def sortable_columns
|
|
50
|
+
%w[class_name queue_name created_at]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def sort_expression
|
|
54
|
+
sql_col = case @sort
|
|
55
|
+
when "class_name" then "solid_queue_jobs.class_name"
|
|
56
|
+
when "queue_name" then "solid_queue_jobs.queue_name"
|
|
57
|
+
else "solid_queue_failed_executions.created_at"
|
|
58
|
+
end
|
|
59
|
+
Arel.sql("#{sql_col} #{@direction == 'asc' ? 'ASC' : 'DESC'}")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def ids_for_error_class(ec)
|
|
63
|
+
::SolidQueue::FailedExecution.pluck(:id, :error).filter_map do |id, raw|
|
|
64
|
+
error = raw.is_a?(Hash) ? raw : JSON.parse(raw)
|
|
65
|
+
id if error["exception_class"] == ec
|
|
66
|
+
rescue StandardError
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
44
71
|
def failed_jobs_csv
|
|
45
72
|
CSV.generate(headers: true) do |csv|
|
|
46
73
|
csv << %w[id class_name queue_name error_class error_message failed_at]
|
|
@@ -16,13 +16,23 @@ module SolidStackWeb
|
|
|
16
16
|
private
|
|
17
17
|
|
|
18
18
|
def set_filters
|
|
19
|
-
@queue
|
|
20
|
-
@search
|
|
21
|
-
@period
|
|
19
|
+
@queue = params[:queue].presence
|
|
20
|
+
@search = params[:q].presence
|
|
21
|
+
@period = params[:period].presence_in(PERIOD_DURATIONS.keys)
|
|
22
|
+
@sort = params[:sort].presence_in(sortable_columns) || "finished_at"
|
|
23
|
+
@direction = params[:direction] == "asc" ? "asc" : "desc"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def sortable_columns
|
|
27
|
+
%w[class_name queue_name finished_at]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def sort_expression
|
|
31
|
+
Arel.sql("#{@sort} #{@direction == 'asc' ? 'ASC' : 'DESC'}")
|
|
22
32
|
end
|
|
23
33
|
|
|
24
34
|
def filtered_scope
|
|
25
|
-
scope = SolidQueue::Job.where.not(finished_at: nil).order(
|
|
35
|
+
scope = SolidQueue::Job.where.not(finished_at: nil).order(sort_expression)
|
|
26
36
|
scope = scope.where(queue_name: @queue) if @queue.present?
|
|
27
37
|
scope = scope.where("class_name LIKE ?", "%#{@search}%") if @search.present?
|
|
28
38
|
scope = scope.where("finished_at >= ?", PERIOD_DURATIONS[@period].ago) if @period.present?
|
|
@@ -32,7 +42,7 @@ module SolidStackWeb
|
|
|
32
42
|
def history_csv(scope)
|
|
33
43
|
CSV.generate(headers: true) do |csv|
|
|
34
44
|
csv << %w[id class_name queue_name duration_seconds finished_at]
|
|
35
|
-
scope.
|
|
45
|
+
scope.each do |job|
|
|
36
46
|
duration = job.finished_at && job.created_at ? (job.finished_at - job.created_at).round : nil
|
|
37
47
|
csv << [job.id, job.class_name, job.queue_name, duration, job.finished_at.iso8601]
|
|
38
48
|
end
|
|
@@ -10,11 +10,13 @@ module SolidStackWeb
|
|
|
10
10
|
count = SolidQueue::Job.where(id: job_ids).destroy_all.size
|
|
11
11
|
|
|
12
12
|
redirect_to jobs_path(
|
|
13
|
-
status:
|
|
14
|
-
q:
|
|
15
|
-
queue:
|
|
16
|
-
period:
|
|
17
|
-
priority:
|
|
13
|
+
status: status,
|
|
14
|
+
q: params[:q].presence,
|
|
15
|
+
queue: params[:queue].presence,
|
|
16
|
+
period: params[:period].presence_in(PERIOD_DURATIONS.keys),
|
|
17
|
+
priority: params[:priority].presence,
|
|
18
|
+
sort: params[:sort].presence,
|
|
19
|
+
direction: params[:direction].presence
|
|
18
20
|
), notice: "#{count} #{count == 1 ? "job" : "jobs"} discarded."
|
|
19
21
|
rescue ArgumentError => e
|
|
20
22
|
redirect_to jobs_path(status: params[:status]), alert: e.message
|
|
@@ -36,13 +36,13 @@ module SolidStackWeb
|
|
|
36
36
|
@notice = "Job discarded."
|
|
37
37
|
|
|
38
38
|
respond_to do |format|
|
|
39
|
-
format.html { redirect_to jobs_path(status: @status, q: @search, queue: @queue, period: @period, priority: @priority) }
|
|
39
|
+
format.html { redirect_to jobs_path(status: @status, q: @search, queue: @queue, period: @period, priority: @priority, sort: @sort, direction: @direction) }
|
|
40
40
|
format.turbo_stream
|
|
41
41
|
end
|
|
42
42
|
else
|
|
43
43
|
job_ids = filtered_scope.pluck(:job_id)
|
|
44
44
|
count = SolidQueue::Job.where(id: job_ids).destroy_all.size
|
|
45
|
-
redirect_to jobs_path(status: @status, q: @search, queue: @queue, period: @period, priority: @priority),
|
|
45
|
+
redirect_to jobs_path(status: @status, q: @search, queue: @queue, period: @period, priority: @priority, sort: @sort, direction: @direction),
|
|
46
46
|
notice: "#{count} #{count == 1 ? "job" : "jobs"} discarded."
|
|
47
47
|
end
|
|
48
48
|
end
|
|
@@ -54,10 +54,26 @@ module SolidStackWeb
|
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
def set_filters
|
|
57
|
-
@search
|
|
58
|
-
@queue
|
|
59
|
-
@period
|
|
60
|
-
@priority
|
|
57
|
+
@search = params[:q].presence
|
|
58
|
+
@queue = params[:queue].presence
|
|
59
|
+
@period = params[:period].presence_in(PERIOD_DURATIONS.keys)
|
|
60
|
+
@priority = params[:priority].presence
|
|
61
|
+
@sort = params[:sort].presence_in(sortable_columns) || "created_at"
|
|
62
|
+
@direction = params[:direction] == "asc" ? "asc" : "desc"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def sortable_columns
|
|
66
|
+
%w[class_name queue_name priority created_at]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def sort_expression
|
|
70
|
+
sql_col = case @sort
|
|
71
|
+
when "class_name" then "solid_queue_jobs.class_name"
|
|
72
|
+
when "queue_name" then "solid_queue_jobs.queue_name"
|
|
73
|
+
when "priority" then "solid_queue_jobs.priority"
|
|
74
|
+
else "#{Job::EXECUTION_MODELS[@status].quoted_table_name}.created_at"
|
|
75
|
+
end
|
|
76
|
+
Arel.sql("#{sql_col} #{@direction == 'asc' ? 'ASC' : 'DESC'}")
|
|
61
77
|
end
|
|
62
78
|
|
|
63
79
|
def require_discardable
|
|
@@ -75,11 +91,11 @@ module SolidStackWeb
|
|
|
75
91
|
end
|
|
76
92
|
|
|
77
93
|
def filtered_scope
|
|
78
|
-
scope = Job::EXECUTION_MODELS[@status].includes(:job).
|
|
79
|
-
scope = scope.
|
|
80
|
-
scope = scope.
|
|
81
|
-
scope = scope.
|
|
82
|
-
scope = scope.
|
|
94
|
+
scope = Job::EXECUTION_MODELS[@status].includes(:job).references(:job).order(sort_expression)
|
|
95
|
+
scope = scope.where("solid_queue_jobs.class_name LIKE ?", "%#{@search}%") if @search.present?
|
|
96
|
+
scope = scope.where("solid_queue_jobs.queue_name = ?", @queue) if @queue.present?
|
|
97
|
+
scope = scope.where("solid_queue_jobs.created_at >= ?", PERIOD_DURATIONS[@period].ago) if @period.present?
|
|
98
|
+
scope = scope.where("solid_queue_jobs.priority = ?", @priority.to_i) if @priority.present?
|
|
83
99
|
scope
|
|
84
100
|
end
|
|
85
101
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module SolidStackWeb
|
|
2
2
|
class StatsController < ApplicationController
|
|
3
|
-
SORTABLE_COLUMNS = %w[class_name count avg p50 p95 min max].freeze
|
|
3
|
+
SORTABLE_COLUMNS = %w[class_name count avg p50 p95 p99 stddev min max].freeze
|
|
4
4
|
|
|
5
5
|
def index
|
|
6
6
|
@sort = params[:sort].presence_in(SORTABLE_COLUMNS) || "p95"
|
|
@@ -18,14 +18,17 @@ module SolidStackWeb
|
|
|
18
18
|
jobs.group_by(&:class_name).map do |class_name, group|
|
|
19
19
|
durations = group.map { |j| (j.finished_at - j.created_at).to_f }.sort
|
|
20
20
|
count = durations.size
|
|
21
|
+
avg = durations.sum / count
|
|
21
22
|
{
|
|
22
23
|
class_name: class_name,
|
|
23
24
|
count: count,
|
|
24
|
-
avg:
|
|
25
|
+
avg: avg,
|
|
25
26
|
min: durations.first,
|
|
26
27
|
max: durations.last,
|
|
27
28
|
p50: percentile(durations, 50),
|
|
28
|
-
p95: percentile(durations, 95)
|
|
29
|
+
p95: percentile(durations, 95),
|
|
30
|
+
p99: percentile(durations, 99),
|
|
31
|
+
stddev: stddev(durations, avg)
|
|
29
32
|
}
|
|
30
33
|
end
|
|
31
34
|
end
|
|
@@ -35,5 +38,11 @@ module SolidStackWeb
|
|
|
35
38
|
k = (sorted.size - 1) * pct / 100.0
|
|
36
39
|
sorted[k.floor] + (sorted[k.ceil] - sorted[k.floor]) * (k - k.floor)
|
|
37
40
|
end
|
|
41
|
+
|
|
42
|
+
def stddev(durations, avg)
|
|
43
|
+
return 0.0 if durations.size < 2
|
|
44
|
+
variance = durations.sum { |d| (d - avg)**2 } / durations.size
|
|
45
|
+
Math.sqrt(variance)
|
|
46
|
+
end
|
|
38
47
|
end
|
|
39
48
|
end
|
|
@@ -87,6 +87,30 @@ module SolidStackWeb
|
|
|
87
87
|
end
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
+
def sort_header_th(label, col, url_proc, current_sort:, current_dir:)
|
|
91
|
+
is_active = current_sort == col
|
|
92
|
+
next_dir = (is_active && current_dir == "desc") ? "asc" : "desc"
|
|
93
|
+
indicator = is_active ? content_tag(:span, current_dir == "desc" ? "↓" : "↑", class: "sqw-sort-indicator") : nil
|
|
94
|
+
tag_opts = { scope: "col" }
|
|
95
|
+
tag_opts[:"aria-sort"] = is_active ? (current_dir == "asc" ? "ascending" : "descending") : nil if is_active
|
|
96
|
+
content_tag(:th, **tag_opts) do
|
|
97
|
+
link_to(url_proc.call(sort: col, direction: next_dir)) do
|
|
98
|
+
safe_join([label, indicator].compact)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def failed_job_sparkline_svg(sparkline)
|
|
104
|
+
build_sparkline_svg(sparkline, aria_label: "Failed jobs over the last 12 hours") do |count, i|
|
|
105
|
+
hours_ago = SolidStackWeb::FailedJobSparkline::HOURS - i
|
|
106
|
+
if hours_ago == 1
|
|
107
|
+
"#{count} #{count == 1 ? "failure" : "failures"} in the last hour"
|
|
108
|
+
else
|
|
109
|
+
"#{count} #{count == 1 ? "failure" : "failures"} (#{hours_ago}h–#{hours_ago - 1}h ago)"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
90
114
|
private
|
|
91
115
|
|
|
92
116
|
def build_sparkline_svg(sparkline, css_class: "sqw-sparkline", aria_label: nil, &tooltip_text)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import "@hotwired/turbo"
|
|
2
2
|
import { Application } from "@hotwired/stimulus"
|
|
3
|
+
import FilterPersistController from "solid_stack_web/filter_persist_controller"
|
|
3
4
|
import RefreshController from "solid_stack_web/refresh_controller"
|
|
4
5
|
import SearchController from "solid_stack_web/search_controller"
|
|
5
6
|
import SelectionController from "solid_stack_web/selection_controller"
|
|
@@ -8,6 +9,7 @@ import ThemeController from "solid_stack_web/theme_controller"
|
|
|
8
9
|
import TimestampController from "solid_stack_web/timestamp_controller"
|
|
9
10
|
|
|
10
11
|
const application = Application.start()
|
|
12
|
+
application.register("filter-persist", FilterPersistController)
|
|
11
13
|
application.register("refresh", RefreshController)
|
|
12
14
|
application.register("search", SearchController)
|
|
13
15
|
application.register("selection", SelectionController)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
const PERSIST_KEYS = ["status", "period", "queue"]
|
|
4
|
+
|
|
5
|
+
export default class extends Controller {
|
|
6
|
+
static values = { key: String }
|
|
7
|
+
|
|
8
|
+
connect() {
|
|
9
|
+
const params = new URLSearchParams(window.location.search)
|
|
10
|
+
|
|
11
|
+
if (params.toString()) {
|
|
12
|
+
const toSave = new URLSearchParams()
|
|
13
|
+
PERSIST_KEYS.forEach(k => { if (params.get(k)) toSave.set(k, params.get(k)) })
|
|
14
|
+
if (toSave.toString()) this.write(toSave.toString())
|
|
15
|
+
} else {
|
|
16
|
+
const stored = this.read()
|
|
17
|
+
if (stored) window.location.replace(`${window.location.pathname}?${stored}`)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
read() {
|
|
22
|
+
try { return localStorage.getItem(this.keyValue) } catch (e) { return null }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
write(value) {
|
|
26
|
+
try { localStorage.setItem(this.keyValue, value) } catch (e) { /* ignore */ }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module SolidStackWeb
|
|
2
|
+
class ErrorFrequencyReport
|
|
3
|
+
Row = Data.define(:exception_class, :message_prefix, :count, :sample_backtrace)
|
|
4
|
+
|
|
5
|
+
MESSAGE_LIMIT = 120
|
|
6
|
+
|
|
7
|
+
def groups
|
|
8
|
+
::SolidQueue::FailedExecution
|
|
9
|
+
.order(created_at: :desc)
|
|
10
|
+
.each_with_object({}) do |execution, acc|
|
|
11
|
+
key = [execution.exception_class.to_s, message_prefix(execution.message)]
|
|
12
|
+
entry = acc[key] ||= { count: 0, sample_backtrace: nil }
|
|
13
|
+
entry[:count] += 1
|
|
14
|
+
entry[:sample_backtrace] ||= execution.backtrace
|
|
15
|
+
end
|
|
16
|
+
.map do |(exception_class, prefix), data|
|
|
17
|
+
Row.new(
|
|
18
|
+
exception_class: exception_class,
|
|
19
|
+
message_prefix: prefix,
|
|
20
|
+
count: data[:count],
|
|
21
|
+
sample_backtrace: data[:sample_backtrace]
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
.sort_by { |row| -row.count }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def message_prefix(message)
|
|
30
|
+
return "" if message.nil?
|
|
31
|
+
message.length > MESSAGE_LIMIT ? "#{message[0, MESSAGE_LIMIT]}…" : message
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module SolidStackWeb
|
|
2
|
+
class FailedJobSparkline
|
|
3
|
+
HOURS = 12
|
|
4
|
+
|
|
5
|
+
def buckets
|
|
6
|
+
@buckets ||= begin
|
|
7
|
+
now = Time.current
|
|
8
|
+
origin = now - HOURS.hours
|
|
9
|
+
times = ::SolidQueue::FailedExecution.where(created_at: origin..now).pluck(:created_at)
|
|
10
|
+
|
|
11
|
+
HOURS.times.map do |i|
|
|
12
|
+
from = origin + i.hours
|
|
13
|
+
to = origin + (i + 1).hours
|
|
14
|
+
times.count { |t| t >= from && t < to }
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def max
|
|
20
|
+
buckets.max || 0
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -68,6 +68,18 @@
|
|
|
68
68
|
<span>now</span>
|
|
69
69
|
</div>
|
|
70
70
|
</div>
|
|
71
|
+
<div class="sqw-sparkline-wrap sqw-sparkline-wrap--failures" data-controller="sparkline-tooltip">
|
|
72
|
+
<span class="sqw-sparkline-label">Failures — last 12 hours</span>
|
|
73
|
+
<div class="sqw-sparkline-positioner">
|
|
74
|
+
<%= failed_job_sparkline_svg(@failures) %>
|
|
75
|
+
<div class="sqw-sparkline-tip" data-sparkline-tooltip-target="tip" hidden></div>
|
|
76
|
+
</div>
|
|
77
|
+
<div class="sqw-sparkline-axis">
|
|
78
|
+
<span>12h ago</span>
|
|
79
|
+
<span>6h ago</span>
|
|
80
|
+
<span>now</span>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
71
83
|
</div>
|
|
72
84
|
|
|
73
85
|
<div class="sqw-gem-card sqw-gem-card--cache">
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<div class="sqw-page-header sqw-page-header--split">
|
|
2
|
+
<h1 class="sqw-page-title">Error Summary</h1>
|
|
3
|
+
<div class="sqw-header-actions">
|
|
4
|
+
<%= link_to "← Failed Jobs", failed_jobs_path, class: "sqw-btn sqw-btn--muted sqw-btn--sm" %>
|
|
5
|
+
</div>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<% if @groups.any? %>
|
|
9
|
+
<table class="sqw-table">
|
|
10
|
+
<thead>
|
|
11
|
+
<tr>
|
|
12
|
+
<th scope="col">Error Class</th>
|
|
13
|
+
<th scope="col">Message</th>
|
|
14
|
+
<th scope="col">Count</th>
|
|
15
|
+
<th scope="col"><span class="sqw-sr-only">Actions</span></th>
|
|
16
|
+
</tr>
|
|
17
|
+
</thead>
|
|
18
|
+
<tbody>
|
|
19
|
+
<% @groups.each do |group| %>
|
|
20
|
+
<tr>
|
|
21
|
+
<td class="sqw-monospace"><%= group.exception_class.presence || "—" %></td>
|
|
22
|
+
<td>
|
|
23
|
+
<% if group.sample_backtrace.present? %>
|
|
24
|
+
<details class="sqw-error-details">
|
|
25
|
+
<summary class="sqw-muted sqw-truncate" title="<%= group.message_prefix %>">
|
|
26
|
+
<%= group.message_prefix.presence || "—" %>
|
|
27
|
+
</summary>
|
|
28
|
+
<pre class="sqw-error-backtrace"><%= Array(group.sample_backtrace).first(10).join("\n") %></pre>
|
|
29
|
+
</details>
|
|
30
|
+
<% else %>
|
|
31
|
+
<span class="sqw-muted sqw-truncate" title="<%= group.message_prefix %>"><%= group.message_prefix.presence || "—" %></span>
|
|
32
|
+
<% end %>
|
|
33
|
+
</td>
|
|
34
|
+
<td><%= group.count %></td>
|
|
35
|
+
<td class="sqw-actions">
|
|
36
|
+
<%= link_to "View Jobs", failed_jobs_path(error_class: group.exception_class),
|
|
37
|
+
class: "sqw-btn sqw-btn--muted sqw-btn--sm" %>
|
|
38
|
+
</td>
|
|
39
|
+
</tr>
|
|
40
|
+
<% end %>
|
|
41
|
+
</tbody>
|
|
42
|
+
</table>
|
|
43
|
+
<% else %>
|
|
44
|
+
<div class="sqw-empty">
|
|
45
|
+
<p class="sqw-empty__title">No failed jobs</p>
|
|
46
|
+
<p class="sqw-empty__hint">All clear — your jobs are running without errors.</p>
|
|
47
|
+
</div>
|
|
48
|
+
<% end %>
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
<div class="sqw-page-header sqw-page-header--split">
|
|
2
|
-
<
|
|
2
|
+
<div>
|
|
3
|
+
<h1 class="sqw-page-title">Failed Jobs</h1>
|
|
4
|
+
<% if @error_class %>
|
|
5
|
+
<div class="sqw-breadcrumb">
|
|
6
|
+
Filtered by <span class="sqw-monospace"><%= @error_class %></span>
|
|
7
|
+
— <%= link_to "Clear filter", failed_jobs_path %>
|
|
8
|
+
</div>
|
|
9
|
+
<% end %>
|
|
10
|
+
</div>
|
|
3
11
|
<div class="sqw-header-actions">
|
|
12
|
+
<%= link_to "Error Summary", failed_job_errors_path, class: "sqw-btn sqw-btn--muted sqw-btn--sm" %>
|
|
4
13
|
<%= link_to "Export CSV", failed_jobs_path(format: :csv),
|
|
5
14
|
class: "sqw-btn sqw-btn--muted sqw-btn--sm", data: { turbo: false } %>
|
|
6
15
|
</div>
|
|
@@ -32,10 +41,11 @@
|
|
|
32
41
|
<th scope="col"><input type="checkbox" class="sqw-checkbox" aria-label="Select all"
|
|
33
42
|
data-selection-target="selectAll"
|
|
34
43
|
data-action="change->selection#selectAll"></th>
|
|
35
|
-
|
|
36
|
-
|
|
44
|
+
<% sort_url = ->(p) { failed_jobs_path(error_class: @error_class, **p) } %>
|
|
45
|
+
<%= sort_header_th("Job Class", "class_name", sort_url, current_sort: @sort, current_dir: @direction) %>
|
|
46
|
+
<%= sort_header_th("Queue", "queue_name", sort_url, current_sort: @sort, current_dir: @direction) %>
|
|
37
47
|
<th scope="col">Error</th>
|
|
38
|
-
|
|
48
|
+
<%= sort_header_th("Failed At", "created_at", sort_url, current_sort: @sort, current_dir: @direction) %>
|
|
39
49
|
<th scope="col"><span class="sqw-sr-only">Actions</span></th>
|
|
40
50
|
</tr>
|
|
41
51
|
</thead>
|
|
@@ -10,11 +10,15 @@
|
|
|
10
10
|
</div>
|
|
11
11
|
</div>
|
|
12
12
|
|
|
13
|
-
<form class="sqw-filters" action="<%= history_path %>" method="get"
|
|
13
|
+
<form class="sqw-filters" action="<%= history_path %>" method="get"
|
|
14
|
+
data-controller="search filter-persist"
|
|
15
|
+
data-filter-persist-key-value="sqw-history-filters">
|
|
14
16
|
<% if @queue.present? %>
|
|
15
17
|
<input type="hidden" name="queue" value="<%= @queue %>">
|
|
16
18
|
<% end %>
|
|
17
19
|
<input type="hidden" name="period" value="<%= @period %>">
|
|
20
|
+
<input type="hidden" name="sort" value="<%= @sort %>">
|
|
21
|
+
<input type="hidden" name="direction" value="<%= @direction %>">
|
|
18
22
|
<input class="sqw-search-input" type="search" name="q" value="<%= @search %>"
|
|
19
23
|
placeholder="Filter by job class…" autocomplete="off" aria-label="Filter by job class"
|
|
20
24
|
data-action="input->search#filter">
|
|
@@ -44,11 +48,12 @@
|
|
|
44
48
|
<div class="sqw-detail-card">
|
|
45
49
|
<table class="sqw-table">
|
|
46
50
|
<thead>
|
|
51
|
+
<% sort_url = ->(p) { history_path(queue: @queue, q: @search, period: @period, **p) } %>
|
|
47
52
|
<tr>
|
|
48
|
-
|
|
49
|
-
|
|
53
|
+
<%= sort_header_th("Job Class", "class_name", sort_url, current_sort: @sort, current_dir: @direction) %>
|
|
54
|
+
<%= sort_header_th("Queue", "queue_name", sort_url, current_sort: @sort, current_dir: @direction) %>
|
|
50
55
|
<th scope="col">Duration</th>
|
|
51
|
-
|
|
56
|
+
<%= sort_header_th("Finished At", "finished_at", sort_url, current_sort: @sort, current_dir: @direction) %>
|
|
52
57
|
</tr>
|
|
53
58
|
</thead>
|
|
54
59
|
<tbody>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<div class="sqw-page-header sqw-page-header--split">
|
|
2
2
|
<h1 class="sqw-page-title">Jobs</h1>
|
|
3
3
|
<div class="sqw-header-actions">
|
|
4
|
-
<%= link_to "Export CSV", jobs_path(format: :csv, status: @status, q: @search, queue: @queue, period: @period, priority: @priority),
|
|
4
|
+
<%= link_to "Export CSV", jobs_path(format: :csv, status: @status, q: @search, queue: @queue, period: @period, priority: @priority, sort: @sort, direction: @direction),
|
|
5
5
|
class: "sqw-btn sqw-btn--muted sqw-btn--sm", data: { turbo: false } %>
|
|
6
6
|
<% if @status == "scheduled" && @executions&.any? %>
|
|
7
7
|
<%= button_to "Run All Now (#{@pagy.count})",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
<div class="sqw-tabs">
|
|
26
26
|
<% SolidStackWeb::Job::TAB_LABELS.each do |status, label| %>
|
|
27
|
-
<%= link_to label, jobs_path(status: status, q: @search, queue: @queue, period: @period, priority: @priority),
|
|
27
|
+
<%= link_to label, jobs_path(status: status, q: @search, queue: @queue, period: @period, priority: @priority, sort: @sort, direction: @direction),
|
|
28
28
|
class: "sqw-tab #{"sqw-tab--active" if @status == status}" %>
|
|
29
29
|
<% end %>
|
|
30
30
|
</div>
|
|
@@ -32,9 +32,13 @@
|
|
|
32
32
|
<%= turbo_frame_tag "sqw-jobs-filter",
|
|
33
33
|
data: { turbo_action: "advance", controller: "refresh",
|
|
34
34
|
refresh_interval_value: SolidStackWeb.default_refresh_interval } do %>
|
|
35
|
-
<form class="sqw-filters" action="<%= jobs_path %>" method="get"
|
|
35
|
+
<form class="sqw-filters" action="<%= jobs_path %>" method="get"
|
|
36
|
+
data-controller="search filter-persist"
|
|
37
|
+
data-filter-persist-key-value="sqw-jobs-filters">
|
|
36
38
|
<%= hidden_field_tag :status, @status %>
|
|
37
39
|
<%= hidden_field_tag :period, @period %>
|
|
40
|
+
<%= hidden_field_tag :sort, @sort %>
|
|
41
|
+
<%= hidden_field_tag :direction, @direction %>
|
|
38
42
|
<input class="sqw-search-input" type="search" name="q" value="<%= @search %>"
|
|
39
43
|
placeholder="Filter by job class…" autocomplete="off" aria-label="Filter by job class"
|
|
40
44
|
data-action="input->search#filter">
|
|
@@ -57,16 +61,16 @@
|
|
|
57
61
|
</select>
|
|
58
62
|
<% end %>
|
|
59
63
|
<% if @search.present? || @queue.present? || @priority.present? %>
|
|
60
|
-
<%= link_to "Clear", jobs_path(status: @status, period: @period), class: "sqw-btn sqw-btn--muted sqw-btn--sm" %>
|
|
64
|
+
<%= link_to "Clear", jobs_path(status: @status, period: @period, sort: @sort, direction: @direction), class: "sqw-btn sqw-btn--muted sqw-btn--sm" %>
|
|
61
65
|
<% end %>
|
|
62
66
|
<div class="sqw-period-filter" role="group" aria-label="Time period">
|
|
63
|
-
<%= link_to "All", jobs_path(status: @status, q: @search, queue: @queue, priority: @priority),
|
|
67
|
+
<%= link_to "All", jobs_path(status: @status, q: @search, queue: @queue, priority: @priority, sort: @sort, direction: @direction),
|
|
64
68
|
class: "sqw-period-btn #{"sqw-period-btn--active" if @period.nil?}" %>
|
|
65
|
-
<%= link_to "1h", jobs_path(status: @status, q: @search, queue: @queue, priority: @priority, period: "1h"),
|
|
69
|
+
<%= link_to "1h", jobs_path(status: @status, q: @search, queue: @queue, priority: @priority, period: "1h", sort: @sort, direction: @direction),
|
|
66
70
|
class: "sqw-period-btn #{"sqw-period-btn--active" if @period == "1h"}" %>
|
|
67
|
-
<%= link_to "24h", jobs_path(status: @status, q: @search, queue: @queue, priority: @priority, period: "24h"),
|
|
71
|
+
<%= link_to "24h", jobs_path(status: @status, q: @search, queue: @queue, priority: @priority, period: "24h", sort: @sort, direction: @direction),
|
|
68
72
|
class: "sqw-period-btn #{"sqw-period-btn--active" if @period == "24h"}" %>
|
|
69
|
-
<%= link_to "7d", jobs_path(status: @status, q: @search, queue: @queue, priority: @priority, period: "7d"),
|
|
73
|
+
<%= link_to "7d", jobs_path(status: @status, q: @search, queue: @queue, priority: @priority, period: "7d", sort: @sort, direction: @direction),
|
|
70
74
|
class: "sqw-period-btn #{"sqw-period-btn--active" if @period == "7d"}" %>
|
|
71
75
|
</div>
|
|
72
76
|
</form>
|
|
@@ -77,11 +81,13 @@
|
|
|
77
81
|
<% if SolidStackWeb::Job::DISCARDABLE.include?(@status) %>
|
|
78
82
|
<%= form_with url: job_selection_path, method: :delete, id: "job-selection-form",
|
|
79
83
|
data: { turbo_confirm: "Discard selected jobs? This cannot be undone." } do |f| %>
|
|
80
|
-
<%= f.hidden_field :status,
|
|
81
|
-
<%= f.hidden_field :q,
|
|
82
|
-
<%= f.hidden_field :queue,
|
|
83
|
-
<%= f.hidden_field :period,
|
|
84
|
-
<%= f.hidden_field :priority,
|
|
84
|
+
<%= f.hidden_field :status, value: @status %>
|
|
85
|
+
<%= f.hidden_field :q, value: @search %>
|
|
86
|
+
<%= f.hidden_field :queue, value: @queue %>
|
|
87
|
+
<%= f.hidden_field :period, value: @period %>
|
|
88
|
+
<%= f.hidden_field :priority, value: @priority %>
|
|
89
|
+
<%= f.hidden_field :sort, value: @sort %>
|
|
90
|
+
<%= f.hidden_field :direction, value: @direction %>
|
|
85
91
|
<% end %>
|
|
86
92
|
|
|
87
93
|
<div class="sqw-selection-bar" data-selection-target="bar" style="display: none;">
|
|
@@ -100,10 +106,11 @@
|
|
|
100
106
|
data-selection-target="selectAll"
|
|
101
107
|
data-action="change->selection#selectAll"></th>
|
|
102
108
|
<% end %>
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
109
|
+
<% sort_url = ->(p) { jobs_path(status: @status, q: @search, queue: @queue, period: @period, priority: @priority, **p) } %>
|
|
110
|
+
<%= sort_header_th("Job Class", "class_name", sort_url, current_sort: @sort, current_dir: @direction) %>
|
|
111
|
+
<%= sort_header_th("Queue", "queue_name", sort_url, current_sort: @sort, current_dir: @direction) %>
|
|
112
|
+
<%= sort_header_th("Priority", "priority", sort_url, current_sort: @sort, current_dir: @direction) %>
|
|
113
|
+
<%= sort_header_th("Enqueued At", "created_at", sort_url, current_sort: @sort, current_dir: @direction) %>
|
|
107
114
|
<% if @status == "scheduled" %><th scope="col">Scheduled At</th><% end %>
|
|
108
115
|
<th scope="col"><span class="sqw-sr-only">Actions</span></th>
|
|
109
116
|
</tr>
|
|
@@ -140,7 +147,7 @@
|
|
|
140
147
|
<% end %>
|
|
141
148
|
<% end %>
|
|
142
149
|
<% if %w[ready scheduled blocked].include?(@status) %>
|
|
143
|
-
<%= button_to "Discard", job_path(execution, status: @status, q: @search, queue: @queue, period: @period, priority: @priority),
|
|
150
|
+
<%= button_to "Discard", job_path(execution, status: @status, q: @search, queue: @queue, period: @period, priority: @priority, sort: @sort, direction: @direction),
|
|
144
151
|
method: :delete, class: "sqw-btn sqw-btn--danger sqw-btn--sm",
|
|
145
152
|
data: { turbo_confirm: "Discard this job?" } %>
|
|
146
153
|
<% end %>
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
["avg", "Avg"],
|
|
13
13
|
["p50", "p50"],
|
|
14
14
|
["p95", "p95"],
|
|
15
|
+
["p99", "p99"],
|
|
16
|
+
["stddev", "Std Dev"],
|
|
15
17
|
["min", "Min"],
|
|
16
18
|
["max", "Max"]
|
|
17
19
|
].each do |col, label| %>
|
|
@@ -35,6 +37,8 @@
|
|
|
35
37
|
<td class="sqw-muted"><%= format_duration(row[:avg]) %></td>
|
|
36
38
|
<td class="sqw-muted"><%= format_duration(row[:p50]) %></td>
|
|
37
39
|
<td><strong><%= format_duration(row[:p95]) %></strong></td>
|
|
40
|
+
<td class="sqw-muted"><%= format_duration(row[:p99]) %></td>
|
|
41
|
+
<td class="sqw-muted"><%= format_duration(row[:stddev]) %></td>
|
|
38
42
|
<td class="sqw-muted"><%= format_duration(row[:min]) %></td>
|
|
39
43
|
<td class="sqw-muted"><%= format_duration(row[:max]) %></td>
|
|
40
44
|
</tr>
|
data/config/routes.rb
CHANGED
|
@@ -20,6 +20,8 @@ SolidStackWeb::Engine.routes.draw do
|
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
get "failed_jobs/errors", to: "failed_jobs/errors#index", as: :failed_job_errors
|
|
24
|
+
|
|
23
25
|
resources :failed_jobs, only: [:index, :show, :destroy] do
|
|
24
26
|
member { post :retry }
|
|
25
27
|
resource :arguments, only: [:update], controller: "failed_jobs/arguments"
|
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: 1.
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chuck Smith
|
|
@@ -160,6 +160,7 @@ files:
|
|
|
160
160
|
- app/controllers/solid_stack_web/cache_entries_controller.rb
|
|
161
161
|
- app/controllers/solid_stack_web/dashboard_controller.rb
|
|
162
162
|
- app/controllers/solid_stack_web/failed_jobs/arguments_controller.rb
|
|
163
|
+
- app/controllers/solid_stack_web/failed_jobs/errors_controller.rb
|
|
163
164
|
- app/controllers/solid_stack_web/failed_jobs/selections_controller.rb
|
|
164
165
|
- app/controllers/solid_stack_web/failed_jobs_controller.rb
|
|
165
166
|
- app/controllers/solid_stack_web/history_controller.rb
|
|
@@ -175,6 +176,7 @@ files:
|
|
|
175
176
|
- app/controllers/solid_stack_web/stats_controller.rb
|
|
176
177
|
- app/helpers/solid_stack_web/application_helper.rb
|
|
177
178
|
- app/javascript/solid_stack_web/application.js
|
|
179
|
+
- app/javascript/solid_stack_web/filter_persist_controller.js
|
|
178
180
|
- app/javascript/solid_stack_web/refresh_controller.js
|
|
179
181
|
- app/javascript/solid_stack_web/search_controller.js
|
|
180
182
|
- app/javascript/solid_stack_web/selection_controller.js
|
|
@@ -187,6 +189,8 @@ files:
|
|
|
187
189
|
- app/models/solid_stack_web/cache_size_stats.rb
|
|
188
190
|
- app/models/solid_stack_web/cache_stats.rb
|
|
189
191
|
- app/models/solid_stack_web/cache_timeline.rb
|
|
192
|
+
- app/models/solid_stack_web/error_frequency_report.rb
|
|
193
|
+
- app/models/solid_stack_web/failed_job_sparkline.rb
|
|
190
194
|
- app/models/solid_stack_web/job.rb
|
|
191
195
|
- app/models/solid_stack_web/queue_depth_sparkline.rb
|
|
192
196
|
- app/models/solid_stack_web/queue_stats.rb
|
|
@@ -201,6 +205,7 @@ files:
|
|
|
201
205
|
- app/views/solid_stack_web/errors/internal_server_error.html.erb
|
|
202
206
|
- app/views/solid_stack_web/errors/not_found.html.erb
|
|
203
207
|
- app/views/solid_stack_web/failed_jobs/destroy.turbo_stream.erb
|
|
208
|
+
- app/views/solid_stack_web/failed_jobs/errors/index.html.erb
|
|
204
209
|
- app/views/solid_stack_web/failed_jobs/index.html.erb
|
|
205
210
|
- app/views/solid_stack_web/failed_jobs/show.html.erb
|
|
206
211
|
- app/views/solid_stack_web/history/index.html.erb
|