cosmonats 0.2.0 → 0.4.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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +300 -187
  3. data/lib/cosmo/active_job/adapter.rb +46 -0
  4. data/lib/cosmo/active_job/executor.rb +16 -0
  5. data/lib/cosmo/active_job/options.rb +50 -0
  6. data/lib/cosmo/active_job.rb +29 -0
  7. data/lib/cosmo/api/busy.rb +2 -2
  8. data/lib/cosmo/api/counter.rb +2 -2
  9. data/lib/cosmo/api/cron/entry.rb +99 -0
  10. data/lib/cosmo/api/cron.rb +118 -0
  11. data/lib/cosmo/api/kv.rb +36 -14
  12. data/lib/cosmo/api/stream.rb +27 -9
  13. data/lib/cosmo/api.rb +1 -0
  14. data/lib/cosmo/cli.rb +27 -9
  15. data/lib/cosmo/client.rb +75 -5
  16. data/lib/cosmo/config.rb +14 -32
  17. data/lib/cosmo/engine.rb +1 -1
  18. data/lib/cosmo/job/data.rb +1 -1
  19. data/lib/cosmo/job/limit.rb +51 -0
  20. data/lib/cosmo/job/processor.rb +82 -63
  21. data/lib/cosmo/job.rb +51 -2
  22. data/lib/cosmo/logger.rb +4 -1
  23. data/lib/cosmo/processor.rb +108 -0
  24. data/lib/cosmo/railtie.rb +21 -0
  25. data/lib/cosmo/stream/processor.rb +24 -60
  26. data/lib/cosmo/stream.rb +4 -3
  27. data/lib/cosmo/utils/hash.rb +13 -24
  28. data/lib/cosmo/utils/overrides.rb +1 -1
  29. data/lib/cosmo/utils/ttl_cache.rb +44 -0
  30. data/lib/cosmo/utils.rb +1 -0
  31. data/lib/cosmo/version.rb +1 -1
  32. data/lib/cosmo/web/assets/app.css +88 -0
  33. data/lib/cosmo/web/controllers/crons.rb +41 -0
  34. data/lib/cosmo/web/controllers/jobs.rb +7 -3
  35. data/lib/cosmo/web/controllers/streams.rb +36 -10
  36. data/lib/cosmo/web/helpers/application.rb +17 -2
  37. data/lib/cosmo/web/views/actions/index.erb +1 -1
  38. data/lib/cosmo/web/views/crons/_table.erb +58 -0
  39. data/lib/cosmo/web/views/crons/index.erb +10 -0
  40. data/lib/cosmo/web/views/jobs/_busy.erb +54 -49
  41. data/lib/cosmo/web/views/jobs/_dead.erb +70 -65
  42. data/lib/cosmo/web/views/jobs/_enqueued.erb +82 -56
  43. data/lib/cosmo/web/views/jobs/_scheduled.erb +53 -48
  44. data/lib/cosmo/web/views/jobs/_tabs.erb +6 -0
  45. data/lib/cosmo/web/views/jobs/busy.erb +8 -6
  46. data/lib/cosmo/web/views/jobs/dead.erb +6 -5
  47. data/lib/cosmo/web/views/jobs/enqueued.erb +8 -6
  48. data/lib/cosmo/web/views/jobs/index.erb +1 -1
  49. data/lib/cosmo/web/views/jobs/scheduled.erb +6 -5
  50. data/lib/cosmo/web/views/layout.erb +1 -1
  51. data/lib/cosmo/web/views/streams/_info.erb +3 -0
  52. data/lib/cosmo/web/views/streams/_pause_banner.erb +17 -0
  53. data/lib/cosmo/web/views/streams/_stream_row.erb +42 -0
  54. data/lib/cosmo/web/views/streams/_table.erb +4 -21
  55. data/lib/cosmo/web.rb +7 -0
  56. data/lib/cosmo.rb +1 -0
  57. data/sig/cosmo/active_job/adapter.rbs +13 -0
  58. data/sig/cosmo/active_job/executor.rbs +9 -0
  59. data/sig/cosmo/active_job/options.rbs +14 -0
  60. data/sig/cosmo/api/cron/entry.rbs +30 -0
  61. data/sig/cosmo/api/cron.rbs +25 -0
  62. data/sig/cosmo/api/kv.rbs +4 -6
  63. data/sig/cosmo/api/stream.rbs +7 -1
  64. data/sig/cosmo/client.rbs +20 -4
  65. data/sig/cosmo/config.rbs +3 -15
  66. data/sig/cosmo/job/data.rbs +1 -1
  67. data/sig/cosmo/job/limit.rbs +18 -0
  68. data/sig/cosmo/job/processor.rbs +19 -9
  69. data/sig/cosmo/job.rbs +9 -4
  70. data/sig/cosmo/processor.rbs +26 -0
  71. data/sig/cosmo/railtie.rbs +4 -0
  72. data/sig/cosmo/stream/processor.rbs +4 -10
  73. data/sig/cosmo/utils/hash.rbs +4 -8
  74. data/sig/cosmo/utils/ttl_cache.rbs +20 -0
  75. metadata +25 -3
  76. data/lib/cosmo/defaults.yml +0 -70
