Files
6krrt/plans/admin-facelift.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

4.2 KiB
Raw Permalink Blame History

Spec: admin dashboard facelift — standardize on Tabler (dark mode)

Status: done -- admin portal shipped

Origin. The admin dashboard (admin/frontend/index.html) is a hand-rolled, single-file dark theme — custom CSS variables, a monospace terminal aesthetic, and vanilla-JS panels with no component library. The ask is to replace that visual language with Tabler — a fully open-source (MIT) Bootstrap 5 admin template with dark mode built into the core product, not gated behind a paid tier — while keeping every existing behavior intact. (An earlier pass at this same ask targeted Themesberg's Volt Dashboard; switched to Tabler specifically because Volt's dark theme may be Pro-only, a risk Tabler doesn't carry.)

Outcome (post-lift)

Spec became reality in commits 23b74a3–906c9e6. The portal is now four pages with a consistent liquid-glass dark theme. The consolidated design system is captured in **

Folders/files now in the admin portal

  • admin.py (~800 LOC) — FastAPI sub-router with API + static HTML
  • admin_schema.sql
  • admin/frontend/index.html — dashboard
  • admin/frontend/models.html — model availability
  • admin/frontend/decisions.html — decision log
  • admin/frontend/controls.html — triggers, knobs, persisted config

Selected architecture decisions

  • CDN for Tabler + Chart.js + Google Fonts, everything else inline. The pages still contain their own <style> and <script> blocks — the original "one self-contained file" architecture from F12 was relaxed, but no static mount was added. Tabler core CSS/JS, Chart.js, and Quicksand load from jsDelivr/fonts.googleapis.com.
  • Inline SVG icon helper. No Tabler Icons webfont; each page carries a minimal icon(name, size) dictionary to avoid an extra font dependency and potential FOUT/CSP issues.

What survived from the original guardrails

  • API surface unchanged.
  • escapeHtml() applied to all interpolated text.
  • Chart instances reused where Chart.js is used.
  • saveAllConfig() posts sequential requests.
  • SSE status indicator reflects real connection state.

What was added beyond the spec

  • Atomic config writes with threading.Lock + .tmp + os.replace (admin.py) plus concurrent-write regression test.
  • Four-page navigation shell instead of a single-page dashboard.
  • Warnings moved from a card into a navbar bell.
  • Quota meter moved from a card into a header chip + modal.
  • Verdict mix converted from a Chart.js doughnut to pure HTML/CSS partition bars (avoids canvas lifecycle issues entirely).
  • History split from one chart into five per-metric mini charts.

Review / process gaps identified from the commit series

  1. Chart-instance assignment bug. renderVerdict initially failed to assign the created Chart instance back to verdictChart, causing repeated "Canvas is already in use" errors every 30-second poll. The QA evidence console log contained the error but was missed by the approving pass. Lesson: read saved console logs, not just screenshots.
  2. Commit granularity. The 8-todo plan specified one commit per todo; the real work coalesced into fewer larger commits. Lesson: granular commits make review and bisection easier.
  3. Service restart gap. The atomic-write fix landed on disk but llm-router.service was not restarted, so the running process still used the old code. Lesson: after a fix that protects the running service, restart explicitly.
  4. Null numeric config fields. objective.max_energy_per_request is null in config.yaml; the frontend sends empty string, which Optional[float] rejects. Not data-destructive, but it produces spurious failure toasts on "Save All".

Suggested future work

  • Extract the repeated cross-page CSS into admin/frontend/admin.css to prevent drift across the four pages.
  • Extract the inline icon() helper into a shared admin/frontend/icons.js module.
  • Null-aware config editor (empty string ↔ null round-trips cleanly).
  • Wire admin_schema.sql RBAC/audit tables behind authentication.
  • Better feedback in models.html when the override dropdown is disabled because a model is already overridden.

Original spec below preserved for context.