# 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 a historie jídel), `mealGroups.ts` (seznam jídel — skupiny podle názvu a zdroje, doplnění a přejmenování), `mealPhotos.ts` (fotky jídel), `nutrients.ts` (živiny), `mealTemplates.ts` (recepty), `activities.ts` (pohyb), `workouts.ts` (šablony tréninků), `sleep.ts` (spánek), `weights.ts` (vážení po dnech), `profile.ts` (tělesné údaje a výpočet BMR), `bodyMetrics.ts` (BMI, podíl tuku, cíle), `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), `MealGroupsPage` (`/jidla` — seznam jídel podle názvu a zdroje s filtrem zdroje, hromadným doplněním údajů a přejmenováním), `WorkoutsPage` (`/treninky` — šablony tréninků), `ProfilePage` (Profil — ukazatele, vývoj hmotnosti, tělesné údaje, cíle a klidový výdej), `StatsPage` (statistiky se záložkami Příjem / Výdej / Útrata a import; panely `IntakeStatsPanel`, `ActivityStatsPanel`, `SpendingStatsPanel`) - **Components:** `components/` (Header, CalorieLookup, MealSuggestions, NutrientsFieldset) a `components/modals/` (MealModal, ActivityModal, ApplyWorkoutModal, ApplyMealModal — výběr z historie a receptů, SettingsModal, ImportModal) - **Klientské enumy** patří do `enums.ts` jako TypeScript `enum` s mapou popisků, ne jako inline union u komponenty (`DayTab`, `StatsTab`, `StatsRange`) - **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é. Přehled dne čte datum z parametru `?datum=YYYY-MM-DD` (`getDayUrl`), na konkrétní den se tak dá odkázat - **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. - Nutriční hodnoty (`nutrientsPer100g`: bílkoviny, sacharidy, z toho cukry, tuky, z toho nasycené, vláknina, sůl) jsou volitelné, v gramech na 100 g jako na etiketě. **Jako jediné nejsou celočíselné** — ukládají se na desetiny gramu, u soli a vlákniny by celé gramy smazaly celou hodnotu. Hodnoty porce se dopočtou z gramáže a násobku (`getPortionNutrients` v `nutrients.ts`), takže jídlo bez gramáže do součtu dne (`DayRecord.totalNutrients`) nevstupuje. ## 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ží. Totéž platí pro `applyMultiplier` (`server/src/utils.ts`), `deriveRecipe` (`server/src/mealTemplates.ts`) a `getPortionNutrients` (`server/src/nutrients.ts`). ## Násobek položky dne Jídlo i aktivita mají volitelný `multiplier` (i necelý, 0,01–100): dvě sušenky jsou ×2, nedojedená polovina ×0,5. Mění se přímo z přehledu dne (`/meals/multiplier`, `/activities/multiplier`), bez otevření formuláře. - **Uložené `calories` jsou vždy za jednu porci** a výsledek je `applyMultiplier(calories, multiplier)`. Kdyby se ukládal už vynásobený výsledek, opakovaná změna násobku by kumulovala zaokrouhlení. - Všechny součty (`buildDayRecord`, `buildActivityDay`, `statsService`) proto musí jít přes `applyMultiplier`, ne přes holé `calories`. - **Cena se nenásobí** — za nedojedené jídlo se platí celé. Dvě kusy se stejnou cenou jsou dva záznamy, nebo jedna cena zadaná za oba. - Násobek 1 je výchozí stav a neukládá se (stejně jako jedna série). ## 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, SEKUNDY, KM, OPAKOVANI) a kalorie se dopočtou jako `getTotalQuantity(quantity, sets) / 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. `quantity` je množství **na jednu sérii**, `sets` je počet sérií; celkové množství je jejich součin. Drží se odděleně schválně: 3 × 40 opakování s pauzami je jiný trénink než 120 v kuse, i když spálené kalorie vyjdou stejně. Jedna série je výchozí stav a neukládá se. Opačný směr k použití šablony je `createWorkoutTemplateFromActivities` — z aktivit odcvičeného dne (všech, nebo vybraných) vznikne šablona. Záznamy ve dni zůstanou beze změny. U jídla nic takového není, opakovaně jedená jídla se nabízejí z historie (viz Historie jídel a recepty). **Dodatečná zátěž** (`extraWeightGrams` u aktivity i cviku šablony, v gramech) je jen informace o tréninku, kalorie **nemění** — žádný jednoduchý vzorec spotřebu podle zátěže nedává, takže by šlo o vymyšlené číslo. Kdo chce, promítne ji do sazby na 100 jednotek. `WorkoutTemplate` (`workouts.ts`) je pojmenovaný seznam položek. Položka je buď cvik (`kind: CVIK`), nebo odkaz na jinou šablonu (`kind: SABLONA`), jejíž cviky se při použití rozbalí. Použitím vzniknou běžné aktivity s vazbou `templateId` — jsou samostatné, takže úprava založené položky šablonu nemění. **Zanoření je povolené jen na jednu úroveň** a `checkNesting` to hlídá z obou stran: šablona nesmí odkazovat na takovou, která sama něco skládá, a zároveň nesmí začít skládat, pokud ji už někdo používá. Bez druhé kontroly by druhá úroveň vznikla oklikou přes úpravu. Smazat nelze šablonu, kterou skládá jiná — zůstal by odkaz do prázdna. Šablony uložené dřív, než skládání přibylo, nemají `kind`; `withKind` jim ho při načtení doplní na `CVIK`. `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 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. Klidový výdej: ručně zadaný `basalCalories` má přednost před hodnotou spočítanou z tělesných údajů (`computeBasalCalories` v `profile.ts`, rovnice Mifflin–St Jeor). `getDayOverview` ho počítá s **hmotností platnou k danému dni** (poslední vážení v ten den nebo dřív), takže starší dny se se zhubnutím zpětně nepřepočítají; `settings.computedBasalCalories` je hodnota z posledního vážení. Výpočet **záměrně nenásobí koeficientem fyzické aktivity (PAL)**, jak to dělají kalkulačky TDEE — pohyb se eviduje zvlášť a přičítá se, takže vynásobení by ho započítalo dvakrát. **Vážení** (`weights.ts`, klíč `weights:` — mapa den → gramy) se zapisuje v přehledu dne nebo na stránce Profil. Hmotnost v profilu (`profile.weightGrams`) je pozůstatek z doby před vážením po dnech a použije se, jen dokud uživatel žádné vážení nemá; klient ji při uložení profilu posílá zpět beze změny. **Ukazatele** (`bodyMetrics.ts`, `UserSettings.bodyMetrics`) počítá server, nic z nich se neukládá: BMI s pásmem podle WHO, zdravé rozmezí hmotnosti (BMI 18,5–25), podíl tuku metodou US Navy z obvodů (muž pas a krk, žena navíc boky) a z cílů (`Goals`) cílová denní bilance a bílkoviny. Tempo počítá s **7 700 kcal na kilogram** — −0,5 kg týdně je −550 kcal denně. Cílová bilance (`EnergyBalance.targetBalance`) i cíl bílkovin (`DayOverview.proteinTargetGrams`) jsou jen informace pro uživatele, do bilance nevstupují. Popisky tempa ve výběru na stránce Profil počítají kcal/den se stejnou konstantou. **Spánek** (`sleep.ts`, klíč `sleep::`) se eviduje jako kontext dne a do bilance **nevstupuje**. Klidový výdej je hodnota za celých 24 hodin včetně spánku a žádná ze standardních rovnic délku spánku jako proměnnou nemá — přičítat nebo odečítat za něj kalorie by bylo vymýšlení čísla. Test to hlídá: bilance se po zadání spánku nesmí změnit. **Statistiky výdeje** (`getActivityStats` ve `statsService.ts`, `GET /api/stats/activity`) dělí pohyb na aktivity ze šablon a mimo ně a u šablon porovnává skutečnost s plánem. Plán si `applyWorkoutTemplate` ukládá při použití (`workoutPlans.ts`, klíč `workoutPlans::`) — **nesrovnává se se současnou podobou šablony**, ta se mohla mezitím změnit, a smazaná aktivita by beze stopy zmizela. Plán nese jednotlivé cviky s `entryId` aktivity, která z nich vznikla; podle něj `analyzeDay` u každého dne pozná cvik **odebraný** (aktivita chybí) a **upravený** (jiné množství, série nebo násobek) — podle názvu by to nešlo, šablona může mít stejný cvik dvakrát. Smazání všech cviků šablony ve dni `dropOrphanedPlans` bere jako vrácené použití a plán zahodí. Aktivity ze šablony bez plánu (cvik přesunutý z jiného dne) se počítají jako splněné přesně dle plánu. **Statistiky příjmu** (`getIntakeStats`, `GET /api/stats/intake`) jsou protějšek výdeje: příjem po chodech a zdrojích, živiny a porovnání s výdejem. Výdej a bilance se počítají **jen za dny, kdy má aspoň jedno jídlo kalorie** (`caloriesDayCount`) — den bez zápisu nebo s jídly z importu bez kalorií by jinak vypadal jako deficit celého výdeje. Ze stejného důvodu z nich vycházejí průměry. Klidový výdej je k hmotnosti platné v daný den (`getBasalOn` v `dayOverview.ts`). Peníze za jídlo jsou na samostatné záložce Útrata (`getFoodStats`, `/api/stats/summary`). `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. ## Historie jídel a recepty Hotová jídla (objednaná, koupená) **nemají šablony**. Při zakládání jídla se k rozepsanému názvu nabízí historie: `getMealHistory` v `meals.ts` (`GET /api/meals/history`) projde všechna jídla uživatele a každou dvojici **název a zdroj** vrátí jednou (`MealSuggestion`). Stejnojmenné jídlo z jiného zdroje (tikka masala z Everestu a z Namaste) je jiné jídlo a nikde se s ním neslučuje — ani v návrzích, ani v seznamu jídel, ani při `fillSameNameMeals`; na klientovi to hlídá `getMealKey` v `Utils.tsx`. Porovnává se znormalizovaně přes `normalizeName`, hodnoty jsou z **posledního** záznamu (cena i gramáž se časem mění) a řadí se od nejčastěji jedených. Návrh nese jen to, co popisuje jídlo samo — **chod, datum, násobek ani poznámku ne**, ty patří ke konkrétnímu dni. Gramáž dopočtená z ceny se vynechá, aby se v novém záznamu dopočetla z jeho ceny; ruční kalorie jen tam, kde nejdou dopočítat z gramáže. Jinak by se z dopočtené hodnoty stala ručně zadaná. Když poslední záznam kalorie nemá (typicky import), doplní se do návrhu z dřívějšího přes `createNutritionFiller`, stejně jako při importu. Návrhy se nabízejí i při **úpravě** jídla, ale až po změně názvu, a doplní jen prázdná pole (`mergeSuggestion` v `MealModal`) — slouží k přejmenování naimportovaného jídla, jehož cena a zdroj jsou skutečné. Po uložení jídla s kaloriemi `DayPage` nabídne (`FillSameNameOffer`) doplnit stejné hodnoty do ostatních jídel stejného názvu a zdroje, která je nemají (`fillSameNameMeals`, `POST /api/meals/fillSameName`); jídla s kaloriemi se nepřepisují. **Seznam jídel** (`MealGroupsPage`, položka Jídla v hlavičce) seskupí jídla podle znormalizovaného názvu **a zdroje** (`getMealGroups`, `GET /api/meals/groups`) a ukáže, kolika chybí kalorie, gramáž a živiny. Skupina nese i dny, kdy se jídlo jedlo (`MealGroup.days`, s příznakem neúplných údajů), a stránka je nabízí jako odkazy na přehled dne v nové záložce. `fillMealGroup` (`POST /api/meals/fillGroup`) doplní do skupiny **jen chybějící hodnoty**: gramáž ne tam, kde jde dopočítat z vlastní ceny a sazby, energii jen jídlům bez kalorií (jinak by dopočet přepsal ručně zadané kalorie), živiny po jednotlivých položkách. Předvyplněné hodnoty jsou z nejnovějšího jídla skupiny; energie a živiny bez nich i z jídla stejného názvu odjinud, sazba a gramáž ne. Druhou akcí skupiny je **přejmenování** (`renameMealGroup`, `POST /api/meals/renameGroup`) — změní název i zdroj všem jídlům skupiny ve všech dnech. Je oddělené od doplnění schválně: doplnění jen připisuje, co chybí, přejmenování přepisuje. Když nový název a zdroj patří jiné skupině, jídla se do ní sloučí (formulář na to předem upozorní). Recept a fotky jdou s jídlem; dvě jídla s recepty sloučit nejde. **Recept a fotky jsou rozšířením jídla v seznamu**, ne samostatnou evidencí — patří dvojici název a zdroj, ne dni. V přehledu dne se nezobrazují. - Recept se k jídlu váže **shodou názvu a zdroje** (`getRecipeKey`), jedno jídlo má nejvýš jeden (`saveMealTemplate` druhý odmítne). Recept jídla, které se ještě nejedlo, je v seznamu jako skupina s `count: 0` a bez `lastDate`. Má-li jídlo recept, doplnění údajů předvyplní gramáž porce a energii z něj. - Fotky (`mealPhotos.ts`): klient je před nahráním zmenší (`imageResize.ts`, ~1600 px JPEG) a pošle i náhled (240 px). Rejstřík s náhledy je jeden pro uživatele (`mealPhotos:`, klíč jídla → fotky), aby ho seznam jídel načetl jedním čtením; plné obrázky leží každý zvlášť (`mealPhoto::`) a načtou se, až když se fotky jídla otevřou. Pole Zdroj (`SourceInput`) nabízí zdroje z historie (`getMealSources`, `GET /api/meals/sources`, nejčastější první) a podniky se sazbou z nastavení; psát jde i nový. `MealTemplate` (`mealTemplates.ts`, klíč `mealTemplates:`) je **recept** — vařené jídlo se surovinami (`ingredients`), počtem porcí (`servings`), hmotností hotového jídla (`cookedWeight`) a postupem v `note`. **Chod nemá** — patří ke dni, ne k jídlu; použije se zvolený při použití, jinak oběd (starší recepty ho v úložišti mít můžou, `toMealTemplate` ho zahodí). `deriveRecipe` z nich při uložení spočítá hodnoty **jedné porce** a uloží je do plochých polí (`weight`, `caloriesPer100g`, `calories`, `price`); použitím vznikne běžný `MealEntry` přes `addMeal`. Energie na 100 g se bere z hmotnosti hotového jídla, ne ze součtu surovin — vařením se voda odpaří nebo nasákne. Postup se do záznamu dne nekopíruje. Recepty se nabízejí v našeptávání i ve výběru z historie (`ApplyMealModal`) a jídlo stejného jména z historie se vedle receptu nenabízí podruhé. Dřív pod stejným klíčem ležely i šablony hotových jídel (`kind: HOTOVE`, nebo bez `kind`). Ty zanikly: `loadRecipes` je při čtení vynechá (poznají se podle toho, že nemají suroviny) a první zápis receptu je z úložiště odklidí. **Jídla z nich založená ve dnech zůstávají**, dál nesou `templateId` zaniklé šablony. Jídlo založené z receptu nese `templateId`. Nastavuje ho jen server (`addMeal` s třetím parametrem) — `MealInput` ho nemá, takže ho klient nepodstrčí a úprava jídla ho zachová. Kontrakt má vlastní `MealTemplateInput`, kde je `id` volitelné a hodnoty porce chybí — dopočítá je server. ## 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. - Luncher kalorie nezná, takže je `createNutritionFiller` (`meals.ts`) doplní z historie: vzorem je nejnovější jídlo **stejného názvu**, které energii nebo živiny má, přednostně ze stejného zdroje. Cena zůstává z importu. Sazba za 100 g se přebírá jen ze stejného zdroje (gramáž se pak dopočte z nové ceny), jinak se vezme gramáž vzoru. - 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. - Režim (`ImportMode`): **PRIDAT** (výchozí) přeskočí už naimportované řádky; **NAHRADIT** v dnech, do kterých se něco zakládá, odebere dřívější záznamy z importu a založí je znovu. Ručně zadaná jídla zůstávají v obou režimech. - Endpoint podporuje `dryRun` pro náhled před uložením; klient ho vždy použije. Náhled vrací všechny řádky, které založit jde, i jídla, která už v dotčených dnech jsou (`existing`). Uživatel v něm řádky odškrtá a ostrý import dostane jen `selectedKeys`. Jídlo, které Luncher zapsal jako jedno ("Chicken tikka massala+garlic nun"), jde v přehledu dne rozdělit (`MealSplitModal`, `splitMeal`, `POST /api/meals/split`) — import se kvůli tomu nekomplikuje. Původní záznam si nechá `importKey`, takže ho režim PRIDAT znovu nezaloží; odštěpená část dostane jen `importSource`, aby ji režim NAHRADIT odebral spolu s původním a nevzniklo jídlo dvakrát. Cena se mezi části nedopočítává, rozdělí se jen na vyžádání (napůl). **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