cosmonats 0.7.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 (147) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +219 -113
  3. data/lib/cosmo/active_job/adapter.rb +1 -2
  4. data/lib/cosmo/api/counter.rb +17 -17
  5. data/lib/cosmo/api/cron/entry.rb +10 -11
  6. data/lib/cosmo/api/cron.rb +6 -3
  7. data/lib/cosmo/api/job.rb +1 -1
  8. data/lib/cosmo/api/kv.rb +3 -3
  9. data/lib/cosmo/api/registry.rb +1 -1
  10. data/lib/cosmo/api/stats/busy.rb +6 -1
  11. data/lib/cosmo/api/stats/metrics.rb +109 -0
  12. data/lib/cosmo/api/stats/processes.rb +14 -4
  13. data/lib/cosmo/api/stats/totals.rb +4 -12
  14. data/lib/cosmo/api/stats.rb +18 -2
  15. data/lib/cosmo/api/stream.rb +7 -2
  16. data/lib/cosmo/batch/counters.rb +19 -0
  17. data/lib/cosmo/batch.rb +6 -6
  18. data/lib/cosmo/cli.rb +21 -5
  19. data/lib/cosmo/client.rb +37 -14
  20. data/lib/cosmo/config/cosmo.yml +58 -0
  21. data/lib/cosmo/config/settings.rb +20 -0
  22. data/lib/cosmo/config.rb +144 -19
  23. data/lib/cosmo/heartbeat.rb +11 -0
  24. data/lib/cosmo/job/data.rb +17 -12
  25. data/lib/cosmo/job/enqueuer.rb +53 -0
  26. data/lib/cosmo/job/limit.rb +2 -2
  27. data/lib/cosmo/job/processor.rb +32 -58
  28. data/lib/cosmo/job/requeue.rb +20 -0
  29. data/lib/cosmo/job.rb +8 -22
  30. data/lib/cosmo/logger.rb +15 -2
  31. data/lib/cosmo/middleware/busy.rb +15 -0
  32. data/lib/cosmo/middleware/chain.rb +137 -0
  33. data/lib/cosmo/middleware/limit.rb +50 -0
  34. data/lib/cosmo/middleware/metrics.rb +39 -0
  35. data/lib/cosmo/{sentry/job_processor_middleware.rb → middleware/sentry.rb} +26 -30
  36. data/lib/cosmo/middleware/totals.rb +29 -0
  37. data/lib/cosmo/middleware.rb +7 -0
  38. data/lib/cosmo/processor.rb +11 -5
  39. data/lib/cosmo/publisher.rb +1 -13
  40. data/lib/cosmo/railtie.rb +2 -2
  41. data/lib/cosmo/services.rb +69 -0
  42. data/lib/cosmo/stream/processor.rb +7 -1
  43. data/lib/cosmo/utils/hash.rb +7 -8
  44. data/lib/cosmo/utils.rb +0 -1
  45. data/lib/cosmo/version.rb +1 -1
  46. data/lib/cosmo/web/assets/app.css +271 -571
  47. data/lib/cosmo/web/assets/app.js +158 -0
  48. data/lib/cosmo/web/assets/fonts/OFL.txt +92 -0
  49. data/lib/cosmo/web/assets/fonts/geist-mono.woff2 +0 -0
  50. data/lib/cosmo/web/assets/fonts/geist.woff2 +0 -0
  51. data/lib/cosmo/web/assets/logo.svg +10 -0
  52. data/lib/cosmo/web/cache.rb +75 -0
  53. data/lib/cosmo/web/chart.rb +122 -0
  54. data/lib/cosmo/web/circuit.rb +55 -0
  55. data/lib/cosmo/web/context.rb +0 -4
  56. data/lib/cosmo/web/controllers/application.rb +16 -0
  57. data/lib/cosmo/web/controllers/batches.rb +1 -0
  58. data/lib/cosmo/web/controllers/crons.rb +17 -2
  59. data/lib/cosmo/web/controllers/jobs.rb +59 -54
  60. data/lib/cosmo/web/controllers/metrics.rb +49 -0
  61. data/lib/cosmo/web/controllers/overview.rb +70 -0
  62. data/lib/cosmo/web/controllers/processes.rb +33 -0
  63. data/lib/cosmo/web/controllers/streams.rb +26 -28
  64. data/lib/cosmo/web/helpers/application.rb +67 -0
  65. data/lib/cosmo/web/renderer.rb +19 -6
  66. data/lib/cosmo/web/throughput.rb +104 -0
  67. data/lib/cosmo/web/views/batches/_table.erb +42 -43
  68. data/lib/cosmo/web/views/batches/index.erb +3 -10
  69. data/lib/cosmo/web/views/crons/_table.erb +32 -53
  70. data/lib/cosmo/web/views/crons/index.erb +2 -9
  71. data/lib/cosmo/web/views/jobs/_args.erb +5 -0
  72. data/lib/cosmo/web/views/jobs/_busy.erb +25 -121
  73. data/lib/cosmo/web/views/jobs/_dead.erb +42 -82
  74. data/lib/cosmo/web/views/jobs/_enqueued.erb +33 -113
  75. data/lib/cosmo/web/views/jobs/_panel.erb +29 -0
  76. data/lib/cosmo/web/views/jobs/_queue_row.erb +18 -0
  77. data/lib/cosmo/web/views/jobs/_queues.erb +20 -0
  78. data/lib/cosmo/web/views/jobs/_scheduled.erb +20 -54
  79. data/lib/cosmo/web/views/jobs/index.erb +4 -17
  80. data/lib/cosmo/web/views/layout.erb +73 -22
  81. data/lib/cosmo/web/views/metrics/_chart.erb +41 -0
  82. data/lib/cosmo/web/views/metrics/_panel.erb +13 -0
  83. data/lib/cosmo/web/views/metrics/_table.erb +25 -0
  84. data/lib/cosmo/web/views/metrics/index.erb +9 -0
  85. data/lib/cosmo/web/views/overview/_badges.erb +3 -0
  86. data/lib/cosmo/web/views/overview/_kpis.erb +19 -0
  87. data/lib/cosmo/web/views/overview/_running.erb +18 -0
  88. data/lib/cosmo/web/views/overview/_throughput.erb +50 -0
  89. data/lib/cosmo/web/views/overview/_workers.erb +25 -0
  90. data/lib/cosmo/web/views/overview/index.erb +28 -0
  91. data/lib/cosmo/web/views/processes/_table.erb +67 -0
  92. data/lib/cosmo/web/views/processes/index.erb +1 -0
  93. data/lib/cosmo/web/views/shared/_pager.erb +22 -0
  94. data/lib/cosmo/web/views/shared/unavailable.erb +7 -0
  95. data/lib/cosmo/web/views/streams/_info.erb +28 -86
  96. data/lib/cosmo/web/views/streams/_list.erb +7 -0
  97. data/lib/cosmo/web/views/streams/_pause_banner.erb +8 -16
  98. data/lib/cosmo/web/views/streams/_stream.erb +52 -0
  99. data/lib/cosmo/web/views/streams/index.erb +3 -11
  100. data/lib/cosmo/web/views/streams/info.erb +3 -11
  101. data/lib/cosmo/web.rb +67 -33
  102. data/lib/cosmo.rb +38 -0
  103. data/sig/cosmo/api/counter.rbs +2 -3
  104. data/sig/cosmo/api/cron/entry.rbs +2 -1
  105. data/sig/cosmo/api/stats/busy.rbs +3 -1
  106. data/sig/cosmo/api/stats/metrics.rbs +42 -0
  107. data/sig/cosmo/api/stats/processes.rbs +3 -0
  108. data/sig/cosmo/api/stats/totals.rbs +3 -2
  109. data/sig/cosmo/api/stats.rbs +4 -0
  110. data/sig/cosmo/api/stream.rbs +2 -0
  111. data/sig/cosmo/batch/counters.rbs +10 -0
  112. data/sig/cosmo/batch.rbs +3 -4
  113. data/sig/cosmo/cli.rbs +2 -0
  114. data/sig/cosmo/client.rbs +4 -1
  115. data/sig/cosmo/config/settings.rbs +31 -0
  116. data/sig/cosmo/config.rbs +59 -5
  117. data/sig/cosmo/heartbeat.rbs +2 -0
  118. data/sig/cosmo/job/data.rbs +5 -3
  119. data/sig/cosmo/job/enqueuer.rbs +11 -0
  120. data/sig/cosmo/job/processor.rbs +3 -5
  121. data/sig/cosmo/job/requeue.rbs +9 -0
  122. data/sig/cosmo/job.rbs +0 -2
  123. data/sig/cosmo/logger.rbs +6 -0
  124. data/sig/cosmo/middleware/chain.rbs +51 -0
  125. data/sig/cosmo/middleware/sentry.rbs +14 -0
  126. data/sig/cosmo/middleware.rbs +35 -0
  127. data/sig/cosmo/processor.rbs +2 -0
  128. data/sig/cosmo/publisher.rbs +1 -9
  129. data/sig/cosmo/services.rbs +18 -0
  130. data/sig/cosmo/stream/processor.rbs +2 -0
  131. data/sig/cosmo/utils/hash.rbs +2 -3
  132. data/sig/cosmo.rbs +11 -0
  133. metadata +60 -20
  134. data/lib/cosmo/sentry/auto.rb +0 -6
  135. data/lib/cosmo/utils/overrides.rb +0 -206
  136. data/lib/cosmo/utils/warnings.rb +0 -17
  137. data/lib/cosmo/web/controllers/actions.rb +0 -16
  138. data/lib/cosmo/web/views/actions/index.erb +0 -7
  139. data/lib/cosmo/web/views/jobs/_processes.erb +0 -131
  140. data/lib/cosmo/web/views/jobs/_stats.erb +0 -73
  141. data/lib/cosmo/web/views/jobs/_tabs.erb +0 -8
  142. data/lib/cosmo/web/views/jobs/busy.erb +0 -18
  143. data/lib/cosmo/web/views/jobs/dead.erb +0 -18
  144. data/lib/cosmo/web/views/jobs/enqueued.erb +0 -18
  145. data/lib/cosmo/web/views/jobs/scheduled.erb +0 -18
  146. data/lib/cosmo/web/views/streams/_stream_row.erb +0 -42
  147. data/lib/cosmo/web/views/streams/_table.erb +0 -25
