cosmonats 0.8.0 → 0.9.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.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +21 -11
  3. data/lib/cosmo/api/counter.rb +1 -1
  4. data/lib/cosmo/api/cron/entry.rb +10 -11
  5. data/lib/cosmo/api/cron.rb +3 -3
  6. data/lib/cosmo/api/job.rb +1 -1
  7. data/lib/cosmo/api/kv.rb +3 -3
  8. data/lib/cosmo/api/stats/busy.rb +5 -0
  9. data/lib/cosmo/api/stats/metrics.rb +3 -3
  10. data/lib/cosmo/api/stats.rb +15 -0
  11. data/lib/cosmo/api/stream.rb +7 -2
  12. data/lib/cosmo/client.rb +10 -13
  13. data/lib/cosmo/job/data.rb +1 -1
  14. data/lib/cosmo/job/processor.rb +5 -6
  15. data/lib/cosmo/utils.rb +0 -1
  16. data/lib/cosmo/version.rb +1 -1
  17. data/lib/cosmo/web/assets/app.css +271 -629
  18. data/lib/cosmo/web/assets/app.js +158 -0
  19. data/lib/cosmo/web/assets/fonts/OFL.txt +92 -0
  20. data/lib/cosmo/web/assets/fonts/geist-mono.woff2 +0 -0
  21. data/lib/cosmo/web/assets/fonts/geist.woff2 +0 -0
  22. data/lib/cosmo/web/assets/logo.svg +10 -0
  23. data/lib/cosmo/web/cache.rb +75 -0
  24. data/lib/cosmo/web/chart.rb +62 -38
  25. data/lib/cosmo/web/circuit.rb +55 -0
  26. data/lib/cosmo/web/context.rb +0 -4
  27. data/lib/cosmo/web/controllers/application.rb +16 -0
  28. data/lib/cosmo/web/controllers/batches.rb +1 -0
  29. data/lib/cosmo/web/controllers/crons.rb +9 -2
  30. data/lib/cosmo/web/controllers/jobs.rb +50 -59
  31. data/lib/cosmo/web/controllers/metrics.rb +1 -0
  32. data/lib/cosmo/web/controllers/overview.rb +70 -0
  33. data/lib/cosmo/web/controllers/processes.rb +33 -0
  34. data/lib/cosmo/web/controllers/streams.rb +26 -28
  35. data/lib/cosmo/web/helpers/application.rb +57 -0
  36. data/lib/cosmo/web/renderer.rb +19 -6
  37. data/lib/cosmo/web/throughput.rb +104 -0
  38. data/lib/cosmo/web/views/batches/_table.erb +42 -54
  39. data/lib/cosmo/web/views/batches/index.erb +3 -10
  40. data/lib/cosmo/web/views/crons/_table.erb +32 -53
  41. data/lib/cosmo/web/views/crons/index.erb +2 -9
  42. data/lib/cosmo/web/views/jobs/_args.erb +5 -0
  43. data/lib/cosmo/web/views/jobs/_busy.erb +25 -121
  44. data/lib/cosmo/web/views/jobs/_dead.erb +42 -82
  45. data/lib/cosmo/web/views/jobs/_enqueued.erb +33 -113
  46. data/lib/cosmo/web/views/jobs/_panel.erb +29 -0
  47. data/lib/cosmo/web/views/jobs/_queue_row.erb +18 -0
  48. data/lib/cosmo/web/views/jobs/_queues.erb +20 -0
  49. data/lib/cosmo/web/views/jobs/_scheduled.erb +20 -54
  50. data/lib/cosmo/web/views/jobs/index.erb +4 -17
  51. data/lib/cosmo/web/views/layout.erb +73 -22
  52. data/lib/cosmo/web/views/metrics/_chart.erb +37 -32
  53. data/lib/cosmo/web/views/metrics/_panel.erb +3 -3
  54. data/lib/cosmo/web/views/metrics/_table.erb +24 -40
  55. data/lib/cosmo/web/views/metrics/index.erb +8 -16
  56. data/lib/cosmo/web/views/overview/_badges.erb +3 -0
  57. data/lib/cosmo/web/views/overview/_kpis.erb +19 -0
  58. data/lib/cosmo/web/views/overview/_running.erb +18 -0
  59. data/lib/cosmo/web/views/overview/_throughput.erb +50 -0
  60. data/lib/cosmo/web/views/overview/_workers.erb +25 -0
  61. data/lib/cosmo/web/views/overview/index.erb +28 -0
  62. data/lib/cosmo/web/views/processes/_table.erb +67 -0
  63. data/lib/cosmo/web/views/processes/index.erb +1 -0
  64. data/lib/cosmo/web/views/shared/_pager.erb +22 -0
  65. data/lib/cosmo/web/views/shared/unavailable.erb +7 -0
  66. data/lib/cosmo/web/views/streams/_info.erb +28 -86
  67. data/lib/cosmo/web/views/streams/_list.erb +7 -0
  68. data/lib/cosmo/web/views/streams/_pause_banner.erb +8 -16
  69. data/lib/cosmo/web/views/streams/_stream.erb +52 -0
  70. data/lib/cosmo/web/views/streams/index.erb +3 -11
  71. data/lib/cosmo/web/views/streams/info.erb +3 -11
  72. data/lib/cosmo/web.rb +66 -35
  73. data/sig/cosmo/api/cron/entry.rbs +2 -1
  74. data/sig/cosmo/api/stats/busy.rbs +2 -0
  75. data/sig/cosmo/api/stats/metrics.rbs +1 -1
  76. data/sig/cosmo/api/stats.rbs +4 -0
  77. data/sig/cosmo/api/stream.rbs +2 -0
  78. data/sig/cosmo/client.rbs +1 -0
  79. metadata +31 -18
  80. data/lib/cosmo/utils/overrides.rb +0 -206
  81. data/lib/cosmo/utils/warnings.rb +0 -17
  82. data/lib/cosmo/web/controllers/actions.rb +0 -16
  83. data/lib/cosmo/web/views/actions/index.erb +0 -7
  84. data/lib/cosmo/web/views/jobs/_processes.erb +0 -145
  85. data/lib/cosmo/web/views/jobs/_stats.erb +0 -77
  86. data/lib/cosmo/web/views/jobs/_tabs.erb +0 -14
  87. data/lib/cosmo/web/views/jobs/busy.erb +0 -18
  88. data/lib/cosmo/web/views/jobs/dead.erb +0 -18
  89. data/lib/cosmo/web/views/jobs/enqueued.erb +0 -18
  90. data/lib/cosmo/web/views/jobs/scheduled.erb +0 -18
  91. data/lib/cosmo/web/views/streams/_stream_row.erb +0 -42
  92. data/lib/cosmo/web/views/streams/_table.erb +0 -25
