Andrei Laas a6b6a7d67c
All checks were successful
CI / Lint, Typen, Tests, Build (push) Successful in 11m30s
CI / Migrationen auf leerer Datenbank (push) Successful in 10m22s
Eine Organisationseinheit aus dem Organigramm heraus anlegen
Bisher gab es dafuer nur den Import. Jetzt sitzt auf jeder Einheit im
Organigramm -- in der Grafik wie in der Liste -- ein Plus, das eine
untergeordnete Einheit anlegt, wahlweise gleich mit Leitungsplanstelle.

Bewusst nur dieser eine Fall. Umbenennen, verschieben und schliessen fehlen
nicht aus Zeitmangel: org_units.parent_id traegt kein Datum, ein Verschieben
aenderte damit auch jede Auswertung auf einen vergangenen Stichtag, und der
fruehere Stand waere danach nirgends mehr ablesbar. Anlegen stellt diese Frage
nicht, weil vorher nichts da war -- und es kann dabei auch kein Kreis
entstehen, was hier mehr wiegt als es klingt: die rekursive Abfrage in
om_reporting_lines hat weder Tiefenbegrenzung noch Kreiserkennung.

Die Leitungsplanstelle entsteht ueber create_position statt durch eine zweite
Fassung derselben Logik -- dort haengen Jobkatalog, Nummernvergabe und die
Pruefung "je Einheit genau eine Leitung".

Die Orgnummer wird eingetragen, nicht vergeben. Vorgeschlagen wird die naechste
freie Nummer der bestehenden Reihe, und nur dann, wenn sich im Bestand genau
eine Systematik ablesen laesst -- eine plausibel aussehende, aber erfundene
Nummer prueft niemand nach.

Das Datum kommt nicht aus dem Stichtag der Ansicht. Wer sich die Struktur zum
letzten Jahresende ansieht und auf Plus drueckt, will in aller Regel eine
Einheit von heute anlegen.
2026-09-28 16:28:33 +02:00
2026-09-07 10:43:22 +02:00
2026-09-07 10:43:22 +02:00
2026-09-07 10:43:22 +02:00
2026-09-07 10:43:22 +02:00
2026-09-07 10:43:22 +02:00
2026-09-07 10:43:22 +02:00

Alpenwerk HR Master

Interne HR-Stammdatenverwaltung: Mitarbeiter:innen, Organisationsstruktur (Bereich/Abteilung/Team), Planstellen, Neueinstellungen, Versetzungen/ Beförderungen/Karenz, Reorganisationen und der zugehörige Audit-Trail.

Zweck

Die App ersetzt Excel-basierte HR-Stammdatenpflege durch ein Werkzeug mit verbindlichen Regeln (z. B. wirksame Daten statt sofortiger Änderungen, eindeutige Positions-/Org-Nummern, verpflichtende Historie) und einem lückenlosen Audit-Trail für jede Änderung.

Alle Mitarbeiterdaten in diesem System sind vertraulich — Stammdaten, Verträge, Sozialversicherungsnummern, Angehörige und Audit-Daten. Zugriff ist auf explizit aktivierte HR-Benutzer:innen beschränkt (siehe Sicherheitsprinzipien).

Tech-Stack

  • Next.js 16 (App Router) — Achtung: Next.js 16 hat Breaking Changes gegenüber älteren Versionen (u. a. proxy.ts statt middleware.ts). Vor Änderungen an Framework-nahen Dateien die lokalen Docs unter node_modules/next/dist/docs/ konsultieren; sie sind maßgeblich, ältere Anleitungen im Netz beschreiben teils überholte APIs.
  • React 19, TypeScript
  • PostgreSQL, angesprochen über Kysely und pg — die Datenhaltung liegt in der Datenbank, nicht im Next.js-Prozess. Der Zugriff läuft ausschliesslich über withUser() (lib/db/), das den Sitzungskontext setzt.
  • Auth.js gegen Microsoft Entra ID — keine eigenen Passwörter.
  • Tailwind CSS v4
  • Vitest — Unit- (Node), Komponenten- (jsdom) und Integrationstests (gegen eine erreichbare Datenbank)

Setup

npm install
cp .env.example .env.local   # Werte eintragen, siehe unten
npm run dev

Für eine lokale Datenbank genügt der Container aus docker-compose.yml:

docker compose up -d db
docker compose run --rm migrate     # spielt db/migrations/ ein

Danach zeigt DATABASE_URL in .env.local auf diese Datenbank.

Umgebungsvariablen

Siehe .env.example für die vollständige, kommentierte Liste. Kurzfassung:

Variable Sichtbarkeit Zweck
DATABASE_URL Nur Server PostgreSQL-Verbindung. Die Rolle darf kein BYPASSRLS haben
DATABASE_SSL Nur Server false für lokal/CI ohne TLS
AUTH_SECRET Nur Server Signiert und verschlüsselt das Sitzungscookie
AUTH_MICROSOFT_ENTRA_ID_ID Nur Server Anwendungs-ID der Entra-Registrierung
AUTH_MICROSOFT_ENTRA_ID_SECRET Nur Server Client-Geheimnis dazu
AUTH_MICROSOFT_ENTRA_ID_ISSUER Nur Server Aussteller mit Mandanten-ID — nicht common
CRON_SECRET Nur Server Schützt /api/cron/apply-pending-changes