@@ -1,572 +1,272 @@
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
- .btn-sm {
167
- box-shadow: none;
168
- font-size: var(--font-size-small);
169
- font-weight: 600;
170
- line-height: var(--space-2x);
171
- padding: var(--space-1-2) var(--space);
172
- }
173
-
174
- /* ── Badges ────────────────────────────────────────────────────────────── */
175
- .badge {
176
- border-radius: var(--space-1-2);
177
- display: inline-block;
178
- font-size: var(--font-size-small);
179
- font-weight: 700;
180
- padding: 2px var(--space);
181
- white-space: nowrap;
182
- }
183
- .badge-success {
184
- background: oklch(from var(--color-success) l c h / 15%);
185
- color: var(--color-success);
186
- }
187
- .badge-warning {
188
- background: oklch(from var(--color-warning) l c h / 20%);
189
- color: oklch(from var(--color-warning) calc(l - 0.15) c h);
190
- }
191
- .badge-danger {
192
- background: oklch(from var(--color-danger) l c h / 15%);
193
- color: var(--color-danger);
194
- }
195
- .badge-info {
196
- background: oklch(from var(--color-info) l c h / 15%);
197
- color: var(--color-info);
198
- }
199
-
200
- /* ── Alerts ────────────────────────────────────────────────────────────── */
201
- .alert {
202
- padding: var(--space-2x);
203
- border-radius: var(--space-1-2);
204
- margin: var(--space-2x) 0;
205
- }
206
- .alert-success {
207
- background: oklch(from var(--color-success) l c h / 10%);
208
- color: var(--color-success);
209
- border-left: 4px solid var(--color-success);
210
- }
211
- .alert-info {
212
- background: oklch(from var(--color-info) l c h / 10%);
213
- color: var(--color-info);
214
- border-left: 4px solid var(--color-info);
215
- }
216
- .alert-warning {
217
- background: oklch(from var(--color-warning) l c h / 10%);
218
- color: var(--color-warning);
219
- border-left: 4px solid var(--color-warning);
220
- }
221
- .alert-danger {
222
- background: oklch(from var(--color-danger) l c h / 10%);
223
- color: var(--color-danger);
224
- border-left: 4px solid var(--color-danger);
225
- }
226
-
227
- /* ── Layout ────────────────────────────────────────────────────────────── */
228
- header {
229
- background: var(--color-elevated);
230
- box-shadow: 0 2px 4px 0 var(--color-shadow);
231
- height: 56px;
232
- position: fixed;
233
- left: 0; right: 0; top: 0;
234
- z-index: 10;
235
- }
236
-
237
- .container {
238
- margin: 0 auto;
239
- max-width: 1280px;
240
- padding: var(--space-2x);
241
- }
242
-
243
- header .container {
244
- display: flex;
245
- padding: var(--space) var(--space-2x);
246
- }
247
-
248
- .nav {
249
- display: flex;
250
- align-items: center;
251
- width: 100%;
252
- gap: var(--space);
253
- }
254
-
255
- .navbar-brand {
256
- color: var(--color-primary);
257
- font-size: 1.125rem;
258
- font-weight: 700;
259
- line-height: 24px;
260
- margin-right: var(--space-2x);
261
- text-decoration: none;
262
- }
263
-
264
- .nav-list {
265
- display: flex;
266
- list-style-type: none;
267
- gap: var(--space-1-2);
268
- margin: 0;
269
- padding: 0;
270
- }
271
-
272
- .nav-list li a {
273
- display: block;
274
- padding: var(--space) var(--space-2x);
275
- line-height: 24px;
276
- color: inherit;
277
- text-decoration: none;
278
- border-radius: var(--space-1-2);
279
- transition: background 0.2s;
280
- }
281
-
282
- .nav-list li a:hover,
283
- .nav-list li a.active {
284
- background: var(--color-selected);
285
- color: var(--color-text);
286
- }
287
-
288
- main.container {
289
- padding-top: calc(56px + var(--space-4x)); /* header height + breathing room = 88px */
290
- min-height: 100vh;
291
- }
292
-
293
- /* ── Sections ──────────────────────────────────────────────────────────── */
294
- section { margin-bottom: var(--space-4x); }
295
-
296
- section > header {
297
- display: flex;
298
- align-items: center;
299
- gap: var(--space);
300
- margin-bottom: var(--space-2x);
301
- /* reset the global fixed-header styles */
302
- background: transparent;
303
- box-shadow: none;
304
- height: auto;
305
- padding: 0;
306
- position: static;
307
- width: auto;
308
- z-index: auto;
309
- }
310
-
311
- /* ── Section tabs ───────────────────────────────────────────────────────── */
312
- .tabs {
313
- display: flex;
314
- gap: 0;
315
- border-bottom: 2px solid var(--color-border);
316
- margin-bottom: var(--space-3x);
317
- }
318
-
319
- .tabs a {
320
- padding: var(--space) var(--space-3x);
321
- text-decoration: none;
322
- color: var(--color-text-light);
323
- border-bottom: 2px solid transparent;
324
- margin-bottom: -2px;
325
- font-weight: 500;
326
- transition: color 0.15s, border-color 0.15s;
327
- }
328
-
329
- .tabs a:hover {
330
- color: var(--color-text);
331
- border-bottom-color: var(--color-border);
332
- }
333
-
334
- .tabs a.active {
335
- color: var(--color-primary);
336
- border-bottom-color: var(--color-primary);
337
- }
338
-
339
- section .nav {
340
- display: flex;
341
- gap: var(--space);
342
- flex-wrap: wrap;
343
- width: fit-content;
344
- margin: var(--space-3x) auto;
345
- }
346
-
347
- section .pending {
348
- margin-bottom: 0.75rem;
349
- display: flex;
350
- justify-content: space-between;
351
- align-items: center;
352
- }
353
-
354
- section .pending .poll-status {
355
- align-items: center;
356
- display: inline-flex;
357
- gap: var(--space);
358
- }
359
-
360
- /* ── Stat cards ────────────────────────────────────────────────────────── */
361
- .cards-container {
362
- display: grid;
363
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
364
- gap: var(--space-2x);
365
- margin-bottom: var(--space-3x);
366
- }
367
-
368
- .stat-card {
369
- background: var(--color-elevated);
370
- border-radius: var(--space-1-2);
371
- box-shadow: 0 1px 3px var(--color-shadow);
372
- padding: var(--space-3x);
373
- text-align: center;
374
- transition: transform 0.2s, box-shadow 0.2s;
375
- }
376
- .stat-card:hover {
377
- transform: translateY(-2px);
378
- box-shadow: 0 4px 8px var(--color-shadow);
379
- }
380
- .stat-card .row {
381
- display: flex;
382
- justify-content: center;
383
- align-items: center;
384
- gap: var(--space);
385
- }
386
- .stat-card .row p {
387
- white-space: nowrap;
388
- text-align: left;
389
- }
390
- .stat-card .row h4 {
391
- width: 100%;
392
- white-space: nowrap;
393
- text-align: right;
394
- }
395
- .stat-card h3 {
396
- color: var(--color-text);
397
- font-size: 2rem;
398
- font-weight: 700;
399
- margin: 0 0 var(--space) 0;
400
- }
401
- .stat-card p {
402
- color: var(--color-text-light);
403
- font-size: var(--font-size-small);
404
- letter-spacing: 0.05em;
405
- margin: 0;
406
- text-transform: uppercase;
407
- }
408
-
409
- /* ── Tables ────────────────────────────────────────────────────────────── */
410
- .table-container {
411
- background: var(--color-elevated);
412
- border-radius: var(--space-1-2);
413
- box-shadow: 0 1px 3px var(--color-shadow);
414
- margin-bottom: var(--space-4x);
415
- overflow: hidden;
416
- }
417
-
418
- table { border-collapse: collapse; width: 100%; }
419
-
420
- th {
421
- background: var(--color-table-bg-alt);
422
- border-bottom: 1px solid var(--color-border);
423
- color: var(--color-text-light);
424
- font-size: var(--font-size-small);
425
- font-weight: 600;
426
- letter-spacing: 0.05em;
427
- padding: var(--space-2x);
428
- text-align: left;
429
- text-transform: uppercase;
430
- }
431
-
432
- td {
433
- border-bottom: 1px solid var(--color-border);
434
- color: var(--color-text);
435
- padding: var(--space-2x);
436
- vertical-align: top;
437
- }
438
-
439
- tr:last-child td { border-bottom: none; }
440
- tr:hover { background: var(--color-table-bg-alt); }
441
-
442
- /* ── Subjects ──────────────────────────────────────────────────────────── */
443
- .subjects { display: flex; flex-wrap: wrap; gap: var(--space-1-2); }
444
-
445
- .subject-tag {
446
- background: oklch(from var(--color-info) l c h / 15%);
447
- border-radius: var(--space-1-2);
448
- color: var(--color-info);
449
- font-family: var(--font-mono);
450
- font-size: var(--font-size-small);
451
- font-weight: 500;
452
- padding: var(--space-1-2) var(--space);
453
- }
454
-
455
- /* ── Job rows ──────────────────────────────────────────────────────────── */
456
- .job-class {
457
- color: var(--color-text);
458
- font-weight: 600;
459
- margin-bottom: var(--space-1-2);
460
- }
461
- .job-id {
462
- color: var(--color-text-light);
463
- font-size: var(--font-size-small);
464
- margin-bottom: var(--space);
465
- }
466
-
467
- .time-badge {
468
- background: var(--color-success);
469
- border-radius: var(--space-1-2);
470
- color: var(--color-elevated);
471
- display: inline-block;
472
- font-size: var(--font-size-small);
473
- font-weight: 700;
474
- margin-bottom: var(--space-1-2);
475
- padding: var(--space-1-2) var(--space);
476
- }
477
-
478
- time {
479
- color: var(--color-text-light);
480
- display: block;
481
- font-size: var(--font-size-small);
482
- }
483
-
484
- .dead-row { border-left: 4px solid var(--color-danger); }
485
-
486
- .error-message strong {
487
- color: var(--color-danger);
488
- display: block;
489
- margin-bottom: var(--space-1-2);
490
- }
491
- .error-message p {
492
- color: var(--color-text);
493
- font-size: var(--font-size-small);
494
- word-wrap: break-word;
495
- }
496
- .error-message details code {
497
- max-width: 600px;
498
- max-height: 320px;
499
- overflow: auto;
500
- white-space: pre;
501
- font-size: var(--font-size-small);
502
- }
503
-
504
- /* ── Links ─────────────────────────────────────────────────────────────── */
505
- .stream-link { color: var(--color-primary); font-weight: 600; }
506
- .stream-link:hover { text-decoration: underline; }
507
-
508
- /* ── Pagination ─────────────────────────────────────────────────────────── */
509
- .pagination {
510
- display: flex;
511
- align-items: center;
512
- justify-content: center;
513
- gap: var(--space);
514
- padding: var(--space-2x) 0 var(--space);
515
- }
516
- .btn-disabled {
517
- opacity: 0.4;
518
- cursor: default;
519
- pointer-events: none;
520
- }
521
- .pagination .btn-primary.btn-disabled {
522
- opacity: 1;
523
- }
524
- .pagination-gap {
525
- color: var(--color-text-light);
526
- padding: 0 var(--space-1-2);
527
- }
528
-
529
- /* ── Actions ───────────────────────────────────────────────────────────── */
530
- .actions-form { display: flex; flex-direction: column; gap: var(--space-1-2); }
531
-
532
- /* ── Details / summary ─────────────────────────────────────────────────── */
533
- details summary {
534
- color: var(--color-text-light);
535
- cursor: pointer;
536
- font-size: var(--font-size-small);
537
- user-select: none;
538
- }
539
- details summary code { display: inline; user-select: text; }
540
- details[open] summary { margin-bottom: var(--space); }
541
- details code { display: block; max-width: 400px; white-space: pre-wrap; }
542
-
543
- /* ── HTMX ──────────────────────────────────────────────────────────────── */
544
- .htmx-indicator { opacity: 0; transition: opacity 200ms; }
545
- .htmx-request .htmx-indicator,
546
- .htmx-request.htmx-indicator { opacity: 1; }
547
-
548
- #global-spinner {
549
- position: fixed;
550
- top: var(--space-2x);
551
- right: var(--space-2x);
552
- width: 24px;
553
- height: 24px;
554
- border: 3px solid var(--color-border);
555
- border-top-color: var(--color-primary);
556
- border-radius: 50%;
557
- animation: spin 0.6s linear infinite;
558
- z-index: 100;
559
- }
560
-
561
- @keyframes spin {
562
- to { transform: rotate(360deg); }
563
- }
564
-
565
- /* ── Responsive ────────────────────────────────────────────────────────── */
566
- @media (max-width: 768px) {
567
- .nav { flex-wrap: wrap; }
568
- .nav-list { order: 3; width: 100%; margin-top: var(--space); }
569
- .cards-container { grid-template-columns: repeat(2, 1fr); }
570
- .table-container { overflow-x: auto; }
571
- th, td { font-size: var(--font-size-small); padding: var(--space); }
1
+ @font-face{font-family:Geist;src:url("fonts/geist.woff2") format("woff2");font-weight:100 900;font-display:swap}
2
+ @font-face{font-family:"Geist Mono";src:url("fonts/geist-mono.woff2") format("woff2");font-weight:100 900;font-display:swap}
3
+
4
+ :root{
5
+ --bg:#f4f5f7;--panel:#fff;--panel2:#eef0f4;--line:#e2e5eb;--line2:#d3d8e1;
6
+ --ink:#0d1120;--mute:#5d6578;--faint:#9aa1b2;
7
+ --acc:oklch(0.52 0.18 262);--ok:oklch(0.55 0.14 155);--warn:oklch(0.6 0.14 70);--err:oklch(0.55 0.18 25);
8
+ --c1:oklch(0.55 0.17 262);--c2:oklch(0.55 0.17 300);--c3:oklch(0.58 0.12 205);--c4:oklch(0.57 0.16 345);--c5:oklch(0.6 0.13 120);
9
+ --sans:Geist,system-ui,-apple-system,sans-serif;--mono:"Geist Mono",ui-monospace,SFMono-Regular,Menlo,monospace;
10
+ color-scheme:light;
11
+ }
12
+ html[data-theme="dark"]{
13
+ --bg:#060913;--panel:#0a0f1b;--panel2:#111829;--line:#18202f;--line2:#243047;
14
+ --ink:#e6e9f2;--mute:#8a93a8;--faint:#566078;
15
+ --acc:oklch(0.74 0.15 258);--ok:oklch(0.76 0.14 158);--warn:oklch(0.8 0.13 80);--err:oklch(0.7 0.16 25);
16
+ --c1:oklch(0.74 0.14 258);--c2:oklch(0.74 0.14 300);--c3:oklch(0.78 0.11 200);--c4:oklch(0.74 0.14 350);--c5:oklch(0.8 0.13 125);
17
+ color-scheme:dark;
18
+ }
19
+ *{box-sizing:border-box}
20
+ html,body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--sans);font-size:14px;-webkit-font-smoothing:antialiased;transition:background .25s}
21
+ a{color:var(--acc);text-decoration:none}a:hover{text-decoration:underline}
22
+ button{font:inherit;color:inherit;cursor:pointer}
23
+ code,pre{font-family:var(--mono);font-size:12px}
24
+ pre{margin:0;white-space:pre-wrap;word-break:break-all}
25
+ .mono{font-family:var(--mono)}
26
+ .num{font-variant-numeric:tabular-nums}
27
+ .mute{color:var(--mute)}.faint{color:var(--faint)}
28
+ .r{text-align:right;justify-self:end}
29
+ .ellip{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
30
+ .strong{font-weight:500}
31
+ @keyframes pulse{0%{box-shadow:0 0 0 0 color-mix(in oklch,var(--ok) 60%,transparent)}70%{box-shadow:0 0 0 6px transparent}100%{box-shadow:0 0 0 0 transparent}}
32
+ @keyframes spin{to{transform:rotate(360deg)}}
33
+
34
+ /* shell */
35
+ .app{display:grid;grid-template-columns:232px minmax(0,1fr);min-height:100vh}
36
+ .sidebar{position:sticky;top:0;height:100vh;overflow-y:auto;display:flex;flex-direction:column;gap:28px;padding:22px 16px;border-right:1px solid var(--line);background:var(--panel);z-index:20;transition:transform .25s ease}
37
+ .brand{display:flex;align-items:center;gap:10px;padding:0 6px;color:var(--ink)}
38
+ .brand:hover{text-decoration:none}
39
+ .brand img{width:28px;height:28px;display:block;object-fit:contain}
40
+ .brand-text{display:flex;flex-direction:column;gap:2px;flex:1}
41
+ .brand-name{font-weight:600;font-size:15px;letter-spacing:-.01em}
42
+ .eyebrow{font-family:var(--mono);font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:var(--faint)}
43
+ .nav{display:flex;flex-direction:column;gap:2px}
44
+ .nav-item{display:flex;align-items:center;gap:12px;padding:9px 10px;border-radius:7px;color:var(--mute)}
45
+ .nav-item:hover{background:var(--panel2);text-decoration:none}
46
+ .nav-item .bar{width:3px;height:16px;border-radius:2px;margin-left:-4px;background:transparent}
47
+ .nav-item .n{font-family:var(--mono);font-size:10px;color:var(--faint);width:16px}
48
+ .nav-item .label{flex:1;font-weight:500}
49
+ .nav-item .badge{font-family:var(--mono);font-size:11px;color:var(--faint)}
50
+ .nav-item.active{background:var(--panel2);color:var(--ink)}
51
+ .nav-item.active .bar{background:var(--hue)}
52
+ .nav-item.active .n{color:var(--hue)}
53
+ .side-foot{margin-top:auto;display:flex;flex-direction:column;gap:14px}
54
+ .conn{display:flex;flex-direction:column;gap:6px;padding:12px;border:1px solid var(--line);border-radius:8px}
55
+ .conn-title{display:flex;align-items:center;gap:8px;font-size:12px;font-weight:500}
56
+ .conn-dot{width:7px;height:7px;border-radius:50%;background:var(--ok);animation:pulse 2s infinite}
57
+ .conn-dot.down{background:var(--err);animation:none}
58
+ .conn-meta{font-family:var(--mono);font-size:11px;color:var(--mute);line-height:1.6;word-break:break-all}
59
+ .theme-toggle{display:grid;grid-template-columns:repeat(3,1fr);padding:3px;border:1px solid var(--line);border-radius:8px;background:var(--panel2)}
60
+ .theme-toggle button{display:flex;align-items:center;justify-content:center;border:0;padding:6px;border-radius:6px;background:transparent;color:var(--faint)}
61
+ .theme-toggle button:hover{color:var(--ink)}
62
+ .theme-toggle button.on{background:var(--panel);color:var(--ink)}
63
+ .theme-toggle svg{width:16px;height:16px;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;fill:none}
64
+ .version{font-family:var(--mono);font-size:10px;color:var(--faint);padding:0 4px}
65
+ .icon-btn{width:40px;height:40px;display:none;align-items:center;justify-content:center;flex-direction:column;gap:4px;border:1px solid var(--line);border-radius:8px;background:var(--panel);padding:0;color:var(--mute);font-size:18px;line-height:1}
66
+ .icon-btn span{width:16px;height:1.5px;background:var(--ink);border-radius:1px}
67
+ .backdrop{display:none;position:fixed;inset:0;z-index:19;background:rgba(4,6,14,.45);backdrop-filter:blur(2px)}
68
+
69
+ .main{min-width:0;display:flex;flex-direction:column}
70
+ .topbar{position:sticky;top:0;z-index:5;display:flex;align-items:center;gap:16px;flex-wrap:wrap;padding:14px 32px;border-bottom:1px solid var(--line);background:color-mix(in oklch,var(--bg) 88%,transparent);backdrop-filter:blur(10px)}
71
+ .topbar .logo-sm{width:24px;height:24px;display:none}
72
+ .crumb{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--mono);font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--faint)}
73
+ .crumb .sep{color:var(--line2)}.crumb .cur{color:var(--mute)}
74
+ .spacer{flex:1}
75
+ .seg{display:flex;padding:3px;border:1px solid var(--line);border-radius:8px;background:var(--panel);flex-wrap:wrap}
76
+ .seg a,.seg button{border:0;padding:5px 11px;border-radius:6px;font-family:var(--mono);font-size:11px;background:transparent;color:var(--mute)}
77
+ .seg a:hover{text-decoration:none;color:var(--ink)}
78
+ .seg .on{background:var(--panel2);color:var(--ink)}
79
+ .live{display:flex;align-items:center;gap:7px;font-family:var(--mono);font-size:11px;color:var(--mute);white-space:nowrap}
80
+ .live::before{content:"";width:6px;height:6px;border-radius:50%;background:var(--ok)}
81
+ body.offline .live{color:var(--err)}
82
+ body.offline .live::before{background:var(--err)}
83
+ .spinner{width:14px;height:14px;border:2px solid var(--line2);border-top-color:var(--acc);border-radius:50%;opacity:0;transition:opacity .2s}
84
+ .spinner.htmx-request{opacity:1;animation:spin .7s linear infinite}
85
+
86
+ .content{display:flex;flex-direction:column;gap:24px;padding:28px 32px 48px;max-width:1440px;width:100%}
87
+ .page-head{display:flex;align-items:flex-end;gap:16px;flex-wrap:wrap}
88
+ .page-head .titles{display:flex;flex-direction:column;gap:6px;flex:1;min-width:240px}
89
+ .page-head h1{margin:0;font-size:30px;font-weight:500;letter-spacing:-.025em}
90
+ .page-head p{margin:0;color:var(--mute);text-wrap:pretty}
91
+ .flash{padding:7px 12px;border-radius:7px;font-size:12px;font-family:var(--mono);background:color-mix(in oklch,var(--ok) 14%,transparent);color:var(--ok)}
92
+ .flash:empty{display:none}
93
+ .view{display:flex;flex-direction:column;gap:24px}
94
+
95
+ /* cards */
96
+ .card{border:1px solid var(--line);border-radius:12px;background:var(--panel);overflow:hidden;min-width:0}
97
+ .card-pad{padding:20px 22px;display:flex;flex-direction:column;gap:18px}
98
+ .card-head{display:flex;align-items:center;gap:18px;flex-wrap:wrap}
99
+ .card-head.bar{padding:18px 22px 14px}
100
+ .card-head.bar.ruled{border-bottom:1px solid var(--line)}
101
+ .card-title{font-weight:500;font-size:15px;flex:1}
102
+ .row-wrap{display:flex;flex-wrap:wrap;gap:24px}
103
+ .row-wrap>*{min-width:0}
104
+ .grow2{flex:2 1 560px}.grow1{flex:1 1 320px}
105
+ .legend{display:flex;align-items:center;gap:7px;font-size:12px;color:var(--mute)}
106
+ .legend i{display:block;width:10px;height:2px;background:var(--acc)}
107
+ .legend i.fail{width:6px;height:8px;background:var(--err)}
108
+ .legend i.bars{width:8px;height:8px;border-radius:2px;background:color-mix(in oklch,var(--c1) 45%,transparent)}
109
+ .legend i.line{background:var(--c4)}
110
+
111
+ /* kpis */
112
+ .kpis{display:flex;flex-wrap:wrap;gap:1px;border:1px solid var(--line);border-radius:12px;background:var(--line);overflow:hidden}
113
+ .kpi{flex:1 1 160px;min-width:0;display:flex;flex-direction:column;gap:12px;padding:18px 20px;background:var(--panel);color:var(--ink)}
114
+ a.kpi:hover{text-decoration:none;background:var(--panel2)}
115
+ .kpi-label{display:flex;align-items:center;gap:8px;font-family:var(--mono);font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--mute)}
116
+ .kpi-label i{width:7px;height:7px;border-radius:2px;background:var(--hue)}
117
+ .kpi-val{display:flex;align-items:baseline;gap:8px}
118
+ .kpi-val b{font-size:30px;font-weight:400;letter-spacing:-.03em;font-variant-numeric:tabular-nums}
119
+ .kpi-sub{font-family:var(--mono);font-size:11px;color:var(--faint)}
120
+
121
+ /* throughput chart */
122
+ .chart{display:grid;grid-template-columns:44px minmax(0,1fr);gap:8px}
123
+ .yaxis{display:flex;flex-direction:column;justify-content:space-between;height:220px;padding-bottom:2px;font-family:var(--mono);font-size:10px;color:var(--faint);text-align:right}
124
+ .plot-col{display:flex;flex-direction:column;gap:8px;min-width:0}
125
+ .plot{position:relative;height:220px;cursor:crosshair}
126
+ .plot svg{position:absolute;inset:0;width:100%;height:100%;overflow:visible}
127
+ .xaxis{display:flex;justify-content:space-between;font-family:var(--mono);font-size:10px;color:var(--faint)}
128
+ .hit{position:absolute;top:0;bottom:0}
129
+ .hit .hv-line,.hit .hv-dot,.hit .tip{display:none}
130
+ .hit:hover .hv-line,.hit:hover .hv-dot{display:block}
131
+ .hit:hover .tip{display:flex}
132
+ .hv-line{position:absolute;top:0;bottom:0;left:50%;width:1px;background:var(--line2);pointer-events:none}
133
+ .hv-dot{position:absolute;left:50%;width:9px;height:9px;margin:-4.5px 0 0 -4.5px;border-radius:50%;background:var(--panel);border:2px solid var(--acc);pointer-events:none}
134
+ .tip{position:absolute;top:8px;left:calc(50% + 14px);z-index:2;flex-direction:column;gap:6px;padding:10px 12px;min-width:160px;border:1px solid var(--line2);border-radius:8px;background:var(--panel);box-shadow:0 8px 24px -12px rgba(0,0,0,.35);pointer-events:none;font-size:12px}
135
+ .hit.flip .tip{left:auto;right:calc(50% + 14px)}
136
+ .tip div{display:flex;justify-content:space-between;gap:16px}
137
+
138
+ /* metrics chart */
139
+ .chart.dual{grid-template-columns:44px minmax(0,1fr) 52px}
140
+ .yaxis.right{text-align:left;color:var(--c4)}
141
+ .xaxis.abs{position:relative;height:12px}
142
+ .xaxis.abs span{position:absolute;transform:translateX(-50%);white-space:nowrap}
143
+ .pt{position:absolute;width:5px;height:5px;margin:-2.5px 0 0 -2.5px;border-radius:50%;background:var(--c4);pointer-events:none}
144
+ .hit.band:hover{background:color-mix(in oklch,var(--ink) 4%,transparent)}
145
+
146
+ /* workers */
147
+ .cells{display:grid;grid-template-columns:repeat(16,1fr);gap:4px}
148
+ .cells i{display:block;aspect-ratio:1;border-radius:3px;background:var(--panel2);transition:background .5s}
149
+ .procs{border-top:1px solid var(--line)}
150
+ .proc{display:grid;grid-template-columns:minmax(0,1fr) auto auto auto;gap:14px;align-items:center;padding:10px 0;border-bottom:1px solid var(--line);font-size:12.5px;font-family:var(--mono)}
151
+ .proc span{font-size:11px}
152
+ .proc:last-child{border-bottom:0}
153
+
154
+ /* tables */
155
+ .scroll{overflow-x:auto}
156
+ .thead,.trow{display:grid;gap:12px;align-items:center;padding:11px 22px;border-bottom:1px solid var(--line)}
157
+ .thead{padding-top:9px;padding-bottom:9px;font-family:var(--mono);font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:var(--faint)}
158
+ .thead.top{border-top:1px solid var(--line)}
159
+ .thead.tint{background:var(--panel2)}
160
+ .trow{font-variant-numeric:tabular-nums}
161
+ .trow.hover:hover{background:var(--panel2)}
162
+ .trow.selectable{cursor:pointer}
163
+ .trow.selected{background:var(--panel2);box-shadow:inset 3px 0 0 var(--acc)}
164
+ .trow:last-child{border-bottom:0}
165
+ .trow.group{padding-top:8px;padding-bottom:8px;background:var(--panel2);font-family:var(--mono);font-size:11px;color:var(--mute)}
166
+ .trow .full{grid-column:1/-1}
167
+ .t-queues{grid-template-columns:1.4fr .8fr .6fr .7fr 80px}
168
+ .t-queues-full{grid-template-columns:1.4fr .8fr .6fr .7fr .8fr 110px;column-gap:28px}
169
+ .t-jobs{grid-template-columns:110px 1.4fr 1.5fr .8fr 100px 60px 110px}
170
+ .t-jobs.actions{grid-template-columns:110px 1.4fr 1.5fr .8fr 100px 110px 90px}
171
+ .t-jobs.plain{grid-template-columns:110px 1.4fr 1.5fr .8fr 100px 150px}
172
+ .t-cons{grid-template-columns:1.5fr .8fr .8fr .7fr .7fr 1.4fr}
173
+ .t-crons{grid-template-columns:1.5fr 1.2fr .8fr 1.2fr 190px;gap:14px}
174
+ .trow.t-crons{padding-top:13px;padding-bottom:13px}
175
+ .t-procs{grid-template-columns:1.4fr 1.5fr 1.3fr 50px 80px 64px 56px 76px 64px 72px}
176
+ .t-metrics{grid-template-columns:2fr .8fr .8fr 1fr 1fr}
177
+ .t-kv{grid-template-columns:200px minmax(0,1fr)}
178
+ .trow.t-jobs,.trow.t-cons,.trow.t-crons,.trow.t-metrics,.trow.t-kv{font-size:13px}
179
+ .trow.t-procs{font-size:12.5px}
180
+ .qname{display:flex;align-items:center;gap:9px;min-width:0}
181
+ .qname .mono{font-size:13px}
182
+ .dot{width:6px;height:6px;border-radius:50%;display:inline-block;flex:none}
183
+ .paused-tag{font-size:10.5px;color:var(--warn)}
184
+ .btn{display:inline-flex;align-items:center;gap:6px;padding:5px 10px;border:1px solid var(--line2);border-radius:6px;background:var(--panel);font-size:12px;color:var(--ink);white-space:nowrap}
185
+ .btn:hover{border-color:var(--mute);text-decoration:none}
186
+ .btn.quiet{color:var(--mute)}
187
+ .btn.danger{color:var(--err)}
188
+ .btn.danger:hover{border-color:var(--err)}
189
+ .btn.on{background:var(--panel2)}
190
+ .btn[disabled],.btn.disabled{opacity:.45;pointer-events:none}
191
+ .btns{display:flex;gap:6px;justify-content:flex-end}
192
+ .pill{justify-self:start;padding:3px 8px;border-radius:5px;font-size:11px;font-weight:500;color:var(--tone);background:color-mix(in oklch,var(--tone) 13%,transparent);white-space:nowrap}
193
+ .stack{display:flex;flex-direction:column;gap:3px;min-width:0}
194
+ .small-mono{font-family:var(--mono);font-size:11px;color:var(--faint)}
195
+ .subjects{display:flex;flex-wrap:wrap;gap:6px}
196
+ .subject{padding:2px 7px;border:1px solid var(--line);border-radius:5px;font-family:var(--mono);font-size:11.5px;color:var(--mute)}
197
+ details>summary{cursor:pointer;list-style:none}
198
+ details>summary::-webkit-details-marker{display:none}
199
+ details[open]>summary{margin-bottom:8px}
200
+ .code-block{padding:10px 12px;border:1px solid var(--line);border-radius:8px;background:var(--panel2);font-family:var(--mono);font-size:11.5px;color:var(--mute);max-height:320px;overflow:auto}
201
+ .error-line{display:flex;flex-direction:column;gap:6px;padding:10px 12px;border-left:2px solid var(--err);border-radius:0 8px 8px 0;background:color-mix(in oklch,var(--err) 7%,transparent);font-size:12.5px}
202
+ .error-line b{color:var(--err);font-weight:500;font-family:var(--mono);font-size:12px}
203
+
204
+ /* tabs */
205
+ .tabs{display:flex;align-items:center;gap:4px;padding:12px 16px;border-bottom:1px solid var(--line);flex-wrap:wrap}
206
+ .tabs a{display:flex;gap:8px;align-items:center;padding:6px 11px;border-radius:6px;font-size:13px;font-weight:500;color:var(--mute)}
207
+ .tabs a:hover{text-decoration:none;color:var(--ink)}
208
+ .tabs a.on{background:var(--panel2);color:var(--ink)}
209
+ .tabs a span{font-family:var(--mono);font-size:10.5px;color:var(--faint)}
210
+ .tabs .tools{margin-left:auto;display:flex;align-items:center;gap:10px}
211
+ .subbar{display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding:12px 22px;border-bottom:1px solid var(--line)}
212
+
213
+ /* feed */
214
+ .feed{display:flex;flex-direction:column;padding:6px 0}
215
+ .feed-row{display:grid;grid-template-columns:62px 8px minmax(0,1fr) auto;gap:10px;align-items:center;padding:7px 22px;font-size:12.5px}
216
+ .feed-row .mono{font-size:10.5px}
217
+
218
+ /* streams */
219
+ .stream-head{display:grid;grid-template-columns:minmax(220px,1.4fr) repeat(auto-fit,minmax(100px,1fr));gap:20px;align-items:end;padding:20px 22px}
220
+ .stream-name{display:flex;align-items:center;gap:10px;font-family:var(--mono);font-size:16px;font-weight:500;color:var(--ink)}
221
+ .stream-name i{width:10px;height:10px;border-radius:3px;background:var(--hue);flex:none}
222
+ .stat{display:flex;flex-direction:column;gap:4px;min-width:0}
223
+ .stat b{font-size:18px;font-weight:400;letter-spacing:-.02em;font-variant-numeric:tabular-nums}
224
+ .stream-tools{display:flex;align-items:center;gap:10px;justify-content:flex-end;padding:0 22px 16px}
225
+ .lag{display:flex;align-items:center;gap:10px}
226
+ .lag-track{flex:1;height:4px;border-radius:2px;background:var(--panel2);overflow:hidden}
227
+ .lag-track i{display:block;height:100%}
228
+ .lag span{font-family:var(--mono);font-size:11px;min-width:44px;text-align:right}
229
+ .banner{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;padding:14px 22px}
230
+ .banner .mute{font-size:13px}
231
+
232
+ /* batches */
233
+ .batches{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,380px),1fr));gap:20px}
234
+ .batch{display:flex;flex-direction:column;gap:16px;padding:20px 22px}
235
+ .batch-top{display:flex;align-items:flex-start;gap:12px}
236
+ .batch-pct{display:flex;align-items:baseline;gap:8px}
237
+ .batch-pct b{font-size:32px;font-weight:400;letter-spacing:-.03em;font-variant-numeric:tabular-nums}
238
+ .progress{display:flex;height:6px;border-radius:3px;overflow:hidden;background:var(--panel2);gap:2px}
239
+ .progress i{display:block}
240
+ .batch-foot{display:flex;gap:18px;font-size:12px;color:var(--mute);flex-wrap:wrap}
241
+ .batch-foot .cb{margin-left:auto;font-family:var(--mono);font-size:11px;display:flex;flex-direction:column;gap:2px;text-align:right}
242
+
243
+ /* states */
244
+ .empty{display:flex;flex-direction:column;align-items:center;gap:6px;padding:36px 22px;text-align:center;color:var(--mute);font-size:13px}
245
+ .empty b{color:var(--ink);font-weight:500;font-size:14px}
246
+ .empty code{color:var(--ink)}
247
+ .card>.empty{border-top:0}
248
+ .notice{padding:12px 16px;border:1px solid var(--line);border-radius:10px;background:var(--panel);color:var(--mute);font-size:13px}
249
+ .notice.warn{border-color:color-mix(in oklch,var(--warn) 40%,var(--line));background:color-mix(in oklch,var(--warn) 8%,var(--panel));color:var(--ink)}
250
+ .loading{padding:28px 22px;color:var(--faint);font-family:var(--mono);font-size:11px}
251
+ .pager{display:flex;align-items:center;justify-content:center;gap:6px;flex-wrap:wrap;padding:14px 22px;border-top:1px solid var(--line)}
252
+ .pager .gap{color:var(--faint);padding:0 4px}
253
+ .pager .cur{background:var(--panel2);border-color:var(--line2);pointer-events:none}
254
+
255
+ /* responsive */
256
+ @media (max-width:1080px){.live .t{display:none}}
257
+ @media (max-width:900px){
258
+ .app{grid-template-columns:minmax(0,1fr)}
259
+ .sidebar{position:fixed;left:0;top:0;width:min(280px,86vw);transform:translateX(-105%)}
260
+ body.menu-open .sidebar{transform:none;box-shadow:0 20px 60px -10px rgba(0,0,0,.45)}
261
+ body.menu-open .backdrop{display:block}
262
+ .icon-btn{display:flex}
263
+ .topbar .logo-sm{display:block}
264
+ .topbar{padding:12px 24px}
265
+ .content{padding:24px 24px 44px}
266
+ }
267
+ @media (max-width:600px){
268
+ .topbar{padding:10px 16px;gap:10px}
269
+ .content{padding:20px 16px 40px;gap:18px}
270
+ .view{gap:18px}
271
+ .page-head h1{font-size:24px}
572
272
  }