# 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 ```bash cd types && yarn install && yarn openapi-ts # Generate API types first cd ../server && yarn install cd ../client && yarn install ``` ### Running dev environment ```bash ./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 ```bash 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 in `types/paths//*.yml`, shared schemas in `types/schemas/_index.yml` - `yarn openapi-ts` generates `types/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.ts` factory 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.tsx` je 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) a `components/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 v `AppRoutes.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 v `App.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ě je `meals::` 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í (`formatPrice` v `Utils.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 (`pricePer100g` v haléřích za 100 g, `caloriesPer100g` v kcal) umožňují dopočet — viz níže. ## Gramáž a kalorie `deriveAmounts` v `server/src/meals.ts` je jediný zdroj pravdy pro dopočty: 1. **gramáž** = `price / pricePer100g * 100`, pokud není zadaná ručně 2. **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.pl` i `/api/v2/search` vrací 503. Produkty bez `energy-kcal_100g` se 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::` - **Výdej** — pohyb (`ActivityEntry`), klíč `activities::` 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č `;`, datum `DD.MM.YYYY` a desetinnou čárku (český Excel). - XLSX má dva listy — čte se `Přehled`, list `Souhrn` se 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 do `note`. - `source` = obchod objednávky, jinak podnik. Stavy volby z `NON_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ší `importKey` slož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 `dryRun` pro 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