Es gibt keine NEXT_PUBLIC_*-Variablen mehr. Nichts wird in das Browser-Bundle eingebacken, weil der Browser mit nichts ausser der Anwendung selbst spricht. Ein Docker-Abbild ist damit umgebungsneutral: einmal gebaut, überall dasselbe — vorher brauchte jede Umgebung ihr eigenes.

Scripts

Befehl Zweck
npm run dev Lokaler Dev-Server
npm run build Produktions-Build
npm run start Produktions-Server (nach build)
npm run lint ESLint (eslint-config-next, Flat Config)
npm run typecheck tsc --noEmit
npm run test Vitest, Unit-Tests (tests/unit/**)
npm run test:integration Vitest gegen eine erreichbare Datenbank; überspringt sich ohne DATABASE_URL
npm run test:e2e Playwright
npm run migrate Ausstehende Migrationen einspielen (--dry-run, --baseline)
npm run check lint + typecheck + types:check + test + build in Folge — dieselben Schritte wie der CI-Job „check"

Sicherheitsprinzipien

  • RLS ist die eigentliche Schranke, nicht die UI. Jede Tabelle hat Row Level Security aktiv; proxy.ts (App-Ebene) ist Defense-in-Depth, keine Ersatzkontrolle.
  • Ein Rollenmodell: profiles.role = 'hr' + profiles.is_active = true, geprüft über die SQL-Funktion is_hr_user(). Kein Sub-Rollensystem — siehe docs/datenkatalog.md.
  • Es gibt keinen privilegierten Zugang mehr. Der Dienstschlüssel, der RLS aushebelte, ist ersatzlos entfallen; auch der nächtliche Lauf benutzt dieselbe Rolle ohne BYPASSRLS. Was ohne angemeldete Person laufen muss, steht als SECURITY DEFINER-Funktion in der Datenbank und prüft dort selbst, was es tut.
  • Jede Abfrage läuft in einer Transaktion mit gesetztem Sitzungskontext. Die Kysely-Instanz wird nicht exportiert — der einzige Weg an die Datenbank ist withUser() (lib/db/index.ts), und eine ESLint-Regel verbietet den Import von pg ausserhalb von lib/db/.
  • Audit-Log ist transaktional in der Datenbank, nicht im App-Code: jede mutierende SQL-Funktion schreibt ihren audit_log-Eintrag in derselben Transaktion wie die Änderung selbst. Details und Prüfung siehe docs/security-review.md.
  • Historie ist append-only (employee_history, audit_log) — RLS erlaubt kein update/delete. Korrekturen sind kompensierende Einträge.

Cron-Konfiguration

/api/cron/apply-pending-changes wendet wirksam gewordene, zukunftsdatierte Änderungen an (pending_org_changes → apply_due_pending_changes()).

  • Den Zeitplan hält der cron-Container aus docker-compose.yml: täglich 03:00 Uhr, mit Authorization: Bearer <CRON_SECRET>.
  • Fehlt CRON_SECRET oder stimmt der Header nicht, antwortet die Route mit 401 (nicht 500 — bewusst, siehe tests/unit/security.test.ts).

Datenbank

  • Schema-Quelle der Wahrheit: db/migrations/. Menschlich lesbare Fassung, aus der laufenden Datenbank erzeugt: docs/datenkatalog.md.
  • Migrationen einspielen: npm run migrate. Der Läufer wendet nur die fehlenden Dateien an, jede in ihrer eigenen Transaktion, und führt darüber Buch in migrationen.schema_migrations.
  • lib/types.ts wird von Hand gepflegt. npm run types:check hält sie Spalte für Spalte gegen die Migrationen.

Testing

  • npm run test — schnell, keine externen Abhängigkeiten, läuft in CI.
  • npm run test:integration — braucht eine erreichbare Datenbank (DATABASE_URL). Geprüft werden Regeln, die es zweimal gibt: einmal als SQL, einmal als TypeScript. Ohne DATABASE_URL überspringen sich die Dateien, statt mit einem Verbindungsfehler abzubrechen.
  • npm run test:e2e — Playwright gegen einen laufenden Dev-/Preview-Server.

Deployment

Siehe DEPLOYMENT.md: Anwendung und Datenbank als Container, Reverse-Proxy/TLS, der nächtliche Lauf, Migrationen und Updates.

Known TODOs vor Produktivbetrieb

  • Content-Security-Policy läuft im Nur-Bericht-Modus (next.config.ts). Erzwungen wird sie erst, wenn die Meldungen sauber sind — eine geratene, erzwungene Richtlinie blendet die Anwendung für alle aus.
  • Seed für die Integrationstests fehlt. Er lag in der abgelösten Umgebung. Zwei der drei Integrationstests vergleichen Regeln über den gesamten Bestand und brauchen dafür Daten; bis der Seed nachgezogen ist, laufen sie nur gegen eine bereits befüllte Datenbank, nicht in der CI.
  • Sicherheitsprüfung des heutigen Aufbaus steht aus — siehe docs/security-review.md.
  • Bildschirmfotos unter .scratch_shots/ stammen aus einer früheren Handprüfung. Sie sind über .gitignore ausgeschlossen; vor der Übergabe durchsehen und löschen.
  • Kein granulareres Rollenmodell — aktuell HR-only (alles-oder-nichts). Falls z. B. eine reine Lese-Rolle künftig gebraucht wird, gehört die Erweiterung in eine neue Migration (is_hr_user()/RLS-Policies), nicht in App-seitigen Code.
Description
No description provided
Readme 3.8 MiB
Languages
TypeScript 57.2%
PLpgSQL 41.7%
JavaScript 0.7%
CSS 0.3%