@@ -1,58 +1,46 @@
1
1
  <% if @batches.empty? -%>
2
- <div class="alert alert-success">No batches tracked.</div>
2
+ <section class="card"><div class="empty"><b>No batches tracked</b>Group jobs with <code>Cosmo::Batch.new.jobs { … }</code>.</div></section>
3
3
  <% else -%>
4
- <div class="table-container">
5
- <table>
6
- <thead>
7
- <tr>
8
- <th>Batch</th>
9
- <th>Parent</th>
10
- <th>Status</th>
11
- <th>Progress</th>
12
- <th>Total</th>
13
- <th>Pending</th>
14
- <th>Succeeded</th>
15
- <th>Failed</th>
16
- <th>Created</th>
17
- </tr>
18
- </thead>
19
- <tbody>
20
- <% @batches.each do |b| -%>
21
- <% stats = b.stats -%>
22
- <% total = stats[:total].to_i -%>
23
- <tr>
24
- <td><code><%= h(b.bid) %></code></td>
25
- <td>
26
- <% if b.parent_id -%>
27
- <code><%= h(b.parent_id) %></code>
28
- <% else -%>
29
- <span class="text-muted">&mdash;</span>
30
- <% end -%>
31
- </td>
32
- <td>
33
- <% if !b.ready? -%>
34
- <span class="badge badge-info">Open</span>
35
- <% elsif stats[:failed].to_i.positive? -%>
36
- <span class="badge badge-danger">Complete</span>
37
- <% else -%>
38
- <span class="badge badge-success">Success</span>
39
- <% end -%>
40
- </td>
41
- <td>
42
- <% if total.positive? -%>
43
- <%= ((total - stats[:pending].to_i) * 100 / total).clamp(0, 100) %>%
44
- <% else -%>
45
- <span class="text-muted">&mdash;</span>
46
- <% end -%>
47
- </td>
48
- <td><%= format_numbers(stats[:total]) %></td>
49
- <td><%= format_numbers(stats[:pending]) %></td>
50
- <td><%= format_numbers(stats[:succeeded]) %></td>
51
- <td><%= format_numbers(stats[:failed]) %></td>
52
- <td><%= format_timestamp(b.created_at) %></td>
53
- </tr>
54
- <% end -%>
55
- </tbody>
56
- </table>
4
+ <div class="batches">
5
+ <% @batches.each do |b| -%>
6
+ <% stats = b.stats -%>
7
+ <% total = stats[:total].to_i -%>
8
+ <% succeeded = stats[:succeeded].to_i -%>
9
+ <% failed = stats[:failed].to_i -%>
10
+ <% done = total - stats[:pending].to_i -%>
11
+ <% status, tone = if !b.ready? then ['open', 'var(--acc)'] elsif failed.positive? then ['complete with failures', 'var(--warn)'] else ['success', 'var(--ok)'] end -%>
12
+ <% callbacks = b.callbacks.compact -%>
13
+ <section class="card batch">
14
+ <div class="batch-top">
15
+ <div class="stack" style="flex:1;gap:4px">
16
+ <div class="mono strong ellip"><%= h(b.bid) %></div>
17
+ <div class="small-mono ellip">
18
+ <% if b.parent_id -%>parent <%= h(b.parent_id) %> · <% end -%>started <%= b.created_at ? "#{elapsed(b.created_at)} ago" : '—' %>
19
+ </div>
20
+ </div>
21
+ <span class="pill" style="--tone:<%= tone %>"><%= status %></span>
22
+ </div>
23
+ <div class="batch-pct">
24
+ <b><%= total.positive? ? (done * 100 / total).clamp(0, 100) : 0 %>%</b>
25
+ <span class="mono mute" style="font-size:11.5px"><%= compact_number(done) %> / <%= compact_number(total) %></span>
26
+ </div>
27
+ <div class="progress">
28
+ <% if total.positive? -%>
29
+ <i style="width:<%= (succeeded * 100.0 / total).round(2) %>%;background:<%= hue(b.bid) %>"></i>
30
+ <% if failed.positive? -%><i style="width:<%= [failed * 100.0 / total, 0.6].max.round(2) %>%;background:var(--err)"></i><% end -%>
31
+ <% end -%>
32
+ </div>
33
+ <div class="batch-foot">
34
+ <span><span class="num" style="color:var(--ink)"><%= format_numbers(succeeded) %></span> succeeded</span>
35
+ <span><span class="num" style="color:var(--err)"><%= format_numbers(failed) %></span> failed</span>
36
+ <span><span class="num" style="color:var(--ink)"><%= format_numbers(stats[:pending]) %></span> pending</span>
37
+ <% if callbacks.any? -%>
38
+ <span class="cb">
39
+ <% callbacks.each do |event, callback| -%><span>on_<%= event %> → <%= h(callback[:class]) %></span><% end -%>
40
+ </span>
41
+ <% end -%>
42
+ </div>
43
+ </section>
44
+ <% end -%>
57
45
  </div>
