@adonis-agora/durable 0.27.0 → 0.28.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 (34) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/assets/spa/assets/index-Db5aPtlJ.js +16 -0
  3. package/dist/assets/spa/assets/index-Db5aPtlJ.js.map +1 -0
  4. package/dist/assets/spa/index.html +1 -1
  5. package/dist/providers/dashboard_provider.d.ts +3 -9
  6. package/dist/providers/dashboard_provider.d.ts.map +1 -1
  7. package/dist/providers/dashboard_provider.js +3 -18
  8. package/dist/providers/dashboard_provider.js.map +1 -1
  9. package/dist/src/dashboard/compat-view.d.ts +1 -1
  10. package/dist/src/dashboard/compat-view.d.ts.map +1 -1
  11. package/dist/src/dashboard/compat-view.js +1 -1
  12. package/dist/src/dashboard/compat-view.js.map +1 -1
  13. package/dist/src/dashboard/handlers.d.ts +3 -3
  14. package/dist/src/dashboard/handlers.js +2 -2
  15. package/dist/src/dashboard/index.d.ts +1 -2
  16. package/dist/src/dashboard/index.d.ts.map +1 -1
  17. package/dist/src/dashboard/index.js +1 -2
  18. package/dist/src/dashboard/index.js.map +1 -1
  19. package/dist/src/dashboard/login_page.d.ts +1 -1
  20. package/dist/src/dashboard/login_page.js +1 -1
  21. package/dist/src/dashboard/spa.d.ts +21 -4
  22. package/dist/src/dashboard/spa.d.ts.map +1 -1
  23. package/dist/src/dashboard/spa.js +19 -5
  24. package/dist/src/dashboard/spa.js.map +1 -1
  25. package/dist/src/index.d.ts +1 -1
  26. package/dist/src/index.js +1 -1
  27. package/package.json +3 -3
  28. package/dist/assets/dashboard.html +0 -559
  29. package/dist/assets/spa/assets/index-CS0tc60i.js +0 -16
  30. package/dist/assets/spa/assets/index-CS0tc60i.js.map +0 -1
  31. package/dist/src/dashboard/html.d.ts +0 -7
  32. package/dist/src/dashboard/html.d.ts.map +0 -1
  33. package/dist/src/dashboard/html.js +0 -24
  34. package/dist/src/dashboard/html.js.map +0 -1
