Skip to content

Repository files navigation

Runth — PoC/MVP

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.

Najważniejsze założenia

  • 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.

Od czego zacząć

  1. Przeczytaj wizję produktu.
  2. Przeczytaj zakres MVP.
  3. Zapoznaj się z architekturą i wdrożeniem na Mikrusie.
  4. Uruchom w Copilot Chat prompt bootstrap-repository z katalogu .github/prompts.
  5. Realizuj zadania kolejno z katalogu docs/tasks.

Planowana struktura repozytorium

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

Status implementacji

  • Task 001 (bootstrap) — zrealizowany: szkielet apps/mobile (Expo Router, design system, ekran „Dzisiaj” na danych mockowych) i services/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 — patrz docs/tasks/002-auth-profile-onboarding.md i docs/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), endpointy plan/weeks/{weekStart}/workouts/{id} z complete/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 — patrz docs/tasks/003-training-calendar.md i docs/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ń BackgroundJob w 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; patrz docs/tasks/004-strava-integration.md po 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 — patrz docs/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: IAiProvider z domyślnym DisabledAiProvider (brak klucza nigdy nie przerywa startu ani nie blokuje głównych funkcji, ten sam wzorzec co Strava) i AnthropicAiProvider (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 — patrz docs/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 do ActivityPath.running przy 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 — patrz docs/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) obok NutritionEngine, oba karmiące jeden wiersz DailyPlan — 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 — patrz docs/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 na FoodProduct, tym samym wzorcem co Recipe (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 — patrz docs/tasks/012-food-product-catalog.md.
  • Task 013 (skanowanie kodu kreskowego i etykiety) — zrealizowany w zakresie opisanym w ADR-026: ILabelRecognitionProvider z dokumentu zadania to istniejący IAiProvider (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 — patrz docs/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 zamockowanym expo-camera/expo-image-picker).
  • Task 014 (analiza zdjęcia posiłku i wspomagane logowanie AI) — zrealizowany w zakresie opisanym w ADR-027: IMealVisionProvider z dokumentu zadania to ten sam rozszerzony IAiProvider co 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 jeden FoodJournalEntry na potwierdzony komponent, wszystkie połączone nowym MealPhotoAnalysisId (pełni podwójną rolę znacznika źródła i klucza grupującego — bez nowego pola Source). 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 — patrz docs/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 zamockowanym expo-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ący Recipe (systemowy, bez zmian) i nowy UserRecipe (własny), rozróżniane polem Kind, ze składnikami jako prawdziwe odwołania do katalogu (FoodProductId/FoodServingId/Quantity), nie wolny tekst jak w Recipe — dzięki temu makra i alergeny liczą się deterministycznie zamiast być ręcznie oznaczane. Wersjonowanie przez RecipeGroupId/Version/Status (Active/Superseded/Deleted, wzorowane na DailyPlan) — edycja nigdy nie mutuje wiersza w miejscu, tylko wstawia nową wersję i przełącza poprzednią na Superseded; nowy FoodJournalEntry.LoggedFromUserRecipeId wskazuje 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ą na GET /recipes bezpoś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 — patrz docs/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ż przetestowanego NutritionEngine) klasyfikuje potrzebę fuelingu na NotNeeded/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. FuelingPlan generuje 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ę). RunFuelEntry rozszerzony dokładnie wzorcem FoodJournalEntry (Task 012) — opcjonalne odniesienie do katalogu, węglowodany/płyn/sód liczone po stronie serwera jako migawka, SetNull przy 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 co wellbeing); generowanie planu i historia tolerancji wymagają połączenia, ten sam podział offline/online co UserRecipe/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 — patrz docs/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: IEnvironmentProvider to jeden interfejs (nie osobne dla pogody i jakości powietrza), dokładnie wzorcem IAiProvider/IFoodProductProviderDisabledEnvironmentProvider domyślny, gdy WEATHER_ENABLED=false, i OpenMeteoEnvironmentProvider (Open-Meteo, bez klucza API, jeden ręczny retry) jako jedyny zewnętrzny dostawca. Deterministyczny EnvironmentClassifier (czysta funkcja, osobna od NutritionEngine/PriorityEngine/FuelingNeedClassifier) klasyfikuje warunki jako normal/attention/considerAdjustment/avoidAutomatedProgression/providerUnavailable wyłą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ż przetestowanym NutritionEngine. 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 przez expo-location — albo zapisana domyślna lokalizacja) z zaokrągleniem do ~11 km w kluczu cache; serwer nigdy nie przechowuje dokładnej historii GPS. WeatherAdjustmentDecision to nowa, strukturalnie równoległa do AdaptationDecision tabela — żadna propozycja nie jest stosowana bez wyraźnego Akceptuj, a zaakceptowana propozycja nigdy nie nadpisuje ręcznie ukończonego treningu. WorkoutEnvironmentSnapshot jest 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 — patrz docs/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ą (Vibration z react-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ący KvStore, tym samym wzorcem co MealPhotoDraft) — jedyny kontakt z serwerem to już istniejący POST .../workouts/{id}/complete wywoł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ów setInterval — poprawny po powrocie z tła. Każde przejście do tła automatycznie pauzuje sesję jako interrupted i 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 kontrolera useAudioSession — ten sam powód co EnvironmentClassifier/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) — patrz docs/tasks/018-audio-coach-walk-run.md.
  • Task 019 (sprzęt sportowy, obuwie i historia użycia) — zrealizowany w zakresie opisanym w ADR-032: GearItem jest lokalny-najpierw dokładnie wzorcem FoodJournalEntry (Task 006, ADR-020) — zapis SQLite natychmiast, synchronizacja przez outbox, expectedUpdatedAtUtc wykrywa konflikt. Agregat przebiegu/czasu (GearAggregateCalculator, czysta funkcja jak EnvironmentClassifier/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. GearAssignment to 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 rozjazd expectedUpdatedAtUtc) 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, że gear_id nie 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) — patrz docs/tasks/019-equipment-mileage.md.
  • Task 020 (plan dnia zawodów i tryb startowy) — zrealizowany w zakresie opisanym w ADR-033: RaceEvent przy tworzeniu automatycznie zakłada własny, dedykowany PlannedWorkout(WorkoutType.Race) (relacja 1:1) — dzięki temu NutritionEngine, FuelingNeedClassifier, EnvironmentContextService i GearSuggestionEngine działają bez żadnej zmiany, a RaceDayPlan je wyłącznie odczytuje, nigdy nie licząc żywienia ani treningu drugi raz. RaceLogisticsChecklistItem (10 kategorii pakowania/logistyki) to celowo osobna klasa od Task 016's RaceChecklistItem (harmonogram jedzenia/picia) — pokazywane w UI obok siebie, żadna nie zastępuje drugiej. Harmonogram dnia (RaceDayTimelineItem) generuje czysta funkcja RaceDayTimelineBuilder z 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. Blokada raceReady liczona 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 — RaceDayPlan to ż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 jednostkowych RaceDayTimelineBuilder, 20 integracyjnych) i mobile 498/498 testów (19 nowych) — patrz docs/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 (status queued) plus BackgroundJob (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 izolacji RepeatableRead obejmują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ą bytea w 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 pole idempotencyKey w 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 przez UserId — 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) — patrz docs/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/timeZone treningów, scheduledLocalTime/purpose posił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 silnik buildReminderOccurrences (zero I/O, zero importów expo-*) jest całkowicie odseparowany od warstwy efektów (notification-scheduler.ts/calendar-sync.ts) — dokładnie ten sam podział co audio-cue-schedule.ts/useAudioSession.ts w Task 018. Integracja z kalendarzem systemowym celowo używa starszego, choć już oznaczonego jako przestarzały, API expo-calendar/legacy zamiast 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, 10 notification-scheduler, 10 calendar-sync, 20 trzech lokalnych repozytoriów, 9 ekranu „Centrum przypomnień”, 6 obsługi deep linków) — patrz docs/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ą.

Uruchomienie lokalne

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 buildzie

Ekran 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).

Dokumenty

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

Zasady pracy z agentem AI

  • Agent ma realizować jedno zadanie z docs/tasks naraz.
  • 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.

About

A mobile app for recreational runners preparing for distances ranging from their first kilometre to a full marathon. The product combines a training plan, completed Strava activities, and dynamic recommendations for nutrition, hydration, and recovery.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages