# PLAN — Bike-Tinder **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 · 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. **Build authorized 2026-11-12** — sign-offs: ≥2-SL default (rich day 21 fresh/2 SL/2 wild) · violet band · Backspace alias; spec = `docs/V1.1-BUILD.md`; build phase in progress. - **v1.2** — analytics suite (`BACKLOG.md`): full interaction capture + from-scratch ledger reset ("fresh start"). ## v1 goal A single-source, single-user, tailnet-only Tinder-style PWA on the Rotarran: nightly-updated catalog, swipe deck (Like / Nope / Maybe), full editable list, taste-ranked ordering. ## Phase 0 — Planning - [x] Engineering consultation (Torres) → memo - [x] Design docs in this repo: SOURCES, DESIGN, DATA-MODEL, RANKING, UI-SPEC - [x] Glenn's calls (2026-11-10): v1 source = **Kijiji** · deck = show all, taste-ranked · new-bike feed = in-app badge only - [x] Repo housekeeping (2026-11-10): branches `dev`/`main`/`release` · dedicated `bike-tinder-import` cron (parked for deploy) · stack-owned Tailscale sidecar in the deploy plan - [x] Build gate opened — Glenn green-lit (2026-11-10) - **Exit criteria:** docs merged to `main` (done 2026-11-10); source decision recorded in `SOURCES.md`. ## Phase 1 — Data spine **Status: complete (2026-11-10).** Built by Torres on `dev` (`5bf180c..19e46cd`), independently re-verified by Alex (fresh-DB import x2, deck CLI, 8/8 selfcheck), merged to `main` (`d890dcd`). Live result: 640 rows → 560 active + 80 dup; deck top = Indian Scout family (91) then Vulcan 900 Classic (73). *(Build work lands on `dev`; verified milestones merge to `main`; deploys track `release`.)* - [x] DB schema + migrations (SQLite) - [x] Importer: source catalog → upsert into DB (new / changed / gone states) - [x] Cold-start scoring wired (existing rubric logic) - [x] Proof: CLI query shows a ranked, deduped queue from the real live catalog - **Exit criteria:** importer runs clean twice in a row (done — idempotent); counts reconcile against the source (done — 560+0+80=640); no schema surprises (done). **Carried into Phase 2 from P1 findings:** - Dedupe pairing refinement — **DONE (P2, 2026-11-10):** `min(ns,nb)` completeness pairing landed; 640 rows = 563 active + 77 dup, sibling rows stay visible. - Image backfill — **DONE (P2, 2026-11-10):** hunt backfill filled 619/620 Kijiji listings; app mirror `app/imgfetch.py` → 559 local webp (3 CDN 404s fall back to placeholder). - Dealer-blast mitigation + Stage 1.5 swipe boost matter immediately — only ~67/560 active rows are family-scored; the rest sort below. ## Phase 2 — The swipe app (core) **Status: built by Torres + verified by Alex (2026-11-10) on `dev`; phone pass LIVE (swiping over the tailnet).** Contract frozen: `UI-SPEC.md` v2 + `reference/mockup-001-fidelity.html` (Variant A "Pure", approved by Glenn; 5-deep live stack, no-lock commit, dossier sheet). Image backfill + app-side mirror (`app/imgfetch.py`) done: 619/620 listings enriched, 559 local webp. - [x] 2–3 UI mock variants → Glenn picked **Variant A "Pure"** (approved 2026-11-10 after two fix rounds) - [x] API (deck cursor, swipe upsert/clear, list + filters, stats, media, import) + PWA frontend (faithful mockup port: stack engine, drag, Like / Nope / Maybe, dossier, empty/undo) — no match overlay (removed by Glenn's call) - [x] Full list view with inline edit; basic filters (decision + sort + search) - [x] Proof: runs locally on :9453 (systemd dev unit) + tailnet `http://rotarran.tailf35a34.ts.net:9453`; phone-viewport browser pass green; **phone pass live 2026-11-10** — Glenn swiping the real deck over the tailnet (24 swipes recorded + verified from the DB; list-edit tap still unexercised by Glenn — automation-covered) - **Exit criteria:** Glenn can swipe a real deck on his phone and edit results in the list — swiping proven; edit-tap pending. ## Phase 3 — Personalization ("Trends") **Status: BUILT → VERIFIED → LIVE (2026-11-11).** Contract frozen 2026-11-10; built by Torres in ~45 min ($3.80, 128 calls, 25/25 selfcheck; commits `04463ca`, `c205d31`, `b6e1851`); independently verified by Alex on DB copies — selfcheck 25/25 re-run, full section loop (compose → strikes → rounds → report → forge), strict-mode 409/200, forge-on-demand, trends, tank gauges, dossier "Why this rank". **One real defect found + fixed** (`1e4d840`): second-look returns were filtered out of the swipe buffer (`decision===null`), leaving round-2 dead in the UI — fixed and verified end to end (SECOND LOOK band, restrike writes round 2). **Live integration 2026-11-11:** backup at `~/dev/bike-tinder-p3/db-backup-pre-p3-20261111.sqlite3`; service restarted; P3 tables + 66 seeded events; section 1 composed; live calibration pass run (`deepseek/deepseek-v4.1-flash`, $0.0042; `agent_runs` #1) → taste profile v1 (`source=agent`). Glenn's approved design (2026-11-10): - **Sections of 25:** the deck is a finite batch; the agent always runs between sections (learning segmentation + anti-doom-scroll). Modes: forge-on-demand (default) / strict (nightly read only). Fallback to the last profile if the agent fails. - **Section lead:** on open/refresh the best suggestions lead; the 5-card live stack is unchanged. - **Wildcards (~1-in-10):** labeled "maybe you'll like this one instead"; plausible stretches; never hard-noped families; their answers teach the model. - **Second looks:** likes + maybes + nopes return (chosen; ≤5/section, ≤2 nopes); Round 2 weighs ×2; flips are the loudest signal; a re-nope rests the item; rounds cap at 2. Returning maybes can bring their variants. - **Match points:** taste + agent adjustments; drive ordering, the section recipe (family caps, price mix), and returns. Explainable in the dossier; tank gauge (non-numeric) in the List. - **Hits objective:** order = expected-like; Trends reports the hit rate (likes; likes+maybes). - **Agent pass:** nightly/on-demand read (mock + live + fallback); **no LLM in the swipe path — ever.** - **Exit criteria:** after ≥30 swipes the deck order visibly reflects taste; explainability visible (dossier breakdown + Trends); the section loop works end-to-end; the agent kill-switch test passes (app fully functional on the last profile with the pass disabled). **Carried forward:** service-side OpenRouter key wiring at P4 — until then each forge's agent pass runs in mock mode and supersedes the live profile · wildcard pool hygiene (rare non-bike listings can surface) · early-ramp texture: dense nopes punish broad cells (year 2020+, 12k+, nb) — the Scouts read mid until the agent carves family-level exceptions · minor reload edge: an already-restruck second look can re-cycle on mid-section reload. **Glenn's call (2026-11-10, late): B1 "Photo experience v2" pulled forward** — built next, *before* Phase 4 ("get that feature baked in before we proceed to phase 4"). Spec frozen in `UI-SPEC.md` §11 (galleries · tap-side photo flip · center-tap fullscreen viewer · fresh lead photo on repeats) + `DATA-MODEL.md` B1 additions. **Status: BUILT → VERIFIED → LIVE (2026-11-11).** Rail: hunt gallery capture + full-catalog backfill — 639/640 state items carry `imgs[]` (612 with 2+ photos); the 1 remainder is a photo-less ad at source ("no photo found", retried and confirmed) → app shows the placeholder, by design. App: Torres on a fresh clone (`~/dev/bike-tinder-b1-build`, commits `3b64635..036a511`, report `~/dev/bike-tinder-b1/TORRES-B1-REPORT.md`); live tree + live DB untouched during the build. Verification: selfcheck 31/31 both sides; `fe_walk_b1` headless §11 walk 390/390 (Alex re-ran green with a fresh setup). **Live integration (2026-11-11, ~00:40 AST):** pre-B1 backups `~/dev/bike-tinder-b1/preb1-live-*.sqlite3` + `preb1-files/`; 11 files deployed; service restarted; `POST /api/import` idempotent (`new:0`); live DB now 639/640 with galleries, 612 multi-photo (sample id 430 Indian Sport Scout n=8; `/img/430?i=1` → 302). Live gesture proof: side-tap flip `?i=0 ↔ ?i=1`, center-tap viewer with segments, screenshot `~/dev/bike-tinder-b1/b1-live-9453.png`. Glenn's ledger intact (248 swipes / 279 events carried). `dev` + `main` merged (`49466fb`); live tree commit `3a399e1`. ## Phase 4 — Deploy + automate (stack deployed 2026-11-12; phone check pending) - ✅ Podman compose stack at `/var/mnt/DATA/podman-stacks/bike-tinder/` (release checkout) + systemd unit `podman-stack-bike-tinder.service` (`Delegate=yes`; verified to survive gateway/desktop restarts and re-raise after `systemctl --user restart`) + in-stack Tailscale sidecar (userspace mode) serving `https://biketinder.tailf35a34.ts.net` — **AUTHORIZED + serving 2026-11-12** (node `biketinder`, `100.114.195.22`; first phone swipes logged through the sidecar) - ✅ `bike-tinder-import` Hermes cron live (08:30 daily, post-hunt): import → live agent read → sqlite backup (14-deep); silent on success (three clean live runs on deploy day) - ✅ DB + image mirror migrated intact into the stack volume; backup drill produced its first files - ✅ Deploy fix (found on first in-container agent call): `requests` was missing from requirements — the host venv had it incidentally, the slim image did not - ✅ Runbook in DESIGN.md (deploy/rollback/logs/import gates/backup-restore/phone checklist) - ✅ **Release v1.0.0 published 2026-11-12** — Gitea release, tag pinned to `main` head; notes summarize the working loop; source zip/tar.gz auto-generated by Gitea (nothing attached) - ✅ In use from Glenn's phone (2026-11-12 — first photo flips + swipes logged through the sidecar); PWA "Add to Home Screen" available; full reboot test optional/pending - **Exit criteria:** survives a reboot (service-level verified; power-level test optional); refreshes overnight unattended (armed, first automatic run 08:30 next day); installable and used from the phone — **achieved 2026-11-12.** ## Non-goals (v1) - Multi-user accounts, social features, comments - Additional sources (v2+); Facebook (never — manual by Glenn) - Notifications beyond what already exists (Conduit is the house channel) - Buying workflow (contact/inspection tracking) — possible v2 ## Open decisions - v1 source — **resolved 2026-11-10: Kijiji** (see `docs/SOURCES.md`) - Mockup direction — Glenn picks at Phase 2 start