Andrei Laas 89ba34c901
Some checks failed
CI / Migrationen auf leerer Datenbank (push) Has been cancelled
CI / Lint, Typen, Tests, Build (push) Has been cancelled
Vorgemerkte Planstellen, Stichtag fuer Einheiten, Kuendigungsschutz und Elternteilzeit
F.24 -- eine Planstelle, auf die eine Versetzung vorgemerkt ist, galt als frei.
Die Vormerkung steht in pending_org_changes, "besetzt?" wurde allein an
position_assignments gefragt. Auffallen wuerde es erst in der Nacht des
Wirksamkeitstages: dort legt der Nachtlauf die zweite Besetzung an und laeuft in
den Teilindex, der genau eine laufende Besetzung je Planstelle zulaesst. Er
arbeitet in einer Transaktion ueber alle faelligen Vorgaenge -- eine einzige
solche Buchung haette ihn vollstaendig zum Stehen gebracht, auch fuer alle
anderen. Neue Auskunft planstelle_vorgemerkt, abgefragt in hire_employee,
rehire_employee, transfer_employee und promote_employee, mit Datum in der
Meldung. Die eigene Vormerkung zaehlt nicht als Hindernis.

D.06 -- die Struktursicht zeigte Einheiten, die es am gewaehlten Stichtag noch
nicht gab: org_units wurde ungefiltert gelesen, waehrend Planstellen und
Besetzungen daneben auf den Stichtag eingeschraenkt waren. Die Druckansicht
filterte schon immer richtig; jetzt tun es alle drei Stellen.

H.09 -- der Kuendigungsschutz stand in der Akte nur als "bis TT.MM.JJJJ",
obwohl Personenkreis, Beginn, Ende und die ganze Begunstigung erfasst werden.
Der Personenkreis ist die eigentliche Auskunft. Die Behinderung bekommt eine
eigene Zeile: sie fuehrt oft zu Kuendigungsschutz, ist aber ein eigener
Bescheid.

Elternteilzeit und Wiedereingliederungsteilzeit sind jetzt auch bei der
Stundenaenderung waehlbar. Sie standen nur bei der Rueckkehr aus einer
Abwesenheit, mit der Begruendung, dass sie typischerweise dann beginnen --
typischerweise ist aber nicht immer. Die Ueberschneidung der beiden Listen ist
damit gewollt; der Test prueft nicht mehr auf Partition, sondern darauf, dass
keine Teilzeit an keinem der beiden Wege haengt.
2026-09-29 19:10:31 +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%