Tréninkový deník: automatická synchronizace s Garmin Connect, statistiky, segmenty a osobní rekordy.
Verze: 2.0 (produkční revize)
Stav: připraveno k implementaci
Umístění v repu: docs/spec.md — zdroj pravdy o architektuře a rozhodnutích
Sunder je webová aplikace pro sledování sportovních aktivit, která se automaticky synchronizuje s Garmin Connect, počítá statistiky, segmenty tratí a osobní rekordy. Open-source (veřejné repo), víceuživatelská, provozovaná v rámci free tierů.
Provozní model: start jako uzavřená instance jen pro majitele, postupné otevírání dalším uživatelům až po ověření bezpečnosti a stability (viz sekce 6.6).
Garmin nemá dostupný oficiální Developer API přístup pro tento typ projektu. Použije se neoficiální přístup přes stejné přihlášení jako Garmin Connect web/appka (garth / python-garminconnect) — ne OAuth. Architektura proto stojí na periodické úloze, která se přihlásí jménem uživatele a stáhne data.
Disclaimer: Tento přístup je proti Garmin Terms of Service. Riziko je omezení/zablokování Garmin účtu za automatizovaný přístup. Musí být uvedeno v README a odsouhlaseno uživatelem při propojování účtu.
Garmin Connect (SSO, neoficiální přístup)
▲
│ 1. přihlášení + stažení nových aktivit (rate-limited, s backoffem)
│
GitHub Actions (cron, Python: garth / python-garminconnect)
│
│ 2. zápis aktivit, statistik a sync logu (service role key)
▼
Supabase (Postgres + Auth + Storage, chráněno RLS)
▲
│ 3. čtení dat (anon key + RLS, scoped na auth.uid())
│
Frontend SPA (React + Vite, hostováno na GitHub Pages)
| Komponenta | Služba | Limit zdarma | Pozn. |
|---|---|---|---|
| Frontend | GitHub Pages | prakticky bez limitu | statický build |
| Sync + CI | GitHub Actions | fair use (veřejné repo) | limit 2000 min/měsíc platí jen pro privátní repa |
| DB + Auth + Storage | Supabase free | 500 MB DB, 1 GB storage, 5 GB egress, 50k MAU | žádné zálohy → viz 8.3 |
Pozastavení Supabase projektu: free projekt se pozastaví po 7 dnech bez requestů. Hodinový sync cron ho udržuje aktivní automaticky — není potřeba samostatný keepalive.
Frekvence synchronizace: periodická (výchozí 1×/hod), ne real-time. Odpovídá požadavku "průběžné stahování".
| Vrstva | Volba | Zdůvodnění |
|---|---|---|
| Frontend | React 18 + Vite + TypeScript | TS povinné — u výpočtů segmentů/statistik chrání před tichými chybami |
| Routing | React Router | standard |
| Data fetching / cache | TanStack Query | cache, retry, invalidace — bez psaní vlastní vrstvy |
| State | TanStack Query + React Context | globální store (Redux/Zustand) není pro tento rozsah potřeba |
| Styling | Tailwind CSS | viz poznámka níže |
| Mapy | MapLibre GL + OpenStreetMap | zdarma, bez API klíče; lepší výkon pro heatmapy než Leaflet |
| Grafy | Recharts | dostačující pro elevační profil, tempo, agregace |
| Backend sync | Python 3.12 + garth / python-garminconnect |
jediné udržované knihovny pro Garmin |
| DB | Supabase (PostgreSQL + PostGIS) | relační data, nativní RLS, PostGIS pro geometrii segmentů |
| Testy FE | Vitest + React Testing Library + Playwright | viz sekce 11 |
| Testy BE | pytest + pytest-mock | viz sekce 11 |
| Lint/format FE | ESLint + Prettier | |
| Lint/format BE | Ruff + mypy | |
| Pre-commit | pre-commit hooks | zabrání commitu neformátovaného kódu a secrets |
Poznámka k Tailwindu: V lednu 2026 Tailwind Labs propustila 75 % inženýrského týmu kvůli propadu tržeb z placených produktů (návštěvnost dokumentace klesla vlivem AI nástrojů). Framework samotný zůstává open-source pod MIT licencí a zdarma — zakladatel to explicitně potvrdil. Riziko pro projekt je pouze pomalejší vývoj nových funkcí, ne výpadek. Alternativa při obavách: CSS Modules / vanilla CSS.
/
├── .github/
│ └── workflows/
│ ├── ci.yml # lint + testy při každém PR
│ ├── deploy.yml # build + deploy frontendu na GH Pages
│ ├── sync.yml # cron: stažení dat z Garminu
│ └── backup.yml # cron: záloha DB (viz 8.3)
├── apps/
│ └── web/ # React + Vite frontend
│ ├── src/
│ │ ├── components/ # znovupoužitelné UI komponenty
│ │ ├── features/ # activities/, segments/, stats/, auth/
│ │ ├── lib/ # supabase klient, formátování, utils
│ │ ├── hooks/
│ │ ├── pages/
│ │ └── types/ # sdílené TS typy (generované ze Supabase schématu)
│ └── tests/
├── services/
│ └── sync/ # Python sync služba
│ ├── src/
│ │ ├── garmin/ # klient, retry, rate limiting
│ │ ├── crypto/ # šifrování/dešifrování credentials
│ │ ├── parsers/ # FIT/GPX → doménový model
│ │ ├── computations/ # statistiky, segmenty, rekordy
│ │ └── db/ # zápis do Supabase
│ └── tests/
│ └── fixtures/ # ukázková Garmin data (anonymizovaná)
├── supabase/
│ ├── migrations/ # verzované SQL migrace
│ └── seed.sql # testovací data pro lokální vývoj
├── docs/
│ ├── spec.md # tento dokument
│ ├── setup.md # self-hosting návod
│ ├── security.md # bezpečnostní model podrobně
│ └── adr/ # Architecture Decision Records
├── .env.example
├── .pre-commit-config.yaml
└── README.md
Proč monorepo: frontend a sync služba sdílí datový model; jedno repo znamená jednu pravdu o schématu a atomické změny napříč vrstvami.
- Registrace / přihlášení do appky (Supabase Auth, email+heslo, volitelně Google OAuth)
- Propojení s Garmin Connect: uživatel zadá své Garmin přihlašovací údaje, uloží se šifrovaně (sekce 6). Není to OAuth flow.
- Správa profilu, odpojení Garmin účtu (tvrdé smazání credentials)
- Export vlastních dat a smazání účtu (GDPR, viz sekce 9)
- Periodické stahování nových aktivit přes GitHub Actions cron
- Backfill historických aktivit (dávkově, s rate limitingem — viz 7.2)
- Typy aktivit: běh, kolo, plavání (rozšiřitelné)
- Ukládané metriky: GPS trasa, tepová frekvence, tempo, kadence, převýšení, čas
- Idempotence: opakovaný běh syncu nesmí vytvořit duplicity (dedup podle Garmin
activity_id)
- Metriky na aktivitu: vzdálenost, čas, tempo/rychlost, převýšení, průměrná/max TF
- Agregace: týden / měsíc / rok — vzdálenost, čas, počet aktivit, převýšení
- Grafy vývoje výkonnosti v čase
- Osobní rekordy: nejrychlejší 1 km, 5 km, 10 km, půlmaraton, maraton; nejdelší trasa; největší převýšení
- MVP: uživatel definuje segment ručně výběrem úseku na mapě z existující aktivity
- Detekce průjezdů segmentem napříč vlastními aktivitami (map-matching s tolerancí)
- Žebříček vlastních pokusů na segmentu (čas, datum, zlepšení)
- Mimo MVP: automatická detekce opakovaných úseků, cross-user leaderboard
- Mapa aktivity s trasou
- Heatmapa všech tras uživatele
- Detail aktivity: graf tempa a TF v čase, elevační profil
- Zobrazení segmentu na mapě s vyznačením pokusů
Nejcitlivější část systému. Implementuje se před tím, než se uloží první reálný záznam.
- Heslo se nikdy neukládá v plain textu
- Aplikační šifrování AES-256-GCM (nebo
pgsodiumv Supabase) před zápisem do DB - Šifrovací klíč není v DB ani v repu — pouze GitHub Secret, dostupný jen sync workflow
- Cíl: i při úniku databáze zůstanou hesla nečitelná
- RLS zapnuté na všech tabulkách s uživatelskými daty od prvního dne
- Politiky scoped na
auth.uid()— uživatel vidí výhradně vlastní data garmin_connections: frontend (anon key) nemá žádný přístup — ani read, ani write- Přístup ke credentials pouze přes service role key (jen v GitHub Secrets)
- Ověřeno testem (sekce 11.3), ne jen předpokladem
| Klíč | Kde smí být | Kde nesmí být |
|---|---|---|
| Supabase anon key | frontend build, veřejně | — (je veřejný by design, chrání ho RLS) |
| Supabase service role key | GitHub Secrets | frontend, repo, logy |
ENCRYPTION_KEY |
GitHub Secrets | frontend, repo, logy, DB |
- Žádné logování hesel, tokenů ani celých request/response těl obsahujících credentials
- Explicitní
::add-mask::pro secrets v GitHub Actions - Sync skript po použití hesla proměnnou okamžitě uvolní
pre-commithook s detekcí secrets (gitleaks) — brání náhodnému commitu klíče
- Formulář odesílá data přímo do Supabase přes Edge Function s omezenými právy — ne přes analytics, error tracking ani jiný mezikrok
- Explicitní souhlas s riziky (checkbox + srozumitelný text) při propojování
- Kdykoliv možné odpojení → tvrdé smazání credentials
Veřejné repo ≠ veřejný přístup. Kód je vidět, přístup řídí Supabase:
- Vypnout registraci: Supabase → Authentication → Settings → "Allow new users to sign up" = off
- Vlastní účet založit ručně přes dashboard nebo Admin API
- RLS zajistí, že i případně autentizovaný cizí účet nevidí nic
- Otevření veřejnosti později = zapnutí signups zpět + email verifikace + rate limiting; žádný refaktoring
- Rotace šifrovacího klíče (procedura popsaná v
docs/security.md) - Audit log přístupů ke
garmin_connections - 2FA na účet appky (Supabase Auth podporuje)
Toto je nejkřehčí část systému — závisí na neoficiálním přístupu ke třetí straně.
- Sync běží per-user v izolaci: selhání jednoho uživatele nesmí shodit celý běh
- Každý uživatel má vlastní try/except blok, chyba se zaloguje a pokračuje se dalším
- Umělá prodleva mezi requesty na Garmin (min. 1–2 s) — chrání před detekcí a blokací
- Exponenciální backoff při chybách (2 s → 4 s → 8 s, max 3 pokusy)
- Backfill historických dat rozdělený do dávek napříč více běhy, ne jeden masivní import
- Při HTTP 429 / detekci blokace: okamžité ukončení pro daného uživatele, označení stavu, žádné další pokusy v tomto běhu
Tabulka garmin_connections.status:
active— funguje normálněauth_failed— neplatné heslo (uživatel si změnil heslo) → notifikace v UI, sync přeskočenrate_limited— dočasné omezení, další pokus pozdějidisabled— uživatel odpojil nebo admin zakázal
Sync přeskakuje vše kromě active a rate_limited.
Tabulka sync_runs (viz datový model): každý běh zaznamená start, konec, počet zpracovaných uživatelů, počet nových aktivit, chyby. Umožňuje diagnostiku bez procházení GitHub Actions logů.
Riziko: Garmin změní přihlašovací flow → garth přestane fungovat.
- Sync workflow při opakovaném selhání (např. 3 běhy v řadě) vytvoří GitHub Issue automaticky
- V UI se uživateli zobrazí, kdy proběhla poslední úspěšná synchronizace
-- users: spravuje Supabase Auth (auth.users)
profiles (
id uuid PK REFERENCES auth.users,
display_name text,
created_at timestamptz
)
garmin_connections (
user_id uuid PK REFERENCES auth.users,
garmin_email text NOT NULL,
garmin_password_encrypted bytea NOT NULL,
status text NOT NULL, -- active | auth_failed | rate_limited | disabled
last_sync_at timestamptz,
last_error text,
created_at timestamptz
)
activities (
id uuid PK,
user_id uuid REFERENCES auth.users,
garmin_activity_id bigint NOT NULL, -- dedup klíč
type text NOT NULL, -- running | cycling | swimming | ...
started_at timestamptz NOT NULL,
duration_seconds int,
distance_meters numeric,
elevation_gain_meters numeric,
avg_heart_rate int,
max_heart_rate int,
avg_pace_seconds_per_km numeric,
track geography(LineString, 4326), -- PostGIS, zjednodušená trasa
stream_path text, -- odkaz do Supabase Storage na detailní stream
created_at timestamptz,
UNIQUE (user_id, garmin_activity_id)
)
segments (
id uuid PK,
user_id uuid REFERENCES auth.users,
name text NOT NULL,
geometry geography(LineString, 4326) NOT NULL,
distance_meters numeric,
created_at timestamptz
)
segment_efforts (
id uuid PK,
segment_id uuid REFERENCES segments,
activity_id uuid REFERENCES activities,
user_id uuid REFERENCES auth.users,
elapsed_seconds int NOT NULL,
started_at timestamptz,
UNIQUE (segment_id, activity_id)
)
personal_records (
id uuid PK,
user_id uuid REFERENCES auth.users,
category text NOT NULL, -- 1k | 5k | 10k | half_marathon | longest | ...
value numeric NOT NULL,
activity_id uuid REFERENCES activities,
achieved_at timestamptz,
UNIQUE (user_id, category)
)
sync_runs (
id uuid PK,
started_at timestamptz,
finished_at timestamptz,
users_processed int,
activities_imported int,
errors jsonb
)Indexy (nutné, ne volitelné):
activities (user_id, started_at DESC)— hlavní výpisactivities (user_id, type, started_at)— filtrované agregace- GiST index na
activities.trackasegments.geometry— prostorové dotazy pro segmenty
Free tier má 500 MB DB. Surová GPS data jednu aktivitu snadno vyženou na stovky kB.
Řešení:
- V DB pouze zjednodušená trasa (Douglas-Peucker, tolerance ~10 m) — pro mapu a segmenty stačí
- Detailní stream (TF, kadence, tempo po sekundách) → Supabase Storage jako komprimovaný JSON/Parquet, v DB jen odkaz (
stream_path) - Načítá se lazy až při otevření detailu aktivity
- Odhad: ~5–15 kB/aktivitu v DB → 500 MB vydrží desítky tisíc aktivit
Supabase free tier nemá žádné automatické zálohy. To je největší provozní riziko projektu.
Řešení: samostatný GitHub Actions workflow (backup.yml), 1×denně:
pg_dumpdatabáze- Šifrování dumpu
- Upload jako GitHub Actions artifact (retence 30 dní) nebo do externího úložiště
- Verzované SQL migrace v
supabase/migrations/, aplikované přes Supabase CLI - Každá migrace má komentář proč, ne jen co
- Migrace se nikdy needitují zpětně — jen se přidává nová
Projekt provozovaný z ČR, ukládá osobní údaje a přihlašovací údaje třetích osob → GDPR se vztahuje, jakmile appku otevřeš dalším lidem.
Pro uzavřený start (jen ty): minimální dopad, zpracováváš vlastní data.
Před otevřením dalším uživatelům je nutné:
- Zásady zpracování osobních údajů (privacy policy) — jaké údaje, proč, jak dlouho, kdo má přístup
- Export vlastních dat na vyžádání (funkce v UI)
- Smazání účtu včetně všech dat (tvrdé, ne soft-delete)
- Srozumitelné upozornění, že jde o neoficiální přístup k Garminu v rozporu s jeho ToS a jaké to nese riziko pro jejich Garmin účet
Poznámka k odpovědnosti: držení hesel cizích lidí k jejich Garmin účtům je výrazně vyšší závazek než osobní nástroj. Doporučené pořadí: nejdřív jen pro sebe → pak úzký okruh lidí, kteří rozumí riziku → veřejné otevření až s vyřešenou privacy policy a ověřenou bezpečností. Nespěchat na poslední krok.
- Responzivita: mobil i desktop (primárně mobil — kontrola po tréninku)
- Přístupnost: sémantické HTML, klávesová navigace, kontrast dle WCAG AA, popisky u grafů
- Výkon: první vykreslení < 2 s; seznam aktivit stránkovaný; mapy lazy-loaded
- Chybové stavy: každá async operace má loading, error i empty state; React Error Boundary na úrovni stránek
- Jazyk: UI česky, kód a dokumentace anglicky (standard pro open-source)
- Provoz v rámci free tierů se sledováním limitů
Testy jsou součástí Fáze 1, ne dodatek.
- Vitest + React Testing Library — komponenty, hooky, formátovací utility
- Playwright — e2e: přihlášení, výpis aktivit, detail aktivity, vytvoření segmentu
- pytest — parsování FIT/GPX, výpočty statistik, detekce segmentů, výpočet rekordů
- Garmin API se v testech nikdy nevolá naživo — mockované odpovědi z
tests/fixtures/(anonymizovaná data) - Testy retry logiky a backoffu (simulované 429/500)
- Test idempotence: dvojí běh syncu nad stejnými daty nevytvoří duplicity
- RLS blokuje přístup k datům jiného uživatele — dva testovací účty, křížový přístup musí selhat
garmin_connectionsje nečitelná přes anon key- Šifrování: uložená hodnota v DB není plain text a jde správně dešifrovat klíčem
- Statická kontrola, že v buildu frontendu není service role key ani
ENCRYPTION_KEY
ci.ymlspouští lint + typecheck + testy na každém PR- Merge do
mainpodmíněný zeleným CI (branch protection) - Pro MVP netlačit na % pokrytí — soustředit se na kritické cesty: auth, RLS, šifrování, parsování, výpočty
| Soubor | Obsah |
|---|---|
README.md |
přehled, architektura, quick start, disclaimer o Garmin ToS |
docs/spec.md |
tento dokument — zdroj pravdy o rozhodnutích |
docs/setup.md |
self-hosting návod (Supabase projekt, secrets, první deploy) |
docs/security.md |
bezpečnostní model, rotace klíčů, co dělat při incidentu |
docs/adr/ |
Architecture Decision Records — proč Supabase, proč neoficiální Garmin přístup, proč monorepo |
Dále: docstringy u všeho, co se dotýká šifrování nebo Garmin přihlášení; komentáře u SQL migrací vysvětlující proč.
Definice hotovo: Přihlásím se do appky, propojím Garmin účet, cron stáhne mé aktivity, vidím je v seznamu a na mapě. Bezpečnostní testy zelené.
- Monorepo scaffold, CI/CD, lint/format/pre-commit
- Supabase projekt, migrace se schématem a RLS politikami dřív, než se uloží první data
- Šifrovací vrstva pro credentials + testy
- Supabase Auth, uzavřená registrace (6.6)
- UI pro propojení Garmin účtu se souhlasem
- Sync workflow: přihlášení, stažení aktivit, dedup, rate limiting, per-user izolace,
sync_runs - Seznam a detail aktivity, mapa trasy
- Backup workflow
Definice hotovo: Vidím týdenní/měsíční/roční souhrny, grafy vývoje a osobní rekordy.
- Agregační dotazy + indexy
- Grafy (tempo, TF, elevace, vývoj v čase)
- Výpočet a zobrazení osobních rekordů
- Backfill historických dat po dávkách
Definice hotovo: Vytvořím segment na mapě, systém najde mé průjezdy napříč aktivitami a ukáže žebříček.
- Ruční definice segmentu na mapě
- Map-matching: detekce průjezdů s tolerancí (PostGIS)
- Žebříček pokusů, zobrazení na mapě
Definice hotovo: Appka je použitelná na mobilu, má heatmapu, kompletní dokumentaci a je připravená otevřít dalším lidem.
- Heatmapa všech tras
- Přístupnost, responzivita, výkonnostní optimalizace
- Kompletní dokumentace + ADR
- Privacy policy, export a smazání dat (sekce 9)
| Secret | Odkud |
|---|---|
SUPABASE_URL |
Supabase → Project Settings → API |
SUPABASE_SERVICE_ROLE_KEY |
tamtéž (tajné, nikdy do frontendu) |
SUPABASE_ANON_KEY |
tamtéž (veřejný, jde do frontend buildu) |
ENCRYPTION_KEY |
vygenerovat: openssl rand -hex 32 |
BACKUP_ENCRYPTION_KEY |
vygenerovat stejně, pro šifrování záloh |
Garmin credentials nejsou GitHub Secret — přicházejí od uživatelů přes UI a ukládají se šifrovaně do Supabase.
- Založit GitHub repo (public)
- Založit Supabase projekt, počkat na provisioning
- Zapnout PostGIS extension v Supabase
- Zkopírovat API klíče do GitHub Secrets (sekce 14)
- Spustit migrace (schéma + RLS politiky)
- Ověřit bezpečnostními testy, že RLS funguje — před uložením reálných dat
- Vypnout veřejnou registraci (6.6), založit si účet ručně
- Nasadit frontend přes GitHub Pages, ověřit načtení
- Propojit vlastní Garmin účet, ručně spustit sync workflow, ověřit data
- Ověřit, že
backup.ymlproběhne a vytvoří artifact - Zapnout branch protection na
main(vyžadovat zelené CI)
Tato sekce je určená k předání implementačnímu agentovi. Zkopíruj ji jako první instrukci.
Projekt se jmenuje Sunder. Použij tento název konzistentně: název repozitáře, package.json (name: "sunder"), Python balíček (sunder_sync), <title> v HTML, nadpis v README.
Implementuješ projekt podle docs/spec.md. Přečti si celou specifikaci před psaním kódu. Klíčové sekce: 6 (bezpečnost), 7 (odolnost syncu), 8 (data).
- Scaffold monorepa dle sekce 4, včetně lint/format/pre-commit a
ci.yml - SQL migrace: schéma ze sekce 8.1 včetně indexů a RLS politik
- Bezpečnostní testy ze sekce 11.3 — musí být zelené dřív, než se implementuje ukládání credentials
- Šifrovací modul (
services/sync/src/crypto/) + testy - Garmin klient s rate limitingem, backoffem a per-user izolací (sekce 7)
- Parsery a zápis do DB s dedup logikou
sync.ymlworkflow- Frontend: auth, propojení Garmin účtu, seznam a detail aktivity, mapa
deploy.ymlabackup.yml
- TypeScript strict mode, žádné
anybez zdůvodnění v komentáři - Python: type hints všude,
ruff+mypyčisté - Každý modul dostane testy současně s implementací, ne později
- Nikdy nelogovat hesla, tokeny ani celá těla requestů s credentials
- Commity: Conventional Commits (
feat:,fix:,test:,docs:) - Malé, revidovatelné commity po logických celcích — ne jeden obří commit na fázi
- Při každém rozhodnutí, které se odchyluje od specifikace, přidat ADR do
docs/adr/
- Hodnoty všech secrets ze sekce 14
- Potvrzení Supabase project URL
- Souhlas před
git pushdo vzdáleného repozitáře
Viz sekce 13. Před ohlášením hotova ověř: CI zelené, bezpečnostní testy zelené, sync workflow proběhl a naimportoval reálná data, frontend je dostupný na GitHub Pages.