@@ -1,559 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <title>Durable · Workflow Runs</title>
7
- <style>
8
- :root {
9
- --bg: #0d1117;
10
- --panel: #161b22;
11
- --panel-2: #1c2128;
12
- --border: #30363d;
13
- --text: #e6edf3;
14
- --muted: #8b949e;
15
- --accent: #58a6ff;
16
- --green: #3fb950;
17
- --red: #f85149;
18
- --amber: #d29922;
19
- --purple: #bc8cff;
20
- --grey: #6e7681;
21
- }
22
- * { box-sizing: border-box; }
23
- html, body {
24
- margin: 0;
25
- background: var(--bg);
26
- color: var(--text);
27
- font: 14px/1.5 ui-sans-serif, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
28
- }
29
- a { color: var(--accent); text-decoration: none; }
30
- a:hover { text-decoration: underline; }
31
- code, .mono {
32
- font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
33
- font-size: 12.5px;
34
- }
35
- header {
36
- display: flex;
37
- align-items: center;
38
- gap: 16px;
39
- padding: 14px 22px;
40
- border-bottom: 1px solid var(--border);
41
- background: var(--panel);
42
- position: sticky;
43
- top: 0;
44
- z-index: 10;
45
- }
46
- header h1 {
47
- font-size: 16px;
48
- margin: 0;
49
- font-weight: 600;
50
- letter-spacing: 0.3px;
51
- }
52
- header .dot { color: var(--accent); }
53
- .spacer { flex: 1; }
54
- main { padding: 22px; max-width: 1180px; margin: 0 auto; }
55
- .controls {
56
- display: flex;
57
- gap: 10px;
58
- align-items: center;
59
- flex-wrap: wrap;
60
- margin-bottom: 16px;
61
- }
62
- select, input, button {
63
- background: var(--panel-2);
64
- color: var(--text);
65
- border: 1px solid var(--border);
66
- border-radius: 6px;
67
- padding: 7px 11px;
68
- font-size: 13px;
69
- }
70
- button { cursor: pointer; transition: border-color 0.15s, background 0.15s; }
71
- button:hover { border-color: var(--accent); }
72
- button:disabled { opacity: 0.5; cursor: not-allowed; }
73
- button.primary { background: #1f6feb; border-color: #1f6feb; color: #fff; }
74
- button.primary:hover { background: #2a7fff; }
75
- button.danger:hover { border-color: var(--red); color: var(--red); }
76
- table { width: 100%; border-collapse: collapse; }
77
- th, td {
78
- text-align: left;
79
- padding: 9px 12px;
80
- border-bottom: 1px solid var(--border);
81
- vertical-align: middle;
82
- }
83
- th {
84
- color: var(--muted);
85
- font-weight: 500;
86
- font-size: 12px;
87
- text-transform: uppercase;
88
- letter-spacing: 0.5px;
89
- }
90
- tbody tr { cursor: pointer; transition: background 0.1s; }
91
- tbody tr:hover { background: var(--panel); }
92
- .badge {
93
- display: inline-block;
94
- padding: 2px 9px;
95
- border-radius: 999px;
96
- font-size: 11.5px;
97
- font-weight: 600;
98
- text-transform: capitalize;
99
- border: 1px solid transparent;
100
- }
101
- .s-completed { color: var(--green); background: rgba(63,185,80,0.12); border-color: rgba(63,185,80,0.3); }
102
- .s-failed, .s-dead { color: var(--red); background: rgba(248,81,73,0.12); border-color: rgba(248,81,73,0.3); }
103
- .s-running, .s-pending { color: var(--accent); background: rgba(88,166,255,0.12); border-color: rgba(88,166,255,0.3); }
104
- .s-suspended { color: var(--amber); background: rgba(210,153,34,0.12); border-color: rgba(210,153,34,0.3); }
105
- .s-cancelled { color: var(--grey); background: rgba(110,118,129,0.15); border-color: rgba(110,118,129,0.3); }
106
- .muted { color: var(--muted); }
107
- .empty { color: var(--muted); padding: 40px; text-align: center; }
108
- .panel {
109
- background: var(--panel);
110
- border: 1px solid var(--border);
111
- border-radius: 8px;
112
- padding: 18px 20px;
113
- margin-bottom: 18px;
114
- }
115
- .panel h2 { font-size: 14px; margin: 0 0 14px; font-weight: 600; }
116
- .kv { display: grid; grid-template-columns: 150px 1fr; gap: 6px 14px; }
117
- .kv dt { color: var(--muted); }
118
- .kv dd { margin: 0; word-break: break-word; }
119
- pre {
120
- background: var(--panel-2);
121
- border: 1px solid var(--border);
122
- border-radius: 6px;
123
- padding: 10px 12px;
124
- overflow: auto;
125
- margin: 4px 0 0;
126
- max-height: 240px;
127
- }
128
- .timeline { display: flex; flex-direction: column; gap: 0; }
129
- .step {
130
- display: flex;
131
- gap: 14px;
132
- padding: 12px 0;
133
- border-bottom: 1px solid var(--border);
134
- }
135
- .step:last-child { border-bottom: none; }
136
- .step .marker { width: 10px; padding-top: 5px; }
137
- .step .marker .pip { width: 10px; height: 10px; border-radius: 50%; }
138
- .step .body { flex: 1; min-width: 0; }
139
- .step .row1 { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
140
- .step .name { font-weight: 600; }
141
- .step .meta { color: var(--muted); font-size: 12px; display: flex; gap: 14px; flex-wrap: wrap; margin-top: 4px; }
142
- .kind {
143
- font-size: 10.5px;
144
- text-transform: uppercase;
145
- letter-spacing: 0.5px;
146
- color: var(--purple);
147
- border: 1px solid rgba(188,140,255,0.3);
148
- background: rgba(188,140,255,0.1);
149
- border-radius: 4px;
150
- padding: 1px 6px;
151
- }
152
- .back { margin-bottom: 14px; display: inline-block; }
153
- .actions { display: flex; gap: 10px; margin: 14px 0 0; }
154
- .toast {
155
- position: fixed;
156
- bottom: 22px;
157
- right: 22px;
158
- background: var(--panel-2);
159
- border: 1px solid var(--border);
160
- border-radius: 8px;
161
- padding: 12px 16px;
162
- max-width: 360px;
163
- box-shadow: 0 8px 24px rgba(0,0,0,0.4);
164
- opacity: 0;
165
- transform: translateY(8px);
166
- transition: opacity 0.2s, transform 0.2s;
167
- pointer-events: none;
168
- }
169
- .toast.show { opacity: 1; transform: translateY(0); }
170
- .toast.err { border-color: var(--red); }
171
- .health { display: flex; gap: 12px; flex-wrap: wrap; }
172
- .health .h-card {
173
- border: 1px solid var(--border);
174
- border-radius: 6px;
175
- padding: 8px 14px;
176
- background: var(--panel-2);
177
- font-size: 12.5px;
178
- }
179
- .health .h-card.stalled { border-color: var(--red); }
180
- .nav { display: flex; gap: 16px; }
181
- .nav a { color: var(--muted); font-size: 13px; }
182
- .nav a:hover, .nav a.active { color: var(--text); }
183
- /* Fleet-health / protocol-compat badges — mirror src/dashboard/compat-view.ts outcomeClass(). */
184
- .badge.c-ok { color: var(--green); background: rgba(63,185,80,0.12); border-color: rgba(63,185,80,0.3); }
185
- .badge.c-degraded { color: var(--amber); background: rgba(210,153,34,0.12); border-color: rgba(210,153,34,0.3); }
186
- .badge.c-bad { color: var(--red); background: rgba(248,81,73,0.12); border-color: rgba(248,81,73,0.3); }
187
- .panel.flagged-panel { border-color: var(--red); }
188
- tr.flagged { background: rgba(248,81,73,0.06); }
189
- .flag-reason { color: var(--red); }
190
- </style>
191
- </head>
192
- <body>
193
- <header>
194
- <h1><span class="dot">●</span> Durable</h1>
195
- <span class="muted mono" id="crumb">runs</span>
196
- <nav class="nav">
197
- <a href="#/" id="nav-runs">Runs</a>
198
- <a href="#/health" id="nav-health">Fleet health</a>
199
- </nav>
200
- <span class="spacer"></span>
201
- <span class="muted" id="health-strip"></span>
202
- </header>
203
- <main id="app"><div class="empty">Loading…</div></main>
204
- <div class="toast" id="toast"></div>
205
-
206
- <script>
207
- // The API base path is injected by the provider at serve time.
208
- const API_BASE = '__API_BASE__';
209
-
210
- const el = (tag, attrs = {}, children = []) => {
211
- const n = document.createElement(tag);
212
- for (const [k, v] of Object.entries(attrs)) {
213
- if (k === 'class') n.className = v;
214
- else if (k === 'html') n.innerHTML = v;
215
- else if (k.startsWith('on') && typeof v === 'function') n.addEventListener(k.slice(2), v);
216
- else if (v != null) n.setAttribute(k, v);
217
- }
218
- for (const c of [].concat(children)) {
219
- if (c == null) continue;
220
- n.appendChild(typeof c === 'string' ? document.createTextNode(c) : c);
221
- }
222
- return n;
223
- };
224
-
225
- const toast = (msg, isErr) => {
226
- const t = document.getElementById('toast');
227
- t.textContent = msg;
228
- t.className = 'toast show' + (isErr ? ' err' : '');
229
- setTimeout(() => { t.className = 'toast'; }, 3200);
230
- };
231
-
232
- const api = async (path, opts) => {
233
- const res = await fetch(API_BASE + path, {
234
- headers: { 'Content-Type': 'application/json' },
235
- ...opts,
236
- });
237
- const body = await res.json().catch(() => ({}));
238
- if (!res.ok) throw new Error(body.error || ('HTTP ' + res.status));
239
- return body;
240
- };
241
-
242
- const badge = (status) =>
243
- el('span', { class: 'badge s-' + status }, status);
244
-
245
- const fmt = (iso) => {
246
- if (!iso) return '—';
247
- const d = new Date(iso);
248
- return d.toLocaleString();
249
- };
250
- const dur = (ms) => {
251
- if (ms == null) return '—';
252
- if (ms < 1000) return ms + 'ms';
253
- if (ms < 60000) return (ms / 1000).toFixed(1) + 's';
254
- return (ms / 60000).toFixed(1) + 'm';
255
- };
256
-
257
- const PIP = {
258
- completed: 'var(--green)', failed: 'var(--red)', running: 'var(--accent)',
259
- pending: 'var(--accent)', suspended: 'var(--amber)', cancelled: 'var(--grey)',
260
- dead: 'var(--red)',
261
- };
262
-
263
- const state = { status: '', workflow: '' };
264
-
265
- async function refreshHealth() {
266
- try {
267
- const { groups } = await api('/health');
268
- const strip = document.getElementById('health-strip');
269
- if (!groups.length) { strip.textContent = ''; return; }
270
- const stalled = groups.filter((g) => g.stalled).length;
271
- strip.innerHTML = stalled
272
- ? '<span style="color:var(--red)">⚠ ' + stalled + ' stalled group(s)</span>'
273
- : groups.length + ' worker group(s)';
274
- } catch (_) { /* health is best-effort */ }
275
- }
276
-
277
- async function renderList() {
278
- document.getElementById('crumb').textContent = 'runs';
279
- const app = document.getElementById('app');
280
- const params = new URLSearchParams();
281
- if (state.status) params.set('status', state.status);
282
- if (state.workflow) params.set('workflow', state.workflow);
283
- let data;
284
- try {
285
- data = await api('/runs?' + params.toString());
286
- } catch (e) {
287
- app.innerHTML = '';
288
- app.appendChild(el('div', { class: 'empty' }, 'Failed to load runs: ' + e.message));
289
- return;
290
- }
291
-
292
- const statusSel = el('select', {
293
- onchange: (e) => { state.status = e.target.value; renderList(); },
294
- }, [el('option', { value: '' }, 'All statuses')].concat(
295
- data.statuses.map((s) =>
296
- el('option', { value: s, ...(s === state.status ? { selected: '' } : {}) }, s)),
297
- ));
298
-
299
- const wfInput = el('input', {
300
- placeholder: 'Filter by workflow…',
301
- value: state.workflow,
302
- onkeydown: (e) => { if (e.key === 'Enter') { state.workflow = e.target.value.trim(); renderList(); } },
303
- });
304
-
305
- const controls = el('div', { class: 'controls' }, [
306
- statusSel,
307
- wfInput,
308
- el('button', { onclick: () => renderList() }, '↻ Refresh'),
309
- ]);
310
-
311
- const rows = data.runs.map((r) =>
312
- el('tr', { onclick: () => navigate(r.id) }, [
313
- el('td', {}, el('code', {}, r.id)),
314
- el('td', {}, r.workflow + (r.workflowVersion ? ' @' + r.workflowVersion : '')),
315
- el('td', {}, badge(r.status)),
316
- el('td', { class: 'muted mono' }, fmt(r.createdAt)),
317
- el('td', { class: 'muted mono' }, fmt(r.updatedAt)),
318
- ]));
319
-
320
- const table = el('table', {}, [
321
- el('thead', {}, el('tr', {}, [
322
- el('th', {}, 'Run id'), el('th', {}, 'Workflow'),
323
- el('th', {}, 'Status'), el('th', {}, 'Started'), el('th', {}, 'Updated'),
324
- ])),
325
- el('tbody', {}, rows.length ? rows : []),
326
- ]);
327
-
328
- app.innerHTML = '';
329
- app.appendChild(controls);
330
- if (rows.length) app.appendChild(table);
331
- else app.appendChild(el('div', { class: 'empty' }, 'No runs match.'));
332
- }
333
-
334
- async function renderDetail(id) {
335
- document.getElementById('crumb').textContent = 'runs / ' + id;
336
- const app = document.getElementById('app');
337
- let data;
338
- try {
339
- data = await api('/runs/' + encodeURIComponent(id));
340
- } catch (e) {
341
- app.innerHTML = '';
342
- app.appendChild(el('a', { class: 'back', href: '#' }, '← back'));
343
- app.appendChild(el('div', { class: 'empty' }, e.message));
344
- return;
345
- }
346
- const run = data.run;
347
-
348
- const canRetry = ['failed', 'dead', 'cancelled', 'suspended'].includes(run.status);
349
- const canCancel = ['pending', 'running', 'suspended'].includes(run.status);
350
-
351
- const actions = el('div', { class: 'actions' }, [
352
- el('button', {
353
- class: 'primary', disabled: canRetry ? null : '',
354
- onclick: () => act('retry', id),
355
- }, '↻ Retry'),
356
- el('button', {
357
- class: 'danger', disabled: canCancel ? null : '',
358
- onclick: () => act('cancel', id),
359
- }, '✕ Cancel'),
360
- ]);
361
-
362
- const overview = el('div', { class: 'panel' }, [
363
- el('h2', {}, 'Overview'),
364
- el('dl', { class: 'kv' }, [
365
- el('dt', {}, 'Status'), el('dd', {}, badge(run.status)),
366
- el('dt', {}, 'Workflow'), el('dd', {}, run.workflow + ' @' + run.workflowVersion),
367
- el('dt', {}, 'Run id'), el('dd', {}, el('code', {}, run.id)),
368
- el('dt', {}, 'Tags'), el('dd', { class: 'muted' }, (run.tags || []).join(', ') || '—'),
369
- el('dt', {}, 'Started'), el('dd', { class: 'mono muted' }, fmt(run.createdAt)),
370
- el('dt', {}, 'Updated'), el('dd', { class: 'mono muted' }, fmt(run.updatedAt)),
371
- ]),
372
- actions,
373
- run.input !== undefined ? el('div', {}, [
374
- el('div', { class: 'muted', style: 'margin-top:14px' }, 'Input'),
375
- el('pre', {}, el('code', {}, JSON.stringify(run.input, null, 2))),
376
- ]) : null,
377
- run.error ? el('div', {}, [
378
- el('div', { class: 'muted', style: 'margin-top:14px;color:var(--red)' }, 'Error'),
379
- el('pre', {}, el('code', {}, JSON.stringify(run.error, null, 2))),
380
- ]) : null,
381
- run.output !== undefined && run.output !== null ? el('div', {}, [
382
- el('div', { class: 'muted', style: 'margin-top:14px' }, 'Output'),
383
- el('pre', {}, el('code', {}, JSON.stringify(run.output, null, 2))),
384
- ]) : null,
385
- ]);
386
-
387
- const steps = data.timeline.map((cp) =>
388
- el('div', { class: 'step' }, [
389
- el('div', { class: 'marker' },
390
- el('div', { class: 'pip', style: 'background:' + (PIP[cp.status] || 'var(--grey)') })),
391
- el('div', { class: 'body' }, [
392
- el('div', { class: 'row1' }, [
393
- el('span', { class: 'name' }, cp.name),
394
- el('span', { class: 'kind' }, cp.kind),
395
- badge(cp.status),
396
- ]),
397
- el('div', { class: 'meta' }, [
398
- el('span', {}, 'attempts: ' + cp.attempts),
399
- el('span', {}, 'duration: ' + dur(cp.durationMs)),
400
- cp.queueMs ? el('span', {}, 'queued: ' + dur(cp.queueMs)) : null,
401
- cp.workerGroup ? el('span', {}, 'group: ' + cp.workerGroup) : null,
402
- el('span', { class: 'mono' }, fmt(cp.startedAt)),
403
- ]),
404
- cp.error ? el('pre', {}, el('code', {}, JSON.stringify(cp.error, null, 2))) : null,
405
- ]),
406
- ]));
407
-
408
- const timeline = el('div', { class: 'panel' }, [
409
- el('h2', {}, 'Timeline (' + data.timeline.length + ' steps)'),
410
- steps.length ? el('div', { class: 'timeline' }, steps)
411
- : el('div', { class: 'muted' }, 'No checkpoints yet.'),
412
- ]);
413
-
414
- const childrenPanel = data.children.length ? el('div', { class: 'panel' }, [
415
- el('h2', {}, 'Children'),
416
- el('div', {}, data.children.map((c) =>
417
- el('div', {}, el('a', { href: '#', onclick: (e) => { e.preventDefault(); navigate(c); } },
418
- el('code', {}, c))))),
419
- ]) : null;
420
-
421
- app.innerHTML = '';
422
- app.appendChild(el('a', {
423
- class: 'back', href: '#',
424
- onclick: (e) => { e.preventDefault(); navigate(''); },
425
- }, '← all runs'));
426
- app.appendChild(overview);
427
- app.appendChild(timeline);
428
- if (childrenPanel) app.appendChild(childrenPanel);
429
- }
430
-
431
- // Mirrors src/dashboard/compat-view.ts — keep the two in lockstep (unit-tested there).
432
- const OUTCOME = {
433
- compatible: { cls: 'c-ok', label: 'compatible' },
434
- degraded: { cls: 'c-degraded', label: 'degraded' },
435
- incompatible: { cls: 'c-bad', label: 'incompatible' },
436
- };
437
- const protoRange = (r) =>
438
- !r ? '—' : (r[0] === r[1] ? 'v' + r[0] : 'v' + r[0] + '–' + r[1]);
439
-
440
- async function renderCompat() {
441
- document.getElementById('crumb').textContent = 'fleet health';
442
- const app = document.getElementById('app');
443
- let data;
444
- try {
445
- data = await api('/compat');
446
- } catch (e) {
447
- app.innerHTML = '';
448
- app.appendChild(el('div', { class: 'empty' }, 'Failed to load fleet health: ' + e.message));
449
- return;
450
- }
451
-
452
- const cp = data.controlPlane;
453
- const cpPanel = el('div', { class: 'panel' }, [
454
- el('h2', {}, 'Control plane'),
455
- el('dl', { class: 'kv' }, [
456
- el('dt', {}, 'Instance'), el('dd', {}, el('code', {}, cp.instanceId)),
457
- el('dt', {}, 'Protocol'), el('dd', {}, protoRange(cp.protocolRange)),
458
- el('dt', {}, 'Capabilities'),
459
- el('dd', { class: 'muted' }, (cp.capabilities || []).join(', ') || '—'),
460
- ]),
461
- ]);
462
-
463
- const groupPanels = data.groups.map((g) => {
464
- const rows = g.pods.map((p) => {
465
- const o = OUTCOME[p.outcome] || { cls: '', label: p.outcome };
466
- return el('tr', p.incompatible ? { class: 'flagged' } : {}, [
467
- el('td', {}, el('code', {}, p.instanceId)),
468
- el('td', { class: 'muted' }, p.runtime),
469
- el('td', {}, protoRange(p.protocolRange)),
470
- el('td', {}, el('span', { class: 'badge ' + o.cls }, o.label)),
471
- el('td', { class: p.incompatible ? 'flag-reason' : 'muted' }, p.reason || '—'),
472
- ]);
473
- });
474
- return el('div', { class: 'panel' + (g.incompatible ? ' flagged-panel' : '') }, [
475
- el('h2', {}, (g.incompatible ? '⚠ ' : '') + g.token),
476
- el('table', {}, [
477
- el('thead', {}, el('tr', {}, [
478
- el('th', {}, 'Pod'), el('th', {}, 'Runtime'), el('th', {}, 'Protocol'),
479
- el('th', {}, 'Negotiated'), el('th', {}, 'Reason'),
480
- ])),
481
- el('tbody', {}, rows),
482
- ]),
483
- ]);
484
- });
485
-
486
- const blockedRows = data.blocked.map((b) =>
487
- el('tr', { onclick: () => navigate(b.id) }, [
488
- el('td', {}, el('code', {}, b.id)),
489
- el('td', {}, b.workflow || '—'),
490
- el('td', { class: 'flag-reason' }, b.reason),
491
- el('td', { class: 'muted' }, (b.requires?.length) ? b.requires.join(', ') : '—'),
492
- el('td', { class: 'muted mono' }, fmt(b.updatedAt)),
493
- ]));
494
- const blockedPanel = el('div', { class: 'panel' + (data.blocked.length ? ' flagged-panel' : '') }, [
495
- el('h2', {}, (data.blocked.length ? '⚠ ' : '') + 'Blocked runs (' + data.blocked.length + ')'),
496
- data.blocked.length
497
- ? el('table', {}, [
498
- el('thead', {}, el('tr', {}, [
499
- el('th', {}, 'Run id'), el('th', {}, 'Workflow'), el('th', {}, 'Reason'),
500
- el('th', {}, 'Requires'), el('th', {}, 'Updated'),
501
- ])),
502
- el('tbody', {}, blockedRows),
503
- ])
504
- : el('div', { class: 'muted' }, 'No blocked runs — every run has a compatible worker.'),
505
- ]);
506
-
507
- app.innerHTML = '';
508
- app.appendChild(el('a', {
509
- class: 'back', href: '#',
510
- onclick: (e) => { e.preventDefault(); navigate(''); },
511
- }, '← all runs'));
512
- app.appendChild(cpPanel);
513
- for (const panel of (data.groups.length ? groupPanels
514
- : [el('div', { class: 'panel' }, [
515
- el('h2', {}, 'Workers'),
516
- el('div', { class: 'muted' }, 'No live worker descriptors advertised.'),
517
- ])])) {
518
- app.appendChild(panel);
519
- }
520
- app.appendChild(blockedPanel);
521
- }
522
-
523
- async function act(action, id) {
524
- try {
525
- await api('/runs/' + encodeURIComponent(id) + '/' + action, {
526
- method: 'POST',
527
- body: JSON.stringify({}),
528
- });
529
- toast(action === 'retry' ? 'Run re-enqueued' : 'Run cancelled');
530
- setTimeout(() => renderDetail(id), 250);
531
- } catch (e) {
532
- toast('Failed: ' + e.message, true);
533
- }
534
- }
535
-
536
- function navigate(id) {
537
- location.hash = id ? '/runs/' + id : '';
538
- }
539
-
540
- function route() {
541
- const hash = location.hash.replace(/^#/, '');
542
- const onHealth = hash === '/health';
543
- document.getElementById('nav-health').className = onHealth ? 'active' : '';
544
- document.getElementById('nav-runs').className = onHealth ? '' : 'active';
545
- if (onHealth) {
546
- renderCompat();
547
- } else {
548
- const m = /^\/runs\/(.+)$/.exec(hash);
549
- if (m) renderDetail(decodeURIComponent(m[1]));
550
- else renderList();
551
- }
552
- refreshHealth();
553
- }
554
-
555
- window.addEventListener('hashchange', route);
556
- route();
557
- </script>
558
- </body>
559
- </html>