# TUI overhaul: catch the dashboard up to the schema Status: done -- tui.py plus the tui_model.py split **Status: FINAL — decision-complete.** Written 2026-09-04 against `main` at `a0c7e8e`. ## Why now The TUI (`src/tui.py`, `tui_model.py`, `tui_screens.py`, `tui_sse.py` — ~1,000 lines total) has drifted behind the data it displays. Five columns were added to `route_decisions` across this session's merges and none reached the dashboard, and the decision table has never had a timestamp. The admin portal got a Profile column in PR #25. The TUI did not. It is the same data. ## 1. The decision table is missing a timestamp Declared columns (`src/tui.py:314-316`): ``` "id", "kind", "category", "tier", "ctx", "selected", "est $", "flex" ``` `observed_at` **is already carried** by `decision_row` in `tui_model.py` — it is fetched and then never rendered. So this is a display change, not a data change. Add a `time` column. Render it **short** (`HH:MM:SS`), not the full ISO timestamp: the rows are dense, the date is almost always today, and the full form would crowd out `selected`, which is the column people actually read. Put it first — it is the natural scan axis for a live feed. ## 2. Five columns exist in the schema and are invisible Measured by diffing `PRAGMA table_info(route_decisions)` against what `decision_row` exposes: | column | shipped in | why it matters | |---|---|---| | `profile` | PR #25 | which named profile served the request | | `exploration` | proficiency branch | whether epsilon-greedy picked this, not the ranking | | `pinch_original_tokens` | PR #18 | context pruning input | | `pinch_final_tokens` | PR #18 | context pruning output | | `request_id` | proficiency branch | the join key to `/outcome` reports | | `session_key` | earlier | hashed session fingerprint | **Do not add six more columns to the table.** It already has eight and the terminal is not wide. Instead: - Add **`profile`** to the table proper. It changes which models were even considered, so a decision cannot be read without it — the same argument that earned it a column in the admin portal. - Add an **`E` flag** in the existing flags idiom for `exploration`, alongside how `flex` is already rendered. An exploratory pick is not a ranking result and must be visually distinguishable, or the operator reads a deliberate random sample as the router's judgement. - Put **`pinch_*`, `request_id`, `session_key`** in the **detail popup** (`tui_screens.py`, opened with Enter or `e`), which already shows the full decision JSON. They are per-decision forensics, not scan-axis data. ## 3. The quota panel shows the wrong thing **This section depends on `plans/quota-balance-and-burn-rate.md` and must not land before it.** That plan replaces percentage-of-plan with balance and burn rate, because the current framing is measurably wrong: the warning says "a quota is a wall, not a bill — requests fail rather than costing more" while usage sat at 146% of plan and nothing failed, since the provider bills overage against a credit balance. Once that lands, the TUI panel (`#quota-panel`, `#quota-progress`, `#quota-legend`) should lead with **balance and projected runway** ("$12.19 left, ~23h at current burn") and demote the percentage bar to secondary. A progress bar against a plan figure that is routinely exceeded is actively misleading — it implies a ceiling that does not exist. **Sequencing:** if the quota plan has not landed when this one runs, do items 1, 2 and 4 and leave the panel alone. Do NOT reimplement balance/burn independently in the TUI — `metrics.quota_burn` is the single source and the TUI reads `/metrics`. ## 4. Surface the warnings that already exist `coverage.warnings` from `/metrics` already carries catalog staleness, quota burn, scoring coverage gaps and ceiling warnings. `#warnings-panel` exists. Confirm every warning class actually reaches it — the vision-ceiling incident on 2026-09-04 showed a whole warning family that was computed and never displayed, and the fix there was surfacing, not computing. This is a verification task as much as a feature: for each warning the `/metrics` `coverage.warnings` list can emit, assert it renders. ## Non-goals - No new data. Everything here is already in `route_decisions` or `/metrics`. - Do not widen the decision table beyond one added column plus one flag. - Do not reimplement any metric in the TUI. `tui_model.py` is the pure data layer over `/metrics` and `/events/decisions`; keep the computation in `metrics.py`. - Do not import `textual` outside the TUI modules. The dispatch path must stay free of the UI dependency — that separation is deliberate and tested. - No colour/theme rework. This is about information, not appearance. ## Success criteria - The decision table shows a short `HH:MM:SS` time column, first. - `profile` is a column; `exploration` renders as a flag beside `flex`. - `pinch_original_tokens`, `pinch_final_tokens`, `request_id` and `session_key` appear in the detail popup. - A test diffs `PRAGMA table_info(route_decisions)` against what the TUI model exposes and fails if a column is added to the schema without a decision about surfacing it. **This is the test that stops the drift recurring** — the rest of this plan is a one-time catch-up, this is the part that keeps it caught up. - Every warning class `/metrics` can emit renders in `#warnings-panel`. - Quota panel leads with balance and runway **if** the quota plan has landed; otherwise untouched and noted. - `textual` still imported only by TUI modules (existing test stays green). - Full suite green with `local_energy.enabled` both true and false. - The user's `config/config.local.yaml` is byte-identical after the run (`local_energy.enabled: true`, `tariff_usd_per_kwh: 0.159`). **Corrected 2026-09-05:** this previously named `config/config.yaml`. PR #26 moved deployment values into the gitignored overlay, so `config/config.yaml` is clean and tracked, and editing it is an ordinary commit. The file that cannot be recovered is the overlay — it is not in git history at all.