Alle 15 Minuten abgleichen, nachts alte Termine und abgelaufene
WebAuthn-Herausforderungen entfernen. Ohne das Aufräumen bliebe der
Terminspiegel für immer stehen.
Der Riegel über globalThis ist nötig, weil Next im Entwicklungsbetrieb
Module mehrfach lädt — sonst liefen nach ein paar Änderungen ein Dutzend
Zeitpläne parallel. Im Demo-Modus starten gar keine Jobs: die
Screenshot-Instanz soll sich nicht selbst verändern, während sie
abfotografiert wird.
Protokolliert werden nur Zahlen, nie Kontonamen oder Termininhalte.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Verbinden, umbenennen, von Hand abgleichen und trennen. Alle Aktionen
arbeiten nur auf Konten des angemeldeten Benutzers — die Kennung geht in
die where-Bedingung, nicht in einen nachträglichen Vergleich.
Ein Anbieter erscheint nur, wenn die Instanz ihn erlaubt *und* er
konfiguriert ist. Ist er erlaubt, aber nicht konfiguriert, sagt die Seite
das ausdrücklich, statt den Knopf wortlos wegzulassen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Verbinden, Token erneuern und seitenweise abrufen — mit blankem fetch,
ohne SDK. Es geht um drei Aufrufe je Anbieter, und die Antworten werden
ohnehin mit zod geprüft; ein SDK brächte vor allem Abhängigkeiten und
blinde Flecken.
Entscheidungen:
- Der Zustandswert im OAuth-Ablauf ist mit AUTH_SECRET signiert und trägt
die Benutzerkennung. Ohne das ließe sich ein fremdes Kalenderkonto an
ein beliebiges Konto hängen.
- Die Berechtigungen schließen das Schreiben ein, obwohl M3 nur liest.
Sonst müsste ab M4 jedes Konto neu zustimmen — und die Zustimmung ist
genau der Schritt, an dem Einrichtung scheitert.
- Läuft der Abgleichverweis ab (beide Anbieter antworten mit 410), wird er
verworfen und von vorn gelesen. Der Ablauf ist so gebaut, dass ein
vollständiger Durchlauf jederzeit möglich ist.
- Googles syncToken trägt die Zeitgrenzen nicht mit; Folgeabgleiche
liefern also auch Änderungen außerhalb unseres Fensters. Was daneben
liegt, wird verworfen — sonst wüchse der Spiegel unbegrenzt.
Die Instanzregel greift serverseitig: Google gibt es nur daheim, in der
Firmeninstanz liefert die Route 404 statt eines Hinweises.
Fehlermeldungen nennen nie den Antwortkörper des Token-Endpunkts — der
enthält das Client-Secret oft im Echo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Kalenderkonto mit Tokens und Abgleichverweis, Termin als örtlicher Spiegel
mit genau so viel, wie die Belegungsrechnung und die Wochenansicht
brauchen. Bereich bekommt sein Standard-Kalenderkonto für M4.
Die Tokens liegen verschlüsselt (AES-256-GCM). Wer die Datenbank oder eine
Sicherung in die Hände bekommt, hätte sonst direkten Zugriff auf fremde
Kalender — das wiegt schwerer als bei den OIDC-Tokens aus M1, die nur die
Anmeldung betreffen. Der Schlüssel wird aus AUTH_SECRET abgeleitet statt
separat konfiguriert: eine weitere Pflichtvariable wäre eine weitere
Stelle, an der die Einrichtung scheitert.
kalenderId ist nicht optional, sondern trägt den Wert "standard" für den
Standardkalender — Prisma lässt in einem zusammengesetzten Unique-Schlüssel
kein null zu.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Fehler fiel erst in den aufgenommenen Bildern auf: Ideen standen
mitten in der Aufgabenliste, obwohl der ganze Sinn der getrennten
Ideenseite ist, dass sie das nicht tun. Ohne Termin hätten sie dort
entweder unsichtbar unten gestanden oder mit erfundenem Termin oben.
Der Filter kennt jetzt ohneIdeen; ein ausdrücklich gewählter Typ gewinnt,
wer nach Ideen filtert, sieht sie also weiterhin.
Die Startseite filterte Ideen bisher von Hand nach dem Laden — das
erledigt jetzt dieselbe Abfrage wie überall sonst.
Ein E2E-Test hält den Fall fest.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Anlegen, ändern, Status setzen, einsortieren, aus einer Idee eine Aufgabe
machen und löschen — jede Aktion prüft die Rolle selbst und hinterlässt
einen Audit-Eintrag.
Statuswechsel gehen nur die Wege, die uebergangErlaubt() zulässt:
Abgeschlossenes wird erst wieder geöffnet, bevor es weitergeht. Wer per
Formular etwas anderes einreicht, bekommt eine Meldung statt eines stillen
Zustandssprungs.
Sortiert wird in der Anwendung, nicht in SQL. Die Regel ist mehrstufig
und steht als geprüfte reine Funktion in src/lib — zwei Fassungen
derselben Regel wären eine Fehlerquelle.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Zwei Mängel, die erst in den aufgenommenen Bildern auffielen.
Auf Handybreite brach die Kopfleiste um und drängte den Namen der
Anwendung in die zweite Zeile, mitten zwischen die Menüpunkte. Sie bleibt
jetzt auf allen Breiten einzeilig; wird es eng, scrollt die Navigation
waagerecht.
Das Tagesdatum auf der Startseite wird serverseitig gerendert, weshalb
Playwrights Uhr nicht griff — die Bilder hätten sich täglich geändert. In
der Screenshot-Instanz (DEMO_MODUS=true) steht die Uhr jetzt fest. Reine
Funktionen bekommen den Zeitpunkt weiterhin als Parameter; src/server/
zeitgeber.ts ist die eine Stelle, an der er entsteht.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Next 16 hat die Dateikonvention "middleware" abgekündigt und in "proxy"
umbenannt; der Bau warnte bei jedem Lauf. Umbenannt sind Datei und
Funktion, die Aufgabe bleibt dieselbe.
Die E2E-Tests laufen unverändert durch: die Umleitung samt gemerktem Ziel
funktioniert weiter.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dreistufiges Dockerfile mit standalone-Ausgabe; im Laufzeitabbild liegen
weder Quelltext noch Bauwerkzeuge, und es läuft unter einem eigenen
Benutzer.
Die Migrationen erledigt ein eigener Einmaldienst aus der Bau-Stufe. Der
Versuch, nur die Prisma-CLI ins Laufzeitabbild zu kopieren, scheiterte an
ihren transitiven Modulen; sie alle mitzunehmen hätte das Abbild
aufgebläht. Der Dienst läuft vor der App und muss sauber durchlaufen.
Zwei Stellen mussten faul werden, weil Next beim Bauen Seitendaten
sammelt und dabei Module lädt, die es zur Bauzeit noch ohne
Umgebungsvariablen gibt:
- Der Prisma-Client entsteht erst beim ersten Zugriff, über einen Proxy.
- Die Auth.js-Konfiguration wird als Funktion übergeben.
Ebenso liest prisma.config.ts die URL über process.env statt über env(),
das beim Bauen ohne Datenbank abbricht.
Dazu backup.sh, update.sh mit Rückrollpfad und scripts/release.sh. Der
Dump wurde nachweislich in eine Wegwerf-Datenbank zurückgespielt und
stimmt zeilengenau mit dem Original überein. Alle Skripte sind
shellcheck-sauber.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Der Integrationstest prüft gegen eine echte Datenbank, was Vitest nicht
sieht: Eindeutigkeit, Fremdschlüssel, Kaskaden und dass das Protokoll den
gelöschten Benutzer überlebt. Er läuft bewusst ohne umschließende
Transaktion — er prüft absichtlich Fälle, die scheitern müssen, und ein
gescheiterter Befehl bricht in PostgreSQL die ganze Transaktion ab.
Stattdessen tragen alle Testdaten dieselbe Kennung und werden am Ende
wieder entfernt.
Der Migrationstest legt eine Wegwerf-Datenbank an, spielt alle
Migrationen ein und vergleicht das Ergebnis mit dem Schema. Dass er
Abweichungen wirklich bemerkt, wurde mit einem probeweise ergänzten Feld
nachgewiesen.
Der E2E-Rauchtest deckt den Weg ab, der in M1 trägt: Umleitung ohne
Anmeldung samt gemerktem Ziel, Abweisung falscher Zugangsdaten,
erfolgreiche Anmeldung und die Verwaltungsseite.
Dafür neu: eine Middleware, die nur den angefragten Pfad als Kopfzeile
durchreicht. Ein Layout kennt seinen Pfad nicht, braucht ihn aber für die
Rückkehr nach der Anmeldung. Anmeldelogik steht dort bewusst nicht — die
Middleware läuft im Edge-Umfeld ohne Prisma.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bereiche mit Farbe, Arbeitsfenster und Tageskapazität – die erste
schreibende Funktion und damit der Beleg, dass die Kette trägt: zod prüft
jede Eingabe, die Rolle wird in der Seite und noch einmal in der Server
Action geprüft, jede Änderung hinterlässt einen Audit-Eintrag.
Die Audit-Details werden vor dem Speichern gesäubert: Felder, deren Name
nach Passwort, Token, Betreff oder Mailinhalt aussieht, werden ersetzt.
Das gilt auch in verschachtelten Objekten und Listen.
Dazu die Protokollseite für die Verwaltung und die Startseite.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Drei Wege hinein, je Instanz zuschaltbar: generischer OIDC-Anbieter
(Entra ID beziehungsweise authentik), Passkeys und wahlweise E-Mail mit
Passwort.
Entscheidungen:
- Passkeys laufen direkt über SimpleWebAuthn 14, nicht über den
experimentellen WebAuthn-Provider von Auth.js, der noch auf Fassung 9
festgelegt ist.
- Eigener Prisma-Adapter, weil der mitgelieferte fest auf prisma.user und
prisma.account zugreift, unsere Modelle aber Benutzer und Konto heißen.
- Sitzungen als JWT, weil Credentials-Provider das verlangen. Damit eine
Sperre trotzdem sofort wirkt, liest der session-Callback Rolle und
Status bei jeder Anfrage frisch aus der Datenbank. Die Tabellen sitzung
und verifizierungs_token entfallen deshalb wieder.
- allowDangerousEmailAccountLinking ist bewusst an: je Instanz gibt es
genau einen vertrauenswürdigen Anbieter, der die Adresse selbst prüft.
Anmeldung, Passwort und Passkey antworten bei Fehlschlag immer gleich,
damit sich nicht abfragen lässt, wer hier ein Konto hat. Die
WebAuthn-Herausforderung wird nach einem Versuch verworfen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
INSTANZ entscheidet, welche Kalenderanbieter, Postfächer und
Integrationen es gibt. Was eine Instanz nicht darf, liefert dort 404
statt eines Hinweises. Ein falsch geschriebener Wert bricht ab, statt
still auf einen Standard zu fallen.
Die Konfiguration wird mit zod geprüft; Fehlermeldungen nennen den
Variablennamen, nie den Wert.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Benutzer mit Rolle, OIDC-Konten, Sitzungen, Passkeys samt kurzlebiger
WebAuthn-Herausforderung, Bereiche mit Tageskapazität und Arbeitsfenster
sowie ein unveränderlicher Audit-Eintrag je schreibender Aktion.
Aufgabe und Zeitblock kommen mit M2 und M4, Kalenderkonto mit M3 — dann
mit den Feldern, die der jeweilige Provider wirklich braucht.
Prisma 7 nimmt die Verbindung nicht mehr aus dem Schema entgegen: die URL
steht in prisma.config.ts, der Client bekommt den pg-Treiberadapter.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>