Maximilian Stubhan 779d6116b2 Add the Honestly survey export to Berichte
A participant list for the employee survey on honestly.de, offered as
CSV and Excel right under the full data export. Columns: Personalnummer,
Email, Firstname, Last Name, Language, Location, OU, OU+1, ... , Role.
Language is always "de", Role always "Respondee".

The org columns run bottom-up: OU is the person's own unit, OU+1 the one
above, up to the top node. Their number follows the deepest chain in
the file, shorter chains are padded with empty cells, and there is
always at least one OU column so the file keeps its shape.

It exports exactly the people the full export would, with the same
filters. To guarantee that, the selection moved out of the full export's
route into lib/export-auswahl.ts and both routes use it; the full
export's columns are untouched.

Email is employees.email, which is the private address and optional --
there is no work address in the schema. Missing addresses stay empty
rather than getting a placeholder that would receive an invitation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 15:10:43 +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%