Files
Bike-Tinder/README.md

143 lines
8.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Bike-Tinder
A private, Tinder-style web app for browsing the motorcycle market: swipe real listings (like / nope / maybe), see everything on one editable list, and let the deck learn your taste. Built for Glenn; runs on the Rotarran, published to the tailnet only.
**Status:** Phase 3 (personalization) built on `dev` (2026-11-11): sections of 25 (wildcards + second looks + new), taste engine with match points, tank gauges, dossier "Why this rank", real Trends tab, agent pass (mock / live / fallback). Phase 2 remains shipped as-is; deployment (podman stack) shipped 2026-11-12 — stack live under `podman-stack-bike-tinder.service`, nightly import + agent read + DB backup armed; the private phone URL (`https://biketinder.tailf35a34.ts.net`) awaits the one-time Tailscale sidecar authorization.
**B1 (photo experience v2)** built on `dev`: full photo galleries mirrored locally, deck flips (tap left/right thirds, clamped), segment bars, a fullscreen viewer (swipe nav, pinch/double-tap zoom, pull-down close), and repeat rotation (a re-shown card leads at its next photo). See UI-SPEC §11 + the "B1 develop notes" section below.
## How it works
```
[single-source crawler] → [nightly importer] → [app DB] → [API + PWA deck] → Glenn's thumb
↓
[swipe history] → [ranking]
```
- **Crawl** — **Kijiji** (NS + NB; decided 2026-11-10 — see `docs/SOURCES.md`) is swept nightly by the existing hunt crawler; a script-only importer folds the results in. No LLM involved, zero token cost.
- **Import** — the importer upserts the source's catalog into the app database: new listings appear, changes update, vanished listings retire after repeated misses (never a blocked page, never a guess).
- **Deck** — a Tinder-like PWA: card stack, drag to swipe, big buttons. Like / Nope / Maybe. "It's a match" moment when you like something.
- **List & edit** — every entry in one table; re-file any call later (a nope can become a like).
- **Ranking** — cold start uses the hunt's rubric score; a cheap, fully explainable swipe-boost lands first (v1.x), then a small learned model (Phase 3, retrained nightly) orders the deck by *your* taste. "Because you liked X" chips, never a black box.
## Docs
- [`docs/SOURCES.md`](docs/SOURCES.md) — the v1 source decision (candidates, evidence, choice)
- [`docs/DESIGN.md`](docs/DESIGN.md) — architecture, components, data flow, deployment
- [`docs/DATA-MODEL.md`](docs/DATA-MODEL.md) — database schema + sync semantics
- [`docs/RANKING.md`](docs/RANKING.md) — cold-start + learned ranking design
- [`docs/UI-SPEC.md`](docs/UI-SPEC.md) — screens, gestures, Tinder-fidelity goals
- [`docs/PLAN.md`](docs/PLAN.md) — phases, acceptance criteria, status
## Branches
- `main` — stable trunk (docs + verified code); the default branch.
- `dev` — active development; build work lands here first.
- `release` — deployable cuts; the Rotarran stack tracks this branch.
## Run & develop
Everything runs from the repo root (Python 3.13+, stdlib where possible).
```sh
# one-time setup
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt # fastapi + uvicorn (+ httpx for tests)
# import the hunt state into the DB (idempotent; safe to re-run any time)
python3 -m app.importer --state-dir ~/.hermes/scripts/motorcycle-hunt.d --db data/bike-tinder.sqlite3
# mirror card images into data/img/ (polite: ~1 req/s, skip-existing, resumable)
.venv/bin/python -m app.imgfetch --db data/bike-tinder.sqlite3
.venv/bin/python -m app.imgfetch --limit 15 # small test runs
# serve the app (127.0.0.1:9453 by default)
.venv/bin/python -m app.serve --db data/bike-tinder.sqlite3 --port 9453
# self-check: P1 + P2 + P3 (25 checks, temp DB copies only)
.venv/bin/python -m app.selfcheck
# P3: agent pass — mock is deterministic + offline (tests, offline forge);
# --live needs OPENROUTER_API_KEY (+ optional BT_AGENT_MODEL); --dry-run
# validates the payload and writes nothing; --print-summary prints exactly
# the strike summary a live pass would be sent.
.venv/bin/python -m app.agentpass --db /tmp/bt-copy.sqlite3 --mock
.venv/bin/python -m app.agentpass --db /tmp/bt-copy.sqlite3 --print-summary --dry-run
# P3: run a scratch server against a COPY (NEVER point a dev server at
# data/bike-tinder.sqlite3 while the live service owns it — use port 9454).
cp data/bike-tinder.sqlite3 /tmp/bt-copy.sqlite3
.venv/bin/python -m app.serve --db /tmp/bt-copy.sqlite3 --img-dir data/img --port 9454
curl -s http://127.0.0.1:9454/api/section # first read composes a section
curl -s -X POST http://127.0.0.1:9454/api/section/forge \
-H 'Content-Type: application/json' -d '{"size": 8}' # mock pass + compose
# proof CLI: ranked deck + counts (read-only)
python3 -m app.deck --db data/bike-tinder.sqlite3 --top 15
```
Key routes: `/` (PWA), `/api/health`, `/api/stats`, `/api/deck?limit=&cursor=` (read-only
legacy fallback), `POST /api/swipe` / `DELETE /api/swipe/{id}`, `/api/list?decision=&sort=&q=&limit=&offset=`,
`/api/listing/{id}`, `POST /api/import`, `POST /api/badge/seen`, `/img/{id}`
(cached file → 302 source CDN → 404 placeholder).
P3 (Phase 3): `GET /api/section`, `POST /api/section/forge` `{size}`,
`GET /api/trends`, `POST /api/agent/run` `{source: mock|live}`,
`POST /api/prefs` `{key: forge_mode, value: ondemand|strict}`. Listing
payloads carry `points`, `tank {value, band}`, `slot`, `round_next`,
`points_breakdown`. Full contract: `docs/UI-SPEC.md` §10 + `docs/RANKING.md`
Stage 2 + the task brief.
`data/` and `.venv/` are gitignored — the DB, image mirror and venv are local state.
## B1 develop notes (photo experience v2)
- **Columns** (guarded in `db.init_db`): `listings.imgs TEXT` (JSON array of
gallery URLs, page order; entry 0 = the primary = `img_url`) ·
`swipes.last_photo_idx INTEGER` (photo on screen at swipe time; drives §11.5
rotation `(last+1) % n`; NULL = photo 0).
- **Importer** copies the hunt state's `imgs[]` into `listings.imgs`; `img_url`
stays the state `img` and falls back to `imgs[0]`. Re-imports are
byte-stable (selfcheck proves it on the 640-row copy).
- **Route:** `GET /img/{id}?i=n` — mirrored file → CDN 302 → 404. `i=0`
(default) keeps the legacy chain (`{id}.webp`, then `{id}_0.webp`, then
`img_url`); `i>0` serves `{id}_{n}.webp` with the CDN fallback from
`imgs[n]`. Out-of-range/unknown → 404.
- **Mirror:** `app/imgfetch.py` downloads every `imgs[n]`. The primary (n=0)
lands at the legacy `data/img/{id}.webp` (one canonical file — no duplicate
fetch); extras land at `data/img/{id}_{n}.webp`. Same politeness as P2
(~1 req/s, skip-existing, `.part`-atomic, per-file CLI progress).
- **Payload:** deck/section/list/listing items add `n_photos` (int) and
`last_photo_idx` (int|null). `POST /api/swipe` accepts optional `photo_idx`
(non-negative int; persisted; absent key preserves, explicit null clears).
- **Frontend:** tap LEFT/RIGHT third = photo flip (clamped, no wrap, commits
nothing), CENTER third = fullscreen viewer (ⓘ still wins); segment bars per
photo (Tinder-style; band chips drop to `top:22px` when segments exist);
viewer = swipe nav + pinch 1×–4× + double-tap 1×↔2.5× + ✕/pull-down close
(returns to the same card/photo); re-shown cards lead at `(last+1) % n`;
preloads the top card's next photo + next card's lead; `sw.js` cache is
`bike-tinder-v3`.
- **FE walk (evidence):** `app/fe_walk_b1.py` — deterministic, headless:
```sh
.venv/bin/python -m app.fe_walk_b1 setup # fixtures: copy DB + 701-706 synthetics + local photos
env -u OPENROUTER_API_KEY .venv/bin/python -m app.serve \
--db /var/home/admin/dev/bike-tinder-b1/test-walk.sqlite3 \
--img-dir /var/home/admin/dev/bike-tinder-b1/test-img \
--host 127.0.0.1 --port 9454 # scratch server (mock forge)
.venv/bin/python -m app.fe_walk_b1 # 390 DOM checks
```
- **Test data:** `b1/test-state/` = hunt-state sample with `imgs` (8/3-photo
galleries; refresh `fetched_at` before an import gate test) ·
`b1/test-img/` = staged `{id}_{n}.webp` photos · `b1/test-copy.sqlite3` =
pristine live-DB snapshot (selfcheck copies it; never write to it).
## Deployment (planned)
A small podman compose stack on the Rotarran under `/var/mnt/DATA/podman-stacks/bike-tinder/`, its own systemd user unit (`Delegate=yes`), SQLite database. The stack ships its own Tailscale node (sidecar container — no host-tailnet coupling) and serves the app privately at `https://biketinder.tailf35a34.ts.net`. Single user; reachable only from Glenn's own devices.
## Crew
- **Glenn** — owner; the thumb that swipes.
- **Alex** — resident agent; architecture, orchestration, docs.
- **Torres** — engineer; builds and maintains this repo.