agent-control-plane 0.7.1 → 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 +9 -7
- package/tools/bin/flow-runtime-doctor.sh +67 -0
- package/tools/bin/heartbeat-safe-auto.sh +161 -0
- package/tools/bin/render-flow-config.sh +98 -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
package/tools/dashboard/app.js
CHANGED
|
@@ -6,6 +6,10 @@ const profilesNode = document.querySelector("#profiles");
|
|
|
6
6
|
const seenAlertIds = new Set();
|
|
7
7
|
let notificationPermissionRequested = false;
|
|
8
8
|
const THEME_STORAGE_KEY = "acp-dashboard-theme";
|
|
9
|
+
const ROWS_PER_PAGE = 10;
|
|
10
|
+
|
|
11
|
+
// Pagination state: { [tableId]: { page: number } }
|
|
12
|
+
window._acpPagination = {};
|
|
9
13
|
|
|
10
14
|
function systemPrefersDark() {
|
|
11
15
|
return typeof window.matchMedia === "function" && window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
@@ -72,6 +76,14 @@ function relativeTime(input) {
|
|
|
72
76
|
return seconds >= 0 ? `${absolute}s ago` : `in ${absolute}s`;
|
|
73
77
|
}
|
|
74
78
|
|
|
79
|
+
function formatCompactDate(input) {
|
|
80
|
+
if (!input) return "n/a";
|
|
81
|
+
const d = new Date(input);
|
|
82
|
+
if (Number.isNaN(d.getTime())) return input;
|
|
83
|
+
const months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
|
|
84
|
+
return `${months[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
75
87
|
function formatDuration(seconds) {
|
|
76
88
|
if (!seconds && seconds !== 0) return "n/a";
|
|
77
89
|
const absSeconds = Math.abs(seconds);
|
|
@@ -107,7 +119,7 @@ function statusClass(status) {
|
|
|
107
119
|
}
|
|
108
120
|
|
|
109
121
|
function renderLifecycle(row) {
|
|
110
|
-
const note = row.result_only_completion === "yes" ? `<div class="muted">Recovered
|
|
122
|
+
const note = row.result_only_completion === "yes" ? `<div class="muted">Recovered</div>` : "";
|
|
111
123
|
return `<span class="status-pill ${statusClass(row.lifecycle_status || row.status)}">${row.lifecycle_status || row.status || "UNKNOWN"}</span>${note}`;
|
|
112
124
|
}
|
|
113
125
|
|
|
@@ -172,12 +184,74 @@ function renderOverview(snapshot) {
|
|
|
172
184
|
.join("");
|
|
173
185
|
}
|
|
174
186
|
|
|
175
|
-
|
|
187
|
+
// Build windowed pagination: max 5 page buttons with ellipsis
|
|
188
|
+
function buildWindowedPages(current, total) {
|
|
189
|
+
const pages = [];
|
|
190
|
+
const maxVisible = 5;
|
|
191
|
+
if (total <= maxVisible) {
|
|
192
|
+
for (let i = 1; i <= total; i++) pages.push(i);
|
|
193
|
+
return pages;
|
|
194
|
+
}
|
|
195
|
+
// Always show first, last, and surrounding pages
|
|
196
|
+
const pagesSet = new Set();
|
|
197
|
+
pagesSet.add(1);
|
|
198
|
+
pagesSet.add(total);
|
|
199
|
+
for (let i = Math.max(1, current - 1); i <= Math.min(total, current + 1); i++) {
|
|
200
|
+
pagesSet.add(i);
|
|
201
|
+
}
|
|
202
|
+
const sorted = Array.from(pagesSet).sort((a, b) => a - b);
|
|
203
|
+
// Add ellipsis markers
|
|
204
|
+
const result = [];
|
|
205
|
+
let prev = 0;
|
|
206
|
+
for (const p of sorted) {
|
|
207
|
+
if (p - prev > 1) result.push("...");
|
|
208
|
+
result.push(p);
|
|
209
|
+
prev = p;
|
|
210
|
+
}
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function renderPagination(tableId, currentPage, totalPages, totalRows) {
|
|
215
|
+
if (totalPages <= 1) return "";
|
|
216
|
+
const start = (currentPage - 1) * ROWS_PER_PAGE + 1;
|
|
217
|
+
const end = Math.min(currentPage * ROWS_PER_PAGE, totalRows);
|
|
218
|
+
const pages = buildWindowedPages(currentPage, totalPages);
|
|
219
|
+
const buttons = pages
|
|
220
|
+
.map((p) => {
|
|
221
|
+
if (p === "...") return `<span class="pagination-ellipsis">…</span>`;
|
|
222
|
+
return `<button class="${p === currentPage ? "active" : ""}" onclick="window._acpGoToPage('${tableId}',${p})">${p}</button>`;
|
|
223
|
+
})
|
|
224
|
+
.join("");
|
|
225
|
+
return `
|
|
226
|
+
<div class="pagination">
|
|
227
|
+
<span class="pagination-info">Showing ${start}-${end} of ${totalRows}</span>
|
|
228
|
+
<div class="pagination-controls">
|
|
229
|
+
<button ${currentPage <= 1 ? "disabled" : ""} onclick="window._acpGoToPage('${tableId}',${currentPage - 1})">‹</button>
|
|
230
|
+
${buttons}
|
|
231
|
+
<button ${currentPage >= totalPages ? "disabled" : ""} onclick="window._acpGoToPage('${tableId}',${currentPage + 1})">›</button>
|
|
232
|
+
</div>
|
|
233
|
+
</div>`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
window._acpGoToPage = function(tableId, page) {
|
|
237
|
+
window._acpPagination[tableId] = { page };
|
|
238
|
+
rerenderAll();
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
function renderTableWithPagination(tableId, columns, rows, emptyMessage = "No data right now.") {
|
|
176
242
|
if (!rows.length) {
|
|
177
243
|
return `<div class="empty-state">${emptyMessage}</div>`;
|
|
178
244
|
}
|
|
245
|
+
const state = window._acpPagination[tableId] || { page: 1 };
|
|
246
|
+
let { page } = state;
|
|
247
|
+
const totalPages = Math.ceil(rows.length / ROWS_PER_PAGE);
|
|
248
|
+
if (page < 1) page = 1;
|
|
249
|
+
if (page > totalPages) page = totalPages;
|
|
250
|
+
window._acpPagination[tableId] = { page };
|
|
251
|
+
const start = (page - 1) * ROWS_PER_PAGE;
|
|
252
|
+
const pageRows = rows.slice(start, start + ROWS_PER_PAGE);
|
|
179
253
|
const headers = columns.map((column) => `<th>${column.label}</th>`).join("");
|
|
180
|
-
const body =
|
|
254
|
+
const body = pageRows
|
|
181
255
|
.map((row) => {
|
|
182
256
|
const cells = columns
|
|
183
257
|
.map((column) => `<td>${column.render ? column.render(row) : row[column.key] ?? ""}</td>`)
|
|
@@ -185,7 +259,8 @@ function renderTable(columns, rows, emptyMessage = "No data right now.") {
|
|
|
185
259
|
return `<tr>${cells}</tr>`;
|
|
186
260
|
})
|
|
187
261
|
.join("");
|
|
188
|
-
|
|
262
|
+
const paginationHtml = renderPagination(tableId, page, totalPages, rows.length);
|
|
263
|
+
return `<div class="table-wrap"><table><thead><tr>${headers}</tr></thead><tbody>${body}</tbody></table></div>${paginationHtml}`;
|
|
189
264
|
}
|
|
190
265
|
|
|
191
266
|
function renderAlerts(alerts) {
|
|
@@ -207,8 +282,8 @@ function renderAlerts(alerts) {
|
|
|
207
282
|
</div>
|
|
208
283
|
<p>${alert.message}</p>
|
|
209
284
|
<div class="alert-meta">
|
|
210
|
-
<span>${alert.reset_at ? `Reset: ${alert.reset_at}` : "Reset: n/a"}</span>
|
|
211
|
-
<span>${alert.updated_at ? `${relativeTime(alert.updated_at)} · ${alert.updated_at}` : "updated n/a"}</span>
|
|
285
|
+
<span>${alert.reset_at ? `Reset: ${formatCompactDate(alert.reset_at)}` : "Reset: n/a"}</span>
|
|
286
|
+
<span>${alert.updated_at ? `${relativeTime(alert.updated_at)} · ${formatCompactDate(alert.updated_at)}` : "updated n/a"}</span>
|
|
212
287
|
</div>
|
|
213
288
|
</article>
|
|
214
289
|
`,
|
|
@@ -225,13 +300,14 @@ function renderCodexRotation(rotation) {
|
|
|
225
300
|
const candidates = (rotation.candidate_labels || []).length ? rotation.candidate_labels.join(", ") : "n/a";
|
|
226
301
|
const ready = (rotation.ready_candidates || []).length ? rotation.ready_candidates.join(", ") : "none";
|
|
227
302
|
const nextRetry = rotation.next_retry_at
|
|
228
|
-
? `${rotation.next_retry_label || "n/a"} · ${relativeTime(rotation.next_retry_at)}<div class="muted">${rotation.next_retry_at}</div>`
|
|
303
|
+
? `${rotation.next_retry_label || "n/a"} · ${relativeTime(rotation.next_retry_at)}<div class="muted">${formatCompactDate(rotation.next_retry_at)}</div>`
|
|
229
304
|
: "n/a";
|
|
230
305
|
const lastSwitch = rotation.last_switch_label
|
|
231
306
|
? `${rotation.last_switch_label}${rotation.last_switch_reason ? ` · ${rotation.last_switch_reason}` : ""}`
|
|
232
307
|
: "n/a";
|
|
233
308
|
|
|
234
|
-
return
|
|
309
|
+
return renderTableWithPagination(
|
|
310
|
+
"codex-rotation",
|
|
235
311
|
[
|
|
236
312
|
{ label: "Current", render: () => `<div class="mono">${rotation.active_label}</div>` },
|
|
237
313
|
{ label: "Decision", render: () => `<span class="status-pill ${statusClass(rotation.switch_decision || "unknown")}">${rotation.switch_decision || "unknown"}</span>` },
|
|
@@ -304,7 +380,8 @@ function renderProfile(profile) {
|
|
|
304
380
|
return true;
|
|
305
381
|
});
|
|
306
382
|
|
|
307
|
-
const runsTable =
|
|
383
|
+
const runsTable = renderTableWithPagination(
|
|
384
|
+
`runs-${profile.id}`,
|
|
308
385
|
[
|
|
309
386
|
{ label: "Session", render: (row) => `<div class="mono">${row.session}</div>` },
|
|
310
387
|
{ label: "Task", render: (row) => `${row.task_kind || "n/a"} ${row.task_id || ""}`.trim() },
|
|
@@ -312,7 +389,7 @@ function renderProfile(profile) {
|
|
|
312
389
|
{ label: "Worker", key: "coding_worker" },
|
|
313
390
|
{ label: "Provider", render: (row) => row.provider_model || "n/a" },
|
|
314
391
|
{ label: "Result", render: renderResult },
|
|
315
|
-
{ label: "Updated", render: (row) => row.updated_at ? `${relativeTime(row.updated_at)}<div class="muted">${row.updated_at}</div>` : "n/a" },
|
|
392
|
+
{ label: "Updated", render: (row) => row.updated_at ? `${relativeTime(row.updated_at)}<div class="muted">${formatCompactDate(row.updated_at)}</div>` : "n/a" },
|
|
316
393
|
],
|
|
317
394
|
filteredRuns,
|
|
318
395
|
"No active run directories for this profile.",
|
|
@@ -334,20 +411,22 @@ function renderProfile(profile) {
|
|
|
334
411
|
return true;
|
|
335
412
|
});
|
|
336
413
|
|
|
337
|
-
const recentHistoryTable =
|
|
414
|
+
const recentHistoryTable = renderTableWithPagination(
|
|
415
|
+
`history-${profile.id}`,
|
|
338
416
|
[
|
|
339
417
|
{ label: "Session", render: (row) => `<div class="mono">${row.session}</div>` },
|
|
340
418
|
{ label: "Task", render: (row) => `${row.task_kind || "n/a"} ${row.task_id || ""}`.trim() },
|
|
341
419
|
{ label: "Lifecycle", render: renderLifecycle },
|
|
342
420
|
{ label: "Worker", key: "coding_worker" },
|
|
343
421
|
{ label: "Result", render: renderResult },
|
|
344
|
-
{ label: "Updated", render: (row) => row.updated_at ? `${relativeTime(row.updated_at)}<div class="muted">${row.updated_at}</div>` : "n/a" },
|
|
422
|
+
{ label: "Updated", render: (row) => row.updated_at ? `${relativeTime(row.updated_at)}<div class="muted">${formatCompactDate(row.updated_at)}</div>` : "n/a" },
|
|
345
423
|
],
|
|
346
424
|
filteredHistory,
|
|
347
425
|
"No recently archived runs.",
|
|
348
426
|
);
|
|
349
427
|
|
|
350
|
-
const controllerTable =
|
|
428
|
+
const controllerTable = renderTableWithPagination(
|
|
429
|
+
`controllers-${profile.id}`,
|
|
351
430
|
[
|
|
352
431
|
{ label: "Issue", key: "issue_id" },
|
|
353
432
|
{ label: "State", render: renderControllerState },
|
|
@@ -356,39 +435,42 @@ function renderProfile(profile) {
|
|
|
356
435
|
{ label: "Provider", render: (row) => `${row.provider_backend || "n/a"} ${row.provider_model || ""}`.trim() },
|
|
357
436
|
{ label: "Failover", render: (row) => `${row.provider_failover_count} failovers / ${row.provider_switch_count} switches` },
|
|
358
437
|
{ label: "Wait", render: (row) => `${row.provider_wait_count} waits / ${row.provider_wait_total_seconds}s` },
|
|
359
|
-
{ label: "Updated", render: (row) => row.updated_at ? `${relativeTime(row.updated_at)}<div class="muted">${row.updated_at}</div>` : "n/a" },
|
|
438
|
+
{ label: "Updated", render: (row) => row.updated_at ? `${relativeTime(row.updated_at)}<div class="muted">${formatCompactDate(row.updated_at)}</div>` : "n/a" },
|
|
360
439
|
],
|
|
361
440
|
profile.resident_controllers,
|
|
362
441
|
"No resident controllers recorded for this profile.",
|
|
363
442
|
);
|
|
364
443
|
|
|
365
|
-
const retryTable =
|
|
444
|
+
const retryTable = renderTableWithPagination(
|
|
445
|
+
`retries-${profile.id}`,
|
|
366
446
|
[
|
|
367
447
|
{ label: "Issue", key: "issue_id" },
|
|
368
448
|
{ label: "Status", render: (row) => `<span class="status-pill ${row.ready ? "" : "waiting-provider"}">${row.ready ? "ready" : "retrying"}</span>` },
|
|
369
449
|
{ label: "Reason", render: (row) => row.last_reason || "n/a" },
|
|
370
450
|
{ label: "Attempts", key: "attempts" },
|
|
371
|
-
{ label: "Next attempt", render: (row) => row.next_attempt_at ? `${relativeTime(row.next_attempt_at)}<div class="muted">${row.next_attempt_at}</div>` : "n/a" },
|
|
451
|
+
{ 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" },
|
|
372
452
|
{ label: "Time Remaining", render: (row) => row.next_attempt_at ? timeRemaining(row.next_attempt_at) : "n/a" },
|
|
373
453
|
],
|
|
374
454
|
profile.issue_retries || [],
|
|
375
455
|
"No issue retries recorded.",
|
|
376
456
|
);
|
|
377
457
|
|
|
378
|
-
const prRetryTable =
|
|
458
|
+
const prRetryTable = renderTableWithPagination(
|
|
459
|
+
`pr-retries-${profile.id}`,
|
|
379
460
|
[
|
|
380
461
|
{ label: "PR", key: "pr_number" },
|
|
381
462
|
{ label: "Status", render: (row) => `<span class="status-pill ${row.ready ? "" : "waiting-provider"}">${row.ready ? "ready" : "retrying"}</span>` },
|
|
382
463
|
{ label: "Reason", render: (row) => row.last_reason || "n/a" },
|
|
383
464
|
{ label: "Attempts", key: "attempts" },
|
|
384
|
-
{ label: "Next attempt", render: (row) => row.next_attempt_at ? `${relativeTime(row.next_attempt_at)}<div class="muted">${row.next_attempt_at}</div>` : "n/a" },
|
|
465
|
+
{ 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" },
|
|
385
466
|
{ label: "Time Remaining", render: (row) => row.next_attempt_at ? timeRemaining(row.next_attempt_at) : "n/a" },
|
|
386
467
|
],
|
|
387
468
|
profile.pr_retries || [],
|
|
388
469
|
"No PR retries recorded.",
|
|
389
470
|
);
|
|
390
471
|
|
|
391
|
-
const workerTable =
|
|
472
|
+
const workerTable = renderTableWithPagination(
|
|
473
|
+
`workers-${profile.id}`,
|
|
392
474
|
[
|
|
393
475
|
{ label: "Key", render: (row) => `<div class="mono">${row.key}</div>` },
|
|
394
476
|
{ label: "Scope", key: "scope" },
|
|
@@ -397,61 +479,66 @@ function renderProfile(profile) {
|
|
|
397
479
|
{ label: "Lane", render: (row) => `${row.resident_lane_kind || "n/a"} / ${row.resident_lane_value || "n/a"}` },
|
|
398
480
|
{ label: "Tasks", key: "task_count" },
|
|
399
481
|
{ label: "Last status", render: (row) => row.last_status || "n/a" },
|
|
400
|
-
{ label: "Last started", render: (row) => row.last_started_at ? `${relativeTime(row.last_started_at)}<div class="muted">${row.last_started_at}</div>` : "n/a" },
|
|
482
|
+
{ 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" },
|
|
401
483
|
],
|
|
402
484
|
profile.resident_workers,
|
|
403
485
|
"No resident worker metadata yet.",
|
|
404
486
|
);
|
|
405
487
|
|
|
406
|
-
const cooldownTable =
|
|
488
|
+
const cooldownTable = renderTableWithPagination(
|
|
489
|
+
`cooldowns-${profile.id}`,
|
|
407
490
|
[
|
|
408
491
|
{ label: "Provider key", render: (row) => `<div class="mono">${row.provider_key}</div>` },
|
|
409
492
|
{ label: "State", render: (row) => `<span class="status-pill ${row.active ? "waiting-provider" : ""}">${row.active ? "cooldown" : "expired"}</span>` },
|
|
410
493
|
{ label: "Reason", render: (row) => row.last_reason || "n/a" },
|
|
411
494
|
{ label: "Attempts", key: "attempts" },
|
|
412
|
-
{ label: "Next attempt", render: (row) => row.next_attempt_at ? `${relativeTime(row.next_attempt_at)}<div class="muted">${row.next_attempt_at}</div>` : "n/a" },
|
|
495
|
+
{ 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" },
|
|
413
496
|
{ label: "Time Remaining", render: (row) => row.next_attempt_at ? timeRemaining(row.next_attempt_at) : "n/a" },
|
|
414
497
|
],
|
|
415
498
|
profile.provider_cooldowns,
|
|
416
499
|
"No provider cooldowns recorded.",
|
|
417
500
|
);
|
|
418
501
|
|
|
419
|
-
const scheduledTable =
|
|
502
|
+
const scheduledTable = renderTableWithPagination(
|
|
503
|
+
`scheduled-${profile.id}`,
|
|
420
504
|
[
|
|
421
505
|
{ label: "Issue", key: "issue_id" },
|
|
422
506
|
{ label: "Interval", render: (row) => `${row.interval_seconds}s` },
|
|
423
|
-
{ label: "Next due", render: (row) => row.next_due_at ? `${relativeTime(row.next_due_at)}<div class="muted">${row.next_due_at}</div>` : "n/a" },
|
|
507
|
+
{ 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" },
|
|
424
508
|
{ label: "Time Remaining", render: (row) => row.next_due_at ? timeRemaining(row.next_due_at) : "n/a" },
|
|
425
|
-
{ label: "Last started", render: (row) => row.last_started_at ? `${relativeTime(row.last_started_at)}<div class="muted">${row.last_started_at}</div>` : "n/a" },
|
|
509
|
+
{ 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" },
|
|
426
510
|
],
|
|
427
511
|
profile.scheduled_issues,
|
|
428
512
|
"No scheduled issue state recorded.",
|
|
429
513
|
);
|
|
430
514
|
|
|
431
|
-
const queueTable =
|
|
515
|
+
const queueTable = renderTableWithPagination(
|
|
516
|
+
`queue-${profile.id}`,
|
|
432
517
|
[
|
|
433
518
|
{ label: "Issue", key: "issue_id" },
|
|
434
519
|
{ label: "Session", render: (row) => row.session ? `<div class="mono">${row.session}</div>` : "n/a" },
|
|
435
520
|
{ label: "Queued by", key: "queued_by" },
|
|
436
|
-
{ label: "Updated", render: (row) => row.updated_at ? `${relativeTime(row.updated_at)}<div class="muted">${row.updated_at}</div>` : "n/a" },
|
|
521
|
+
{ label: "Updated", render: (row) => row.updated_at ? `${relativeTime(row.updated_at)}<div class="muted">${formatCompactDate(row.updated_at)}</div>` : "n/a" },
|
|
437
522
|
],
|
|
438
523
|
profile.issue_queue.pending,
|
|
439
524
|
"No pending leased issues.",
|
|
440
525
|
);
|
|
441
526
|
|
|
442
|
-
const claimsTable =
|
|
527
|
+
const claimsTable = renderTableWithPagination(
|
|
528
|
+
`claims-${profile.id}`,
|
|
443
529
|
[
|
|
444
530
|
{ label: "Issue", key: "issue_id" },
|
|
445
531
|
{ label: "Session", render: (row) => row.session ? `<div class="mono">${row.session}</div>` : "n/a" },
|
|
446
532
|
{ label: "Claimed by", key: "claimer" },
|
|
447
|
-
{ label: "Updated", render: (row) => row.updated_at ? `${relativeTime(row.updated_at)}<div class="muted">${row.updated_at}</div>` : "n/a" },
|
|
533
|
+
{ label: "Updated", render: (row) => row.updated_at ? `${relativeTime(row.updated_at)}<div class="muted">${formatCompactDate(row.updated_at)}</div>` : "n/a" },
|
|
448
534
|
],
|
|
449
535
|
profile.issue_queue.claims || [],
|
|
450
536
|
"No claimed issues.",
|
|
451
537
|
);
|
|
452
538
|
|
|
453
539
|
const githubOutbox = profile.github_outbox || { counts: {}, pending: [] };
|
|
454
|
-
const githubOutboxTable =
|
|
540
|
+
const githubOutboxTable = renderTableWithPagination(
|
|
541
|
+
`github-outbox-${profile.id}`,
|
|
455
542
|
[
|
|
456
543
|
{ label: "Type", render: (row) => row.type || "n/a" },
|
|
457
544
|
{ label: "Target", render: (row) => `${row.kind || row.type || "write"} #${row.number || "?"}` },
|
|
@@ -464,7 +551,7 @@ function renderProfile(profile) {
|
|
|
464
551
|
return row.body_preview || "n/a";
|
|
465
552
|
},
|
|
466
553
|
},
|
|
467
|
-
{ label: "Created", render: (row) => row.created_at ? `${relativeTime(row.created_at)}<div class="muted">${row.created_at}</div>` : "n/a" },
|
|
554
|
+
{ label: "Created", render: (row) => row.created_at ? `${relativeTime(row.created_at)}<div class="muted">${formatCompactDate(row.created_at)}</div>` : "n/a" },
|
|
468
555
|
],
|
|
469
556
|
githubOutbox.pending || [],
|
|
470
557
|
"No pending GitHub write intents.",
|
|
@@ -556,9 +643,9 @@ function renderProfile(profile) {
|
|
|
556
643
|
<h3>Troubleshooting</h3>
|
|
557
644
|
<p class="panel-subtitle">Run diagnostics or debugging tools against this live profile.</p>
|
|
558
645
|
<div class="action-bar">
|
|
559
|
-
<button class="action-btn" onclick="runDoctor('${profile.id}')"
|
|
560
|
-
<button class="action-btn" onclick="exportProfile('${profile.id}')"
|
|
561
|
-
<button class="action-btn" onclick="document.getElementById('import-file-${profile.id}').click()"
|
|
646
|
+
<button class="action-btn" onclick="runDoctor('${profile.id}')">🔧 Run Doctor</button>
|
|
647
|
+
<button class="action-btn" onclick="exportProfile('${profile.id}')">📤 Export</button>
|
|
648
|
+
<button class="action-btn" onclick="document.getElementById('import-file-${profile.id}').click()">📥 Import</button>
|
|
562
649
|
<input type="file" id="import-file-${profile.id}" style="display:none" accept=".json" onchange="importProfile('${profile.id}', this)">
|
|
563
650
|
<span id="doctor-status-${profile.id}"></span>
|
|
564
651
|
</div>
|
|
@@ -639,7 +726,7 @@ async function loadSnapshot() {
|
|
|
639
726
|
}
|
|
640
727
|
|
|
641
728
|
function renderFromSnapshot(snapshot) {
|
|
642
|
-
generatedAtNode.textContent = `Snapshot: ${snapshot.generated_at}`;
|
|
729
|
+
generatedAtNode.textContent = `Snapshot: ${formatCompactDate(snapshot.generated_at)}`;
|
|
643
730
|
renderOverview(snapshot);
|
|
644
731
|
profilesNode.innerHTML = snapshot.profiles.map(renderProfile).join("");
|
|
645
732
|
}
|
|
@@ -716,7 +803,6 @@ async function importProfile(profileId, inputEl) {
|
|
|
716
803
|
const result = await response.json();
|
|
717
804
|
if (response.ok) {
|
|
718
805
|
alert(`Profile ${profileId} imported successfully!`);
|
|
719
|
-
// Refresh the page to show imported profile
|
|
720
806
|
setTimeout(() => window.location.reload(), 1000);
|
|
721
807
|
} else {
|
|
722
808
|
alert(`Import failed: ${result.error || response.status}`);
|
|
@@ -750,9 +836,14 @@ function connectWebSocket() {
|
|
|
750
836
|
console.log("ACP Dashboard: WebSocket connected");
|
|
751
837
|
};
|
|
752
838
|
|
|
753
|
-
ws.onmessage = (event) => {
|
|
839
|
+
ws.onmessage = async (event) => {
|
|
754
840
|
try {
|
|
755
|
-
|
|
841
|
+
let data = event.data;
|
|
842
|
+
// Handle Blob (binary) or string
|
|
843
|
+
if (data instanceof Blob) {
|
|
844
|
+
data = await data.text();
|
|
845
|
+
}
|
|
846
|
+
const snapshot = JSON.parse(data);
|
|
756
847
|
window._acpSnapshot = snapshot;
|
|
757
848
|
renderFromSnapshot(snapshot);
|
|
758
849
|
maybeNotifyAlerts(snapshot);
|