@@ -8,6 +8,12 @@ module Cosmo
8
8
  module Application
9
9
  include Renderer
10
10
 
11
+ def render(template, locals = nil)
12
+ defaults = { request: @request }
13
+ locals = Hash(locals).merge(defaults)
14
+ erb(template, locals)
15
+ end
16
+
11
17
  def format_bytes(bytes)
12
18
  b = bytes.to_i
13
19
  return "0 B" if b.zero?
@@ -64,11 +70,20 @@ module Cosmo
64
70
  def current_page?(path)
65
71
  request_path = @request.path_info
66
72
  request_path = "/" if request_path.empty?
67
- request_path == url_for(path)
73
+ request_path == path
74
+ end
75
+
76
+ def path_prefix?(*values)
77
+ values.any? { |v| @request.path_info.start_with?(v) }
68
78
  end
69
79
 
70
80
  def referrer?(path)
71
- URI(@request.referrer).path == path
81
+ referrer_uri = URI(@request.referrer)
82
+ referrer_path = referrer_uri.path
83
+ script_name = @request.script_name
84
+ referrer_path = referrer_path.delete_prefix(script_name) if script_name && !script_name.empty?
85
+ referrer_path = "/" if referrer_path.empty?
86
+ referrer_path == path
72
87
  end
73
88
  end
74
89
  end
@@ -2,6 +2,6 @@
2
2
  <header></header>
3
3
 
4
4
  <div>
5
- <div class="alert alert-info">None</div>
5
+ <div class="alert alert-info">Coming soon, stay tuned</div>
6
6
  </div>
7
7
  </section>