58
46
  <% end -%>
@@ -1,10 +1,3 @@
1
- <section>
2
- <header><%= render('jobs/_tabs') %></header>
3
-
4
- <div id="batches-table"
5
- hx-get="<%= url_for('/batches/_table') %>"
6
- hx-trigger="load, every <%= poll_interval %>s"
7
- hx-swap="innerHTML">
8
- <div class="alert alert-info">Loading batches&hellip;</div>
9
- </div>
10
- </section>
1
+ <div hx-get="<%= url_for('/batches/_table') %>" hx-trigger="load, cosmo:poll" data-poll="<%= poll_interval %>" hx-swap="innerHTML">
2
+ <section class="card"><div class="loading">Loading batches&hellip;</div></section>
3
+ </div>
@@ -1,58 +1,37 @@
1
1
  <% if @schedules.empty? -%>
2
- <div class="alert alert-success">
3
- No cron schedules deployed in NATS.<br>
4
- Add a <code>cron:</code> section to <code>cosmo.yml</code> and run
5
- <code>cosmo --setup</code> to publish them.
2
+ <div class="empty">
3
+ <b>No cron schedules deployed in NATS</b>
4
+ <span>Add a <code>cron:</code> section to <code>cosmo.yml</code> and run <code>cosmo --setup</code> to publish them.</span>
6
5
  </div>
