cosmonats 0.1.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +129 -67
  3. data/lib/cosmo/api/busy.rb +66 -0
  4. data/lib/cosmo/api/counter.rb +70 -0
  5. data/lib/cosmo/api/job.rb +46 -0
  6. data/lib/cosmo/api/kv.rb +63 -0
  7. data/lib/cosmo/api/stats.rb +44 -0
  8. data/lib/cosmo/api/stream.rb +123 -0
  9. data/lib/cosmo/api.rb +11 -0
  10. data/lib/cosmo/cli.rb +8 -5
  11. data/lib/cosmo/client.rb +58 -3
  12. data/lib/cosmo/config.rb +13 -38
  13. data/lib/cosmo/engine.rb +1 -1
  14. data/lib/cosmo/job/processor.rb +66 -57
  15. data/lib/cosmo/job.rb +1 -1
  16. data/lib/cosmo/logger.rb +8 -1
  17. data/lib/cosmo/processor.rb +110 -2
  18. data/lib/cosmo/stream/processor.rb +23 -59
  19. data/lib/cosmo/stream.rb +2 -2
  20. data/lib/cosmo/utils/hash.rb +3 -27
  21. data/lib/cosmo/utils/overrides.rb +15 -0
  22. data/lib/cosmo/utils/ttl_cache.rb +44 -0
  23. data/lib/cosmo/utils/warnings.rb +17 -0
  24. data/lib/cosmo/utils.rb +15 -0
  25. data/lib/cosmo/version.rb +1 -1
  26. data/lib/cosmo/web/assets/app.css +477 -0
  27. data/lib/cosmo/web/assets/htmx.2.0.8.min.js.gz +0 -0
  28. data/lib/cosmo/web/context.rb +28 -0
  29. data/lib/cosmo/web/controllers/actions.rb +16 -0
  30. data/lib/cosmo/web/controllers/application.rb +43 -0
  31. data/lib/cosmo/web/controllers/jobs.rb +97 -0
  32. data/lib/cosmo/web/controllers/streams.rb +70 -0
  33. data/lib/cosmo/web/helpers/application.rb +87 -0
  34. data/lib/cosmo/web/renderer.rb +58 -0
  35. data/lib/cosmo/web/views/actions/index.erb +7 -0
  36. data/lib/cosmo/web/views/jobs/_busy.erb +50 -0
  37. data/lib/cosmo/web/views/jobs/_dead.erb +65 -0
  38. data/lib/cosmo/web/views/jobs/_enqueued.erb +60 -0
  39. data/lib/cosmo/web/views/jobs/_scheduled.erb +49 -0
  40. data/lib/cosmo/web/views/jobs/_stats.erb +69 -0
  41. data/lib/cosmo/web/views/jobs/busy.erb +16 -0
  42. data/lib/cosmo/web/views/jobs/dead.erb +17 -0
  43. data/lib/cosmo/web/views/jobs/enqueued.erb +16 -0
  44. data/lib/cosmo/web/views/jobs/index.erb +12 -0
  45. data/lib/cosmo/web/views/jobs/scheduled.erb +17 -0
  46. data/lib/cosmo/web/views/layout.erb +33 -0
  47. data/lib/cosmo/web/views/streams/_info.erb +92 -0
  48. data/lib/cosmo/web/views/streams/_pause_banner.erb +17 -0
  49. data/lib/cosmo/web/views/streams/_stream_row.erb +42 -0
  50. data/lib/cosmo/web/views/streams/_table.erb +25 -0
  51. data/lib/cosmo/web/views/streams/index.erb +11 -0
  52. data/lib/cosmo/web/views/streams/info.erb +11 -0
  53. data/lib/cosmo/web.rb +68 -0
  54. data/lib/cosmo.rb +2 -7
  55. data/sig/cosmo/api/busy.rbs +35 -0
  56. data/sig/cosmo/api/counter.rbs +34 -0
  57. data/sig/cosmo/api/job.rbs +31 -0
  58. data/sig/cosmo/api/kv.rbs +30 -0
  59. data/sig/cosmo/api/stats.rbs +21 -0
  60. data/sig/cosmo/api/stream.rbs +50 -0
  61. data/sig/cosmo/client.rbs +21 -3
  62. data/sig/cosmo/config.rbs +3 -15
  63. data/sig/cosmo/job/processor.rbs +16 -8
  64. data/sig/cosmo/processor.rbs +26 -0
  65. data/sig/cosmo/stream/processor.rbs +4 -10
  66. data/sig/cosmo/utils/hash.rbs +0 -8
  67. data/sig/cosmo/utils/ttl_cache.rbs +20 -0
  68. metadata +62 -3
  69. data/lib/cosmo/defaults.yml +0 -69
