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.
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— the v1 source decision (candidates, evidence, choice)docs/DESIGN.md— architecture, components, data flow, deploymentdocs/DATA-MODEL.md— database schema + sync semanticsdocs/RANKING.md— cold-start + learned ranking designdocs/UI-SPEC.md— screens, gestures, Tinder-fidelity goalsdocs/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).
# 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[]intolistings.imgs;img_urlstays the stateimgand falls back toimgs[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, thenimg_url);i>0serves{id}_{n}.webpwith the CDN fallback fromimgs[n]. Out-of-range/unknown → 404. -
Mirror:
app/imgfetch.pydownloads everyimgs[n]. The primary (n=0) lands at the legacydata/img/{id}.webp(one canonical file — no duplicate fetch); extras land atdata/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) andlast_photo_idx(int|null).POST /api/swipeaccepts optionalphoto_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:22pxwhen 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.jscache isbike-tinder-v3. -
FE walk (evidence):
app/fe_walk_b1.py— deterministic, headless:.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 withimgs(8/3-photo galleries; refreshfetched_atbefore an import gate test) ·b1/test-img/= staged{id}_{n}.webpphotos ·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.