Files
6krrt/plans/router-admin-portal-plan-review.md
adlee-was-taken 3523dcf93e docs(plans): give every plan a Status line so the queue is greppable
plans/ held 58 documents and exactly one said whether it was open. The rest
mixed finished work, reviews of shipped work, parked specs and genuinely
pending ones, with nothing distinguishing them, so "how many plans are in
the queue" had no answer short of reading all 58.

Now `grep -H '^Status:' plans/*.md` is the answer:

    50 done   3 in progress   2 planned   2 reference   1 parked

Statuses were derived rather than guessed: CLAUDE.md's own built list and
"What's NOT built yet" section, plus checking the subject exists in the
code. A review of work that shipped counts as done -- it records what was
found, it is not a request for anything. `reference` separates the two docs
that are conventions rather than work items (admin-design-standards,
admin-work-framework), which otherwise read as permanently-open plans.

The vocabulary is deliberately five words. A larger one invites "mostly
done" and "blocked-ish", which is how the directory became unreadable.

test_plans_declare_status.py keeps it from rotting: a new plan without a
marker fails, as does an unknown status, one buried below the eighth line,
or an open status with no reason -- "planned" alone is the state that rots,
since nobody can tell later whether it waits on a decision, a dependency,
or just nobody's turn.

Also updates the sweep plan with what landed and what did not, including
that #9 was not a defect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRQXz5SYZYVWscxS1QqF6U
2026-09-08 18:55:16 -04:00