7
6
  <% else -%>
8
- <div class="table-container">
9
- <table>
10
- <thead>
11
- <tr>
12
- <th>Job</th>
13
- <th>Expression</th>
14
- <th>Timezone</th>
15
- <th>Args</th>
16
- <th>Stream</th>
17
- <th>Actions</th>
18
- </tr>
19
- </thead>
20
- <tbody>
21
- <% @schedules.each do |s| -%>
22
- <tr id="cron-row-<%= h(s[:registry_key].to_s.gsub("/", "-")) %>">
23
- <td>
24
- <div class="job-class"><%= h(s[:class].to_s) %></div>
25
- <% if s[:name] -%>
26
- <code class="text-muted" style="font-size:0.8em;"><%= h(s[:name].to_s) %></code>
27
- <% end -%>
28
- <div style="font-size:0.75em; color:#888; margin-top:2px;">
29
- <code><%= h(s[:schedule_subject].to_s) %></code>
30
- </div>
31
- </td>
32
- <td><code class="subject-tag"><%= h(s[:schedule].to_s) %></code></td>
33
- <td><%= h(s[:timezone] || "UTC") %></td>
34
- <td>
35
- <% if s[:args]&.any? -%>
36
- <code><%= h(s[:args].inspect) %></code>
37
- <% else -%>
38
- <span class="text-muted">&mdash;</span>
39
- <% end -%>
40
- </td>
41
- <td><code><%= h(s[:stream].to_s) %></code></td>
42
- <td style="white-space:nowrap;">
43
- <a hx-post="<%= url_for('/crons/run', { subject: u(s[:schedule_subject].to_s) }) %>"
44
- class="btn btn-success btn-sm"
45
- title="Dispatch this job now, bypassing the schedule timer">&#x25BA; Run Now</a>
46
- <a hx-delete="<%= url_for('/crons/delete', { subject: u(s[:schedule_subject].to_s) }) %>"
47
- hx-target="#crons-table"
48
- hx-swap="innerHTML"
49
- hx-confirm="Delete '<%= h(s[:schedule_subject].to_s) %>'? NATS will stop firing it."
50
- class="btn btn-danger btn-sm"
51
- title="Purge schedule from NATS">&#x1F5D1; Delete</a>
52
- </td>
53
- </tr>
7
+ <div class="scroll"><div style="min-width:900px">
8
+ <div class="thead t-crons"><span>Schedule</span><span>Cron</span><span>Stream</span><span>Args</span><span></span></div>
9
+ <% @schedules.each do |s| -%>
10
+ <% subject = s[:schedule_subject].to_s -%>
11
+ <div class="trow hover t-crons">
12
+ <div class="stack">
13
+ <span class="strong ellip"><%= h(s[:name] || s[:class]) %></span>
14
+ <% if s[:name] -%><span class="small-mono ellip"><%= h(s[:class]) %></span><% end -%>
15
+ <span class="small-mono ellip" title="<%= h(subject) %>"><%= h(subject) %></span>
16
+ </div>
17
+ <div class="stack">
18
+ <span class="mono" style="font-size:12.5px"><%= h(s[:schedule]) %></span>
19
+ <span class="mute" style="font-size:11.5px"><%= h(s[:timezone] || 'UTC') %></span>
20
+ </div>
21
+ <span class="qname"><span class="dot" style="background:<%= hue(s[:stream]) %>"></span><span class="mono ellip"><%= h(s[:stream]) %></span></span>
22
+ <% if s[:args]&.any? -%>
23
+ <span class="mono mute ellip" style="font-size:11.5px" title="<%= h(s[:args].inspect) %>"><%= h(s[:args].inspect) %></span>
24
+ <% else -%>
25
+ <span class="faint">—</span>
54
26
  <% end -%>
