tumwater 0.4.0 → 0.4.2
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 +4 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +5 -2
- package/dist/src/agent/backend-traits.js +21 -0
- package/dist/src/agent/claude/claude-backend.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +181 -182
- package/dist/src/agent/claude/{write-guard-hook.js → claude-write-guard-hook.js} +1 -1
- package/dist/src/agent/claude/claude-write-guard.js +5 -4
- package/dist/src/agent/codex/codex-backend.js +1 -1
- package/dist/src/agent/codex/codex-rollout.js +48 -45
- package/dist/src/agent/codex/codex-stream.js +269 -215
- package/dist/src/agent/codex/{write-guard-hook.js → codex-write-guard-hook.js} +2 -1
- package/dist/src/agent/codex/codex-write-guard.js +3 -3
- package/dist/src/agent/image-input.js +18 -87
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +2 -2
- package/dist/src/agent/opencode/opencode-catalog.js +2 -2
- package/dist/src/agent/opencode/opencode-config.js +2 -2
- package/dist/src/agent/opencode/opencode-stream.js +101 -92
- package/dist/src/agent/run-agent.js +76 -382
- package/dist/src/agent/run-settle.js +25 -15
- package/dist/src/agent/run-spawn.js +226 -0
- package/dist/src/agent/run-stream.js +185 -0
- package/dist/src/agent/session-ledger.js +16 -0
- package/dist/src/agent/write-guard.js +5 -3
- package/dist/src/backlog/backlog-archive.js +86 -170
- package/dist/src/backlog/backlog-cli-view.js +89 -70
- package/dist/src/backlog/backlog-eligibility.js +51 -59
- package/dist/src/backlog/backlog-file-bugs.js +133 -0
- package/dist/src/backlog/backlog-index-data.js +159 -0
- package/dist/src/backlog/backlog-index.js +7 -5
- package/dist/src/backlog/backlog-md.js +39 -28
- package/dist/src/backlog/backlog-release.js +21 -36
- package/dist/src/backlog/backlog-render.js +1 -1
- package/dist/src/backlog/backlog-structure.js +14 -145
- package/dist/src/backlog/backlog-write.js +31 -18
- package/dist/src/backlog/backlog.js +14 -4
- package/dist/src/backlog/question-answer.js +33 -15
- package/dist/src/baseline/main-baseline.js +71 -26
- package/dist/src/baseline/main-guardrails.js +76 -15
- package/dist/src/baseline/main-red.js +45 -94
- package/dist/src/baseline/main-watch.js +9 -3
- package/dist/src/brief/brief.js +40 -0
- package/dist/src/budget/budget-fallback-view.js +50 -8
- package/dist/src/budget/budget-wiring.js +15 -11
- package/dist/src/budget/budget.js +3 -2
- package/dist/src/build/build-check-events.js +4 -2
- package/dist/src/build/build-check-report.js +2 -1
- package/dist/src/build/build-check-scoped.js +70 -41
- package/dist/src/change/change-data.js +4 -2
- package/dist/src/cli/cli-args.js +9 -0
- package/dist/src/cli/cli-command-args.js +199 -98
- package/dist/src/cli/cli-flag-specs.js +6 -4
- package/dist/src/cli/cli-marker-commands.js +2 -2
- package/dist/src/cli/cli-query-commands.js +41 -6
- package/dist/src/cli/cli-review-commands.js +2 -1
- package/dist/src/cli/cli-role-command.js +46 -92
- package/dist/src/cli/cli-start.js +123 -47
- package/dist/src/cli/follow-view.js +3 -1
- package/dist/src/cli/help.js +19 -12
- package/dist/src/cli/log-commands.js +128 -107
- package/dist/src/cli/question-commands.js +82 -63
- package/dist/src/cli.js +396 -343
- package/dist/src/collections.js +46 -9
- package/dist/src/config/config-fallbacks.js +0 -4
- package/dist/src/config/config-live.js +179 -147
- package/dist/src/config/config-request.js +176 -51
- package/dist/src/config/config-validation-roles.js +271 -0
- package/dist/src/config/config-validation.js +10 -205
- package/dist/src/config/config-views.js +2 -7
- package/dist/src/config/config-write.js +205 -175
- package/dist/src/config/config.js +2 -1
- package/dist/src/doctor/doctor-backlog-bugs.js +24 -122
- package/dist/src/doctor/doctor-backlog-plans.js +24 -25
- package/dist/src/doctor/doctor-backlog.js +34 -42
- package/dist/src/doctor/doctor-check-names.js +4 -2
- package/dist/src/doctor/doctor-checks.js +16 -4
- package/dist/src/doctor/doctor-claude-checks.js +2 -2
- package/dist/src/doctor/doctor-codex-checks.js +9 -20
- package/dist/src/doctor/doctor-contention-checks.js +52 -19
- package/dist/src/doctor/doctor-holds.js +22 -20
- package/dist/src/doctor/doctor-model-checks.js +8 -184
- package/dist/src/doctor/doctor-notify.js +2 -5
- package/dist/src/doctor/doctor-opencode-checks.js +90 -41
- package/dist/src/doctor/doctor-principles.js +26 -0
- package/dist/src/doctor/doctor-project-checks.js +69 -19
- package/dist/src/doctor/doctor-role-notes.js +33 -0
- package/dist/src/doctor/doctor.js +13 -6
- package/dist/src/doctor/probe.js +1 -1
- package/dist/src/events/event-format.js +307 -162
- package/dist/src/events/event-labels.js +0 -1
- package/dist/src/events/event-legacy.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +15 -2
- package/dist/src/events/notify.js +1 -1
- package/dist/src/failure/failure-cluster.js +4 -3
- package/dist/src/failure/failure-data.js +126 -94
- package/dist/src/failure/failure-render.js +58 -17
- package/dist/src/failure/failure-state-change.js +308 -27
- package/dist/src/failure/time-spend.js +139 -106
- package/dist/src/files/files.js +7 -0
- package/dist/src/files/json-object.js +15 -2
- package/dist/src/gates/budget-gates.js +90 -68
- package/dist/src/gates/gate-polls.js +92 -61
- package/dist/src/gates/role-cap-gates.js +1 -1
- package/dist/src/gates/startup-gate.js +28 -28
- package/dist/src/gates/streak-gate.js +1 -1
- package/dist/src/git/git.js +1 -1
- package/dist/src/git/slot-salvage.js +9 -9
- package/dist/src/git/slots-state.js +12 -3
- package/dist/src/git/worktree-pool-startup.js +127 -0
- package/dist/src/git/worktree-pool.js +1 -120
- package/dist/src/gui/gui-admin-endpoints.js +3 -4
- package/dist/src/gui/gui-args.js +1 -1
- package/dist/src/gui/gui-backlog-commands.js +20 -11
- package/dist/src/gui/gui-backlog-endpoints.js +2 -8
- package/dist/src/gui/gui-config-commands.js +51 -76
- package/dist/src/gui/gui-endpoint-commands.js +4 -2
- package/dist/src/gui/gui-endpoint-common.js +4 -3
- package/dist/src/gui/gui-history-endpoints.js +36 -27
- package/dist/src/gui/gui-operator-commands.js +43 -17
- package/dist/src/gui/gui-prompt-commands.js +27 -21
- package/dist/src/gui/gui-quarantine-commands.js +30 -0
- package/dist/src/gui/gui-report-endpoints.js +13 -9
- package/dist/src/gui/gui-server.js +5 -3
- package/dist/src/gui/gui-team-commands.js +1 -1
- package/dist/src/history/history-data.js +7 -1
- package/dist/src/history/history-render.js +15 -10
- package/dist/src/history/history.js +103 -68
- package/dist/src/inbox/inbox-submit.js +1 -5
- package/dist/src/inbox/pending-prompt.js +3 -2
- package/dist/src/inbox/prompt-commands.js +194 -161
- package/dist/src/init/init.js +104 -25
- package/dist/src/merge/merge-core.js +1 -1
- package/dist/src/merge/merge-drain.js +1 -1
- package/dist/src/merge/merge-git.js +5 -5
- package/dist/src/merge/{merge-post-review.js → merge-post-review-drain.js} +1 -1
- package/dist/src/merge/merge-queue.js +2 -1
- package/dist/src/merge/merge-to-main.js +1 -1
- package/dist/src/merge/merge-verify.js +3 -2
- package/dist/src/operator/operator-commands.js +84 -52
- package/dist/src/operator/operator-intent.js +26 -3
- package/dist/src/operator/retire.js +2 -1
- package/dist/src/operator/revert.js +18 -12
- package/dist/src/orchestrator/orchestrator-poll.js +2 -2
- package/dist/src/orchestrator/orchestrator.js +1 -1
- package/dist/src/orchestrator/retention.js +6 -5
- package/dist/src/paths.js +10 -10
- package/dist/src/pi/pi-bin.js +2 -2
- package/dist/src/pi/pi-models.js +9 -0
- package/dist/src/pi/pi-stream.js +63 -41
- package/dist/src/pi/pi-watchdogs.js +84 -49
- package/dist/src/pi-extension/context-shake.js +55 -31
- package/dist/src/pi-extension/full-output.js +2 -3
- package/dist/src/process/process.js +24 -1
- package/dist/src/process/run-marker.js +2 -2
- package/dist/src/process/supervisor.js +24 -5
- package/dist/src/prompt/gate-notes.js +41 -16
- package/dist/src/prompt/notes-backlog.js +22 -29
- package/dist/src/prompt/principles.js +11 -0
- package/dist/src/prompt/prompt-followup.js +4 -12
- package/dist/src/prompt/prompt.js +15 -12
- package/dist/src/redeploy/redeploy-probes.js +11 -1
- package/dist/src/redeploy/redeployer.js +54 -7
- package/dist/src/redeploy/self-reload.js +7 -2
- package/dist/src/report/report-data.js +224 -320
- package/dist/src/report/report-fold.js +23 -231
- package/dist/src/report/report-main-red.js +70 -3
- package/dist/src/report/report-render.js +181 -48
- package/dist/src/report/report-since-data.js +218 -0
- package/dist/src/report/report-usage-fold.js +288 -0
- package/dist/src/report/report.js +2 -1
- package/dist/src/review/known-flakes.js +22 -3
- package/dist/src/review/post-merge-review-accept.js +3 -2
- package/dist/src/review/post-merge-review.js +16 -14
- package/dist/src/review/quarantine-render.js +29 -11
- package/dist/src/review/review-precheck.js +2 -1
- package/dist/src/roles/role-catalog.js +30 -29
- package/dist/src/roles/role-guidance.js +26 -43
- package/dist/src/roles/role-render.js +19 -0
- package/dist/src/roles/role-view.js +19 -5
- package/dist/src/roles/roles.js +2 -22
- package/dist/src/run/ci-parity.js +5 -6
- package/dist/src/run/feedback-filings.js +10 -1
- package/dist/src/run/qa-coverage.js +4 -4
- package/dist/src/run/run-apply.js +14 -2
- package/dist/src/run/run-detail-data.js +50 -10
- package/dist/src/run/run-detail-render.js +32 -1
- package/dist/src/run/run-detail.js +44 -22
- package/dist/src/run/run-dismiss.js +43 -0
- package/dist/src/run/run-finalize.js +75 -46
- package/dist/src/run/run-prompt.js +271 -187
- package/dist/src/run/run-resume.js +32 -31
- package/dist/src/run/run-stage.js +116 -64
- package/dist/src/run/run-timing.js +18 -4
- package/dist/src/run/run-verdict.js +119 -83
- package/dist/src/run/stage-backlog-checks.js +4 -31
- package/dist/src/run/stage-check.js +4 -12
- package/dist/src/run/stage-checks-shared.js +15 -0
- package/dist/src/run/stage-name-checks.js +3 -13
- package/dist/src/run/stage-path-checks.js +26 -29
- package/dist/src/runner/leftover.js +13 -4
- package/dist/src/runner/revision.js +2 -2
- package/dist/src/runner/runner-fallback.js +1 -1
- package/dist/src/runner/runner-outcome.js +0 -5
- package/dist/src/runner/runner-pi.js +72 -49
- package/dist/src/runner/runner-run.js +19 -102
- package/dist/src/runner/runner-state-load.js +0 -10
- package/dist/src/runner/runner.js +1 -32
- package/dist/src/scheduling/claims.js +4 -11
- package/dist/src/status/status-data.js +140 -91
- package/dist/src/status/status-polls.js +41 -5
- package/dist/src/status/status-role-notes.js +53 -0
- package/dist/src/status/status-roles.js +10 -37
- package/dist/src/text/datetime.js +8 -1
- package/dist/src/text/format.js +13 -1
- package/dist/src/text/phrases.js +16 -2
- package/dist/src/text/text.js +12 -0
- package/dist/src/ui/alert-feeds.js +115 -0
- package/dist/src/ui/badges.js +3 -4
- package/dist/src/ui/gui/gui-client-boot.js +78 -57
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -3
- package/dist/src/ui/gui/gui-client-drawer.js +18 -2
- package/dist/src/ui/gui/gui-client-feed.js +3 -8
- package/dist/src/ui/gui/gui-client-history.js +16 -16
- package/dist/src/ui/gui/gui-client-model.js +9 -9
- package/dist/src/ui/gui/gui-client-operator.js +6 -10
- package/dist/src/ui/gui/gui-client-pending.js +4 -4
- package/dist/src/ui/gui/gui-client-report-chart.js +111 -10
- package/dist/src/ui/gui/gui-client-report.js +70 -11
- package/dist/src/ui/gui/gui-client-roles.js +52 -12
- package/dist/src/ui/gui/gui-client-settings.js +31 -26
- package/dist/src/ui/gui/gui-client-team.js +28 -2
- package/dist/src/ui/gui/gui-client.js +33 -4
- package/dist/src/ui/gui/gui-icons.js +7 -1
- package/dist/src/ui/progress-data.js +2 -2
- package/dist/src/ui/run-progress-model.js +2 -2
- package/dist/src/ui/status-model.js +26 -16
- package/dist/src/ui/status-payload.js +149 -326
- package/dist/src/ui/status-render.js +131 -80
- package/dist/src/ui/team-alert-backlog.js +236 -0
- package/dist/src/ui/team-alert-shared.js +92 -0
- package/dist/src/ui/team-alert-status.js +326 -0
- package/dist/src/ui/team-alert-tail.js +238 -0
- package/dist/src/ui/team-alerts.js +16 -984
- package/dist/src/ui/transcript-tail.js +137 -94
- package/dist/src/ui/tui/tui-backlog.js +1 -1
- package/dist/src/ui/tui/tui-editor-answer.js +54 -44
- package/dist/src/ui/tui/tui-editor-attach.js +74 -63
- package/dist/src/ui/tui/tui-editor-backends.js +62 -56
- package/dist/src/ui/tui/tui-editor-brief.js +64 -54
- package/dist/src/ui/tui/tui-editor-budget.js +106 -89
- package/dist/src/ui/tui/tui-editor-config.js +2 -4
- package/dist/src/ui/tui/tui-editor-ctx.js +12 -1
- package/dist/src/ui/tui/tui-editor-entry.js +171 -153
- package/dist/src/ui/tui/tui-editor-instances.js +2 -1
- package/dist/src/ui/tui/tui-editor-instructions.js +50 -107
- package/dist/src/ui/tui/tui-editor-interval.js +39 -112
- package/dist/src/ui/tui/tui-editor-notes.js +28 -84
- package/dist/src/ui/tui/tui-editor-pause.js +210 -183
- package/dist/src/ui/tui/tui-editor-prompt.js +46 -35
- package/dist/src/ui/tui/tui-editor-queued.js +74 -64
- package/dist/src/ui/tui/tui-editor-quiet.js +122 -108
- package/dist/src/ui/tui/tui-editor-release.js +85 -0
- package/dist/src/ui/tui/tui-editor-retire.js +57 -46
- package/dist/src/ui/tui/tui-editor-role-enabled.js +90 -80
- package/dist/src/ui/tui/tui-editor-role-file.js +87 -0
- package/dist/src/ui/tui/tui-editor-role-key.js +50 -42
- package/dist/src/ui/tui/tui-editor-search.js +70 -58
- package/dist/src/ui/tui/tui-editor-session.js +3 -0
- package/dist/src/ui/tui/tui-editors.js +4 -1
- package/dist/src/ui/tui/tui-frame.js +7 -3
- package/dist/src/ui/tui/tui-history.js +12 -10
- package/dist/src/ui/tui/tui-keys.js +6 -1
- package/dist/src/ui/tui/tui-pane-activity.js +62 -50
- package/dist/src/ui/tui/tui-pane-backlog.js +4 -4
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +45 -26
- package/dist/src/ui/tui/tui-pane-history.js +296 -264
- package/dist/src/ui/tui/tui-pane-report.js +74 -57
- package/dist/src/ui/tui/tui-pane-session.js +6 -2
- package/dist/src/ui/tui/tui-pane-transcript.js +158 -131
- package/dist/src/ui/tui/tui-pane.js +201 -174
- package/dist/src/ui/tui/tui-panes.js +211 -148
- package/dist/src/ui/tui/tui.js +4 -4
- package/dist/src/verdict/fix-claim.js +18 -6
- package/dist/src/verdict/refusal.js +32 -7
- package/package.json +1 -1
- package/dist/src/doctor/image-probe.js +0 -257
- /package/dist/src/gui/{http-body.js → gui-http-body.js} +0 -0
- /package/dist/src/merge/{backlog-conflicts.js → merge-backlog-conflicts.js} +0 -0
|
@@ -74,10 +74,10 @@ export const GUI_CLIENT_PENDING_JS = String.raw ` let pendingTeam = null;
|
|
|
74
74
|
// optional merge label, and summary. Shared by the Merge queue card and the role drawer so
|
|
75
75
|
// the two cannot drift; a malformed entry renders stand-ins rather than undefined/NaN.
|
|
76
76
|
function queueEntryLineHtml(e, position, now, label, cancel, top) {
|
|
77
|
-
const sec = Number.isFinite(e.enqueuedAt) ?
|
|
77
|
+
const sec = Number.isFinite(e.enqueuedAt) ? secsSince(e.enqueuedAt, now) : 0;
|
|
78
78
|
const run = Number.isFinite(e.run) ? e.run : "?";
|
|
79
79
|
const note = Number.isFinite(e.revisionRound) ? " · revision " + e.revisionRound + (Number.isFinite(e.priorObjections) && e.priorObjections > 0 ? " · " + plural(e.priorObjections, "prior objection") : "") : "";
|
|
80
|
-
const retry = typeof e.retry === "string" ? " · re-queued after " + e.retry + (Number.isFinite(e.retryCount) && e.retryCount > 1 ? " · ×" + e.retryCount : "") + (Number.isFinite(e.notBefore) && e.notBefore > now ? " · eligible in " + humanSeconds(
|
|
80
|
+
const retry = typeof e.retry === "string" ? " · re-queued after " + e.retry + (Number.isFinite(e.retryCount) && e.retryCount > 1 ? " · ×" + e.retryCount : "") + (Number.isFinite(e.notBefore) && e.notBefore > now ? " · eligible in " + humanSeconds(secsUntil(e.notBefore, now)) : "") : "";
|
|
81
81
|
let line = "<div class='mono clamp1' style='padding:3px 0'>" + position + ". " + esc(e.role ? roleLabel(e.role) : "?") +
|
|
82
82
|
" run #" + esc(run) + " · " + esc(shortSha(e.sha)) + " · " + esc(humanSeconds(sec)) +
|
|
83
83
|
" ago" + (label && typeof label === "string" ? " · [" + esc(label) + "]" : "") +
|
|
@@ -238,7 +238,7 @@ export const GUI_CLIENT_PENDING_JS = String.raw ` let pendingTeam = null;
|
|
|
238
238
|
// verdict-less failed-review-run count when positive, else "". Hand-kept copy because the
|
|
239
239
|
// script cannot import the server renderer; pinned by the client test against the CLI wording.
|
|
240
240
|
const squashText = (s, max) => {
|
|
241
|
-
const t =
|
|
241
|
+
const t = squash(s);
|
|
242
242
|
if (t.length <= max) return t;
|
|
243
243
|
return t.slice(0, max - 1).trimEnd() + "…";
|
|
244
244
|
};
|
|
@@ -297,7 +297,7 @@ export const GUI_CLIENT_PENDING_JS = String.raw ` let pendingTeam = null;
|
|
|
297
297
|
const lines = kept.map((e) => {
|
|
298
298
|
const label = postReviewStatusLabel(e);
|
|
299
299
|
const age = typeof e.enqueuedAt === "number" && Number.isFinite(now)
|
|
300
|
-
? " · " + esc(humanSeconds(
|
|
300
|
+
? " · " + esc(humanSeconds(secsSince(e.enqueuedAt, now))) + " ago"
|
|
301
301
|
: "";
|
|
302
302
|
let line = "<div class='queue-entry mono clamp1'>" + esc(e.role ? roleLabel(e.role) : "?") + " " + esc(shortSha(e.commit)) +
|
|
303
303
|
age +
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/** The dashboard Usage view's chart builders, browser-side: the report-chart region. Pure
|
|
2
2
|
* functions from /api/report's day series to SVG strings — the per-day bar and stacked charts,
|
|
3
3
|
* their role/provider/model orderings, the shared role palette, the has-data predicates that
|
|
4
|
-
* decide which charts draw,
|
|
5
|
-
*
|
|
4
|
+
* decide which charts draw, the merge-share line's wording, and the cost-per-commit /
|
|
5
|
+
* estimated-per-commit lines (costPerCommitParts, costPerCommitText, estimatedPerCommitText) —
|
|
6
|
+
* with no DOM access, so the tests exercise them in isolation.
|
|
6
7
|
* Spliced into gui-client.ts's script ahead of gui-client-report.ts's hover chip, Role picker,
|
|
7
8
|
* and view code, which call them through that concatenation. */
|
|
9
|
+
import { INSTANCE_ID_SOURCE } from "../../roles/runner-ids.js";
|
|
8
10
|
export const GUI_CLIENT_REPORT_CHART_JS = String.raw `
|
|
9
11
|
// report-chart:start
|
|
10
12
|
// One color per role, assigned in the report's role order so a role keeps its color across
|
|
@@ -12,6 +14,12 @@ export const GUI_CLIENT_REPORT_CHART_JS = String.raw `
|
|
|
12
14
|
const REPORT_PALETTE = ["#6366f1", "#f59e0b", "#10b981", "#ef4444", "#0ea5e9", "#a855f7", "#f97316", "#14b8a6",
|
|
13
15
|
"#ec4899", "#84cc16", "#06b6d4", "#eab308", "#64748b", "#d946ef"];
|
|
14
16
|
|
|
17
|
+
// A [key, number][] ordering by total desc then key asc — the shared shape of every
|
|
18
|
+
// role/provider/model ordering (tallyAcross's role/provider/model rows and costPerCommitParts's
|
|
19
|
+
// mergeDone and idle pairs each spelled the b[1]-a[1] || a[0].localeCompare(b[0]) comparator
|
|
20
|
+
// inline), so none of them can drift on the count-then-name rule.
|
|
21
|
+
const byTotalThenName = (a, b) => b[1] - a[1] || a[0].localeCompare(b[0]);
|
|
22
|
+
|
|
15
23
|
// One [key, total][] for a day series, ordered by total desc then name asc — the shared
|
|
16
24
|
// shape of the role/provider/model orderings. sumFields are the fields that contribute a
|
|
17
25
|
// running total (runsByRole, costByProvider, runsByModel); seedFields are fields whose
|
|
@@ -31,7 +39,7 @@ export const GUI_CLIENT_REPORT_CHART_JS = String.raw `
|
|
|
31
39
|
for (const k of Object.keys(d[field] || {})) if (!Object.hasOwn(by, k)) by[k] = 0;
|
|
32
40
|
}
|
|
33
41
|
}
|
|
34
|
-
return Object.entries(by).sort(
|
|
42
|
+
return Object.entries(by).sort(byTotalThenName);
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
// Window totals per role — runs desc, then name — so the legend and the stack order match
|
|
@@ -40,7 +48,7 @@ export const GUI_CLIENT_REPORT_CHART_JS = String.raw `
|
|
|
40
48
|
// Object.prototype key whose inherited value would otherwise seed the counter and satisfy
|
|
41
49
|
// the membership test (the browser twin of addTo's fix; BUGS.md 2026-10-10).
|
|
42
50
|
function reportRoleOrder(data) {
|
|
43
|
-
return tallyAcross(data.series, ["runsByRole"], ["costByRole", "notionalByRole", "rejectionsByRole", "revisionsByRole", "refusalsByRole", "fallbacksByRole", "
|
|
51
|
+
return tallyAcross(data.series, ["runsByRole"], ["costByRole", "notionalByRole", "rejectionsByRole", "revisionsByRole", "refusalsByRole", "fallbacksByRole", "failedRunsByRole", "mergeFailedByRole", "commitsByRole", "mergedByRole"]);
|
|
44
52
|
}
|
|
45
53
|
|
|
46
54
|
// X-axis labels: MM-DD, thinned to at most seven, counted back from the newest day so today
|
|
@@ -155,8 +163,8 @@ export const GUI_CLIENT_REPORT_CHART_JS = String.raw `
|
|
|
155
163
|
countAxis, true);
|
|
156
164
|
}
|
|
157
165
|
// Main's red wall-clock per day, from mainRedMs (the same main_check/main_red_hold rule G1
|
|
158
|
-
// uses): the span
|
|
159
|
-
// median charts; the red color reads beside the red-check-count chart it follows.
|
|
166
|
+
// uses): the span main had no passing full check; a day without one keeps its empty slot,
|
|
167
|
+
// like the median charts; the red color reads beside the red-check-count chart it follows.
|
|
160
168
|
function chartMainRedMs(data) {
|
|
161
169
|
return reportSvg(data.series, (d) => [{ value: Number(d.mainRedMs) || 0, color: "#dc2626", title: d.date + ": " + fmtSpan(Number(d.mainRedMs) || 0) + " main red" }],
|
|
162
170
|
(v) => fmtSpan(v), false);
|
|
@@ -280,6 +288,21 @@ export const GUI_CLIENT_REPORT_CHART_JS = String.raw `
|
|
|
280
288
|
return roleStackChart(data, "tokensByRole", (n) => fmtTokens(Number(n) || 0) + " output tokens", (v) => fmtTokens(Math.round(v)), true);
|
|
281
289
|
}
|
|
282
290
|
|
|
291
|
+
// The day's prompt tokens, per role, the browser twin of the report's "Prompt tokens by
|
|
292
|
+
// role" line — same roleStackChart shape, palette, legend order and integer count axis as
|
|
293
|
+
// chartTokensByRole, so a role keeps its palette color and legend order from
|
|
294
|
+
// reportRoleOrder.
|
|
295
|
+
function chartPromptTokensByRole(data) {
|
|
296
|
+
return roleStackChart(data, "promptTokensByRole", (n) => fmtTokens(Number(n) || 0) + " prompt tokens", (v) => fmtTokens(Math.round(v)), true);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// The day's cache-read prompt tokens, per role, the browser twin of the report's "Prompt
|
|
300
|
+
// tokens by role" line's cache-read share — the same shape over cacheReadTokensByRole with
|
|
301
|
+
// the " cached prompt tokens" label, keeping the same palette order and count axis.
|
|
302
|
+
function chartCacheReadTokensByRole(data) {
|
|
303
|
+
return roleStackChart(data, "cacheReadTokensByRole", (n) => fmtTokens(Number(n) || 0) + " cached prompt tokens", (v) => fmtTokens(Math.round(v)), true);
|
|
304
|
+
}
|
|
305
|
+
|
|
283
306
|
// The API-equivalent (notional) spend of subscription runs, per role, on the same axis as
|
|
284
307
|
// the cap spend.
|
|
285
308
|
function chartNotionalByRole(data) {
|
|
@@ -353,10 +376,6 @@ export const GUI_CLIENT_REPORT_CHART_JS = String.raw `
|
|
|
353
376
|
function chartFallbacksByRole(data) {
|
|
354
377
|
return roleStackChart(data, "fallbacksByRole", (n) => plural(n, "fallback"), countAxis, true);
|
|
355
378
|
}
|
|
356
|
-
// Image reroutes per role, drawn the same way: one segment per (day, role) with a reroute.
|
|
357
|
-
function chartImageReroutesByRole(data) {
|
|
358
|
-
return roleStackChart(data, "imageReroutesByRole", (n) => plural(n, "image reroute"), countAxis, true);
|
|
359
|
-
}
|
|
360
379
|
// Runs that ended in an error, per role, drawn the same way.
|
|
361
380
|
function chartFailedRunsByRole(data) {
|
|
362
381
|
return roleStackChart(data, "failedRunsByRole", (n) => plural(n, "failed run"), countAxis, true);
|
|
@@ -385,6 +404,32 @@ export const GUI_CLIENT_REPORT_CHART_JS = String.raw `
|
|
|
385
404
|
function chartValidationGaps(data) {
|
|
386
405
|
return stackChart(data, reportValidationGapOrder(data), "validationGapsByTag", (n) => plural(n, "validation gap"), countAxis, true);
|
|
387
406
|
}
|
|
407
|
+
// Window tallies per main-red failing test, count desc then name asc — the browser twin of
|
|
408
|
+
// the day report's "Main red by test" line. Test keys are dynamic, so reads and seeds go
|
|
409
|
+
// through tallyAcross's Object.hasOwn checks as its siblings do; a test no day recorded
|
|
410
|
+
// still gets a row only if some day carries it.
|
|
411
|
+
function reportMainRedTestOrder(data) {
|
|
412
|
+
return tallyAcross(data.series, ["mainRedByTest"], []);
|
|
413
|
+
}
|
|
414
|
+
// The team's main-red failing tests, per day, stacked by test: each segment one (day, test),
|
|
415
|
+
// the dashboard twin of the day report's "Main red by test" line. Sparse like the model
|
|
416
|
+
// chart; test names are the legend labels.
|
|
417
|
+
function chartMainRedByTest(data) {
|
|
418
|
+
return stackChart(data, reportMainRedTestOrder(data), "mainRedByTest", (n) => plural(n, "red check"), countAxis, true);
|
|
419
|
+
}
|
|
420
|
+
// Window tallies per main-red blamed merge (the report's "<role> <shortSha>" label), count
|
|
421
|
+
// desc then name asc — the browser twin of the day report's "Main red by merge" line.
|
|
422
|
+
// Culprit keys are dynamic, so reads and seeds go through tallyAcross's Object.hasOwn checks
|
|
423
|
+
// as its siblings do; a culprit no day recorded still gets a row only if some day carries it.
|
|
424
|
+
function reportMainRedCulpritOrder(data) {
|
|
425
|
+
return tallyAcross(data.series, ["mainRedByCulprit"], []);
|
|
426
|
+
}
|
|
427
|
+
// The team's main-red blamed merges, per day, stacked by culprit: each segment one
|
|
428
|
+
// (day, culprit), the dashboard twin of the day report's "Main red by merge" line. Sparse
|
|
429
|
+
// like the model chart; the culprit labels are the legend labels.
|
|
430
|
+
function chartMainRedByMerge(data) {
|
|
431
|
+
return stackChart(data, reportMainRedCulpritOrder(data), "mainRedByCulprit", (n) => plural(n, "blame"), countAxis, true);
|
|
432
|
+
}
|
|
388
433
|
// The day's merged outcomes per role, the success sibling of chartMergeFailuresByRole,
|
|
389
434
|
// drawn exactly like it (same roleStackChart shape, palette, legend order and integer count
|
|
390
435
|
// axis) so the two read as one pair.
|
|
@@ -415,6 +460,62 @@ export const GUI_CLIENT_REPORT_CHART_JS = String.raw `
|
|
|
415
460
|
function mergeShareText(t) {
|
|
416
461
|
return "of which merge runs: " + t.mergeRuns + " runs" + ((t.mergeFailed || 0) > 0 ? " (" + t.mergeFailed + " failed)" : "") + " · " + fmtTokens(t.mergeTokens || 0) + " tokens · " + fmtUsd(t.mergeCostUsd || 0) + " (reviewer + conflict resolution)";
|
|
417
462
|
}
|
|
463
|
+
|
|
464
|
+
// The instance-id matcher for the cost-per-commit lines below, the same pattern
|
|
465
|
+
// gui-client-labels.ts uses, named distinctly from that region's INSTANCE_ID so the
|
|
466
|
+
// concatenated script has no duplicate const. Interpolated from the server's
|
|
467
|
+
// INSTANCE_ID_SOURCE so the base-role fold cannot drift from baseRoleOf.
|
|
468
|
+
const COST_INSTANCE_ID = new RegExp(${JSON.stringify(INSTANCE_ID_SOURCE)});
|
|
469
|
+
|
|
470
|
+
// Cost per merged commit by base role, the browser twin of report-render.ts's
|
|
471
|
+
// costPerCommitPairs: spend folded to its base role (feature-2 joins feature), commits read
|
|
472
|
+
// as-is (already base-keyed). mergeDone is [role, cost per commit, commits] for roles with
|
|
473
|
+
// both, ranked by cost per commit desc then name asc; idle is [role, cost] for roles that
|
|
474
|
+
// spent and merged nothing, ranked by spend. pick selects another per-role-id spend map
|
|
475
|
+
// (e.g. notionalByRole); a role with commits but no spend in the picked map appears in
|
|
476
|
+
// neither list.
|
|
477
|
+
function costPerCommitParts(series, pick) {
|
|
478
|
+
const cost = {};
|
|
479
|
+
const commits = {};
|
|
480
|
+
for (const d of series) {
|
|
481
|
+
const pickSpend = pick || ((day) => day.costByRole);
|
|
482
|
+
for (const [id, c] of Object.entries(pickSpend(d) || {})) {
|
|
483
|
+
const m = COST_INSTANCE_ID.exec(id);
|
|
484
|
+
const base = m ? m[1] : id;
|
|
485
|
+
cost[base] = (Object.hasOwn(cost, base) ? cost[base] : 0) + c;
|
|
486
|
+
}
|
|
487
|
+
for (const [role, n] of Object.entries(d.commitsByRole || {})) {
|
|
488
|
+
commits[role] = (Object.hasOwn(commits, role) ? commits[role] : 0) + n;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
const mergeDone = [];
|
|
492
|
+
const idle = [];
|
|
493
|
+
for (const [role, c] of Object.entries(cost)) {
|
|
494
|
+
if (!(c > 0)) continue;
|
|
495
|
+
const n = commits[role] || 0;
|
|
496
|
+
if (n > 0) mergeDone.push([role, c / n, n]);
|
|
497
|
+
else idle.push([role, c]);
|
|
498
|
+
}
|
|
499
|
+
mergeDone.sort(byTotalThenName);
|
|
500
|
+
idle.sort(byTotalThenName);
|
|
501
|
+
return { mergeDone, idle };
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// One "label: role — cost (n) · role — cost, no commits" line from costPerCommitParts, the
|
|
505
|
+
// browser twin of report-render.ts's costPerCommitLine; "-" when nothing to show.
|
|
506
|
+
function costPerCommitText(label, series, pick) {
|
|
507
|
+
const parts = costPerCommitParts(series, pick);
|
|
508
|
+
const lines = [];
|
|
509
|
+
for (const [r, per, n] of parts.mergeDone) lines.push(r + " — " + fmtUsd(per) + " (" + n + ")");
|
|
510
|
+
for (const [r, c] of parts.idle) lines.push(r + " — " + fmtUsd(c) + ", no commits");
|
|
511
|
+
return label + ": " + (lines.length === 0 ? "-" : lines.join(" · "));
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// The Estimated per commit line, over notionalByRole, the browser twin of
|
|
515
|
+
// renderReportMarkdown's notional-gated line; the caller decides when it prints.
|
|
516
|
+
function estimatedPerCommitText(series) {
|
|
517
|
+
return costPerCommitText("Estimated per commit", series, (d) => d.notionalByRole);
|
|
518
|
+
}
|
|
418
519
|
// The per-day filing-stamp twin of chartBacklogItems: plans added and bugs filed, the same
|
|
419
520
|
// two colors as the done chart so the opened and done charts read as a pair. A day with
|
|
420
521
|
// neither carries an empty slot, and zero segments draw no rect.
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* CLI; the since shape draws the returned Markdown instead of the charts. Failures renders
|
|
10
10
|
* the failure digest (/api/failures, the Markdown `tumwater report --failures` prints) through
|
|
11
11
|
* the page's Markdown renderer, over the same kind of window and Role pickers. The Failures view
|
|
12
|
-
* also paints the current flake quarantine below the digest, the
|
|
13
|
-
*
|
|
12
|
+
* also paints the current flake quarantine below the digest, the interactive release twin of
|
|
13
|
+
* `tumwater quarantine --release <n>` (each row's Release button). Both fetch when
|
|
14
14
|
* their view opens, on Refresh, and — Usage only, throttled — when new events arrive while it
|
|
15
15
|
* is open. On a trailing window, ◀ Earlier / Later ▶ step it back by its own length through
|
|
16
16
|
* `?sinceMs=&untilMs=`, the twin of `report --since --until`. The chart builders are pure and
|
|
@@ -206,6 +206,12 @@ export const GUI_CLIENT_REPORT_JS = String.raw `// report-tip:start
|
|
|
206
206
|
async function fetchView(throttled) {
|
|
207
207
|
if (throttled && Date.now() - fetchedAt < 60000) return;
|
|
208
208
|
fetchedAt = Date.now();
|
|
209
|
+
// A re-render replaces every Release button with a fresh "Release" label, so the armed
|
|
210
|
+
// state (whichever button previously carried "Click again to release") no longer refers
|
|
211
|
+
// to a live button — clear it just before the rebuild, or a single post-render click
|
|
212
|
+
// would pass for the second. Only a fetch that actually rebuilds does this: a throttled
|
|
213
|
+
// fetch that returns early above must not clear a button the user has armed.
|
|
214
|
+
disarmQuarantineRelease();
|
|
209
215
|
buildOnce(o.panelId, viewHead(o.title, o.blurb, o.pickerId, o.choices, days, o.refreshId, o.rolePickerId, o.sinceChoices) +
|
|
210
216
|
"<div id='" + o.bodyId + "'><div class='empty'>Loading…</div></div>");
|
|
211
217
|
// Exactly one of the two controls is active: a live since window clears the day buttons
|
|
@@ -235,6 +241,17 @@ export const GUI_CLIENT_REPORT_JS = String.raw `// report-tip:start
|
|
|
235
241
|
fetchView();
|
|
236
242
|
});
|
|
237
243
|
container.addEventListener("click", (ev) => {
|
|
244
|
+
// The flake-quarantine card's Release buttons (Failures view): the first click arms,
|
|
245
|
+
// the second posts and re-fetches only after the post resolves (quarantineReleaseClick)
|
|
246
|
+
// so the released row disappears; any other click disarms.
|
|
247
|
+
const rel = clickClosest(ev, "[data-action='quarantine-release']");
|
|
248
|
+
if (rel) {
|
|
249
|
+
quarantineReleaseClick(rel,
|
|
250
|
+
(test) => postAction("/api/quarantine-release", { test }, (d) => d && d.message),
|
|
251
|
+
fetchView);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
disarmQuarantineRelease();
|
|
238
255
|
const st = clickClosest(ev, "[data-step]");
|
|
239
256
|
if (st) {
|
|
240
257
|
if (st.dataset.step === "earlier") {
|
|
@@ -310,16 +327,15 @@ export const GUI_CLIENT_REPORT_JS = String.raw `// report-tip:start
|
|
|
310
327
|
"</h2><span class='muted'>" + esc(sub || "per day") + "</span></header><div class='chart'>" + svg + "</div></section>";
|
|
311
328
|
// A scoped report drops to the four scoped tiles and says what it left out, mirroring the
|
|
312
329
|
// CLI's --role report. The team-wide render is unchanged.
|
|
313
|
-
return "<div class='stats" + (d.role ? "" : " six") + "'>" + reportSummary(d) + "</div>" + (d.totals.mergeRuns > 0 ? "<p class='muted merge-share'>" + esc(mergeShareText(d.totals)) + "</p>" : "") + "<div class='grid-2 even'>" +
|
|
330
|
+
return "<div class='stats" + (d.role ? "" : " six") + "'>" + reportSummary(d) + "</div>" + (d.totals.mergeRuns > 0 ? "<p class='muted merge-share'>" + esc(mergeShareText(d.totals)) + "</p>" : "") + "<p class='muted cost-per-commit'>" + esc(costPerCommitText("Cost per commit", d.series)) + "</p>" + (d.totals.notionalCostUsd > 0 ? "<p class='muted cost-per-commit'>" + esc(estimatedPerCommitText(d.series)) + "</p>" : "") + "<div class='grid-2 even'>" +
|
|
314
331
|
block("Work vs maintenance commits", chartCommitTiers(d), "per day · work + maintenance") + block("Merged commits", chartCommits(d)) + block("Commits by role", chartCommitsByRole(d)) + (seriesHasBacklogAdded(d) ? block("Backlog items opened", chartBacklogItemsOpened(d), "per day · plans added + bugs filed") : "") + (seriesHasBacklogItems(d) ? block("Backlog items done", chartBacklogItems(d), "per day · features done + bugs fixed") : "") + (seriesHasBacklogLagMedian(d) ? block("Backlog lag median", chartBacklogLagMedian(d), "per day · filing to done") : "") + block("Runs by role", chartRunsByRole(d)) +
|
|
315
|
-
block("Output tokens", chartTokens(d)) + block("Tokens by role", chartTokensByRole(d)) + block("Spend by role", chartCostByRole(d)) +
|
|
332
|
+
block("Output tokens", chartTokens(d)) + block("Tokens by role", chartTokensByRole(d)) + (seriesHasAny(d, "promptTokensByRole") ? block("Prompt tokens by role", chartPromptTokensByRole(d)) : "") + (seriesHasAny(d, "cacheReadTokensByRole") ? block("Cached prompt tokens by role", chartCacheReadTokensByRole(d)) : "") + block("Spend by role", chartCostByRole(d)) +
|
|
316
333
|
(!d.role && seriesHasAny(d, "costByProvider") ? block("Spend by provider", chartCostByProvider(d)) : "") +
|
|
317
334
|
block("Rejections by role", chartRejectionsByRole(d)) +
|
|
318
335
|
(seriesHasAny(d, "postMergeRejectionsByRole") ? block("Post-merge rejections by role", chartPostMergeRejectionsByRole(d)) : "") +
|
|
319
336
|
(seriesHasAny(d, "revisionsByRole") ? block("Revisions by role", chartRevisionsByRole(d)) : "") +
|
|
320
337
|
(seriesHasAny(d, "refusalsByRole") ? block("Refusals by role", chartRefusalsByRole(d)) : "") +
|
|
321
338
|
(seriesHasAny(d, "fallbacksByRole") ? block("Fallbacks by role", chartFallbacksByRole(d)) : "") +
|
|
322
|
-
(seriesHasAny(d, "imageReroutesByRole") ? block("Image reroutes by role", chartImageReroutesByRole(d)) : "") +
|
|
323
339
|
(seriesHasAny(d, "failedRunsByRole") ? block("Failed runs by role", chartFailedRunsByRole(d)) : "") +
|
|
324
340
|
(seriesHasAny(d, "mergedByRole") ? block("Merged by role", chartMergedByRole(d)) : "") +
|
|
325
341
|
(!d.role && seriesHasAny(d, "runsByModel") ? block("Runs by model", chartRunsByModel(d)) : "") +
|
|
@@ -330,6 +346,8 @@ export const GUI_CLIENT_REPORT_JS = String.raw `// report-tip:start
|
|
|
330
346
|
(seriesHasCheckMedian(d) ? block("Merge check median", chartCheckMedian(d), "per day · passing full-suite checks") : "") +
|
|
331
347
|
(seriesHasCheckFailures(d) ? block("Red merge checks", chartCheckFailures(d), "per day · failed full-suite checks") : "") +
|
|
332
348
|
(seriesHasMainRedMs(d) ? block("Main red time", chartMainRedMs(d), "per day · no passing full check") : "") +
|
|
349
|
+
(!d.role && seriesHasAny(d, "mainRedByTest") ? block("Main red by test", chartMainRedByTest(d), "per day · failing tests main's red checks named") : "") +
|
|
350
|
+
(!d.role && seriesHasAny(d, "mainRedByCulprit") ? block("Main red by merge", chartMainRedByMerge(d), "per day · merges the main watch blamed") : "") +
|
|
333
351
|
(seriesHasMergeWaitMedian(d) ? block("Merge wait median", chartMergeWaitMedian(d), "per day · enqueue to merged on main") : "") +
|
|
334
352
|
(seriesHasMergeSlotMedian(d) ? block("Merge slot median", chartMergeSlotMedian(d), "per day · verification start to outcome") : "") +
|
|
335
353
|
(seriesHasRunMedian(d) ? block("Run duration median", chartRunMedian(d), "per day · run_start to run_end") : "") +
|
|
@@ -350,12 +368,53 @@ export const GUI_CLIENT_REPORT_JS = String.raw `// report-tip:start
|
|
|
350
368
|
|
|
351
369
|
// report-quarantine:start
|
|
352
370
|
// The Failures view's flake-quarantine list, painted as its own card below the digest: the
|
|
353
|
-
//
|
|
354
|
-
// or absent section (a clean quarantine) renders nothing, so a window with a digest
|
|
355
|
-
// quarantine is unchanged.
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
371
|
+
// interactive twin of the doctor's flaky-checks row (whose Release buttons land here). An
|
|
372
|
+
// empty or absent section (a clean quarantine) renders nothing, so a window with a digest
|
|
373
|
+
// but no quarantine is unchanged. The card is structured ({ warning, rows } from
|
|
374
|
+
// /api/failures) rather than Markdown, so each entry carries a Release button that runs the
|
|
375
|
+
// same releaseQuarantinedTest core as tumwater quarantine --release <n>.
|
|
376
|
+
let quarantineReleaseArmed = null;
|
|
377
|
+
// Restore the armed button's label and clear the arm. Safe when nothing is armed, and safe
|
|
378
|
+
// after a re-render rebuilds the Failures body (the armed element is detached then, so its
|
|
379
|
+
// label is moot); keeps the card from showing a stale "Click again to release" on a button
|
|
380
|
+
// the user no longer intends to commit.
|
|
381
|
+
function disarmQuarantineRelease() {
|
|
382
|
+
if (quarantineReleaseArmed) {
|
|
383
|
+
quarantineReleaseArmed.textContent = "Release";
|
|
384
|
+
quarantineReleaseArmed = null;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
function quarantineCardHtml(q) {
|
|
388
|
+
const rows = (q && q.rows) || [];
|
|
389
|
+
if (!rows || rows.length === 0) return "";
|
|
390
|
+
return "<article class='card md'><h2>Flake quarantine</h2>" +
|
|
391
|
+
(q.warning ? "<p>" + esc(q.warning) + "</p>" : "") +
|
|
392
|
+
"<ul>" + rows.map((r) =>
|
|
393
|
+
"<li>" + esc(r.test) + " — " + esc(r.detail) +
|
|
394
|
+
"<button type='button' class='btn btn-sm' data-action='quarantine-release' data-test='" +
|
|
395
|
+
esc(r.test) + "'>Release</button></li>",
|
|
396
|
+
).join("") + "</ul></article>";
|
|
397
|
+
}
|
|
398
|
+
// A Release click on the card: the first click on a row arms it (relabelling to confirm,
|
|
399
|
+
// like the other committing reaps); a second click on that same row posts the release and
|
|
400
|
+
// only then re-fetches, so the released row disappears once the server has released it; any
|
|
401
|
+
// other click, or a re-render, disarms and restores the armed button's label. Module state
|
|
402
|
+
// plus the injected post/refetch, so the browser's wireClicks and the test drive one
|
|
403
|
+
// decision. Returns "armed" / "released" / "disarmed".
|
|
404
|
+
async function quarantineReleaseClick(rel, post, refetch) {
|
|
405
|
+
const test = rel.dataset.test || "";
|
|
406
|
+
if (!test) return "disarmed";
|
|
407
|
+
if (quarantineReleaseArmed !== rel) {
|
|
408
|
+
disarmQuarantineRelease();
|
|
409
|
+
quarantineReleaseArmed = rel;
|
|
410
|
+
rel.textContent = "Click again to release";
|
|
411
|
+
return "armed";
|
|
412
|
+
}
|
|
413
|
+
quarantineReleaseArmed = null;
|
|
414
|
+
rel.textContent = "Release";
|
|
415
|
+
await post(test);
|
|
416
|
+
refetch();
|
|
417
|
+
return "released";
|
|
359
418
|
}
|
|
360
419
|
// report-quarantine:end
|
|
361
420
|
|
|
@@ -30,6 +30,14 @@ export const GUI_CLIENT_ROLES_JS = String.raw `
|
|
|
30
30
|
// abort fires, while a first click on another role's abort re-arms instead of posting.
|
|
31
31
|
const confirmAbortSlot = confirmSlot(rerender);
|
|
32
32
|
const abortConfirming = (role) => confirmAbortSlot.armed(role);
|
|
33
|
+
// The abort-all slot: keyed "all" (no role id), so a first click on any other action's
|
|
34
|
+
// button disarms a pending abort-all — the same per-key arming as the per-role slot above.
|
|
35
|
+
const confirmAbortAllSlot = confirmSlot(rerender);
|
|
36
|
+
const abortAllConfirming = () => confirmAbortAllSlot.armed("all");
|
|
37
|
+
// The Roles card header's Abort all button (inserted into the card head at script load):
|
|
38
|
+
// one click arms it, a second within 5 s hits POST /api/abort-all. Danger tone; the title
|
|
39
|
+
// names the confirm-again contract, like the per-row stop's.
|
|
40
|
+
const rolesAbortAllButton = () => "<button type='button' class='btn btn-sm btn-danger' id='abortall' data-action='abort-all' title='Click again to stop every running role's run'>" + icon("stop") + "<span>Abort all</span></button>";
|
|
33
41
|
// The drawer's Disable slot: a second click on the same role's Disable fires, a first click
|
|
34
42
|
// on another role's re-arms — the same per-role keying as the abort slot it sits beside.
|
|
35
43
|
const confirmDisableSlot = confirmSlot(rerender);
|
|
@@ -44,6 +52,21 @@ export const GUI_CLIENT_ROLES_JS = String.raw `
|
|
|
44
52
|
// wake, pause/resume (the target state is always explicit), and abort, which discards the
|
|
45
53
|
// run's work — the first click arms it, a second within 5 s fires it.
|
|
46
54
|
async function rowAction(action, role, forSeconds) {
|
|
55
|
+
if (action === "abort-all") {
|
|
56
|
+
if (!abortAllConfirming()) {
|
|
57
|
+
// First click arms it; any other click (handled below) disarms it.
|
|
58
|
+
confirmAbortAllSlot.arm("all");
|
|
59
|
+
confirmAbortSlot.disarm();
|
|
60
|
+
confirmDisableSlot.disarm();
|
|
61
|
+
roleChooser = null;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
confirmAbortAllSlot.disarm();
|
|
65
|
+
await postAction("/api/abort-all", {}, (d) =>
|
|
66
|
+
(d && typeof d.message === "string" ? d.message : "abort requested for every running role"));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
confirmAbortAllSlot.disarm();
|
|
47
70
|
if (action === "prompt") {
|
|
48
71
|
setTarget(role);
|
|
49
72
|
focusComposer();
|
|
@@ -105,7 +128,7 @@ export const GUI_CLIENT_ROLES_JS = String.raw `
|
|
|
105
128
|
const detail = phaseDetailBase(l, info, d);
|
|
106
129
|
const w = scheduledWakeFor(l, d);
|
|
107
130
|
if (!w) return detail;
|
|
108
|
-
return (detail ? detail + " · " : "") + "your wake in " + humanSeconds(
|
|
131
|
+
return (detail ? detail + " · " : "") + "your wake in " + humanSeconds(secsUntil(w.atMs, Date.now()));
|
|
109
132
|
}
|
|
110
133
|
function phaseDetailBase(l, info, d) {
|
|
111
134
|
const raw = fmtNextRun(l, d.running);
|
|
@@ -114,18 +137,22 @@ export const GUI_CLIENT_ROLES_JS = String.raw `
|
|
|
114
137
|
const backoff = next.startsWith("sleeping ") ? "sleeping — next check in " + next.slice(9) : "";
|
|
115
138
|
// A yield-scaled clock: the role's recent runs merged nothing, so it runs less often.
|
|
116
139
|
const note = slowed ? " · slowed ×" + slowed[1] + ", nothing merged lately" : "";
|
|
117
|
-
// The consecutive no-change
|
|
118
|
-
// status-model rolePhase's
|
|
119
|
-
// returned nothing to do. Shown only on a resting idle cell
|
|
120
|
-
// deferred) — never on live work, a pause, a failure, or main-red,
|
|
121
|
-
// label likewise leaves bare.
|
|
122
|
-
//
|
|
123
|
-
|
|
140
|
+
// The consecutive cut-off and no-change streaks (status-payload cutOffStreak/noChangeStreak,
|
|
141
|
+
// mirrored from status-model rolePhase's suffix): how many completed runs in a row hit the
|
|
142
|
+
// context ceiling or returned nothing to do. Shown only on a resting idle cell
|
|
143
|
+
// (sleeping/queued/idle/deferred) — never on live work, a pause, a failure, or main-red,
|
|
144
|
+
// which the status label likewise leaves bare. A cut-off run is also a no_change, so the
|
|
145
|
+
// cut-off streak wins when both read 2+ — it names the costly cause the plain suffix would
|
|
146
|
+
// bury. Observational only, like the phase text.
|
|
147
|
+
const cutOff = ["sleeping", "queued", "idle", "deferred"].includes(info.key) && (l.cutOffStreak || 0) >= 2
|
|
148
|
+
? " · cut off ×" + l.cutOffStreak : "";
|
|
149
|
+
const noChange = !cutOff && ["sleeping", "queued", "idle", "deferred"].includes(info.key) && (l.noChangeStreak || 0) >= 2
|
|
124
150
|
? " · no change ×" + l.noChangeStreak : "";
|
|
125
|
-
|
|
126
|
-
if (info.key === "
|
|
127
|
-
if (info.key === "
|
|
128
|
-
return info.detail +
|
|
151
|
+
const streak = cutOff || noChange;
|
|
152
|
+
if (info.key === "sleeping") return (backoff || (next === "-" ? "" : next === "now" ? "wakes now" : "wakes in " + next)) + note + streak;
|
|
153
|
+
if (info.key === "queued") return (backoff || info.detail) + note + streak;
|
|
154
|
+
if (info.key === "paused" && d.pausedRolesUntil && !d.paused && (d.pausedLoops || []).includes(l.role)) return info.detail + " · resumes in " + humanSeconds(secsUntil(d.pausedRolesUntil, Date.now())); // retired-name-ok: serialized /api key
|
|
155
|
+
return info.detail + streak;
|
|
129
156
|
}
|
|
130
157
|
function roleCells(l, d) {
|
|
131
158
|
const info = phaseInfo(l.phase);
|
|
@@ -231,6 +258,19 @@ export const GUI_CLIENT_ROLES_JS = String.raw `
|
|
|
231
258
|
postAction("/api/reset-counters", {}, (d) => (d && typeof d.message === "string" ? d.message : "counters reset")));
|
|
232
259
|
$("wakeall").addEventListener("click", () =>
|
|
233
260
|
postAction("/api/wake", {}, (d) => (d && typeof d.message === "string" ? d.message : "every role woken")));
|
|
261
|
+
// Insert the Abort all button into the Roles card header beside the card title/refresh
|
|
262
|
+
// control (the same card head that holds Reset counters / Wake later / Wake all), and arm
|
|
263
|
+
// its two-click confirm through the shared rowAction abort-all branch. One insert on load;
|
|
264
|
+
// re-renders leave the header alone.
|
|
265
|
+
(function () {
|
|
266
|
+
const head = $("roles-title") && $("roles-title").parentElement;
|
|
267
|
+
if (!head || document.getElementById("abortall")) return;
|
|
268
|
+
head.insertAdjacentHTML("beforeend", rolesAbortAllButton());
|
|
269
|
+
document.getElementById("abortall").addEventListener("click", (ev) => {
|
|
270
|
+
ev.preventDefault();
|
|
271
|
+
rowAction("abort-all", "all");
|
|
272
|
+
});
|
|
273
|
+
})();
|
|
234
274
|
// wake-later:start
|
|
235
275
|
const WAKE_LATER_CHOICES = [[1800, "In 30 minutes"], [3600, "In 1 hour"], [7200, "In 2 hours"], [28800, "In 8 hours"]];
|
|
236
276
|
function openWakeLaterMenu() {
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
* gui-client.ts's script, reaching its helpers (esc, $, getJson, postJson, showFlash) through
|
|
12
12
|
* that concatenation. A second card, "Health check", runs the same checks as `tumwater doctor`
|
|
13
13
|
* through GET /api/doctor on each open (and on its Re-run button) and paints the report (its
|
|
14
|
-
* Re-probe models button POSTs /api/doctor-reprobe instead, spending the image probes; its
|
|
15
14
|
* Commit backlog button POSTs /api/doctor-commit-backlog instead, the same commitUncommittedBacklog
|
|
16
|
-
* `tumwater doctor --commit-backlog` runs
|
|
15
|
+
* `tumwater doctor --commit-backlog` runs; its Archive backlog button POSTs
|
|
16
|
+
* /api/doctor-archive-backlog instead, the same archiveFinishedOnMain `tumwater archive` runs); a
|
|
17
17
|
* third, "Disabled roles", lists every disabled role (GET /api/retire), each with an Enable
|
|
18
18
|
* button (POST /api/role-enable) and — for a role that still keeps worktree/branch leftovers — a
|
|
19
19
|
* two-click Retire button (POST /api/retire). */
|
|
@@ -73,12 +73,12 @@ export const GUI_CLIENT_SETTINGS_JS = String.raw ` // settings-view:start
|
|
|
73
73
|
"<span>The same checks as <code>tumwater doctor</code>.</span>" +
|
|
74
74
|
"<label><input type='checkbox' data-action='doctor-problems'> Problems only</label>" +
|
|
75
75
|
"<button type='button' class='btn btn-sm' data-action='doctor-rerun'>Re-run</button>" +
|
|
76
|
-
"<button type='button' class='btn btn-sm' data-action='doctor-reprobe' title='Re-probe every image-input model (spends one model call each), like tumwater doctor --reprobe'>Re-probe models</button>" +
|
|
77
76
|
"<button type='button' class='btn btn-sm' data-action='doctor-kill-orphans'>Kill orphans</button>" +
|
|
78
77
|
"<button type='button' class='btn btn-sm' data-action='doctor-drop-broken-pins' title=\"Drops each refs/tumwater/ pin whose commit is gone - the manual git update-ref -d the tumwater pins row names, like tumwater doctor --drop-broken-pins\">Drop broken pins</button>" +
|
|
79
78
|
"<button type='button' class='btn btn-sm' data-action='doctor-commit-backlog' title='Commit each uncommitted tracked backlog or brief file on main, like tumwater doctor --commit-backlog'>Commit backlog</button>" +
|
|
80
79
|
"<button type='button' class='btn btn-sm' data-action='doctor-resync-install' title=\"Re-sync the root checkout's install after a failed post-merge re-sync, like tumwater doctor --resync-install\">Re-sync install</button>" +
|
|
81
|
-
"<button type='button' class='btn btn-sm' data-action='doctor-retire-leftovers' title=\"Retire every disabled role's leftover worktree, branch and merge ref in one pass, like tumwater doctor --retire-leftovers (or tumwater retire --role <id> per role)\">Retire leftovers</button
|
|
80
|
+
"<button type='button' class='btn btn-sm' data-action='doctor-retire-leftovers' title=\"Retire every disabled role's leftover worktree, branch and merge ref in one pass, like tumwater doctor --retire-leftovers (or tumwater retire --role <id> per role)\">Retire leftovers</button>" +
|
|
81
|
+
"<button type='button' class='btn btn-sm' data-action='doctor-archive-backlog' title=\"Move the finished entries under PLANS.md ## Done and BUGS.md ## Fixed into the archive files and commit them on main, like tumwater archive\">Archive backlog</button></div>" +
|
|
82
82
|
"<div id='doctorbody'><div class='empty'>Checking…</div></div></div>" +
|
|
83
83
|
"<div class='card' id='retirecard'><div class='empty'>Loading…</div></div>" +
|
|
84
84
|
"<div class='card' id='briefcard'><div class='empty'>Loading…</div></div>");
|
|
@@ -97,7 +97,7 @@ export const GUI_CLIENT_SETTINGS_JS = String.raw ` // settings-view:start
|
|
|
97
97
|
"<div class='settings-row'><span class='pill t-" + (tones[c.level] || "amber") + "'>" + esc(c.level) + "</span>" +
|
|
98
98
|
"<strong>" + esc(c.name) + "</strong><span>" + esc(c.detail) + "</span></div>").join("");
|
|
99
99
|
return "<p>" + esc(d.header) + "</p>" + rows +
|
|
100
|
-
(problems && hidden > 0 ? "<p class='empty'>" +
|
|
100
|
+
(problems && hidden > 0 ? "<p class='empty'>" + plural(hidden, "ok check") + " hidden</p>" : "") +
|
|
101
101
|
"<p>" + esc(d.verdict) + "</p>";
|
|
102
102
|
}
|
|
103
103
|
// The last resolved doctor report and whether the card paints only its problems; the
|
|
@@ -117,27 +117,6 @@ export const GUI_CLIENT_SETTINGS_JS = String.raw ` // settings-view:start
|
|
|
117
117
|
body.innerHTML = errorPanel("Health checks could not run", err, "empty");
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
|
-
let doctorReprobing = false;
|
|
121
|
-
async function reprobeDoctorClick() {
|
|
122
|
-
if (doctorReprobing) return;
|
|
123
|
-
doctorReprobing = true;
|
|
124
|
-
try {
|
|
125
|
-
buildSettingsView();
|
|
126
|
-
const body = $("doctorbody");
|
|
127
|
-
body.innerHTML = "<div class='empty'>Re-probing…</div>";
|
|
128
|
-
showFlash("Re-probing image-input models…");
|
|
129
|
-
try {
|
|
130
|
-
const report = await postJson("/api/doctor-reprobe", {});
|
|
131
|
-
lastDoctor = report;
|
|
132
|
-
body.innerHTML = renderDoctorReport(report, doctorProblems);
|
|
133
|
-
} catch (err) {
|
|
134
|
-
body.innerHTML = errorPanel("Health checks could not run", err, "empty");
|
|
135
|
-
showFlash("error: " + clientErrorMessage(err));
|
|
136
|
-
}
|
|
137
|
-
} finally {
|
|
138
|
-
doctorReprobing = false;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
120
|
function toggleDoctorProblems(checked) {
|
|
142
121
|
doctorProblems = checked;
|
|
143
122
|
const body = $("doctorbody");
|
|
@@ -280,6 +259,32 @@ export const GUI_CLIENT_SETTINGS_JS = String.raw ` // settings-view:start
|
|
|
280
259
|
}
|
|
281
260
|
await fetchDoctor();
|
|
282
261
|
}
|
|
262
|
+
// The Health card's Archive backlog button: a two-click confirm, then POST
|
|
263
|
+
// /api/doctor-archive-backlog runs the same archive as "tumwater archive" (holding the merge
|
|
264
|
+
// lock) and the card re-runs its checks so the backlog archive row repaints. ok is false
|
|
265
|
+
// when the archive was skipped (an off-base checkout or uncommitted backlog/archive files),
|
|
266
|
+
// and its reason still flashes so the operator sees why nothing moved.
|
|
267
|
+
function paintArchiveBacklogButton() {
|
|
268
|
+
const btn = document.querySelector("button[data-action='doctor-archive-backlog']");
|
|
269
|
+
if (btn) btn.textContent = archiveBacklogSlot.armed("archive-backlog") ? "Confirm archive" : "Archive backlog";
|
|
270
|
+
}
|
|
271
|
+
const archiveBacklogSlot = confirmSlot(paintArchiveBacklogButton);
|
|
272
|
+
async function archiveBacklogClick() {
|
|
273
|
+
if (!archiveBacklogSlot.armed("archive-backlog")) {
|
|
274
|
+
archiveBacklogSlot.arm("archive-backlog");
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
archiveBacklogSlot.disarm();
|
|
278
|
+
paintArchiveBacklogButton();
|
|
279
|
+
showFlash("Archiving the finished backlog entries…");
|
|
280
|
+
try {
|
|
281
|
+
const d = await postJson("/api/doctor-archive-backlog", {});
|
|
282
|
+
showFlash((d.ok ? "" : "error: ") + d.message);
|
|
283
|
+
} catch (err) {
|
|
284
|
+
showFlash("error: " + clientErrorMessage(err));
|
|
285
|
+
}
|
|
286
|
+
await fetchDoctor();
|
|
287
|
+
}
|
|
283
288
|
// The Health card's Commit backlog button: a single click, because the commit is the doctor's
|
|
284
289
|
// own recommended repair and is reversible (unlike the two-click Kill orphans arm), POSTing
|
|
285
290
|
// /api/doctor-commit-backlog — the same commitUncommittedBacklog "tumwater doctor
|
|
@@ -29,7 +29,7 @@ export const GUI_CLIENT_TEAM_JS = String.raw ` // ---- sidebar: project, team s
|
|
|
29
29
|
// label style. One home for the rule, so the preformatted badges the payload ships to the
|
|
30
30
|
// sidebar (disk, bootstrap, main check) cannot drift in how the row presents them.
|
|
31
31
|
const stripBadge = (badge) => String(badge || "").replace(/^ · /, "");
|
|
32
|
-
const badgeLabel = (badge) => stripBadge(badge)
|
|
32
|
+
const badgeLabel = (badge) => capitalize(stripBadge(badge));
|
|
33
33
|
function renderSidebar(d) {
|
|
34
34
|
const project = $("project");
|
|
35
35
|
const name = d && d.project ? d.project : "";
|
|
@@ -49,7 +49,7 @@ export const GUI_CLIENT_TEAM_JS = String.raw ` // ---- sidebar: project, team s
|
|
|
49
49
|
// decoration.
|
|
50
50
|
const lead = stale ? restartButton(icon("refresh")) : icon("refresh");
|
|
51
51
|
rows += row(stale ? (d.build.restartBlocked ? "amber" : "blue") : "", lead,
|
|
52
|
-
"Version <span class='mono'>" + esc(shortSha(d.build.sha)) + "</span>" + (stale ? " · " + esc(d.build.aheadCommits || 0) + " behind" : ""),
|
|
52
|
+
"Version <span class='mono'>" + esc(shortSha(d.build.sha)) + "</span>" + (stale ? " · " + esc(plural(d.build.aheadCommits || 0, "commit")) + " behind" : ""),
|
|
53
53
|
localizeInstants(("Running version" + (d.buildBadge || "").replace(/^, build/, "")).trim()));
|
|
54
54
|
}
|
|
55
55
|
if (d.mainCheck) {
|
|
@@ -150,6 +150,7 @@ export const GUI_CLIENT_TEAM_JS = String.raw ` // ---- sidebar: project, team s
|
|
|
150
150
|
// sidebar:end
|
|
151
151
|
|
|
152
152
|
// ---- alerts ----
|
|
153
|
+
// alerts:start
|
|
153
154
|
// Alerts are patched by key, and only their text is rewritten in place: a stall alert's
|
|
154
155
|
// "no pi output for 9m47s" updates every second, and rebuilding it would swap its buttons out
|
|
155
156
|
// from under a click in progress.
|
|
@@ -171,6 +172,9 @@ export const GUI_CLIENT_TEAM_JS = String.raw ` // ---- sidebar: project, team s
|
|
|
171
172
|
const alerts = pageAlerts(d, offline);
|
|
172
173
|
const box = $("alerts");
|
|
173
174
|
const keep = new Set(alerts.map((a) => a.key));
|
|
175
|
+
// doctor-band is managed below, not by the alert-patch loop (its key would not survive a
|
|
176
|
+
// keep pass built only from the feed's alert keys).
|
|
177
|
+
keep.add("doctor-band");
|
|
174
178
|
for (const el of Array.from(box.children)) if (!keep.has(el.dataset.key)) el.remove();
|
|
175
179
|
alerts.forEach((a, i) => {
|
|
176
180
|
const parts = alertParts(a, a.key === "build" && d && d.build && d.build.stale);
|
|
@@ -190,6 +194,27 @@ export const GUI_CLIENT_TEAM_JS = String.raw ` // ---- sidebar: project, team s
|
|
|
190
194
|
detail.hidden = !a.detail;
|
|
191
195
|
if (box.children[i] !== el) box.insertBefore(el, box.children[i] || null);
|
|
192
196
|
});
|
|
197
|
+
// The band's one link to where every alert's detail lives: the Settings view's Health check
|
|
198
|
+
// card (#doctorcard), which runs the same checks as tumwater doctor. It rides the existing
|
|
199
|
+
// view/hash routing (data-act='view' data-arg='settings' -> location.hash; gui-client-boot).
|
|
200
|
+
// It appears only while the band has alerts — an empty feed paints nothing, so there is no
|
|
201
|
+
// "0 alerts" copy.
|
|
202
|
+
const doctorEl = box.querySelector("[data-key='doctor-band']");
|
|
203
|
+
if (alerts.length > 0) {
|
|
204
|
+
if (!doctorEl) {
|
|
205
|
+
const band = document.createElement("div");
|
|
206
|
+
band.dataset.key = "doctor-band";
|
|
207
|
+
band.className = "alert t-blue";
|
|
208
|
+
band.innerHTML = "<span class='alert-icon'>" + icon("settings") + "</span>" +
|
|
209
|
+
"<div class='alert-body'><div class='alert-title'>Details in Doctor</div>" +
|
|
210
|
+
"<div class='alert-detail'>The Settings view's Health check card keeps each alert's detail.</div></div>" +
|
|
211
|
+
"<div class='alert-actions'><button type='button' class='btn btn-sm' data-act='view' data-arg='settings' " +
|
|
212
|
+
"title='Open the Health check card'>Open Health check</button></div>";
|
|
213
|
+
box.appendChild(band);
|
|
214
|
+
}
|
|
215
|
+
} else if (doctorEl) {
|
|
216
|
+
doctorEl.remove();
|
|
217
|
+
}
|
|
193
218
|
const n = needsYou(alerts);
|
|
194
219
|
const title = (n ? "(" + n + ") " : "") + (d && d.project ? d.project + " · " : "") + "tumwater";
|
|
195
220
|
if (document.title !== title) document.title = title;
|
|
@@ -198,6 +223,7 @@ export const GUI_CLIENT_TEAM_JS = String.raw ` // ---- sidebar: project, team s
|
|
|
198
223
|
if (badge.textContent !== String(n)) badge.textContent = String(n);
|
|
199
224
|
badge.title = plural(n, "thing needs", "things need") + " you";
|
|
200
225
|
}
|
|
226
|
+
// alerts:end
|
|
201
227
|
|
|
202
228
|
// ---- today's progress ----
|
|
203
229
|
// /api/report?days=1, re-read when new events arrive (throttled) — never on a timer.
|