9.4 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
Food Tracer je aplikace pro evidenci snědených jídel a útraty za jídlo. Czech-language UI.
Full-stack TypeScript monorepo. Technologicky i vzhledově navazuje na sesterský projekt
Luncher (../Luncher) — sdílí s ním stack, konvence i design systém.
Monorepo Structure
types/ → Shared OpenAPI-generated TypeScript types (source of truth: types/api.yml)
server/ → Express 5 backend (Node.js 22, ts-node)
client/ → React 19 frontend (Vite 7, React Bootstrap)
Package manager: Yarn Classic. Deployment: Dockerfile, compose.yml.
Development Commands
Initial setup
cd types && yarn install && yarn openapi-ts # Generate API types first
cd ../server && yarn install
cd ../client && yarn install
Running dev environment
./run_dev.sh # tmux (Linux/macOS)
.\run_dev.ps1 # dvě PowerShell okna (Windows)
# Nebo ručně:
cd server && NODE_ENV=development yarn startReload # Port 3001
cd client && yarn start # Port 3000, proxies /api → 3001
Building & tests
cd types && yarn openapi-ts # Regenerate types from api.yml
cd server && yarn build # tsc → server/dist
cd server && yarn test # Jest (in-memory storage)
cd server && yarn test meals # Run one file by name
cd client && yarn build # tsc --noEmit + vite build → client/dist
Architecture
API Types (types/)
- OpenAPI 3.0 spec in
types/api.yml— thin aggregator, endpoint specs intypes/paths/<domain>/*.yml, shared schemas intypes/schemas/_index.yml yarn openapi-tsgeneratestypes/gen/(client.gen.ts, sdk.gen.ts, types.gen.ts)- Both server and client import from these generated types
- When changing API contracts: update api.yml first, then regenerate
Server (server/src/)
- Entry:
index.ts— Express app, auth middleware, error middleware - Routes:
routes/—dayRoutes,mealRoutes,activityRoutes,workoutRoutes,statsRoutes,calorieRoutes,settingsRoutes,importRoutes - Domain:
meals.ts(jídlo),activities.ts(pohyb),workouts.ts(šablony tréninků),dayOverview.ts(spojení dne a bilance),statsService.ts(agregace),luncherImport.ts(parsování exportů z Luncheru),settings.ts,calories.ts - Auth:
auth.ts— JWT + volitelná autentizace z hlavičky reverzní proxy - Storage:
storage/index.tsfactory dle proměnnéSTORAGE; backendy:json.ts(soubor, vývoj),redis.ts(produkce),memory.ts(testy) - Config:
.env.development/.env.production(viz.env.template)
Client (client/src/)
- Entry:
index.tsx→AppRoutes.tsx;Login.tsxje přihlašovací obrazovka - Pages:
pages/—DayPage(přehled dne se záložkami Příjem/Výdej),StatsPage(statistiky + import) - Components:
components/(Header, CalorieLookup) acomponents/modals/(MealModal, ActivityModal, WorkoutModal, SettingsModal, ImportModal) - Context:
context/auth.tsx(JWT),context/settings.tsx(světlý/tmavý motiv) - Routing: konstanty adres jsou v
routes.ts, ne vAppRoutes.tsx— hlavička je potřebuje a kruhový import by je nechal nedefinované - Styling: Bootstrap 5 + React Bootstrap + SCSS; design systém a proměnné
--ft-*jsou vApp.scss, stránkové styly co-located vedle komponent - API: přes OpenAPI SDK z
types/gen/
Data model
- Jídlo (
MealEntry) patří jednomu dni a jednomu uživateli; klíč úložiště jemeals:<login>:<YYYY-MM-DD>a hodnotou je pole záznamů dne. - Ceny jsou všude celá čísla v haléřích — v úložišti, v API i mezi klientem
a serverem. Na koruny se převádí až při zobrazení (
formatPricevUtils.tsx). Stejnou konvenci má Luncher, díky tomu se částky z importu přenesou beze ztráty. - Kalorie jsou volitelné celé číslo v kcal. Jídla bez kalorií se do součtů nezapočítávají.
- Gramáž (
weight, g) a jednotkové hodnoty (pricePer100gv haléřích za 100 g,caloriesPer100gv kcal) umožňují dopočet — viz níže.
Gramáž a kalorie
deriveAmounts v server/src/meals.ts je jediný zdroj pravdy pro dopočty:
- gramáž =
price / pricePer100g * 100, pokud není zadaná ručně - kalorie =
weight / 100 * caloriesPer100g, jinak ručně zadaná hodnota
Ručně zadaná gramáž má vždy přednost — porci lze zvážit přesněji, než kolik řekne
cena. Sazba je na každém záznamu zvlášť, takže hlavní jídlo za 44 Kč/100 g a salát
s jiným cenováním můžou být ve stejném dni vedle sebe. Výchozí sazby podniků drží
server/src/settings.ts (sourceRates) a klient jimi předvyplňuje pole, když
uživatel vybere zdroj.
client/src/Utils.tsx má stejnojmennou funkci pro živý náhled ve formuláři.
Když se změní jedna, musí se změnit i druhá — jinak klient ukazuje něco jiného,
než server uloží.
Zdroje energetických hodnot
server/src/calorieProvider.ts definuje rozhraní CalorieProvider a jeho
implementaci nad Open Food Facts. Poskytovatel je záměrně vyměnitelný za jeden
soubor a nikdy nevyhazuje výjimku — při nedostupnosti vrací unavailable,
takže hledání jen přijde o návrhy a nespadne. CALORIE_PROVIDER=none externí
dotazy vypne (offline provoz, testy).
Poznámky ke zdrojům, ověřené v září 2026:
- KalorickéTabulky.cz nemají veřejné API. Jejich robots.txt zakazuje
/*query.page(jejich vyhledávání) a smluvní podmínky omezují užití nad rámec zamýšleného účelu, takže se odtud nesmí scrapovat. Aplikace jen nabídne odkaz na jejich tabulku potravin a zkopíruje název jídla do schránky. Předvyplnit jejich hledání z URL nejde — běží v JavaScriptu a parametry v query stringu ignoruje (ověřeno porovnáním odpovědí). - Open Food Facts: používá se
search.openfoodfacts.org, ne hlavníworld.openfoodfacts.org— tamní/cgi/search.pli/api/v2/searchvrací 503. Produkty bezenergy-kcal_100gse odfiltrují. - Vlastní knihovna (
server/src/calories.ts) si pamatuje kcal/100 g pod znormalizovaným názvem jídla a řadí se před externí návrhy — na kantýnová jídla sedí líp než databáze balených potravin. Plní se sama při uložení jídla.
Pohyb a energetická bilance
Den má dvě strany a DayPage je dělí do záložek:
- Příjem — jídla (
MealEntry), klíčmeals:<login>:<datum> - Výdej — pohyb (
ActivityEntry), klíčactivities:<login>:<datum>
Aktivita se měří v jednotce (ActivityUnit: KROKY, MINUTY, KM, OPAKOVANI) a
kalorie se dopočtou jako quantity / 100 * caloriesPer100Units. Sto jednotek,
ne jedna — u kroků by sazba na jeden krok byla zlomek (~0,04 kcal) a aplikace
všude pracuje s celými čísly. Stejná konvence jako u energie jídla na 100 g.
WorkoutTemplate (workouts.ts) je pojmenovaný seznam cviků. Použitím vzniknou
běžné aktivity s vazbou templateId — jsou samostatné, takže úprava založené
položky šablonu nemění.
buildEnergyBalance v dayOverview.ts počítá:
bilance = příjem − (klidový výdej + pohyb). Záporná hodnota je deficit.
Bez nastaveného klidového výdeje (basalCalories v nastavení) porovnává bilance
jen jídlo proti pohybu — to není skutečný deficit, proto se to přes
hasBasal: false propisuje do UI, aby to číslo nikoho nemátlo.
GET /api/day?date= vrací DayOverview se vším naráz (jídlo, pohyb, bilance),
takže DayPage si vystačí s jedním voláním.
Import z Luncheru
server/src/luncherImport.ts čte měsíční přehled ze stránky statistik Luncheru
ve všech třech formátech, které Luncher exportuje (XLSX, CSV, JSON — viz
../Luncher/server/src/userExport.ts).
- Sloupce se hledají podle názvu hlavičky, ne podle pozice.
- CSV má BOM, oddělovač
;, datumDD.MM.YYYYa desetinnou čárku (český Excel). - XLSX má dva listy — čte se
Přehled, listSouhrnse ignoruje. - Řádky se zakládají jako oběd, pokud se neurčí jinak (Luncher řeší obědy).
name= jídlo → poznámka → typ záznamu. U voleb "Budu objednávat" a "Rozhoduji se" bývá sloupec s jídlem prázdný a co se reálně jedlo stojí v poznámce ("Chefie - Těstovinový salát"), proto ten mezikrok. Poznámka, ze které se stal název, se už nekopíruje donote.source= obchod objednávky, jinak podnik. Stavy volby zNON_PLACE_TYPES("Budu objednávat", "Rozhoduji se", "Objednávka", "Mám vlastní/neobědvám") nejsou místa, takže se jako zdroj nepoužijí a záznam zůstane bez zdroje.- Duplicity řeší
importKeysložený z data, pořadí řádku v rámci dne, typu, jídla a částky — opakovaný import stejného měsíce nic nezduplikuje. - Endpoint podporuje
dryRunpro náhled před uložením; klient ho vždy použije.
Když se změní formát exportu v Luncheru, je potřeba upravit i tento parser
a testy v server/src/tests/luncherImport.test.ts, které si export sestavují
přesně tak, jak ho Luncher generuje.
Conventions
- Czech naming for domain variables and UI strings; English for infrastructure code
- TypeScript strict mode in both client and server
- Server module resolution: Node16; Client: ESNext/bundler
- Komentáře v češtině, u netriviálních míst vysvětlují proč, ne co