55
- </tbody>
56
- </table>
57
- </div>
27
+ <div class="btns">
28
+ <button type="button" class="btn" title="Dispatch this job now, bypassing the schedule timer"
29
+ hx-post="<%= url_for('/crons/run', { subject: u(subject) }) %>" hx-swap="none" hx-indicator="#global-spinner">► Run now</button>
30
+ <button type="button" class="btn danger" title="Purge schedule from NATS"
31
+ hx-delete="<%= url_for('/crons/delete', { subject: u(subject) }) %>" hx-target="#crons-table" hx-swap="innerHTML"
32
+ hx-indicator="#global-spinner" hx-confirm="Delete '<%= h(subject) %>'? NATS will stop firing it.">Delete</button>
33
+ </div>
34
+ </div>
35
+ <% end -%>
36
+ </div></div>
58
37
  <% end -%>
@@ -1,10 +1,3 @@
1
- <section>
2
- <header><%= render('jobs/_tabs') %></header>
3
-
4
- <div id="crons-table"
5
- hx-get="<%= url_for('/crons/_table') %>"
6
- hx-trigger="load, every 10s"
7
- hx-swap="innerHTML">
8
- <div class="alert alert-info">Loading cron schedules&hellip;</div>
9
- </div>
1
+ <section class="card" id="crons-table" hx-get="<%= url_for('/crons/_table') %>" hx-trigger="load, cosmo:poll" data-poll="10" hx-swap="innerHTML">
2
+ <div class="loading">Loading cron schedules&hellip;</div>
10
3
  </section>
