section_state now mirrors the composed recipe at body.recipe; the forge
note appends 'catalog fully reviewed' when the batch has zero fresh cards.
The FE shows the BACKLOG sentence in the report (dryCap) and in the forge
status line whenever recipe.new===0 with cards served; a non-dry forge
behaves exactly as before. No new fetch loops.
A card is decided only when a swipe event carries THIS section's id;
the lifetime swipes.action a second look wears into its section no longer
paints it as pre-decided. progress/undecided, report counts (latest
in-section event per listing) and maybe_exhaust all use the new
section_decided_ids() set; read path only, no writes. Every section_state
item carries decided_this_section; the FE counts server truth unless a
local intent is in flight, and sectDecided is clamped to sectTotal
everywhere (commit + paintSect) so the pill can never read past N/N.
Semantics note: completing a section now means striking EVERY item in it
(returns re-struck on their second look) — the exhaust selfcheck was
updated to strike all items, matching the deck's real flow. Undo-on-a-
return residual (event stays after the DELETE) is the accepted Q5 clamp-
only behavior, covered by a dedicated selfcheck case.
- prefs API accepts the new rubric_debug key (off|on), the only new runtime
write in v1.1; every section_state body now carries prefs.rubric_debug.
- fillRank renders the tank gauge + rounded % by default; the breakdown
lines, audit footer and the quiet listing-enrichment fetch run only when
the pref is on. List rows and the card face stay rubric-free (unchanged).
- Trends 'Actions' gains a 'Why breakdown: off | on' row in the setMode
style; updateSectionUI repaints it on every section hydrate.
Item 1: ArrowLeft/Right/Up drive nope/like/maybe through commit(); ArrowDown
and Backspace (kept alias) drive undo(); Esc closes the top open sheet only
(viewer -> dossier -> deck, inert on report). Text inputs/TEXTAREA/SELECT/
contentEditable swallow keys; arrow defaults are prevented (no page scroll).
Item 2: card center tap now opens the dossier (bike data first); side taps
keep flipping photos; the direct card -> viewer door is retired. The deck
dossier records its pile entry; the hero tap opens the viewer with a
backToDetail chain so Esc walks viewer -> dossier -> deck (repeatable).
Item 3: deck card + dossier hero photos switch from cover to contain over a
soft blurred backdrop of the same image (option C, demo panel C); thumb and
fullscreen viewer untouched; nophoto paths render no backdrop.
- cross-province groups now pair ns<->nb by completeness (img > km > older)
up to min per region instead of collapsing the whole group to one row;
surplus siblings (multi-unit dealer ads) stay active — no false merges
- verified on live DB: dup 80 -> 77, active 560 -> 563, total 640; the 3
freed rows are the BMW G 310 R dealer siblings (450/451/452); deck top
order unchanged (Indian Scout 91 x3 then Vulcan 900 73); re-import
idempotent (77 stable)
- selfcheck +2: multi-unit pairing semantics, reversibility (flip to
active + re-import re-derives the pairing) — 16/16 green
- 8 P1 spine checks unchanged; 6 new API checks via fastapi.testclient
against create_app() on a temp COPY of the live DB (live file never
opened by tests): root/static, stats-live==active+health, deck page 1
monotonic + undecided-only + full cursor walk zero-overlap vs SQL truth,
swipe like->list->edit->delete lifecycle (+invalid payload rejection),
/img/{id} cached-200/hotlink-302/none-404, badge roundtrip
- bare-python run keeps the 8 P1 checks and notes the skipped P2 set
- known framework quirk documented: this fastapi+httpx TestClient pair
mangles RedirectResponse to a bare 404; real uvicorn serves the true 302
(proof run verifies over the wire)
- db.init_db gains guarded ALTER TABLE additions for listings.year /
seller_type / extras (schema.sql v1 untouched; PRAGMA-checked idempotent)
- rubric: extras_from_text() returns matched EXTRAS labels (table order)
- importer: normalize computes year (existing helper), seller_type ONLY
from a real state-item signal (dealer/private, else NULL), extras JSON;
upsert rewrites the new columns on both paths
- verified on live DB: import idempotent, counts stay 640/560/80, year
backfilled on 599 rows, extras on 45, seller_type 0 (no state signal)