diff --git a/README.md b/README.md index e45fcb4..7c4afef 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ 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. +**Status:** Live. Phases 1–3 are deployed (sections — wildcards + second looks + new; taste engine with match points; tank gauges; dossier "Why this rank"; real Trends tab; agent pass mock / live / fallback), B1 is live (photo galleries, deck flips, fullscreen viewer), and v1.1.0 (QOL patrol) was deployed + released on 2026-11-13. The podman stack has served since 2026-11-12 under `podman-stack-bike-tinder.service`, with the nightly import + agent read + DB backup armed; the private phone URL (`https://biketinder.tailf35a34.ts.net`) was authorized (Tailscale sidecar) on 2026-11-12 and is in use from Glenn's phone. -**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. +**B1 (photo experience v2)** shipped 2026-11-11: 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 @@ -54,7 +54,7 @@ python3 -m app.importer --state-dir ~/.hermes/scripts/motorcycle-hunt.d --db dat # 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) +# self-check: P1–P3 + B1 + v1.1 (35 checks, temp DB copies only) .venv/bin/python -m app.selfcheck # P3: agent pass — mock is deterministic + offline (tests, offline forge); @@ -131,9 +131,11 @@ Stage 2 + the task brief. `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) +## Deployment (live) -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. +The app runs as a small podman compose stack on the Rotarran under `/var/mnt/DATA/podman-stacks/bike-tinder/` (a checkout of `release`), under its own systemd user unit (`podman-stack-bike-tinder.service`, `Delegate=yes`), SQLite database — deployed 2026-11-12. The stack ships its own Tailscale node (sidecar container — no host-tailnet coupling); the node was authorized on 2026-11-12, and the app serves privately at `https://biketinder.tailf35a34.ts.net` — in use from Glenn's phone. Single user; reachable only from Glenn's own devices. + +The nightly chain (`bike-tinder-import`, 08:30 daily) keeps it current: catalog import → live agent read (taste profile) → SQLite backup into the stack's `data/backup/` (rotation 14 deep). ## Crew diff --git a/app/fe_walk_v11.py b/app/fe_walk_v11.py index a4ebb25..b500969 100644 --- a/app/fe_walk_v11.py +++ b/app/fe_walk_v11.py @@ -544,6 +544,33 @@ def walk_rich(): and ('%d fresh' % stats['fresh']) in pill_txt, 'item 7: pill reads live + fresh (%s)' % pill_txt.replace('\n', ' ')) + # --- v1.1.1 (issue #2): the header pill keeps itself live -------------- + pl0 = api('/api/stats') + key('ArrowRight') # probe swipe, undone right after + flick(300) + pl1 = api('/api/stats') + dl = time.time() + 5 + ptx = '' + while time.time() < dl: + ptx = page.evaluate("() => document.getElementById('pill').textContent") + if ('%d fresh' % pl1['fresh']) in ptx: + break + time.sleep(0.2) + check(('%d fresh' % pl1['fresh']) in ptx and ('%d live' % pl1['live']) in ptx, + 'issue #2: pill refreshes itself after a swipe, no reload (%s -> %s)' + % (pl0['fresh'], ptx)) + key('ArrowDown') # undo the probe swipe (net-zero) + flick(300) + pl2 = api('/api/stats') + dl = time.time() + 5 + while time.time() < dl: + ptx = page.evaluate("() => document.getElementById('pill').textContent") + if ('%d fresh' % pl2['fresh']) in ptx: + break + time.sleep(0.2) + check(('%d fresh' % pl2['fresh']) in ptx, + 'issue #2: pill refreshes itself after an undo, no reload (%s)' % ptx) + # --- B1 no-lock: rapid commit storm (no sheet, no lock) ---------------- before = sect_pill()['txt'] taps(2, 40) diff --git a/app/static/index.html b/app/static/index.html index 75e71ae..a598722 100644 --- a/app/static/index.html +++ b/app/static/index.html @@ -415,6 +415,7 @@ function setDecision(id, act, secId=null, photoIdx=null){ localDecisions.set(id, act); updateRowDecision(id, act); // optimistic paint enqueue(id, act, secId, photoIdx); + refreshStats(); // v1.1.1 (#2): every decision change nudges the header counters } /* ---------- payload -> display helpers ---------- */ @@ -660,6 +661,7 @@ async function loadSection(){ buffer.push(...fresh); fillPile(); updateSectionUI(body); + refreshStats(); // v1.1.1 (#2): a new batch landed — freshen the counters }catch(e){ setTimeout(()=>loadSection(),15000); } finally{ deckLoading=false; } maybeRefill(); @@ -1206,6 +1208,19 @@ async function loadStats(){ fetch('/api/badge/seen',{method:'POST'}).catch(()=>{}); } } +/* v1.1.1 (issue #2): loadStats only ran at boot, so the header pill and the + list sub-line went stale after every swipe until a full reload. Refresh + shortly after any decision change (swipes, undo, dossier/list/matches + edits) and whenever a new batch lands. Coalesced + fire-and-forget: a + swipe never waits on it. */ +let statsTimer=null; +function refreshStats(){ + if(statsTimer) return; // bursts collapse into one read + statsTimer=setTimeout(()=>{ statsTimer=null; loadStats(); },600); +} +/* foreground/resume: catch anything that changed while the app slept + (e.g. the 08:30 import) without waiting for the next swipe */ +document.addEventListener('visibilitychange',()=>{ if(!document.hidden) refreshStats(); }); /* ---------- P3 section UI: progress pill + report + forge + modes ---------- */ let sectDecided=0, sectTotal=0; @@ -1297,6 +1312,7 @@ $('btnForge').onclick=async()=>{ buffer.push(...fresh); fillPile(); updateSectionUI(body); + refreshStats(); // v1.1.1 (#2): forged batch — freshen the counters if(pile.length){ $('report').classList.remove('on'); } // item 8: a zero-item forge on a fully drained catalog -> empty2, // no lone Forge button inviting a wasted agent run diff --git a/docs/UI-SPEC.md b/docs/UI-SPEC.md index 8a1574c..25f8173 100644 --- a/docs/UI-SPEC.md +++ b/docs/UI-SPEC.md @@ -90,7 +90,7 @@ Opened from deck ⓘ (or tap) AND list row tap. iOS-sheet geometry: top gap 26px 1. **Data wiring.** Deck buffer from `GET /api/deck` (limit 40, opaque cursor); refill when buffer <5 and a cursor exists. List from `GET /api/list`. Dossier from the row/card payload. Header from `GET /api/stats`. 2. **Swipe persistence.** Commits fire `POST /api/swipe` optimistically — the UI never waits. Failures go to a retry queue (attempts at ~2s/5s/10s, then every 15s while the tab lives); toast "Couldn't save — retrying" on first failure. Undo fires `DELETE /api/swipe/{id}` through the same queue. -3. **Badge.** `POST /api/badge/seen` once per session when the deck first renders. Header pill shows `{live} live · {fresh} fresh` always, and `· {new} new` only when new > 0. `fresh` = active listings with no swipe (the deck's new-pool predicate) — v1.1. +3. **Badge.** `POST /api/badge/seen` once per session when the deck first renders. Header pill shows `{live} live · {fresh} fresh` always, and `· {new} new` only when new > 0. `fresh` = active listings with no swipe (the deck's new-pool predicate) — v1.1. The pill and the list sub-line refresh themselves shortly after any decision change (swipes, undo, dossier/list/matches edits) and whenever a new batch lands — no reload needed (v1.1.1, issue #2). 4. **List filters + search** per §4 (mockup had placeholder chips only). 5. **Matches real (§5); Trends stays stub.** 6. **Images.** Every card/thumb references `/img/{id}` (server: cached file → 302 CDN → 404 → placeholder). The client never hotlinks directly.