218 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Review: `router-admin-portal` work plan (pre-implementation)
Status: done -- review of shipped work
**What it was reviewing:** `plans/.omo/plans/router-admin-portal.md` —
a 10-todo plan to add an integrated `/admin` FastAPI web portal (dashboards
+ operator controls: refresh catalog, reseed energy, restart service,
runtime toggles, allowlisted config writes, model-availability overrides).
Reviewed before any implementation exists, against the actual current code
rather than the plan's own descriptions, and cross-checked against
`plans/admin-portal-gap-analysis.md` (a 15-finding critique of an
earlier draft, `plans/.omo/drafts/router-admin-portal.md`) to see
whether the final plan actually incorporated what that analysis found.
## Verdict: citations are excellent; one likely functional gap and one unexamined security assumption before this should be approved as-is
### Citation accuracy — verified, essentially flawless
Checked every file:line reference that matters for correctness, not a
sample:
- `dispatcher.py:154` (`cfg`), `:278` (`_db`), `:1306`/`:1353`/`:1417`
(`/health`/`/metrics`/`/events/decisions`) — all exact.
- `config.py:185` (`default_flex_preference: FlexPreference =
FlexPreference.auto`) — confirmed real field, and every one of its four
cited use sites (`dispatcher.py:815, 2364, 2446` plus the docstring
mention at `:186`) — exact.
- Todo 5's entire runtime-toggle line list — eleven citations across seven
config knobs (`circuit_breaker.enabled` at 2556/2573/2820/2823,
`log_route_decisions` at 935, `log_energy_observations` at 1261,
`verification.local_llm_enabled` at 2653/2794, `session_cache.enabled` at
2266/2336, `pinch.enabled` at 2247/2475, the `pinch.relevance` gate at
1827) — every single one checked out exact via direct grep.
One trivial slip: Todo 3 cites `dispatcher.py:2126-2125` for `/v1/models` —
a backwards range (start line > end line); the actual decorator is a single
line at `2126`. Cosmetic, not worth blocking on.
### The admin override table may not actually affect routing — verify before approving
Todo 7 creates `admin_model_overrides(model_id, provider, availability,
reason, updated_at)` specifically to survive `poller.py` overwriting
`models.availability` on every catalog refresh. That part is correctly
diagnosed: `poller.py`'s upsert really does set `availability = "active"`
on every routable row it sees, per the gap analysis's F1 (confirmed
independently, not taken on the gap analysis's word).
But Todo 7's own text only says `GET /admin/api/models` **merges** the
override into what it *returns to the dashboard*. Nowhere across Todos
1–10 is there a step that feeds `admin_model_overrides` into
`routing.select_candidates` / `routing.rejection_reason` — the functions
that actually decide what a live request routes to. As specified, a model
marked "deprecated" through the admin UI would show as deprecated on the
dashboard while the router keeps dispatching to it exactly as before,
because the hard filters still only read `models.availability` /
`models.deprecated`, which the override table never touches.
This is the same shape of defect the last review caught in
`circuit_breaker.record_success`: a feature that is structurally complete
— table, API, tests — but never reaches the code path it exists to affect.
It matters more here, because the stated purpose of this whole control
("mark a flaky model out of rotation") is exactly what was done by hand
with a raw `UPDATE models SET deprecated = 1 ...` earlier this session when
`gemma-4-31b` started failing — if the admin UI's version of that same
action doesn't reach `routing.py`, the feature doesn't do the thing it's
being built for. Add an explicit todo (or fold into Todo 7's acceptance
criteria) requiring `route()`'s candidate-filtering to consult the override
table the same way `_open_circuits` already consults `circuit_breaker.is_down`
for the circuit-breaker exclusion set, and add a test that actually routes
a request against an overridden model and asserts it's excluded — not just
that the API reports it correctly.
### Loopback-only/no-auth is inherited from a read-only posture without being re-examined for a read-write one
The plan states "loopback-only and unauthenticated, matching the router's
current security posture" as an already-made decision. The *current*
posture (`/metrics`, `/health`, SSE decisions) is read-only observability,
and loopback-only is a reasonable ACL for that. This plan adds `POST
/admin/api/restart-service`, on-disk config writes, and subprocess
triggers — a materially larger blast radius reachable by the same
unauthenticated loopback bind. Loopback-only does not mean "only the
operator can reach it": with no auth and no CSRF protection specified
anywhere in the plan, any process running as the same user, or any webpage
the operator's browser visits while the router happens to be running, can
POST to `localhost:8080/admin/api/restart-service` via a plain HTML form —
no preflight required to block a same-origin-policy-naive form POST. This
is a known category of local-dev-server attack (CSRF/DNS-rebinding against
`localhost`) that a pure read-only surface never had to consider. Worth a
deliberate decision — even if the answer stays "loopback-only is
acceptable for a personal single-operator tool" — rather than carrying the
old answer forward because it was already the answer for a different kind
of surface.
Separately, Todo 4's restart-service mechanism is stated aspirationally
("is expected to terminate the serving process; return a `restarting`
status before the process exits") without specifying how the HTTP response
gets flushed to the client before `systemctl --user restart` causes SIGTERM
to land on the same process handling that request. If the endpoint `await`s
the systemctl subprocess inline, the response can't be sent before the
process that would send it is torn down. This needs an explicit
fire-and-forget mechanism (e.g., schedule the restart via `BackgroundTasks`
so it runs after the response is already flushed) named in the plan, not
left as an implementation detail to be discovered while writing Todo 4.
### Three "Recommended" gap-analysis findings are silently absent, not declared as deferred
F9 (no audit trail for admin actions), F10 (no lock against concurrent
config.yaml writes), and F11 (no endpoint to clear circuit-breaker/session
cache state) are all "Recommended" rather than "Must-fix: Yes" in the gap
analysis's own severity table, and none appear in the final plan's
Must-have list. That's a defensible call for a personal, single-operator
tool — but none of the three are named in the plan's "Must NOT have"
section either, which is where deliberate exclusions are supposed to live.
As written, a reader of the final plan alone has no way to tell "considered
and deferred" from "the gap analysis was never actually read." Move them
there explicitly, even as a one-line "deferred: F9/F10/F11, personal-use
tool, revisit if this gets multi-operator" note.
### Cross-repo citations also check out — including the parts I could actually verify
`daashbrd` exists locally (`/home/alee/Sources/daashbrd`), so the plan's
references to it aren't unverifiable hand-waving. Checked directly:
`app/history.py` and `app/frontend/index.html` (2303 lines, matching Todo
12's "fine for a single-page <3k line file" claim in the gap analysis)
exist as cited; `app/main.py:154` is exactly the `StaticFiles.mount(...)`
call and `:303` is exactly the `FileResponse(...)` call the gap analysis's
F12 cited; `tests/test_main.py:23` and `:29` are exactly
`test_frontend_dir_is_absolute_and_exists` and
`test_static_mount_points_to_frontend_dir`, matching "mount checks."
`poller.py`'s `main()`/`__main__` at 298/319, `feedback.py` at 143/171,
`tier.py` at 82/92, and `schema.sql`'s `models` table (line 7) and its
three indexes (244-246) all match Todo 4's and Todo 7's citations exactly
as well. At this point essentially every checkable citation in the entire
plan has been verified — this is the most citation-accurate plan reviewed
in this loop so far.
### Todo 2's history store duplicates data the router already durably records
`admin_history.py` is specified as a **separate SQLite file**
(`router_admin_history.db`) fed by a **background task sampling every
60s** — a pattern lifted directly from `daashbrd`, which doesn't have
anything better to query. This router already isn't in that position:
`energy_observations` records `energy_kwh`, `cost_usd`, and
`carbon_g_co2eq` **per request, as it happens**, and `route_decisions`
records one row per routing decision — both timestamped, both already
durable. `metrics.py`'s existing `/metrics` endpoint already computes
rolling aggregates from `energy_observations` on demand, no sampling loop
required.
A bucketed time-series `GET /admin/api/history` can almost certainly be a
`GROUP BY`-bucketed SQL query against the tables that already exist,
computed on request rather than sampled every 60s into a second database.
That would avoid three problems the current spec creates and doesn't
solve: (1) a background-task lifecycle question that Todo 2 doesn't
actually answer — `admin.py` must never import `dispatcher` per Todo 1, so
nothing in the plan specifies *what* schedules this recurring task against
the running event loop or on which object's startup hook; (2) a second
SQLite file whose freshness is bounded by a 60s sampling interval instead
of being exact; (3) genuinely duplicated data. Given this router's real
traffic volume (one personal deployment; `config.yaml`'s own comments
mention total billed traffic to date as $0.07), query performance against
the existing tables is not a concern that justifies pre-aggregation. Worth
reconsidering before Wave 1, since it's foundational to Todo 2's whole
shape.
### `ruamel.yaml` is a new dependency with no todo to pin it
Todo 6 commits to "ruamel.yaml round-trip preservation" for config writes,
but `ruamel.yaml` is not in `requirements.txt` today (checked directly —
zero matches) and no todo in the plan adds it. `CLAUDE.md`'s own README
section is explicit and deliberate about this: dependencies are pinned
specifically so "a service that restarts on boot shouldn't change its
dependency tree underneath itself," and bumps are meant to be deliberate.
Introducing a new library for a real reason (comment-preserving YAML
writes is the right tool for what F2 needed) is a legitimate, deliberate
addition — but it needs its own explicit line in `requirements.txt` with a
pinned version, and a todo (or an amendment to Todo 6) that says so, rather
than being assumed available.
### Minor: "single self-contained HTML file" and a `/admin/static` mount say two different things
Todo 8 describes "a single self-contained HTML file using Chart.js from
CDN" (matching gap-analysis F12's option (a): one `HTMLResponse` string,
no static mount needed). Todo 9 then separately mounts
`admin/frontend/` under `/admin/static`. If the page is genuinely
self-contained with only a CDN script tag, there's nothing left to serve
under a static mount and it can be dropped; if there *are* separate static
assets planned (a favicon, a stylesheet), "self-contained" should say so.
Small inconsistency, easy to resolve either direction — flagging so
whichever way it goes is a decision rather than a leftover from copying
daashbrd's shape wholesale.
## Recommendation
Don't approve as-is. Four things need an answer before implementation
starts, not after — none of them require redoing work already done well:
1. Confirm — or add a todo requiring — that `admin_model_overrides`
actually reaches `routing.select_candidates`, since without that the
model-management feature doesn't do what it's for.
2. Make an explicit, stated decision about auth/CSRF for the new
write-capable endpoints rather than inheriting the read-only surface's
answer unexamined, and specify the restart-service
response-before-death mechanism concretely.
3. Reconsider Todo 2's separate sampled history database against just
querying `energy_observations`/`route_decisions` directly — it's
simpler, exact instead of 60s-stale, and doesn't leave the background-
task lifecycle question unanswered.
4. Add the missing `requirements.txt` pin for `ruamel.yaml` (or a todo that
does), matching this project's own stated policy on deliberate,
pinned dependency changes.
The citation work underneath all of this is excellent — genuinely the most
accurate plan reviewed in this loop so far, including cross-repo references
into `daashbrd` that all checked out — and doesn't need rework. This is a
plan worth sending back for one more pass on the four points above, not a
rewrite.