side_bro 0.2.2

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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +37 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +50 -0
  5. data/Rakefile +10 -0
  6. data/lib/side_bro/version.rb +5 -0
  7. data/lib/side_bro/web/action.rb +115 -0
  8. data/lib/side_bro/web/application.rb +331 -0
  9. data/lib/side_bro/web/helpers.rb +176 -0
  10. data/lib/side_bro/web/router.rb +50 -0
  11. data/lib/side_bro/web.rb +96 -0
  12. data/lib/side_bro.rb +8 -0
  13. data/sig/side_bro.rbs +4 -0
  14. data/web/assets/images/.keep +0 -0
  15. data/web/assets/javascripts/application.js +62 -0
  16. data/web/assets/javascripts/base-charts.js +1 -0
  17. data/web/assets/javascripts/dashboard-charts.js +1 -0
  18. data/web/assets/javascripts/dashboard.js +262 -0
  19. data/web/assets/javascripts/metrics.js +1 -0
  20. data/web/assets/stylesheets/style.css +636 -0
  21. data/web/locales/en.yml +88 -0
  22. data/web/views/_footer.html.erb +3 -0
  23. data/web/views/_job_info.html.erb +23 -0
  24. data/web/views/_metrics_period_select.html.erb +5 -0
  25. data/web/views/_nav.html.erb +76 -0
  26. data/web/views/_paging.html.erb +11 -0
  27. data/web/views/_poll_link.html.erb +4 -0
  28. data/web/views/_summary.html.erb +44 -0
  29. data/web/views/busy.html.erb +104 -0
  30. data/web/views/dashboard.html.erb +124 -0
  31. data/web/views/dead.html.erb +31 -0
  32. data/web/views/layout.html.erb +61 -0
  33. data/web/views/metrics.html.erb +56 -0
  34. data/web/views/metrics_for_job.html.erb +67 -0
  35. data/web/views/morgue.html.erb +82 -0
  36. data/web/views/queue.html.erb +190 -0
  37. data/web/views/queues.html.erb +59 -0
  38. data/web/views/retries.html.erb +99 -0
  39. data/web/views/retry.html.erb +32 -0
  40. data/web/views/scheduled.html.erb +79 -0
  41. data/web/views/scheduled_job_info.html.erb +31 -0
  42. metadata +126 -0
