diff --git a/README.md b/README.md index 97d547f..6e7521b 100644 --- a/README.md +++ b/README.md @@ -27,9 +27,15 @@ A private, Tinder-style web app for browsing the motorcycle market: swipe real l - [`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. + ## 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, published to the tailnet only via `tailscale serve` on port 9453. Single user; no auth beyond the tailnet. +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 diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 86ac2a8..4aadf9b 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -16,7 +16,7 @@ Architecture for v1 (single source, single user, tailnet-only PWA). Written 2026 ``` 08:00 motorcycle-hunt crawl (existing, unchanged) → state JSONs: kijiji-ns.json, kijiji-nb.json -08:15 importer (script-only, zero LLM): +08:30 importer — script-only, zero LLM (Hermes cron `bike-tinder-import` → POST /api/import): gates → upsert → dedupe → score → gone-marking → image queue → SQLite (WAL) Deck phone ⇄ API ⇄ SQLite @@ -33,9 +33,9 @@ Deck phone ⇄ API ⇄ SQLite - **Scoring:** rubric tables ported from `reference/motorcycle-hunt-rank.py` (family tiers + extras + veto) — the score is Glenn's accumulated taste, computed at import, stored on each row. See `RANKING.md`. - **Idempotent:** safe to re-run any time; also exposed as `POST /api/import` for manual triggers. -## Scheduling 【decided】 +## Scheduling 【updated 2026-11-10 — Glenn's call】 -An in-container daily task (asyncio, 08:15 America/Halifax) inside the API process, with on-boot catch-up for missed runs. No host crontab is involved (the host user crontab is empty; the 08:00 crawl lives in Hermes cron — the app depends on neither). Every run writes an `import_runs` row; `GET /api/stats` surfaces the last run. +The nightly import is triggered by its own Hermes cron job — `bike-tinder-import`, **08:30 America/Halifax** (after the 08:00 hunt), run **script-only (zero LLM tokens)** and silent on success. The hunt's own cron is untouched. The container additionally keeps an **on-boot catch-up** (runs once if the last import is >26 h stale). Every path calls the same idempotent importer and writes an `import_runs` row; `GET /api/stats` surfaces the last run. (The cron job is created now, parked; it wakes at first deploy — Phase 4.) ## API surface (v1) @@ -47,7 +47,7 @@ An in-container daily task (asyncio, 08:15 America/Halifax) inside the API proce | `POST /api/swipe` `{id, action}` | record or edit like / nope / maybe (upsert) | | `DELETE /api/swipe/{id}` | clear a call (returns the card to the deck) | | `GET /api/list?filter=&q=&offset=` | full list, filters + search | -| `POST /api/import` | manual import trigger | +| `POST /api/import` | manual import trigger (also the cron hook) | | `GET /img/{id}` | cached image file, else 302 to source CDN | **Deck query:** `status='active'`, not noped, and either no swipe or a `maybe` older than 7 days — ordered `score DESC, first_seen DESC`. Unscored rows sort below scored ones but still appear — nothing is hidden. @@ -64,15 +64,19 @@ No-build SPA — vanilla JS + CSS, pointer-events drag, keyboard arrows on deskt ## Deployment (house constraints) -- **Stack dir:** `/var/mnt/DATA/podman-stacks/bike-tinder/` — `docker-compose.yml` + `Dockerfile` (`python:3.14-slim` + fastapi/uvicorn/requests), volume `./data:/data`, port `127.0.0.1:9453:8000`, `restart: unless-stopped`. +- **Stack dir:** `/var/mnt/DATA/podman-stacks/bike-tinder/` — `docker-compose.yml` + `Dockerfile` (`python:3.14-slim` + fastapi/uvicorn/requests), volume `./data:/data`, `restart: unless-stopped`. +- **App networking:** the app binds plain HTTP on the compose network and publishes **localhost-only** (`127.0.0.1:9453:8000`) — that's how the nightly trigger and local debugging reach it. No LAN exposure. +- **Tailscale sidecar — baked into the stack** (the repo carries **no host-tailnet coupling**; the app itself knows nothing about Tailscale): + - a `tailscale/tailscale` container joins the tailnet as its own node and serves the app at **`https://biketinder.tailf35a34.ts.net`** (TCP 443 via `tailscale serve` → `http://app:8000` over the compose network); + - auth: a deploy-time auth key minted by Glenn, stored only in the stack's local `.env` — never committed, never pasted anywhere else; + - deploy-time checks: the tailnet ACL must allow Glenn's devices to reach the `biketinder` node; first verification happens from his phone (the tailnet name can't be curled from this host). - After adding files: `chcon -Rt container_file_t /var/mnt/DATA/podman-stacks/bike-tinder`. - **Systemd user unit** `podman-stack-bike-tinder.service`, cloned from the machine's `podman-stack-searxng.service` pattern: `Type=oneshot`, `RemainAfterExit=yes`, **`Delegate=yes`** (keeps the stack out of the hermes-gateway cgroup so gateway restarts can't SIGKILL it); ExecStart/ExecStop = the venv `podman-compose up -d` / `down`. -- **Tailnet:** `tailscale serve --bg 9453` → reachable at `http://rotarran.tailf35a34.ts.net:9453`, tailnet-only. The tailnet name can't be curled from this host — first deploy verification happens from Glenn's phone. - **Backups:** nightly `sqlite3 .backup` into `data/backup/` right after import (the DB is the only mutable state; house rule). Restore = stop stack, swap the file, start. ## Security -Single user; **no auth in v1** — localhost bind + tailnet-only reachability. An optional `AUTH_TOKEN` env var is noted as a one-line later hardening. No secrets are required in the stack. +Single user; **no auth in v1** — the app is reached only through the stack's own Tailscale node (plus a localhost-only port for triggers and debugging). An optional `AUTH_TOKEN` env var is noted as a one-line later hardening. One deploy-time secret exists in the stack: the Tailscale auth key (local `.env` only). ## Risks & mitigations (from consultation) @@ -90,7 +94,7 @@ Single user; **no auth in v1** — localhost bind + tailnet-only reachability. A ``` app/ importer.py · api.py · static/{index.html,app.js,app.css,manifest.json,sw.js,icons} -deploy/ docker-compose.yml · Dockerfile · podman-stack-bike-tinder.service +deploy/ docker-compose.yml (app + Tailscale sidecar) · Dockerfile · podman-stack-bike-tinder.service reference/ copies of the hunt modules the app vendors from data/ runtime state (gitignored) docs/ this design set diff --git a/docs/PLAN.md b/docs/PLAN.md index 92ec010..11e5080 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -11,11 +11,14 @@ A single-source, single-user, tailnet-only Tinder-style PWA on the Rotarran: nig - [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 - [ ] Glenn's skim of the docs — build gate opens after - **Exit criteria:** docs merged to `main` (done 2026-11-10); source decision recorded in `SOURCES.md`. ## Phase 1 — Data spine +*(Build work lands on `dev`; verified milestones merge to `main`; deploys track `release`.)* + - DB schema + migrations (SQLite) - Importer: source catalog → upsert into DB (new / changed / gone states) - Cold-start scoring wired (existing rubric logic) @@ -39,9 +42,8 @@ A single-source, single-user, tailnet-only Tinder-style PWA on the Rotarran: nig ## Phase 4 — Deploy + automate -- Podman compose stack at `/var/mnt/DATA/podman-stacks/bike-tinder/` + systemd unit (`Delegate=yes`) -- Tailscale serve on 9453; PWA install on Glenn's phone -- Nightly no-LLM importer cron (post-crawl); DB backup routine +- Podman compose stack at `/var/mnt/DATA/podman-stacks/bike-tinder/` + systemd unit (`Delegate=yes`) + in-stack Tailscale sidecar serving `https://biketinder.tailf35a34.ts.net`; PWA install on Glenn's phone +- Enable the new `bike-tinder-import` Hermes cron (08:30 daily, post-hunt; script-only, zero LLM; parked until deploy); DB backup routine - Runbook in DESIGN.md; announce + handover note - **Exit criteria:** survives a reboot; refreshes overnight unattended; installable and used from the phone.