@@ -0,0 +1,58 @@
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.
6
+ </div>
7
+ <% 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>
54
+ <% end -%>
55
+ </tbody>
56
+ </table>
57
+ </div>
58
+ <% end -%>
@@ -0,0 +1,10 @@
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>
10
+ </section>
@@ -1,50 +1,55 @@
1
- <div class="pending">
2
- <span></span>
3
- <span class="text-muted"><%= @total %> job(s) running</span>
4
- </div>
5
-
6
- <% if @jobs.empty? -%>
7
- <div class="alert alert-success">No jobs are currently running</div>
8
- <% else -%>
9
- <div class="table-container">
10
- <table>
11
- <thead>
12
- <tr>
13
- <th>Job</th>
14
- <th>JID[Args]</th>
15
- <th>Stream</th>
16
- <th>Worker</th>
17
- <th>Running for</th>
18
- </tr>
19
- </thead>
20
- <tbody>
21
- <% @jobs.each do |job| -%>
22
- <tr>
23
- <td>
24
- <div class="job-class">
25
- <%= h(job.dig(:data, :class).to_s) %>
26
- </div>
27
- </td>
28
- <td>
29
- <div class="job-id">
30
- <details>
31
- <summary>
32
- <code><%= h(job.dig(:data, :jid)) %></code>
33
- </summary>
34
- <code><%= h(JSON.pretty_generate(job.dig(:data, :args))) %></code>
35
- </details>
36
- </div>
37
- </td>
38
- <td>
39
- <code><%= h(job[:stream].to_s) %></code>
40
- </td>
41
- <td><code style="font-size: var(--font-size-small);"><%= h(job[:worker].to_s) %></code></td>
42
- <td>
43
- <%= elapsed(job[:started_at]) %>
44
- </td>
45
- </tr>
46
- <% end -%>
47
- </tbody>
48
- </table>
1
+ <div id="busy-poller"
2
+ hx-get="<%= url_for('/jobs/_busy') %>"
3
+ hx-trigger="every 5s"
4
+ hx-swap="outerHTML">
5
+ <div class="pending">
6
+ <span></span>
7
+ <span class="text-muted"><%= @total %> job(s) running</span>
49
8
  </div>
