The tailnet-only Hungarian community “top kép” voting system on telep-mainframe — people vote favourite Frigate person-detection snapshots per day (/nap), which roll up into weekly (/het) then monthly (/honap) rounds. Voted pics are saved locally on the mainframe (survive Frigate’s ~14-day event purge); Telegram reminders fire on a schedule. LIVE as of 2026-08-10. This is the canonical “how it works / where it lives” doc. It REPLACES the Google-Drive top-pics workflow (2026-08-10-top-kepek-drive-curation-print-pipeline).

For Agents

Everything runs on telep-mainframe. Identity is the Tailscale-User-Login request header — one vote per tailnet identity. No secrets belong in this note — the Telegram bot token is referenced by file path only and read at runtime. The design spec is 2026-08-10-top-kep-community-voting-spec; the task-by-task implementation plan is 2026-08-10-top-kep-community-voting-plan. top4-web.py is a stdlib http.server app, NOT Flask (Flask is not installed on the box) — earlier vault text calling it “Flask” is wrong.

Quick Reference

  • Web app: /home/levander/top4-web.py, systemd top4-web.service (User=levander), binds 127.0.0.1:8090 → exposed https://telep-mainframe.taild4189d.ts.net:8443
  • Routes (new): GET /nap /het /honap (Hungarian voting grids) · GET /pic/<event_id> (local saved jpg if present, else proxies Frigate) · POST /vote (form event_id/camera/captured_ts → JSON {votes}; 403 without Tailscale-User-Login)
  • Data/logic module: /home/levander/top_kep_store.py (Python 3 stdlib only, no deps)
  • SQLite DB: /srv/top-kep/votes.db · Saved images: /srv/top-kep/img/YYYY-MM-DD/<event_id>.jpg
  • Reminder CLI: /home/levander/top_kep_remind.py (symlink ~/top-kep-remind.py) — --daily|--weekly|--monthly [--dry]
  • Timers: top-kep-remind-daily.timer (20:00) · top-kep-remind-weekly.timer (Sun 19:30) · top-kep-remind-monthly.timer (daily 21:00, guarded)
  • Frigate API: http://127.0.0.1:5000 · Telegram group: -1004475187307 (『Telephely biztonsági riasztások』)
  • Bot token (runtime read, NOT in source): /home/levander/nvr/frigate-notify/config.yml
  • Tests: /home/levander/test_top_kep.py (stdlib unittest), 12/12 pass
  • Home portal tile: ~/home-portal/index.html (:8093) “Top képek” tile → …:8443/nap

What it does

Replaces the Drive-based top-pics flow: instead of an agent/user pushing curated frames to Google Drive, the community browses and votes favourite camera pics themselves, tailnet-only, in Hungarian. Daily votes roll up — the week and month re-rank the pics already voted in that window. Picked images are stored on the mainframe (not Drive) until further instruction.

Web routes (added to top4-web.py)

Added to the existing stdlib http.server app (class H(BaseHTTPRequestHandler) + ThreadingHTTPServer), reusing the existing :8443 tailnet exposure — no new service.

RouteMethodBehaviour
/napGETToday’s deduped person candidates as a Hungarian grid + ❤ buttons, chronological
/hetGETPics with ≥1 vote in the current ISO week (Mon–Sun), vote-count desc
/honapGETPics with ≥1 vote in the current calendar month, vote-count desc
/pic/<event_id>GETServes the locally saved jpg if it exists, else proxies the snapshot from Frigate
/votePOSTForm event_id/camera/captured_ts → toggles the caller’s ❤, returns JSON {votes}; 403 without Tailscale-User-Login

/vote resolves the voter from the Tailscale-User-Login header. If a (event_id, voter) vote row exists it is deleted (un-vote), else inserted. On the pic’s first-ever vote its full-res clean snapshot (bbox=0&quality=100) is fetched from Frigate and persisted locally (idempotent — never re-fetched). Week = ISO week containing “now”; month = calendar month; all Europe/Budapest.

Data & logic — top_kep_store.py

Pure Python 3 stdlib (sqlite3 + urllib), zero deps, unit-testable in isolation. SQLite at /srv/top-kep/votes.db:

pics(event_id PK, camera, captured_ts, saved_path, first_voted_ts)
votes(event_id, voter, voted_ts, PRIMARY KEY(event_id, voter))
reminder_state(kind PK, last_event_ts)
  • Candidates = Frigate label=person events for the day, deduped to one per (camera, whole-second) keeping the highest top_score (mirrors a de-noised /explore?labels=person).
  • Persist-on-first-vote: the clean full-res snapshot is written to /srv/top-kep/img/YYYY-MM-DD/<event_id>.jpg (date = capture date) so votes survive Frigate’s ~14-day event purge. Unvoted pics are never stored.
  • event_id is the unique key — NOT the _score that appears in the old Drive-flow filenames (ties share a score; see the gotcha).

Reminders — top_kep_remind.py + systemd timers

