Files
alpenwerk-hr/DEPLOYMENT.md
Maximilian Stubhan 79f0e19bf8 Org assignment history, mobile support, and a correctness pass
Data model
- employee_assignments records org placement over time (valid_from/valid_to),
  written by a trigger on `employees` rather than inside each RPC: ~70
  `update employees` statements spread over fifteen migrations mean per-call
  bookkeeping would miss paths today and again with every future RPC. A
  partial unique index enforces the one-open-interval invariant the trigger
  relies on when closing the current row.
- The Organigramm gains a Stichtag (default today). Membership comes from
  entry/exit/karenz, past placement from the new history, future placement
  projected from pending_org_changes. Placements predating the migration are
  backfilled with today's values and flagged as such in the UI, since
  employee_history only ever stored free text and cannot be reconstructed.

Correctness
- Reports and exports silently truncated at PostgREST's 1000-row cap
  (db.max_rows); employee_history is already past it at ~800 staff. Every
  whole-table read now pages explicitly.
- XLSX date cells were a day early: ExcelJS converts a Date to an Excel
  serial straight off getTime(), so a Date built at local midnight lands on
  the previous day's serial in any positive-offset zone.
- Date handling is pinned to Europe/Vienna throughout, and date-only strings
  are formatted without a Date round-trip. The dashboard's YTD window was
  built by round-tripping a local Date through toISOString(), which shifted
  it a day early and dropped 31 December entirely.
- Export routes parsed measure/group/split/eventType with unchecked `as`
  casts, so an unknown value reached column headers as `undefined` and the
  Content-Disposition filename. Parsed against the label maps now, with the
  filename slugged as a backstop.
- toXlsx keyed columns by header text, silently dropping the second of any
  two columns sharing a name — split columns take their header from data.
- The org chart tree walks had no cycle guard; nothing in the schema forbids
  a manager_id cycle, and one would hang the tab rather than misreport.
- The login page reflected ?error= verbatim, letting anyone put arbitrary
  text on the real sign-in screen; messages are looked up by code now.
- React Flow needs elementsSelectable on, or it sets pointer-events:none on
  the whole node and the expand control stops responding.

UI
- Mobile: the shell was unusable below lg — a fixed 236px margin pushed
  content off-screen with no mobile navigation at all. The sidebar is now a
  drawer, dvh replaces vh, safe-area insets are honoured, inputs are 16px so
  iOS stops zooming on focus, and form grids stack.
- Org chart nodes redesigned: per-kind accent stripes and icons, vacant
  roles called out, expand control moved to the bottom edge carrying the
  child count.
- Pagination is windowed; it previously rendered one link per page (54 for
  the employee list, unbounded for the audit log).
- Positions page reduced to open positions with a single "Besetzen" action.
- The employee Organisation tab links into the org chart focused on that
  person, reusing the chart's existing search-match highlighting.

Also included, uncommitted until now
- Dependants, HR notes, academic titles, split address fields, position
  validity and role/employment fields, with their migrations and UI.
- Docker/compose deployment setup, data-model and security-review docs.
2026-07-24 23:38:10 +02:00

