Top kép community voting (Drive-free) — design spec
Replaces the Google-Drive top-pics workflow with a self-hosted, tailnet-only community voting system on telep-mainframe: people vote their favourite camera pics for the day, then the week, then the month. Daily votes roll up. Picked images are stored on the mainframe until further instruction. Hungarian UI + reminders. Supersedes the Drive half of 2026-08-10-top-kepek-drive-curation-print-pipeline.
For Agents
Extends the existing Flask picker
~/top4-web.py(top4-web.service,127.0.0.1:8090→https://telep-mainframe.taild4189d.ts.net:8443). Frigate API athttp://127.0.0.1:5000. No secrets in this note — the Telegram bot token is referenced by file path only.
Goal / success criteria
- Community browses and votes favourite pics per Nap / Hét / Hónap in Hungarian, tailnet-only.
- Reminders fire: daily if new candidates since last reminder, Sun 19:30 weekly, month-end 21:00 monthly — all to the existing Telegram security group.
- Voted pics persist on the mainframe (survive Frigate’s ~14-day event purge).
- Google Drive is out of the new flow entirely.
Decisions (locked)
| Fork | Decision |
|---|---|
| Voting channel | Extend the existing tailnet Flask picker (not Telegram-native, not public) |
| Identity | Tailscale-User-Login request header (one vote per tailnet identity; shared login = shared vote) |
| Aggregation | Daily votes roll up — week/month re-rank pics already voted in that window |
| Reminder target | Existing group -1004475187307 (『Telephely biztonsági riasztások』), existing bot token |
| Candidate set | Frigate person events for the day, deduped to one per second per camera (mirrors /explore?labels=person) |
| Save trigger | On first vote — fetch clean full-res snapshot, persist locally. Unvoted pics never stored. |
| Daily reminder trigger | Only if new candidates since last reminder (watermark), evaluated at 20:00 |
| Drive | New flow writes nothing to Drive; weekly top4-export.timer disabled; full rip-out deferred |
Architecture
Single new module added to the existing top4-web.py Flask app — no new service, reuses the :8443 tailnet exposure. SQLite for state (stdlib sqlite3, zero deps). Reminders are standalone systemd timers calling a small sender script.
Frigate API (person events) ──dedup/sec/cam──> candidate feed ──render──> /nap /het /honap (grid + ❤)
│ vote (Tailscale-User-Login)
▼
first vote ──fetch full-res──> /srv/top-kep/img/YYYY-MM-DD/<id>.jpg
│
SQLite /srv/top-kep/votes.db
│
systemd timers (daily/weekly/monthly) ──> top-kep-remind.py ──Bot API──> Telegram group -1004475187307
Units
voteblueprint intop4-web.py— routes, candidate feed, dedup, vote toggle, tier ranking. Depends on: Frigate API, the store.- Store (
top-kep-storehelpers) — thin SQLite wrapper:record_vote,remove_vote,votes_for(window),persist_pic. Depends on:sqlite3, Frigate snapshot fetch. top-kep-remind.py— CLI:--daily | --weekly | --monthly; builds the Hungarian message, applies the trigger guard, sends via Bot API. Depends on: the store (for candidate/watermark counts), bot token file.
Data model — SQLite /srv/top-kep/votes.db
CREATE TABLE pics (
event_id TEXT PRIMARY KEY, -- Frigate event id (unique key; NOT the _score in filenames)
camera TEXT NOT NULL,
captured_ts INTEGER NOT NULL, -- unix seconds (Frigate start_time)
saved_path TEXT, -- set when persisted on first vote
first_voted_ts INTEGER
);
CREATE TABLE votes (
event_id TEXT NOT NULL,
voter TEXT NOT NULL, -- Tailscale-User-Login
voted_ts INTEGER NOT NULL,
PRIMARY KEY (event_id, voter)
);
CREATE TABLE reminder_state (
kind TEXT PRIMARY KEY, -- 'daily'
last_event_ts INTEGER -- watermark: newest candidate captured_ts at last daily reminder
);Images: /srv/top-kep/img/YYYY-MM-DD/<event_id>.jpg (date = capture date), native-res clean snapshot (bbox=0&quality=100).
Candidate feed & dedup
- Query
GET http://127.0.0.1:5000/api/events?label=person&after=<day_start>&before=<day_end>&has_snapshot=1. - Dedup to one event per (camera, whole-second): bucket by
(camera, floor(start_time)), keep the highesttop_scorein each bucket. This mirrors what the user sees on/explore?labels=personde-noised to a browseable set. - Thumbnails for unvoted candidates are fetched live from Frigate (
/api/events/<id>/snapshot.jpg?bbox=0&quality=100), consistent with the existing picker’s on-demand thumbnail loading. Reuse the existingIntersectionObserverbatched scroll to avoid hammering Frigate.
Routes (Hungarian)
| Route | Shows | Order |
|---|---|---|
GET /nap | today’s deduped candidates | chronological |
GET /het | pics with ≥1 vote in the current ISO week | vote count desc |
GET /honap | pics with ≥1 vote in the current calendar month | vote count desc |
POST /vote | {event_id} → toggle ❤ for the caller | — |
/vote behaviour:
- Resolve voter from
Tailscale-User-Loginheader. If absent →403(tailnet identity required).ponytail:header-only per the locked decision; add name-pick fallback only if shared-device voting becomes a real complaint. - If a vote row
(event_id, voter)exists → delete it (un-vote). Else insert it. - On the first-ever vote for
event_id(pics row has nosaved_path): fetch the full-res snapshot from Frigate, write to/srv/top-kep/img/…, setsaved_path+first_voted_ts. Idempotent — never re-fetch if already saved. - Return the new vote count.
Week = ISO week (Mon–Sun) containing “now”, Europe/Budapest. Month = calendar month, Europe/Budapest.
Reminders — systemd timers → Telegram
Bot token read from /home/levander/nvr/frigate-notify/config.yml (mode 600, already used by other services); target chat -1004475187307. Sent via Bot API sendMessage. Link base: https://telep-mainframe.taild4189d.ts.net:8443.
| Timer | Schedule | Guard | Message |
|---|---|---|---|
top-kep-remind-daily.timer | daily 20:00 | send only if newest candidate captured_ts > reminder_state.daily.last_event_ts; then update watermark | 📸 Vannak új képek a mai napról — szavazz a kedvenceidre: …/nap |
top-kep-remind-weekly.timer | OnCalendar=Sun *-*-* 19:30 | none | 🗓️ Heti szavazás — válaszd ki a hét legjobb képeit: …/het |
top-kep-remind-monthly.timer | daily 21:00 | send only if tomorrow is the 1st (covers 28/30/31) | 📅 Havi szavazás — a hónap legjobb képei: …/honap |
Timers use Persistent=true so a reminder missed during a power outage fires on the next boot. All units User=levander.
Drive removal
- New voting flow: no Drive writes.
- Disable
top4-export.timer(weekly Mon 08:00 Drive export) — superseded. - Leave the old picker’s
/save-full-to-drive//save-crop-to-driveendpoints, the bulk helpers (~/top-kep-drive.py,~/top-kep-id.py), and the 4-slot camwall flow (/srv/frigate/top4/{1..4}.jpg) in place and working — not part of this change. Full Drive rip-out is a separate, later task on the user’s word.
Home portal
Repoint the existing “Top képek” tile in /home/levander/home-portal/index.html (served :8093, node home.taild4189d.ts.net) from the old picker to …:8443/nap. Leave an index.html.bak-* backup (existing convention).
Verification (one runnable self-check)
test_top_kep_vote.py (stdlib unittest / plain asserts, no framework), asserting:
- Dedup: a synthetic burst of N
personevents within the same second on one camera collapses to exactly 1 candidate; different seconds or different cameras stay separate. - Vote toggle: two
/votecalls for the same(event_id, voter)net to zero; count is per-identity, not per-request. - Persist-once:
saved_pathis set on first vote and unchanged (no re-fetch) on subsequent votes. - Month guard: the monthly-reminder guard returns true iff tomorrow is day 1, for a spread of month-lengths (Feb 28, Apr 30, Jul 31).
Open / deferred
- Daily reminder time 20:00 is a chosen default — trivially adjustable in the timer.
- Full Drive rip-out (endpoints, helpers, config) — deferred until the user confirms nothing there is still wanted.
- Print/export path off the local store — deferred (“stored on mainframe until further instruction”).
- Larger-than-720p pics for printing would require pulling from the record/main stream (2304×1296), same limitation as the old pipeline — out of scope here.
Related
- 2026-08-10-top-kepek-drive-curation-print-pipeline — the Drive pipeline this replaces
- telep-mainframe · telep-mainframe-handover · telep-router
- 2026-07-24-global-dashboard — home portal tile
- tailnet-service-exposure-convention · 2026-08-10-session-handover