@@ -0,0 +1,67 @@
1
+ <% @page_title = "Metrics: #{@name}" %>
2
+ <% @breadcrumb = [{label: "Metrics", href: "#{action.root_path}metrics"}, {label: @name}] %>
3
+
4
+ <div class="page-head">
5
+ <div class="page-title-block">
6
+ <div class="eyebrow"><span>Monitoring</span></div>
7
+ <h1><span class="queue-name"><%= action.h(@name) %></span></h1>
8
+ </div>
9
+ <div class="page-head-meta">
10
+ <%= action.render_partial(:metrics_period_select) %>
11
+ </div>
12
+ </div>
13
+
14
+ <%- job_result = @metrics&.job_results&.[](@name) %>
15
+
16
+ <%- if job_result.nil? || job_result.totals.empty? %>
17
+ <div class="panel">
18
+ <div class="empty"><%= action.t(:no_jobs) %></div>
19
+ </div>
20
+ <%- else %>
21
+ <%- processed = job_result.totals["p"].to_i %>
22
+ <%- failures = job_result.totals["f"].to_i %>
23
+ <%- success = processed - failures %>
24
+ <%- total_s = job_result.totals["s"].to_f %>
25
+ <%- completed = [success, 0].max %>
26
+ <%- avg_s = completed > 0 ? total_s / completed : 0.0 %>
27
+
28
+ <div class="panel">
29
+ <div class="panel-head">
30
+ <div class="ph-title">Summary</div>
31
+ <div class="ph-meta mono">Period: <%= @period %></div>
32
+ </div>
33
+ <table>
34
+ <thead>
35
+ <tr>
36
+ <th><%= action.t(:success) %></th>
37
+ <th><%= action.t(:failed) %></th>
38
+ <th><%= action.t(:total_exec_time) %></th>
39
+ <th><%= action.t(:avg_exec_time) %></th>
40
+ </tr>
41
+ </thead>
42
+ <tbody>
43
+ <tr>
44
+ <td><span style="color:var(--lime)"><%= success %></span></td>
45
+ <td><span style="color:var(--red)"><%= failures %></span></td>
46
+ <td><span class="mono" style="font-size:12px"><%= "%.2fs" % total_s %></span></td>
47
+ <td><span class="mono" style="font-size:12px"><%= "%.3fs" % avg_s %></span></td>
48
+ </tr>
49
+ </tbody>
50
+ </table>
51
+ </div>
52
+
53
+ <script nonce="<%= nonce %>">
54
+ window.JOB_METRICS = <%= {
55
+ name: @name,
56
+ period: @period,
57
+ processed: job_result.series["p"],
58
+ failed: job_result.series["f"],
59
+ avg_ms: job_result.series_avg("ms")
60
+ }.to_json %>;
61
+ </script>
62
+ <%- end %>
63
+
64
+ <a href="<%= action.root_path %>metrics" class="btn ghost" style="margin-top:8px;">
65
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>
66
+ &larr; Back to Metrics
67
+ </a>
@@ -0,0 +1,82 @@
1
+ <% @page_title = "Dead Jobs" %>
2
+
3
+ <div class="page-head">
4
+ <div class="page-title-block">
5
+ <div class="eyebrow"><span>Lifecycle</span></div>
6
+ <h1>Dead <span class="badge-num"><%= @total %></span></h1>
7
+ </div>
8
+ </div>
9
+
10
+ <%- if @jobs.empty? %>
11
+ <div class="panel">
12
+ <div class="empty"><%= action.t(:no_dead) %></div>
13
+ </div>
14
+ <%- else %>
15
+ <form method="post" action="<%= action.root_path %>morgue">
16
+ <div class="panel">
17
+ <div class="panel-head">
18
+ <div class="ph-title">Morgue</div>
19
+ <div class="ph-meta mono"><%= @total %> jobs</div>
20
+ <div class="ph-spacer"></div>
21
+ <div class="bulk-bar">
22
+ <button type="submit" name="action" value="retry" class="btn ghost">
23
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
24
+ <%= action.t(:retry_now) %>
25
+ </button>
26
+ <button type="submit" name="action" value="delete" class="btn danger">
27
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg>
28
+ <%= action.t(:delete) %>
29
+ </button>
30
+ </div>
31
+ </div>
32
+ <div class="table-scroll">
33
+ <table>
34
+ <thead>
35
+ <tr>
36
+ <th class="col-select"><input type="checkbox" class="check" id="select-all"></th>
37
+ <th><%= action.t(:last_retry) %></th>
38
+ <th><%= action.t(:queue) %></th>
39
+ <th><%= action.t(:class) %></th>
40
+ <th><%= action.t(:args) %></th>
41
+ <th><%= action.t(:error) %></th>
42
+ </tr>
43
+ </thead>
44
+ <tbody>
45
+ <%- @jobs.each do |job| %>
46
+ <tr>
47
+ <td class="col-select"><input type="checkbox" class="check" name="key[]" value="<%= action.h(job.jid) %>"></td>
48
+ <td><span class="mono" style="font-size:12px"><%= job["failed_at"] ? action.relative_time(Time.at(job["failed_at"])) : "—" %></span></td>
49
+ <td><%= action.h(job["queue"]) %></td>
50
+ <td><a href="<%= action.root_path %>morgue/<%= action.h(job.jid) %>"><span class="job-name"><%= action.h(action.job_display_class(job)) %></span></a></td>
51
+ <td><code class="args" style="font-size:11px"><%= action.h(action.format_args_short(job)) %></code></td>
52
+ <td style="color:var(--red); font-size:12px"><%= action.h(action.truncate("#{job["error_class"]}: #{job["error_message"]}", 120)) %></td>
53
+ </tr>
54
+ <%- end %>
55
+ </tbody>
56
+ </table>
57
+ </div>
58
+ <div class="panel-foot">
59
+ <%= action.render_partial(:paging) %>
60
+ </div>
61
+ </div>
62
+ </form>
63
+
64
+ <div style="display:flex; gap:8px; flex-wrap:wrap; margin-top:8px;">
65
+ <form method="post" action="<%= action.root_path %>morgue/all/retry" style="display:inline">
66
+ <button type="submit" class="btn ghost"><%= action.t(:retry_all) %></button>
67
+ </form>
68
+ <form method="post" action="<%= action.root_path %>morgue/all/delete" style="display:inline">
69
+ <button type="submit" class="btn danger"><%= action.t(:delete_all) %></button>
70
+ </form>
71
+ </div>
72
+ <%- end %>
73
+
74
+ <script nonce="<%= nonce %>">
75
+ (function () {
76
+ var all = document.getElementById("select-all");
77
+ if (!all) return;
78
+ all.addEventListener("change", function () {
79
+ document.querySelectorAll("input.check[name='key[]']").forEach(function (c) { c.checked = all.checked; });
80
+ });
81
+ })();
82
+ </script>
@@ -0,0 +1,190 @@
1
+ <% @page_title = "Queue: #{@queue.name}" %>
2
+ <% @breadcrumb = [{label: "Queues", href: "#{action.root_path}queues"}, {label: @queue.name}] %>
3
+
4
+ <div class="page-head">
5
+ <div class="page-title-block">
6
+ <div class="eyebrow">
7
+ <span>Queue</span>
8
+ </div>
9
+ <h1>
10
+ <span>Jobs in</span>
11
+ <span class="queue-name"><%= action.h(@queue.name) %></span>
12
+ <span class="badge-num"><%= @total %></span>
13
+ </h1>
14
+ </div>
15
+ <div class="page-head-meta">
16
+ <div class="meta-item">
17
+ <span class="meta-k">Latency</span>
18
+ <span class="meta-v"><%= "%.2fs" % @queue.latency %></span>
19
+ </div>
20
+ </div>
21
+ </div>
22
+
23
+ <div class="toolbar">
24
+ <div class="search">
25
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
26
+ <input id="queueSearch" placeholder="Search jobs, args, JIDs…" />
27
+ <kbd>⌘K</kbd>
28
+ </div>
29
+
30
+ <div class="divider-v"></div>
31
+
32
+ <a href="?<%= action.query_string("direction" => (@asc ? "desc" : "asc"), "page" => "1") %>" class="chip">
33
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>
34
+ <%= @asc ? "Newest first" : "Oldest first" %>
35
+ </a>
36
+
37
+ <div style="margin-left:auto; display:flex; gap:8px;">
38
+ <form method="post" action="<%= action.root_path %>queues/<%= action.h(@queue.name) %>" style="display:inline">
39
+ <%- if @queue.respond_to?(:paused?) && @queue.paused? %>
40
+ <button type="submit" name="unpause" value="1" class="btn ghost"><%= action.t(:unpause) %></button>
41
+ <%- elsif @queue.respond_to?(:pause!) %>
42
+ <button type="submit" name="pause" value="1" class="btn ghost">
43
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>
44
+ <%= action.t(:pause) %>
45
+ </button>
46
+ <%- end %>
47
+ <button type="submit" name="clear" value="1" class="btn danger"
48
+ onclick="return confirm('Clear all jobs from <%= action.h(@queue.name) %>?')">
49
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg>
50
+ <%= action.t(:clear) %>
51
+ </button>
52
+ </form>
53
+ </div>
54
+ </div>
55
+
56
+ <%- if @filtering %>
57
+ <div class="active-filters">
58
+ <span>Filtering by</span>
59
+ <span class="filter-tag">
60
+ <%- if @filter_args.empty? %>
61
+ <span><%= action.h(@filter_job) %></span>
62
+ <%- else %>
63
+ <span><%= action.h(@filter_job) %> &nbsp;·&nbsp; <%= action.h(@filter_args.length > 60 ? @filter_args[0, 60] + "…" : @filter_args) %></span>
64
+ <%- end %>
65
+ <a href="?<%= action.query_string("filter_job" => nil, "filter_args" => nil, "page" => "1") %>" class="x" title="Clear filter">✕</a>
66
+ </span>
67
+ <form method="post" action="<%= action.root_path %>queues/<%= action.h(@queue.name) %>/delete_filtered" style="display:inline">
68
+ <input type="hidden" name="filter_job" value="<%= action.h(@filter_job) %>">
69
+ <input type="hidden" name="filter_args" value="<%= action.h(@filter_args) %>">
70
+ <button type="submit" class="btn danger" style="padding:4px 10px;font-size:12px"
71
+ onclick="return confirm('Delete all <%= @total %> filtered job<%= @total == 1 ? "" : "s" %>?')">
72
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg>
73
+ Delete filtered
74
+ </button>
75
+ </form>
76
+ </div>
77
+ <%- end %>
78
+
79
+ <%- if @jobs.empty? %>
80
+ <div class="panel">
81
+ <div class="empty"><%= action.t(:no_jobs) %></div>
82
+ </div>
83
+ <%- else %>
84
+ <div class="panel">
85
+ <div class="panel-head">
86
+ <div class="ph-title">Jobs</div>
87
+ <div class="ph-meta">
88
+ <%- if @filtering %>
89
+ first <%= @jobs.size %> matches · newest first
90
+ <%- else %>
91
+ <%= @total %> total · sorted <%= @asc ? "newest first" : "oldest first" %>
92
+ <%- end %>
93
+ </div>
94
+ <div class="ph-spacer"></div>
95
+ </div>
96
+
97
+ <div class="table-head-wrap">
98
+ <table>
99
+ <colgroup>
100
+ <col style="width:56px"/>
101
+ <col style="width:auto"/>
102
+ <col style="width:36%"/>
103
+ <col style="width:160px"/>
104
+ <col style="width:90px"/>
105
+ </colgroup>
106
+ <thead>
107
+ <tr>
108
+ <th class="col-num">#</th>
109
+ <th>Job / JID</th>
110
+ <th>Arguments</th>
111
+ <th>Enqueued</th>
112
+ <th class="col-actions">Actions</th>
113
+ </tr>
114
+ </thead>
115
+ </table>
116
+ </div>
117
+ <div class="table-scroll">
118
+ <table id="queueTable">
119
+ <colgroup>
120
+ <col style="width:56px"/>
121
+ <col style="width:auto"/>
122
+ <col style="width:36%"/>
123
+ <col style="width:160px"/>
124
+ <col style="width:90px"/>
125
+ </colgroup>
126
+ <tbody>
127
+ <%- @jobs.each_with_index do |job, i| %>
128
+ <tr>
129
+ <td class="col-num"><%= @page * @per_page + i + 1 %></td>
130
+ <td>
131
+ <div class="job-cell">
132
+ <a href="?<%= action.query_string("filter_job" => action.job_display_class(job), "filter_args" => nil, "page" => "1") %>" class="job-name" title="Filter by this job class"><%= action.h(action.job_display_class(job)) %></a>
133
+ <span class="job-meta">
134
+ <span class="tag">JID <span class="mono"><%= action.h(job.jid.to_s) %></span></span>
135
+ </span>
136
+ </div>
137
+ </td>
138
+ <td>
139
+ <a href="?<%= action.query_string("filter_job" => action.job_display_class(job), "filter_args" => action.display_args(job), "page" => "1") %>" class="args-link" title="Filter by class + arguments">
140
+ <code class="args" style="font-size:11px"><%= action.h(action.format_args_short(job)) %></code>
141
+ </a>
142
+ </td>
143
+ <td><span class="mono" style="color:var(--text-dim);font-size:12px"><%= job["enqueued_at"] ? Time.at(job["enqueued_at"]).utc.strftime("%Y-%m-%d %H:%M:%S") : "—" %></span></td>
144
+ <td class="col-actions">
145
+ <span class="row-actions">
146
+ <form method="post" action="<%= action.root_path %>queues/<%= action.h(@queue.name) %>/delete" style="display:inline">
147
+ <input type="hidden" name="key_val" value="<%= action.h(job.jid) %>">
148
+ <button type="submit" class="iconlink danger" title="<%= action.t(:delete) %>">
149
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg>
150
+ </button>
151
+ </form>
152
+ </span>
153
+ </td>
154
+ </tr>
155
+ <%- end %>
156
+ </tbody>
157
+ </table>
158
+ </div>
159
+
160
+ <div class="panel-foot">
161
+ <%- if @filtering %>
162
+ <div class="paging" style="color:var(--text-faint);font-size:12px;justify-content:center">
163
+ Showing first <%= @per_page %> matches — filtered results are not paginated
164
+ </div>
165
+ <%- else %>
166
+ <%= action.render_partial(:paging) %>
167
+ <%- end %>
168
+ </div>
169
+ </div>
170
+ <%- end %>
171
+
172
+ <script nonce="<%= nonce %>">
173
+ (function () {
174
+ var input = document.getElementById("queueSearch");
175
+ var table = document.getElementById("queueTable");
176
+ if (!input || !table) return;
177
+ input.addEventListener("input", function () {
178
+ var q = input.value.toLowerCase();
179
+ table.querySelectorAll("tbody tr").forEach(function (row) {
180
+ row.style.display = q === "" || row.textContent.toLowerCase().includes(q) ? "" : "none";
181
+ });
182
+ });
183
+ document.addEventListener("keydown", function (e) {
184
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
185
+ e.preventDefault();
186
+ input.focus();
187
+ }
188
+ });
189
+ })();
190
+ </script>
@@ -0,0 +1,59 @@
1
+ <% @page_title = "Queues" %>
2
+
3
+ <div class="page-head">
4
+ <div class="page-title-block">
5
+ <div class="eyebrow"><span>Workers</span></div>
6
+ <h1>Queues <span class="badge-num"><%= @queues.size %></span></h1>
7
+ </div>
8
+ </div>
9
+
10
+ <%- if @queues.empty? %>
11
+ <div class="panel">
12
+ <div class="empty"><%= action.t(:no_jobs) %></div>
13
+ </div>
14
+ <%- else %>
15
+ <div class="panel">
16
+ <div class="panel-head">
17
+ <div class="ph-title">All Queues</div>
18
+ <div class="ph-meta mono"><%= @queues.size %> queues</div>
19
+ </div>
20
+ <div class="table-scroll">
21
+ <table>
22
+ <thead>
23
+ <tr>
24
+ <th><%= action.t(:queue_name) %></th>
25
+ <th><%= action.t(:size) %></th>
26
+ <th><%= action.t(:latency) %></th>
27
+ <th class="col-actions"><%= action.t(:actions) %></th>
28
+ </tr>
29
+ </thead>
30
+ <tbody>
31
+ <%- @queues.each do |queue| %>
32
+ <tr>
33
+ <td><a href="<%= action.root_path %>queues/<%= action.h(queue.name) %>"><span class="job-name"><%= action.h(queue.name) %></span></a></td>
34
+ <td><span class="mono" style="color:var(--cyan)"><%= queue.size %></span></td>
35
+ <td><span class="mono" style="font-size:12px"><%= "%.2f" % queue.latency %>s</span></td>
36
+ <td class="col-actions">
37
+ <form method="post" action="<%= action.root_path %>queues/<%= action.h(queue.name) %>" style="display:inline-flex; gap:4px;">
38
+ <%- if queue.respond_to?(:paused?) && queue.paused? %>
39
+ <button type="submit" name="unpause" value="1" class="iconlink" title="<%= action.t(:unpause) %>">
40
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg>
41
+ </button>
42
+ <%- elsif queue.respond_to?(:pause!) %>
43
+ <button type="submit" name="pause" value="1" class="iconlink" title="<%= action.t(:pause) %>">
44
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>
45
+ </button>
46
+ <%- end %>
47
+ <button type="submit" name="clear" value="1" class="iconlink danger" title="<%= action.t(:clear) %>"
48
+ onclick="return confirm('Clear all jobs from <%= action.h(queue.name) %>?')">
49
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg>
50
+ </button>
51
+ </form>
52
+ </td>
53
+ </tr>
54
+ <%- end %>
55
+ </tbody>
56
+ </table>
57
+ </div>
58
+ </div>
59
+ <%- end %>
@@ -0,0 +1,99 @@
1
+ <% @page_title = "Retries" %>
2
+
3
+ <div class="page-head">
4
+ <div class="page-title-block">
5
+ <div class="eyebrow"><span>Lifecycle</span></div>
6
+ <h1>Retries <span class="badge-num"><%= @total %></span></h1>
7
+ </div>
8
+ </div>
9
+
10
+ <%- if @jobs.empty? %>
11
+ <div class="panel">
12
+ <div class="empty"><%= action.t(:no_retries) %></div>
13
+ </div>
14
+ <%- else %>
15
+ <form method="post" action="<%= action.root_path %>retries">
16
+ <div class="panel">
17
+ <div class="panel-head">
18
+ <div class="ph-title">Retry Queue</div>
19
+ <div class="ph-meta mono"><%= @total %> jobs</div>
20
+ <div class="ph-spacer"></div>
21
+ <div class="bulk-bar">
22
+ <button type="submit" name="action" value="retry" class="btn ghost">
23
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
24
+ <%= action.t(:retry_now) %>
25
+ </button>
26
+ <button type="submit" name="action" value="kill" class="btn ghost">
27
+ <%= action.t(:kill) %>
28
+ </button>
29
+ <button type="submit" name="action" value="delete" class="btn danger">
30
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg>
31
+ <%= action.t(:delete) %>
32
+ </button>
33
+ </div>
34
+ </div>
35
+ <div class="table-scroll">
36
+ <table>
37
+ <colgroup>
38
+ <col style="width:32px"/>
39
+ <col class="w-sm"/>
40
+ <col class="w-xs"/>
41
+ <col class="w-md"/>
42
+ <col style="width:170px"/>
43
+ <col style="width:200px"/>
44
+ <col/>
45
+ </colgroup>
46
+ <thead>
47
+ <tr>
48
+ <th class="col-select"><input type="checkbox" class="check" id="select-all"></th>
49
+ <th class="col-compact"><%= action.t(:next_retry) %></th>
50
+ <th class="col-compact"><%= action.t(:retry_count) %></th>
51
+ <th><%= action.t(:queue) %></th>
52
+ <th><%= action.t(:class) %></th>
53
+ <th><%= action.t(:args) %></th>
54
+ <th><%= action.t(:error) %></th>
55
+ </tr>
56
+ </thead>
57
+ <tbody>
58
+ <%- @jobs.each do |job| %>
59
+ <tr>
60
+ <td class="col-select"><input type="checkbox" class="check" name="key[]" value="<%= action.h(job.jid) %>"></td>
61
+ <td class="col-compact"><span class="mono" style="font-size:12px"><%= job["retry_at"] ? action.relative_time(Time.at(job["retry_at"])) : "—" %></span></td>
62
+ <td class="col-compact"><span class="mono" style="font-size:12px"><%= job["retry_count"] || 0 %></span></td>
63
+ <td><span style="font-size:12px"><%= action.h(job["queue"]) %></span></td>
64
+ <td><a href="<%= action.root_path %>retries/<%= action.h(job.jid) %>"><span class="job-name"><%= action.h(action.job_display_class(job)) %></span></a></td>
65
+ <td><code class="args" style="font-size:11px"><%= action.h(action.format_args_short(job)) %></code></td>
66
+ <td style="color:var(--red);font-size:11.5px"><%= action.h(action.truncate("#{job["error_class"]}: #{job["error_message"]}", 120)) %></td>
67
+ </tr>
68
+ <%- end %>
69
+ </tbody>
70
+ </table>
71
+ </div>
72
+ <div class="panel-foot">
73
+ <%= action.render_partial(:paging) %>
74
+ </div>
75
+ </div>
76
+ </form>
77
+
78
+ <div style="display:flex; gap:8px; flex-wrap:wrap; margin-top:8px;">
79
+ <form method="post" action="<%= action.root_path %>retries/all/retry" style="display:inline">
80
+ <button type="submit" class="btn ghost"><%= action.t(:retry_all) %></button>
81
+ </form>
82
+ <form method="post" action="<%= action.root_path %>retries/all/delete" style="display:inline">
83
+ <button type="submit" class="btn danger"><%= action.t(:delete_all) %></button>
84
+ </form>
85
+ <form method="post" action="<%= action.root_path %>retries/all/kill" style="display:inline">
86
+ <button type="submit" class="btn ghost"><%= action.t(:kill_all) %></button>
87
+ </form>
88
+ </div>
89
+ <%- end %>
90
+
91
+ <script nonce="<%= nonce %>">
92
+ (function () {
93
+ var all = document.getElementById("select-all");
94
+ if (!all) return;
95
+ all.addEventListener("change", function () {
96
+ document.querySelectorAll("input.check[name='key[]']").forEach(function (c) { c.checked = all.checked; });
97
+ });
98
+ })();
99
+ </script>
@@ -0,0 +1,32 @@
1
+ <% @page_title = "Retry: #{action.job_display_class(@job)}" %>
2
+ <% @breadcrumb = [{label: "Retries", href: "#{action.root_path}retries"}, {label: action.job_display_class(@job)}] %>
3
+
4
+ <div class="page-head">
5
+ <div class="page-title-block">
6
+ <div class="eyebrow"><span>Lifecycle</span></div>
7
+ <h1><span class="queue-name"><%= action.h(action.job_display_class(@job)) %></span></h1>
8
+ </div>
9
+ <div class="page-head-meta">
10
+ <form method="post" action="<%= action.root_path %>retries" style="display:inline-flex; gap:8px;">
11
+ <input type="hidden" name="key[]" value="<%= action.h(@job.jid) %>">
12
+ <button type="submit" name="action" value="retry" class="btn ghost">
13
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
14
+ <%= action.t(:retry_now) %>
15
+ </button>
16
+ <button type="submit" name="action" value="kill" class="btn ghost"><%= action.t(:kill) %></button>
17
+ <button type="submit" name="action" value="delete" class="btn danger">
18
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg>
19
+ <%= action.t(:delete) %>
20
+ </button>
21
+ </form>
22
+ </div>
23
+ </div>
24
+
25
+ <div class="panel">
26
+ <div class="panel-head">
27
+ <div class="ph-title">Job Details</div>
28
+ </div>
29
+ <div style="padding: 20px 16px;">
30
+ <%= action.render_partial(:job_info) %>
31
+ </div>
32
+ </div>
@@ -0,0 +1,79 @@
1
+ <% @page_title = "Scheduled" %>
2
+
3
+ <div class="page-head">
4
+ <div class="page-title-block">
5
+ <div class="eyebrow"><span>Lifecycle</span></div>
6
+ <h1>Scheduled <span class="badge-num"><%= @total %></span></h1>
7
+ </div>
8
+ </div>
9
+
10
+ <%- if @jobs.empty? %>
11
+ <div class="panel">
12
+ <div class="empty"><%= action.t(:no_scheduled) %></div>
13
+ </div>
14
+ <%- else %>
15
+ <form method="post" action="<%= action.root_path %>scheduled">
16
+ <div class="panel">
17
+ <div class="panel-head">
18
+ <div class="ph-title">Scheduled Jobs</div>
19
+ <div class="ph-meta mono"><%= @total %> jobs</div>
20
+ <div class="ph-spacer"></div>
21
+ <div class="bulk-bar">
22
+ <button type="submit" name="action" value="add_to_queue" class="btn ghost"><%= action.t(:add_to_queue) %></button>
23
+ <button type="submit" name="action" value="delete" class="btn danger">
24
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg>
25
+ <%= action.t(:delete) %>
26
+ </button>
27
+ </div>
28
+ </div>
29
+ <div class="table-scroll">
30
+ <table>
31
+ <thead>
32
+ <tr>
33
+ <th class="col-select"><input type="checkbox" class="check" id="select-all"></th>
34
+ <th><%= action.t(:scheduled_at) %></th>
35
+ <th><%= action.t(:queue) %></th>
36
+ <th><%= action.t(:class) %></th>
37
+ <th><%= action.t(:args) %></th>
38
+ </tr>
39
+ </thead>
40
+ <tbody>
41
+ <%- @jobs.each do |job| %>
42
+ <tr>
43
+ <td class="col-select"><input type="checkbox" class="check" name="key[]" value="<%= action.h(job.jid) %>"></td>
44
+ <td title="<%= job.at ? job.at.utc.strftime('%Y-%m-%d %H:%M:%S UTC') : '' %>">
45
+ <span class="mono" style="font-size:12px"><%= job.at ? action.relative_time(job.at) : "—" %></span>
46
+ </td>
47
+ <td><%= action.h(job["queue"]) %></td>
48
+ <td><a href="<%= action.root_path %>scheduled/<%= action.h(job.jid) %>"><span class="job-name"><%= action.h(action.job_display_class(job)) %></span></a></td>
49
+ <td><code class="args" style="font-size:11px"><%= action.h(action.format_args_short(job)) %></code></td>
50
+ </tr>
51
+ <%- end %>
52
+ </tbody>
53
+ </table>
54
+ </div>
55
+ <div class="panel-foot">
56
+ <%= action.render_partial(:paging) %>
57
+ </div>
58
+ </div>
59
+ </form>
60
+
61
+ <div style="display:flex; gap:8px; flex-wrap:wrap; margin-top:8px;">
62
+ <form method="post" action="<%= action.root_path %>scheduled/all/add_to_queue" style="display:inline">
63
+ <button type="submit" class="btn ghost"><%= action.t(:add_all_to_queue) %></button>
64
+ </form>
65
+ <form method="post" action="<%= action.root_path %>scheduled/all/delete" style="display:inline">
66
+ <button type="submit" class="btn danger"><%= action.t(:delete_all) %></button>
67
+ </form>
68
+ </div>
69
+ <%- end %>
70
+
71
+ <script nonce="<%= nonce %>">
72
+ (function () {
73
+ var all = document.getElementById("select-all");
74
+ if (!all) return;
75
+ all.addEventListener("change", function () {
76
+ document.querySelectorAll("input.check[name='key[]']").forEach(function (c) { c.checked = all.checked; });
77
+ });
78
+ })();
79
+ </script>
@@ -0,0 +1,31 @@
1
+ <% @page_title = "Scheduled: #{action.job_display_class(@job)}" %>
2
+ <% @breadcrumb = [{label: "Scheduled", href: "#{action.root_path}scheduled"}, {label: action.job_display_class(@job)}] %>
3
+
4
+ <div class="page-head">
5
+ <div class="page-title-block">
6
+ <div class="eyebrow"><span>Lifecycle</span></div>
7
+ <h1><span class="queue-name"><%= action.h(action.job_display_class(@job)) %></span></h1>
8
+ </div>
9
+ <div class="page-head-meta">
10
+ <form method="post" action="<%= action.root_path %>scheduled" style="display:inline-flex; gap:8px;">
11
+ <input type="hidden" name="key[]" value="<%= action.h(@job.jid) %>">
12
+ <button type="submit" name="action" value="add_to_queue" class="btn ghost"><%= action.t(:add_to_queue) %></button>
13
+ <button type="submit" name="action" value="delete" class="btn danger">
14
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg>
15
+ <%= action.t(:delete) %>
16
+ </button>
17
+ </form>
18
+ </div>
19
+ </div>
20
+
21
+ <div class="panel">
22
+ <div class="panel-head">
23
+ <div class="ph-title">Job Details</div>
24
+ <%- if @job.at %>
25
+ <div class="ph-meta">Scheduled for <%= @job.at.utc.strftime("%Y-%m-%d %H:%M:%S UTC") %></div>
26
+ <%- end %>
27
+ </div>
28
+ <div style="padding: 20px 16px;">
29
+ <%= action.render_partial(:job_info) %>
30
+ </div>
31
+ </div>