Die Schnellerfassung braucht ein Feld und einen Knopf; wer beim Erfassen
erst ein Formular ausfüllen muss, erfasst nichts. Der Fokus bleibt danach
im Feld, damit mehrere Sachen hintereinander hineingehen.
Die Filter der Aufgabenliste stehen in der Adresse, nicht im Zustand der
Komponente — so lässt sich eine gefilterte Liste als Lesezeichen ablegen,
und der Zurück-Knopf tut, was er soll.
Die Startseite zeigt jetzt, was überfällig, heute oder morgen fällig ist,
statt nur die Bereiche aufzuzählen.
Für die Auswahlfelder bewusst das native <select>: es bedient sich auf
dem Handy mit dem Systemdialog und ist ohne Zusatzcode tastaturbedienbar.
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>
Aufgabe mit Typ, Status, Priorität, Aufwand, Fälligkeit und Herkunft;
Projekt als eigene Klammer darum.
Zwei Entwurfsentscheidungen:
- Die Inbox ist kein eigener Zustand, sondern schlicht eine Aufgabe ohne
Bereich. Einsortieren heißt: einen Bereich setzen. Ein zusätzliches
Feld könnte mit der Zuordnung auseinanderlaufen.
- Projekt ist ein eigenes Modell, kein Aufgabentyp. Ein Projekt hat
selbst weder Aufwand noch Fälligkeit, sondern leitet beides aus dem
ab, was drinsteckt. ARBEITSPAKET und MEILENSTEIN sind Typen der
Aufgaben *innerhalb* eines Projekts.
@@unique([quelle, quelleRef]) ist die Grundlage für die Integrationen ab
M6: sie upserten darüber, statt zu doppeln. Von Hand erfasste Aufgaben
tragen quelleRef = null; PostgreSQL sieht NULL-Werte als verschieden an,
sie behindern sich also nicht.
Verworfene Aufgaben zählen bewusst gar nicht in den Fortschritt eines
Projekts — sonst sähe es fertig aus, weil die Hälfte gestrichen wurde.
Fälligkeit wird in Kalendertagen in Europe/Berlin gerechnet, nicht in
24-Stunden-Schritten: was heute um 23:00 fällig ist, ist heute fällig.
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>
/api/health meldet Status, Version, Instanz und Erreichbarkeit der
Datenbank – ohne Anmeldung, deshalb ohne Zählstände, Namen oder Adressen.
update.sh wird sich später darauf stützen.
Der Seed legt die vier Bereiche an und ein erstes Verwaltungskonto aus
ADMIN_EMAIL und ADMIN_PASSWORT. Er läuft mehrfach ohne Schaden; ohne
ADMIN_PASSWORT entsteht ein Konto, das sich nur über OIDC oder Passkey
anmelden kann.
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>
Deutsche Oberfläche, Hell und Dunkel über next-themes, Navigation mit
Rollenfilter und die shadcn-Primitiven, die M1 braucht.
Der Farbschema-Knopf kommt ohne Effekt und ohne Zustand aus: welches
Symbol gilt, entscheidet die dark-Klasse und damit CSS. next-themes setzt
sie schon vor der Hydratation, deshalb blitzt nichts auf — und die
React-Compiler-Regel gegen setState im Effekt bleibt eingehalten.
Farben als OKLCH, damit Hell und Dunkel dieselbe wahrgenommene Helligkeit
halten.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Tagesgrenzen, Wochenbeginn am Montag und Wochentagsindex passend zu den
Kapazitätsfeldern eines Bereichs. Alle Funktionen bekommen den Zeitpunkt
als Parameter, lesen also nie selbst die Uhr.
Die Tests laufen bewusst in UTC, damit sofort auffällt, wenn irgendwo die
Ortszeit des Servers durchschlägt; sie decken beide Zeitzonen-Halbjahre
und die Umstellungswoche ab.
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>