From eff1ae313fe65ddf4f70112cd10c175fdac18e02 Mon Sep 17 00:00:00 2001 From: Torres Date: Thu, 12 Nov 2026 14:31:29 -0400 Subject: [PATCH 01/17] =?UTF-8?q?docs:=20BACKLOG=20=E2=80=94=20v1.1=20QOL?= =?UTF-8?q?=20wishlist=20(shortcuts,=20tap=20flow,=20photo=20fit,=20rubric?= =?UTF-8?q?)=20+=20analytics=20phase=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/BACKLOG.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index ccc33ab..1e47002 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -2,6 +2,29 @@ Glenn's "later versions" idea queue. He drops ideas in conversation; they land here (near-verbatim + a scope sketch + open questions), and get built in a future version — never mid-phase. Newest first. +## v1.1 — QOL wishlist (drafted 2026-11-12; feasibility triaged same day; build awaits Glenn's go-ahead) + +Glenn's words: *"Going to go through a laundry list of fixes before we do lets say 1.1?"* — nothing here is started. + +### Shortcuts +- Arrow keys: Left = Nope, Right = Like, Up = Maybe, **Down = back one** *(read as: undo the last swipe — confirm at spec time)*. **Esc** = exit fullscreen views. +- Feasibility: trivial — keyboard invokes the exact actions gestures/buttons already call (undo exists). Care: ignore keys while a text input has focus; prevent arrow-key page scroll. Desktop/Bluetooth-keyboard benefit only; the app is reachable from any tailnet device (MacBook included). + +### UI changes +1. **Tap flow — info first, then photos.** Center tap opens the fullscreen **bike data view** (today it opens the photo); tapping the photo there opens the fullscreen **image viewer**. Side taps keep flipping photos on the card. Esc/back pops viewer → data → deck. Resolves "can't get info unless I hit the ⓘ button." Revisit `UI-SPEC.md` §11 tap map before build (v1.1 spec bump). +2. **Photo fit: fill → contain.** Show photos uncropped — no "zoomed" crops. One CSS property; visual tradeoff = dark letterbox bars. *Open question:* viewer only, or the card stack too? (Cards = the approved full-bleed look; recommendation: contain in the viewer, cards stay full-bleed.) +3. **Rubric display goes minimal.** Show ONLY the gas-tank gauge + percent full (e.g. "76%"); drop the rest of the rubric-fit display in bike views. *(Read as "less cluttered"; confirm wording at spec time.)* + +## Analytics phase (post-1.1; notes per Glenn 2026-11-12) + +Glenn's words: *"take in just about all interactions… how long I have looked at each picture, how many pictures I flipped through, how quickly I swiped… kept on an individual bike perspective and fed to the model for further accuracy"* + *"start the database from scratch, giving myself a new start."* + +- **Capture (local-only, per bike):** dwell time per photo, flips (count/order), decision latency (card visible → swipe), maybe/undo/list-edit events, session id; card-dwell vs fullscreen-dwell as separate signals. +- **Store:** new append-only `interaction_events` table (same pattern as `swipe_events`); batched client flush (`sendBeacon`/pagehide); iOS PWA: pause dwell clocks on `visibilitychange` so background time can't pollute. +- **Feed the model:** nightly agent pass consumes per-bike aggregates as extra taste features. HARD LAW unchanged: no LLM in the swipe path — capture is write-only until the nightly read. +- **Reset:** its own phase (separate from v1.1), paired with a from-scratch DB reset so telemetry starts clean on the new ledger. Scope to confirm: wipe swipes + events + profile only (listings/catalog stay — re-import is idempotent); legacy data stays in `data/backup/` archives. +- **Open questions:** reset scope; raw events forever vs aggregate-and-prune; include list-browsing events; surface any of it in the UI (probably not). + ## Dry-pool UX + agent feedback (noted 2026-11-12) Glenn hit a fully-reviewed catalog: 563/563 actives decided → fresh pool = 0 → forges returned 2-card returns-only sections ("stuck on a couple bikes"), and "Run agent now" looked inert (a pass ran, but the deck can't change without fresh listings). Possible polish: (a) report/forge state says "catalog fully reviewed — fresh listings arrive with the next import" instead of silently dripping 2 cards; (b) agent button shows a richer confirmation (profile version + lean delta); (c) re-open "second look" eligibility or widen the wildcard pool when the catalog is dry; (d) a "review your Maybes" shortcut at exhaustion. Related: the nightly import + nightly agent read make the pool self-refreshing (Phase 4). From e77ed38ebf121d8002faf5486c9e2e8080498401 Mon Sep 17 00:00:00 2001 From: Torres Date: Thu, 12 Nov 2026 14:35:15 -0400 Subject: [PATCH 02/17] =?UTF-8?q?docs:=20BACKLOG=20=E2=80=94=20gallery=20m?= =?UTF-8?q?irror=20gap=20(galleries=20not=20locally=20hedged;=20primaries?= =?UTF-8?q?=20only)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/BACKLOG.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index 1e47002..76fbbab 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -12,8 +12,8 @@ Glenn's words: *"Going to go through a laundry list of fixes before we do lets s ### UI changes 1. **Tap flow — info first, then photos.** Center tap opens the fullscreen **bike data view** (today it opens the photo); tapping the photo there opens the fullscreen **image viewer**. Side taps keep flipping photos on the card. Esc/back pops viewer → data → deck. Resolves "can't get info unless I hit the ⓘ button." Revisit `UI-SPEC.md` §11 tap map before build (v1.1 spec bump). -2. **Photo fit: fill → contain.** Show photos uncropped — no "zoomed" crops. One CSS property; visual tradeoff = dark letterbox bars. *Open question:* viewer only, or the card stack too? (Cards = the approved full-bleed look; recommendation: contain in the viewer, cards stay full-bleed.) -3. **Rubric display goes minimal.** Show ONLY the gas-tank gauge + percent full (e.g. "76%"); drop the rest of the rubric-fit display in bike views. *(Read as "less cluttered"; confirm wording at spec time.)* +2. **Photo fit: fill → contain.** Show photos uncropped — no "zoomed" crops. One CSS property; visual tradeoff = dark letterbox bars. *Open question:* viewer only, or the card stack too? Explained to Glenn 2026-11-12 with a 3-panel demo (fill vs contain vs contain+blurred backdrop — `~/dev/bike-tinder-demos/fit-vs-fill.html`); awaiting his choice. +3. **Rubric display goes minimal.** Show ONLY the gas-tank gauge + percent full (e.g. "76%"); drop the rest of the rubric-fit display in bike views — **plus a setting that re-enables the full "why" breakdown for debugging** (Glenn, 2026-11-12; wire via the existing `prefs` key/value API — same pattern as `forge_mode`). ## Analytics phase (post-1.1; notes per Glenn 2026-11-12) @@ -25,6 +25,10 @@ Glenn's words: *"take in just about all interactions… how long I have looked a - **Reset:** its own phase (separate from v1.1), paired with a from-scratch DB reset so telemetry starts clean on the new ledger. Scope to confirm: wipe swipes + events + profile only (listings/catalog stay — re-import is idempotent); legacy data stays in `data/backup/` archives. - **Open questions:** reset scope; raw events forever vs aggregate-and-prune; include list-browsing events; surface any of it in the UI (probably not). +## Gallery mirror gap (found 2026-11-12, filing for a later phase) + +Verified while preparing the fit/fill demo: the local image mirror (`data/img/{id}.webp`) holds **primaries only**. Gallery photos beyond the first (`/img/{id}?i≥1`) serve a **302 redirect to the Kijiji CDN** — not hedged locally. If an ad is deleted we keep photo 1, but the gallery (often where the good shots are) is gone. Fix options for a later phase: mirror galleries on import/first-touch (estimates in the B1 notes: roughly +350–600 MB for the full catalog — fine for the mirror volume), keep the redirect as fallback. Not urgent; filed so it is not forgotten. + ## Dry-pool UX + agent feedback (noted 2026-11-12) Glenn hit a fully-reviewed catalog: 563/563 actives decided → fresh pool = 0 → forges returned 2-card returns-only sections ("stuck on a couple bikes"), and "Run agent now" looked inert (a pass ran, but the deck can't change without fresh listings). Possible polish: (a) report/forge state says "catalog fully reviewed — fresh listings arrive with the next import" instead of silently dripping 2 cards; (b) agent button shows a richer confirmation (profile version + lean delta); (c) re-open "second look" eligibility or widen the wildcard pool when the catalog is dry; (d) a "review your Maybes" shortcut at exhaustion. Related: the nightly import + nightly agent read make the pool self-refreshing (Phase 4). From 085b2bc2ceabc8ed037a8a7cbbf10e0a9c9785f0 Mon Sep 17 00:00:00 2001 From: Torres Date: Thu, 12 Nov 2026 14:37:31 -0400 Subject: [PATCH 03/17] =?UTF-8?q?docs:=20BACKLOG=20=E2=80=94=20v1.1=20phot?= =?UTF-8?q?o=20treatment=20decided:=20option=20C=20(fit=20+=20blurred=20ba?= =?UTF-8?q?ckdrop)=20for=20card=20+=20info=20hero?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/BACKLOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index 76fbbab..15d5e93 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -12,7 +12,7 @@ Glenn's words: *"Going to go through a laundry list of fixes before we do lets s ### UI changes 1. **Tap flow — info first, then photos.** Center tap opens the fullscreen **bike data view** (today it opens the photo); tapping the photo there opens the fullscreen **image viewer**. Side taps keep flipping photos on the card. Esc/back pops viewer → data → deck. Resolves "can't get info unless I hit the ⓘ button." Revisit `UI-SPEC.md` §11 tap map before build (v1.1 spec bump). -2. **Photo fit: fill → contain.** Show photos uncropped — no "zoomed" crops. One CSS property; visual tradeoff = dark letterbox bars. *Open question:* viewer only, or the card stack too? Explained to Glenn 2026-11-12 with a 3-panel demo (fill vs contain vs contain+blurred backdrop — `~/dev/bike-tinder-demos/fit-vs-fill.html`); awaiting his choice. +2. **Photo fit: fill → contain, option C chosen.** Show photos uncropped — no "zoomed" crops. **Decided 2026-11-12 (Glenn: "OH C! I love C!"): option C — fit + blurred backdrop (whole photo over a soft blur of the same image) on the card AND the info-page hero.** Fullscreen viewer already fits (unchanged); list thumbs stay fill (56px, irrelevant). Demo reference: `~/dev/bike-tinder-demos/fit-vs-fill.html` (panel C). 3. **Rubric display goes minimal.** Show ONLY the gas-tank gauge + percent full (e.g. "76%"); drop the rest of the rubric-fit display in bike views — **plus a setting that re-enables the full "why" breakdown for debugging** (Glenn, 2026-11-12; wire via the existing `prefs` key/value API — same pattern as `forge_mode`). ## Analytics phase (post-1.1; notes per Glenn 2026-11-12) From d1c3adddff0dd7dc1f5019258341f1cb0c26a4e5 Mon Sep 17 00:00:00 2001 From: Torres Date: Thu, 12 Nov 2026 14:39:07 -0400 Subject: [PATCH 04/17] =?UTF-8?q?docs:=20version=20map=20=E2=80=94=20v1.1?= =?UTF-8?q?=20(QOL=20batch,=20awaiting=20build=20order)=20/=20v1.2=20(anal?= =?UTF-8?q?ytics=20suite=20+=20fresh-ledger=20reset)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/BACKLOG.md | 4 ++-- docs/PLAN.md | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index 15d5e93..393768a 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -15,14 +15,14 @@ Glenn's words: *"Going to go through a laundry list of fixes before we do lets s 2. **Photo fit: fill → contain, option C chosen.** Show photos uncropped — no "zoomed" crops. **Decided 2026-11-12 (Glenn: "OH C! I love C!"): option C — fit + blurred backdrop (whole photo over a soft blur of the same image) on the card AND the info-page hero.** Fullscreen viewer already fits (unchanged); list thumbs stay fill (56px, irrelevant). Demo reference: `~/dev/bike-tinder-demos/fit-vs-fill.html` (panel C). 3. **Rubric display goes minimal.** Show ONLY the gas-tank gauge + percent full (e.g. "76%"); drop the rest of the rubric-fit display in bike views — **plus a setting that re-enables the full "why" breakdown for debugging** (Glenn, 2026-11-12; wire via the existing `prefs` key/value API — same pattern as `forge_mode`). -## Analytics phase (post-1.1; notes per Glenn 2026-11-12) +## v1.2 — Analytics suite (version split set with Glenn 2026-11-12) Glenn's words: *"take in just about all interactions… how long I have looked at each picture, how many pictures I flipped through, how quickly I swiped… kept on an individual bike perspective and fed to the model for further accuracy"* + *"start the database from scratch, giving myself a new start."* - **Capture (local-only, per bike):** dwell time per photo, flips (count/order), decision latency (card visible → swipe), maybe/undo/list-edit events, session id; card-dwell vs fullscreen-dwell as separate signals. - **Store:** new append-only `interaction_events` table (same pattern as `swipe_events`); batched client flush (`sendBeacon`/pagehide); iOS PWA: pause dwell clocks on `visibilitychange` so background time can't pollute. - **Feed the model:** nightly agent pass consumes per-bike aggregates as extra taste features. HARD LAW unchanged: no LLM in the swipe path — capture is write-only until the nightly read. -- **Reset:** its own phase (separate from v1.1), paired with a from-scratch DB reset so telemetry starts clean on the new ledger. Scope to confirm: wipe swipes + events + profile only (listings/catalog stay — re-import is idempotent); legacy data stays in `data/backup/` archives. +- **Reset:** ships with v1.2, paired with a from-scratch DB reset so telemetry starts clean on the new ledger. Scope to confirm: wipe swipes + events + profile only (listings/catalog stay — re-import is idempotent); legacy data stays in `data/backup/` archives. (v1.1 does NOT reset anything — swipe history survives it untouched.) - **Open questions:** reset scope; raw events forever vs aggregate-and-prune; include list-browsing events; surface any of it in the UI (probably not). ## Gallery mirror gap (found 2026-11-12, filing for a later phase) diff --git a/docs/PLAN.md b/docs/PLAN.md index 65dbc09..adb93f7 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -1,6 +1,12 @@ # PLAN — Bike-Tinder -**Status:** Build phase — Phase 2 complete + phone-proven (2026-11-10); **Phase 3 built, verified, and live (2026-11-11); B1 photo experience v2 built, verified, and live (2026-11-11)**; Phase 4 next. Phases are gated: each lands, is verified, then the next begins. Glenn's standing rule applies — "pause" stops all spend immediately. +**Status:** **v1.0 SHIPPED (2026-11-12)** — Phases 1–4 complete: stack live under its unit, nightly chain armed, tailnet URL in daily use, Gitea release `v1.0.0` cut. **v1.1** (QOL batch) is spec'd and awaits Glenn's build order; **v1.2** (analytics suite + fresh-ledger reset) queued. Phases are gated: each lands, is verified, then the next begins. Glenn's standing rule applies — "pause" stops all spend immediately. + +## Version map + +- **v1.0** — shipped 2026-11-12: the working loop (Phases 1–4 — catalog, swipe deck, personalization, deployed stack + nightly automation). Gitea release `v1.0.0`. +- **v1.1** — QOL batch, spec'd 2026-11-12 (`BACKLOG.md`): keyboard shortcuts · info-first tap flow · option C photos (fit + blurred backdrop) · minimal rubric + debug "why" toggle. No data reset. +- **v1.2** — analytics suite (`BACKLOG.md`): full interaction capture + from-scratch ledger reset ("fresh start"). ## v1 goal From 7e4c41cffd7fc4182315e8a2fe703b42ca28dd09 Mon Sep 17 00:00:00 2001 From: Torres Date: Thu, 12 Nov 2026 14:44:07 -0400 Subject: [PATCH 05/17] docs: v1.1 gains section-counter bug fix + dry-catalog messaging (diagnosed live 2026-11-12) --- docs/BACKLOG.md | 9 +++++++++ docs/PLAN.md | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index 393768a..27fd5fc 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -6,6 +6,13 @@ Glenn's "later versions" idea queue. He drops ideas in conversation; they land h Glenn's words: *"Going to go through a laundry list of fixes before we do lets say 1.1?"* — nothing here is started. +### Bugs & dry-catalog messaging (reported live 2026-11-12 evening) + +Glenn's words: *"I just got a new batch and its marked as 2/2. When I go to the next, its 3/2… I don't believe it's counting the entries correctly. Furthermore, I'm only again getting 2 bikes at a time… I feel I should be getting batches of 25 at a time?"* + +- **Counter bug (diagnosed in code 2026-11-12):** the pill shows `decided/total` for the current section. **Returns arrive wearing their previous decision** (the section payload joins `swipes.action`), so a fresh second-looks-only batch instantly counts as fully decided — a new batch paints "2/2" before it is touched, and the hot/complete state fires prematurely. Swiping then increments optimistically on top (`sectDecided++`), so the count runs past the total — "3/2", "4/2". **Fix sketch:** count a card as decided only if decided *during this section* (server per-item flag from events stamped with this section_id — the source `section_counts` already uses); clamp to total; keep the hot state off until a real in-section decision exists. +- **Dry-catalog messaging (pulled forward from the Dry-pool UX note below):** when the fresh pool is 0 the deck silently degrades to returns-only sections (2–3 cards: ≤5 returns budget, one-shot per item, family caps) — by design but confusing. v1.1: surface the state ("Catalog fully reviewed — fresh listings arrive with the next 08:30 import; showing second looks") whenever a composed/forged batch reports `new: 0`, and label forge results accordingly so it never silently drips 2 cards again. + ### Shortcuts - Arrow keys: Left = Nope, Right = Like, Up = Maybe, **Down = back one** *(read as: undo the last swipe — confirm at spec time)*. **Esc** = exit fullscreen views. - Feasibility: trivial — keyboard invokes the exact actions gestures/buttons already call (undo exists). Care: ignore keys while a text input has focus; prevent arrow-key page scroll. Desktop/Bluetooth-keyboard benefit only; the app is reachable from any tailnet device (MacBook included). @@ -33,6 +40,8 @@ Verified while preparing the fit/fill demo: the local image mirror (`data/img/{i Glenn hit a fully-reviewed catalog: 563/563 actives decided → fresh pool = 0 → forges returned 2-card returns-only sections ("stuck on a couple bikes"), and "Run agent now" looked inert (a pass ran, but the deck can't change without fresh listings). Possible polish: (a) report/forge state says "catalog fully reviewed — fresh listings arrive with the next import" instead of silently dripping 2 cards; (b) agent button shows a richer confirmation (profile version + lean delta); (c) re-open "second look" eligibility or widen the wildcard pool when the catalog is dry; (d) a "review your Maybes" shortcut at exhaustion. Related: the nightly import + nightly agent read make the pool self-refreshing (Phase 4). +**Pulled into v1.1 (2026-11-12):** option (a) — dry-state messaging — is now part of the v1.1 scope (see Bugs & dry-catalog messaging above); options (b)/(c)/(d) remain queued for a later version. + ## B1 · Photo experience v2 — multi-image galleries, photo swipe, fullscreen viewer **Queued 2026-11-10** (during the P3 build; for the version after it). **PULLED FORWARD 2026-11-10; SHIPPED 2026-11-11 — built, verified (walk 390/390, selfcheck 31/31), LIVE on :9453; see `PLAN.md`.** Spec frozen in `UI-SPEC.md` §11 + `DATA-MODEL.md` B1 additions. diff --git a/docs/PLAN.md b/docs/PLAN.md index adb93f7..4cc1787 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -5,7 +5,7 @@ ## Version map - **v1.0** — shipped 2026-11-12: the working loop (Phases 1–4 — catalog, swipe deck, personalization, deployed stack + nightly automation). Gitea release `v1.0.0`. -- **v1.1** — QOL batch, spec'd 2026-11-12 (`BACKLOG.md`): keyboard shortcuts · info-first tap flow · option C photos (fit + blurred backdrop) · minimal rubric + debug "why" toggle. No data reset. +- **v1.1** — QOL batch, spec'd 2026-11-12 (`BACKLOG.md`): keyboard shortcuts · info-first tap flow · option C photos (fit + blurred backdrop) · minimal rubric + debug "why" toggle · section-counter fix + dry-catalog messaging (added 2026-11-12 evening). No data reset. - **v1.2** — analytics suite (`BACKLOG.md`): full interaction capture + from-scratch ledger reset ("fresh start"). ## v1 goal From 381b6c8a9c505edbb7cd3af01b4d89ea363a4d72 Mon Sep 17 00:00:00 2001 From: Torres Date: Thu, 12 Nov 2026 14:51:43 -0400 Subject: [PATCH 06/17] =?UTF-8?q?docs:=20v1.1=20round=202=20=E2=80=94=20fr?= =?UTF-8?q?esh=20counter,=20loud=20empty=20state,=20batch=20formula=20v2?= =?UTF-8?q?=20(25-target,=20second-look=20fill),=20Second=20Look=20band=20?= =?UTF-8?q?pop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/BACKLOG.md | 11 +++++++++++ docs/PLAN.md | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index 27fd5fc..3c4541c 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -13,6 +13,17 @@ Glenn's words: *"I just got a new batch and its marked as 2/2. When I go to the - **Counter bug (diagnosed in code 2026-11-12):** the pill shows `decided/total` for the current section. **Returns arrive wearing their previous decision** (the section payload joins `swipes.action`), so a fresh second-looks-only batch instantly counts as fully decided — a new batch paints "2/2" before it is touched, and the hot/complete state fires prematurely. Swiping then increments optimistically on top (`sectDecided++`), so the count runs past the total — "3/2", "4/2". **Fix sketch:** count a card as decided only if decided *during this section* (server per-item flag from events stamped with this section_id — the source `section_counts` already uses); clamp to total; keep the hot state off until a real in-section decision exists. - **Dry-catalog messaging (pulled forward from the Dry-pool UX note below):** when the fresh pool is 0 the deck silently degrades to returns-only sections (2–3 cards: ≤5 returns budget, one-shot per item, family caps) — by design but confusing. v1.1: surface the state ("Catalog fully reviewed — fresh listings arrive with the next 08:30 import; showing second looks") whenever a composed/forged batch reports `new: 0`, and label forge results accordingly so it never silently drips 2 cards again. +### Batch formula v2 + fresh counter + empty state + Second Look pop (requested live 2026-11-12 evening, round 2) + +Glenn's words: *"Alright lets add a new counter to the top of 'bikes lef' or something to indicate how many bikes are fresh. I think if there is no more new bikes, the empty page (all bikes gone through) should say 'no more new bikes!'. Maybe there is already but I didn't read it. Make the UI more pronounced so I can see it clearly. One more thing, I think there should be 25 bikes at a time, min 2 of second look. So say there is 5 bikes left, those 5 fresh bikes are listed, with the rest being second looks. Ok last thing, the Second Look indicator, make it more pronounced visually. Change the color a bit so it pops a bit more."* + +- **Fresh counter (header):** new chip in the top header showing **fresh (undecided) bikes left** — extend `/api/stats` with `fresh` (active listings with no swipe — the exact NEW_POOL predicate). Header pill gains `K fresh`. When 0, the state of the hunt reads at a glance. +- **Empty state, loud:** the exhausted screen becomes prominent **"No more new bikes!"** (today `.empty` says a quiet "You're all caught up" and the report screen can mask it in section mode). Show whenever the deck can serve *nothing* at all (no fresh, no eligible second looks, no wildcards); keep "New bikes arrive nightly — next import 08:30". +- **Batch formula v2** *(amends the frozen ≤5-returns rule — build-time UI-SPEC/P3 note):* every batch targets **25 cards**. Fresh first (up to 23 slots); **≥2 second looks per batch whenever eligible stock exists** *(reading Glenn's "min 2": a full-fresh day = 23 fresh + 2 second looks; veto line if he wants pure-fresh instead)*; when fresh < 23, second looks **fill the remainder up to 25** (his example: 5 fresh + 20 second looks); leftover fresh rolls to the next batch. Limits kept: ≤2 re-noped returns per batch; family cap relaxes for fill-returns when needed (build detail); returns stay one-shot. Fresh exhausted → second-looks-only batches; returns exhausted → fresh-only batches; both → the loud empty state. + - *Noted consequence:* with fill-to-25, the one-shot second-look pool (~30 remaining as of tonight) burns through in a batch or two — after that only nightly imports feed the deck. Re-offer policy (e.g. re-eligible after everything's had its turn) stays queued (option (c) below); revisit after living with v2. +- **Second Look band pops:** `.band.sl` is currently near-invisible (white at ~10% on dark); re-tint to a distinct **violet accent** (bold fill + border) on card face + info view. Wildcard stays amber for contrast. +- **UI-SPEC amendments at build:** §11 tap flow (already queued) + `.band.sl` palette + empty-state copy + header fresh counter. + ### Shortcuts - Arrow keys: Left = Nope, Right = Like, Up = Maybe, **Down = back one** *(read as: undo the last swipe — confirm at spec time)*. **Esc** = exit fullscreen views. - Feasibility: trivial — keyboard invokes the exact actions gestures/buttons already call (undo exists). Care: ignore keys while a text input has focus; prevent arrow-key page scroll. Desktop/Bluetooth-keyboard benefit only; the app is reachable from any tailnet device (MacBook included). diff --git a/docs/PLAN.md b/docs/PLAN.md index 4cc1787..8b8c4c4 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -5,7 +5,7 @@ ## Version map - **v1.0** — shipped 2026-11-12: the working loop (Phases 1–4 — catalog, swipe deck, personalization, deployed stack + nightly automation). Gitea release `v1.0.0`. -- **v1.1** — QOL batch, spec'd 2026-11-12 (`BACKLOG.md`): keyboard shortcuts · info-first tap flow · option C photos (fit + blurred backdrop) · minimal rubric + debug "why" toggle · section-counter fix + dry-catalog messaging (added 2026-11-12 evening). No data reset. +- **v1.1** — QOL batch, spec'd 2026-11-12 (`BACKLOG.md`): keyboard shortcuts · info-first tap flow · option C photos (fit + blurred backdrop) · minimal rubric + debug "why" toggle · section-counter fix + dry-catalog messaging · fresh counter + loud "No more new bikes!" empty state · batch formula v2 (25-target, second-look fill, ≥2 per batch) + Second Look band pop. No data reset. - **v1.2** — analytics suite (`BACKLOG.md`): full interaction capture + from-scratch ledger reset ("fresh start"). ## v1 goal From 829d43851b671023569ad3dfc33414b0ca598862 Mon Sep 17 00:00:00 2001 From: Torres Date: Thu, 12 Nov 2026 15:04:40 -0400 Subject: [PATCH 07/17] docs: v1.1 implementation specification --- docs/V1.1-BUILD.md | 624 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 624 insertions(+) create mode 100644 docs/V1.1-BUILD.md diff --git a/docs/V1.1-BUILD.md b/docs/V1.1-BUILD.md new file mode 100644 index 0000000..22de53f --- /dev/null +++ b/docs/V1.1-BUILD.md @@ -0,0 +1,624 @@ +# V1.1-BUILD — v1.1 implementation specification + +Build spec for the v1.1 QOL batch. Requirements: `docs/BACKLOG.md` "v1.1 — QOL wishlist" (both subsections). Frozen UI contract this amends: `docs/UI-SPEC.md` (v2 + P3 §10 + B1 §11). Version map: `docs/PLAN.md`. + +**Status:** spec — awaiting Glenn's go-ahead. Every item below states goal, exact change plan (file/function/line anchors verified against the code at `381b6c8`), pseudocode where the logic is heavy, edge cases, acceptance criteria, and the verification steps the parent will run. Nothing is built yet. + +**No data reset. No schema change. v1.1 is frontend + read-path/query only** (see "Data safety"). The only logic rewrite is the compose allocation itself (`compose_section`), which still writes *only* what it writes today (a new `sections`/`section_items` row set, exactly as a forge already does). + +## Code facts everyone should start from (verified 2026-11-12, commit `381b6c8`) + +Backend: + +- `app/sections.py` (488 lines): `NEW_POOL_SQL` (40–44), `RETURNS_SQL` (46–55), `WILDCARD_SQL` (57–61), constants `FAMILY_CAP=4 MAX_RETURNS=5 MAX_RETURN_NOPES=2 WILDCARDS=2 DEFAULT_SIZE=25 MIN_BANDS=3` (28–33), `pick_new` (160–178), `repair_bands` (181–256), `compose_section` (259–404), `section_items` (417–425), `section_counts` (428–454), `maybe_exhaust` (457–466), `read_section` (469–488). +- `app/api.py` (738 lines): `/api/stats` (234–264), `swipe` POST (299–357; `section_id` optional at 307–309, `append_event` call at 345–346), `section_state` (446–471), `get_section` (473–488; cold compose hardcodes `size=25` at 482–485), `forge` (490–527), `/api/prefs` (652–666; `allowed = {'forge_mode': ('ondemand','strict')}` at 656), `img` with `object-fit:contain` viewer served at 670–708. +- `app/db.py`: `swipe_events` schema with `section_id` (18–26), `append_event` (120–143) — already stamps `section_id` when the client sends it; P3 seeding copies events with `section_id=NULL` (111–117). + +Frontend: `app/static/index.html` (1282 lines, single-file app): + +- Keyboard handler ALREADY EXISTS at 1263–1270 (see item 1 — this must be reconciliated with the BACKLOG key map). +- Tap zones: `tapZone` (756–763) — sides flip, center opens viewer. Built faithful to B1 §11.4. v1.1 changes the center-target only. +- Deck→dossier door: `.ibtn` click (578–580); `openDetail` (768–796); dossier hero with `dphoto` (245) and hero-tap→viewer in `dEndH` (832–837). +- Viewer: `openViewer` (870–877), `closeViewer` (878–883), `vCtx={item,idx,back}` (853, 873); `.vimg` (197) already `object-fit:contain`. +- Card DOM: `phwrapHTML` (512–513) puts `` inside the `.phwrap`; `.card .photo` is `object-fit:cover` (CSS 36). +- Header: `#pill` (206, filled by `loadStats` 1127–1136 — it renders `live · new` from `/api/stats`), `#sect` (207) is the section progress pill, `.sect` CSS 137–138, `.pill` 29–30. +- Empty state: `#empty` (212) with copy "You're all caught up."; CSS 95–97; shown only by `maybeRefill` in **legacy mode** (652) — unreachable in section mode, where the report card (`#report`, 213–224, shown at 660–662) owns exhaustion. +- Section counter: client state `sectDecided/sectTotal` (1139); `paintSect` (1140–1145); increments in `commit` (684) and `undo` (706); server truth re-derived on every `updateSectionUI` (1157–1173) from `body.items[].decision` — the bug: `decision` is the listing's *lifetime* verdict, not a this-section decision. +- `bandHTML` (482–489); `.band` (139), `.band.wild` (140), `.band.sl` (141); `hasSegs` band offset (188); `updateSectionUI` (1157); `forge` button flow (1180–1206); prefs pattern `setMode` (1174–1179); `loadStats` (1127). +- Info-page hero: `.dphoto` (70) is `object-fit:cover`; list thumbs `.thumb` (110) are `cover` and stay that way. +- Rubric display: dossier "Why this rank" block `#drank` (250–257) filled by `fillRank` (798–821) — gauge + `{points}/100` (805) + up to 4 breakdown lines (806–812) + footer (813–814); list rows show tank only (985). + +Discrepancies found between docs and code (code wins; both recorded): + +- B1 §11.4 says center-tap = fullscreen viewer. Code matches. v1.1 re-maps center-tap = dossier (BACKLOG UI change 1) — the UI-SPEC amendment in this doc supersedes §11.3/§11.4. +- `sections.py` module docstring (2–17) and `docs/RANKING.md:29` still say "≤5 returns"; DATA-MODEL line 165 says "Max 5 returns per section". All replaced by item 9's formula (docs to update: RANKING.md:29, DATA-MODEL.md:165, the module docstring — the spec's one doc-touch, see item 9). +- The 2026-11-12 empty-pool symptom ("forges returned 2-card sections") means the eligible-return pool is *near-empty today*; details in item 9. +- Current keyboard handler maps **Backspace**, not ArrowDown, to undo, and does not prevent arrow page scroll — item 1 fixes this. + +--- + +## Item 1 — Keyboard shortcuts + +**Goal (BACKLOG "Shortcuts"):** ← nope, → like, ↑ maybe, ↓ undo (back one), Esc exits fullscreen views. Desktop/Bluetooth-keyboard benefit; no touch changes. + +Notes before the plan: + +- The expected row of actions shares handlers: commits go through `commit()` (`index.html` 677–689; button bindings at 721–723), undo through `undo()` (702–720; binding 724). The keys must call these exact functions so all P3 invariants (optimistic persist through `setDecision`, photo_idx, no-lock flight) hold — never bespoke handlers. +- A keyboard handler already exists (1263–1270): ← / → / ↑ do the right things, but undo is **Backspace**, ↓ is unmapped, Esc is unmapped, there is no scroll guard, and the text-input guard covers only `INPUT`. + +**Plan (all in `app/static/index.html`):** + +1. Replace the handler (1264–1270) with the v1.1 key map: + ``` + ArrowLeft -> nope (commit(pile[0].el,'nope')) + ArrowRight -> like (commit(pile[0].el,'like')) + ArrowUp -> maybe (commit(pile[0].el,'maybe')) + ArrowDown -> undo (undo()) + Backspace -> undo (undo()) // kept: existing users' muscle memory — note in Open Questions + Escape -> exit fullscreen views (below) + ``` +2. Guard before acting: **ignore all keys while a text input has focus.** Extend the existing `e.target.tagName==='INPUT'` check to also skip `TEXTAREA`, `SELECT`, or any element with `isContentEditable` (the list screen has a live search input; searching with arrows must filter, not swipe): + ``` + const t=e.target; + if(t && (t.tagName==='INPUT'||t.tagName==='TEXTAREA'||t.tagName==='SELECT'||t.isContentEditable)) return; + ``` +3. **Prevent arrow-key page scroll** (the app body is `overflow:hidden`, but focused buttons can still scroll; be safe): for the four arrow keys, `e.preventDefault()` immediately after the guard passes — i.e. before letting the browser's default horizontal/vertical scroll run. Backspace default is harmless and untouched. +4. **Esc, one key, stack order** (viewer `#viewer` z-72 > dossier `#detail` z-70 > deck): + - if `#viewer` has class `on` -> `closeViewer()` (878) + - else if `#detail` has class `on` -> `closeDetail()` (822; returns to deck or, from a list row, to the list screen that stays open underneath — same as today's pull-down close) + - else if `#report` has class `on` -> do nothing (Esc never forges or navigates tabs) + - else -> ignore. + Note: the currently-open sheet also swallows keys before Esc gets them, because the existing early-return for open `#detail`/`#viewer` is removed in favour of the Esc-first ordering above. Esc only closes; arrows only work on the deck. + +**Edge cases:** + +- Arrow keys must do nothing when no pile: `commit` and `undo` already no-op on empty `pile[0]`/`hist` (677–678, 703). +- Key repeat (holding →): `commit` drains the pile as fast as repeat fires — same behaviour as mashing the like button, acceptable, no extra lock needed (the no-lock law already tolerates fast commits). +- The report card is up but the section is closed: arrows no-op (pile empty); Esc no-ops; the Forge button remains mouse/tap only. + +**Acceptance criteria (objective):** + +1. New chromium/headless page, deck served: dispatch `ArrowLeft/Right/Up` -> top card flies nope/like/maybe; the very same network payloads fire as button taps (verify no second code path: instrument or assert `sectDecided`/paintSect side effects identical). +2. Dispatch `ArrowDown` -> the swiped card returns to the top of the pile (undo flight + `DELETE /api/swipe/{id}` queued). +3. Dispatch `Backspace` -> same undo. +4. Focus the list search input, type arrows -> input cursor moves / text unchanged, zero swipe commits (assert pile length unchanged). +5. While the dossier is open, all arrow keys are inert; Esc closes it. While the viewer is open, Esc closes only the viewer (dossier, if underneath, stays open; a second Esc closes it). +6. Call `window.scrollTo(0,100)`-style check: with `document.body.scrollTop` manipulable, arrow dispatch does not change scroll (assert via `e.defaultPrevented` spy on `keydown`). + +**Parent verification:** + +1. `fe_walk_b1`-style headless walk: boot app, run acceptance 1–6 in order, assert green. +2. Optional human spot-check on MacBook over the tailnet. + +--- + +## Item 2 — Info-first tap flow + +**Goal (BACKLOG UI changes 1):** center tap → bike data dossier first; tapping the photo there → fullscreen image viewer; side taps still flip photos; Esc walks back out (viewer → data → deck). Resolves "can't get info unless I hit the ⓘ button". Esc routing is item 1's stack; this item changes the tap targets + the viewer's back-chain. + +**Current state:** `tapZone` (756–763) sends the center third to `openViewer(entry.d, entry.cur, {type:'card',entry})`; side thirds flip (`flipCard`, 525–536). `openDetail` is only reachable via ⓘ (578–580) / list rows (1036) / matches rows (1113). Dossier hero tap opens the viewer (832–837). Viewer close returns to whatever was under it (878–883). + +**Plan (all `app/static/index.html`):** + +1. `tapZone` (761): change the center branch to `openDetail(entry.d, {type:'deck'})`. Sides unchanged. Do not clear the card's photo position — the dossier is a sheet over the deck. +2. **Esc walk-back chain (new):** `openDetail` (768) must record *how it was opened* so a viewer reached from it can walk back. Add one module-level variable, e.g. `let dEntry=null;` — set in `tapZone`'s new branch to `{...dCtx, entry}` (the pile entry with its live `cur` photo index), set to `null` for row/matches opens (1036, 1113) and for ⓘ (580). `closeDetail` (822) clears it. +3. Viewer back-chain: when `tapZone`'s center branch opens the dossier, the dossier hero keeps its existing tap→`openViewer(dData,0,{type:'dossier'})` (835). Extend the viewer context so it can walk back to the dossier-on-deck: + - in `openViewer` (870–877): pass anextra field on `back`, e.g. `{type:'dossier', walkBack:dEntry}` or simpler: make the dossier-hero call (835) pass `{type:'dossier', backToDetail:dEntry}`; + - item 1's Esc handler calls, before plain `closeViewer()`, a new tiny `vWalkBack()`: if `vCtx.back.backToDetail` exists, capture the item, call `closeViewer()`, then `openDetail(item, backToDetail)` — image returns to the deck dossier (photo 0, per §3's hero contract), where the *next* Esc closes to the deck. If no `backToDetail`, plain `closeViewer()`. +4. The dossier closes to exactly the deck it came from: when opened from a deck card, the ⓘ/pull-down close semantics (822) already leave the pile untouched; no change. From a list row, Esc closes to the list (already true). + +**Edge cases:** + +- One-photo / no-photo cards: center tap still opens the dossier (data-first is the point); the hero tap no-ops when `dData.img` is null (835 already guards). +- The dossier's own action buttons (838–848) still commit the current top card — unchanged; the flow is open data, read it, swipe from inside it. That is desired: dossier actions exist today. +- Viewer opened the old way from a deck card — gone: after item 2 there is no direct card→viewer path; every deck-carded viewer is reached via dossier. `{type:'card',entry}` back shape (873) becomes unused from the deck; leave the plumbing, back-chain field only added for the dossier path. + +**Acceptance criteria:** + +1. Center tap (≤6px movement) on the top card opens `#detail` (`.on`), populated with that card's payload; **no** viewer opens; no commit fires. +2. Side taps still flip photos both directions, clamped; no dossier opens (existing B1 §11.2 behaviour re-verified). +3. Tap the dossier hero photo -> `#viewer` opens at photo 0. Esc -> viewer closes, dossier still open. Esc -> dossier closes, same deck/pile state and same top card (photo index intact). +4. ⓘ tap still opens the dossier (regression). +5. From the list: row tap -> dossier -> hero tap -> viewer -> Esc -> dossier -> Esc -> list (regression with the new Esc routing). + +**Parent verification:** + +1. Headless walk of acceptance 1–5 plus the B1 §11.7 suite re-run green (flip clamps, viewer swarm, no-lock, one `.topcard`). +2. Human: phone pass — Glenn taps the middle of a card, sees data, taps the photo, sees fullscreen, Esc/Esc (desktop) or ✕/pull-close (touch) walks back out. + +--- + +## Item 3 — Photos = option C (fit + blurred backdrop) + +**Goal (BACKLOG UI changes 2, decided 2026-11-12: "OH C! I love C!"):** the whole photo is shown uncropped (fit) over a soft blurred backdrop of itself, on the deck card AND the info-page hero. Fullscreen viewer unchanged (already fits). List thumbs stay `cover` (56px, irrelevant). Demo reference panel C: `~/dev/bike-tinder-demos/fit-vs-fill.html`. + +**Verified current CSS (index.html):** + +- Deck card photo: `.card .photo{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}` (36) — the element is inside `.phwrap` (513). +- Info hero: `.dphoto{width:100%;height:100%;object-fit:cover;display:block}` (70) inside `.dhero` (69; bg `#17171c`). +- Viewer: `.vimg{...object-fit:contain...}` (197) — already fits; **verify only, change nothing.** +- List thumbs: `.thumb{...object-fit:cover...}` (110) — **unchanged.** + +**Plan (CSS-only, `app/static/index.html`