50
- <% end -%>
9
+
10
+ <% if @jobs.empty? -%>
11
+ <div class="alert alert-success">No jobs are currently running</div>
12
+ <% else -%>
13
+ <div class="table-container">
14
+ <table>
15
+ <thead>
16
+ <tr>
17
+ <th>Job</th>
18
+ <th>JID[Args]</th>
19
+ <th>Stream</th>
20
+ <th>Worker</th>
21
+ <th>Running for</th>
22
+ </tr>
23
+ </thead>
24
+ <tbody>
25
+ <% @jobs.each do |job| -%>
26
+ <tr>
27
+ <td>
28
+ <div class="job-class">
29
+ <%= h(job.dig(:data, :class).to_s) %>
30
+ </div>
31
+ </td>
32
+ <td>
33
+ <div class="job-id">
34
+ <details>
35
+ <summary>
36
+ <code><%= h(job.dig(:data, :jid)) %></code>
37
+ </summary>
38
+ <code><%= h(JSON.pretty_generate(job.dig(:data, :args))) %></code>
39
+ </details>
40
+ </div>
41
+ </td>
42
+ <td>
43
+ <code><%= h(job[:stream].to_s) %></code>
44
+ </td>
45
+ <td><code style="font-size: var(--font-size-small);"><%= h(job[:worker].to_s) %></code></td>
46
+ <td>
47
+ <%= elapsed(job[:started_at]) %>
48
+ </td>
49
+ </tr>
50
+ <% end -%>
51
+ </tbody>
52
+ </table>
53
+ </div>
54
+ <% end -%>
55
+ </div>
@@ -1,65 +1,70 @@
1
- <% if @jobs.empty? -%>
2
- <div class="alert alert-success">No dead jobs found</div>
3
- <% else -%>
4
- <div class="table-container">
5
- <table>
6
- <thead>
7
- <tr>
8
- <th>Job</th>
9
- <th>JID[Args]</th>
10
- <th>Stream</th>
11
- <th>Error</th>
12
- <th>Failed</th>
13
- <th>Actions</th>
14
- </tr>
15
- </thead>
16
- <tbody>
17
- <% @jobs.each do |job| -%>
18
- <tr class="dead-row" id="dead-row-<%= job.seq %>">
19
- <td>
20
- <div class="job-class">
21
- <%= h(job.data[:class].to_s) %>
22
- </div>
23
- </td>
24
- <td>
25
- <div class="job-id">
26
- <details>
27
- <summary>
28
- <code><%= h(job.data[:jid].to_s) %></code>
29
- </summary>
30
- <code><%= h((JSON.pretty_generate(job.data[:args]) rescue job.data[:args].to_s)) %></code>
31
- </details>
32
- </div>
33
- </td>
34
- <td>
35
- <code><%= h(job.stream) %></code>
36
- </td>
37
- <td style="max-width: 400px;">
38
- <% if job.data[:error] -%>
39
- <div class="error-message">
40
- <strong><%= h(job.data[:error].to_s.split(":").first || "Error") %></strong>
41
- <p><%= h(job.data[:error]) %></p>
42
- </div>
43
- <% end -%>
44
- </td>
45
- <td style="white-space: nowrap;">
46
- <time><%= h(job.timestamp) %></time>
47
- </td>
48
- <td>
49
- <a hx-patch="<%= url_for("/jobs/retry/#{job.seq}") %>"
50
- hx-target="#dead-row-<%= job.seq %>"
51
- hx-swap="innerHTML"
52
- class="btn btn-primary"
53
- style="width: 100%; text-align: center; margin-bottom: 3px">&#x21BB; Retry</a>
54
- <a hx-delete="<%= url_for("/jobs/delete/#{job.seq}") %>"
55
- hx-target="#dead-row-<%= job.seq %>"
56
- hx-swap="innerHTML"
57
- class="btn btn-danger"
58
- style="width: 100%; text-align: center; margin-bottom: 3px">&#x2715; Delete</a>
59
- </td>
60
- </tr>
61
- <% end -%>
62
- </tbody>
63
- </table>
64
- </div>
65
- <% end -%>
1
+ <div id="dead-poller"
2
+ hx-get="<%= url_for('/jobs/_dead') %>"
3
+ hx-trigger="every 5s"
4
+ hx-swap="outerHTML">
5
+ <% if @jobs.empty? -%>
6
+ <div class="alert alert-success">No dead jobs found</div>
7
+ <% else -%>
8
+ <div class="table-container">
9
+ <table>
10
+ <thead>
11
+ <tr>
12
+ <th>Job</th>
13
+ <th>JID[Args]</th>
14
+ <th>Stream</th>
15
+ <th>Error</th>
16
+ <th>Failed</th>
17
+ <th>Actions</th>
18
+ </tr>
19
+ </thead>
20
+ <tbody>
21
+ <% @jobs.each do |job| -%>
22
+ <tr class="dead-row" id="dead-row-<%= job.seq %>">
23
+ <td>
24
+ <div class="job-class">
25
+ <%= h(job.data[:class].to_s) %>
26
+ </div>
27
+ </td>
28
+ <td>
29
+ <div class="job-id">
30
+ <details>
31
+ <summary>
32
+ <code><%= h(job.data[:jid].to_s) %></code>
33
+ </summary>
34
+ <code><%= h((JSON.pretty_generate(job.data[:args]) rescue job.data[:args].to_s)) %></code>
35
+ </details>
36
+ </div>
37
+ </td>
38
+ <td>
39
+ <code><%= h(job.stream) %></code>
40
+ </td>
41
+ <td style="max-width: 400px;">
42
+ <% if job.data[:error] -%>
43
+ <div class="error-message">
44
+ <strong><%= h(job.data[:error].to_s.split(":").first || "Error") %></strong>
45
+ <p><%= h(job.data[:error]) %></p>
46
+ </div>
47
+ <% end -%>
48
+ </td>
49
+ <td style="white-space: nowrap;">
50
+ <time><%= h(job.timestamp) %></time>
51
+ </td>
52
+ <td>
53
+ <a hx-patch="<%= url_for("/jobs/retry/#{job.seq}") %>"
54
+ hx-target="#dead-row-<%= job.seq %>"
55
+ hx-swap="innerHTML"
56
+ class="btn btn-primary"
57
+ style="width: 100%; text-align: center; margin-bottom: 3px">&#x21BB; Retry</a>
58
+ <a hx-delete="<%= url_for("/jobs/delete/#{job.seq}") %>"
59
+ hx-target="#dead-row-<%= job.seq %>"
60
+ hx-swap="innerHTML"
61
+ class="btn btn-danger"
62
+ style="width: 100%; text-align: center; margin-bottom: 3px">&#x2715; Delete</a>
63
+ </td>
64
+ </tr>
65
+ <% end -%>
66
+ </tbody>
67
+ </table>
68
+ </div>
69
+ <% end -%>
70
+ </div>
@@ -1,60 +1,86 @@
1
- <div class="nav">
2
- <% @stream_names.each do |stream_name| -%>
3
- <button hx-get="<%= url_for('/jobs/enqueued', { stream_name: }) %>"
4
- hx-target="#content"
5
- hx-swap="innerHTML"
6
- hx-push-url="true"
7
- class="btn <%= "btn-primary" if @stream_name.to_s == stream_name.to_s %>">
8
- <%= h(stream_name) %>
9
- </button>
1
+ <div id="enqueued-poller">
2
+ <div class="nav">
3
+ <% @stream_names.each do |stream_name| -%>
4
+ <button hx-get="<%= url_for('/jobs/enqueued', { stream_name: }) %>"
5
+ hx-target="#content"
6
+ hx-swap="innerHTML"
7
+ hx-push-url="true"
8
+ class="btn <%= "btn-primary" if @stream_name.to_s == stream_name.to_s %>">
9
+ <%= h(stream_name) %>
10
+ </button>
11
+ <% end -%>
12
+ </div>
13
+
14
+ <div class="pending">
15
+ <span></span>
16
+ <span class="text-muted"><%= @total %> message(s) pending</span>
17
+ </div>
18
+
19
+ <% if @jobs.empty? -%>
20
+ <div class="alert alert-success">Stream <code><%= h(@stream_name) %></code> is empty</div>
21
+ <% else -%>
22
+ <div class="table-container">
23
+ <table>
24
+ <thead>
25
+ <tr>
26
+ <th>Job</th>
27
+ <th>JID[Args]</th>
28
+ <th>Stream</th>
29
+ <th>Seq</th>
30
+ </tr>
31
+ </thead>
32
+ <tbody>
33
+ <% @jobs.each do |job| -%>
34
+ <tr>
35
+ <td>
36
+ <div class="job-class">
37
+ <%= h(job.data[:class].to_s) %>
38
+ </div>
39
+ </td>
40
+ <td>
41
+ <div class="job-id">
42
+ <details>
43
+ <summary>
44
+ <code><%= h(job.data[:jid].to_s) %></code>
45
+ </summary>
46
+ <code><%= h((JSON.pretty_generate(job.data[:args]) rescue job.data[:args].to_s)) %></code>
47
+ </details>
48
+ </div>
49
+ </td>
50
+ <td>
51
+ <code><%= h(job.stream) %></code>
52
+ </td>
53
+ <td>
54
+ <code><%= job.seq %></code>
55
+ </td>
56
+ </tr>
57
+ <% end -%>
58
+ </tbody>
59
+ </table>
60
+ </div>
10
61
  <% end -%>