157 lines
5.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Deployment mit Docker
Dieser Guide beschreibt, wie die App (bisher auf Vercel deployed, siehe
`vercel.json`) stattdessen als Docker-Container auf einem beliebigen Server
läuft.
## Was wird containerisiert und was nicht
- **Containerisiert:** nur die Next.js-App selbst (`Dockerfile`).
- **Nicht containerisiert:** Supabase (Datenbank + Auth). Die App verbindet
sich per URL/Key zu einem bestehenden Supabase-Projekt (Cloud oder
selbst gehostet) das bleibt unverändert. `supabase/` in diesem Repo ist
nur die lokale Dev-/Migrations-Umgebung (`supabase start`), kein Teil des
Deployments.
- **Ersetzt:** der Vercel-Cron-Job aus `vercel.json` (täglich 03:00 Uhr,
ruft `/api/cron/apply-pending-changes` auf, um fällige Versetzungen/
Beförderungen/Karenz/Reorg-Änderungen zu übernehmen). Da es außerhalb von
Vercel kein Vercel-Cron gibt, übernimmt das im `docker-compose.yml`
enthaltene `cron`-Sidecar-Container diese Aufgabe mit demselben Schema
und demselben Bearer-Secret, das die Route bereits erwartet.
## Voraussetzungen
- Docker + Docker Compose (v2, das im Docker Desktop/Docker Engine
enthaltene `docker compose`) auf dem Zielserver.
- Ein bestehendes Supabase-Projekt mit den Migrationen aus
`supabase/migrations/` bereits eingespielt (`supabase db push` bzw. wie
bisher).
## 1. `.env` anlegen
```bash
cp .env.example .env
```
Werte eintragen:
| Variable | Woher |
|---|---|
| `NEXT_PUBLIC_SUPABASE_URL` | Supabase-Projekt → Settings → API |
| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Supabase-Projekt → Settings → API |
| `SUPABASE_SERVICE_ROLE_KEY` | Supabase-Projekt → Settings → API (geheim!) |
| `CRON_SECRET` | selbst generieren: `openssl rand -hex 32` |
Wichtig zum Verständnis:
- `NEXT_PUBLIC_*`-Variablen werden **beim Build** in das Browser-Bundle
eingebacken (Next.js-Verhalten, nicht Docker-spezifisch). Ändern sich
diese Werte, muss das Image **neu gebaut** werden ein reiner Container-
Neustart reicht nicht.
- `SUPABASE_SERVICE_ROLE_KEY` und `CRON_SECRET` sind Server-only-Secrets.
Sie werden bewusst **nicht** als Build-Arg übergeben (das würde sie im
Image-Layer-History sichtbar machen), sondern erst zur Laufzeit über
`env_file` injiziert.
- `.env` steht schon in `.gitignore` nicht committen.
## 2. Bauen und lokal testen
```bash
docker compose build
docker compose up -d
docker compose logs -f app
```
App ist danach unter `http://localhost:3000` erreichbar. Healthcheck prüft
`GET /login`; Status siehe `docker compose ps`.
Cron-Sidecar prüfen:
```bash
docker compose logs -f cron
```
## 3. Auf einem Server deployen
Einfachste Variante Repo direkt auf dem Server bauen:
```bash
git clone <repo-url> && cd manner-app
cp .env.example .env # Werte eintragen
docker compose up -d --build
```
Alternative für CI/CD (Image einmal bauen, überall pullen): Image in einer
Registry (GHCR, Docker Hub, …) bauen und pushen, auf dem Server nur
`docker compose pull && docker compose up -d` ausführen. Dafür in
`docker-compose.yml` zusätzlich `image: <registry>/<name>:<tag>` setzen und
den Build in der CI-Pipeline mit den `--build-arg`-Werten für
`NEXT_PUBLIC_*` laufen lassen.
## 4. Reverse Proxy + HTTPS
Next.js selbst sollte laut den offiziellen Docs **nicht** direkt exponiert
werden ein Reverse Proxy übernimmt TLS, Rate-Limiting und Request-
Validierung. Beispiel mit [Caddy](https://caddyserver.com/) (automatisches
HTTPS via Let's Encrypt):
```caddyfile
# /etc/caddy/Caddyfile
hr.example.com {
reverse_proxy localhost:3000
}
```
`docker-compose.yml` published Port 3000 aktuell auf den Host bei
Verwendung eines Reverse Proxys auf demselben Host kann das Publishing auf
`127.0.0.1:3000:3000` eingeschränkt werden, damit der Container-Port nicht
direkt von außen erreichbar ist.
## 5. Updates ausrollen
```bash
git pull
docker compose build
docker compose up -d
```
Kurzer Downtime-Moment beim Neustart des `app`-Containers ist bei dieser
Single-Instance-Compose-Konfiguration normal. Für Zero-Downtime-Deployments
wäre eine zweite Instanz + Load Balancer nötig (siehe Abschnitt 6).
Datenbank-Migrationen (`supabase/migrations/*.sql`) werden weiterhin über
die Supabase CLI gegen das Supabase-Projekt gefahren, unabhängig vom
App-Deployment:
```bash
supabase db push
```
## 6. Hinweis bei mehreren Replicas
Läuft die App skaliert (mehrere `app`-Container hinter einem Load
Balancer), muss `NEXT_SERVER_ACTIONS_ENCRYPTION_KEY` explizit gesetzt und
auf allen Instanzen identisch sein sonst schlagen Server Actions
(`actions/*.ts`, z. B. Mitarbeiter- und Positions-Mutationen) mit "Failed to
find Server Action" fehl, wenn eine Anfrage auf einer anderen Instanz landet
als der, die das Formular gerendert hat. Erzeugen mit:
```bash
openssl rand -base64 32
```
Als zusätzliche Env-Variable in `.env` eintragen. Bei der aktuellen
Single-Instance-Compose-Konfiguration ist das nicht nötig.
## Troubleshooting
- **Login-Redirect-Loop / `proxy.ts` verhält sich falsch:** meist falsche
`NEXT_PUBLIC_SUPABASE_URL`/`ANON_KEY` Image neu bauen (siehe oben, diese
Werte sind eingebacken).
- **Cron läuft nicht:** `docker compose logs cron` prüft, ob
`/etc/crontabs/root` korrekt geschrieben wurde und ob `CRON_SECRET` in
`.env` gesetzt ist (leer/fehlend führt serverseitig zu `401`).
- **Healthcheck rot:** `docker compose logs app` meist fehlende/falsche
Supabase-Env-Variablen zur Laufzeit (`SUPABASE_SERVICE_ROLE_KEY`,
Server-Komponenten).