feat(admin): Lift A.2, surface loops and stalling models with one-click block #104

Merged
alee merged 17 commits from feat/no-progress-lift-a2 into main 2026-09-28 23:56:34 +00:00
Owner

Lift A.2: surfaces watchdog loops and stalling models in the admin portal, with the lever beside the evidence (North Star #4). Detection is unchanged; this is the surfacing half Lift A left thin.

What ships

  • Loops panel (Home, #loops): a table of open alerts with severity, session title, model, top repeated target, calls and $ since the last landed change, time open, and Block model. Alerts resolved in the last 24h are listed below, dimmed. GET /admin/api/watchdog/loops now joins each open alert to its root's latest flagged verdict and returns {alerts, resolved}.
  • Stalls by model (Models): 1h / 24h / 7d radio. Per model: distinct stalled sessions, $ in stalled sessions (MAX per session, then SUM, because the column is cumulative), traffic share as its own column, availability, and Block. New GET /admin/api/watchdog/rollup?hours=N.
  • Block with a reason, inline: an inline confirm row with a prefilled, editable reason (stalled N sessions in 24h, looping session <title>), no browser dialogs. The dropdown path now records set from Models dropdown.
  • Blocked models card: reason, blocked-since and one-click Unblock. New GET /admin/api/models/blocked.
  • Alert text carries the evidence: <title>: <calls> calls, $<cost> since last landed change on <model>. <url>#loops.

Verification

  • Full suite: 2441 passed, 1 failed (test_incumbent_routing::test_debug_log_emits_incumbent_identity, also failing on main).
  • Pinned ruff@0.16.9: no new findings in touched Python files.
  • Visual QA at 1400 and 800 px on an 8081 sandbox. The Block, Confirm, Blocked list, Unblock round trip was clicked through by hand on a sqlite-backup copy of the live DB. The live DB was never written.
  • Every time window uses julianday() on both sides; stored timestamps are ISO with T/+00:00.

Known follow-ups (not in this PR)

  • Argument-less tools such as Playwright browser_snapshot count as exact repeats. That caused one real false alert (a QA worker), which triggered and resolved correctly.
  • calls_since_landed resets on any landing in the whole session tree, not just the session's own descendants.

After merge: systemctl --user restart llm-router.service so 8080 serves the new endpoints and pages. The watchdog picks up the new alert text at its next tick.

🤖 Generated with Claude Code

https://claude.ai/code/session_01N9biTbFC63yDfYfUsZmhgd

Lift A.2: surfaces watchdog loops and stalling models in the admin portal, with the lever beside the evidence (North Star #4). Detection is unchanged; this is the surfacing half Lift A left thin. **What ships** - **Loops panel** (Home, `#loops`): a table of open alerts with severity, session title, model, top repeated target, calls and $ since the last landed change, time open, and **Block model**. Alerts resolved in the last 24h are listed below, dimmed. `GET /admin/api/watchdog/loops` now joins each open alert to its root's latest flagged verdict and returns `{alerts, resolved}`. - **Stalls by model** (Models): 1h / 24h / 7d radio. Per model: distinct stalled sessions, $ in stalled sessions (MAX per session, then SUM, because the column is cumulative), traffic share as its own column, availability, and **Block**. New `GET /admin/api/watchdog/rollup?hours=N`. - **Block with a reason, inline**: an inline confirm row with a prefilled, editable reason (`stalled N sessions in 24h`, `looping session <title>`), no browser dialogs. The dropdown path now records `set from Models dropdown`. - **Blocked models** card: reason, blocked-since and one-click Unblock. New `GET /admin/api/models/blocked`. - **Alert text carries the evidence**: `<title>: <calls> calls, $<cost> since last landed change on <model>. <url>#loops`. **Verification** - Full suite: 2441 passed, 1 failed (`test_incumbent_routing::test_debug_log_emits_incumbent_identity`, also failing on `main`). - Pinned `ruff@0.16.9`: no new findings in touched Python files. - Visual QA at 1400 and 800 px on an 8081 sandbox. The Block, Confirm, Blocked list, Unblock round trip was clicked through by hand on a sqlite-backup copy of the live DB. The live DB was never written. - Every time window uses `julianday()` on both sides; stored timestamps are ISO with `T`/`+00:00`. **Known follow-ups (not in this PR)** - Argument-less tools such as Playwright `browser_snapshot` count as exact repeats. That caused one real false alert (a QA worker), which triggered and resolved correctly. - `calls_since_landed` resets on any landing in the whole session tree, not just the session's own descendants. **After merge:** `systemctl --user restart llm-router.service` so 8080 serves the new endpoints and pages. The watchdog picks up the new alert text at its next tick. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01N9biTbFC63yDfYfUsZmhgd
alee added 17 commits 2026-09-28 23:52:17 +00:00
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9biTbFC63yDfYfUsZmhgd
- Add onclick='blockModelConfirm()' to stalls table Block buttons;
  commit 6be41ff added data-* attrs but never wired the handler
- Fix toast div: id="toast class=" → id="toast" class=" (was missing
  closing quote, causing toast() JS to crash)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9biTbFC63yDfYfUsZmhgd
alee merged commit 8d532069d3 into main 2026-09-28 23:56:34 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: alee/6krrt#104