11
- </div>
12
62
 
13
- <div class="pending">
14
- <span></span>
15
- <span class="text-muted"><%= @total %> message(s) pending</span>
16
- </div>
63
+ <% if @total_pages > 1 -%>
64
+ <div class="pagination">
65
+ <% if @page > 1 -%>
66
+ <a hx-get="<%= url_for('/jobs/_enqueued', stream_name: @stream_name, page: @page - 1, limit: @limit) %>"
67
+ hx-target="#enqueued-poller"
68
+ hx-swap="outerHTML"
69
+ class="btn">&#x2190; Prev</a>
70
+ <% else -%>
71
+ <span class="btn btn-disabled">&#x2190; Prev</span>
72
+ <% end -%>
73
+
74
+ <span class="text-muted">Page <%= @page %> of <%= @total_pages %></span>
17
75
 
18
- <% if @jobs.empty? -%>
19
- <div class="alert alert-success">Stream <code><%= h(@stream_name) %></code> is empty</div>
20
- <% else -%>
21
- <div class="table-container">
22
- <table>
23
- <thead>
24
- <tr>
25
- <th>Job</th>
26
- <th>JID[Args]</th>
27
- <th>Stream</th>
28
- <th>Seq</th>
29
- </tr>
30
- </thead>
31
- <tbody>
32
- <% @jobs.each do |job| -%>
33
- <tr>
34
- <td>
35
- <div class="job-class">
36
- <%= h(job.data[:class].to_s) %>
37
- </div>
38
- </td>
39
- <td>
40
- <div class="job-id">
41
- <details>
42
- <summary>
43
- <code><%= h(job.data[:jid].to_s) %></code>
44
- </summary>
45
- <code><%= h((JSON.pretty_generate(job.data[:args]) rescue job.data[:args].to_s)) %></code>
46
- </details>
47
- </div>
48
- </td>
49
- <td>
50
- <code><%= h(job.stream) %></code>
51
- </td>
52
- <td>
53
- <code><%= job.seq %></code>
54
- </td>
55
- </tr>
56
- <% end -%>
57
- </tbody>
58
- </table>
76
+ <% if @page < @total_pages -%>
77
+ <a hx-get="<%= url_for('/jobs/_enqueued', stream_name: @stream_name, page: @page + 1, limit: @limit) %>"
78
+ hx-target="#enqueued-poller"
79
+ hx-swap="outerHTML"
80
+ class="btn">Next &#x2192;</a>
81
+ <% else -%>
82
+ <span class="btn btn-disabled">Next &#x2192;</span>
83
+ <% end -%>
59
84
  </div>
