# R1 UX-fixes checklist (M8)

The consolidated sweep of every "UI note / wart" the feature specs
(`/srv/apps/work/docs/rebuild/01-current-system/features/*.md`) recorded
as *port-or-fix*, each given an explicit decision and a status verified
against the shipped rebuild frontend (`frontend/src/`). This is the
list rian signs at the M8 gate.

**Bottom line:** all 7 explicitly-flagged M8 UX candidates are **FIXED**
in the rebuild. The FK-display model + URL-as-state SPA architecture
made most of them structural (they can't regress the old way). Residual
nits and their disposition are listed at the end.

## UI / UX-facing warts

| # | wart (old app) | intended fix | test id | status | where (evidence) |
|---|----------------|--------------|---------|--------|------------------|
| 1 | mutations drop filter/page context (delete → bare `/entries`; edits full-reload + jump to top; `_return_url` hack) | every edit/bulk/delete/invoice-apply returns to the EXACT filtered/paged URL state | T-ENT-26, T-E2E-002 | ✅ FIXED | SPA never navigates on mutation; state is URL search + TanStack cache. `EntriesPage.tsx` `commitPatch`/`patchPage` keep prior search; `EntryPanel` delete calls `onDeleted()`+`onClose()`, no navigate. No `navigate({to` on any mutation path. |
| 2 | `?invoice=` scopes the list but NOT the totals card / pivot options | totals + options scope to the invoice too | T-ENT-07, T-INV-019 | ✅ FIXED | `filters.ts` `PIVOT_KEYS` includes `invoice`; totals query uses the same `toApiParams`. Server: `reporting/test_filter_contract.py::test_t_ent_07_invoice_filter_scopes_list_AND_totals`. |
| 3 | free-text search fires a query per keystroke | debounce `q` | — | ✅ FIXED | `EntriesPage.tsx` 300ms debounce on `qDraft`. Projects commits on blur/Enter (no per-keystroke spam); summary has no free-text input. |
| 4 | saving an unchanged form still writes/confirms | unchanged save = friendly no-op | — | ✅ FIXED (entry panel) | `EntryPanel.save` early-returns when the draft is empty. CC line save always fires but is an idempotent friendly no-op (see residuals). |
| 5 | mutating actions give no progress feedback | spinner / disabled / "Saving…" on every mutating control | — | ✅ FIXED | Broad pending/disabled states: BulkBar ("Working — verifying counts…"), EntryPanel/NewEntryRow (busy-disabled), InvoiceDetail ("Saving…"/"Verifying…", status buttons now disable mid-mutation — M8), InvoicesPage ("Creating…"), CC detail ("Re-applying…"/"Adding…"), Manage/Import/Resolve. |
| 6 | summary paging goes backward only; no next-month; year-rollover risk | add next-month; Dec↔Jan rollover; "This month" resets | T-SUM-004, T-SUM-005 | ✅ FIXED | `SummaryPage.tsx` `MonthNav` renders `{nextLabel} →` with rollover; prev/"This month" preserved; next/This-month hidden when current. |
| 7 | "Clear filters" on summary wipes `month` (jumps to current month) | Clear pivots but PRESERVE `month` | T-SFLT-005 | ✅ FIXED | `SummaryPage.tsx` Clear → keeps `month` if present, else `{}`. |
| 8 | pivot filters must survive month prev/next | month paging keeps active pivots | T-SUM-005 | ✅ FIXED | `MonthNav.patch` merges prior search, sets only `month`. |
| 9 | pagination must carry every filter; filter change resets to page 1 | prev/next keep filters; filter change drops `page` | T-ENT-09 | ✅ FIXED | `patchPage` preserves prior search; `patch` deletes `page` on any filter change. |
| 10 | empty result sets read poorly | friendly empty states | — | ✅ FIXED | `emptyMessage` on entries/projects/summary; invoice/CC empty copy present. |
| 11 | import: some-rows-parse → per-row parse errors silently discarded | surface skipped-row errors | T-IMP-011 | ✅ FIXED (backend) | `pending_imports` persists `parse_errors`/`parse_warnings` (alembic 0004); `imports/test_clockify_parser.py::test_imp_011_...` + staging test cover it; the resolver page renders the report. |
| 12 | no progress indication during parse/import/cancel | pending labels | — | ✅ FIXED | `ImportPage` "Parsing CSV…"; `ResolvePage` "Importing…"/"Cancelling…". |
| 13 | confusing "unassigned lines" copy on CC | fix the copy | — | ✅ FIXED | CC detail empty state: "Assign at least one line to a project to populate the paste block." |
| 14 | hardcoded per-page quick-chips; no user-saved views | saved views (name/pin/apply) replace the chips | — (M8 feature) | ✅ SHIPPED | The saved-views centerpiece — see `docs/features/saved-views.md`. Retired `QuickChips` (entries) + `QUICK_FILTERS` (projects); seeded rian's recurring views. |

## Backend-semantics warts (swept for completeness)

These were verified as already handled by earlier milestones; listed so
the sweep is complete.

| wart | fix | test id | status |
|------|-----|---------|--------|
| divergent invoice-eligibility predicates | ONE predicate | T-INV-010, T-PHX-009 | ✅ (M6 + M8 dedup of the sort copy) |
| detach on already-detached rows | no-op, verified count | T-INV-016 | ✅ |
| deleting an invoice destroys audit | line snapshots + guarded delete | — | ✅ |
| resolve-clear leaves stale billout | null billout on clear | T-ENT-33 | ✅ (M5) |
| view-as delete used inconsistent identity | ONE identity for attribution + gating | T-COM-109 | ✅ (M7) |
| CC "Feb 31" parses | calendar validation → None | T-CCE-D-007 | ✅ (M7) |
| pct override with no lower bound | floor at -100% | T-ADJ-009 | ✅ (M8 — new guard, `team_admin`) |

## Residual nits (non-blocking; recorded, not fixed in M8)

- **CC line Save isn't gated on dirtiness** (wart #4 for the CC surface):
  saving an unchanged CC line still fires `updateCcLine`. It succeeds as
  a friendly no-op (no error, correct result), so this is cosmetic;
  deferred.
- **No `?next=` return-URL after login** (auth-and-login E3). The spec
  itself rates this "mild… never complained about"; deferred. A deep
  link currently lands on `/` after a fresh sign-in.
- A few inline editors (EntryPanel/NewEntryRow/CcLineRow) show a
  disabled state rather than a "Saving…" label during a mutation —
  progress IS communicated (disabled), just without text.

## How to review
Load `/entries`, apply filters + a search, edit a cell → confirm the URL
(filters + page) is unchanged after save. Open `/summary`, page months
forward/back across a year boundary with a pivot set → pivots survive,
"Clear filters" keeps the month. Open a `?invoice=` link → the totals
card matches the visible rows. Save + pin a view, reload → it's in the
nav. Shrink the window to ~390px on Summary/Projects → tables scroll,
the page doesn't.
