code-coordinator 0.5.46__py3-none-any.whl
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.
- code_coordinator-0.5.46.dist-info/METADATA +625 -0
- code_coordinator-0.5.46.dist-info/RECORD +295 -0
- code_coordinator-0.5.46.dist-info/WHEEL +5 -0
- code_coordinator-0.5.46.dist-info/entry_points.txt +2 -0
- code_coordinator-0.5.46.dist-info/licenses/LICENSE +110 -0
- code_coordinator-0.5.46.dist-info/top_level.txt +1 -0
- coord/__init__.py +176 -0
- coord/_board_mapping.py +229 -0
- coord/acceptance.py +468 -0
- coord/acceptance_drivers.py +632 -0
- coord/agent.py +7517 -0
- coord/agent_app.py +1555 -0
- coord/agent_update.py +417 -0
- coord/agents/opencode/.gitignore +13 -0
- coord/agents/opencode/agents/work.md +129 -0
- coord/agents/opencode/routing.jsonc +49 -0
- coord/audit.py +301 -0
- coord/auto_loop.py +1440 -0
- coord/board_bool_guard.py +72 -0
- coord/board_service.py +141 -0
- coord/board_wire.py +309 -0
- coord/brain.py +581 -0
- coord/branch_model.py +214 -0
- coord/cargo_cache.py +258 -0
- coord/ci_github.py +386 -0
- coord/ci_store.py +560 -0
- coord/claim.py +353 -0
- coord/cli.py +454 -0
- coord/client.py +610 -0
- coord/commands/__init__.py +1 -0
- coord/commands/_common.py +329 -0
- coord/commands/acceptance.py +916 -0
- coord/commands/agent_ops.py +1339 -0
- coord/commands/audit.py +131 -0
- coord/commands/chat.py +320 -0
- coord/commands/dispatch.py +1780 -0
- coord/commands/dispatch_workers.py +4894 -0
- coord/commands/drive.py +616 -0
- coord/commands/drive_queue.py +1203 -0
- coord/commands/gate_a.py +217 -0
- coord/commands/gates.py +89 -0
- coord/commands/issues.py +681 -0
- coord/commands/lifecycle.py +513 -0
- coord/commands/merge.py +1900 -0
- coord/commands/milestone.py +2081 -0
- coord/commands/plan_followup.py +1243 -0
- coord/commands/plans.py +156 -0
- coord/commands/release.py +2232 -0
- coord/commands/report.py +341 -0
- coord/commands/review.py +1523 -0
- coord/commands/scorecard.py +252 -0
- coord/commands/sessions.py +1930 -0
- coord/commands/setup.py +576 -0
- coord/commands/status.py +2089 -0
- coord/commands/terminal.py +385 -0
- coord/commands/test_gate.py +775 -0
- coord/commands/tui.py +288 -0
- coord/comments.py +718 -0
- coord/config.py +3032 -0
- coord/conflict_fix.py +633 -0
- coord/dao.py +483 -0
- coord/dashboard/__init__.py +0 -0
- coord/dashboard/fixture.py +376 -0
- coord/dashboard/index.html +658 -0
- coord/dashboard/server.py +1894 -0
- coord/dashboard/terminal.py +382 -0
- coord/dashboard/webapp/.gitignore +9 -0
- coord/dashboard/webapp/components.json +17 -0
- coord/dashboard/webapp/dist/assets/Gallery-da3qNiIw.js +71 -0
- coord/dashboard/webapp/dist/assets/Terminal-9CEnUXvW.css +32 -0
- coord/dashboard/webapp/dist/assets/Terminal-skVFCxPU.js +63 -0
- coord/dashboard/webapp/dist/assets/index-DltfZR5f.js +184 -0
- coord/dashboard/webapp/dist/assets/index-Dq4kwTdw.css +1 -0
- coord/dashboard/webapp/dist/assets/workbox-window.prod.es5-BqEJf4Xk.js +2 -0
- coord/dashboard/webapp/dist/icons/icon-192.png +0 -0
- coord/dashboard/webapp/dist/icons/icon-512.png +0 -0
- coord/dashboard/webapp/dist/icons/icon.svg +5 -0
- coord/dashboard/webapp/dist/index.html +38 -0
- coord/dashboard/webapp/dist/manifest.webmanifest +1 -0
- coord/dashboard/webapp/dist/sw.js +1 -0
- coord/dashboard/webapp/dist/workbox-e4022e15.js +1 -0
- coord/dashboard/webapp/e2e/available-gates-terminal.spec.ts +75 -0
- coord/dashboard/webapp/e2e/deep-link.spec.ts +172 -0
- coord/dashboard/webapp/e2e/fixtureServer.ts +155 -0
- coord/dashboard/webapp/e2e/live-update-fixture.spec.ts +113 -0
- coord/dashboard/webapp/e2e/realtime.spec.ts +238 -0
- coord/dashboard/webapp/e2e/shell.spec.ts +309 -0
- coord/dashboard/webapp/e2e/smoke.spec.ts +191 -0
- coord/dashboard/webapp/e2e/terminal.spec.ts +420 -0
- coord/dashboard/webapp/e2e/theme.spec.ts +138 -0
- coord/dashboard/webapp/eslint.config.js +20 -0
- coord/dashboard/webapp/index.html +37 -0
- coord/dashboard/webapp/node_modules/flatted/python/flatted.py +144 -0
- coord/dashboard/webapp/package-lock.json +10584 -0
- coord/dashboard/webapp/package.json +63 -0
- coord/dashboard/webapp/playwright.acceptance.config.ts +166 -0
- coord/dashboard/webapp/playwright.config.ts +93 -0
- coord/dashboard/webapp/postcss.config.js +6 -0
- coord/dashboard/webapp/public/icons/icon-192.png +0 -0
- coord/dashboard/webapp/public/icons/icon-512.png +0 -0
- coord/dashboard/webapp/public/icons/icon.svg +5 -0
- coord/dashboard/webapp/src/App.tsx +140 -0
- coord/dashboard/webapp/src/api/client.ts +199 -0
- coord/dashboard/webapp/src/api/generated.ts +176 -0
- coord/dashboard/webapp/src/components/ConnectionBadge.tsx +52 -0
- coord/dashboard/webapp/src/components/Detail.tsx +800 -0
- coord/dashboard/webapp/src/components/Gallery.tsx +341 -0
- coord/dashboard/webapp/src/components/Home.tsx +435 -0
- coord/dashboard/webapp/src/components/MobileKeyBar.tsx +280 -0
- coord/dashboard/webapp/src/components/PanelHeader.tsx +59 -0
- coord/dashboard/webapp/src/components/PipelineCard.tsx +168 -0
- coord/dashboard/webapp/src/components/SessionCard.tsx +99 -0
- coord/dashboard/webapp/src/components/SessionDetail.tsx +140 -0
- coord/dashboard/webapp/src/components/SessionsList.tsx +81 -0
- coord/dashboard/webapp/src/components/Terminal.tsx +376 -0
- coord/dashboard/webapp/src/components/__tests__/ConnectionBadge.test.tsx +81 -0
- coord/dashboard/webapp/src/components/__tests__/Detail.test.tsx +680 -0
- coord/dashboard/webapp/src/components/__tests__/Gallery.test.tsx +83 -0
- coord/dashboard/webapp/src/components/__tests__/Home.test.tsx +271 -0
- coord/dashboard/webapp/src/components/__tests__/MobileKeyBar.test.tsx +197 -0
- coord/dashboard/webapp/src/components/__tests__/PipelineCard.test.tsx +143 -0
- coord/dashboard/webapp/src/components/__tests__/SessionCard.test.tsx +106 -0
- coord/dashboard/webapp/src/components/__tests__/Terminal.test.tsx +504 -0
- coord/dashboard/webapp/src/components/ui/badge.tsx +41 -0
- coord/dashboard/webapp/src/components/ui/button.tsx +54 -0
- coord/dashboard/webapp/src/components/ui/card.tsx +55 -0
- coord/dashboard/webapp/src/components/ui/dialog.tsx +99 -0
- coord/dashboard/webapp/src/components/ui/dropdown-menu.tsx +189 -0
- coord/dashboard/webapp/src/components/ui/empty-state.tsx +35 -0
- coord/dashboard/webapp/src/components/ui/sheet.tsx +123 -0
- coord/dashboard/webapp/src/components/ui/skeleton.tsx +9 -0
- coord/dashboard/webapp/src/components/ui/tabs.tsx +55 -0
- coord/dashboard/webapp/src/components/ui/theme-provider.tsx +78 -0
- coord/dashboard/webapp/src/components/ui/theme-toggle.tsx +20 -0
- coord/dashboard/webapp/src/components/ui/toast.tsx +123 -0
- coord/dashboard/webapp/src/components/ui/toaster.tsx +30 -0
- coord/dashboard/webapp/src/components/ui/tooltip.tsx +26 -0
- coord/dashboard/webapp/src/components/ui/use-toast.ts +134 -0
- coord/dashboard/webapp/src/index.css +210 -0
- coord/dashboard/webapp/src/lib/pipeline.ts +29 -0
- coord/dashboard/webapp/src/lib/utils.ts +6 -0
- coord/dashboard/webapp/src/main.tsx +46 -0
- coord/dashboard/webapp/src/realtime/RealtimeProvider.tsx +112 -0
- coord/dashboard/webapp/src/realtime/__tests__/RealtimeProvider.test.tsx +189 -0
- coord/dashboard/webapp/src/realtime/__tests__/connection.test.ts +255 -0
- coord/dashboard/webapp/src/realtime/connection.ts +227 -0
- coord/dashboard/webapp/src/realtime/events.ts +100 -0
- coord/dashboard/webapp/src/routes/__tests__/paths.test.ts +92 -0
- coord/dashboard/webapp/src/routes/paths.ts +92 -0
- coord/dashboard/webapp/src/shell/ActivityRail.tsx +335 -0
- coord/dashboard/webapp/src/shell/AppShell.tsx +276 -0
- coord/dashboard/webapp/src/shell/ComingSoon.tsx +33 -0
- coord/dashboard/webapp/src/shell/EmptyDetail.tsx +26 -0
- coord/dashboard/webapp/src/shell/RouteNotFound.tsx +33 -0
- coord/dashboard/webapp/src/shell/ShellLayout.tsx +147 -0
- coord/dashboard/webapp/src/shell/StatusBar.tsx +46 -0
- coord/dashboard/webapp/src/shell/__tests__/ShellLayout.test.tsx +520 -0
- coord/dashboard/webapp/src/shell/__tests__/shellState.test.ts +95 -0
- coord/dashboard/webapp/src/shell/__tests__/stubViewport.ts +40 -0
- coord/dashboard/webapp/src/shell/breakpoints.ts +87 -0
- coord/dashboard/webapp/src/shell/railItems.ts +105 -0
- coord/dashboard/webapp/src/shell/shellState.ts +174 -0
- coord/dashboard/webapp/src/shell/useRegionFocus.ts +95 -0
- coord/dashboard/webapp/src/test-setup.ts +41 -0
- coord/dashboard/webapp/src/vite-env.d.ts +2 -0
- coord/dashboard/webapp/tailwind.config.js +140 -0
- coord/dashboard/webapp/tsconfig.json +25 -0
- coord/dashboard/webapp/tsconfig.node.json +11 -0
- coord/dashboard/webapp/vite.config.ts +71 -0
- coord/db.py +1076 -0
- coord/dead_end.py +332 -0
- coord/deploy/README.md +33 -0
- coord/deploy/coord-agent.service +89 -0
- coord/deploy/coord-db-backup.service +60 -0
- coord/deploy/coord-db-backup.sh +74 -0
- coord/deploy/coord-db-backup.timer +18 -0
- coord/deploy/coord-drive-queue.service +117 -0
- coord/deploy/coord-drive-queue.timer +39 -0
- coord/deploy/coord-notify.service +48 -0
- coord/deploy/coord-notify.timer +24 -0
- coord/deploy/coord-release-propagate.service +83 -0
- coord/deploy/coord-release-propagate.timer +38 -0
- coord/deploy/coord-release-window.service +119 -0
- coord/deploy/coord-release-window.timer +36 -0
- coord/deploy/coord-serve.service +82 -0
- coord/deploy/coord-web-dist-build.service +43 -0
- coord/deploy/coord-web-dist-build.timer +36 -0
- coord/deploy/coord-web.service +125 -0
- coord/deploy_manifest.py +80 -0
- coord/deploy_units.py +384 -0
- coord/deps.py +115 -0
- coord/diagnose.py +1623 -0
- coord/dispatch.py +1009 -0
- coord/dist_name.py +123 -0
- coord/drive.py +3101 -0
- coord/drive_queue.py +2298 -0
- coord/drive_state.py +870 -0
- coord/events.py +381 -0
- coord/failure_class.py +914 -0
- coord/filelock.py +168 -0
- coord/fleet_config_health.py +300 -0
- coord/freshness.py +206 -0
- coord/gate_a.py +469 -0
- coord/gate_b.py +411 -0
- coord/gate_snapshot.py +385 -0
- coord/gates.py +582 -0
- coord/github_ops.py +1954 -0
- coord/goal.py +125 -0
- coord/graph_health.py +348 -0
- coord/health/__init__.py +69 -0
- coord/health/aggregate.py +129 -0
- coord/health/checks/__init__.py +13 -0
- coord/health/checks/agent_install.py +280 -0
- coord/health/checks/cargo_targets.py +171 -0
- coord/health/checks/claude_binary.py +65 -0
- coord/health/checks/deploy_lane_facts.py +458 -0
- coord/health/checks/disk.py +99 -0
- coord/health/checks/fleet_board.py +89 -0
- coord/health/checks/fleet_deploy_lanes.py +469 -0
- coord/health/checks/fleet_phantom.py +69 -0
- coord/health/checks/fleet_unit_drift.py +151 -0
- coord/health/checks/graph.py +192 -0
- coord/health/checks/plan_usage.py +88 -0
- coord/health/checks/repo_state.py +161 -0
- coord/health/checks/spawned_coord.py +465 -0
- coord/health/checks/timer_active.py +254 -0
- coord/health/checks/toolchain.py +547 -0
- coord/health/checks/unit_drift.py +648 -0
- coord/health/checks/unit_enablement.py +171 -0
- coord/health/checks/worktrees.py +96 -0
- coord/health/cli.py +121 -0
- coord/health/context.py +106 -0
- coord/health/fleet_snapshot.py +477 -0
- coord/health/models.py +250 -0
- coord/health/pypi.py +231 -0
- coord/health/registry.py +240 -0
- coord/health/render.py +82 -0
- coord/health/units.py +60 -0
- coord/hooks.py +106 -0
- coord/housekeeping.py +204 -0
- coord/interactive.py +4286 -0
- coord/issue_store.py +1496 -0
- coord/liveness_auditor.py +293 -0
- coord/machine_pause.py +755 -0
- coord/merge_queue.py +4681 -0
- coord/milestone_chat.py +600 -0
- coord/milestone_dispatch.py +943 -0
- coord/milestone_gate.py +709 -0
- coord/milestone_order.py +840 -0
- coord/mock_author.py +334 -0
- coord/models.py +891 -0
- coord/network.py +269 -0
- coord/new_issue_chat.py +229 -0
- coord/notify.py +3226 -0
- coord/openapi.py +404 -0
- coord/overlap_fence.py +133 -0
- coord/parentage.py +200 -0
- coord/parentage_github.py +58 -0
- coord/pipeline.py +481 -0
- coord/plan_parser.py +266 -0
- coord/plans.py +543 -0
- coord/platform_paths.py +43 -0
- coord/pr_body_lint.py +67 -0
- coord/prereqs.py +533 -0
- coord/progress.py +425 -0
- coord/providers/__init__.py +683 -0
- coord/providers/base.py +218 -0
- coord/providers/claude.py +284 -0
- coord/providers/claude_pty.py +610 -0
- coord/providers/opencode.py +896 -0
- coord/reconcile.py +2233 -0
- coord/refine_chat.py +485 -0
- coord/release_cordon.py +525 -0
- coord/release_propagate.py +1176 -0
- coord/release_verify.py +777 -0
- coord/release_window.py +322 -0
- coord/reports.py +1643 -0
- coord/revalidate.py +1101 -0
- coord/review.py +3317 -0
- coord/scorecard.py +484 -0
- coord/serve_app.py +7192 -0
- coord/skills/update-issue/SKILL.md +93 -0
- coord/smoke.py +1030 -0
- coord/split_work.py +210 -0
- coord/stage_projection.py +650 -0
- coord/state.py +5720 -0
- coord/test_author.py +1064 -0
- coord/test_chat.py +352 -0
- coord/test_orchestrator.py +494 -0
- coord/test_report.py +178 -0
- coord/tui_release.py +271 -0
- coord/usage.py +753 -0
- coord/usage_limits.py +358 -0
- coord/usage_rollup.py +709 -0
- coord/worker_events.py +954 -0
|
@@ -0,0 +1,658 @@
|
|
|
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>coord — dashboard</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
+
body { font-family: -apple-system, system-ui, sans-serif; background: #0d1117; color: #c9d1d9; line-height: 1.5; }
|
|
10
|
+
.container { max-width: 900px; margin: 0 auto; padding: 12px; }
|
|
11
|
+
h1 { font-size: 1.2em; color: #58a6ff; margin-bottom: 12px; }
|
|
12
|
+
h2 { font-size: 1em; color: #8b949e; margin: 16px 0 8px; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
13
|
+
|
|
14
|
+
.card { background: #161b22; border: 1px solid #30363d; border-radius: 6px; padding: 12px; margin-bottom: 8px; }
|
|
15
|
+
.card-header { font-weight: 600; color: #e6edf3; }
|
|
16
|
+
.badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 0.8em; font-weight: 600; }
|
|
17
|
+
.badge-online { background: #238636; color: #fff; }
|
|
18
|
+
.badge-offline { background: #da3633; color: #fff; }
|
|
19
|
+
.badge-busy { background: #d29922; color: #000; }
|
|
20
|
+
.badge-done { background: #238636; color: #fff; }
|
|
21
|
+
.badge-failed { background: #da3633; color: #fff; }
|
|
22
|
+
.badge-running { background: #1f6feb; color: #fff; }
|
|
23
|
+
.meta { color: #8b949e; font-size: 0.85em; }
|
|
24
|
+
a { color: #58a6ff; }
|
|
25
|
+
|
|
26
|
+
.tabs { display: flex; gap: 4px; margin-bottom: 12px; flex-wrap: wrap; }
|
|
27
|
+
.tab { padding: 8px 16px; background: #21262d; border: 1px solid #30363d; border-radius: 6px; cursor: pointer; color: #c9d1d9; font-size: 0.9em; min-height: 44px; }
|
|
28
|
+
.tab.active { background: #1f6feb; border-color: #1f6feb; color: #fff; }
|
|
29
|
+
|
|
30
|
+
.pane { display: none; }
|
|
31
|
+
.pane.active { display: block; }
|
|
32
|
+
|
|
33
|
+
.chat-container { display: flex; flex-direction: column; height: 50vh; }
|
|
34
|
+
.chat-messages { flex: 1; overflow-y: auto; padding: 8px; background: #0d1117; border: 1px solid #30363d; border-radius: 6px; margin-bottom: 8px; }
|
|
35
|
+
.chat-msg { margin-bottom: 8px; padding: 8px; border-radius: 6px; white-space: pre-wrap; word-break: break-word; }
|
|
36
|
+
.chat-msg.user { background: #1f6feb; color: #fff; margin-left: 20%; }
|
|
37
|
+
.chat-msg.assistant { background: #161b22; margin-right: 20%; }
|
|
38
|
+
.chat-input { display: flex; gap: 8px; }
|
|
39
|
+
.chat-input input { flex: 1; padding: 10px; background: #0d1117; border: 1px solid #30363d; border-radius: 6px; color: #c9d1d9; font-size: 16px; min-height: 44px; }
|
|
40
|
+
.chat-input button { padding: 10px 20px; background: #238636; border: none; border-radius: 6px; color: #fff; cursor: pointer; font-size: 16px; min-height: 44px; }
|
|
41
|
+
|
|
42
|
+
.proposal { border-left: 3px solid #1f6feb; padding-left: 10px; margin-bottom: 12px; padding-bottom: 8px; }
|
|
43
|
+
.btn { padding: 8px 16px; border: none; border-radius: 6px; color: #fff; cursor: pointer; font-size: 14px; min-height: 44px; margin-right: 6px; margin-top: 6px; }
|
|
44
|
+
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
45
|
+
.btn-approve { background: #238636; }
|
|
46
|
+
.btn-reject { background: #da3633; }
|
|
47
|
+
.btn-all { background: #1f6feb; }
|
|
48
|
+
.btn-small { padding: 4px 10px; min-height: 32px; font-size: 0.8em; }
|
|
49
|
+
|
|
50
|
+
.briefing-edit { width: 100%; min-height: 60px; margin-top: 6px; padding: 8px; background: #0d1117; border: 1px solid #30363d; border-radius: 6px; color: #c9d1d9; font-family: monospace; font-size: 13px; resize: vertical; }
|
|
51
|
+
|
|
52
|
+
.diff-toggle { cursor: pointer; color: #58a6ff; font-size: 0.85em; margin-top: 4px; }
|
|
53
|
+
.diff-content { display: none; margin-top: 6px; padding: 8px; background: #0d1117; border: 1px solid #30363d; border-radius: 6px; font-family: monospace; font-size: 12px; white-space: pre-wrap; overflow-x: auto; max-height: 400px; overflow-y: auto; }
|
|
54
|
+
.diff-content.open { display: block; }
|
|
55
|
+
|
|
56
|
+
.refresh-btn { float: right; padding: 4px 12px; background: #21262d; border: 1px solid #30363d; border-radius: 6px; color: #8b949e; cursor: pointer; font-size: 0.85em; }
|
|
57
|
+
|
|
58
|
+
.pipeline-bar { display: flex; gap: 4px; flex-wrap: wrap; margin-top: 6px; align-items: center; }
|
|
59
|
+
.pipeline-pill { padding: 2px 8px; border-radius: 10px; font-size: 0.75em; font-weight: 600; }
|
|
60
|
+
.pill-completed { background: #238636; color: #fff; }
|
|
61
|
+
.pill-active { background: #1f6feb; color: #fff; }
|
|
62
|
+
.pill-waiting { background: #21262d; color: #8b949e; border: 1px solid #30363d; }
|
|
63
|
+
.pill-skipped { background: transparent; color: #484f58; font-size: 0.7em; text-decoration: line-through; }
|
|
64
|
+
.pipeline-arrow { color: #484f58; font-size: 0.8em; padding: 0 1px; }
|
|
65
|
+
.pipeline-progress { height: 3px; background: #30363d; border-radius: 2px; margin-top: 4px; overflow: hidden; }
|
|
66
|
+
.pipeline-progress-fill { height: 100%; background: #1f6feb; border-radius: 2px; transition: width 0.3s; }
|
|
67
|
+
.gate-row { margin-top: 6px; display: flex; gap: 4px; flex-wrap: wrap; }
|
|
68
|
+
.btn-gate { padding: 4px 10px; min-height: 28px; font-size: 0.78em; border-radius: 4px; border: none; color: #fff; cursor: pointer; background: #1f6feb; }
|
|
69
|
+
.btn-gate:hover { opacity: 0.85; }
|
|
70
|
+
.btn-gate:disabled { opacity: 0.45; cursor: not-allowed; }
|
|
71
|
+
.btn-gate-danger { background: #da3633; }
|
|
72
|
+
.btn-gate-green { background: #238636; }
|
|
73
|
+
.btn-gate-warn { background: #d29922; color: #000; }
|
|
74
|
+
.pill-warn { background: #d29922; color: #000; }
|
|
75
|
+
|
|
76
|
+
.dispatch-status { margin-top: 6px; padding: 4px 8px; border-radius: 4px; font-size: 0.82em; font-family: monospace; word-break: break-word; }
|
|
77
|
+
.dispatch-pending { background: #161b22; color: #8b949e; border: 1px solid #30363d; }
|
|
78
|
+
.dispatch-ok { background: #0d2b16; color: #3fb950; border: 1px solid #238636; }
|
|
79
|
+
.dispatch-err { background: #2d0e0e; color: #ff7b72; border: 1px solid #da3633; }
|
|
80
|
+
.dispatch-err-toggle { cursor: pointer; color: #58a6ff; font-size: 0.85em; margin-left: 8px; font-style: italic; }
|
|
81
|
+
.dispatch-err-detail { margin-top: 6px; padding: 6px; background: #0d1117; border: 1px solid #30363d; border-radius: 4px; white-space: pre-wrap; word-break: break-all; font-size: 0.9em; max-height: 150px; overflow-y: auto; display: none; }
|
|
82
|
+
.dispatch-err-detail.open { display: block; }
|
|
83
|
+
|
|
84
|
+
/* ── Toast notifications ─────────────────────────────────────────────── */
|
|
85
|
+
#toast-container { position: fixed; top: 16px; right: 16px; z-index: 9999; display: flex; flex-direction: column; gap: 8px; max-width: 440px; pointer-events: none; }
|
|
86
|
+
.toast { padding: 12px 16px; border-radius: 8px; font-size: 0.88em; font-weight: 500; cursor: pointer; pointer-events: all; border-left: 4px solid; line-height: 1.4; box-shadow: 0 4px 12px rgba(0,0,0,0.4); animation: toast-slide-in 0.2s ease; }
|
|
87
|
+
.toast-done { background: #0d2b16; color: #3fb950; border-color: #238636; }
|
|
88
|
+
.toast-failed { background: #2d0e0e; color: #ff7b72; border-color: #da3633; }
|
|
89
|
+
.toast-stuck { background: #2b1a00; color: #e3b341; border-color: #d29922; }
|
|
90
|
+
.toast-dismiss { float: right; opacity: 0.6; margin-left: 12px; font-size: 1.1em; line-height: 1; }
|
|
91
|
+
.toast-dismiss:hover { opacity: 1; }
|
|
92
|
+
@keyframes toast-slide-in { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
|
|
93
|
+
.toast.toast-fade-out { animation: toast-fade-out 0.4s ease forwards; }
|
|
94
|
+
@keyframes toast-fade-out { to { opacity: 0; transform: translateX(20px); } }
|
|
95
|
+
|
|
96
|
+
/* ── Possibly-stuck warning on active cards ──────────────────────────── */
|
|
97
|
+
.stuck-banner { margin-top: 8px; padding: 8px 10px; background: #2b1a00; border: 1px solid #d29922; border-radius: 6px; color: #e3b341; font-size: 0.82em; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
98
|
+
.stuck-banner .stuck-msg { flex: 1; }
|
|
99
|
+
.btn-unstick { padding: 3px 10px; min-height: 26px; font-size: 0.78em; border-radius: 4px; border: none; color: #000; background: #d29922; cursor: pointer; }
|
|
100
|
+
.btn-unstick:hover { background: #e3b341; }
|
|
101
|
+
|
|
102
|
+
/* ── SSE connection status indicator ─────────────────────────────────── */
|
|
103
|
+
/* Bug 6: audio bell toggle button */
|
|
104
|
+
.bell-btn { background: none; border: none; cursor: pointer; font-size: 0.9em; padding: 0 2px; vertical-align: middle; opacity: 0.7; line-height: 1; }
|
|
105
|
+
.bell-btn:hover { opacity: 1; }
|
|
106
|
+
/* Bug 4 fix: `title` is an HTML attribute, not a CSS property — removed. */
|
|
107
|
+
.sse-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: #484f58; margin-right: 4px; vertical-align: middle; }
|
|
108
|
+
.sse-dot.connected { background: #238636; }
|
|
109
|
+
|
|
110
|
+
@media (max-width: 600px) {
|
|
111
|
+
.chat-msg.user { margin-left: 10%; }
|
|
112
|
+
.chat-msg.assistant { margin-right: 10%; }
|
|
113
|
+
}
|
|
114
|
+
</style>
|
|
115
|
+
</head>
|
|
116
|
+
<body>
|
|
117
|
+
<div id="toast-container"></div>
|
|
118
|
+
<div class="container">
|
|
119
|
+
<h1>coord dashboard <span class="meta" id="round-info"></span> <span class="sse-dot" id="sse-dot" title="Live events disconnected"></span><button class="bell-btn" id="bell-btn" onclick="toggleBell()" title="Toggle audio notifications on failure"></button></h1>
|
|
120
|
+
|
|
121
|
+
<div class="tabs">
|
|
122
|
+
<div class="tab active" data-pane="board">Board</div>
|
|
123
|
+
<div class="tab" data-pane="machines">Machines</div>
|
|
124
|
+
<div class="tab" data-pane="proposals">Proposals</div>
|
|
125
|
+
<div class="tab" data-pane="chat">Chat</div>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<div class="pane active" id="pane-board">
|
|
129
|
+
<button class="refresh-btn" onclick="loadBoard()">refresh</button>
|
|
130
|
+
<h2>Active</h2>
|
|
131
|
+
<div id="active-list"></div>
|
|
132
|
+
<h2>Completed (recent)</h2>
|
|
133
|
+
<div id="completed-list"></div>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<div class="pane" id="pane-machines">
|
|
137
|
+
<button class="refresh-btn" onclick="loadMachines()">refresh</button>
|
|
138
|
+
<div id="machines-list"></div>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<div class="pane" id="pane-proposals">
|
|
142
|
+
<button class="refresh-btn" onclick="loadProposals()">refresh</button>
|
|
143
|
+
<div id="proposals-list"></div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<div class="pane" id="pane-chat">
|
|
147
|
+
<div class="chat-container">
|
|
148
|
+
<div class="chat-messages" id="chat-messages"></div>
|
|
149
|
+
<div class="chat-input">
|
|
150
|
+
<input type="text" id="chat-input" placeholder="Ask about the board..." autocomplete="off">
|
|
151
|
+
<button onclick="sendChat()">Send</button>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<script>
|
|
158
|
+
const E = s => { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; };
|
|
159
|
+
|
|
160
|
+
document.querySelectorAll('.tab').forEach(tab => {
|
|
161
|
+
tab.addEventListener('click', () => {
|
|
162
|
+
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
163
|
+
document.querySelectorAll('.pane').forEach(p => p.classList.remove('active'));
|
|
164
|
+
tab.classList.add('active');
|
|
165
|
+
document.getElementById('pane-' + tab.dataset.pane).classList.add('active');
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
function badge(cls, text) {
|
|
170
|
+
return `<span class="badge badge-${E(cls)}">${E(text)}</span>`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── Pipeline ───────────────────────────────────────────────────────────
|
|
174
|
+
let pipelineCache = {}; // assignment_id → PipelineView
|
|
175
|
+
const dispatchStatus = {}; // assignment_id → {phase:'pending'|'ok'|'err', msg, detail?}
|
|
176
|
+
|
|
177
|
+
function renderDispatchStatus(aid) {
|
|
178
|
+
const ds = dispatchStatus[aid];
|
|
179
|
+
if (!ds) return '';
|
|
180
|
+
const clsMap = {pending: 'dispatch-pending', ok: 'dispatch-ok', err: 'dispatch-err'};
|
|
181
|
+
const cls = clsMap[ds.phase] || '';
|
|
182
|
+
let html = `<div class="dispatch-status ${cls}">${E(ds.msg)}`;
|
|
183
|
+
if (ds.detail) {
|
|
184
|
+
const detailId = 'dsdetail-' + aid;
|
|
185
|
+
html += `<span class="dispatch-err-toggle" onclick="toggleDispatchDetail(${JSON.stringify(aid)})">show details</span><div class="dispatch-err-detail" id="${detailId}">${E(ds.detail)}</div>`;
|
|
186
|
+
}
|
|
187
|
+
html += '</div>';
|
|
188
|
+
return html;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function toggleDispatchDetail(aid) {
|
|
192
|
+
const el = document.getElementById('dsdetail-' + aid);
|
|
193
|
+
if (el) el.classList.toggle('open');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function updateCardPipeline(aid, issueNum) {
|
|
197
|
+
const area = document.querySelector('#card-' + aid + ' .pipeline-area');
|
|
198
|
+
if (!area) return;
|
|
199
|
+
const pv = pipelineCache[aid];
|
|
200
|
+
area.innerHTML = pv ? renderPipelineBar(pv, issueNum) : renderDispatchStatus(aid);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function loadPipeline() {
|
|
204
|
+
try {
|
|
205
|
+
const r = await fetch('/api/pipeline');
|
|
206
|
+
if (r.ok) {
|
|
207
|
+
const data = await r.json();
|
|
208
|
+
pipelineCache = {};
|
|
209
|
+
for (const pv of data) pipelineCache[pv.assignment_id] = pv;
|
|
210
|
+
}
|
|
211
|
+
} catch(e) { /* fail silently — board still renders without pipeline data */ }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function renderPipelineBar(pv, issueNum) {
|
|
215
|
+
if (!pv) return '';
|
|
216
|
+
const pills = pv.stages.map(s => {
|
|
217
|
+
let cls = 'pill-waiting';
|
|
218
|
+
// If review findings are pending, show the review pill with a warning colour.
|
|
219
|
+
if (s.name === 'review' && pv.review_findings_pending) cls = 'pill-warn';
|
|
220
|
+
else if (s.status === 'completed') cls = 'pill-completed';
|
|
221
|
+
else if (s.status === 'active') cls = 'pill-active';
|
|
222
|
+
else if (s.status === 'skipped') cls = 'pill-skipped';
|
|
223
|
+
const label = (s.name === 'review' && pv.review_findings_pending)
|
|
224
|
+
? `⚠ ${E(s.name)}` : E(s.name);
|
|
225
|
+
return `<span class="pipeline-pill ${cls}" title="${pv.review_findings_pending && s.name === 'review' ? 'Review findings not posted to GitHub' : ''}">${label}</span>`;
|
|
226
|
+
}).join('<span class="pipeline-arrow">›</span>');
|
|
227
|
+
|
|
228
|
+
const isPending = dispatchStatus[pv.assignment_id]?.phase === 'pending';
|
|
229
|
+
const gates = pv.available_gates.map(g => {
|
|
230
|
+
let extra = '';
|
|
231
|
+
if (g.action === 'retry' || g.action === 'dispatch_fix') extra = 'btn-gate-danger';
|
|
232
|
+
else if (g.action === 'merge') extra = 'btn-gate-green';
|
|
233
|
+
else if (g.action === 'post_findings') extra = 'btn-gate-warn';
|
|
234
|
+
const dis = isPending ? ' disabled' : '';
|
|
235
|
+
return `<button class="btn-gate ${extra}"${dis} onclick="doPipelineAction(${JSON.stringify(pv.assignment_id)},${JSON.stringify(g.action)},${JSON.stringify(issueNum || null)})">${E(g.label)}</button>`;
|
|
236
|
+
}).join('');
|
|
237
|
+
|
|
238
|
+
const pct = Math.max(0, Math.min(100, pv.progress_pct || 0));
|
|
239
|
+
const progress = `<div class="pipeline-progress"><div class="pipeline-progress-fill" style="width:${pct}%"></div></div>`;
|
|
240
|
+
|
|
241
|
+
return `<div class="pipeline-bar">${pills}</div>${gates ? `<div class="gate-row">${gates}</div>` : ''}${renderDispatchStatus(pv.assignment_id)}${progress}`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function doPipelineAction(assignmentId, action, issueNum) {
|
|
245
|
+
// Show "Dispatching…" immediately — persistent, not a toast.
|
|
246
|
+
dispatchStatus[assignmentId] = {
|
|
247
|
+
phase: 'pending',
|
|
248
|
+
msg: issueNum ? `Dispatching #${issueNum}…` : 'Dispatching…',
|
|
249
|
+
};
|
|
250
|
+
updateCardPipeline(assignmentId, issueNum);
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
const r = await fetch('/api/pipeline/action', {
|
|
254
|
+
method: 'POST',
|
|
255
|
+
headers: {'Content-Type': 'application/json'},
|
|
256
|
+
body: JSON.stringify({assignment_id: assignmentId, action}),
|
|
257
|
+
});
|
|
258
|
+
const data = await r.json();
|
|
259
|
+
if (!r.ok || data.ok === false) {
|
|
260
|
+
const errText = data.error || JSON.stringify(data, null, 2);
|
|
261
|
+
const summary = errText.length > 120 ? errText.slice(0, 120) + '…' : errText;
|
|
262
|
+
dispatchStatus[assignmentId] = {
|
|
263
|
+
phase: 'err',
|
|
264
|
+
msg: `✗ Dispatch failed: ${summary}`,
|
|
265
|
+
detail: errText,
|
|
266
|
+
};
|
|
267
|
+
} else {
|
|
268
|
+
let msg = '✓ Dispatched';
|
|
269
|
+
if (issueNum) msg += ` #${issueNum}`;
|
|
270
|
+
if (data.machine_name) msg += ` → ${data.machine_name}`;
|
|
271
|
+
if (data.assignment_id) msg += ` (assignment ${String(data.assignment_id).slice(0, 8)})`;
|
|
272
|
+
if (data.events && data.events.length) {
|
|
273
|
+
const last = data.events[data.events.length - 1];
|
|
274
|
+
if (last.message) msg += ` — ${last.message}`;
|
|
275
|
+
}
|
|
276
|
+
dispatchStatus[assignmentId] = {phase: 'ok', msg};
|
|
277
|
+
}
|
|
278
|
+
} catch(e) {
|
|
279
|
+
dispatchStatus[assignmentId] = {phase: 'err', msg: `✗ Dispatch failed: ${e.message}`};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
await loadBoard();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ── Toast notifications ────────────────────────────────────────────────
|
|
286
|
+
function showToast(message, kind, autoDismissMs) {
|
|
287
|
+
if (autoDismissMs === undefined) autoDismissMs = kind === 'failed' ? 0 : 8000;
|
|
288
|
+
const container = document.getElementById('toast-container');
|
|
289
|
+
const toast = document.createElement('div');
|
|
290
|
+
toast.className = `toast toast-${kind}`;
|
|
291
|
+
const dismiss = document.createElement('span');
|
|
292
|
+
dismiss.className = 'toast-dismiss';
|
|
293
|
+
dismiss.textContent = '×';
|
|
294
|
+
dismiss.onclick = e => { e.stopPropagation(); _dismissToast(toast); };
|
|
295
|
+
const text = document.createElement('span');
|
|
296
|
+
text.textContent = message;
|
|
297
|
+
toast.appendChild(dismiss);
|
|
298
|
+
toast.appendChild(text);
|
|
299
|
+
toast.addEventListener('click', () => _dismissToast(toast));
|
|
300
|
+
container.appendChild(toast);
|
|
301
|
+
if (autoDismissMs > 0) {
|
|
302
|
+
setTimeout(() => _dismissToast(toast), autoDismissMs);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function _dismissToast(toast) {
|
|
307
|
+
if (!toast.parentNode) return;
|
|
308
|
+
toast.classList.add('toast-fade-out');
|
|
309
|
+
setTimeout(() => { if (toast.parentNode) toast.parentNode.removeChild(toast); }, 400);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ── Audio bell ─────────────────────────────────────────────────────────
|
|
313
|
+
// Bug 6: persist bell preference across page loads via localStorage.
|
|
314
|
+
let bellEnabled = localStorage.getItem('bellEnabled') !== 'false';
|
|
315
|
+
function _updateBellBtn() {
|
|
316
|
+
const btn = document.getElementById('bell-btn');
|
|
317
|
+
if (btn) btn.textContent = bellEnabled ? '🔔' : '🔕';
|
|
318
|
+
}
|
|
319
|
+
function toggleBell() {
|
|
320
|
+
bellEnabled = !bellEnabled;
|
|
321
|
+
localStorage.setItem('bellEnabled', String(bellEnabled));
|
|
322
|
+
_updateBellBtn();
|
|
323
|
+
}
|
|
324
|
+
_updateBellBtn();
|
|
325
|
+
function playBell(freq, duration) {
|
|
326
|
+
if (!bellEnabled) return;
|
|
327
|
+
freq = freq || 440;
|
|
328
|
+
duration = duration || 0.35;
|
|
329
|
+
try {
|
|
330
|
+
const ctx = new (window.AudioContext || window.webkitAudioContext)();
|
|
331
|
+
const osc = ctx.createOscillator();
|
|
332
|
+
const gain = ctx.createGain();
|
|
333
|
+
osc.connect(gain);
|
|
334
|
+
gain.connect(ctx.destination);
|
|
335
|
+
osc.type = 'sine';
|
|
336
|
+
osc.frequency.setValueAtTime(freq, ctx.currentTime);
|
|
337
|
+
gain.gain.setValueAtTime(0.25, ctx.currentTime);
|
|
338
|
+
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + duration);
|
|
339
|
+
osc.start(ctx.currentTime);
|
|
340
|
+
osc.stop(ctx.currentTime + duration);
|
|
341
|
+
ctx.close();
|
|
342
|
+
} catch(e) {}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ── Possibly-stuck tracking ────────────────────────────────────────────
|
|
346
|
+
// Maps assignment_id → { repo_name, issue_number, machine_name, dispatched_ago_seconds }
|
|
347
|
+
const possiblyStuck = {};
|
|
348
|
+
|
|
349
|
+
function updatePossiblyStuck(items) {
|
|
350
|
+
// Clear old entries not present in the latest poll
|
|
351
|
+
const fresh = new Set(items.map(x => x.assignment_id));
|
|
352
|
+
for (const aid of Object.keys(possiblyStuck)) {
|
|
353
|
+
if (!fresh.has(aid)) delete possiblyStuck[aid];
|
|
354
|
+
}
|
|
355
|
+
for (const item of items) {
|
|
356
|
+
if (!possiblyStuck[item.assignment_id]) {
|
|
357
|
+
// First time we see it — show a toast too
|
|
358
|
+
const mins = Math.round((item.dispatched_ago_seconds || 0) / 60);
|
|
359
|
+
showToast(
|
|
360
|
+
`⚠ #${item.issue_number} ${item.repo_name} on ${item.machine_name} may be stuck (${mins}m, agent has no record)`,
|
|
361
|
+
'stuck', 20000
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
possiblyStuck[item.assignment_id] = item;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function renderStuckBanner(aid, issueNum) {
|
|
369
|
+
const info = possiblyStuck[aid];
|
|
370
|
+
if (!info) return '';
|
|
371
|
+
const mins = info.dispatched_ago_seconds ? Math.round(info.dispatched_ago_seconds / 60) : '?';
|
|
372
|
+
return `<div class="stuck-banner">
|
|
373
|
+
<span class="stuck-msg">⚠ Agent has no record of this assignment (${mins}m). Possibly stuck or orphaned.</span>
|
|
374
|
+
<button class="btn-unstick" onclick="doPipelineAction(${JSON.stringify(aid)},'unstick',${JSON.stringify(issueNum || null)})">Cancel & Unstick</button>
|
|
375
|
+
</div>`;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ── SSE live-events connection ─────────────────────────────────────────
|
|
379
|
+
let _sse = null;
|
|
380
|
+
let _sseReconnectTimer = null;
|
|
381
|
+
|
|
382
|
+
function connectSSE() {
|
|
383
|
+
if (_sse) return;
|
|
384
|
+
_sse = new EventSource('/events');
|
|
385
|
+
|
|
386
|
+
_sse.addEventListener('open', () => {
|
|
387
|
+
const dot = document.getElementById('sse-dot');
|
|
388
|
+
if (dot) { dot.classList.add('connected'); dot.title = 'Live events connected'; }
|
|
389
|
+
if (_sseReconnectTimer) { clearTimeout(_sseReconnectTimer); _sseReconnectTimer = null; }
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
_sse.addEventListener('assignment_completed', e => {
|
|
393
|
+
const d = JSON.parse(e.data || '{}');
|
|
394
|
+
// Bug 3 fix: use != null so a cost of $0 still shows (falsy check hid it).
|
|
395
|
+
// Bug 2 fix: showToast uses textContent — no E() needed (would double-encode).
|
|
396
|
+
const turns = d.stats?.num_turns != null ? `, turns=${d.stats.num_turns}` : '';
|
|
397
|
+
const cost = d.stats?.total_cost_usd != null ? `, cost=$${Number(d.stats.total_cost_usd).toFixed(2)}` : '';
|
|
398
|
+
showToast(`✓ #${d.issue_number} ${d.repo_name} on ${d.machine_name}: DONE${turns}${cost}`, 'done');
|
|
399
|
+
loadBoard();
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
_sse.addEventListener('assignment_failed', e => {
|
|
403
|
+
const d = JSON.parse(e.data || '{}');
|
|
404
|
+
const exit = d.exit_code != null ? `, exit=${d.exit_code}` : '';
|
|
405
|
+
const last = d.stats && d.stats.last_tool ? `, last_tool=${d.stats.last_tool}` : '';
|
|
406
|
+
// Bug 2 fix: showToast uses textContent — no E() needed.
|
|
407
|
+
showToast(`✗ #${d.issue_number} ${d.repo_name} on ${d.machine_name}: FAILED${exit}${last}`, 'failed');
|
|
408
|
+
playBell(220, 0.5); // Low tone for failures
|
|
409
|
+
loadBoard();
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// Bug 1 fix: cancelled assignments get their own event type so they are not
|
|
413
|
+
// displayed as FAILED on the client.
|
|
414
|
+
_sse.addEventListener('assignment_cancelled', e => {
|
|
415
|
+
const d = JSON.parse(e.data || '{}');
|
|
416
|
+
showToast(`⊘ #${d.issue_number} ${d.repo_name} on ${d.machine_name}: CANCELLED`, 'stuck');
|
|
417
|
+
loadBoard();
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
_sse.addEventListener('board_updated', e => {
|
|
421
|
+
const d = JSON.parse(e.data || '{}');
|
|
422
|
+
if (Array.isArray(d.possibly_stuck)) updatePossiblyStuck(d.possibly_stuck);
|
|
423
|
+
loadBoard();
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
_sse.onerror = () => {
|
|
427
|
+
const dot = document.getElementById('sse-dot');
|
|
428
|
+
if (dot) { dot.classList.remove('connected'); dot.title = 'Live events disconnected — reconnecting'; }
|
|
429
|
+
_sse.close();
|
|
430
|
+
_sse = null;
|
|
431
|
+
_sseReconnectTimer = setTimeout(connectSSE, 5000);
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ── Board ──────────────────────────────────────────────────────────────
|
|
436
|
+
async function loadBoard() {
|
|
437
|
+
await loadPipeline();
|
|
438
|
+
const r = await fetch('/api/board');
|
|
439
|
+
const data = await r.json();
|
|
440
|
+
document.getElementById('round-info').textContent = `round ${data.round_number}`;
|
|
441
|
+
|
|
442
|
+
const activeEl = document.getElementById('active-list');
|
|
443
|
+
if (!data.active.length) {
|
|
444
|
+
activeEl.innerHTML = '<div class="meta">No active assignments</div>';
|
|
445
|
+
} else {
|
|
446
|
+
activeEl.innerHTML = data.active.map(a => {
|
|
447
|
+
const isWork = !a.type || a.type === 'work';
|
|
448
|
+
const typeTag = (a.type && a.type !== 'work') ? ` <span class="meta">[${E(a.type)}]</span>` : '';
|
|
449
|
+
const pv = isWork ? pipelineCache[a.assignment_id] : null;
|
|
450
|
+
const cardId = a.assignment_id ? ` id="card-${E(a.assignment_id)}"` : '';
|
|
451
|
+
const pipelineHtml = pv ? `<div class="pipeline-area">${renderPipelineBar(pv, a.issue_number)}</div>` : '';
|
|
452
|
+
const stuckHtml = a.assignment_id ? renderStuckBanner(a.assignment_id, a.issue_number) : '';
|
|
453
|
+
return `
|
|
454
|
+
<div class="card"${cardId}>
|
|
455
|
+
<div class="card-header">${E(a.repo_name)} #${a.issue_number}: ${E(a.issue_title)}${typeTag}</div>
|
|
456
|
+
<div class="meta">${badge('running', a.status)} on ${E(a.machine_name)}</div>
|
|
457
|
+
${pipelineHtml}
|
|
458
|
+
${stuckHtml}
|
|
459
|
+
</div>
|
|
460
|
+
`;
|
|
461
|
+
}).join('');
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const compEl = document.getElementById('completed-list');
|
|
465
|
+
const recent = data.completed.slice(-10).reverse();
|
|
466
|
+
if (!recent.length) {
|
|
467
|
+
compEl.innerHTML = '<div class="meta">No completed assignments yet</div>';
|
|
468
|
+
} else {
|
|
469
|
+
compEl.innerHTML = recent.map(a => {
|
|
470
|
+
const cls = a.status === 'done' ? 'done' : 'failed';
|
|
471
|
+
const branchInfo = a.branch ? ` — branch: ${E(a.branch)}` : '';
|
|
472
|
+
const prLink = a.pr_url ? ` — <a href="${E(a.pr_url)}" target="_blank">PR</a>` : '';
|
|
473
|
+
const diffId = a.assignment_id ? `diff-${E(a.assignment_id)}` : '';
|
|
474
|
+
const diffToggle = a.branch && a.assignment_id
|
|
475
|
+
? `<div class="diff-toggle" onclick="toggleDiff('${E(a.assignment_id)}')">show diff</div>
|
|
476
|
+
<div class="diff-content" id="${diffId}"></div>`
|
|
477
|
+
: '';
|
|
478
|
+
const isWork = !a.type || a.type === 'work';
|
|
479
|
+
const typeTag = (a.type && a.type !== 'work') ? ` <span class="meta">[${E(a.type)}]</span>` : '';
|
|
480
|
+
const pv = isWork ? pipelineCache[a.assignment_id] : null;
|
|
481
|
+
const cardId = a.assignment_id ? ` id="card-${E(a.assignment_id)}"` : '';
|
|
482
|
+
const pipelineHtml = pv ? `<div class="pipeline-area">${renderPipelineBar(pv, a.issue_number)}</div>` : '';
|
|
483
|
+
return `
|
|
484
|
+
<div class="card"${cardId}>
|
|
485
|
+
<div class="card-header">${E(a.repo_name)} #${a.issue_number}: ${E(a.issue_title)}${typeTag}</div>
|
|
486
|
+
<div class="meta">${badge(cls, a.status)} on ${E(a.machine_name)}${branchInfo}${prLink}</div>
|
|
487
|
+
${pipelineHtml}
|
|
488
|
+
${diffToggle}
|
|
489
|
+
</div>
|
|
490
|
+
`;
|
|
491
|
+
}).join('');
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
async function toggleDiff(aid) {
|
|
496
|
+
const el = document.getElementById('diff-' + aid);
|
|
497
|
+
if (!el) return;
|
|
498
|
+
if (el.classList.contains('open')) { el.classList.remove('open'); return; }
|
|
499
|
+
if (el.dataset.loaded) { el.classList.add('open'); return; }
|
|
500
|
+
el.textContent = 'Loading diff...';
|
|
501
|
+
el.classList.add('open');
|
|
502
|
+
try {
|
|
503
|
+
const r = await fetch('/api/diff/' + encodeURIComponent(aid));
|
|
504
|
+
const data = await r.json();
|
|
505
|
+
if (data.error) { el.textContent = 'Error: ' + data.error; }
|
|
506
|
+
else { el.textContent = data.diff || '(empty diff)'; }
|
|
507
|
+
} catch(e) { el.textContent = 'Error: ' + e.message; }
|
|
508
|
+
el.dataset.loaded = '1';
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ── Machines ───────────────────────────────────────────────────────────
|
|
512
|
+
async function loadMachines() {
|
|
513
|
+
const r = await fetch('/api/machines');
|
|
514
|
+
const data = await r.json();
|
|
515
|
+
const el = document.getElementById('machines-list');
|
|
516
|
+
el.innerHTML = data.map(m => {
|
|
517
|
+
const state = m.state === 'online' ? 'online' : 'offline';
|
|
518
|
+
const latency = m.latency_ms ? ` (${Math.round(m.latency_ms)}ms)` : '';
|
|
519
|
+
let detail = '';
|
|
520
|
+
if (m.assignments && m.assignments.active && m.assignments.active.length) {
|
|
521
|
+
const a = m.assignments.active[0];
|
|
522
|
+
const spec = a.spec || {};
|
|
523
|
+
detail = `<div class="meta">${badge('busy', 'busy')} #${spec.issue_number}: ${E(spec.issue_title || '')}</div>`;
|
|
524
|
+
if (a.progress && a.progress.updates && a.progress.updates.length) {
|
|
525
|
+
detail += `<div class="meta">latest: ${E(a.progress.updates[a.progress.updates.length-1])}</div>`;
|
|
526
|
+
}
|
|
527
|
+
if (a.progress && a.progress.stuck) {
|
|
528
|
+
detail += `<div class="meta" style="color:#da3633">STUCK: ${E(a.progress.stuck)}</div>`;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return `
|
|
532
|
+
<div class="card">
|
|
533
|
+
<div class="card-header">${E(m.name)} ${badge(state, state)}${latency}</div>
|
|
534
|
+
<div class="meta">host: ${E(m.host)} — repos: ${E((m.repos || []).join(', ') || 'none')}</div>
|
|
535
|
+
${detail}
|
|
536
|
+
</div>
|
|
537
|
+
`;
|
|
538
|
+
}).join('');
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// ── Proposals ──────────────────────────────────────────────────────────
|
|
542
|
+
async function loadProposals() {
|
|
543
|
+
const r = await fetch('/api/proposals');
|
|
544
|
+
const data = await r.json();
|
|
545
|
+
const el = document.getElementById('proposals-list');
|
|
546
|
+
if (!data.length) {
|
|
547
|
+
el.innerHTML = '<div class="meta">No pending proposals. Run <code>coord plan</code> first.</div>';
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
el.innerHTML = data.map(p => `
|
|
551
|
+
<div class="proposal" id="proposal-${p.id}">
|
|
552
|
+
<div class="card-header">[${p.id}] ${E(p.machine_name)} → ${E(p.repo_name)} #${p.issue_number}: ${E(p.issue_title)}</div>
|
|
553
|
+
<div class="meta">${E(p.rationale)}</div>
|
|
554
|
+
${p.files_likely.length ? `<div class="meta">files: ${E(p.files_likely.join(', '))}</div>` : ''}
|
|
555
|
+
<textarea class="briefing-edit" id="briefing-${p.id}">${E(p.briefing || '')}</textarea>
|
|
556
|
+
<div>
|
|
557
|
+
<button class="btn btn-approve" onclick="approveOne(${p.id})">Approve</button>
|
|
558
|
+
<button class="btn btn-reject" onclick="rejectOne(${p.id})">Reject</button>
|
|
559
|
+
</div>
|
|
560
|
+
</div>
|
|
561
|
+
`).join('') + `<button class="btn btn-all" onclick="approveAll()">Approve All</button>`;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async function approveOne(id) {
|
|
565
|
+
const briefing = document.getElementById('briefing-' + id)?.value;
|
|
566
|
+
const briefings = {};
|
|
567
|
+
if (briefing !== undefined) briefings[String(id)] = briefing;
|
|
568
|
+
const resp = await fetch('/api/approve', {
|
|
569
|
+
method: 'POST',
|
|
570
|
+
headers: {'Content-Type': 'application/json'},
|
|
571
|
+
body: JSON.stringify({ids: [id], briefings}),
|
|
572
|
+
});
|
|
573
|
+
const data = await resp.json();
|
|
574
|
+
const el = document.getElementById('proposal-' + id);
|
|
575
|
+
if (el && data.results && data.results[0]?.ok) {
|
|
576
|
+
el.style.opacity = '0.4';
|
|
577
|
+
el.querySelector('.btn-approve').disabled = true;
|
|
578
|
+
}
|
|
579
|
+
loadBoard();
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
async function rejectOne(id) {
|
|
583
|
+
await fetch('/api/reject', {
|
|
584
|
+
method: 'POST',
|
|
585
|
+
headers: {'Content-Type': 'application/json'},
|
|
586
|
+
body: JSON.stringify({ids: [id]}),
|
|
587
|
+
});
|
|
588
|
+
loadProposals();
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
async function approveAll() {
|
|
592
|
+
const r = await fetch('/api/proposals');
|
|
593
|
+
const proposals = await r.json();
|
|
594
|
+
const ids = proposals.map(p => p.id);
|
|
595
|
+
const briefings = {};
|
|
596
|
+
for (const p of proposals) {
|
|
597
|
+
const el = document.getElementById('briefing-' + p.id);
|
|
598
|
+
if (el) briefings[String(p.id)] = el.value;
|
|
599
|
+
}
|
|
600
|
+
await fetch('/api/approve', {
|
|
601
|
+
method: 'POST',
|
|
602
|
+
headers: {'Content-Type': 'application/json'},
|
|
603
|
+
body: JSON.stringify({ids, briefings}),
|
|
604
|
+
});
|
|
605
|
+
loadProposals();
|
|
606
|
+
loadBoard();
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// ── Chat ───────────────────────────────────────────────────────────────
|
|
610
|
+
async function sendChat() {
|
|
611
|
+
const input = document.getElementById('chat-input');
|
|
612
|
+
const msg = input.value.trim();
|
|
613
|
+
if (!msg) return;
|
|
614
|
+
input.value = '';
|
|
615
|
+
|
|
616
|
+
const messages = document.getElementById('chat-messages');
|
|
617
|
+
const userDiv = document.createElement('div');
|
|
618
|
+
userDiv.className = 'chat-msg user';
|
|
619
|
+
userDiv.textContent = msg;
|
|
620
|
+
messages.appendChild(userDiv);
|
|
621
|
+
|
|
622
|
+
const responseDiv = document.createElement('div');
|
|
623
|
+
responseDiv.className = 'chat-msg assistant';
|
|
624
|
+
responseDiv.textContent = '...';
|
|
625
|
+
messages.appendChild(responseDiv);
|
|
626
|
+
messages.scrollTop = messages.scrollHeight;
|
|
627
|
+
|
|
628
|
+
try {
|
|
629
|
+
const r = await fetch('/api/chat', {
|
|
630
|
+
method: 'POST',
|
|
631
|
+
headers: {'Content-Type': 'application/json'},
|
|
632
|
+
body: JSON.stringify({message: msg}),
|
|
633
|
+
});
|
|
634
|
+
const reader = r.body.getReader();
|
|
635
|
+
const decoder = new TextDecoder();
|
|
636
|
+
let text = '';
|
|
637
|
+
while (true) {
|
|
638
|
+
const {done, value} = await reader.read();
|
|
639
|
+
if (done) break;
|
|
640
|
+
const chunk = decoder.decode(value);
|
|
641
|
+
for (const line of chunk.split('\n')) {
|
|
642
|
+
if (line.startsWith('data: ') && line !== 'data: [DONE]') {
|
|
643
|
+
try { const d = JSON.parse(line.slice(6)); text += d.text; responseDiv.textContent = text; } catch(e) {}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
messages.scrollTop = messages.scrollHeight;
|
|
647
|
+
}
|
|
648
|
+
if (!text) responseDiv.textContent = '(no response)';
|
|
649
|
+
} catch(e) { responseDiv.textContent = 'Error: ' + e.message; }
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
document.getElementById('chat-input').addEventListener('keydown', e => { if (e.key === 'Enter') sendChat(); });
|
|
653
|
+
|
|
654
|
+
loadBoard();
|
|
655
|
+
connectSSE();
|
|
656
|
+
</script>
|
|
657
|
+
</body>
|
|
658
|
+
</html>
|