@@ -0,0 +1,5 @@
1
+ <% json = (JSON.generate(@args) rescue @args.to_s) -%>
2
+ <details class="stack">
3
+ <summary class="mono mute ellip" style="font-size:11.5px" title="<%= h(json) %>"><%= h(json) %></summary>
4
+ <pre class="code-block"><%= h((JSON.pretty_generate(@args) rescue @args.to_s)) %></pre>
5
+ </details>
@@ -1,121 +1,25 @@
1
- <% nav = @polling ? { limit: @limit } : { limit: @limit, poll: 0 } -%>
2
- <div id="busy-poller"
3
- <% if @polling -%>
4
- hx-get="<%= url_for('/jobs/_busy', page: @page, limit: @limit) %>"
5
- hx-trigger="every <%= poll_interval %>s"
6
- hx-swap="outerHTML"
7
- <% end -%>
8
- >
9
- <div class="pending">
10
- <span></span>
11
- <span class="poll-status">
12
- <span class="text-muted"><%= @total %> job(s) running</span>
13
- <% if @polling -%>
14
- <a class="btn btn-sm"
15
- hx-get="<%= url_for('/jobs/_busy', nav.merge(page: @page, poll: 0)) %>"
16
- hx-target="#busy-poller"
17
- hx-swap="outerHTML"
18
- hx-indicator="#global-spinner">&#9209; Stop Polling</a>
19
- <% else -%>
20
- <a class="btn btn-sm btn-primary"
21
- hx-get="<%= url_for('/jobs/_busy', page: @page, limit: @limit) %>"
22
- hx-target="#busy-poller"
23
- hx-swap="outerHTML"
24
- hx-indicator="#global-spinner">&#x25B6; Live Poll</a>
25
- <% end -%>
26
- </span>
27
- </div>
28
-
29
- <% if @jobs.empty? -%>
30
- <div class="alert alert-success">No jobs are currently running</div>
31
- <% else -%>
32
- <div class="table-container">
33
- <table>
34
- <thead>
35
- <tr>
36
- <th>Job</th>
37
- <th>JID[Args]</th>
38
- <th>Stream</th>
39
- <th>Worker</th>
40
- <th>Attempt</th>
41
- <th>Running for</th>
42
- </tr>
43
- </thead>
44
- <tbody>
45
- <% @jobs.each do |job| -%>
46
- <tr>
47
- <td>
48
- <div class="job-class">
49
- <%= h(job.dig(:data, :class).to_s) %>
50
- </div>
51
- </td>
52
- <td>
53
- <div class="job-id">
54
- <details>
55
- <summary>
56
- <code><%= h(job.dig(:data, :jid)) %></code>
57
- </summary>
58
- <code><%= h(JSON.pretty_generate(job.dig(:data, :args))) %></code>
59
- </details>
60
- </div>
61
- </td>
62
- <td>
63
- <code><%= h(job[:stream].to_s) %></code>
64
- </td>
65
- <td><code style="font-size: var(--font-size-small);"><%= h(job[:worker].to_s) %></code></td>
66
- <td>
67
- <% delivery = job[:delivery].to_i -%>
68
- <% if delivery > 1 -%>
69
- <span class="badge <%= delivery >= 5 ? 'badge-danger' : 'badge-warning' %>"><%= delivery %></span>
70
- <% else -%>
71
- <span class="text-muted"><%= delivery.zero? ? '&mdash;' : delivery %></span>
72
- <% end -%>
73
- </td>
74
- <td>
75
- <%= elapsed(job[:started_at]) %>
76
- </td>
77
- </tr>
78
- <% end -%>
79
- </tbody>
80
- </table>
81
- </div>
82
- <% end -%>
83
-
84
- <% if @total_pages.to_i > 1 -%>
85
- <div class="pagination">
86
- <% if @page > 1 -%>
87
- <a hx-get="<%= url_for('/jobs/_busy', nav.merge(page: @page - 1)) %>"
88
- hx-target="#busy-poller"
89
- hx-swap="outerHTML"
90
- hx-indicator="#global-spinner"
91
- class="btn">&#x2190; Prev</a>
92
- <% else -%>
93
- <span class="btn btn-disabled">&#x2190; Prev</span>
94
- <% end -%>
95
-
96
- <% pages(@page, @total_pages).each do |p| -%>
97
- <% if p == :gap -%>
98
- <span class="pagination-gap">&hellip;</span>
99
- <% elsif p == @page -%>
100
- <span class="btn btn-primary btn-disabled"><%= p %></span>
101
- <% else -%>
102
- <a hx-get="<%= url_for('/jobs/_busy', nav.merge(page: p)) %>"
103
- hx-target="#busy-poller"
104
- hx-swap="outerHTML"
105
- hx-indicator="#global-spinner"
106
- class="btn"><%= p %></a>
107
- <% end -%>
108
- <% end -%>
109
-
110
- <% if @page < @total_pages -%>
111
- <a hx-get="<%= url_for('/jobs/_busy', nav.merge(page: @page + 1)) %>"
112
- hx-target="#busy-poller"
113
- hx-swap="outerHTML"
114
- hx-indicator="#global-spinner"
115
- class="btn">Next &#x2192;</a>
116
- <% else -%>
117
- <span class="btn btn-disabled">Next &#x2192;</span>
118
- <% end -%>
119
- </div>
120
- <% end -%>
121
- </div>
1
+ <% if @jobs.empty? -%>
2
+ <div class="empty"><b>All quiet</b>No jobs are running right now.</div>
3
+ <% else -%>
4
+ <div class="scroll"><div style="min-width:860px">
5
+ <div class="thead t-jobs"><span>JID</span><span>Class</span><span>Args</span><span>Queue</span><span>Status</span><span class="r">Try</span><span class="r">Running for</span></div>
6
+ <% @jobs.each do |job| -%>
7
+ <% delivery = job[:delivery].to_i -%>
8
+ <% retries = job.dig(:data, :retry) -%>
9
+ <div class="trow hover t-jobs">
10
+ <span class="mono mute ellip" style="font-size:11.5px" title="<%= h(job.dig(:data, :jid)) %>"><%= h(job.dig(:data, :jid)) %></span>
11
+ <div class="stack">
12
+ <span class="strong ellip"><%= h(job.dig(:data, :class)) %></span>
13
+ <span class="small-mono ellip" title="<%= h(job[:worker]) %>"><%= h(job[:worker]) %></span>
14
+ </div>
15
+ <%= render('jobs/_args', { args: job.dig(:data, :args) }) %>
16
+ <span class="mono ellip" style="font-size:12px"><%= h(job[:stream]) %></span>
17
+ <span class="pill" style="--tone:var(--acc)">running</span>
18
+ <span class="mono r" style="font-size:12px;color:<%= delivery >= 5 ? 'var(--err)' : delivery > 1 ? 'var(--warn)' : 'var(--mute)' %>">
19
+ <%= delivery.zero? ? '—' : delivery %><%= "/#{retries.to_i + 1}" if retries.is_a?(Integer) && delivery.positive? %>
20
+ </span>
21
+ <span class="mono mute r" style="font-size:11.5px"><%= elapsed(job[:started_at]) %></span>
22
+ </div>
23
+ <% end -%>
24
+ </div></div>
25
+ <% end -%>
@@ -1,83 +1,43 @@
1
- <div id="dead-jobs">
2
- <% if @jobs.empty? -%>
3
- <div class="alert alert-success">No dead jobs found</div>
4
- <% else -%>
5
- <div class="table-container">
6
- <table>
7
- <thead>
8
- <tr>
9
- <th>Job</th>
10
- <th>JID[Args]</th>
11
- <th>Stream</th>
12
- <th>Error</th>
13
- <th>Failed</th>
14
- <th>Actions</th>
15
- </tr>
16
- </thead>
17
- <tbody>
18
- <% @jobs.each do |job| -%>
19
- <tr class="dead-row" id="dead-row-<%= job.seq %>">
20
- <td>
21
- <div class="job-class">
22
- <%= h(job.data[:class].to_s) %>
23
- </div>
24
- </td>
25
- <td>
26
- <div class="job-id">
27
- <details>
28
- <summary>
29
- <code><%= h(job.data[:jid].to_s) %></code>
30
- </summary>
31
- <code><%= h((JSON.pretty_generate(job.data[:args]) rescue job.data[:args].to_s)) %></code>
32
- </details>
33
- </div>
34
- </td>
35
- <td style="white-space: nowrap;">
36
- <% if job.x_stream -%><code><%= h(job.x_stream) %></code><% else -%><span class="text-muted">&mdash;</span><% end -%>
37
- </td>
38
- <td style="min-width: 420px;">
39
- <% if job.error? -%>
40
- <div class="error-message">
41
- <strong><%= h(job.error_class.to_s.empty? ? "Error" : job.error_class) %></strong>
42
- <p><%= h(job.error_message) %></p>
43
- <% if job.error_backtrace -%>
44
- <details>
45
- <summary>Backtrace</summary>
46
- <code><%= h(job.error_backtrace_lines.join("\n")) %></code>
47
- </details>
48
- <% end -%>
49
- </div>
50
- <% else -%>
51
- <div class="error-message">
52
- <strong>Unknown</strong>
53
- <p>No error recorded for this job</p>
54
- </div>
55
- <% end -%>
56
- </td>
57
- <td style="white-space: nowrap;">
58
- <% if (failed_at = job.stored_at) -%>
59
- <time datetime="<%= h(job.timestamp) %>" title="<%= h(job.timestamp) %>"><%= h(format_timestamp(failed_at)) %></time>
60
- <div class="text-muted" style="font-size: var(--font-size-small);"><%= elapsed(failed_at) %> ago</div>
61
- <% else -%>
62
- <span class="text-muted">&mdash;</span>
63
- <% end -%>
64
- </td>
65
- <td style="white-space: nowrap;">
66
- <a hx-patch="<%= url_for("/jobs/retry/#{job.seq}") %>"
67
- hx-target="#dead-row-<%= job.seq %>"
68
- hx-swap="innerHTML"
69
- title="Retry"
70
- class="btn btn-primary btn-sm">&#x21BB;</a>
71
- <a hx-delete="<%= url_for("/jobs/delete/#{job.seq}") %>"
72
- hx-target="#dead-row-<%= job.seq %>"
73
- hx-swap="innerHTML"
74
- title="Delete"
75
- class="btn btn-danger btn-sm">&#x2715;</a>
76
- </td>
77
- </tr>
1
+ <% if @jobs.empty? -%>
2
+ <div class="empty"><b>No dead jobs</b>Nothing has given up.</div>
3
+ <% else -%>
4
+ <div class="scroll"><div style="min-width:860px">
5
+ <div class="thead t-jobs actions"><span>JID</span><span>Class</span><span>Args</span><span>Queue</span><span>Status</span><span class="r">Failed</span><span></span></div>
6
+ <% @jobs.each do |job| -%>
7
+ <div class="trow hover t-jobs actions">
8
+ <span class="mono mute ellip" style="font-size:11.5px" title="<%= h(job.data[:jid]) %>"><%= h(job.data[:jid]) %></span>
9
+ <span class="strong ellip"><%= h(job.data[:class]) %></span>
10
+ <%= render('jobs/_args', { args: job.data[:args] }) %>
11
+ <span class="mono ellip" style="font-size:12px"><%= job.x_stream ? h(job.x_stream) : '—' %></span>
12
+ <span class="pill" style="--tone:var(--err)">dead</span>
13
+ <% if (failed_at = job.stored_at) -%>
14
+ <span class="mono mute r" style="font-size:11.5px" title="<%= h(format_timestamp(failed_at)) %>"><%= elapsed(failed_at) %> ago</span>
15
+ <% else -%>
16
+ <span class="mono mute r">—</span>
17
+ <% end -%>
18
+ <div class="btns">
19
+ <button type="button" class="btn" title="Retry"
20
+ hx-patch="<%= url_for("/jobs/retry/#{job.seq}") %>" hx-target="closest .trow" hx-swap="outerHTML"
21
+ hx-indicator="#global-spinner">↻</button>
22
+ <button type="button" class="btn danger" title="Delete"
23
+ hx-delete="<%= url_for("/jobs/delete/#{job.seq}") %>" hx-target="closest .trow" hx-swap="outerHTML"
24
+ hx-indicator="#global-spinner" hx-confirm="Delete dead job #<%= job.seq %>?">✕</button>
25
+ </div>
26
+ <div class="full error-line">
27
+ <% if job.error? -%>
28
+ <b><%= h(job.error_class.to_s.empty? ? 'Error' : job.error_class) %></b>
29
+ <span><%= h(job.error_message) %></span>
30
+ <% if job.error_backtrace -%>
31
+ <details>
32
+ <summary class="small-mono">Backtrace</summary>
33
+ <pre class="code-block"><%= h(job.error_backtrace_lines.join("\n")) %></pre>
34
+ </details>
35
+ <% end -%>
36
+ <% else -%>
37
+ <b>Unknown</b><span class="mute">No error recorded for this job</span>
78
38
  <% end -%>
79
- </tbody>
80
- </table>
81
- </div>
82
- <% end -%>
83
- </div>
39
+ </div>
40
+ </div>
41
+ <% end -%>
42
+ </div></div>
43
+ <% end -%>