Torres 0d2e5a592f feat(v1.1): item 6 — dry-catalog messaging (forged batches with 0 fresh are labeled)
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.
2026-11-13 13:39:22 -04:00

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

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[] 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:

    .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.
Description
Tinder. For motorcycles.
Readme 1 MiB
2026-11-13 14:29:08 -04:00
Languages
Python 74.7%
HTML 24.6%
JavaScript 0.5%
Dockerfile 0.2%