Case study
Smakowity Plan
PWA, która układa rodzinny jadłospis na tydzień, generuje przepisy przez sztuczną inteligencję i składa z nich listę zakupów pogrupowaną po kategoriach.
Kontekst
Mamy dwoje dzieci. Żona codziennie wymyślała, co ugotować — nie dlatego, że nie umie, tylko dlatego, że to praca, której nikt nie widzi i która nigdy się nie kończy. Aplikacja powstała, żeby ją z tego zdjąć: tydzień posiłków, przepisy i lista zakupów w jednym kroku, zamiast codziennego „co dziś na obiad”.
Co zbudowałem
- Generowanie tygodniowego jadłospisu przez model językowy, z uwzględnieniem liczby domowników i preferencji.
- Przepis pod każdym posiłkiem: makroskładniki, składniki z gramaturą, kroki przygotowania.
- Listę zakupów składaną automatycznie z całego tygodnia i grupowaną po kategoriach sklepu.
- Historię zapisanych jadłospisów z ulubionymi.
- Panel administracyjny z analityką użycia i audytem.
Sedno
Decyzje techniczne
Dlaczego PIN, a nie hasła?
To aplikacja dla domowników, nie dla internetu — baza użytkowników jest zamknięta, rejestracji nie ma. Przy takim założeniu hasło jest kosztem bez zysku: trzeba je wymyślić, zapamiętać, resetować. PIN wpisuje się jedną ręką, przy garnku.
„Na ten moment wystarczy” jest tu decyzją, nie przeoczeniem — i dlatego
reszta jest zbudowana tak, żeby ta decyzja dała się cofnąć: PIN-y żyją
w users.json, nie w kodzie, a sesja stoi na tokenie podpisanym
HMAC-iem. Wymiana samego mechanizmu logowania nie wymaga ruszania aplikacji.
Dlaczego limit zapytań jest per-endpoint, a nie globalny?
Licznik rozdzielony na kubełki (gemini, auth) rozwiązuje
konkretny problem: bez tego pięć generacji jadłospisu wyczerpywałoby limit i blokowało
logowanie domownikom. Limit ma chronić klucz API, nie zamykać drzwi rodzinie.
Dlaczego limiter przepuszcza żądanie, gdy nie może zapisać licznika?
Fail-open. Gdy magazyn liczników jest niedostępny, żądanie przechodzi — lepiej wpuścić nadmiarowe zapytanie niż odciąć domownika od aplikacji przez problem z dyskiem. Przy limicie chroniącym budżet, a nie dane, to właściwy kompromis.
Dlaczego porównanie PIN-u nie przerywa pętli po trafieniu?
Pętla przechodzi przez wszystkie PIN-y i używa hash_equals() zamiast
===. Bez tego czas odpowiedzi zdradzałby, ile PIN-ów sprawdzono, zanim
trafiono — a przy czterech cyfrach każda wskazówka skraca atak.
Dlaczego klucz do API modelu nigdy nie trafia do przeglądarki?
Zapytania idą przez własny endpoint PHP, który dokłada klucz po stronie serwera. Aplikacja może wskazać model, ale tylko z białej listy walidowanej serwerowo — klient nie decyduje, do czego sięga mój klucz.
Dlaczego plik z kluczem ma strażnika, skoro blokuje go .htaccess?
Obrona w głąb. config.php przerywa wykonanie, jeśli nie został dołączony
przez aplikację — działa nawet gdyby reguła Apache'a zniknęła przy migracji hostingu
albo ktoś nadpisał .htaccess. Dwie niezależne warstwy, bo pierwsza kiedyś padnie.
Jak to wygląda
Stack
- Vite
- PWA / Workbox
- React
- PHP 8
- Gemini 2.5 / 3.5 Flash
- react-pdf
- Apache / OVH
Aplikacja jest zamknięta PIN-em — chroni klucz API i dane domowników. Chętnie założę dostęp, jeśli chcesz zobaczyć ją w działaniu.