tests/test_plans_declare_status.py requires 'Status: <done|planned|in progress|parked|reference> -- <reason>' in the first 8 lines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N9biTbFC63yDfYfUsZmhgd
218 lines
11 KiB
Markdown
218 lines
11 KiB
Markdown
# Cockpit quick wins: six small admin-portal fixes
|
|
|
|
Status: done -- shipped in PR #101
|
|
Date: 2026-09-23
|
|
Source: `plans/cockpit-brainstorm.md` section 5 (items R2, R5, E3, C1, G1, G3).
|
|
Each item below was observed on the live portal or checked read-only against
|
|
the live `router.db` on 2026-09-23.
|
|
|
|
## Ground rules (all items)
|
|
|
|
- **Worktree, not the main checkout.** Create a worktree from `main` HEAD
|
|
(`c87e675` or later) on branch `feat/cockpit-quick-wins`. The main checkout
|
|
at `/home/alee/Sources/6krrt` has UNCOMMITTED edits from other work,
|
|
including `admin/frontend/controls.html` (a `confidence_threshold` ->
|
|
`confidence_min` rename around lines 1026 and 1150) and
|
|
`tests/test_admin_frontend.py`. Do not touch, stash, or commit them. C1 edits
|
|
a different region of `controls.html`; keep it that way so the later merge
|
|
is clean.
|
|
- Commit by explicit path. Never `git add -A` or `git add .`.
|
|
- **Never touch port 8080** (production). For visual checks, run a throwaway
|
|
instance on **8081** from the worktree, with a temp copy of the DB, never the
|
|
live `router.db` for writes.
|
|
- Never edit `config/config.yaml` or `config/config.local.yaml`.
|
|
- ASCII only in new code, comments, and UI strings. No middle-dot separators
|
|
(U+00B7); relate facts with layout, or with `:` or a rephrase.
|
|
- No paragraphs of prose in the UI. One line per hint at most.
|
|
- One commit per item, in the order listed. `pytest` (offline) and
|
|
`ruff check` stay green after each commit.
|
|
- For every UI change, take a screenshot on 8081 and check the changed area
|
|
zoomed in, not just the item's checklist.
|
|
|
|
---
|
|
|
|
## 1. R5: Classifier card shows a wrong value on first paint
|
|
|
|
**Problem.** `admin/frontend/controls.html:264-268`: the
|
|
`#classifier-mode-select` `<select>` renders with `local_llm` selected by
|
|
default (first `<option>`). Until `loadClassifierConfig` (sets `.value` at
|
|
~line 1116) returns, the card claims `local_llm` when the live mode is
|
|
`local_encoder`.
|
|
|
|
**Fix.** Render the select disabled, with a leading
|
|
`<option value="" selected>loading...</option>`, and the Save button
|
|
disabled. On load: remove the placeholder option, set the value, enable both.
|
|
On fetch failure: keep them disabled and show an inline one-line error.
|
|
|
|
**Test.** In `tests/test_admin_frontend.py`: assert that the static HTML of the
|
|
select has a selected placeholder and the `disabled` attribute, and that no
|
|
real mode option is `selected` in the static markup.
|
|
|
|
## 2. G1: Nav links hardcoded in eight files
|
|
|
|
**Problem.** Every page (`index.html`, `models.html`, `profiles.html`,
|
|
`proficiency.html`, `decisions.html`, `quota.html`, `controls.html`,
|
|
`providers.html`) hardcodes the same `<ul>` of seven `nav-item` links, and
|
|
sets `active` by hand. `navbar.js` (the header comment) already records that
|
|
adding Quota was an eight-file edit.
|
|
|
|
**Fix.** Define the link list once in `navbar.js`
|
|
(`[{href, label}]`, same order as today: Models, Profiles, Proficiency,
|
|
Decisions, Quota, Controls, Providers). Render it into a placeholder
|
|
(e.g. `<ul class="navbar-nav" id="nav-links"></ul>`) on each page. Mark
|
|
`active` by matching `location.pathname`, with `/admin` and `/admin/` mapping
|
|
to Home (no link active). Remove the hardcoded `<li>`s from all eight pages.
|
|
|
|
Keep the rendered markup identical: same classes (`nav-item`, `nav-link`,
|
|
`active`), same hrefs, same order. Pages must still render their nav when
|
|
`navbar.js` is loaded, and it is already loaded on every page.
|
|
|
|
**Test.** One test that parses all eight HTML files and asserts none contains
|
|
a hardcoded `class="nav-link" href="/admin/...` list. One test asserting that
|
|
the `navbar.js` link list has exactly the seven hrefs above.
|
|
|
|
## 3. E3: Decisions filters are not in the URL
|
|
|
|
**Problem.** `admin/frontend/decisions.html` never reads or writes
|
|
`URLSearchParams`. No other page can link to "decisions for this model" or
|
|
"this session", and a filtered view cannot be shared or reloaded.
|
|
|
|
**Fix.**
|
|
- On load, after the filter `<select>`s are populated (~lines 405-420), read
|
|
`kind`, `category`, `profile`, `tier`, `q` (search) and `size` from
|
|
`location.search` and apply them. If a value is not among a select's
|
|
options, add it as an option rather than dropping it (a link may name a
|
|
category that has no rows in the loaded window).
|
|
- On any filter change (`resetPageAndRender`, the Clear button, page size),
|
|
write the current filters with `history.replaceState`. Omit empty values.
|
|
Don't push history entries.
|
|
- `q` also accepts a model id or a session id, since search already matches
|
|
both (`search` at ~line 433).
|
|
|
|
**Test.** A static test that `decisions.html` reads `URLSearchParams` and calls
|
|
`history.replaceState`. On 8081, open
|
|
`/admin/decisions?category=diff_checking&q=deepseek`, confirm the filters and
|
|
the rows match, and screenshot it.
|
|
|
|
## 4. R2: The Decisions tile counts verifications and mixes diagnostics with ground truth
|
|
|
|
**Problem.** `admin/frontend/index.html:597-610`: the Home "Decisions" tile
|
|
sums `_snap.verdict_mix`, which is `metrics.verdict_mix()` and counts
|
|
`verifications` rows by verdict. Live DB, last 7 days:
|
|
|
|
| verdict | kind | n |
|
|
|---|---|---|
|
|
| unverifiable | structural | 2,660 |
|
|
| succeeded | client_outcome | 96 |
|
|
| failed | client_outcome | 82 |
|
|
| ok | local_llm | 29 |
|
|
| ok | structural | 7 |
|
|
| malformed | local_llm / structural | 4 / 7 |
|
|
| truncated | structural | 2 |
|
|
|
|
The tile shows "2,887 verified in 7 days: 132 ok, 93 failed". Actual route
|
|
decisions in 7 days: **2,678**. "ok" adds client `succeeded` to structural and
|
|
`local_llm` ok. "failed" adds client `failed` to `malformed`. Per CLAUDE.md,
|
|
structural and `local_llm` verdicts are diagnostics only; `/outcome`
|
|
(`kind = 'client_outcome'`) is the only ground truth.
|
|
|
|
**Fix.**
|
|
- Backend: in `src/metrics.py`, add `decision_outcome_summary(conn,
|
|
since_days=7)` returning
|
|
`{decisions, client_reports, client_ok, client_failed}`:
|
|
`decisions` = `route_decisions` rows with `observed_at` inside the window;
|
|
`client_*` = `verifications` rows with `kind = 'client_outcome'` inside the
|
|
window (`succeeded` = ok, `failed` = failed). Include it in the admin
|
|
snapshot (`src/admin.py` near line 2131) as `decision_outcomes`. Leave
|
|
`verdict_mix` unchanged; the TUI uses it.
|
|
- Frontend: the tile headline becomes `decisions`. Qualifier lines:
|
|
- `routed in 7 days`
|
|
- `178 client reports (6.6%): 96 ok, 82 failed` (coverage =
|
|
`client_reports / decisions`; the failed count uses the warn colour when
|
|
the fail rate among reports is above 10%, matching the existing Signal
|
|
quality card threshold).
|
|
- With zero reports: `no client reports in 7 days`.
|
|
- Drop the word "verified".
|
|
- Use `metrics.py`'s existing window idiom:
|
|
`julianday(observed_at) > julianday('now', '-' || ? || ' days')`.
|
|
|
|
**Test.** `tests/`: a metrics unit test with a seeded in-memory DB that
|
|
includes structural, `local_llm` and `client_outcome` rows, asserting only
|
|
`client_outcome` rows are counted and that `decisions` comes from
|
|
`route_decisions`. A frontend static test that the tile no longer reads
|
|
`verdict_mix`.
|
|
|
|
## 5. G3: Warning dismissals don't stick on rate warnings
|
|
|
|
**Problem.** `admin/frontend/index.html:1010-1040`: a dismissal is keyed on
|
|
the warning's exact text. That's deliberate ("when the underlying numbers move
|
|
the text moves with them... the warning comes back"). But warnings that embed
|
|
a live rate or count ("3.2x pace", "n=4") change text on nearly every poll, so
|
|
a dismissal never holds.
|
|
|
|
**Fix (interim, until structured warnings exist).** Key each dismissal on
|
|
`digit-normalized text + severity`: replace every run of digits (and decimal
|
|
points inside a number) with `#`, then append `|` + `warningSeverity(text)`.
|
|
The warning comes back when its wording or its severity changes, not when a
|
|
digit moves. This is the same normalization `rejection_warnings` already uses
|
|
for grouping (CLAUDE.md, the reactive detector section).
|
|
- Migrate the existing stored keys on load: normalize each and dedupe, same
|
|
pattern as the existing one-time carry-over from `dismissedWarnings`.
|
|
- Update the block comment to state the new rule and why it changed.
|
|
|
|
**Test.** A static or JS-extracted unit test: two texts differing only in
|
|
digits produce the same key; the same text at different severity produces a
|
|
different key.
|
|
|
|
## 6. C1: One knob table instead of two columns
|
|
|
|
**Problem.** `admin/frontend/controls.html`: "Runtime Knobs" (`#runtime-list`,
|
|
`renderRuntime` ~line 687) and "Persisted Config" (`#config-list`,
|
|
`renderConfig` ~line 821) list largely the same knobs in two side-by-side
|
|
cards, in different orders, and persisted labels truncate
|
|
(`objective.incumbent_cache_pri...`). Checking whether a runtime value has
|
|
drifted from its persisted value means matching rows by eye.
|
|
|
|
**Fix.**
|
|
- One card, **Knobs**, full width, as a table:
|
|
`knob | live | persisted | layer | ` with the columns meaning:
|
|
- `knob`: the dotted config key, never truncated (wrap if needed).
|
|
- `live`: the runtime control, if the knob has one, else empty.
|
|
- `persisted`: the persisted-config control, if the key is in the persisted
|
|
allowlist, else empty.
|
|
- `layer`: the existing `base` / `overlay` badge.
|
|
- A **drift** badge in the row when live differs from persisted
|
|
("reverts on restart").
|
|
- Pair rows through the runtime-to-config mapping that already exists in
|
|
`src/admin.py` (`_BOOL_KNOBS` at ~line 348, plus the non-bool runtime knob
|
|
tables beside it). Expose that mapping to the frontend: add the dotted
|
|
config key to each item in the `GET /admin/api/runtime` response, rather
|
|
than duplicating the table in JS.
|
|
- Keep the existing save semantics exactly: runtime controls POST to
|
|
`api/runtime/{knob}` immediately, as today; persisted controls stay dirty
|
|
until the Save button, as today. Keep the existing hint badges (for example
|
|
"enables the reuse window").
|
|
- Keep the help lines ("Live on the next request...", "Applied on
|
|
restart...") as one line each, placed as column-header tooltips or a single
|
|
line under the table.
|
|
- Don't touch the classifier-card region (lines ~1000-1160); another branch
|
|
edits it.
|
|
|
|
**Test.** `test_admin_knob_coverage.py` must still pass unchanged. Add a
|
|
test that every runtime knob in the API response carries a config key, and
|
|
that the key resolves to a real `RouterConfig` path. Visual check on 8081:
|
|
set one runtime knob so it differs from persisted (on the 8081 instance
|
|
only), then confirm the drift badge shows. Screenshot the whole table and a
|
|
zoomed crop of one drifted row.
|
|
|
|
---
|
|
|
|
## Done means
|
|
|
|
- Six commits on `feat/cockpit-quick-wins`, one per item, in the order above.
|
|
- `pytest` and `ruff check` green on the worktree.
|
|
- Screenshots from 8081 for items 1, 3, 4 and 6.
|
|
- The 8081 instance stopped, by the PID you started.
|
|
- A short report listing each commit, the tests added, and anything
|
|
deferred, with the reason.
|