Planner jedzeniowy
  • TypeScript 60.9%
  • JavaScript 29.2%
  • CSS 8.7%
  • Dockerfile 0.9%
  • HTML 0.3%
Find a file
picekx be2d8146d6 fix: force redirect_uri scheme/host from APP_URL in OIDC callback
Keycloak rejected the token exchange: redirect_uri sent was
http://planer.mycld.it/... but the one saved at /login was https://.
openid-client derives redirect_uri from the current request URL, which
was http — Traefik terminates TLS and forwards to the container over
plain http. Override protocol/host from APP_URL before the grant call.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 14:57:09 +02:00
.claude Initial commit - v1.0 2026-07-25 10:07:04 +02:00
packages fix: force redirect_uri scheme/host from APP_URL in OIDC callback 2026-08-03 14:57:09 +02:00
.dockerignore fix: unignore src/data — bare data/ pattern swallowed packages/web/src/data 2026-08-02 21:50:49 +02:00
.env.example added keycloak login - v1.1 2026-08-02 20:53:54 +02:00
.gitignore fix: unignore src/data — bare data/ pattern swallowed packages/web/src/data 2026-08-02 21:50:49 +02:00
CLAUDE.md added keycloak login - v1.1 2026-08-02 20:53:54 +02:00
docker-compose.local.yml added keycloak login - v1.1 2026-08-02 20:53:54 +02:00
docker-compose.yml added keycloak login - v1.1 2026-08-02 20:53:54 +02:00
Dockerfile Initial commit - v1.0 2026-07-25 10:07:04 +02:00
info.md added keycloak login - v1.1 2026-08-02 20:53:54 +02:00
package-lock.json added keycloak login - v1.1 2026-08-02 20:53:54 +02:00
package.json Initial commit - v1.0 2026-07-25 10:07:04 +02:00
planer-posilkow-v2.jsx Initial commit - v1.0 2026-07-25 10:07:04 +02:00
README.md added keycloak login - v1.1 2026-08-02 20:53:54 +02:00
tsconfig.base.json Initial commit - v1.0 2026-07-25 10:07:04 +02:00

Planer posiłków

Planer posiłków dla dwuosobowego gospodarstwa. Zastępuje arkusz Excela: tydzień to 7 dni × 5 posiłków, potrawy z biblioteki, osobno dla dwóch osób, z pamięcią tego, co już było. Interfejs po polsku, kod po angielsku. Kontekst produktowy: CLAUDE.md.

Architektura

Monorepo (npm workspaces), trzy pakiety:

pakiet rola stack
packages/shared model domenowy, helpery dat, klucz komórki, projekcja historii, DTO API TypeScript, jedyne źródło prawdy
packages/server REST API + persystencja Hono, Drizzle, SQLite (libSQL)
packages/web interfejs Vite, React, TypeScript, PWA

Dane żyją na serwerze (aplikacja jest współdzielona). Zapis idzie per komórka z semantyką last-write-wins — dwie osoby edytujące różne sloty się nie nadpisują. Synchronizacja to polling przy fokusie okna. Auth: Keycloak (OIDC) — logowanie to redirect do istniejącej instancji Keycloaka, po powrocie serwer wystawia własne podpisane ciasteczko sesji.

web (React/PWA)  ──HTTP /api──►  server (Hono)  ──►  SQLite
        └── repozytorium danych (jeden interfejs, jedna implementacja)

Wymagania

  • Node ≥ 20 (testowane na 26)
  • npm 11

Uruchomienie deweloperskie

npm install
npm run dev

Bez skonfigurowanego Keycloaka (KEYCLOAK_ISSUER_URL/KEYCLOAK_CLIENT_ID/ KEYCLOAK_CLIENT_SECRET) auth jest wyłączony — w devie wchodzisz od razu, bez ekranu logowania. Baza powstaje jako packages/server/data/planer.sqlite i zostaje wypełniona przykładowymi potrawami przy pierwszym starcie.

Skrypty

npm run build       # zbuduj shared → server → web
npm test            # testy shared + server (vitest)
npm run typecheck   # tsc --noEmit we wszystkich pakietach
npm run dev         # server (tsx watch) + web (vite) równolegle

Zmienne środowiskowe

Patrz .env.example. Najważniejsze:

zmienna znaczenie
KEYCLOAK_ISSUER_URL adres realmu Keycloaka. Puste = auth OFF (tylko dev/prywatna sieć). W produkcji ustaw.
KEYCLOAK_CLIENT_ID / KEYCLOAK_CLIENT_SECRET client (confidential) skonfigurowany w Keycloaku
SESSION_SECRET podpis własnego ciasteczka sesji (JWT HS256), min. 32 znaki
APP_URL publiczny adres appki — musi zgadzać się z redirect_uri w Keycloaku (APP_URL/api/auth/callback)
DB_PATH plik SQLite. :memory: w testach.
PORT port API i serwowania frontendu w produkcji
CORS_ORIGIN dozwolone originy (dev). Puste w prod (jeden origin).

Docker

Jeden kontener serwuje API i zbudowany frontend.

Lokalnie / weryfikacja — port 8787 wprost, bez Traefika:

cp .env.example .env      # ustaw KEYCLOAK_*, SESSION_SECRET, APP_URL
docker compose -f docker-compose.local.yml up -d --build
# http://localhost:8787

Produkcja za Traefikiem:

cp .env.example .env      # ustaw KEYCLOAK_*, SESSION_SECRET, APP_URL
docker compose up -d --build

docker-compose.yml zakłada istniejącą sieć Traefika web i resolver TLS le — zmień Host(...), entrypoints i certresolver pod swój homelab. Dane trzyma wolumen planer-data (/data/planer.sqlite).

API (skrót)

metoda ścieżka opis
GET /api/auth/login redirect do Keycloaka
GET /api/auth/callback powrót z Keycloaka, wystawia ciasteczko sesji
GET /api/auth/logout czyści sesję, redirect do wylogowania w Keycloaku
GET /api/bootstrap?from=&to= dishes + people + tygodnie zakresu (jedno zapytanie na start)
GET /api/weeks?from=&to= sam zakres tygodni (polling)
PUT /api/cell zapis jednej komórki (LWW)
GET/POST/PATCH/DELETE /api/dishes biblioteka potraw (+ /bulk na wklejanie z Excela)
GET/PUT /api/people osoby (edycja imion)

Spłacone długi prototypu

  • window.storage → warstwa repozytorium za interfejsem (packages/web/src/data).
  • Sekwencyjne ładowanie 30 tygodni → jedno zapytanie o zakres (/api/bootstrap).
  • CSS w stringu → pliki w packages/web/src/styles, tokeny 1:1.
  • Brak auth → logowanie Keycloak (OIDC). Brak sync → polling + LWW per komórka.
  • Zero testów → vitest dla dat, klucza komórki, projekcji historii i repozytorium.

Świadome ograniczenia

  • Dokładnie dwie osoby. Model dopuszcza N, ale UI (inicjały p1/p2, przełącznik „Obie osoby", miernik x/70) zakłada dwie. Uogólnienie to osobna decyzja.
  • Historia mówi, co zaplanowano, nie co zjedzono — patrz CLAUDE.md.
  • PWA offline jest read-only (odczyt z cache); zapisy wymagają sieci.