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
11 KiB
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
mainHEAD (c87e675or later) on branchfeat/cockpit-quick-wins. The main checkout at/home/alee/Sources/6krrthas UNCOMMITTED edits from other work, includingadmin/frontend/controls.html(aconfidence_threshold->confidence_minrename around lines 1026 and 1150) andtests/test_admin_frontend.py. Do not touch, stash, or commit them. C1 edits a different region ofcontrols.html; keep it that way so the later merge is clean. - Commit by explicit path. Never
git add -Aorgit 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.dbfor writes. - Never edit
config/config.yamlorconfig/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) andruff checkstay 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), readkind,category,profile,tier,q(search) andsizefromlocation.searchand 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 withhistory.replaceState. Omit empty values. Don't push history entries. qalso accepts a model id or a session id, since search already matches both (searchat ~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, adddecision_outcome_summary(conn, since_days=7)returning{decisions, client_reports, client_ok, client_failed}:decisions=route_decisionsrows withobserved_atinside the window;client_*=verificationsrows withkind = 'client_outcome'inside the window (succeeded= ok,failed= failed). Include it in the admin snapshot (src/admin.pynear line 2131) asdecision_outcomes. Leaveverdict_mixunchanged; the TUI uses it. - Frontend: the tile headline becomes
decisions. Qualifier lines:routed in 7 days178 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 existingbase/overlaybadge.- 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_KNOBSat ~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 theGET /admin/api/runtimeresponse, 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. pytestandruff checkgreen 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.