CLI /home/levander/top_kep_remind.py (symlink ~/top-kep-remind.py): --daily | --weekly | --monthly [--dry]. Reads the bot token at runtime from /home/levander/nvr/frigate-notify/config.yml (no secret in source), sends via the Telegram Bot API to group -1004475187307. --dry is fully side-effect-free.

TimerScheduleGuard
top-kep-remind-daily.timerOnCalendar=*-*-* 20:00Sends only if today’s newest candidate is newer than the stored watermark (reminder_state.daily.last_event_ts); watermark advances only after a confirmed non-dry send
top-kep-remind-weekly.timerOnCalendar=Sun *-*-* 19:30none
top-kep-remind-monthly.timerOnCalendar=*-*-* 21:00 (daily)CLI sends only when tomorrow is the 1st — a single daily timer covers 28/30/31-day months

All units User=levander, WorkingDirectory=/home/levander, Persistent=true (a reminder missed during a power outage fires on the next boot).

Drive removal

  • The new voting flow writes nothing to Drive.
  • The weekly Drive export top4-export.timer (Mon 08:00) was DISABLED.
  • The old picker’s Drive buttons/endpoints (/save-full-to-drive, /save-crop-to-drive, ~/top-kep-drive.py, ~/top-kep-id.py) and the 4-slot camwall flow (/srv/frigate/top4/{1..4}.jpg) were left intact and working — a full Drive rip-out is deferred until the user asks. See 2026-08-10-top-kepek-drive-curation-print-pipeline for that (now-superseded) flow.

Home portal

The “Top képek” tile in ~/home-portal/index.html (served :8093, node home.taild4189d.ts.net — the home portal) was repointed from the old picker to …:8443/nap.

Testing

/home/levander/test_top_kep.py (stdlib unittest, no framework) — 12/12 pass. Covers dedup collapse, per-identity vote toggle netting to zero, persist-once (saved_path set on first vote, unchanged after), and the month-end reminder guard across 28/30/31-day months.

Known follow-ups / deferred

  • Cosmetic: the ❤ button doesn’t pre-highlight pics the current user already voted on after a page reload (the server-side tally stays correct).
  • Deferred: full Drive rip-out; a print/export path off the local /srv/top-kep/img store (“stored on mainframe until further instruction”).
  • Resolution ceiling: pics are 720p (detect stream). >720p for printing would need pulling from the record/main stream — same limitation as the old pipeline.

Review-pass hardening (2026-08-10)

A two-lens adversarial review pass — correctness/concurrency + security/data-integrity — was run over the voting system. Findings were fixed and verified on telep-mainframe.

CRITICAL: client-side voting had never worked (pre-existing, missed by earlier reviews)

GRID_TAIL was concatenated raw into the served HTML, but had been written with doubled {{/}} braces (as if it were going through .format() like GRID_HEAD/GRID_CARD). The doubled braces leaked literally into the <script>, making it invalid JS — so vote() was never defined and clicking the ❤ did nothing. Fixed: GRID_TAIL now uses single braces. Verified: the served /nap script passes node --check. Why earlier reviews missed it: they curl’d the JSON endpoint and never rendered the page in a browser. Lesson: verify web UIs by rendering the page / checking the served JS, not only by curling the JSON endpoint.

Confirmed SAFE (verified)

  • Identity spoofing not possible. top4-web binds 127.0.0.1 only, fronted by tailscale serve (HTTP-proxy) which overwrites the Tailscale-User-Login header from the verified tailnet peer — a client cannot forge it.
  • Telegram bot token cannot leak via exceptions. str(e) on urllib errors omits the URL, and the web process never touches the token (token is read only by the reminder CLI).

Fixed and verified

  • Stored XSScamera was client-supplied in the /vote POST and rendered unescaped in the grid. render_grid now HTML-escapes event_id/camera/title (html.escape, quote=True). Verified: /het renders the payload escaped.
  • Concurrent toggle race — a double-tap fired two /vote POSTs → IntegrityError → HTTP 500 (or a lost vote). toggle_vote rewritten atomic: DELETE … rowcount detects an un-vote, INSERT OR IGNORE for the vote.
  • Disk reclaim — un-voting never freed the saved jpg. Now when a pic drops to 0 votes its jpg and pics row are removed. Verified.
  • Client error handling — the vote() JS treated any non-403 error as success (showed undefined); added an if(!r.ok) guard.
  • CSRF hardening/vote now 403s when Sec-Fetch-Site: cross-site. Verified.
  • DB perms/srv/top-kep/votes.db was mode 644 (exposed voter emails), now 600. Verified.

Open product decision (NOT yet done)

The paired physical cameras (telep_cam1/telep_cam2 = south, telep_cam3/telep_cam4 = north) are different Frigate camera names, so the same physical moment can appear twice in /nap (dedup keys on camera-name + second). Decide: dedup by physical-camera group, or keep both angles.

Deferred minors

  • Derive camera server-side from Frigate instead of trusting the client.
  • Validate event_id against Frigate’s id charset.
  • Per-handler sqlite conn close (benign — CPython closes at scope exit).

