Files
food-tracer/CLAUDE.md
T

9.4 KiB
Raw Blame History

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 in types/paths/<domain>/*.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.tsxAppRoutes.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:<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í (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:<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č ;, 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