v1.0.3
add multi lang
This commit is contained in:
192
README.md
192
README.md
@@ -1,149 +1,165 @@
|
||||
# KtoCo — wspólne wydatki dla grup, par i współlokatorów
|
||||
# WhoWhat — shared expenses for groups, couples and roommates
|
||||
|
||||
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.
|
||||
A web app (PWA) for splitting expenses within a household — who paid what, who owes whom, with history, stats and settlements. Supports any number of people per household and any number of households per user. Mobile-first, installable on your phone, works partially offline. Available in Polish and English, with more languages easy to add.
|
||||
|
||||
## Funkcje
|
||||
## Features
|
||||
|
||||
- **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.
|
||||
- **Dashboard** — "who owes whom" tiles (automatically simplified to the minimum number of transfers for the whole group) with a "Settle up" button, a pie chart of expenses by category, and a month summary.
|
||||
- **Add expense** — large amount field, categories with icons, payer selection, three split modes: equally (among all members), exact amounts, or the full amount to one person.
|
||||
- **History** — expense list with filters (month/category/payer), edit and delete.
|
||||
- **Stats** — month-over-month expenses, comparison of household members' spending, category ranking.
|
||||
- **Multiple households** — a user can belong to several households at once and switch between them (Settings → "Your households"); each household can have any number of members, who join with the same invite code.
|
||||
- **Settings** — switch and manage households (rename/change currency, remove a member, delete a household, invites), custom categories (rename/re-icon), light/dark/system theme, **language switcher (Polish/English)**, email notifications, CSV export.
|
||||
- **Account** — email/password registration, login, rename, change password, password reminder/reset by email, account deletion.
|
||||
- **PWA / offline** — installable on your phone's home screen, cached views, expenses added offline are queued and sync automatically once the connection is back.
|
||||
|
||||
## Stack technologiczny
|
||||
## Tech stack
|
||||
|
||||
| Warstwa | Technologia |
|
||||
| Layer | Technology |
|
||||
|---|---|
|
||||
| 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) |
|
||||
| Database | SQLite (a file on a bind mount at `./sqlite`) |
|
||||
| Infrastructure | Docker Compose, nginx (serves the frontend + proxies `/api`), optionally Traefik (TLS + domain routing) |
|
||||
|
||||
## Struktura projektu
|
||||
## Internationalization (i18n)
|
||||
|
||||
The UI ships in Polish and English out of the box, switchable at any time from Settings → Language (persisted to `localStorage`, and to the user's account once logged in, so transactional emails and CSV exports match their preference too). Backend API errors are returned as stable machine-readable codes (e.g. `invalid_credentials`) and translated client-side — the server never hardcodes user-facing language.
|
||||
|
||||
Frontend translations live in `frontend/src/i18n/locales/<lang>/<namespace>.json`, one JSON file per component/page, auto-discovered at build time (no registration step). **To add a new language:** copy the `locales/en/` directory to `locales/<code>/`, translate every value, and add `{ code, label }` to `frontend/src/i18n/languages.js`. Backend-rendered content (emails, default category names, CSV headers) lives in `backend/src/i18n.js` — add the new language code there too if you want those translated as well.
|
||||
|
||||
## Project structure
|
||||
|
||||
```
|
||||
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)
|
||||
whowhat/
|
||||
├── docker-compose.yaml # the only file needed to run everything
|
||||
├── .env # configuration (secrets, domain, SMTP) — do NOT commit
|
||||
├── .env.example # configuration template to copy
|
||||
├── sqlite/ # bind mount — app.db lives here (data persistence)
|
||||
├── backend/
|
||||
│ ├── Dockerfile
|
||||
│ └── src/
|
||||
│ ├── index.js # Express app, montowanie routerów
|
||||
│ ├── db/ # schema.sql + połączenie better-sqlite3
|
||||
│ ├── middleware/auth.js # weryfikacja JWT
|
||||
│ ├── index.js # Express app, router mounting
|
||||
│ ├── i18n.js # server-rendered translations (emails, CSV, default categories)
|
||||
│ ├── db/ # schema.sql + better-sqlite3 connection
|
||||
│ ├── middleware/auth.js # JWT verification
|
||||
│ ├── routes/ # auth, households, categories, expenses, settlements, stats
|
||||
│ └── utils/ # obliczanie salda, mailer (nodemailer), pomocnicze household
|
||||
│ └── utils/ # balance calculation, mailer (nodemailer), household helpers
|
||||
└── frontend/
|
||||
├── Dockerfile # multi-stage: build (node) -> serve (nginx)
|
||||
├── nginx.conf # proxy /api -> backend:3000
|
||||
├── vite.config.js # konfiguracja PWA (manifest, service worker)
|
||||
├── nginx.conf # proxies /api -> backend:3000
|
||||
├── vite.config.js # PWA config (manifest, service worker)
|
||||
└── src/
|
||||
├── i18n/ # I18nContext, per-namespace locale JSON files
|
||||
├── 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
|
||||
├── components/ # BottomNav, charts, forms, Icon, Switch, LanguageSwitcher, ...
|
||||
├── api/ # fetch client + React Query hooks
|
||||
├── auth/ # auth context (JWT in localStorage)
|
||||
├── household/ # active household context (list + switching)
|
||||
├── theme/ # light/dark/system theme context
|
||||
└── offline/ # IndexedDB queue + sync on reconnect
|
||||
```
|
||||
|
||||
## Uruchomienie
|
||||
## Running it
|
||||
|
||||
Wymagany jest tylko Docker (z pluginem Compose).
|
||||
Docker (with the Compose plugin) is the only requirement.
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# uzupełnij .env (patrz sekcja niżej) — nie trzeba edytować docker-compose.yaml
|
||||
# fill in .env (see below) — no need to edit 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).
|
||||
The app will be available at `http://localhost:8856` (and at your Traefik domain, if configured — see below).
|
||||
|
||||
## Konfiguracja (`.env`)
|
||||
## Configuration (`.env`)
|
||||
|
||||
Cała konfiguracja wdrożeniowa znajduje się w `.env` — `docker-compose.yaml` nie wymaga edycji.
|
||||
All deployment configuration lives in `.env` — `docker-compose.yaml` doesn't need editing.
|
||||
|
||||
| Zmienna | Opis | Domyślnie |
|
||||
| Variable | Description | Default |
|
||||
|---|---|---|
|
||||
| `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` |
|
||||
| `JWT_SECRET` | Secret used to sign login tokens. Generate with: `openssl rand -hex 32` | — (required) |
|
||||
| `FRONTEND_URL` | Public URL of the app, used in email links (e.g. password reset) | `https://example.com` |
|
||||
| `DOMAIN` | Domain Traefik should expose the app on | `example.com` |
|
||||
| `TRAEFIK_NETWORK` | Name of the existing external Docker network Traefik is attached to | `traefik_public` |
|
||||
| `SMTP_HOST` | SMTP server address. Empty = email sending disabled (console log only) | — (optional) |
|
||||
| `SMTP_PORT` | SMTP port | `587` |
|
||||
| `SMTP_SECURE` | `true` for immediate SSL/TLS (port 465), otherwise `false` (STARTTLS) | `false` |
|
||||
| `SMTP_USER` / `SMTP_PASS` | SMTP login credentials | — |
|
||||
| `SMTP_FROM` | Sender address on outgoing emails | `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).
|
||||
Without SMTP configured the app works normally — "password reset" and "email notifications" simply don't send real emails (the backend logs to the console that sending was skipped).
|
||||
|
||||
## Dane / trwałość
|
||||
## Data / persistence
|
||||
|
||||
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.
|
||||
The SQLite database lives at `./sqlite/app.db` on the host (a bind mount, not a named Docker volume) — easy to copy, back up, or inspect with the `sqlite3` CLI without entering the container.
|
||||
|
||||
## Wdrożenie za Traefikiem
|
||||
## Deploying behind Traefik
|
||||
|
||||
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):
|
||||
The `frontend` service is attached to the external `traefik_public` network (name configurable via `TRAEFIK_NETWORK`) and carries Traefik labels (domain routing from `.env`, TLS via `tls-resolver`). Prerequisite: the `traefik_public` network must already exist on the host (normally created by the Traefik stack itself):
|
||||
|
||||
```bash
|
||||
docker network create traefik_public # tylko jeśli jeszcze nie istnieje
|
||||
docker network create traefik_public # only if it doesn't exist yet
|
||||
```
|
||||
|
||||
Port `8856` frontend jest dodatkowo opublikowany bezpośrednio na hosta — przydatne przy testach lokalnych równolegle z dostępem przez Traefik.
|
||||
The frontend's port `8856` is also published directly on the host — useful for local testing alongside access through Traefik.
|
||||
|
||||
## Model danych (SQLite)
|
||||
## Data model (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ę")
|
||||
- `users` — accounts (email, password hash, email notification preference, UI language)
|
||||
- `households` — households (name, currency)
|
||||
- `household_members` — household membership (any number of people; a user can be in several households at once)
|
||||
- `invites` — household invite codes (valid 7 days, reusable — they don't expire after a single join)
|
||||
- `password_resets` — one-time password reset tokens (valid 1h)
|
||||
- `categories` — expense categories (name, Material Symbols icon, color)
|
||||
- `expenses` — expenses (amount, payer, category, date, split type)
|
||||
- `expense_shares` — the final split of an expense across household members (always sums to the expense amount regardless of split type)
|
||||
- `settlements` — settlement history ("Settle up")
|
||||
|
||||
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ą).
|
||||
Per-person balance is computed as: `(amount they paid) − (sum of their expense shares) − (net settlements)`. For the "who owes whom" display, balances are simplified with a greedy algorithm (`backend/src/utils/balance.js: simplifyDebts`) that produces the minimum number of transfers to settle everyone (instead of a separate debt between every pair).
|
||||
|
||||
## Wiele gospodarstw — jak to działa
|
||||
New rows in `users` get a `language` column (`'pl'` or `'en'`, default `'pl'`); on an existing database this is added automatically on startup via `ALTER TABLE ... ADD COLUMN` — no manual migration, and no existing rows are touched or deleted.
|
||||
|
||||
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: <id aktywnego gospodarstwa>` 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.
|
||||
## Multiple households — how it works
|
||||
|
||||
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.
|
||||
A user can belong to multiple households. Since every request for household-scoped resources (expenses, categories, balance, stats, settlements) needs to know which household it's about, the frontend sends an `X-Household-Id: <active household id>` header on every such request (set automatically by `frontend/src/household/HouseholdContext.jsx` whenever the active household is switched in Settings). The backend's `requireHousehold` middleware verifies the logged-in user is actually a member of the given household.
|
||||
|
||||
## API (skrót)
|
||||
Removing the last member from a household automatically deletes the household itself (along with its entire expense history — cascading deletes via foreign keys). Deleting a user account that has shared financial history with others (expenses/shares/settlements) doesn't physically remove it from the database (that would break the history visible to the rest of the household) — instead the account is anonymized (name → "Deleted account", email replaced with a unique non-existent address, password invalidated). A fresh account with no history is deleted outright.
|
||||
|
||||
Wszystkie endpointy poza `/auth/register`, `/auth/login`, `/auth/forgot-password`, `/auth/reset-password` i `/health` wymagają nagłówka `Authorization: Bearer <token>`. Endpointy gospodarstwa/kategorii/wydatków/rozliczeń/statystyk dodatkowo wymagają `X-Household-Id: <id>`.
|
||||
## API (overview)
|
||||
|
||||
| Grupa | Endpointy |
|
||||
Every endpoint except `/auth/register`, `/auth/login`, `/auth/forgot-password`, `/auth/reset-password` and `/health` requires an `Authorization: Bearer <token>` header. Household/category/expense/settlement/stats endpoints additionally require `X-Household-Id: <id>`.
|
||||
|
||||
Error responses are `{ "error": "<code>" }`, where `<code>` is a stable snake_case identifier (e.g. `invalid_credentials`, `household_not_found`) meant to be translated client-side — see [`frontend/src/i18n/locales/en/errors.json`](frontend/src/i18n/locales/en/errors.json) for the full list.
|
||||
|
||||
| Group | Endpoints |
|
||||
|---|---|
|
||||
| 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` |
|
||||
| Auth | `POST /auth/register`, `/login`, `/change-password`, `/forgot-password`, `/reset-password`, `GET /auth/me`, `PUT /auth/me`, `PUT /auth/me/notifications`, `PUT /auth/me/language`, `DELETE /auth/me` |
|
||||
| Households | `GET /households` (yours), `GET/PUT/DELETE /households/:id`, `POST /households`, `POST /households/:id/invite`, `POST /households/join`, `DELETE /households/:id/members/:userId` |
|
||||
| Categories | `GET/POST/PUT/DELETE /categories[/:id]` |
|
||||
| Expenses | `GET/POST/PUT/DELETE /expenses[/:id]` (filters: `month`, `categoryId`, `payerId`) |
|
||||
| Settlements | `GET/POST /settlements` (POST immediately settles all simplified transfers) |
|
||||
| Stats | `GET /stats/balance`, `/summary`, `/monthly`, `/export.csv` |
|
||||
|
||||
## Tryb offline (PWA)
|
||||
## Offline mode (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.
|
||||
A service worker (Workbox, via `vite-plugin-pwa`) caches the app shell and recently fetched GET data from the API (`NetworkFirst` strategy), so the app opens and shows data even without a connection. A new expense added offline is queued in IndexedDB (`frontend/src/offline/`) and sent automatically once the connection comes back (`online` event) — a banner then shows the number of pending entries.
|
||||
|
||||
## Znane ograniczenia
|
||||
## Known limitations
|
||||
|
||||
- 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.
|
||||
- Copying the invite code via `navigator.clipboard` requires a secure context (HTTPS or `localhost`) — over plain HTTP on a local network the browser may block it; the code is therefore always also available as a selectable text field (manual copy always works).
|
||||
- `schema.sql` uses `CREATE TABLE IF NOT EXISTS` — adding a new column to an existing table on an already-running database requires either a manual migration (`ALTER TABLE`) or startup logic like the one already in place for `users.language` (see `backend/src/db/db.js`); a fresh database gets the current schema immediately.
|
||||
- A household invite code doesn't expire after first use (intentionally — it lets you invite any number of people with the same code), only after time (7 days) or a manual regeneration in Settings.
|
||||
|
||||
## Rozwój lokalny (bez Dockera)
|
||||
## Local development (without Docker)
|
||||
|
||||
Wymaga Node.js 20+.
|
||||
Requires 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
|
||||
cd frontend && npm install && npm run dev # dev server on :5173, proxies /api -> :3000
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
||||
Reference in New Issue
Block a user