Update — 2026-08-11: mobile UI, lightbox, person/car filter, HD recording + HD upgrade

A second build pass on the voting system, all DONE and verified live in a real (headless Playwright) browser at phone width on telep-mainframe. The headline: pics can now be served at real HD (2304×1296), not just the 720p detect stream — but only for events captured after a Frigate recording reconfig done in this pass.

For Agents

New routes/params on ~/top4-web.py: GET /pic/<id>?full=1 (full-res persisted snapshot for the lightbox), GET /pic/<id>?hq=1 (HD recording-snapshot, cached to /srv/top-kep/hq/<id>.jpg), and a ?label=person|car query param on /nap /het /honap (default person, validated against LABELS=("person","car")). A label TEXT column was added to the pics table (ALTER-TABLE migration in init_db). HD only works for events recorded AFTER the reconfig below; older pics stay 720p.

Mobile-first UI + tap-to-zoom lightbox

render_grid/templates in top4-web.py reworked for phones: bigger 16:9 cards, full-width Nap/Hét/Hónap tabs, 44px+ touch targets, responsive grid repeat(auto-fill, minmax(min(100%, 340px), 1fr)). Tapping an image opens a full-screen lightbox (#lb) showing the full-res snapshot (/pic/<id>?full=1) with:

  • a “Nagyítás” zoom toggle (native-size scrollable + mobile pinch-zoom),
  • a vote button,
  • a “Magasabb minőség” (HD) button (see below),
  • “Bezár” to close.

Person / Car filter

_frigate_events / day_candidates / votes_for_window now take a label param; the routes read ?label=person|car (default person, validated against LABELS=("person","car")). A label TEXT column was added to the pics table (ALTER-TABLE migration in init_db), stored on persist and threaded through toggle_vote / _vote. A Személy / Autó filter bar in the UI persists the chosen label across the day/week/month tiers. Verified live: today person=189, car=36; a car vote stored pics.label='car'.

HD recording — Frigate reconfig (the real enabler)

Root cause of the 720p ceiling

Previously every camera both detected and recorded the 720p sub stream, so no HD frame ever existed for past events — the “pics are 720p” limitation was baked in at the recording layer, not just the snapshot layer.

Reconfig: each telep_camN now detects on camN_sub (720p, role: detect) and RECORDS a new camN_main go2rtc stream (role: record) at 2304×1296. Lens mapping (verified by still comparison):

Frigate cameraPhysical lensmain stream (record)
telep_cam1DÉL fix.119 / stream1
telep_cam2DÉL PTZ.119 / stream6
telep_cam3ÉSZAK fix.139 / stream1
telep_cam4ÉSZAK PTZ.139 / stream6
  • Config backup on the box: /home/levander/nvr/frigate/config.yml.bak-hqrec-1786445779.
  • Detection fps unaffected (~5 on the subs).
  • Storage impact ~3–4× recordings, on a disk that’s 6% used (3.2T free) — fine.

Caveat to watch — camera-side RTSP connection limits

Each Tapo now serves 4 concurrent RTSP pulls (2 lenses × sub+main) + ONVIF. Watch for camera-side connection-limit refusals if anything else starts pulling from .119 / .139.

”Magasabb minőség” (HD) button

The lightbox HD button hits /pic/<id>?hq=1, which fetches the main-stream recording-snapshot at the event’s timestamp (/api/<camera>/recordings/<start_time>/snapshot.jpg, now HD), caches it to /srv/top-kep/hq/<id>.jpg, and serves 2304×1296. Fallback chain (never 500s):

  1. recording-snapshot (HD), else
  2. event-clip ffmpeg frame, else
  3. 720p detect snapshot.

event_id is charset-validated (^[0-9.]+-[A-Za-z0-9]+$) before any subprocess / URL / path use — this also closes the earlier “validate event_id charset” deferred item. HD works only for events captured AFTER the recording reconfig; older pics stay 720p.

Invariants maintained (for future agents)

Don't break these when editing top4-web.py

  • All interpolated values in render_grid are html.escaped — the stored-XSS fix stays.
  • GRID_HEAD / GRID_CARD are .format()ed (braces doubled); GRID_TAIL is raw-concatenated (single braces) — the served <script> must contain zero {{ (this was the prior critical GRID_TAIL bug).
  • event_id is charset-validated before any subprocess/URL/path use.

Backups on the box from this pass: top4-web.py.bak-ui-*, .bak-hq-*, .bak-lbl-*, .bak-hq2-*; top_kep_store.py.bak-lbl-*.

Verification (2026-08-11)

Confirmed live via a headless real browser (Playwright) at phone width, with the Tailscale identity injected by tailscale serve:

  • filter toggles work (car page = 36 cards, data-label=car),
  • the lightbox opens with the full-res image,
  • “Magasabb minőség” upgrades the image 1280 → 2304×1296 (button → “HD ✓”),
  • the vote → persist → un-vote → reclaim path works end-to-end.