60
- <% end -%>
85
+ <% end -%>
86
+ </div>
@@ -1,49 +1,54 @@
1
- <div class="pending">
2
- <span></span>
3
- <span class="text-muted"><%= @total %> job(s) scheduled</span>
4
- </div>
5
-
6
- <% if @jobs.empty? -%>
7
- <div class="alert alert-success">No scheduled jobs found</div>
8
- <% else -%>
9
- <div class="table-container">
10
- <table>
11
- <thead>
12
- <tr>
13
- <th>Job</th>
14
- <th>JID[Args]</th>
15
- <th>Stream</th>
16
- <th>When</th>
17
- </tr>
18
- </thead>
19
- <tbody>
20
- <% @jobs.each do |job| -%>
21
- <tr>
22
- <td>
23
- <div class="job-class">
24
- <%= h(job.data[:class].to_s) %>
25
- </div>
26
- </td>
27
- <td>
28
- <div class="job-id">
29
- <details>
30
- <summary>
31
- <code><%= h(job.data[:jid].to_s) %></code>
32
- </summary>
33
- <code><%= h((JSON.pretty_generate(job.data[:args]) rescue job.data[:args].to_s)) %></code>
34
- </details>
35
- </div>
36
- </td>
37
- <td>
38
- <code><%= h(job.x_stream) %></code>
39
- </td>
40
- <td style="white-space: nowrap;">
41
- <div class="time-badge"><%= h(time_until(job.execute_at)) %></div>
42
- <time><%= h(format_timestamp(job.execute_at)) %></time>
43
- </td>
44
- </tr>
45
- <% end -%>
46
- </tbody>
47
- </table>
1
+ <div id="scheduled-poller"
2
+ hx-get="<%= url_for('/jobs/_scheduled') %>"
3
+ hx-trigger="every 5s"
4
+ hx-swap="outerHTML">
5
+ <div class="pending">
6
+ <span></span>
7
+ <span class="text-muted"><%= @total %> job(s) scheduled</span>
48
8
  </div>
