Mobilna aplikacja dla biegaczy rekreacyjnych przygotowujących się do dystansów od pierwszego kilometra do maratonu. Produkt łączy plan treningowy, wykonane aktywności ze Stravy oraz dynamiczne zalecenia dotyczące posiłków, nawodnienia i regeneracji.
Status: dokumentacja startowa do budowy PoC/MVP z użyciem GitHub Copilot w Visual Studio Code.
- jedna aplikacja mobilna na iOS i Android,
- React Native + TypeScript + Expo Development Build,
- backend jako modularny monolit w .NET 10,
- PostgreSQL,
- wdrożenie przez Docker Compose na Mikrusie 3.5 lub 4.1,
- integracja ze Stravą jako pierwsze źródło treningów,
- jeden tester w fazie PoC,
- mały koszt utrzymania i łatwa migracja na większą infrastrukturę,
- AI wspiera użytkownika, ale nie podejmuje samodzielnie decyzji medycznych.
- Przeczytaj wizję produktu.
- Przeczytaj zakres MVP.
- Zapoznaj się z architekturą i wdrożeniem na Mikrusie.
- Uruchom w Copilot Chat prompt
bootstrap-repositoryz katalogu.github/prompts. - Realizuj zadania kolejno z katalogu
docs/tasks.
running-fuel/
├── apps/
│ └── mobile/ # React Native / Expo
├── services/
│ └── api/ # .NET 10 modular monolith
├── tests/
│ ├── api.integration/
│ └── architecture/
├── infra/
│ ├── docker/
│ ├── scripts/
│ └── mikrus/
├── docs/
├── .github/
│ ├── workflows/
│ ├── instructions/
│ └── prompts/
├── AGENTS.md
└── README.md
- Task 001 (bootstrap) — zrealizowany: szkielet
apps/mobile(Expo Router, design system, ekran „Dzisiaj” na danych mockowych) iservices/api(health checks, brak modułów biznesowych), Docker Compose, testy. - Task 002 (konto, profil, onboarding) — etap 1 — zrealizowany w zakresie opisanym w ADR-011–014: deweloperska sesja (
/api/v1/dev/session, EF Core + Postgres, SecureStore po stronie mobile), chronione trasy ((auth)/(protected)), pełny onboarding (dane podstawowe, doświadczenie biegowe, cel, żywienie, bezpieczeństwo, podsumowanie) z lokalnym zapisem postępu, ekran profilu biegacza, przygotowanie architektury połączenia ze Stravą (7 stanów UI, endpointy statusu/connect/sync z jawnym trybem wyłączonym). Pełne ASP.NET Core Identity (rejestracja e-mail/hasło) i realne OAuth Stravy pozostają do zrobienia — patrzdocs/tasks/002-auth-profile-onboarding.mdidocs/tasks/004-strava-integration.md. - Task 003 (kalendarz treningowy) — zrealizowany w zakresie opisanym w ADR-017: deterministyczny, idempotentny seed 12-tygodniowego planu przygotowań do półmaratonu (EF Core + Postgres), ustrukturyzowane kroki treningu (
WorkoutStep, nie wolny tekst), endpointyplan/weeks/{weekStart}/workouts/{id}zcomplete/skip/restore/PATCH, ekran Trening (pasek dni, zakładki Plan/Wykonane, karta najbliższego treningu z wizualizacją interwałów SVG, podsumowanie tygodnia), ekran szczegółów treningu z ręcznym ukończeniem/pominięciem/przywróceniem/edycją, oraz ekran Dzisiaj czytający to samo źródło danych treningowych co ekran Trening. Tworzenie nowego treningu przez użytkownika, dedykowana akcja „Anuluj” i dopasowanie do aktywności Stravy pozostają do zrobienia — patrzdocs/tasks/003-training-calendar.mdidocs/tasks/004-strava-integration.md. - Task 004 (integracja ze Stravą) — zrealizowany w zakresie opisanym w ADR-018: serwerowy OAuth 2.0 (backend jako jedyny posiadacz tokenów, szyfrowanie przez ASP.NET Core Data Protection z kluczami na trwałym wolumenie), import ostatnich 30 dni aktywności i synchronizacja przyrostowa/webhookowa przez generyczną tabelę zadań
BackgroundJobw PostgreSQL (FOR UPDATE SKIP LOCKED, bez Redis/RabbitMQ), deterministyczny silnik dopasowania aktywność↔trening (ActivityWorkoutMatcher) z automatycznym ukończeniem powyżej progu pewności i sugestią poniżej, ochrona ręcznie ukończonych treningów przed nadpisaniem, ekran integracji Strava i szczegółu zaimportowanej aktywności z potwierdzeniem/zmianą dopasowania, plakietki źródła ukończenia na ekranach Dzisiaj/Trening/szczegół treningu, odłączenie z zachowaniem historii Runth. Prawdziwe uwierzytelnienie OAuth wobec API Stravy nie zostało zweryfikowane w tym środowisku (brak realnych poświadczeń) — testy backendu używająFakeStravaApiClient; patrzdocs/tasks/004-strava-integration.mdpo kroki ręcznej weryfikacji. Integracja z Garminem pozostaje do zrobienia. - Task 005 (Running Nutrition Engine) — zrealizowany w zakresie opisanym w ADR-019: deterministyczny silnik żywieniowy (klasyfikacja dnia, cel energetyczny wg formuły Mifflin-St Jeor + bonus treningowy zależny wyłącznie od typu dnia i czasu trwania — nigdy z kalorii Stravy, makroskładniki periodyzowane, cel nawodnienia, timing posiłków względem godziny treningu), wersjonowana konfiguracja reguł (
nutrition-engine-v1), 28 oryginalnych przepisów bez scrapingu z deterministycznym filtrowaniem dietetycznym (twarde wykluczenia alergenów/nietolerancji/nielubianych produktów) i doborem, wersjonowany append-only plan dnia z leniwą idempotentną regeneracją odporną na współbieżność (porównanie hashy wejść treningowych/profilowych), zamiana posiłku i zmiana porcji, ekran Żywienie i szczegół posiłku, integracja z ekranem Dzisiaj i szczegółem treningu (żywienie okołotreningowe). Pełny dziennik jedzenia/logowanie wody i lista zakupów pozostają do zrobienia — patrzdocs/tasks/005-nutrition-engine.md. - Task 006 (Dziennik i lista zakupów) — zrealizowany w zakresie opisanym w ADR-020: dziennik (własny wpis posiłku ze statusem szacunkowym, nawodnienie, żywienie na biegu, samopoczucie), lista zakupów z agregacją jednostek i kategoryzacją sklepową, przeglądalna/filtrowalna biblioteka przepisów z ulubionymi (zamyka FR-RCP-003). Mobile działa lokalnie-najpierw (SQLite + jeden generyczny outbox) — Dziennik i Lista zakupów są w pełni użyteczne offline; konflikty synchronizacji (współbieżna edycja) są zawsze rozwiązywane jawnym wyborem użytkownika, nigdy cichym nadpisaniem. Arkusz Szybkie Dodaj i check-in samopoczucia na ekranie Dzisiaj są teraz realnie podłączone (wcześniej wizualne stuby). Backend 204/204 i mobile 254/254 testów — patrz
docs/tasks/006-journal-shopping.md. - Task 007 (Postępy i raport tygodniowy) — zrealizowany w zakresie opisanym w ADR-021: kilometraż tygodniowy, realizacja planu, RPE, energia, realizacja posiłków okołotreningowych, trend masy (z możliwością ukrycia) i deterministyczny raport tygodniowy — wszystko bez AI. Cała agregacja jest czystą, testowaną funkcją mobile nad już istniejącymi endpointami Treningu/Żywienia/Dziennika (rozszerzenie jawnej zasady „weekly math lives on mobile” z Task 003) — jedyny nowy zasób backendu to pomiar masy, lokalny-najpierw jak Dziennik. Każda uśredniona/porównawcza wartość wymaga co najmniej dwóch punktów danych, inaczej pokazuje jawne „za mało danych” zamiast twierdzenia z jednego pomiaru; żadna obserwacja nigdy nie łączy dwóch różnych metryk w jedno zdanie. Backend 214/214 i mobile 280/280 testów — patrz
docs/tasks/007-progress-weekly-report.md. - Task 008 (Bezpieczne funkcje AI) — zrealizowany w zakresie opisanym w ADR-022:
IAiProviderz domyślnymDisabledAiProvider(brak klucza nigdy nie przerywa startu ani nie blokuje głównych funkcji, ten sam wzorzec co Strava) iAnthropicAiProvider(Anthropic Claude przez oficjalny SDK, structured outputs, jedna próba naprawy przy błędzie transportu), cztery endpointy (explain-recommendation,summarize-week,search-recipes,parse-meal-description) zawsze zwracające bezpieczny fallback zamiast błędu, heurystyczny walidator bezpieczeństwa jako druga warstwa obrony nad strukturalnym ograniczeniem promptu (AI nigdy nie dostaje surowych liczb — tylko zamknięte kody powodów/obserwacji, więc nie ma z czego wymyślić fałszywy związek przyczynowy), dzienny limit kosztu i cache (wyjaśnienia globalnie po kodach, raport tygodniowy raz na dobę). Wpięte w Dzisiaj (wyjaśnienie rekomendacji), Postępy (osobna karta AI pod deterministycznymi obserwacjami, nigdy zmieszana), Przepisy (wyszukiwanie opisowe) i Dziennik (propozycja wartości posiłku z opisu, zawsze do zatwierdzenia). Testy nigdy nie wywołują prawdziwego API —FakeAiProvider. Backend 227/227 i mobile 289/289 testów — patrzdocs/tasks/008-ai-assistant.md. - Task 009 (wdrożenie na Mikrus) — celowo pominięty na tym etapie kolejki zadań; zostanie zrealizowany jako ostatni, po zamknięciu pozostałych zadań funkcjonalnych — patrz
docs/tasks/_009-mikrus-deployment.md. - Task 010 (cele, priorytety i ścieżki aktywności) — zrealizowany w zakresie opisanym w ADR-023: onboarding rozpoczyna się wyborem ścieżki aktywności (spacer, nordic walking, marszobieg, codzienny ruch, bieganie — nigdy trwale zamknięty wybór) zamiast zakładać biegacza, po czym jeden adaptacyjny ekran oceny możliwości pyta tylko o pola adekwatne do wybranej ścieżki (zod
.superRefine()zamiast rozgałęzień schematu, żeby zachować jeden stabilny typ formularza) i ekran celów oferuje katalog celu głównego przefiltrowany per ścieżka plus do trzech celów dodatkowych ze wspólnego katalogu — bez suwaka 0–100, z rangą (główny/ważny/dodatkowy) i deterministycznie wyliczonym miernikiem sukcesu. Przesiew bezpieczeństwa ma teraz cztery wskazówki zamiast dwóch (standardGuidance/conservativeStart/professionalConsultationSuggested/automatedProgressionPaused), wyliczane deterministyczną regułą pierwszeństwa. Profil biegowy sprzed Task 010 (lokalny i w bazie) jest bezstratnie migrowany doActivityPath.runningprzy pierwszym odczycie; pola specyficzne dla biegania są zachowane bez zmiany znaczenia dla kompatybilności wstecznej API. Adaptacja generowania planu do nowych ścieżek pozostaje poza zakresem (Task 011). Backend 229/229 i mobile 322/322 testów, w tym dedykowane testy każdej z pięciu ścieżek aktywności — patrzdocs/tasks/010-activity-goals-onboarding-v2.md. - Task 011 (adaptacyjny silnik priorytetów treningowych i żywieniowych) — zrealizowany w zakresie opisanym w ADR-024: drugi, niezależnie wersjonowany deterministyczny silnik (
PriorityEngine,priority-engine-v1) obokNutritionEngine, oba karmiące jeden wierszDailyPlan— jedyne wspólne źródło priorytetów dla Dzisiaj/Treningu/Żywienia/Postępów. Czternastowartościowa klasyfikacja dnia obejmuje wszystkie pięć ścieżek aktywności (dla biegania redukuje się 1:1 do nietkniętej dotychczasowej klasyfikacji — zero regresji), deterministyczna ocena gotowości (prosty system punktowy, brak danych nigdy nie obniża wyniku) i kaskada propozycji adaptacji z jawną kolejnością (bezpieczeństwo → przeciążenie → gotowość → frekwencja → brama gotowości początkującego → progresja), wymagająca zatwierdzenia użytkownika poza jednym jawnym wyjątkiem bezpieczeństwa. Dziewięć szablonów planu treningowego (był jeden) dobieranych wg ścieżki i poziomu wejściowego, archiwizujących — nie kasujących — niepasujący plan sprzed onboardingu. Postępy zyskały przegląd priorytetów z listą propozycji adaptacji i przyciskami zatwierdź/odrzuć. Podczas implementacji znaleziono i naprawiono rzeczywisty błąd: wielość planów na użytkownika (aktywny + zarchiwizowane) ujawniła brakujące filtrowanie po aktywnym planie w trzech miejscach odpytujących treningi po dacie — zabezpieczone testem regresyjnym. Backend 274/274 i mobile 330/330 testów — patrzdocs/tasks/011-adaptive-training-nutrition-priority-engine.md. - Task 012 (katalog produktów spożywczych) — zrealizowany w zakresie opisanym w ADR-025: katalog produktów podobny do Fitatu (wyszukiwanie po nazwie/marce/kodzie kreskowym, wybór porcji, prywatne produkty, jawny priorytet i jakość źródeł) osadzony jako pięć encji EF (
FoodProduct/FoodBrand/FoodServing/FoodProductRevision/FoodProductFavorite), nie dziesięć nazwanych modeli z dokumentu zadania — reszta to pola/enumy naFoodProduct, tym samym wzorcem coRecipe(Task 005). Jedyny zewnętrzny dostawca to Open Food Facts, wyłącznie dla odczytu po kodzie kreskowym (nigdy wyszukiwania tekstowego), z jawnym priorytetem źródeł przy zapisie — zatwierdzone dane Runth/społeczności nigdy nie są cicho nadpisywane. Moderacja kandydatów to deterministyczne reguły akceptacji (FoodProductValidator), nie kolejka z człowiekiem — dokument zadania to jawnie dopuszcza. Dodawanie produktu do dziennika rozszerza istniejący endpoint i mechanizm offline/outbox (Task 006) o trzy opcjonalne pola zamiast budować drugi mechanizm; serwer zawsze przelicza makra z profilu produktu, nigdy nie ufa wartościom klienta. Backend 309/309 i mobile 340/340 testów — patrzdocs/tasks/012-food-product-catalog.md. - Task 013 (skanowanie kodu kreskowego i etykiety) — zrealizowany w zakresie opisanym w ADR-026:
ILabelRecognitionProviderz dokumentu zadania to istniejącyIAiProvider(Task 008) rozszerzony o opcjonalne zdjęcia, nie osobny port — rozpoznawanie etykiety (nazwa, marka, ilość netto, wartości odżywcze na 100 g/ml, składniki, sugerowane alergeny) zwraca structured output z Claude z jawną kategorią pewności (high/medium/low/unknown) na każde pole, nigdy procentem. Skan kodu kreskowego ponownie wykorzystuje endpoint odczytu po kodzie z Task 012 bez nowego endpointu; skan etykiety prowadzi na to samo, w pełni edytowalne rozszerzenie formularza własnego produktu z Task 012 — z podpowiedzią pewności przy polu, banerem ostrzeżeń niespójności i osobnymi grupami „zawiera”/„może zawierać” dla alergenów, nigdy automatycznym zapisem na podstawie samego OCR. Zdjęcia są przetwarzane wyłącznie w pamięci na czas jednego żądania i nigdy nie trafiają na dysk backendu; aparat i galeria wywoływane są z wyłączonym odczytem EXIF, więc metadane lokalizacyjne nigdy nie powstają. Szkic skanu etykiety żyje wyłącznie w lokalnym magazynie urządzenia (Task 006), celowo poza outboxem — analiza to ponawialne żądanie odczytu, nie kolejkowana mutacja. Backend 319/319 i mobile 355/355 testów — patrzdocs/tasks/013-barcode-label-ocr.md. Kamera i przepływ zdjęć nie zostały fizycznie zweryfikowane na urządzeniu w tym środowisku (brak dostępu do kamery/Expo Development Build) — sprawdzone statycznie (kompilacja,tsc, testy z zamockowanymexpo-camera/expo-image-picker). - Task 014 (analiza zdjęcia posiłku i wspomagane logowanie AI) — zrealizowany w zakresie opisanym w ADR-027:
IMealVisionProviderz dokumentu zadania to ten sam rozszerzonyIAiProviderco Task 013, nie osobna integracja — zdjęcie posiłku (do trzech ujęć) zwraca listę komponentów, każdy z jedną kategorią pewności (nie per pole, jak w Task 013 — dokument zadania wprost tego wymaga), szacowanym zakresem porcji i sugerowanymi niewidocznymi dodatkami (olej, sos...), plus do trzech pytań doprecyzowujących. Dopasowanie komponentu do katalogu dzieje się wyłącznie po stronie mobile przez istniejące wyszukiwanie (Task 012) — endpoint analizy nigdy nie zwraca kandydatów produktu, a energia i makra dla dopasowanego komponentu są zawsze liczone dokładnie tą samą, już przetestowaną ścieżką co ręczne dodanie z katalogu; AI nigdy nie nadpisuje wartości odżywczych. Zapis tworzy jedenFoodJournalEntryna potwierdzony komponent, wszystkie połączone nowymMealPhotoAnalysisId(pełni podwójną rolę znacznika źródła i klucza grupującego — bez nowego polaSource). Zdjęcia nigdy nie trafiają na dysk backendu — jedyna nowa tabela (MealPhotoAnalysis) przechowuje tylko wynik, z hashem obrazu jako kluczem krótkotrwałego cache (identyczne zdjęcie tego samego użytkownika nie jest ponownie wysyłane do AI). Aparat i galeria wywoływane są z wyłączonym odczytem EXIF. Backend 333/333 i mobile 366/366 testów — patrzdocs/tasks/014-ai-meal-photo-analysis.md. Kamera i przepływ zdjęć nie zostały fizycznie zweryfikowane na urządzeniu w tym środowisku (to samo ograniczenie co Task 013) — sprawdzone statycznie (kompilacja,tsc, testy z zamockowanymexpo-camera/expo-image-picker). - Task 015 (biblioteka posiłków i przepisów 2.0) — zrealizowany w zakresie opisanym w ADR-028: sześć typów zawartości ze szkicu (
Recipe,MealTemplate,UserRecipe,MealBundle,QuickMeal,PreparedProductMeal) mapuje się na dwa byty — istniejącyRecipe(systemowy, bez zmian) i nowyUserRecipe(własny), rozróżniane polemKind, ze składnikami jako prawdziwe odwołania do katalogu (FoodProductId/FoodServingId/Quantity), nie wolny tekst jak wRecipe— dzięki temu makra i alergeny liczą się deterministycznie zamiast być ręcznie oznaczane. Wersjonowanie przezRecipeGroupId/Version/Status(Active/Superseded/Deleted, wzorowane naDailyPlan) — edycja nigdy nie mutuje wiersza w miejscu, tylko wstawia nową wersję i przełącza poprzednią naSuperseded; nowyFoodJournalEntry.LoggedFromUserRecipeIdwskazuje konkretną, niemutowalną wersję, więc historyczny wpis nigdy nie zmienia się po edycji przepisu — bez żadnej nowej logiki migawki, bo zapis do dziennika i tak rozkłada przepis na pojedyncze wpisy per składnik (ten sam mechanizm co Task 012/014). Przy okazji znaleziona i zamknięta luka:GET /recipes(zwykłe przeglądanie) nigdy wcześniej nie miało parametrów wykluczenia alergenów/nietolerancji/diety — tylko ścieżka AI (POST /ai/search-recipes) je stosowała; teraz oba istnieją naGET /recipesbezpośrednio i wynik łączy pulę systemową z własnymi przepisami wywołującego w jednej stronie. AI ogranicza się do sugerowania tekstu instrukcji dla już wybranych przez użytkownika składników (POST /ai/suggest-recipe-instructions) — nigdy nie widzi ani nie wybiera składników, więc strukturalnie (nie przez sprawdzenie w locie) nie może ominąć twardych wykluczeń. Spiżarnia (PantryItem) to wyłącznie miękki sygnał rankingowy (preferPantry=true), nigdy filtr. Świadomie poza zakresem: meal prep, publiczne przepisy/moderacja, zdjęcie przepisu (brak blob storage w stosie), szersze generatywne AI, filtr budżetu (brak danych cenowych) — żadne z nich nie jest wymagane przez kryteria akceptacji. Backend 348/348 i mobile 383/383 testów — patrzdocs/tasks/015-meal-recipe-library-v2.md. - Task 016 (żywienie podczas ruchu i tolerancja układu pokarmowego) — zrealizowany w zakresie opisanym w ADR-029: nowy, samodzielny
FuelingNeedClassifier(celowo osobny od już przetestowanegoNutritionEngine) klasyfikuje potrzebę fuelingu naNotNeeded/HydrationOnly/CarbohydrateGuidance/FullFuelingPlan— spacery i nordic walking nigdy nie przekraczająHydrationOnly(żadnej automatycznej sugestii żeli), zawody zawsze dostają pełny plan, krótka sesja dostaje jawny wynik zamiast pustego ekranu.FuelingPlangeneruje się leniwie per trening i preferuje produkty już przetestowane bez zgłoszonych objawów z historii tolerancji; regenerowanie zastępuje pozycje planu, ale nigdy nie dotyka checklisty startowej zawodów (pięć stałych etapów, odznaczony postęp przeżywa regenerację).RunFuelEntryrozszerzony dokładnie wzorcemFoodJournalEntry(Task 012) — opcjonalne odniesienie do katalogu, węglowodany/płyn/sód liczone po stronie serwera jako migawka,SetNullprzy usunięciu produktu. Historia tolerancji i przegląd sesji po aktywności konsekwentnie mówią wyłącznie o współwystępowaniu objawów, nigdy o przyczynowości ("sesje z objawami, w których produkt był użyty", nie "produkt powodujący X") — mała próba jest jawnie oznaczana, nigdy ukrywana; silne lub powtarzające się objawy pokazują neutralną rekomendację konsultacji, nigdy diagnozę. Rejestr rzeczywistego spożycia i przegląd sesji działają offline przez istniejący outbox (Task 006, ten sam mechanizm cowellbeing); generowanie planu i historia tolerancji wymagają połączenia, ten sam podział offline/online coUserRecipe/ShoppingList(Task 015). Przy okazji naprawiony rzeczywisty gap: przycisk "szczegóły sesji" na ekranie Dzisiaj był no-opem — teraz nawiguje do szczegółów treningu, gdzie widoczny jest link do planu żywienia tylko wtedy, gdy trening go wymaga. Backend 372/372 i mobile 398/398 testów — patrzdocs/tasks/016-run-fuel-gi-tolerance.md. - Task 017 (kontekst pogody, jakości powietrza i warunków treningowych) — zrealizowany w zakresie opisanym w ADR-030:
IEnvironmentProviderto jeden interfejs (nie osobne dla pogody i jakości powietrza), dokładnie wzorcemIAiProvider/IFoodProductProvider—DisabledEnvironmentProviderdomyślny, gdyWEATHER_ENABLED=false, iOpenMeteoEnvironmentProvider(Open-Meteo, bez klucza API, jeden ręczny retry) jako jedyny zewnętrzny dostawca. DeterministycznyEnvironmentClassifier(czysta funkcja, osobna odNutritionEngine/PriorityEngine/FuelingNeedClassifier) klasyfikuje warunki jakonormal/attention/considerAdjustment/avoidAutomatedProgression/providerUnavailablewyłącznie z listy kodów powodów, nigdy z jednego arbitralnego wyniku — nawodnienie dostaje tylko flagę przypomnienia, ubiór tylko neutralne kategorie, żadne z nich nie zmienia automatycznie liczbowego celu kalorii/płynów w już przetestowanymNutritionEngine. Lokalizacja rozwiązywana w dwóch warstwach po stronie serwera (jawne współrzędne żądania — obejmujące zarówno ręczny wybór, jak i lokalizację urządzenia po zgodzie przezexpo-location— albo zapisana domyślna lokalizacja) z zaokrągleniem do ~11 km w kluczu cache; serwer nigdy nie przechowuje dokładnej historii GPS.WeatherAdjustmentDecisionto nowa, strukturalnie równoległa doAdaptationDecisiontabela — żadna propozycja nie jest stosowana bez wyraźnego Akceptuj, a zaakceptowana propozycja nigdy nie nadpisuje ręcznie ukończonego treningu.WorkoutEnvironmentSnapshotjest append-only — aktualizacja prognozy nigdy nie zmienia już zapisanego historycznego snapshotu. Wyszukiwanie lepszej pory jest ograniczone do ±3h i maks. 5 kandydatów, nigdy nieograniczone przeszukiwanie. Backend 400/400 i mobile 407/407 testów — patrzdocs/tasks/017-weather-environment-context.md. - Task 018 (asystent audio dla spaceru, nordic walkingu i marszobiegu) — zrealizowany w całości po stronie mobile, w zakresie opisanym w ADR-031: synteza mowy przez
expo-speech(natywny TTS systemu, offline, bez klucza API — nigdy głos generowany przez AI) z wibracją (Vibrationzreact-native) jako niezależnie przełączalnym kanałem, więc brak dostępnej syntezy mowy nigdy nie blokuje treningu.AudioGuidedSessionżyje wyłącznie lokalnie (SQLite przez istniejącyKvStore, tym samym wzorcem coMealPhotoDraft) — jedyny kontakt z serwerem to już istniejącyPOST .../workouts/{id}/completewywoływany raz na końcu sesji, żaden nowy endpoint backendu nie powstał. Czas sesji liczony wyłącznie z zapisanych znaczników czasu i przedziałów pauzy, nigdy z akumulowanych tickówsetInterval— poprawny po powrocie z tła. Każde przejście do tła automatycznie pauzuje sesję jakointerruptedi nigdy nie wznawia głosu samoczynnie po powrocie — wymaga jawnego dotknięcia „Wznów”; restart aplikacji odzyskuje przerwaną sesję z lokalnego zapisu (ekran przygotowania oferuje wznów/zacznij od nowa) zamiast ją cicho tracić. Czysty silnik harmonogramu komunikatów (flattenWorkoutSteps/buildTimeCueSchedule, osobny od stanowego kontrolerauseAudioSession— ten sam powód coEnvironmentClassifier/FuelingNeedClassifier) spłaszcza zagnieżdżone bloki powtórzeń marsz/bieg i generuje komunikaty „zwolnij” przy przejściu z wysiłku na odpoczynek; przypomnienie o piciu pojawia się tylko, gdy już istniejący plan żywienia (Task 016) tego wymaga — audio nigdy nie liczy własnego progu. Kroki wyłącznie dystansowe (bez śledzenia GPS na żywo w tym stosie) nie blokują sesji — działają jako ręczne rozpoczęcie/zakończenie. Przycisk „Rozpocznij z asystentem audio” na szczególe treningu pojawia się tylko, gdy cała struktura jest czasowa. Mobile 66/66 zestawów, 456/456 testów (49 nowych: silnik, repozytorium, kontroler pod fałszywym zegarem, ekran) — patrzdocs/tasks/018-audio-coach-walk-run.md. - Task 019 (sprzęt sportowy, obuwie i historia użycia) — zrealizowany w zakresie opisanym w ADR-032:
GearItemjest lokalny-najpierw dokładnie wzorcemFoodJournalEntry(Task 006, ADR-020) — zapis SQLite natychmiast, synchronizacja przez outbox,expectedUpdatedAtUtcwykrywa konflikt. Agregat przebiegu/czasu (GearAggregateCalculator, czysta funkcja jakEnvironmentClassifier/FuelingNeedClassifier) jest liczony na żądanie wyłącznie po stronie serwera i wymaga połączenia — nigdy cache'owany na urządzeniu, ten sam podział offline/online co plan żywienia w Task 016.GearAssignmentto osobna tabela (jeden główny element plus dowolna liczba akcesoriów na jeden trening/aktywność); przypisanie tego samego sprzętu do obu stron dopasowanej pary trening↔aktywność (ActivityWorkoutMatcher, Task 004) jest odrzucane przy tworzeniu (409), co zapobiega podwójnemu liczeniu jednej realnej aktywności. Tworzenie przypisania jest celowo online-only — bezpośrednie wywołanie API zamiast lokalnego-najpierw zapisu — bo ma walidację międzyencyjną (duplikat, zajęty slot głównego elementu), której istniejący generyczny mechanizm konfliktu outboxa (rozpoznający wyłącznie rozjazdexpectedUpdatedAtUtc) nie obsługuje; usuwanie przypisania zostaje lokalne-najpierw.GearSuggestionEngine(kolejna czysta funkcja) sugeruje sprzęt na podstawie zgodności preferowanego typu aktywności i ostatniego użycia — zawsze wymaga potwierdzenia, nigdy nie przypisuje automatycznie. Próg przypomnienia jest w całości ustalany przez użytkownika (dystans/czas/data/brak) — komunikat mówi wyłącznie o osiągnięciu własnego progu, nigdy o tym, że sprzęt powoduje lub zapobiega urazowi. Identyfikator sprzętu Stravy to zwykłe, ręcznie wpisywane pole — zweryfikowane, żegear_idnie jest dziś w ogóle pobierane z API Stravy. Zdjęcie sprzętu nigdy nie opuszcza urządzenia (URI lokalnego pliku, brak blob storage w stosie, jak Task 013/014/015). Backend 433/433 (33 nowe: 16 integracyjnych, 17 jednostkowych) i mobile 475/475 testów (19 nowych) — patrzdocs/tasks/019-equipment-mileage.md. - Task 020 (plan dnia zawodów i tryb startowy) — zrealizowany w zakresie opisanym w ADR-033:
RaceEventprzy tworzeniu automatycznie zakłada własny, dedykowanyPlannedWorkout(WorkoutType.Race)(relacja 1:1) — dzięki temuNutritionEngine,FuelingNeedClassifier,EnvironmentContextServiceiGearSuggestionEnginedziałają bez żadnej zmiany, aRaceDayPlanje wyłącznie odczytuje, nigdy nie licząc żywienia ani treningu drugi raz.RaceLogisticsChecklistItem(10 kategorii pakowania/logistyki) to celowo osobna klasa od Task 016'sRaceChecklistItem(harmonogram jedzenia/picia) — pokazywane w UI obok siebie, żadna nie zastępuje drugiej. Harmonogram dnia (RaceDayTimelineItem) generuje czysta funkcjaRaceDayTimelineBuilderz ustalonych przesunięć od godziny startu; zmiana startu deterministycznie przelicza wyłącznie pozycje, których użytkownik ręcznie nie edytował. Plan awaryjny (RaceContingencyItem) to zawsze dokładnie sześć logistycznych (nigdy medycznych) scenariuszy z edytowalną domyślną treścią — scenariusz "gorsze samopoczucie" kieruje do pomocy obsługi organizatora lub służb ratunkowych, nigdy do diagnozy. BlokadaraceReadyliczona jest dynamicznie z progu czasowego (48h przed startem, konwersja UTC bezpieczna wobec DST), bez zadania w tle — po progu edycja harmonogramu/strategii wymaga jawnego potwierdzenia i zapisuje migawkę audytową (RacePlanVersion) przed zmianą, nigdy nie kasując cicho poprzedniej strategii. Cel startowy (RaceGoalKind, 7 wartości) nigdy nie wymaga wyniku czasowego. Wszystko poza pobranym pakietem offline jest online-only —RaceDayPlanto żywa agregacja, nie coś urządzenie mogłoby samo odtworzyć; pobrany pakiet działa w pełni offline, a lokalne odznaczenie checklisty w tym trybie nigdy nie synchronizuje się z powrotem do serwera. Podsumowanie po zawodach wymaga, by backing trening był już ukończony przez istniejący endpoint ukończenia — nigdy nie duplikuje obiektywnego wyniku, tylko dodaje subiektywną ocenę. Backend 460/460 (27 nowych: 7 jednostkowychRaceDayTimelineBuilder, 20 integracyjnych) i mobile 498/498 testów (19 nowych) — patrzdocs/tasks/020-race-day-plan.md. - Task 021 (eksport danych i raport PDF) — zrealizowany w zakresie opisanym w ADR-034: każdy eksport — nawet mały JSON jednej sekcji — przechodzi przez tę samą ścieżkę,
DataExport(statusqueued) plusBackgroundJob(export.generate), nigdy generowanie w żądaniu HTTP; jedna ścieżka kodu zamiast progu „duży/mały”. Migawka danych to jedna transakcja Postgresa na poziomie izolacjiRepeatableReadobejmująca wszystkie wybrane sekcje naraz — dosłownie pierwsza z opcji, które dokument zadania wymienia wprost, więc żadna sekcja raportu nie może pokazać danych z innego momentu niż pozostałe. Wygenerowany plik jest kolumnąbyteaw Postgresie, nie plikiem na dysku kontenera ani zewnętrznym blob storage — w stosie nigdy nie było abstrakcji storage (ten sam powód co zdjęcia w Task 013/014/019), a pojedynczy Mikrus VPS nie ma trwałego wolumenu na pliki użytkownika. „Podstawowe trendy” w raporcie to celowo nowy, mały silnik (ExportTrendsBuilder) liczący wąski zestaw faktów (sesje zaplanowane/ukończone, średnie RPE i zmiana masy ciała, obie tylko przy ≥2 próbkach) nad już zapisanymi wierszami — nie port pełnej logiki tygodniowego raportu z Task 007, która istnieje wyłącznie po stronie mobile jako czysta funkcja i nigdy nie jest wywoływana przez backend. PDF renderuje QuestPDF na foncie DejaVu Sans instalowanym jako pakiet systemowy w obrazie Dockera (nigdy jako plik dołączony do repo czy do eksportu użytkownika) — brak danych w sekcji zawsze opisany zdaniem, nigdy zastąpiony zerem, a wizualna kontrola renderu w testach korzysta z wbudowanej w QuestPDF rasteryzacji stron do obrazów zamiast zrzutu ekranu aplikacji. Format CSV dla więcej niż jednej sekcji (i zawsze ZIP) pakuje po jednym pliku na sekcję do archiwum — nigdy nie miesza wielu tabel w jednym nieczytelnym CSV. Idempotencja to poleidempotencyKeyw ciele żądania (unikalne w parze z użytkownikiem), nie nagłówek HTTP — powtórzone żądanie zwraca dokładnie ten sam wiersz niezależnie od statusu. Opcjonalny jednorazowy bezpieczny link jest jedynym endpointem w całym API bez ownership-scopingu przezUserId— autoryzowany wyłącznie nieprzewidywalnym tokenem, bo jego funkcją jest pobranie przez specjalistę bez konta w Runth. Audit zapisuje wyłącznie zdarzenia i metadane, nigdy treść dokumentu. Backend 483/483 (23 nowe: 5 jednostkowych, 7 wizualnej regresji PDF, 11 integracyjnych) i mobile 508/508 testów (10 nowych) — patrzdocs/tasks/021-data-export-pdf-report.md. - Task 022 (kalendarz systemowy, przypomnienia i ciche godziny) — zrealizowany w zakresie opisanym w ADR-035: cała funkcja po stronie mobile, zero nowego backendu — dokładnie ten sam wniosek co Task 018's asystent audio, bo backend już wystawia wszystko potrzebne (
scheduledStartLocal/timeZonetreningów,scheduledLocalTime/purposeposiłków,RaceDayTimelineItem,GearSummary.reminderThresholdReached). Idempotencja opiera się wyłącznie na deterministycznym logicznym id przypomnienia (kategoria:zasób:rodzajWystąpienia, nigdy losowe) — ponowne przeliczenie porównuje z zapisanym stanem dostarczenia i albo nic nie robi, albo anuluje+przekłada pod tym samym id, bez osobnego pola „wersja planu”. Czysty silnikbuildReminderOccurrences(zero I/O, zero importówexpo-*) jest całkowicie odseparowany od warstwy efektów (notification-scheduler.ts/calendar-sync.ts) — dokładnie ten sam podział coaudio-cue-schedule.ts/useAudioSession.tsw Task 018. Integracja z kalendarzem systemowym celowo używa starszego, choć już oznaczonego jako przestarzały, APIexpo-calendar/legacyzamiast nowego API obiektowego z SDK 57 — w tym środowisku bez możliwości testu na urządzeniu, dobrze udokumentowane, stabilne sygnatury (zweryfikowane wprost w źródle pakietu) biją nieprzetestowaną nowość. Ciche godziny obsługują zakres przechodzący przez północ i dopuszczają wyjątek tylko dla kategorii zawodów za jawnym, potwierdzonym przez użytkownika flagą — nigdy milcząco. Zdarzenie w kalendarzu ma zawsze puste pole notatek (żadnych danych żywieniowych/zdrowotnych) i powstaje wyłącznie po jawnej akcji użytkownika; wykrywanie konfliktów kalendarza zostało świadomie wycięte z zakresu (dokument zadania sam oznacza je jako opcjonalne). Mobile 588/588 testów (80 nowych: 25 silnika harmonogramu, 10notification-scheduler, 10calendar-sync, 20 trzech lokalnych repozytoriów, 9 ekranu „Centrum przypomnień”, 6 obsługi deep linków) — patrzdocs/tasks/022-calendar-reminders.md. - Kolejne zadania realizowane po kolei z
docs/tasks— patrz też 16-decisions-and-open-questions.md w razie rozbieżności między mockupami a dokumentacją.
Wymagania: Node.js 24 LTS, .NET SDK 10, Docker Desktop (Compose v2). Szczegóły w 22-local-development.md.
# 1. Sekrety
cp .env.example .env
cp apps/mobile/.env.example apps/mobile/.env
# 2. Backend + baza (Postgres + API w Dockerze)
docker compose up --build -d
curl http://localhost:8080/health/live
curl http://localhost:8080/health/ready
# 3. Aplikacja mobilna
cd apps/mobile
npm install
npm start # otwórz w Expo Go / development buildzieEkran logowania wymaga działającego backendu (EXPO_PUBLIC_API_BASE_URL w apps/mobile/.env, domyślnie http://localhost:8080). Na fizycznym urządzeniu/emulatorze zamiast localhost wpisz adres IP komputera w sieci lokalnej.
Na Windows bez make użyj ./scripts/dev.ps1 <target> (patrz Makefile dla listy targetów: setup, dev-infra, dev-api, dev-mobile, test, lint, format, build, clean).
Weryfikacja jakości mobile (apps/mobile): npm run format:check, npm run lint, npm run typecheck, npm test, npx expo-doctor.
Weryfikacja jakości backendu (root): dotnet format --verify-no-changes, dotnet build, dotnet test (wymaga Dockera — testy Testcontainers).
| Dokument | Cel |
|---|---|
| 00-product-vision.md | wizja, użytkownik i wartość produktu |
| 01-mvp-scope.md | zakres PoC/MVP oraz funkcje odłożone |
| 02-user-flows.md | podstawowe przepływy użytkownika |
| 03-functional-requirements.md | wymagania funkcjonalne |
| 04-non-functional-requirements.md | wydajność, bezpieczeństwo i utrzymanie |
| 05-architecture.md | architektura rozwiązania |
| 06-domain-model.md | model domenowy i encje |
| 07-api-contract.md | kontrakt REST API |
| 08-running-nutrition-engine.md | reguły dopasowania żywienia do biegu |
| 09-strava-integration.md | OAuth, webhooki i synchronizacja Stravy |
| 10-ai-features.md | funkcje AI i ograniczenia bezpieczeństwa |
| 11-security-privacy.md | prywatność i bezpieczeństwo danych |
| 12-infrastructure-mikrus.md | budżetowe wdrożenie na VPS |
| 13-testing-strategy.md | strategia testów |
| 14-roadmap.md | kolejność budowy produktu |
| 15-definition-of-done.md | wspólne kryteria ukończenia |
| 16-decisions-and-open-questions.md | decyzje architektoniczne i pytania |
| 17-ui-ux-spec.md | nawigacja i ekrany mobilne |
| 18-observability-backup.md | logi, monitoring i kopie zapasowe |
| 19-content-data-policy.md | źródła przepisów i danych żywieniowych |
| 20-version-matrix.md | wersje startowe technologii |
| 21-environment-configuration.md | zmienne i środowiska |
| 22-local-development.md | lokalne uruchomienie projektu |
- Agent ma realizować jedno zadanie z
docs/tasksnaraz. - Przed kodowaniem agent musi wskazać dokumenty, na których opiera rozwiązanie.
- Każda zmiana musi zawierać testy lub uzasadnienie ich braku.
- Agent nie może dodawać nowej usługi infrastrukturalnej bez aktualizacji dokumentacji i ADR.
- Agent nie może implementować funkcji spoza MVP „przy okazji”.
- Sekrety nigdy nie trafiają do repozytorium.