# KtoCo — wspólne wydatki dla grup, par i współlokatorów Aplikacja webowa (PWA) do dzielenia się wydatkami w gospodarstwie domowym — kto ile wydał, kto komu jest winien, z historią, statystykami i rozliczeniami. Obsługuje dowolną liczbę osób w gospodarstwie i dowolną liczbę gospodarstw na użytkownika. Mobile-first, instalowalna na telefonie, działa częściowo offline. ## Funkcje - **Dashboard** — kafelki „kto komu ile jest winien" (automatycznie uproszczone do minimalnej liczby przelewów dla całej grupy) z przyciskiem „Rozlicz się", wykres kołowy wydatków wg kategorii, podsumowanie miesiąca. - **Dodawanie wydatku** — duże pole kwoty, kategorie z ikonami, wybór płacącego, trzy tryby podziału: po równo (między wszystkich członków), dokładny podział, całość na jedną osobę. - **Historia** — lista wydatków z filtrami (miesiąc/kategoria/płacący), edycja i usuwanie. - **Statystyki** — wydatki miesiąc do miesiąca, porównanie konsumpcji członków gospodarstwa, ranking kategorii. - **Wiele gospodarstw** — użytkownik może należeć do wielu gospodarstw jednocześnie i przełączać się między nimi (Ustawienia → „Twoje gospodarstwa"); każde gospodarstwo może mieć dowolną liczbę członków, dołączanych tym samym kodem zaproszenia. - **Ustawienia** — przełącznik i zarządzanie gospodarstwami (zmiana nazwy/waluty, usuwanie członka, usunięcie całego gospodarstwa, zaproszenia), własne kategorie (z edycją nazwy/ikony), motyw jasny/ciemny/systemowy, powiadomienia mailowe, eksport CSV. - **Konto** — rejestracja e-mail/hasło, logowanie, zmiana nazwy, zmiana hasła, przypomnienie/reset hasła mailem, usunięcie konta. - **PWA / offline** — instalowalna na ekranie głównym telefonu, cache widoków, wydatki dodane offline trafiają do kolejki i synchronizują się automatycznie po powrocie sieci. ## Stack technologiczny | Warstwa | Technologia | |---|---| | Frontend | React (Vite), react-router-dom, TanStack Query, Recharts, vite-plugin-pwa, Material Symbols (Google Fonts) | | Backend | Node.js + Express, better-sqlite3, JWT (jsonwebtoken), bcryptjs, nodemailer | | Baza danych | SQLite (plik na bind mouncie `./sqlite`) | | Infrastruktura | Docker Compose, nginx (serwuje frontend + proxy `/api`), opcjonalnie Traefik (TLS + routing domenowy) | ## Struktura projektu ``` ktoco/ ├── docker-compose.yaml # jedyny plik potrzebny do uruchomienia całości ├── .env # konfiguracja (sekrety, domena, SMTP) — NIE commitować ├── .env.example # szablon konfiguracji do skopiowania ├── sqlite/ # bind mount — tu leży plik app.db (trwałość danych) ├── backend/ │ ├── Dockerfile │ └── src/ │ ├── index.js # Express app, montowanie routerów │ ├── db/ # schema.sql + połączenie better-sqlite3 │ ├── middleware/auth.js # weryfikacja JWT │ ├── routes/ # auth, households, categories, expenses, settlements, stats │ └── utils/ # obliczanie salda, mailer (nodemailer), pomocnicze household └── frontend/ ├── Dockerfile # multi-stage: build (node) -> serve (nginx) ├── nginx.conf # proxy /api -> backend:3000 ├── vite.config.js # konfiguracja PWA (manifest, service worker) └── src/ ├── pages/ # Dashboard, AddExpense, History, Stats, Settings, Login, Register, ... ├── components/ # BottomNav, wykresy, formularze, Icon, Switch, ... ├── api/ # klient fetch + hooki React Query ├── auth/ # kontekst autoryzacji (JWT w localStorage) ├── household/ # kontekst aktywnego gospodarstwa (lista + przełączanie) ├── theme/ # kontekst motywu jasny/ciemny/systemowy └── offline/ # kolejka IndexedDB + synchronizacja po powrocie sieci ``` ## Uruchomienie Wymagany jest tylko Docker (z pluginem Compose). ```bash cp .env.example .env # uzupełnij .env (patrz sekcja niżej) — nie trzeba edytować docker-compose.yaml sudo docker compose up -d --build ``` Aplikacja będzie dostępna pod `http://localhost:8856` (oraz pod domeną z Traefika, jeśli skonfigurowana — patrz niżej). ## Konfiguracja (`.env`) Cała konfiguracja wdrożeniowa znajduje się w `.env` — `docker-compose.yaml` nie wymaga edycji. | Zmienna | Opis | Domyślnie | |---|---|---| | `JWT_SECRET` | Sekret do podpisywania tokenów logowania. Wygeneruj: `openssl rand -hex 32` | — (wymagany) | | `FRONTEND_URL` | Publiczny adres aplikacji, używany w linkach w mailach (np. reset hasła) | `https://ktoco.kzbikowski.pl` | | `DOMAIN` | Domena, pod którą Traefik wystawia aplikację | `ktoco.kzbikowski.pl` | | `TRAEFIK_NETWORK` | Nazwa istniejącej zewnętrznej sieci Docker, do której podłączony jest Traefik | `traefik_public` | | `SMTP_HOST` | Adres serwera SMTP. Puste = wysyłka maili wyłączona (tylko log w konsoli) | — (opcjonalne) | | `SMTP_PORT` | Port SMTP | `587` | | `SMTP_SECURE` | `true` dla połączenia SSL/TLS od razu (port 465), inaczej `false` (STARTTLS) | `false` | | `SMTP_USER` / `SMTP_PASS` | Dane logowania do SMTP | — | | `SMTP_FROM` | Adres nadawcy w wysyłanych mailach | `SMTP_USER` | Bez skonfigurowanego SMTP aplikacja działa normalnie — funkcje „reset hasła" i „powiadomienia mailowe" po prostu nie wysyłają realnych maili (backend loguje w konsoli, że wysyłkę pominięto). ## Dane / trwałość Baza SQLite leży w `./sqlite/app.db` na hoście (bind mount, nie nazwany wolumen Dockera) — łatwo ją skopiować, zbackupować albo podejrzeć narzędziem `sqlite3` bez wchodzenia do kontenera. ## Wdrożenie za Traefikiem Serwis `frontend` jest podłączony do zewnętrznej sieci `traefik_public` (nazwa konfigurowalna przez `TRAEFIK_NETWORK`) i ma etykiety Traefika (routing po domenie z `.env`, TLS przez `tls-resolver`). Warunek: sieć `traefik_public` musi już istnieć na hoście (tworzy ją zwykle stack samego Traefika): ```bash docker network create traefik_public # tylko jeśli jeszcze nie istnieje ``` Port `8856` frontend jest dodatkowo opublikowany bezpośrednio na hosta — przydatne przy testach lokalnych równolegle z dostępem przez Traefik. ## Model danych (SQLite) - `users` — konta (e-mail, hash hasła, preferencja powiadomień mailowych) - `households` — gospodarstwa domowe (nazwa, waluta) - `household_members` — członkowie gospodarstwa (dowolna liczba osób; użytkownik może być w wielu gospodarstwach naraz) - `invites` — kody zaproszeń do gospodarstwa (ważne 7 dni, wielokrotnego użytku — nie wygasają po jednym dołączeniu) - `password_resets` — jednorazowe tokeny resetu hasła (ważne 1h) - `categories` — kategorie wydatków (nazwa, ikona Material Symbols, kolor) - `expenses` — wydatki (kwota, płacący, kategoria, data, typ podziału) - `expense_shares` — finalny podział wydatku między członków gospodarstwa (niezależnie od typu podziału zawsze sumuje się do kwoty wydatku) - `settlements` — historia rozliczeń („Rozlicz się") Saldo per osoba liczone jest jako: `(suma zapłacona przez osobę) − (suma jej udziałów w wydatkach) − (netto rozliczeń)`. Do prezentacji „kto komu ile jest winien" salda są upraszczane zachłannym algorytmem (`backend/src/utils/balance.js: simplifyDebts`), który dla N osób generuje minimalną liczbę przelewów rozliczających wszystkich (zamiast osobnego długu między każdą parą). ## Wiele gospodarstw — jak to działa Użytkownik może należeć do wielu gospodarstw. Ponieważ każdy request do zasobów powiązanych z gospodarstwem (wydatki, kategorie, saldo, statystyki, rozliczenia) musi wiedzieć, którego gospodarstwa dotyczy, frontend wysyła nagłówek `X-Household-Id: ` przy każdym takim żądaniu (ustawiany automatycznie przez `frontend/src/household/HouseholdContext.jsx` po przełączeniu gospodarstwa w Ustawieniach). Backend weryfikuje w `requireHousehold` middleware, że zalogowany użytkownik faktycznie jest członkiem podanego gospodarstwa. Usunięcie ostatniego członka z gospodarstwa automatycznie kasuje samo gospodarstwo (wraz z całą historią wydatków — kasowanie kaskadowe przez klucze obce). Usunięcie konta użytkownika, który ma współdzieloną historię finansową z innymi (wydatki/udziały/rozliczenia), nie usuwa go fizycznie z bazy (zepsułoby to historię widoczną dla reszty gospodarstwa) — konto jest wtedy anonimizowane (nazwa → „Usunięte konto", e-mail zastąpiony unikalnym nieistniejącym adresem, hasło unieważnione). Świeże konto bez żadnej historii jest usuwane w całości. ## API (skrót) Wszystkie endpointy poza `/auth/register`, `/auth/login`, `/auth/forgot-password`, `/auth/reset-password` i `/health` wymagają nagłówka `Authorization: Bearer `. Endpointy gospodarstwa/kategorii/wydatków/rozliczeń/statystyk dodatkowo wymagają `X-Household-Id: `. | Grupa | Endpointy | |---|---| | Auth | `POST /auth/register`, `/login`, `/change-password`, `/forgot-password`, `/reset-password`, `GET /auth/me`, `PUT /auth/me`, `PUT /auth/me/notifications`, `DELETE /auth/me` | | Gospodarstwa | `GET /households` (lista Twoich), `GET/PUT/DELETE /households/:id`, `POST /households`, `POST /households/:id/invite`, `POST /households/join`, `DELETE /households/:id/members/:userId` | | Kategorie | `GET/POST/PUT/DELETE /categories[/:id]` | | Wydatki | `GET/POST/PUT/DELETE /expenses[/:id]` (filtry: `month`, `categoryId`, `payerId`) | | Rozliczenia | `GET/POST /settlements` (POST rozlicza od razu wszystkie uproszczone przelewy) | | Statystyki | `GET /stats/balance`, `/summary`, `/monthly`, `/export.csv` | ## Tryb offline (PWA) Service worker (Workbox, przez `vite-plugin-pwa`) cache'uje powłokę aplikacji i ostatnio pobrane dane GET z API (strategia `NetworkFirst`), więc appka otwiera się i pokazuje dane nawet bez sieci. Nowy wydatek dodany offline trafia do kolejki w IndexedDB (`frontend/src/offline/`) i zostaje automatycznie wysłany po wykryciu powrotu połączenia (`online` event) — widoczny jest wtedy baner z liczbą oczekujących wpisów. ## Znane ograniczenia - Kopiowanie kodu zaproszenia przez `navigator.clipboard` wymaga bezpiecznego kontekstu (HTTPS lub `localhost`) — na zwykłym HTTP w sieci lokalnej przeglądarka może to zablokować; dlatego kod jest zawsze dostępny też jako zaznaczalne pole tekstowe (ręczne kopiowanie zawsze działa). - `schema.sql` używa `CREATE TABLE IF NOT EXISTS` — dodanie nowej kolumny do istniejącej tabeli w już działającej bazie wymaga ręcznej migracji (`ALTER TABLE`); przy starcie na czystej bazie schemat tworzy się poprawnie od razu. - Kod zaproszenia do gospodarstwa nie wygasa po pierwszym użyciu (celowo — pozwala zaprosić dowolną liczbę osób tym samym kodem), tylko po czasie (7 dni) lub ręcznym wygenerowaniu nowego w Ustawieniach. ## Rozwój lokalny (bez Dockera) Wymaga Node.js 20+. ```bash cd backend && npm install && JWT_SECRET=dev DATABASE_PATH=./data/app.db npm start cd frontend && npm install && npm run dev # serwer dev na :5173, proxy /api -> :3000 ```