@@ -0,0 +1,477 @@
1
+ :root {
2
+ --color-primary: oklch(0.66 0.188 250.179);
3
+ --color-bg: oklch(99% 0.005 256);
4
+ --color-elevated: oklch(100% 0 256);
5
+ --color-border: oklch(95% 0.005 256);
6
+ --color-selected: oklch(93% 0.005 256);
7
+ --color-table-bg-alt: oklch(99% 0.005 256);
8
+ --color-shadow: oklch(27% 0.005 256 / 5%);
9
+ --color-text: oklch(27% 0.005 256);
10
+ --color-text-light: oklch(52% 0.005 256);
11
+ --color-success: oklch(0.70 0.15 145);
12
+ --color-info: oklch(0.66 0.188 250.179);
13
+ --color-danger: oklch(0.65 0.22 25);
14
+ --color-warning: oklch(0.75 0.15 85);
15
+
16
+ --space-1-2: 4px;
17
+ --space: 8px;
18
+ --space-2x: 16px;
19
+ --space-3x: 24px;
20
+ --space-4x: 32px;
21
+
22
+ --font-sans: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
23
+ Oxygen, Ubuntu, Cantarell, sans-serif;
24
+ --font-mono: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas,
25
+ 'DejaVu Sans Mono', monospace;
26
+ --font-size: 16px;
27
+ --font-size-large: 1.728rem;
28
+ --font-size-small: 0.833rem;
29
+ }
30
+
31
+ @media (prefers-color-scheme: dark) {
32
+ :root {
33
+ --color-primary: oklch(0.72 0.188 250.179);
34
+ --color-bg: oklch(20% 0.005 256);
35
+ --color-elevated: oklch(24% 0.005 256);
36
+ --color-border: oklch(30% 0.005 256);
37
+ --color-selected: oklch(28% 0.005 256);
38
+ --color-table-bg-alt: oklch(22% 0.005 256);
39
+ --color-shadow: oklch(10% 0.005 256 / 30%);
40
+ --color-text: oklch(90% 0.005 256);
41
+ --color-text-light: oklch(65% 0.005 256);
42
+ }
43
+ }
44
+
45
+ /* ── Reset ─────────────────────────────────────────────────────────────── */
46
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
47
+
48
+ /* ── Base ──────────────────────────────────────────────────────────────── */
49
+ body {
50
+ font-family: var(--font-sans);
51
+ font-size: var(--font-size);
52
+ line-height: 1.5;
53
+ background: var(--color-bg);
54
+ color: var(--color-text);
55
+ font-variant-numeric: tabular-nums;
56
+ -webkit-font-smoothing: antialiased;
57
+ min-height: 100vh;
58
+ }
59
+
60
+ h1, h2, h3 {
61
+ font-size: var(--font-size-large);
62
+ font-weight: normal;
63
+ overflow-wrap: break-word;
64
+ margin: var(--space) 0 var(--space);
65
+ padding: 0;
66
+ }
67
+
68
+ code {
69
+ background-color: var(--color-elevated);
70
+ border-radius: 2px;
71
+ box-shadow: 0 0 0 1px var(--color-border);
72
+ color: var(--color-text-light);
73
+ display: inline-block;
74
+ font-family: var(--font-mono);
75
+ font-size: var(--font-size-small);
76
+ padding: var(--space-1-2);
77
+ word-wrap: anywhere;
78
+ }
79
+
80
+ a { color: inherit; text-decoration: none; }
81
+ a.active, a:hover { color: oklch(from var(--color-primary) calc(l + 0.06) c h); }
82
+
83
+ /* ── Buttons ───────────────────────────────────────────────────────────── */
84
+ button, .btn {
85
+ background: linear-gradient(
86
+ oklch(from var(--color-text) calc(l + 0.06) c h),
87
+ var(--color-text)
88
+ );
89
+ border: none;
90
+ border-radius: var(--space-1-2);
91
+ box-shadow: 0 1px 1px 1px var(--color-shadow);
92
+ color: var(--color-bg);
93
+ cursor: pointer;
94
+ display: inline-block;
95
+ font-family: inherit;
96
+ font-size: var(--font-size);
97
+ font-weight: 700;
98
+ line-height: var(--space-3x);
99
+ padding: var(--space) var(--space-2x);
100
+ text-decoration: none;
101
+ white-space: nowrap;
102
+ }
103
+
104
+ button:hover, .btn:hover {
105
+ background: linear-gradient(
106
+ oklch(from var(--color-text) calc(l + 0.16) c h),
107
+ oklch(from var(--color-text) calc(l + 0.1) c h)
108
+ );
109
+ color: var(--color-bg);
110
+ }
111
+
112
+ .btn-primary {
113
+ background: linear-gradient(
114
+ oklch(from var(--color-primary) calc(l + 0.06) c h),
115
+ var(--color-primary)
116
+ );
117
+ }
118
+ .btn-primary:hover {
119
+ background: linear-gradient(
120
+ oklch(from var(--color-primary) calc(l + 0.16) c h),
121
+ oklch(from var(--color-primary) calc(l + 0.1) c h)
122
+ );
123
+ }
124
+
125
+ .btn-danger {
126
+ background: linear-gradient(
127
+ oklch(from var(--color-danger) calc(l + 0.06) c h),
128
+ var(--color-danger)
129
+ );
130
+ }
131
+ .btn-danger:hover {
132
+ background: linear-gradient(
133
+ oklch(from var(--color-danger) calc(l + 0.16) c h),
134
+ oklch(from var(--color-danger) calc(l + 0.1) c h)
135
+ );
136
+ }
137
+
138
+ .btn-warning {
139
+ background: linear-gradient(
140
+ oklch(from var(--color-warning) calc(l + 0.06) c h),
141
+ var(--color-warning)
142
+ );
143
+ color: oklch(27% 0.005 256);
144
+ }
145
+ .btn-warning:hover {
146
+ background: linear-gradient(
147
+ oklch(from var(--color-warning) calc(l + 0.16) c h),
148
+ oklch(from var(--color-warning) calc(l + 0.1) c h)
149
+ );
150
+ color: oklch(27% 0.005 256);
151
+ }
152
+
153
+ .btn-success {
154
+ background: linear-gradient(
155
+ oklch(from var(--color-success) calc(l + 0.06) c h),
156
+ var(--color-success)
157
+ );
158
+ }
159
+ .btn-success:hover {
160
+ background: linear-gradient(
161
+ oklch(from var(--color-success) calc(l + 0.16) c h),
162
+ oklch(from var(--color-success) calc(l + 0.1) c h)
163
+ );
164
+ }
165
+
166
+ /* ── Badges ────────────────────────────────────────────────────────────── */
167
+ .badge {
168
+ border-radius: var(--space-1-2);
169
+ display: inline-block;
170
+ font-size: var(--font-size-small);
171
+ font-weight: 700;
172
+ padding: 2px var(--space);
173
+ white-space: nowrap;
174
+ }
175
+ .badge-success {
176
+ background: oklch(from var(--color-success) l c h / 15%);
177
+ color: var(--color-success);
178
+ }
179
+ .badge-warning {
180
+ background: oklch(from var(--color-warning) l c h / 20%);
181
+ color: oklch(from var(--color-warning) calc(l - 0.15) c h);
182
+ }
183
+
184
+ /* ── Alerts ────────────────────────────────────────────────────────────── */
185
+ .alert {
186
+ padding: var(--space-2x);
187
+ border-radius: var(--space-1-2);
188
+ margin: var(--space-2x) 0;
189
+ }
190
+ .alert-success {
191
+ background: oklch(from var(--color-success) l c h / 10%);
192
+ color: var(--color-success);
193
+ border-left: 4px solid var(--color-success);
194
+ }
195
+ .alert-info {
196
+ background: oklch(from var(--color-info) l c h / 10%);
197
+ color: var(--color-info);
198
+ border-left: 4px solid var(--color-info);
199
+ }
200
+ .alert-warning {
201
+ background: oklch(from var(--color-warning) l c h / 10%);
202
+ color: var(--color-warning);
203
+ border-left: 4px solid var(--color-warning);
204
+ }
205
+ .alert-danger {
206
+ background: oklch(from var(--color-danger) l c h / 10%);
207
+ color: var(--color-danger);
208
+ border-left: 4px solid var(--color-danger);
209
+ }
210
+
211
+ /* ── Layout ────────────────────────────────────────────────────────────── */
212
+ header {
213
+ background: var(--color-elevated);
214
+ box-shadow: 0 2px 4px 0 var(--color-shadow);
215
+ height: 56px;
216
+ position: fixed;
217
+ left: 0; right: 0; top: 0;
218
+ z-index: 10;
219
+ }
220
+
221
+ .container {
222
+ margin: 0 auto;
223
+ max-width: 1280px;
224
+ padding: var(--space-2x);
225
+ }
226
+
227
+ header .container {
228
+ display: flex;
229
+ padding: var(--space) var(--space-2x);
230
+ }
231
+
232
+ .nav {
233
+ display: flex;
234
+ align-items: center;
235
+ width: 100%;
236
+ gap: var(--space);
237
+ }
238
+
239
+ .navbar-brand {
240
+ color: var(--color-primary);
241
+ font-size: 1.125rem;
242
+ font-weight: 700;
243
+ line-height: 24px;
244
+ margin-right: var(--space-2x);
245
+ text-decoration: none;
246
+ }
247
+
248
+ .nav-list {
249
+ display: flex;
250
+ list-style-type: none;
251
+ gap: var(--space-1-2);
252
+ margin: 0;
253
+ padding: 0;
254
+ }
255
+
256
+ .nav-list li a {
257
+ display: block;
258
+ padding: var(--space) var(--space-2x);
259
+ line-height: 24px;
260
+ color: inherit;
261
+ text-decoration: none;
262
+ border-radius: var(--space-1-2);
263
+ transition: background 0.2s;
264
+ }
265
+
266
+ .nav-list li a:hover,
267
+ .nav-list li a.active {
268
+ background: var(--color-selected);
269
+ color: var(--color-text);
270
+ }
271
+
272
+ main.container {
273
+ padding-top: calc(56px + var(--space-4x)); /* header height + breathing room = 88px */
274
+ min-height: 100vh;
275
+ }
276
+
277
+ /* ── Sections ──────────────────────────────────────────────────────────── */
278
+ section { margin-bottom: var(--space-4x); }
279
+
280
+ section > header {
281
+ display: flex;
282
+ align-items: center;
283
+ gap: var(--space);
284
+ margin-bottom: var(--space-2x);
285
+ /* reset the global fixed-header styles */
286
+ background: transparent;
287
+ box-shadow: none;
288
+ height: auto;
289
+ padding: 0;
290
+ position: static;
291
+ width: auto;
292
+ z-index: auto;
293
+ }
294
+
295
+ section .nav {
296
+ display: flex;
297
+ gap: var(--space);
298
+ flex-wrap: wrap;
299
+ width: fit-content;
300
+ margin: var(--space-3x) auto;
301
+ }
302
+
303
+ section .pending {
304
+ margin-bottom: 0.75rem;
305
+ display: flex;
306
+ justify-content: space-between;
307
+ align-items: center;
308
+ }
309
+
310
+ /* ── Stat cards ────────────────────────────────────────────────────────── */
311
+ .cards-container {
312
+ display: grid;
313
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
314
+ gap: var(--space-2x);
315
+ margin-bottom: var(--space-3x);
316
+ }
317
+
318
+ .stat-card {
319
+ background: var(--color-elevated);
320
+ border-radius: var(--space-1-2);
321
+ box-shadow: 0 1px 3px var(--color-shadow);
322
+ padding: var(--space-3x);
323
+ text-align: center;
324
+ transition: transform 0.2s, box-shadow 0.2s;
325
+ }
326
+ .stat-card:hover {
327
+ transform: translateY(-2px);
328
+ box-shadow: 0 4px 8px var(--color-shadow);
329
+ }
330
+ .stat-card .row {
331
+ display: flex;
332
+ justify-content: center;
333
+ align-items: center;
334
+ gap: var(--space);
335
+ }
336
+ .stat-card .row p {
337
+ white-space: nowrap;
338
+ text-align: left;
339
+ }
340
+ .stat-card .row h4 {
341
+ width: 100%;
342
+ white-space: nowrap;
343
+ text-align: right;
344
+ }
345
+ .stat-card h3 {
346
+ color: var(--color-text);
347
+ font-size: 2rem;
348
+ font-weight: 700;
349
+ margin: 0 0 var(--space) 0;
350
+ }
351
+ .stat-card p {
352
+ color: var(--color-text-light);
353
+ font-size: var(--font-size-small);
354
+ letter-spacing: 0.05em;
355
+ margin: 0;
356
+ text-transform: uppercase;
357
+ }
358
+
359
+ /* ── Tables ────────────────────────────────────────────────────────────── */
360
+ .table-container {
361
+ background: var(--color-elevated);
362
+ border-radius: var(--space-1-2);
363
+ box-shadow: 0 1px 3px var(--color-shadow);
364
+ margin-bottom: var(--space-4x);
365
+ overflow: hidden;
366
+ }
367
+
368
+ table { border-collapse: collapse; width: 100%; }
369
+
370
+ th {
371
+ background: var(--color-table-bg-alt);
372
+ border-bottom: 1px solid var(--color-border);
373
+ color: var(--color-text-light);
374
+ font-size: var(--font-size-small);
375
+ font-weight: 600;
376
+ letter-spacing: 0.05em;
377
+ padding: var(--space-2x);
378
+ text-align: left;
379
+ text-transform: uppercase;
380
+ }
381
+
382
+ td {
383
+ border-bottom: 1px solid var(--color-border);
384
+ color: var(--color-text);
385
+ padding: var(--space-2x);
386
+ vertical-align: top;
387
+ }
388
+
389
+ tr:last-child td { border-bottom: none; }
390
+ tr:hover { background: var(--color-table-bg-alt); }
391
+
392
+ /* ── Subjects ──────────────────────────────────────────────────────────── */
393
+ .subjects { display: flex; flex-wrap: wrap; gap: var(--space-1-2); }
394
+
395
+ .subject-tag {
396
+ background: oklch(from var(--color-info) l c h / 15%);
397
+ border-radius: var(--space-1-2);
398
+ color: var(--color-info);
399
+ font-family: var(--font-mono);
400
+ font-size: var(--font-size-small);
401
+ font-weight: 500;
402
+ padding: var(--space-1-2) var(--space);
403
+ }
404
+
405
+ /* ── Job rows ──────────────────────────────────────────────────────────── */
406
+ .job-class {
407
+ color: var(--color-text);
408
+ font-weight: 600;
409
+ margin-bottom: var(--space-1-2);
410
+ }
411
+ .job-id {
412
+ color: var(--color-text-light);
413
+ font-size: var(--font-size-small);
414
+ margin-bottom: var(--space);
415
+ }
416
+
417
+ .time-badge {
418
+ background: var(--color-success);
419
+ border-radius: var(--space-1-2);
420
+ color: var(--color-elevated);
421
+ display: inline-block;
422
+ font-size: var(--font-size-small);
423
+ font-weight: 700;
424
+ margin-bottom: var(--space-1-2);
425
+ padding: var(--space-1-2) var(--space);
426
+ }
427
+
428
+ time {
429
+ color: var(--color-text-light);
430
+ display: block;
431
+ font-size: var(--font-size-small);
432
+ }
433
+
434
+ .dead-row { border-left: 4px solid var(--color-danger); }
435
+
436
+ .error-message strong {
437
+ color: var(--color-danger);
438
+ display: block;
439
+ margin-bottom: var(--space-1-2);
440
+ }
441
+ .error-message p {
442
+ color: var(--color-text);
443
+ font-size: var(--font-size-small);
444
+ word-wrap: break-word;
445
+ }
446
+
447
+ /* ── Links ─────────────────────────────────────────────────────────────── */
448
+ .stream-link { color: var(--color-primary); font-weight: 600; }
449
+ .stream-link:hover { text-decoration: underline; }
450
+
451
+ /* ── Actions ───────────────────────────────────────────────────────────── */
452
+ .actions-form { display: flex; flex-direction: column; gap: var(--space-1-2); }
453
+
454
+ /* ── Details / summary ─────────────────────────────────────────────────── */
455
+ details summary {
456
+ color: var(--color-text-light);
457
+ cursor: pointer;
458
+ font-size: var(--font-size-small);
459
+ user-select: none;
460
+ }
461
+ details summary code { display: inline; }
462
+ details[open] summary { margin-bottom: var(--space); }
463
+ details code { display: block; max-width: 400px; white-space: pre-wrap; }
464
+
465
+ /* ── HTMX ──────────────────────────────────────────────────────────────── */
466
+ .htmx-indicator { opacity: 0; transition: opacity 200ms; }
467
+ .htmx-request .htmx-indicator,
468
+ .htmx-request.htmx-indicator { opacity: 1; }
469
+
470
+ /* ── Responsive ────────────────────────────────────────────────────────── */
471
+ @media (max-width: 768px) {
472
+ .nav { flex-wrap: wrap; }
473
+ .nav-list { order: 3; width: 100%; margin-top: var(--space); }
474
+ .cards-container { grid-template-columns: repeat(2, 1fr); }
475
+ .table-container { overflow-x: auto; }
476
+ th, td { font-size: var(--font-size-small); padding: var(--space); }
477
+ }
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cosmo/web/helpers/application"
4
+
5
+ module Cosmo
6
+ class Web
7
+ class Context
8
+ include Helpers::Application
9
+
10
+ def initialize(locals, content_for = nil)
11
+ @content_for = Hash(content_for)
12
+ locals.each { |k, v| instance_variable_set("@#{k}", v) }
13
+ end
14
+
15
+ def binding # rubocop:disable Lint/UselessMethodDefinition
16
+ super
17
+ end
18
+
19
+ def content_for(name)
20
+ @content_for[name]
21
+ end
22
+
23
+ def content_for?(name)
24
+ @content_for.key?(name)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cosmo/web/controllers/application"
4
+
5
+ module Cosmo
6
+ class Web
7
+ module Controllers
8
+ class Actions < Application
9
+ def index
10
+ content_for :title, "Actions"
11
+ ok render("actions/index", layout: true)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cosmo
4
+ class Web
5
+ module Controllers
6
+ class Application
7
+ include Renderer
8
+
9
+ def initialize(request)
10
+ @request = request
11
+ end
12
+
13
+ def content_for(name, content)
14
+ @content_for ||= {}
15
+ @content_for[name] = content
16
+ end
17
+
18
+ def render(template, locals = nil, layout: false)
19
+ defaults = { request: @request }
20
+ locals = Hash(locals).merge(defaults)
21
+ view = erb(template, locals)
22
+ return view unless layout
23
+
24
+ @content_for ||= {}
25
+ @content_for[:view] = view
26
+ erb("layout", defaults, @content_for)
27
+ end
28
+
29
+ def params
30
+ @request.params
31
+ end
32
+
33
+ def path
34
+ @request.path
35
+ end
36
+
37
+ def hx_request?
38
+ @request.get_header("HTTP_HX_REQUEST") == "true"
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cosmo/web/controllers/application"
4
+
5
+ module Cosmo
6
+ class Web
7
+ module Controllers
8
+ class Jobs < Application
9
+ def index
10
+ content_for :title, "Jobs"
11
+ ok render("jobs/index", layout: true)
12
+ end
13
+
14
+ def busy
15
+ return _busy if hx_request?
16
+
17
+ content_for :title, "Busy Jobs"
18
+ ok render("jobs/busy", layout: true)
19
+ end
20
+
21
+ def enqueued
22
+ return _enqueued if hx_request?
23
+
24
+ content_for :title, "Enqueued Jobs"
25
+ stream_name, _stream_names = streams
26
+ ok render("jobs/enqueued", { stream_name: }, layout: true)
27
+ end
28
+
29
+ def scheduled
30
+ return _scheduled if hx_request?
31
+
32
+ content_for :title, "Scheduled Jobs"
33
+ ok render("jobs/scheduled", layout: true)
34
+ end
35
+
36
+ def dead
37
+ return _dead if hx_request?
38
+
39
+ content_for :title, "Dead Jobs"
40
+ ok render("jobs/dead", layout: true)
41
+ end
42
+
43
+ def retry
44
+ seq = path.split("/").last.to_i
45
+ stream = API::Stream.new("dead")
46
+ stream.retry(seq)
47
+ ok
48
+ end
49
+
50
+ def delete
51
+ seq = path.split("/").last.to_i
52
+ stream = API::Stream.new("dead")
53
+ stream.delete(seq)
54
+ ok
55
+ end
56
+
57
+ def _scheduled
58
+ stream = API::Stream.new("scheduled")
59
+ jobs = stream.messages(page: params["page"], limit: params["limit"])
60
+ ok render("jobs/_scheduled", { jobs: jobs, total: stream.total })
61
+ end
62
+
63
+ def _dead
64
+ stream = API::Stream.new("dead")
65
+ jobs = stream.messages(page: params["page"], limit: params["limit"])
66
+ ok render("jobs/_dead", { jobs: jobs, total: stream.total })
67
+ end
68
+
69
+ def _busy
70
+ limit = (limit || 25).to_i
71
+ jobs = API::Busy.instance.list(limit:)
72
+ ok render("jobs/_busy", { jobs: jobs, total: API::Busy.instance.size })
73
+ end
74
+
75
+ def _enqueued
76
+ stream_name, stream_names = streams
77
+ stream = API::Stream.new(stream_name)
78
+ jobs = stream.messages(page: params["page"], limit: params["limit"])
79
+
80
+ ok render("jobs/_enqueued", { jobs:, total: stream.total, stream_name:, stream_names: })
81
+ end
82
+
83
+ def _stats
84
+ ok render("jobs/_stats", API::Stats.summary)
85
+ end
86
+
87
+ private
88
+
89
+ def streams
90
+ stream_names = API::Stream.jobs.map(&:name)
91
+ stream_name = params.fetch("stream_name", stream_names.first)
92
+ [stream_name, stream_names]
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end