Farben, Schrift, Logo und Symbole folgen jetzt dem CD Manual 2022. An der Logik aendert sich nichts: keine Migration, keine Abfrage, keine Berechtigung. Die zwei Markenfarben stehen in §1.1 als #F69686 (Rosa) und #164194 (Blau). Welche davon was traegt, ist nicht gewaehlt, sondern nachgerechnet: Weiss auf Rosa kommt auf 2.18:1 und faellt damit auch fuer Grossschrift durch, Blau auf Rosa auf 4.34:1 und reicht fuer das Logo, nicht fuer Text. Schwarz auf Rosa sind 7.47:1, Weiss auf Blau 9.47:1. Rosa ist deshalb Flaeche, Blau ist Interaktion — zwei Skalen, brand-* und accent-*, statt einer geteilten. Von den 26 Stellen mit brand-50/100/200 sind nur sieben auf accent gewandert. Der Rest meint einen Zustand und keinen Markenton: die Zeile unter dem Zeiger, der gewaehlte Listeneintrag, der aktive Menuepunkt. Gegen die warme Seitenflaeche ist ein kuehler Ton dort deutlicher, und Blau heisst in dieser Oberflaeche ab jetzt "reagiert auf dich". Nach accent gingen die Faelle ohne eigene Bedeutung — "Geplant", "Offen", der neutrale Protokoll-Chip — und die offene Planstelle im Organigramm, die vorher ein blasses Blau war und damit wie eine schwaechere Person aussah. Die Funktionsfarben bleiben, was sie sind. Das Manual regelt die Identitaet, nicht die Rueckmeldung: Rot heisst Fehler, weil die Benutzerin das mitbringt. `info` bleibt bewusst tuerkis — Blau saehe ab jetzt bedienbar aus, und gemessen kaeme ein blaues info dem violetten Chip auf dE 6.8 nahe, also nicht unterscheidbar. Violett ist dabei nachgezogen: gegen den Fehler-Chip stand es bei dE 8.0, "Austritt" und "Befoerderung" waren im Vorbeigehen dieselbe blasse Flaeche. Jetzt dE 15.0. Die Schrift ist Barlow. Nachgezaehlt ist das Manual zu 95 % in DIN gesetzt (Regular 79 %, Bold 16 %); Helvetica Neue steht nur in den Visitenkarten und im Claim. DIN laesst sich nicht ausliefern — eine Drucklizenz deckt keinen Webfont —, und die freien Nachbauten der DIN 1451 sind Schilderschriften, bei 14 px in einer langen Tabelle schlechter lesbar als das, was sie ersetzen. Die Variable heisst --font-din und nicht --font-barlow: liegt eines Tages eine Web-Lizenz vor, ist der Wechsel diese eine Deklaration. Nebenbei zwei Dinge repariert, die vorher schon falsch waren. Die Umrandung von Eingabefeldern, Knoepfen und Suchfeldern lag bei 1.30:1 und damit unter den 3:1, die WCAG 1.4.11 fuer Bedienelemente verlangt; border-strong bringt 3.56:1. Und das mitgelieferte favicon.ico liess sich gar nicht bauen: eingebettet waren 24-Bit-RGB-PNG, waehrend der Kopf 32 bpp behauptete. Alle Rastersymbole liegen jetzt als RGBA vor, und ihr Blau ist auf den CD-Wert gezogen — samt der kantengeglaetteten Raender, indem je Pixel der Blauanteil bestimmt und neu gemischt wurde. Das Logo ist das Markenlogo (§4.1), nicht das Unternehmenslogo, das §3.2 fuer eine Anwendung mit der AG als Absender vorsaehe — es liegt nicht vor. Der Freiraum X/3 steckt im Bauteil selbst und nicht in den Aufrufstellen, sonst haengt seine Einhaltung daran, dass jede einzelne daran denkt. docs/farbschema.html zeigt Token, Kontraste und Bauteile nebeneinander und laesst sich ohne Server oeffnen. Nicht im Browser gesehen: Anmeldung laeuft ueber das Firmenkonto und die Datenbank ist von hier nicht erreichbar. Lint, Typen, Schemaabgleich, 524 Tests und der Build sind sauber. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.tsstattmiddleware.ts). Vor Änderungen an Framework-nahen Dateien die lokalen Docs unternode_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 überwithUser()(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-Funktionis_hr_user(). Kein Sub-Rollensystem — siehedocs/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 alsSECURITY 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 vonpgausserhalb vonlib/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 siehedocs/security-review.md. - Historie ist append-only (
employee_history,audit_log) — RLS erlaubt keinupdate/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 ausdocker-compose.yml: täglich 03:00 Uhr, mitAuthorization: Bearer <CRON_SECRET>. - Fehlt
CRON_SECREToder stimmt der Header nicht, antwortet die Route mit401(nicht500— bewusst, siehetests/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 inmigrationen.schema_migrations. lib/types.tswird von Hand gepflegt.npm run types:checkhä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. OhneDATABASE_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.gitignoreausgeschlossen; 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.