agent-control-plane 0.7.0 → 0.8.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.
- package/README.md +305 -7
- package/hooks/pr-reconcile-hooks.sh +12 -1
- package/package.json +13 -9
- package/tools/bin/adapter-capabilities.sh +84 -0
- package/tools/bin/adapter-interface.sh +97 -0
- package/tools/bin/claude-adapter.sh +73 -0
- package/tools/bin/codex-adapter.sh +123 -0
- package/tools/bin/flow-runtime-doctor.sh +67 -0
- package/tools/bin/heartbeat-safe-auto.sh +161 -0
- package/tools/bin/kilo-adapter.sh +108 -0
- package/tools/bin/ollama-adapter.sh +160 -0
- package/tools/bin/openclaw-adapter.sh +69 -0
- package/tools/bin/opencode-adapter.sh +98 -0
- package/tools/bin/pi-adapter.sh +95 -0
- package/tools/bin/render-flow-config.sh +98 -0
- package/tools/bin/run-with-adapter.sh +34 -0
- package/tools/bin/sync-shared-agent-home.sh +23 -0
- package/tools/dashboard/__pycache__/server.cpython-311.pyc +0 -0
- package/tools/dashboard/app-v2.js +1120 -0
- package/tools/dashboard/app.js +129 -38
- package/tools/dashboard/index-inline.html +1533 -0
- package/tools/dashboard/index-v2.html +45 -0
- package/tools/dashboard/server.py +64 -15
- package/tools/dashboard/styles.css +595 -521
- package/tools/bin/profile-activate.sh +0 -109
- package/tools/bin/profile-adopt.sh +0 -225
- package/tools/bin/profile-smoke.sh +0 -461
- package/tools/bin/test-smoke.sh +0 -119
|
@@ -0,0 +1,1533 @@
|
|
|
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
|
+
<meta name="color-scheme" content="light dark">
|
|
7
|
+
<title>ACP Worker Dashboard</title>
|
|
8
|
+
<style>
|
|
9
|
+
:root {
|
|
10
|
+
--bg: #f5f5f7;
|
|
11
|
+
--panel: #ffffff;
|
|
12
|
+
--panel-strong: #f8f8fa;
|
|
13
|
+
--ink: #1a1a2e;
|
|
14
|
+
--muted: #6b7280;
|
|
15
|
+
--line: #e5e7eb;
|
|
16
|
+
--accent: #0ea5e9;
|
|
17
|
+
--accent-soft: #e0f2fe;
|
|
18
|
+
--warn: #f59e0b;
|
|
19
|
+
--warn-soft: #fef3c7;
|
|
20
|
+
--danger: #ef4444;
|
|
21
|
+
--danger-soft: #fee2e2;
|
|
22
|
+
--shadow: 0 1px 3px rgba(0,0,0,0.08);
|
|
23
|
+
--button-bg: var(--ink);
|
|
24
|
+
--button-ink: #ffffff;
|
|
25
|
+
--button-hover: #2d2d44;
|
|
26
|
+
--hero-bg: rgba(255,255,255,0.95);
|
|
27
|
+
--profile-bg: rgba(255,255,255,0.97);
|
|
28
|
+
--body-gradient-top: transparent;
|
|
29
|
+
--body-gradient-bottom: #f5f5f7;
|
|
30
|
+
--theme-toggle-bg: var(--panel);
|
|
31
|
+
--theme-toggle-ink: var(--ink);
|
|
32
|
+
--theme-toggle-line: var(--line);
|
|
33
|
+
--theme-toggle-hover: var(--panel-strong);
|
|
34
|
+
--reported-soft: #dbeafe;
|
|
35
|
+
--reported-ink: #2563eb;
|
|
36
|
+
--implemented-soft: #dcfce7;
|
|
37
|
+
--implemented-ink: #16a34a;
|
|
38
|
+
--blocked-soft: #fef3c7;
|
|
39
|
+
--blocked-ink: #d97706;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
:root[data-theme="dark"] {
|
|
43
|
+
--bg: #0f0f1a;
|
|
44
|
+
--panel: #1a1a2e;
|
|
45
|
+
--panel-strong: #23233a;
|
|
46
|
+
--ink: #e5e7eb;
|
|
47
|
+
--muted: #9ca3af;
|
|
48
|
+
--line: #2d2d44;
|
|
49
|
+
--accent: #38bdf8;
|
|
50
|
+
--accent-soft: #0c2a3d;
|
|
51
|
+
--warn: #fbbf24;
|
|
52
|
+
--warn-soft: #2d2410;
|
|
53
|
+
--danger: #f87171;
|
|
54
|
+
--danger-soft: #2d1a1a;
|
|
55
|
+
--shadow: 0 1px 3px rgba(0,0,0,0.3);
|
|
56
|
+
--button-bg: #e5e7eb;
|
|
57
|
+
--button-ink: #0f0f1a;
|
|
58
|
+
--button-hover: #d1d5db;
|
|
59
|
+
--hero-bg: rgba(26,26,46,0.95);
|
|
60
|
+
--profile-bg: rgba(26,26,46,0.97);
|
|
61
|
+
--body-gradient-top: transparent;
|
|
62
|
+
--body-gradient-bottom: #0a0a14;
|
|
63
|
+
--theme-toggle-bg: #23233a;
|
|
64
|
+
--theme-toggle-ink: #e5e7eb;
|
|
65
|
+
--theme-toggle-line: #2d2d44;
|
|
66
|
+
--theme-toggle-hover: #2d2d44;
|
|
67
|
+
--reported-soft: #1e3a5f;
|
|
68
|
+
--reported-ink: #60a5fa;
|
|
69
|
+
--implemented-soft: #1a3a2a;
|
|
70
|
+
--implemented-ink: #4ade80;
|
|
71
|
+
--blocked-soft: #2d2410;
|
|
72
|
+
--blocked-ink: #fbbf24;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
* {
|
|
76
|
+
box-sizing: border-box;
|
|
77
|
+
margin: 0;
|
|
78
|
+
padding: 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
body {
|
|
82
|
+
margin: 0;
|
|
83
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
|
84
|
+
background: var(--bg);
|
|
85
|
+
color: var(--ink);
|
|
86
|
+
font-size: 14px;
|
|
87
|
+
line-height: 1.5;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.page {
|
|
91
|
+
max-width: 1440px;
|
|
92
|
+
margin: 0 auto;
|
|
93
|
+
padding: 16px 16px 32px;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.hero {
|
|
97
|
+
display: flex;
|
|
98
|
+
justify-content: space-between;
|
|
99
|
+
gap: 16px;
|
|
100
|
+
align-items: flex-start;
|
|
101
|
+
padding: 16px;
|
|
102
|
+
border: 1px solid var(--line);
|
|
103
|
+
border-radius: 12px;
|
|
104
|
+
background: var(--hero-bg);
|
|
105
|
+
box-shadow: var(--shadow);
|
|
106
|
+
margin-bottom: 16px;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.eyebrow {
|
|
110
|
+
margin: 0 0 4px;
|
|
111
|
+
text-transform: uppercase;
|
|
112
|
+
letter-spacing: 0.08em;
|
|
113
|
+
font-size: 11px;
|
|
114
|
+
color: var(--accent);
|
|
115
|
+
font-weight: 600;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.hero h1 {
|
|
119
|
+
margin: 0;
|
|
120
|
+
font-size: 24px;
|
|
121
|
+
font-weight: 700;
|
|
122
|
+
line-height: 1.2;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.subtitle {
|
|
126
|
+
max-width: 760px;
|
|
127
|
+
margin: 8px 0 0;
|
|
128
|
+
color: var(--muted);
|
|
129
|
+
font-size: 13px;
|
|
130
|
+
line-height: 1.4;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.hero-actions {
|
|
134
|
+
min-width: 200px;
|
|
135
|
+
display: flex;
|
|
136
|
+
flex-direction: column;
|
|
137
|
+
gap: 8px;
|
|
138
|
+
align-items: flex-end;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.hero-controls {
|
|
142
|
+
display: flex;
|
|
143
|
+
gap: 8px;
|
|
144
|
+
align-items: center;
|
|
145
|
+
flex-wrap: wrap;
|
|
146
|
+
justify-content: flex-end;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
button {
|
|
150
|
+
appearance: none;
|
|
151
|
+
border: 0;
|
|
152
|
+
border-radius: 6px;
|
|
153
|
+
padding: 6px 12px;
|
|
154
|
+
font: inherit;
|
|
155
|
+
font-size: 13px;
|
|
156
|
+
background: var(--button-bg);
|
|
157
|
+
color: var(--button-ink);
|
|
158
|
+
cursor: pointer;
|
|
159
|
+
transition: background 140ms ease;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
button:hover {
|
|
163
|
+
background: var(--button-hover);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
button:active {
|
|
167
|
+
transform: translateY(1px);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
#theme-toggle {
|
|
171
|
+
background: var(--theme-toggle-bg);
|
|
172
|
+
color: var(--theme-toggle-ink);
|
|
173
|
+
border: 1px solid var(--theme-toggle-line);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
#theme-toggle:hover {
|
|
177
|
+
background: var(--theme-toggle-hover);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.meta {
|
|
181
|
+
text-align: right;
|
|
182
|
+
color: var(--muted);
|
|
183
|
+
font-size: 12px;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.overview {
|
|
187
|
+
margin-top: 16px;
|
|
188
|
+
display: grid;
|
|
189
|
+
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
|
190
|
+
gap: 8px;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.card {
|
|
194
|
+
padding: 10px 12px;
|
|
195
|
+
border-radius: 8px;
|
|
196
|
+
border: 1px solid var(--line);
|
|
197
|
+
background: var(--panel);
|
|
198
|
+
box-shadow: var(--shadow);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.stat-label {
|
|
202
|
+
color: var(--muted);
|
|
203
|
+
font-size: 11px;
|
|
204
|
+
text-transform: uppercase;
|
|
205
|
+
letter-spacing: 0.05em;
|
|
206
|
+
font-weight: 500;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.stat-value {
|
|
210
|
+
margin-top: 4px;
|
|
211
|
+
font-size: 24px;
|
|
212
|
+
font-weight: 700;
|
|
213
|
+
line-height: 1.2;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.profiles {
|
|
217
|
+
margin-top: 16px;
|
|
218
|
+
display: flex;
|
|
219
|
+
flex-direction: column;
|
|
220
|
+
gap: 16px;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.profile {
|
|
224
|
+
padding: 16px;
|
|
225
|
+
border-radius: 12px;
|
|
226
|
+
border: 1px solid var(--line);
|
|
227
|
+
background: var(--profile-bg);
|
|
228
|
+
box-shadow: var(--shadow);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.profile-header {
|
|
232
|
+
display: flex;
|
|
233
|
+
justify-content: space-between;
|
|
234
|
+
gap: 12px;
|
|
235
|
+
align-items: flex-start;
|
|
236
|
+
margin-bottom: 12px;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.profile-title {
|
|
240
|
+
display: flex;
|
|
241
|
+
flex-wrap: wrap;
|
|
242
|
+
gap: 8px;
|
|
243
|
+
align-items: center;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.profile-title h2 {
|
|
247
|
+
margin: 0;
|
|
248
|
+
font-size: 18px;
|
|
249
|
+
font-weight: 700;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.profile-subtitle {
|
|
253
|
+
margin-top: 4px;
|
|
254
|
+
color: var(--muted);
|
|
255
|
+
font-size: 12px;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.badge-row {
|
|
259
|
+
display: flex;
|
|
260
|
+
flex-wrap: wrap;
|
|
261
|
+
gap: 6px;
|
|
262
|
+
justify-content: flex-end;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.badge {
|
|
266
|
+
display: inline-flex;
|
|
267
|
+
align-items: center;
|
|
268
|
+
gap: 4px;
|
|
269
|
+
padding: 3px 8px;
|
|
270
|
+
border-radius: 4px;
|
|
271
|
+
font-size: 12px;
|
|
272
|
+
background: var(--panel-strong);
|
|
273
|
+
border: 1px solid var(--line);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.badge.good {
|
|
277
|
+
background: var(--accent-soft);
|
|
278
|
+
color: var(--accent);
|
|
279
|
+
border-color: transparent;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.badge.warn {
|
|
283
|
+
background: var(--warn-soft);
|
|
284
|
+
color: var(--warn);
|
|
285
|
+
border-color: transparent;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.badge.danger {
|
|
289
|
+
background: var(--danger-soft);
|
|
290
|
+
color: var(--danger);
|
|
291
|
+
border-color: transparent;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.profile-grid {
|
|
295
|
+
margin-top: 12px;
|
|
296
|
+
display: grid;
|
|
297
|
+
grid-template-columns: repeat(12, 1fr);
|
|
298
|
+
gap: 8px;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.panel {
|
|
302
|
+
grid-column: span 12;
|
|
303
|
+
padding: 10px 12px;
|
|
304
|
+
border-radius: 8px;
|
|
305
|
+
border: 1px solid var(--line);
|
|
306
|
+
background: var(--panel);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.panel.half {
|
|
310
|
+
grid-column: span 6;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.panel.third {
|
|
314
|
+
grid-column: span 4;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.panel h3 {
|
|
318
|
+
margin: 0 0 8px;
|
|
319
|
+
font-size: 14px;
|
|
320
|
+
font-weight: 600;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.panel-subtitle {
|
|
324
|
+
margin: -4px 0 8px;
|
|
325
|
+
color: var(--muted);
|
|
326
|
+
font-size: 12px;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.alert-list {
|
|
330
|
+
display: grid;
|
|
331
|
+
gap: 8px;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.alert-card {
|
|
335
|
+
padding: 10px 12px;
|
|
336
|
+
border-radius: 8px;
|
|
337
|
+
border: 1px solid var(--line);
|
|
338
|
+
background: var(--panel-strong);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.alert-card.warn {
|
|
342
|
+
border-color: var(--warn);
|
|
343
|
+
background: var(--warn-soft);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.alert-card h4 {
|
|
347
|
+
margin: 0;
|
|
348
|
+
font-size: 14px;
|
|
349
|
+
font-weight: 600;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.alert-card p {
|
|
353
|
+
margin: 6px 0 0;
|
|
354
|
+
font-size: 13px;
|
|
355
|
+
line-height: 1.4;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.alert-header {
|
|
359
|
+
display: flex;
|
|
360
|
+
gap: 8px;
|
|
361
|
+
justify-content: space-between;
|
|
362
|
+
align-items: flex-start;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.alert-meta {
|
|
366
|
+
margin-top: 6px;
|
|
367
|
+
display: flex;
|
|
368
|
+
flex-wrap: wrap;
|
|
369
|
+
gap: 8px;
|
|
370
|
+
color: var(--muted);
|
|
371
|
+
font-size: 11px;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.table-wrap {
|
|
375
|
+
overflow-x: auto;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
table {
|
|
379
|
+
width: 100%;
|
|
380
|
+
border-collapse: collapse;
|
|
381
|
+
font-size: 13px;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
th,
|
|
385
|
+
td {
|
|
386
|
+
padding: 6px 8px;
|
|
387
|
+
text-align: left;
|
|
388
|
+
vertical-align: top;
|
|
389
|
+
border-top: 1px solid var(--line);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
th {
|
|
393
|
+
color: var(--muted);
|
|
394
|
+
font-size: 11px;
|
|
395
|
+
text-transform: uppercase;
|
|
396
|
+
letter-spacing: 0.05em;
|
|
397
|
+
font-weight: 600;
|
|
398
|
+
border-top: 0;
|
|
399
|
+
background: var(--panel-strong);
|
|
400
|
+
position: sticky;
|
|
401
|
+
top: 0;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.mono {
|
|
405
|
+
font-family: 'SF Mono', ui-monospace, Menlo, Consolas, monospace;
|
|
406
|
+
font-size: 12px;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
.muted {
|
|
410
|
+
color: var(--muted);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.status-pill {
|
|
414
|
+
display: inline-flex;
|
|
415
|
+
align-items: center;
|
|
416
|
+
padding: 2px 6px;
|
|
417
|
+
border-radius: 4px;
|
|
418
|
+
font-size: 11px;
|
|
419
|
+
font-weight: 600;
|
|
420
|
+
background: var(--panel-strong);
|
|
421
|
+
border: 1px solid var(--line);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
.status-pill.RUNNING,
|
|
425
|
+
.status-pill.waiting-worker,
|
|
426
|
+
.status-pill.launching,
|
|
427
|
+
.status-pill.reconciling {
|
|
428
|
+
background: var(--accent-soft);
|
|
429
|
+
color: var(--accent);
|
|
430
|
+
border-color: transparent;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
.status-pill.waiting-provider,
|
|
434
|
+
.status-pill.waiting-due,
|
|
435
|
+
.status-pill.waiting-open-pr,
|
|
436
|
+
.status-pill.idle,
|
|
437
|
+
.status-pill.sleeping {
|
|
438
|
+
background: var(--warn-soft);
|
|
439
|
+
color: var(--warn);
|
|
440
|
+
border-color: transparent;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
.status-pill.FAILED,
|
|
444
|
+
.status-pill.stopped,
|
|
445
|
+
.status-pill.launch-failed {
|
|
446
|
+
background: var(--danger-soft);
|
|
447
|
+
color: var(--danger);
|
|
448
|
+
border-color: transparent;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.status-pill.implemented {
|
|
452
|
+
background: var(--implemented-soft);
|
|
453
|
+
color: var(--implemented-ink);
|
|
454
|
+
border-color: transparent;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
.status-pill.reported,
|
|
458
|
+
.status-pill.completed {
|
|
459
|
+
background: var(--reported-soft);
|
|
460
|
+
color: var(--reported-ink);
|
|
461
|
+
border-color: transparent;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
.status-pill.blocked {
|
|
465
|
+
background: var(--blocked-soft);
|
|
466
|
+
color: var(--blocked-ink);
|
|
467
|
+
border-color: transparent;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.status-pill.failed,
|
|
471
|
+
.status-pill.unknown {
|
|
472
|
+
background: var(--danger-soft);
|
|
473
|
+
color: var(--danger);
|
|
474
|
+
border-color: transparent;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
.status-pill.running {
|
|
478
|
+
background: var(--accent-soft);
|
|
479
|
+
color: var(--accent);
|
|
480
|
+
border-color: transparent;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.empty-state {
|
|
484
|
+
color: var(--muted);
|
|
485
|
+
padding: 8px 4px;
|
|
486
|
+
font-size: 13px;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/* Filter bar */
|
|
490
|
+
.filter-bar {
|
|
491
|
+
display: flex;
|
|
492
|
+
gap: 6px;
|
|
493
|
+
margin-bottom: 8px;
|
|
494
|
+
flex-wrap: wrap;
|
|
495
|
+
align-items: center;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
.filter-search {
|
|
499
|
+
flex: 1;
|
|
500
|
+
min-width: 180px;
|
|
501
|
+
padding: 4px 8px;
|
|
502
|
+
border: 1px solid var(--line);
|
|
503
|
+
border-radius: 4px;
|
|
504
|
+
background: var(--panel);
|
|
505
|
+
color: var(--ink);
|
|
506
|
+
font-size: 12px;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
.filter-search:focus {
|
|
510
|
+
outline: none;
|
|
511
|
+
border-color: var(--accent);
|
|
512
|
+
box-shadow: 0 0 0 2px var(--accent-soft);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
.filter-btn {
|
|
516
|
+
padding: 4px 10px;
|
|
517
|
+
font-size: 12px;
|
|
518
|
+
border-radius: 4px;
|
|
519
|
+
background: var(--panel);
|
|
520
|
+
color: var(--ink);
|
|
521
|
+
border: 1px solid var(--line);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.filter-btn:hover {
|
|
525
|
+
background: var(--panel-strong);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
.filter-btn.active {
|
|
529
|
+
background: var(--accent);
|
|
530
|
+
color: var(--button-ink);
|
|
531
|
+
border-color: transparent;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/* Pagination */
|
|
535
|
+
.pagination {
|
|
536
|
+
display: flex;
|
|
537
|
+
gap: 4px;
|
|
538
|
+
align-items: center;
|
|
539
|
+
justify-content: space-between;
|
|
540
|
+
margin-top: 8px;
|
|
541
|
+
font-size: 12px;
|
|
542
|
+
color: var(--muted);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
.pagination-info {
|
|
546
|
+
font-size: 12px;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
.pagination-controls {
|
|
550
|
+
display: flex;
|
|
551
|
+
gap: 2px;
|
|
552
|
+
align-items: center;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
.pagination-controls button {
|
|
556
|
+
padding: 4px 8px;
|
|
557
|
+
font-size: 12px;
|
|
558
|
+
min-width: 28px;
|
|
559
|
+
text-align: center;
|
|
560
|
+
background: var(--panel);
|
|
561
|
+
color: var(--ink);
|
|
562
|
+
border: 1px solid var(--line);
|
|
563
|
+
border-radius: 4px;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
.pagination-controls button:hover {
|
|
567
|
+
background: var(--panel-strong);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
.pagination-controls button.active {
|
|
571
|
+
background: var(--accent);
|
|
572
|
+
color: var(--button-ink);
|
|
573
|
+
border-color: transparent;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.pagination-controls button:disabled {
|
|
577
|
+
opacity: 0.4;
|
|
578
|
+
cursor: not-allowed;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
.pagination-ellipsis {
|
|
582
|
+
padding: 4px 4px;
|
|
583
|
+
color: var(--muted);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/* Icon-only action buttons */
|
|
587
|
+
.action-btn {
|
|
588
|
+
padding: 4px 6px;
|
|
589
|
+
font-size: 14px;
|
|
590
|
+
line-height: 1;
|
|
591
|
+
background: transparent;
|
|
592
|
+
color: var(--muted);
|
|
593
|
+
border: 1px solid transparent;
|
|
594
|
+
border-radius: 4px;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
.action-btn:hover {
|
|
598
|
+
background: var(--panel-strong);
|
|
599
|
+
color: var(--ink);
|
|
600
|
+
border-color: var(--line);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
@media (max-width: 960px) {
|
|
604
|
+
.hero,
|
|
605
|
+
.profile-header {
|
|
606
|
+
flex-direction: column;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
.hero-actions,
|
|
610
|
+
.badge-row {
|
|
611
|
+
align-items: flex-start;
|
|
612
|
+
justify-content: flex-start;
|
|
613
|
+
text-align: left;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
.hero-controls {
|
|
617
|
+
justify-content: flex-start;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
.panel.half,
|
|
621
|
+
.panel.third {
|
|
622
|
+
grid-column: span 12;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
</style>
|
|
627
|
+
</head>
|
|
628
|
+
<body>
|
|
629
|
+
<main class="page">
|
|
630
|
+
<header class="hero">
|
|
631
|
+
<div>
|
|
632
|
+
<p class="eyebrow">Agent Control Plane</p>
|
|
633
|
+
<h1>Worker Dashboard</h1>
|
|
634
|
+
<p class="subtitle">
|
|
635
|
+
Track active runs, resident controllers, queue pressure, and provider failover in one place.
|
|
636
|
+
</p>
|
|
637
|
+
<p class="subtitle">
|
|
638
|
+
Lifecycle shows whether a worker session finished cleanly. Result shows whether that cycle implemented work, reported findings, or stopped blocked.
|
|
639
|
+
</p>
|
|
640
|
+
</div>
|
|
641
|
+
<div class="hero-actions">
|
|
642
|
+
<div class="hero-controls">
|
|
643
|
+
<button id="theme-toggle" type="button" aria-label="Toggle dark mode">Dark mode</button>
|
|
644
|
+
<button id="refresh-button" type="button">Refresh now</button>
|
|
645
|
+
</div>
|
|
646
|
+
<div class="meta">
|
|
647
|
+
<div>Auto refresh: <strong>5s</strong></div>
|
|
648
|
+
<div id="generated-at">Loading snapshot...</div>
|
|
649
|
+
</div>
|
|
650
|
+
</div>
|
|
651
|
+
</header>
|
|
652
|
+
|
|
653
|
+
<section id="overview" class="overview"></section>
|
|
654
|
+
<section id="scheduler-status" class="scheduler-status"></section>
|
|
655
|
+
<section id="profiles" class="profiles"></section>
|
|
656
|
+
</main>
|
|
657
|
+
|
|
658
|
+
<template id="empty-table-template">
|
|
659
|
+
<div class="empty-state">No data right now.</div>
|
|
660
|
+
</template>
|
|
661
|
+
|
|
662
|
+
<script>
|
|
663
|
+
const refreshButton = document.querySelector("#refresh-button");
|
|
664
|
+
const themeToggleButton = document.querySelector("#theme-toggle");
|
|
665
|
+
const generatedAtNode = document.querySelector("#generated-at");
|
|
666
|
+
const overviewNode = document.querySelector("#overview");
|
|
667
|
+
const profilesNode = document.querySelector("#profiles");
|
|
668
|
+
const seenAlertIds = new Set();
|
|
669
|
+
let notificationPermissionRequested = false;
|
|
670
|
+
const THEME_STORAGE_KEY = "acp-dashboard-theme";
|
|
671
|
+
const ROWS_PER_PAGE = 10;
|
|
672
|
+
|
|
673
|
+
// Pagination state: { [tableId]: { page: number } }
|
|
674
|
+
window._acpPagination = {};
|
|
675
|
+
|
|
676
|
+
function systemPrefersDark() {
|
|
677
|
+
return typeof window.matchMedia === "function" && window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function currentThemePreference() {
|
|
681
|
+
try {
|
|
682
|
+
const stored = window.localStorage.getItem(THEME_STORAGE_KEY);
|
|
683
|
+
if (stored === "light" || stored === "dark") return stored;
|
|
684
|
+
} catch (_error) {
|
|
685
|
+
// Ignore storage access issues and fall back to system preference.
|
|
686
|
+
}
|
|
687
|
+
return systemPrefersDark() ? "dark" : "light";
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function updateThemeToggleLabel(theme) {
|
|
691
|
+
if (!themeToggleButton) return;
|
|
692
|
+
const nextTheme = theme === "dark" ? "light" : "dark";
|
|
693
|
+
const label = nextTheme === "dark" ? "Dark mode" : "Light mode";
|
|
694
|
+
themeToggleButton.textContent = label;
|
|
695
|
+
themeToggleButton.setAttribute("aria-label", `Switch to ${label.toLowerCase()}`);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function applyTheme(theme) {
|
|
699
|
+
document.documentElement.dataset.theme = theme;
|
|
700
|
+
updateThemeToggleLabel(theme);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function persistTheme(theme) {
|
|
704
|
+
try {
|
|
705
|
+
window.localStorage.setItem(THEME_STORAGE_KEY, theme);
|
|
706
|
+
} catch (_error) {
|
|
707
|
+
// Ignore storage access issues.
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function initializeTheme() {
|
|
712
|
+
applyTheme(currentThemePreference());
|
|
713
|
+
if (!themeToggleButton) return;
|
|
714
|
+
themeToggleButton.addEventListener("click", () => {
|
|
715
|
+
const nextTheme = document.documentElement.dataset.theme === "dark" ? "light" : "dark";
|
|
716
|
+
applyTheme(nextTheme);
|
|
717
|
+
persistTheme(nextTheme);
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function relativeTime(input) {
|
|
722
|
+
if (!input) return "n/a";
|
|
723
|
+
const value = new Date(input);
|
|
724
|
+
if (Number.isNaN(value.getTime())) return input;
|
|
725
|
+
const seconds = Math.round((Date.now() - value.getTime()) / 1000);
|
|
726
|
+
const absolute = Math.abs(seconds);
|
|
727
|
+
const parts = [
|
|
728
|
+
[86400, "d"],
|
|
729
|
+
[3600, "h"],
|
|
730
|
+
[60, "m"],
|
|
731
|
+
];
|
|
732
|
+
for (const [unitSeconds, label] of parts) {
|
|
733
|
+
if (absolute >= unitSeconds) {
|
|
734
|
+
const amount = Math.round(absolute / unitSeconds);
|
|
735
|
+
return seconds >= 0 ? `${amount}${label} ago` : `in ${amount}${label}`;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return seconds >= 0 ? `${absolute}s ago` : `in ${absolute}s`;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function formatCompactDate(input) {
|
|
742
|
+
if (!input) return "n/a";
|
|
743
|
+
const d = new Date(input);
|
|
744
|
+
if (Number.isNaN(d.getTime())) return input;
|
|
745
|
+
const months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
|
|
746
|
+
return `${months[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function formatDuration(seconds) {
|
|
750
|
+
if (!seconds && seconds !== 0) return "n/a";
|
|
751
|
+
const absSeconds = Math.abs(seconds);
|
|
752
|
+
const parts = [];
|
|
753
|
+
const units = [
|
|
754
|
+
[86400, "d"],
|
|
755
|
+
[3600, "h"],
|
|
756
|
+
[60, "m"],
|
|
757
|
+
[1, "s"],
|
|
758
|
+
];
|
|
759
|
+
for (const [unitSeconds, label] of units) {
|
|
760
|
+
if (absSeconds >= unitSeconds) {
|
|
761
|
+
const amount = Math.floor(absSeconds / unitSeconds);
|
|
762
|
+
parts.push(`${amount}${label}`);
|
|
763
|
+
seconds -= amount * unitSeconds;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
return parts.slice(0, 2).join(" ") || "0s";
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function timeRemaining(isoString) {
|
|
770
|
+
if (!isoString) return "n/a";
|
|
771
|
+
const next = new Date(isoString);
|
|
772
|
+
if (Number.isNaN(next.getTime())) return isoString;
|
|
773
|
+
const diffSeconds = Math.round((next.getTime() - Date.now()) / 1000);
|
|
774
|
+
if (diffSeconds <= 0) return "ready now";
|
|
775
|
+
return formatDuration(diffSeconds);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function statusClass(status) {
|
|
779
|
+
if (!status) return "";
|
|
780
|
+
return status.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function renderLifecycle(row) {
|
|
784
|
+
const note = row.result_only_completion === "yes" ? `<div class="muted">Recovered</div>` : "";
|
|
785
|
+
return `<span class="status-pill ${statusClass(row.lifecycle_status || row.status)}">${row.lifecycle_status || row.status || "UNKNOWN"}</span>${note}`;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function renderResult(row) {
|
|
789
|
+
const primary = row.result_label || row.outcome || row.failure_reason || "n/a";
|
|
790
|
+
const secondary = [];
|
|
791
|
+
if (row.outcome && primary !== row.outcome) secondary.push(row.outcome);
|
|
792
|
+
if (row.action) secondary.push(row.action);
|
|
793
|
+
return `<span class="status-pill ${statusClass(row.result_kind || "unknown")}">${primary}</span>${
|
|
794
|
+
secondary.length ? `<div class="muted">${secondary.join(" · ")}</div>` : ""
|
|
795
|
+
}`;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function renderControllerState(row) {
|
|
799
|
+
const state = row.state || "n/a";
|
|
800
|
+
const stale = row.controller_stale === true || (state !== "stopped" && row.controller_live === false);
|
|
801
|
+
const label = stale ? `${state} (stale)` : state;
|
|
802
|
+
return `<span class="status-pill ${statusClass(stale ? "stale" : state)}">${label}</span>`;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function renderOverview(snapshot) {
|
|
806
|
+
const totals = snapshot.profiles.reduce(
|
|
807
|
+
(acc, profile) => {
|
|
808
|
+
acc.activeRuns += profile.counts.active_runs;
|
|
809
|
+
acc.runningRuns += profile.counts.running_runs;
|
|
810
|
+
acc.implementedRuns += profile.counts.implemented_runs;
|
|
811
|
+
acc.reportedRuns += profile.counts.reported_runs;
|
|
812
|
+
acc.blockedRuns += profile.counts.blocked_runs;
|
|
813
|
+
acc.controllers += profile.counts.live_resident_controllers;
|
|
814
|
+
acc.cooldowns += profile.counts.provider_cooldowns;
|
|
815
|
+
acc.queue += profile.counts.queued_issues;
|
|
816
|
+
acc.alerts += profile.counts.alerts || 0;
|
|
817
|
+
acc.pendingGithubWrites += profile.counts.pending_github_writes || 0;
|
|
818
|
+
return acc;
|
|
819
|
+
},
|
|
820
|
+
{ activeRuns: 0, runningRuns: 0, implementedRuns: 0, reportedRuns: 0, blockedRuns: 0, controllers: 0, cooldowns: 0, queue: 0, alerts: 0, pendingGithubWrites: 0 },
|
|
821
|
+
);
|
|
822
|
+
|
|
823
|
+
overviewNode.innerHTML = [
|
|
824
|
+
["Profiles", snapshot.profile_count],
|
|
825
|
+
["Run sessions", totals.activeRuns],
|
|
826
|
+
["Running", totals.runningRuns],
|
|
827
|
+
["Implemented", totals.implementedRuns],
|
|
828
|
+
["Reported", totals.reportedRuns],
|
|
829
|
+
["Blocked", totals.blockedRuns],
|
|
830
|
+
["Live Controllers", totals.controllers],
|
|
831
|
+
["Provider Cooldowns", totals.cooldowns],
|
|
832
|
+
["Pending GitHub Writes", totals.pendingGithubWrites],
|
|
833
|
+
["Alerts", totals.alerts],
|
|
834
|
+
["Queued Issues", totals.queue],
|
|
835
|
+
["Retries", totals.retries || 0],
|
|
836
|
+
["Blockers", totals.blockers || 0],
|
|
837
|
+
]
|
|
838
|
+
.map(
|
|
839
|
+
([label, value]) => `
|
|
840
|
+
<article class="card">
|
|
841
|
+
<div class="stat-label">${label}</div>
|
|
842
|
+
<div class="stat-value">${value}</div>
|
|
843
|
+
</article>
|
|
844
|
+
`,
|
|
845
|
+
)
|
|
846
|
+
.join("");
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Build windowed pagination: max 5 page buttons with ellipsis
|
|
850
|
+
function buildWindowedPages(current, total) {
|
|
851
|
+
const pages = [];
|
|
852
|
+
const maxVisible = 5;
|
|
853
|
+
if (total <= maxVisible) {
|
|
854
|
+
for (let i = 1; i <= total; i++) pages.push(i);
|
|
855
|
+
return pages;
|
|
856
|
+
}
|
|
857
|
+
// Always show first, last, and surrounding pages
|
|
858
|
+
const pagesSet = new Set();
|
|
859
|
+
pagesSet.add(1);
|
|
860
|
+
pagesSet.add(total);
|
|
861
|
+
for (let i = Math.max(1, current - 1); i <= Math.min(total, current + 1); i++) {
|
|
862
|
+
pagesSet.add(i);
|
|
863
|
+
}
|
|
864
|
+
const sorted = Array.from(pagesSet).sort((a, b) => a - b);
|
|
865
|
+
// Add ellipsis markers
|
|
866
|
+
const result = [];
|
|
867
|
+
let prev = 0;
|
|
868
|
+
for (const p of sorted) {
|
|
869
|
+
if (p - prev > 1) result.push("...");
|
|
870
|
+
result.push(p);
|
|
871
|
+
prev = p;
|
|
872
|
+
}
|
|
873
|
+
return result;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function renderPagination(tableId, currentPage, totalPages, totalRows) {
|
|
877
|
+
if (totalPages <= 1) return "";
|
|
878
|
+
const start = (currentPage - 1) * ROWS_PER_PAGE + 1;
|
|
879
|
+
const end = Math.min(currentPage * ROWS_PER_PAGE, totalRows);
|
|
880
|
+
const pages = buildWindowedPages(currentPage, totalPages);
|
|
881
|
+
const buttons = pages
|
|
882
|
+
.map((p) => {
|
|
883
|
+
if (p === "...") return `<span class="pagination-ellipsis">…</span>`;
|
|
884
|
+
return `<button class="${p === currentPage ? "active" : ""}" onclick="window._acpGoToPage('${tableId}',${p})">${p}</button>`;
|
|
885
|
+
})
|
|
886
|
+
.join("");
|
|
887
|
+
return `
|
|
888
|
+
<div class="pagination">
|
|
889
|
+
<span class="pagination-info">Showing ${start}-${end} of ${totalRows}</span>
|
|
890
|
+
<div class="pagination-controls">
|
|
891
|
+
<button ${currentPage <= 1 ? "disabled" : ""} onclick="window._acpGoToPage('${tableId}',${currentPage - 1})">‹</button>
|
|
892
|
+
${buttons}
|
|
893
|
+
<button ${currentPage >= totalPages ? "disabled" : ""} onclick="window._acpGoToPage('${tableId}',${currentPage + 1})">›</button>
|
|
894
|
+
</div>
|
|
895
|
+
</div>`;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
window._acpGoToPage = function(tableId, page) {
|
|
899
|
+
window._acpPagination[tableId] = { page };
|
|
900
|
+
rerenderAll();
|
|
901
|
+
};
|
|
902
|
+
|
|
903
|
+
function renderTableWithPagination(tableId, columns, rows, emptyMessage = "No data right now.") {
|
|
904
|
+
if (!rows.length) {
|
|
905
|
+
return `<div class="empty-state">${emptyMessage}</div>`;
|
|
906
|
+
}
|
|
907
|
+
const state = window._acpPagination[tableId] || { page: 1 };
|
|
908
|
+
let { page } = state;
|
|
909
|
+
const totalPages = Math.ceil(rows.length / ROWS_PER_PAGE);
|
|
910
|
+
if (page < 1) page = 1;
|
|
911
|
+
if (page > totalPages) page = totalPages;
|
|
912
|
+
window._acpPagination[tableId] = { page };
|
|
913
|
+
const start = (page - 1) * ROWS_PER_PAGE;
|
|
914
|
+
const pageRows = rows.slice(start, start + ROWS_PER_PAGE);
|
|
915
|
+
const headers = columns.map((column) => `<th>${column.label}</th>`).join("");
|
|
916
|
+
const body = pageRows
|
|
917
|
+
.map((row) => {
|
|
918
|
+
const cells = columns
|
|
919
|
+
.map((column) => `<td>${column.render ? column.render(row) : row[column.key] ?? ""}</td>`)
|
|
920
|
+
.join("");
|
|
921
|
+
return `<tr>${cells}</tr>`;
|
|
922
|
+
})
|
|
923
|
+
.join("");
|
|
924
|
+
const paginationHtml = renderPagination(tableId, page, totalPages, rows.length);
|
|
925
|
+
return `<div class="table-wrap"><table><thead><tr>${headers}</tr></thead><tbody>${body}</tbody></table></div>${paginationHtml}`;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function renderAlerts(alerts) {
|
|
929
|
+
if (!alerts.length) {
|
|
930
|
+
return `<div class="empty-state">No active alerts for this profile.</div>`;
|
|
931
|
+
}
|
|
932
|
+
return `
|
|
933
|
+
<div class="alert-list">
|
|
934
|
+
${alerts
|
|
935
|
+
.map(
|
|
936
|
+
(alert) => `
|
|
937
|
+
<article class="alert-card ${statusClass(alert.severity || "warn")}">
|
|
938
|
+
<div class="alert-header">
|
|
939
|
+
<div>
|
|
940
|
+
<h4>${alert.title}</h4>
|
|
941
|
+
<div class="muted mono">${alert.session || "n/a"} · ${alert.task_kind || "task"} ${alert.task_id || ""}</div>
|
|
942
|
+
</div>
|
|
943
|
+
<span class="badge warn">${alert.kind}</span>
|
|
944
|
+
</div>
|
|
945
|
+
<p>${alert.message}</p>
|
|
946
|
+
<div class="alert-meta">
|
|
947
|
+
<span>${alert.reset_at ? `Reset: ${formatCompactDate(alert.reset_at)}` : "Reset: n/a"}</span>
|
|
948
|
+
<span>${alert.updated_at ? `${relativeTime(alert.updated_at)} · ${formatCompactDate(alert.updated_at)}` : "updated n/a"}</span>
|
|
949
|
+
</div>
|
|
950
|
+
</article>
|
|
951
|
+
`,
|
|
952
|
+
)
|
|
953
|
+
.join("")}
|
|
954
|
+
</div>
|
|
955
|
+
`;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function renderCodexRotation(rotation) {
|
|
959
|
+
if (!rotation || !rotation.active_label) {
|
|
960
|
+
return `<div class="empty-state">Codex rotation data is not available yet for this Codex profile.</div>`;
|
|
961
|
+
}
|
|
962
|
+
const candidates = (rotation.candidate_labels || []).length ? rotation.candidate_labels.join(", ") : "n/a";
|
|
963
|
+
const ready = (rotation.ready_candidates || []).length ? rotation.ready_candidates.join(", ") : "none";
|
|
964
|
+
const nextRetry = rotation.next_retry_at
|
|
965
|
+
? `${rotation.next_retry_label || "n/a"} · ${relativeTime(rotation.next_retry_at)}<div class="muted">${formatCompactDate(rotation.next_retry_at)}</div>`
|
|
966
|
+
: "n/a";
|
|
967
|
+
const lastSwitch = rotation.last_switch_label
|
|
968
|
+
? `${rotation.last_switch_label}${rotation.last_switch_reason ? ` · ${rotation.last_switch_reason}` : ""}`
|
|
969
|
+
: "n/a";
|
|
970
|
+
|
|
971
|
+
return renderTableWithPagination(
|
|
972
|
+
"codex-rotation",
|
|
973
|
+
[
|
|
974
|
+
{ label: "Current", render: () => `<div class="mono">${rotation.active_label}</div>` },
|
|
975
|
+
{ label: "Decision", render: () => `<span class="status-pill ${statusClass(rotation.switch_decision || "unknown")}">${rotation.switch_decision || "unknown"}</span>` },
|
|
976
|
+
{ label: "Candidates", render: () => `<div class="mono">${candidates}</div>` },
|
|
977
|
+
{ label: "Ready now", render: () => `<div class="mono">${ready}</div>` },
|
|
978
|
+
{ label: "Next retry", render: () => nextRetry },
|
|
979
|
+
{ label: "Last switch", render: () => `<div class="mono">${lastSwitch}</div>` },
|
|
980
|
+
],
|
|
981
|
+
[{}],
|
|
982
|
+
"No Codex rotation data for this profile.",
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function renderProfile(profile) {
|
|
987
|
+
const providerBadges = [
|
|
988
|
+
profile.coding_worker ? `<span class="badge good">${profile.coding_worker}</span>` : "",
|
|
989
|
+
profile.provider_pool.backend
|
|
990
|
+
? `<span class="badge">${profile.provider_pool.backend}: ${profile.provider_pool.model || "n/a"}</span>`
|
|
991
|
+
: "",
|
|
992
|
+
profile.provider_pool.name ? `<span class="badge">${profile.provider_pool.name}</span>` : "",
|
|
993
|
+
profile.provider_pool.pools_exhausted
|
|
994
|
+
? `<span class="badge warn">pools exhausted</span>`
|
|
995
|
+
: "",
|
|
996
|
+
profile.provider_pool.last_reason ? `<span class="badge warn">${profile.provider_pool.last_reason}</span>` : "",
|
|
997
|
+
]
|
|
998
|
+
.filter(Boolean)
|
|
999
|
+
.join("");
|
|
1000
|
+
|
|
1001
|
+
const summaryCards = [
|
|
1002
|
+
["Run sessions", profile.counts.active_runs],
|
|
1003
|
+
["Running", profile.counts.running_runs],
|
|
1004
|
+
["Recent completed", profile.counts.recent_history_runs || 0],
|
|
1005
|
+
["Implemented", profile.counts.implemented_runs],
|
|
1006
|
+
["Reported", profile.counts.reported_runs],
|
|
1007
|
+
["Blocked", profile.counts.blocked_runs],
|
|
1008
|
+
["Live controllers", profile.counts.live_resident_controllers],
|
|
1009
|
+
["Stale controllers", profile.counts.stale_resident_controllers],
|
|
1010
|
+
["Provider cooldowns", profile.counts.provider_cooldowns],
|
|
1011
|
+
["Pending GitHub writes", profile.counts.pending_github_writes || 0],
|
|
1012
|
+
["Failed GitHub writes", profile.counts.failed_github_writes || 0],
|
|
1013
|
+
["Alerts", profile.counts.alerts || 0],
|
|
1014
|
+
["Issue retries", profile.counts.active_retries],
|
|
1015
|
+
["Queued issues", profile.counts.queued_issues],
|
|
1016
|
+
["Scheduled", profile.counts.scheduled_issues],
|
|
1017
|
+
]
|
|
1018
|
+
.map(
|
|
1019
|
+
([label, value]) => `
|
|
1020
|
+
<article class="card">
|
|
1021
|
+
<div class="stat-label">${label}</div>
|
|
1022
|
+
<div class="stat-value">${value}</div>
|
|
1023
|
+
</article>
|
|
1024
|
+
`,
|
|
1025
|
+
)
|
|
1026
|
+
.join("");
|
|
1027
|
+
|
|
1028
|
+
const runsFilterState = window._acpRunsFilter || { search: "", status: "all" };
|
|
1029
|
+
window._acpRunsFilter = runsFilterState;
|
|
1030
|
+
|
|
1031
|
+
const filteredRuns = profile.runs.filter((row) => {
|
|
1032
|
+
if (runsFilterState.status !== "all" && row.status !== runsFilterState.status) return false;
|
|
1033
|
+
if (runsFilterState.search) {
|
|
1034
|
+
const q = runsFilterState.search.toLowerCase();
|
|
1035
|
+
return (
|
|
1036
|
+
(row.session || "").toLowerCase().includes(q) ||
|
|
1037
|
+
(row.coding_worker || "").toLowerCase().includes(q) ||
|
|
1038
|
+
(row.task_kind || "").toLowerCase().includes(q) ||
|
|
1039
|
+
(row.task_id || "").toLowerCase().includes(q)
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
return true;
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
const runsTable = renderTableWithPagination(
|
|
1046
|
+
`runs-${profile.id}`,
|
|
1047
|
+
[
|
|
1048
|
+
{ label: "Session", render: (row) => `<div class="mono">${row.session}</div>` },
|
|
1049
|
+
{ label: "Task", render: (row) => `${row.task_kind || "n/a"} ${row.task_id || ""}`.trim() },
|
|
1050
|
+
{ label: "Lifecycle", render: renderLifecycle },
|
|
1051
|
+
{ label: "Worker", key: "coding_worker" },
|
|
1052
|
+
{ label: "Provider", render: (row) => row.provider_model || "n/a" },
|
|
1053
|
+
{ label: "Result", render: renderResult },
|
|
1054
|
+
{ label: "Updated", render: (row) => row.updated_at ? `${relativeTime(row.updated_at)}<div class="muted">${formatCompactDate(row.updated_at)}</div>` : "n/a" },
|
|
1055
|
+
],
|
|
1056
|
+
filteredRuns,
|
|
1057
|
+
"No active run directories for this profile.",
|
|
1058
|
+
);
|
|
1059
|
+
|
|
1060
|
+
const historyFilterState = window._acpHistoryFilter || { search: "", result: "all" };
|
|
1061
|
+
window._acpHistoryFilter = historyFilterState;
|
|
1062
|
+
|
|
1063
|
+
const filteredHistory = (profile.recent_history || []).filter((row) => {
|
|
1064
|
+
if (historyFilterState.result !== "all" && row.result_kind !== historyFilterState.result) return false;
|
|
1065
|
+
if (historyFilterState.search) {
|
|
1066
|
+
const q = historyFilterState.search.toLowerCase();
|
|
1067
|
+
return (
|
|
1068
|
+
(row.session || "").toLowerCase().includes(q) ||
|
|
1069
|
+
(row.coding_worker || "").toLowerCase().includes(q) ||
|
|
1070
|
+
(row.task_kind || "").toLowerCase().includes(q)
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
return true;
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
const recentHistoryTable = renderTableWithPagination(
|
|
1077
|
+
`history-${profile.id}`,
|
|
1078
|
+
[
|
|
1079
|
+
{ label: "Session", render: (row) => `<div class="mono">${row.session}</div>` },
|
|
1080
|
+
{ label: "Task", render: (row) => `${row.task_kind || "n/a"} ${row.task_id || ""}`.trim() },
|
|
1081
|
+
{ label: "Lifecycle", render: renderLifecycle },
|
|
1082
|
+
{ label: "Worker", key: "coding_worker" },
|
|
1083
|
+
{ label: "Result", render: renderResult },
|
|
1084
|
+
{ label: "Updated", render: (row) => row.updated_at ? `${relativeTime(row.updated_at)}<div class="muted">${formatCompactDate(row.updated_at)}</div>` : "n/a" },
|
|
1085
|
+
],
|
|
1086
|
+
filteredHistory,
|
|
1087
|
+
"No recently archived runs.",
|
|
1088
|
+
);
|
|
1089
|
+
|
|
1090
|
+
const controllerTable = renderTableWithPagination(
|
|
1091
|
+
`controllers-${profile.id}`,
|
|
1092
|
+
[
|
|
1093
|
+
{ label: "Issue", key: "issue_id" },
|
|
1094
|
+
{ label: "State", render: renderControllerState },
|
|
1095
|
+
{ label: "Lane", render: (row) => `${row.lane_kind || "n/a"} / ${row.lane_value || "n/a"}` },
|
|
1096
|
+
{ label: "Reason", render: (row) => row.reason || "n/a" },
|
|
1097
|
+
{ label: "Provider", render: (row) => `${row.provider_backend || "n/a"} ${row.provider_model || ""}`.trim() },
|
|
1098
|
+
{ label: "Failover", render: (row) => `${row.provider_failover_count} failovers / ${row.provider_switch_count} switches` },
|
|
1099
|
+
{ label: "Wait", render: (row) => `${row.provider_wait_count} waits / ${row.provider_wait_total_seconds}s` },
|
|
1100
|
+
{ label: "Updated", render: (row) => row.updated_at ? `${relativeTime(row.updated_at)}<div class="muted">${formatCompactDate(row.updated_at)}</div>` : "n/a" },
|
|
1101
|
+
],
|
|
1102
|
+
profile.resident_controllers,
|
|
1103
|
+
"No resident controllers recorded for this profile.",
|
|
1104
|
+
);
|
|
1105
|
+
|
|
1106
|
+
const retryTable = renderTableWithPagination(
|
|
1107
|
+
`retries-${profile.id}`,
|
|
1108
|
+
[
|
|
1109
|
+
{ label: "Issue", key: "issue_id" },
|
|
1110
|
+
{ label: "Status", render: (row) => `<span class="status-pill ${row.ready ? "" : "waiting-provider"}">${row.ready ? "ready" : "retrying"}</span>` },
|
|
1111
|
+
{ label: "Reason", render: (row) => row.last_reason || "n/a" },
|
|
1112
|
+
{ label: "Attempts", key: "attempts" },
|
|
1113
|
+
{ label: "Next attempt", render: (row) => row.next_attempt_at ? `${relativeTime(row.next_attempt_at)}<div class="muted">${formatCompactDate(row.next_attempt_at)}</div>` : "n/a" },
|
|
1114
|
+
{ label: "Time Remaining", render: (row) => row.next_attempt_at ? timeRemaining(row.next_attempt_at) : "n/a" },
|
|
1115
|
+
],
|
|
1116
|
+
profile.issue_retries || [],
|
|
1117
|
+
"No issue retries recorded.",
|
|
1118
|
+
);
|
|
1119
|
+
|
|
1120
|
+
const prRetryTable = renderTableWithPagination(
|
|
1121
|
+
`pr-retries-${profile.id}`,
|
|
1122
|
+
[
|
|
1123
|
+
{ label: "PR", key: "pr_number" },
|
|
1124
|
+
{ label: "Status", render: (row) => `<span class="status-pill ${row.ready ? "" : "waiting-provider"}">${row.ready ? "ready" : "retrying"}</span>` },
|
|
1125
|
+
{ label: "Reason", render: (row) => row.last_reason || "n/a" },
|
|
1126
|
+
{ label: "Attempts", key: "attempts" },
|
|
1127
|
+
{ label: "Next attempt", render: (row) => row.next_attempt_at ? `${relativeTime(row.next_attempt_at)}<div class="muted">${formatCompactDate(row.next_attempt_at)}</div>` : "n/a" },
|
|
1128
|
+
{ label: "Time Remaining", render: (row) => row.next_attempt_at ? timeRemaining(row.next_attempt_at) : "n/a" },
|
|
1129
|
+
],
|
|
1130
|
+
profile.pr_retries || [],
|
|
1131
|
+
"No PR retries recorded.",
|
|
1132
|
+
);
|
|
1133
|
+
|
|
1134
|
+
const workerTable = renderTableWithPagination(
|
|
1135
|
+
`workers-${profile.id}`,
|
|
1136
|
+
[
|
|
1137
|
+
{ label: "Key", render: (row) => `<div class="mono">${row.key}</div>` },
|
|
1138
|
+
{ label: "Scope", key: "scope" },
|
|
1139
|
+
{ label: "Worker", key: "coding_worker" },
|
|
1140
|
+
{ label: "Issue", render: (row) => row.issue_id || "n/a" },
|
|
1141
|
+
{ label: "Lane", render: (row) => `${row.resident_lane_kind || "n/a"} / ${row.resident_lane_value || "n/a"}` },
|
|
1142
|
+
{ label: "Tasks", key: "task_count" },
|
|
1143
|
+
{ label: "Last status", render: (row) => row.last_status || "n/a" },
|
|
1144
|
+
{ label: "Last started", render: (row) => row.last_started_at ? `${relativeTime(row.last_started_at)}<div class="muted">${formatCompactDate(row.last_started_at)}</div>` : "n/a" },
|
|
1145
|
+
],
|
|
1146
|
+
profile.resident_workers,
|
|
1147
|
+
"No resident worker metadata yet.",
|
|
1148
|
+
);
|
|
1149
|
+
|
|
1150
|
+
const cooldownTable = renderTableWithPagination(
|
|
1151
|
+
`cooldowns-${profile.id}`,
|
|
1152
|
+
[
|
|
1153
|
+
{ label: "Provider key", render: (row) => `<div class="mono">${row.provider_key}</div>` },
|
|
1154
|
+
{ label: "State", render: (row) => `<span class="status-pill ${row.active ? "waiting-provider" : ""}">${row.active ? "cooldown" : "expired"}</span>` },
|
|
1155
|
+
{ label: "Reason", render: (row) => row.last_reason || "n/a" },
|
|
1156
|
+
{ label: "Attempts", key: "attempts" },
|
|
1157
|
+
{ label: "Next attempt", render: (row) => row.next_attempt_at ? `${relativeTime(row.next_attempt_at)}<div class="muted">${formatCompactDate(row.next_attempt_at)}</div>` : "n/a" },
|
|
1158
|
+
{ label: "Time Remaining", render: (row) => row.next_attempt_at ? timeRemaining(row.next_attempt_at) : "n/a" },
|
|
1159
|
+
],
|
|
1160
|
+
profile.provider_cooldowns,
|
|
1161
|
+
"No provider cooldowns recorded.",
|
|
1162
|
+
);
|
|
1163
|
+
|
|
1164
|
+
const scheduledTable = renderTableWithPagination(
|
|
1165
|
+
`scheduled-${profile.id}`,
|
|
1166
|
+
[
|
|
1167
|
+
{ label: "Issue", key: "issue_id" },
|
|
1168
|
+
{ label: "Interval", render: (row) => `${row.interval_seconds}s` },
|
|
1169
|
+
{ label: "Next due", render: (row) => row.next_due_at ? `${relativeTime(row.next_due_at)}<div class="muted">${formatCompactDate(row.next_due_at)}</div>` : "n/a" },
|
|
1170
|
+
{ label: "Time Remaining", render: (row) => row.next_due_at ? timeRemaining(row.next_due_at) : "n/a" },
|
|
1171
|
+
{ label: "Last started", render: (row) => row.last_started_at ? `${relativeTime(row.last_started_at)}<div class="muted">${formatCompactDate(row.last_started_at)}</div>` : "n/a" },
|
|
1172
|
+
],
|
|
1173
|
+
profile.scheduled_issues,
|
|
1174
|
+
"No scheduled issue state recorded.",
|
|
1175
|
+
);
|
|
1176
|
+
|
|
1177
|
+
const queueTable = renderTableWithPagination(
|
|
1178
|
+
`queue-${profile.id}`,
|
|
1179
|
+
[
|
|
1180
|
+
{ label: "Issue", key: "issue_id" },
|
|
1181
|
+
{ label: "Session", render: (row) => row.session ? `<div class="mono">${row.session}</div>` : "n/a" },
|
|
1182
|
+
{ label: "Queued by", key: "queued_by" },
|
|
1183
|
+
{ label: "Updated", render: (row) => row.updated_at ? `${relativeTime(row.updated_at)}<div class="muted">${formatCompactDate(row.updated_at)}</div>` : "n/a" },
|
|
1184
|
+
],
|
|
1185
|
+
profile.issue_queue.pending,
|
|
1186
|
+
"No pending leased issues.",
|
|
1187
|
+
);
|
|
1188
|
+
|
|
1189
|
+
const claimsTable = renderTableWithPagination(
|
|
1190
|
+
`claims-${profile.id}`,
|
|
1191
|
+
[
|
|
1192
|
+
{ label: "Issue", key: "issue_id" },
|
|
1193
|
+
{ label: "Session", render: (row) => row.session ? `<div class="mono">${row.session}</div>` : "n/a" },
|
|
1194
|
+
{ label: "Claimed by", key: "claimer" },
|
|
1195
|
+
{ label: "Updated", render: (row) => row.updated_at ? `${relativeTime(row.updated_at)}<div class="muted">${formatCompactDate(row.updated_at)}</div>` : "n/a" },
|
|
1196
|
+
],
|
|
1197
|
+
profile.issue_queue.claims || [],
|
|
1198
|
+
"No claimed issues.",
|
|
1199
|
+
);
|
|
1200
|
+
|
|
1201
|
+
const githubOutbox = profile.github_outbox || { counts: {}, pending: [] };
|
|
1202
|
+
const githubOutboxTable = renderTableWithPagination(
|
|
1203
|
+
`github-outbox-${profile.id}`,
|
|
1204
|
+
[
|
|
1205
|
+
{ label: "Type", render: (row) => row.type || "n/a" },
|
|
1206
|
+
{ label: "Target", render: (row) => `${row.kind || row.type || "write"} #${row.number || "?"}` },
|
|
1207
|
+
{
|
|
1208
|
+
label: "Payload",
|
|
1209
|
+
render: (row) => {
|
|
1210
|
+
if (row.type === "labels") {
|
|
1211
|
+
return `+${row.add_count || 0} / -${row.remove_count || 0}`;
|
|
1212
|
+
}
|
|
1213
|
+
return row.body_preview || "n/a";
|
|
1214
|
+
},
|
|
1215
|
+
},
|
|
1216
|
+
{ label: "Created", render: (row) => row.created_at ? `${relativeTime(row.created_at)}<div class="muted">${formatCompactDate(row.created_at)}</div>` : "n/a" },
|
|
1217
|
+
],
|
|
1218
|
+
githubOutbox.pending || [],
|
|
1219
|
+
"No pending GitHub write intents.",
|
|
1220
|
+
);
|
|
1221
|
+
|
|
1222
|
+
const codexRotationPanel =
|
|
1223
|
+
profile.coding_worker === "codex"
|
|
1224
|
+
? `
|
|
1225
|
+
<section class="panel">
|
|
1226
|
+
<h3>Codex Rotation</h3>
|
|
1227
|
+
<p class="panel-subtitle">Shows the active Codex label, candidate labels, and whether failover is ready or deferred.</p>
|
|
1228
|
+
${renderCodexRotation(profile.codex_rotation)}
|
|
1229
|
+
</section>
|
|
1230
|
+
`
|
|
1231
|
+
: "";
|
|
1232
|
+
|
|
1233
|
+
const runsFilterBar = `
|
|
1234
|
+
<div class="filter-bar">
|
|
1235
|
+
<input type="text" class="filter-search" placeholder="Search runs..." value="${runsFilterState.search}"
|
|
1236
|
+
oninput="window._acpRunsFilter.search=this.value; rerenderAll();" />
|
|
1237
|
+
<button class="filter-btn ${runsFilterState.status === 'all' ? 'active' : ''}" onclick="window._acpRunsFilter.status='all'; rerenderAll();">All</button>
|
|
1238
|
+
<button class="filter-btn ${runsFilterState.status === 'RUNNING' ? 'active' : ''}" onclick="window._acpRunsFilter.status='RUNNING'; rerenderAll();">Running</button>
|
|
1239
|
+
<button class="filter-btn ${runsFilterState.status === 'SUCCEEDED' ? 'active' : ''}" onclick="window._acpRunsFilter.status='SUCCEEDED'; rerenderAll();">Completed</button>
|
|
1240
|
+
<button class="filter-btn ${runsFilterState.status === 'FAILED' ? 'active' : ''}" onclick="window._acpRunsFilter.status='FAILED'; rerenderAll();">Failed</button>
|
|
1241
|
+
</div>
|
|
1242
|
+
`;
|
|
1243
|
+
|
|
1244
|
+
const historyFilterBar = `
|
|
1245
|
+
<div class="filter-bar">
|
|
1246
|
+
<input type="text" class="filter-search" placeholder="Search history..." value="${historyFilterState.search}"
|
|
1247
|
+
oninput="window._acpHistoryFilter.search=this.value; rerenderAll();" />
|
|
1248
|
+
<button class="filter-btn ${historyFilterState.result === 'all' ? 'active' : ''}" onclick="window._acpHistoryFilter.result='all'; rerenderAll();">All</button>
|
|
1249
|
+
<button class="filter-btn ${historyFilterState.result === 'implemented' ? 'active' : ''}" onclick="window._acpHistoryFilter.result='implemented'; rerenderAll();">Implemented</button>
|
|
1250
|
+
<button class="filter-btn ${historyFilterState.result === 'reported' ? 'active' : ''}" onclick="window._acpHistoryFilter.result='reported'; rerenderAll();">Reported</button>
|
|
1251
|
+
<button class="filter-btn ${historyFilterState.result === 'blocked' ? 'active' : ''}" onclick="window._acpHistoryFilter.result='blocked'; rerenderAll();">Blocked</button>
|
|
1252
|
+
</div>
|
|
1253
|
+
`;
|
|
1254
|
+
|
|
1255
|
+
return `
|
|
1256
|
+
<article class="profile">
|
|
1257
|
+
<header class="profile-header">
|
|
1258
|
+
<div>
|
|
1259
|
+
<div class="profile-title">
|
|
1260
|
+
<h2>${profile.id}</h2>
|
|
1261
|
+
<span class="badge">${profile.repo_slug || "repo slug unavailable"}</span>
|
|
1262
|
+
</div>
|
|
1263
|
+
<div class="profile-subtitle mono">${profile.runs_root}</div>
|
|
1264
|
+
</div>
|
|
1265
|
+
<div class="badge-row">${providerBadges}</div>
|
|
1266
|
+
</header>
|
|
1267
|
+
<section class="overview">${summaryCards}</section>
|
|
1268
|
+
<section class="profile-grid">
|
|
1269
|
+
<section class="panel">
|
|
1270
|
+
<h3>Host Alerts</h3>
|
|
1271
|
+
<p class="panel-subtitle">High-signal operational blockers surfaced from active run logs and comment artifacts.</p>
|
|
1272
|
+
${renderAlerts(profile.alerts || [])}
|
|
1273
|
+
</section>
|
|
1274
|
+
<section class="panel">
|
|
1275
|
+
<h3>Active Runs</h3>
|
|
1276
|
+
<p class="panel-subtitle">Lifecycle shows technical session completion. Result shows what the run achieved: implemented, reported, or blocked.</p>
|
|
1277
|
+
${runsFilterBar}
|
|
1278
|
+
${runsTable}
|
|
1279
|
+
</section>
|
|
1280
|
+
<section class="panel">
|
|
1281
|
+
<h3>Recent Completed Runs</h3>
|
|
1282
|
+
<p class="panel-subtitle">Recently archived runs so they do not disappear from the dashboard immediately after completion.</p>
|
|
1283
|
+
${historyFilterBar}
|
|
1284
|
+
${recentHistoryTable}
|
|
1285
|
+
</section>
|
|
1286
|
+
<section class="panel">
|
|
1287
|
+
<h3>Resident Controllers</h3>
|
|
1288
|
+
<p class="panel-subtitle">Includes provider wait and failover telemetry. Stale controllers show a warning.</p>
|
|
1289
|
+
${controllerTable}
|
|
1290
|
+
</section>
|
|
1291
|
+
${codexRotationPanel}
|
|
1292
|
+
<section class="panel half">
|
|
1293
|
+
<h3>Issue Retries</h3>
|
|
1294
|
+
${retryTable}
|
|
1295
|
+
</section>
|
|
1296
|
+
<section class="panel half">
|
|
1297
|
+
<h3>PR Retries</h3>
|
|
1298
|
+
${prRetryTable}
|
|
1299
|
+
</section>
|
|
1300
|
+
<section class="panel">
|
|
1301
|
+
<h3>Resident Worker Metadata</h3>
|
|
1302
|
+
${workerTable}
|
|
1303
|
+
</section>
|
|
1304
|
+
<section class="panel">
|
|
1305
|
+
<h3>Troubleshooting</h3>
|
|
1306
|
+
<p class="panel-subtitle">Run diagnostics or debugging tools against this live profile.</p>
|
|
1307
|
+
<div class="action-bar">
|
|
1308
|
+
<button class="action-btn" onclick="runDoctor('${profile.id}')">🔧 Run Doctor</button>
|
|
1309
|
+
<button class="action-btn" onclick="exportProfile('${profile.id}')">📤 Export</button>
|
|
1310
|
+
<button class="action-btn" onclick="document.getElementById('import-file-${profile.id}').click()">📥 Import</button>
|
|
1311
|
+
<input type="file" id="import-file-${profile.id}" style="display:none" accept=".json" onchange="importProfile('${profile.id}', this)">
|
|
1312
|
+
<span id="doctor-status-${profile.id}"></span>
|
|
1313
|
+
</div>
|
|
1314
|
+
<pre id="doctor-output-${profile.id}" class="doctor-output" style="display:none;"></pre>
|
|
1315
|
+
</section>
|
|
1316
|
+
<section class="panel half">
|
|
1317
|
+
<h3>Provider Cooldowns</h3>
|
|
1318
|
+
${cooldownTable}
|
|
1319
|
+
</section>
|
|
1320
|
+
<section class="panel half">
|
|
1321
|
+
<h3>Scheduled Issues</h3>
|
|
1322
|
+
${scheduledTable}
|
|
1323
|
+
</section>
|
|
1324
|
+
<section class="panel half">
|
|
1325
|
+
<h3>Pending Issue Queue</h3>
|
|
1326
|
+
${queueTable}
|
|
1327
|
+
</section>
|
|
1328
|
+
<section class="panel half">
|
|
1329
|
+
<h3>Claimed Issues</h3>
|
|
1330
|
+
${claimsTable}
|
|
1331
|
+
</section>
|
|
1332
|
+
<section class="panel">
|
|
1333
|
+
<h3>GitHub Outbox</h3>
|
|
1334
|
+
<p class="panel-subtitle">Local write intents queued while ACP defers or retries GitHub sync. Pending ${githubOutbox.counts?.pending || 0}, sent ${githubOutbox.counts?.sent || 0}, failed ${githubOutbox.counts?.failed || 0}.</p>
|
|
1335
|
+
${githubOutboxTable}
|
|
1336
|
+
</section>
|
|
1337
|
+
</section>
|
|
1338
|
+
</article>
|
|
1339
|
+
`;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
async function maybeNotifyAlerts(snapshot) {
|
|
1343
|
+
const alerts = (snapshot.alerts || []).filter((alert) => alert && alert.id);
|
|
1344
|
+
if (!alerts.length || typeof window.Notification === "undefined") return;
|
|
1345
|
+
|
|
1346
|
+
if (window.Notification.permission === "default" && !notificationPermissionRequested) {
|
|
1347
|
+
notificationPermissionRequested = true;
|
|
1348
|
+
try {
|
|
1349
|
+
await window.Notification.requestPermission();
|
|
1350
|
+
} catch (_error) {
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
if (window.Notification.permission !== "granted") return;
|
|
1356
|
+
|
|
1357
|
+
for (const alert of alerts) {
|
|
1358
|
+
if (seenAlertIds.has(alert.id)) continue;
|
|
1359
|
+
seenAlertIds.add(alert.id);
|
|
1360
|
+
const bodyParts = [];
|
|
1361
|
+
if (alert.session) bodyParts.push(alert.session);
|
|
1362
|
+
if (alert.reset_at) bodyParts.push(`reset ${alert.reset_at}`);
|
|
1363
|
+
if (alert.message) bodyParts.push(alert.message);
|
|
1364
|
+
new window.Notification(alert.title || "ACP alert", {
|
|
1365
|
+
body: bodyParts.join(" · ").slice(0, 240),
|
|
1366
|
+
tag: alert.id,
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
async function loadSnapshot() {
|
|
1372
|
+
refreshButton.disabled = true;
|
|
1373
|
+
try {
|
|
1374
|
+
const response = await fetch("./api/snapshot.json", { cache: "no-store" });
|
|
1375
|
+
if (!response.ok) {
|
|
1376
|
+
throw new Error(`Snapshot request failed with ${response.status}`);
|
|
1377
|
+
}
|
|
1378
|
+
const snapshot = await response.json();
|
|
1379
|
+
window._acpSnapshot = snapshot;
|
|
1380
|
+
renderFromSnapshot(snapshot);
|
|
1381
|
+
await maybeNotifyAlerts(snapshot);
|
|
1382
|
+
} catch (error) {
|
|
1383
|
+
generatedAtNode.textContent = `Snapshot load failed: ${error.message}`;
|
|
1384
|
+
profilesNode.innerHTML = `<article class="profile"><div class="empty-state">${error.message}</div></article>`;
|
|
1385
|
+
} finally {
|
|
1386
|
+
refreshButton.disabled = false;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
function renderFromSnapshot(snapshot) {
|
|
1391
|
+
generatedAtNode.textContent = `Snapshot: ${formatCompactDate(snapshot.generated_at)}`;
|
|
1392
|
+
renderOverview(snapshot);
|
|
1393
|
+
profilesNode.innerHTML = snapshot.profiles.map(renderProfile).join("");
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
function rerenderAll() {
|
|
1397
|
+
const snapshot = window._acpSnapshot;
|
|
1398
|
+
if (!snapshot) return;
|
|
1399
|
+
renderFromSnapshot(snapshot);
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
async function runDoctor(profileId) {
|
|
1403
|
+
const statusEl = document.getElementById(`doctor-status-${profileId}`);
|
|
1404
|
+
const outputEl = document.getElementById(`doctor-output-${profileId}`);
|
|
1405
|
+
if (statusEl) statusEl.textContent = "Running...";
|
|
1406
|
+
if (outputEl) {
|
|
1407
|
+
outputEl.style.display = "none";
|
|
1408
|
+
outputEl.textContent = "";
|
|
1409
|
+
}
|
|
1410
|
+
try {
|
|
1411
|
+
const response = await fetch(`/api/doctor?profile_id=${encodeURIComponent(profileId)}`, { cache: "no-store" });
|
|
1412
|
+
const data = await response.json();
|
|
1413
|
+
if (statusEl) statusEl.textContent = response.ok ? "Done" : `Error: ${data.error || response.status}`;
|
|
1414
|
+
if (outputEl) {
|
|
1415
|
+
outputEl.style.display = "block";
|
|
1416
|
+
outputEl.textContent = data.output || data.error || "No output";
|
|
1417
|
+
}
|
|
1418
|
+
} catch (error) {
|
|
1419
|
+
if (statusEl) statusEl.textContent = `Error: ${error.message}`;
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
async function exportProfile(profileId) {
|
|
1424
|
+
try {
|
|
1425
|
+
const response = await fetch(`/api/profile/export?profile_id=${encodeURIComponent(profileId)}`, { cache: "no-store" });
|
|
1426
|
+
if (!response.ok) {
|
|
1427
|
+
const data = await response.json();
|
|
1428
|
+
alert(`Export failed: ${data.error || response.status}`);
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
const data = await response.json();
|
|
1432
|
+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
|
|
1433
|
+
const url = URL.createObjectURL(blob);
|
|
1434
|
+
const a = document.createElement("a");
|
|
1435
|
+
a.href = url;
|
|
1436
|
+
a.download = `acp-profile-${profileId}.json`;
|
|
1437
|
+
document.body.appendChild(a);
|
|
1438
|
+
a.click();
|
|
1439
|
+
document.body.removeChild(a);
|
|
1440
|
+
URL.revokeObjectURL(url);
|
|
1441
|
+
} catch (error) {
|
|
1442
|
+
alert(`Export failed: ${error.message}`);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
async function importProfile(profileId, inputEl) {
|
|
1447
|
+
const file = inputEl.files[0];
|
|
1448
|
+
if (!file) return;
|
|
1449
|
+
|
|
1450
|
+
try {
|
|
1451
|
+
const text = await file.text();
|
|
1452
|
+
const data = JSON.parse(text);
|
|
1453
|
+
|
|
1454
|
+
if (!data.profile_id || !data.config) {
|
|
1455
|
+
alert("Invalid profile file: missing profile_id or config");
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
const response = await fetch("/api/profile/import", {
|
|
1460
|
+
method: "POST",
|
|
1461
|
+
headers: { "Content-Type": "application/json" },
|
|
1462
|
+
body: JSON.stringify(data),
|
|
1463
|
+
});
|
|
1464
|
+
|
|
1465
|
+
const result = await response.json();
|
|
1466
|
+
if (response.ok) {
|
|
1467
|
+
alert(`Profile ${profileId} imported successfully!`);
|
|
1468
|
+
setTimeout(() => window.location.reload(), 1000);
|
|
1469
|
+
} else {
|
|
1470
|
+
alert(`Import failed: ${result.error || response.status}`);
|
|
1471
|
+
}
|
|
1472
|
+
} catch (error) {
|
|
1473
|
+
alert(`Import failed: ${error.message}`);
|
|
1474
|
+
} finally {
|
|
1475
|
+
inputEl.value = "";
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
refreshButton.addEventListener("click", () => {
|
|
1480
|
+
void loadSnapshot();
|
|
1481
|
+
});
|
|
1482
|
+
|
|
1483
|
+
initializeTheme();
|
|
1484
|
+
void loadSnapshot();
|
|
1485
|
+
|
|
1486
|
+
// WebSocket live updates
|
|
1487
|
+
let wsReconnectDelay = 1000;
|
|
1488
|
+
let wsConnectionActive = false;
|
|
1489
|
+
|
|
1490
|
+
function connectWebSocket() {
|
|
1491
|
+
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
|
|
1492
|
+
const wsUrl = `${protocol}//${location.host}/ws`;
|
|
1493
|
+
const ws = new WebSocket(wsUrl);
|
|
1494
|
+
|
|
1495
|
+
ws.onopen = () => {
|
|
1496
|
+
wsReconnectDelay = 1000;
|
|
1497
|
+
wsConnectionActive = true;
|
|
1498
|
+
console.log("ACP Dashboard: WebSocket connected");
|
|
1499
|
+
};
|
|
1500
|
+
|
|
1501
|
+
ws.onmessage = async (event) => {
|
|
1502
|
+
try {
|
|
1503
|
+
let data = event.data;
|
|
1504
|
+
// Handle Blob (binary) or string
|
|
1505
|
+
if (data instanceof Blob) {
|
|
1506
|
+
data = await data.text();
|
|
1507
|
+
}
|
|
1508
|
+
const snapshot = JSON.parse(data);
|
|
1509
|
+
window._acpSnapshot = snapshot;
|
|
1510
|
+
renderFromSnapshot(snapshot);
|
|
1511
|
+
maybeNotifyAlerts(snapshot);
|
|
1512
|
+
} catch (error) {
|
|
1513
|
+
console.error("ACP Dashboard: Failed to parse WebSocket message", error);
|
|
1514
|
+
}
|
|
1515
|
+
};
|
|
1516
|
+
|
|
1517
|
+
ws.onclose = () => {
|
|
1518
|
+
wsConnectionActive = false;
|
|
1519
|
+
console.log(`ACP Dashboard: WebSocket disconnected, reconnecting in ${wsReconnectDelay}ms`);
|
|
1520
|
+
setTimeout(connectWebSocket, wsReconnectDelay);
|
|
1521
|
+
wsReconnectDelay = Math.min(wsReconnectDelay * 2, 30000);
|
|
1522
|
+
};
|
|
1523
|
+
|
|
1524
|
+
ws.onerror = (error) => {
|
|
1525
|
+
console.error("ACP Dashboard: WebSocket error", error);
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
connectWebSocket();
|
|
1530
|
+
|
|
1531
|
+
</script>
|
|
1532
|
+
</body>
|
|
1533
|
+
</html>
|