49
- <% end -%>
9
+
10
+ <% if @jobs.empty? -%>
11
+ <div class="alert alert-success">No scheduled jobs found</div>
12
+ <% else -%>
13
+ <div class="table-container">
14
+ <table>
15
+ <thead>
16
+ <tr>
17
+ <th>Job</th>
18
+ <th>JID[Args]</th>
19
+ <th>Stream</th>
20
+ <th>When</th>
21
+ </tr>
22
+ </thead>
23
+ <tbody>
24
+ <% @jobs.each do |job| -%>
25
+ <tr>
26
+ <td>
27
+ <div class="job-class">
28
+ <%= h(job.data[:class].to_s) %>
29
+ </div>
30
+ </td>
31
+ <td>
32
+ <div class="job-id">
33
+ <details>
34
+ <summary>
35
+ <code><%= h(job.data[:jid].to_s) %></code>
36
+ </summary>
37
+ <code><%= h((JSON.pretty_generate(job.data[:args]) rescue job.data[:args].to_s)) %></code>
38
+ </details>
39
+ </div>
40
+ </td>
41
+ <td>
42
+ <code><%= h(job.x_stream) %></code>
43
+ </td>
44
+ <td style="white-space: nowrap;">
45
+ <div class="time-badge"><%= h(time_until(job.execute_at)) %></div>
46
+ <time><%= h(format_timestamp(job.execute_at)) %></time>
47
+ </td>
48
+ </tr>
49
+ <% end -%>
50
+ </tbody>
51
+ </table>
52
+ </div>
53
+ <% end -%>
54
+ </div>
@@ -0,0 +1,6 @@
1
+ <nav class="tabs">
2
+ <a href="<%= url_for('/jobs') %>"
3
+ class="<%= 'active' if path_prefix?('/jobs') %>">&#x1F4CB; Dashboard</a>
4
+ <a href="<%= url_for('/crons') %>"
5
+ class="<%= 'active' if path_prefix?('/crons') %>">&#x23F0; Crons</a>
6
+ </nav>
@@ -1,5 +1,5 @@
1
1
  <section>
2
- <header></header>
2
+ <header><%= render('jobs/_tabs') %></header>
3
3
 
4
4
  <div hx-get="<%= url_for('/jobs/_stats') %>"
5
5
  hx-trigger="load, every 5s"
@@ -7,10 +7,12 @@
7
7
  <div class="alert alert-info">Loading statistics…</div>
8
8
  </div>
9
9
 
10
- <div id="content"
11
- hx-get="<%= url_for('/jobs/_busy') %>"
12
- hx-trigger="load, every 5s"
13
- hx-swap="innerHTML">
14
- <div class="alert alert-info">Loading busy jobs&hellip;</div>
10
+ <div id="content">
11
+ <div id="busy-poller"
12
+ hx-get="<%= url_for('/jobs/_busy') %>"
13
+ hx-trigger="load"
14
+ hx-swap="outerHTML">
15
+ <div class="alert alert-info">Loading busy jobs&hellip;</div>
16
+ </div>
15
17
  </div>
16
18
  </section>
@@ -1,5 +1,5 @@
1
1
  <section>
2
- <header></header>
2
+ <header><%= render('jobs/_tabs') %></header>
3
3
 
4
4
  <div hx-get="<%= url_for('/jobs/_stats') %>"
5
5
  hx-trigger="load, every 5s"
@@ -8,10 +8,11 @@
8
8
  </div>
9
9
 
10
10
  <div id="content">
11
- <div hx-get="<%= url_for('/jobs/_dead') %>"
12
- hx-trigger="load, every 5s"
13
- hx-swap="innerHTML">
14
- <div class="alert alert-info">Loading dead jobs…</div>
11
+ <div id="dead-poller"
12
+ hx-get="<%= url_for('/jobs/_dead') %>"
13
+ hx-trigger="load"
14
+ hx-swap="outerHTML">
15
+ <div class="alert alert-info">Loading dead jobs&hellip;</div>
15
16
  </div>
16
17
  </div>
17
18
  </section>