Torres ac613e680a feat(v1.1): item 4 — minimal rubric (gauge + %) and rubric_debug 'why' toggle
- prefs API accepts the new rubric_debug key (off|on), the only new runtime
  write in v1.1; every section_state body now carries prefs.rubric_debug.
- fillRank renders the tank gauge + rounded % by default; the breakdown
  lines, audit footer and the quiet listing-enrichment fetch run only when
  the pref is on. List rows and the card face stay rubric-free (unchanged).
- Trends 'Actions' gains a 'Why breakdown: off | on' row in the setMode
  style; updateSectionUI repaints it on every section hydrate.
2026-11-13 13:31:21 -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%