Ziel, Erledigtes, die Entscheidungen mit Begründung, die Probleme samt Lösung und die offenen Punkte — allen voran, dass der Abgleich nicht gegen die echten APIs getestet werden konnte. Dauer laut zeiten.csv: 41 Minuten. Gesamt bislang 246 Minuten. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
18 KiB
Entwicklungstagebuch
Neuester Eintrag oben. Die Dauer je Sitzung stammt aus devlog/zeiten.csv,
die Übersicht aus scripts/zeit.sh summe.
Zeit je Meilenstein
| Meilenstein | Art | Minuten | Stunden |
|---|---|---|---|
| M1 | claude | 161 | 2,68 |
| M2 | claude | 44 | 0,73 |
| M3 | claude | 41 | 0,68 |
| Gesamt | 246 | 4,10 |
2026-09-10 – M3: Kalender lesen
Dauer: 41 Minuten (17:48–18:29), Art claude.
Ziel der Sitzung: Termine aus Microsoft 365 und Google lesen, damit der Planer weiß, wann überhaupt Platz ist.
Erledigt
KalenderkontoundTerminsamt Migration; Bereich bekommt sein Standard-Kalenderkonto für M4- Tokenverschlüsselung mit AES-256-GCM
- Antworten beider Anbieter mit zod geprüft und in eine gemeinsame Form gebracht, geprüft gegen anonymisierte Beispielantworten
- Belegungsrechnung: Kapazität gegen Termine, freie Fenster für M4
- OAuth-Verbinden, Tokenerneuerung, seitenweiser Abgleich mit Rückfall auf einen vollständigen Durchlauf
- Seite für die Kalenderkonten, Belegung des Tages auf der Startseite
- Hintergrundjobs für Abgleich und nächtliches Aufräumen
- 68 neue Unit-Tests, 10 neue Integrationsprüfungen
- Handbuchseite und Einrichtungsanleitung für Entra ID und Google
Entscheidungen mit Begründung
Tokens verschlüsselt, Schlüssel aus AUTH_SECRET abgeleitet. Wer die Datenbank oder eine Sicherung bekommt, hätte sonst direkten Zugriff auf fremde Kalender — das wiegt schwerer als bei den OIDC-Tokens aus M1, die nur die Anmeldung betreffen. Eine eigene Pflichtvariable wäre eine weitere Stelle, an der die Einrichtung scheitert; die Ableitung über HKDF kostet nichts und hat die richtige Nebenwirkung: wer AUTH_SECRET wechselt, muss die Konten neu verbinden — dann sind ohnehin alle Sitzungen ungültig.
Bei fremder Zeitzone abbrechen statt raten. Graph liefert Datum und Zeitzone getrennt, ohne Versatz im Text. Wir fragen ausdrücklich UTC an; kommt etwas anderes zurück, wäre jede Umrechnung geraten und würde Termine um Stunden verschieben. Ein Abbruch mit klarer Meldung ist besser als ein Kalender, der leise falsch liegt.
Unbekanntes showAs zählt als belegt. Lieber eine Stunde zu viel blockiert als eine Doppelbuchung.
Schreibrechte schon jetzt anfragen. M3 liest nur, aber M4 schreibt. Später erneut zuzustimmen ist genau der Schritt, an dem Einrichtung scheitert — einmal breiter fragen ist das kleinere Übel.
Ganztägige Termine zählen nicht zur Belegung. Ein Urlaubstag oder Geburtstag blockiert sonst den ganzen Tag, obwohl er keine Arbeitszeit kostet.
Alle Kalender zählen gegen jeden Bereich. Ein Termin blockiert die Zeit ohnehin, gleich zu welchem Lebensbereich er gehört.
Kein SDK für Graph und Google. 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.
Probleme und Lösungen
| Problem | Lösung |
|---|---|
| Prisma lässt in einem zusammengesetzten Unique-Schlüssel kein null zu | kalenderId ist nicht optional, sondern trägt den sprechenden Wert "standard" für den Standardkalender. |
Googles syncToken verträgt sich nicht mit timeMin/timeMax |
Folgeabgleiche liefern auch Änderungen außerhalb des Fensters. Was daneben liegt, wird beim Spiegeln verworfen — sonst wüchse der Spiegel unbegrenzt. |
| Der Verweis für die nächste Runde kann ablaufen (HTTP 410) | Verweis verwerfen, Spiegel leeren, von vorn lesen. Der Ablauf ist so gebaut, dass ein vollständiger Durchlauf jederzeit möglich ist. |
| Die Shell zerlegte in mehreren Anläufen Template-Literale und Backticks | Auf node - <<'SKRIPT' umgestellt; damit geht der Text unverändert durch. |
Offene Punkte
- Nicht gegen die echten APIs getestet. Ich habe keine Zugangsdaten für Entra ID oder Google. Provider und Abgleich sind gegen anonymisierte Beispielantworten geprüft; den ersten echten Verbindungsversuch muss Florian machen. Die Anleitung dafür steht in der README.
- Nur der Standardkalender je Konto. Mehrere Kalender desselben Kontos sind im Datenmodell vorgesehen, es fehlt die Auswahl in der Oberfläche.
- Der Abgleich läuft in einer Schleife über alle Konten. Bei vielen Konten wäre das nacheinander zu langsam; bei der erwarteten Zahl ist es das einfachere Verhalten.
Commits
79e6c0dfeat(kalender): Datenmodell für Kalenderkonten und Termine6fda2a5feat(kalender): Antworten von Graph und Google lesen, Belegung rechnen9946e7cfeat(kalender): OAuth und Abgleich für Graph und Google0c67b73feat(kalender): Seite für die Kalenderkonten42c98c8feat(kalender): Hintergrundjobs für Abgleich und Aufräumend8d3227feat(kalender): Belegung des Tages auf der Startseitedf50627test: Verschlüsselung und Kalendermodelle absicherna8b2ff0docs: Handbuchseite und Einrichtung der Kalenderanbieter
2026-09-10 – M2: Aufgaben und Projekte
Dauer: 44 Minuten (15:57–16:41), Art claude.
Ziel der Sitzung: Erfassen und Wiederfinden — Aufgaben, Inbox, Ideen und Projekte. Noch ohne Kalender.
Erledigt
- Datenmodell
AufgabeundProjektsamt Migration - Schnellerfassung, Inbox mit Einsortieren, Aufgabenliste mit Filtern
- Ideen als eigene Ansicht, mit Übernahme in eine Aufgabe
- Projekte mit Arbeitspaketen, Meilensteinen und Fortschritt
- Startseite zeigt jetzt, was überfällig oder demnächst fällig ist
- 53 neue Unit-Tests, 11 neue Integrationsprüfungen, 4 neue E2E-Tests
- Handbuchseiten zu Aufgaben, Ideen und Projekten; Bilder neu aufgenommen
Entscheidungen mit Begründung
Die Inbox ist kein eigener Zustand. Eine Aufgabe liegt genau dann in der Inbox, wenn sie keinen Bereich hat. Ein zusätzliches Feld „einsortiert" hätte mit der Zuordnung auseinanderlaufen können; so ist der Zustand nicht darstellbar.
Projekt als eigenes Modell, nicht als Aufgabentyp. Ein Projekt hat
selbst weder Aufwand noch Fälligkeit — beides ergibt sich aus dem Inhalt.
ARBEITSPAKET und MEILENSTEIN bleiben Typen der Aufgaben innerhalb
eines Projekts.
Verworfene Aufgaben zählen gar nicht in den Fortschritt. Weder als erledigt noch als offen. Sonst sähe ein Projekt fertig aus, weil die Hälfte gestrichen wurde — eine Zahl, die lügt, ist schlechter als keine.
Sortiert wird in der Anwendung, nicht in SQL. Die Reihenfolge ist mehrstufig (offen vor abgeschlossen, dann Termin, dann Priorität, dann Titel mit deutscher Sortierung). Als geprüfte reine Funktion steht sie an einer Stelle; eine zweite Fassung in SQL wäre eine Fehlerquelle. Bei den erwarteten Mengen ist das unkritisch — sollte es einmal eng werden, ist der Ort für die Änderung klar.
Filter stehen in der Adresse. Eine gefilterte Liste lässt sich als Lesezeichen ablegen, und der Zurück-Knopf tut, was er soll.
Statuswechsel folgen festen Übergängen. Aus ERLEDIGT und VERWORFEN
geht es nur zurück nach OFFEN. Wer per Formular etwas anderes einreicht,
bekommt eine Meldung — nicht einen stillen Zustandssprung.
Fälligkeit in Kalendertagen, nicht in Stunden. Was heute um 23:00 fällig ist, ist heute fällig. Alles andere fühlt sich beim Benutzen falsch an.
Natives <select> statt einer eigenen Auswahlkomponente. Es bedient
sich auf dem Handy mit dem Systemdialog, ist ohne Zusatzcode
tastaturbedienbar und braucht kein JavaScript zum Absenden.
Probleme und Lösungen
| Problem | Lösung |
|---|---|
ESLint beanstandete String(antwort.meldung) |
Ich hatte die Rückmeldung als Record<string, unknown> typisiert, statt den vorhandenen Rueckmeldung-Typ zu nehmen. Mit dem richtigen Typ entfällt die Umwandlung ganz. |
Zwei Stellen rechneten die Tagesgrenze mit einem toLocaleString-Trick |
src/lib/zeit.ts konnte das längst sauber. Beide Stellen nutzen jetzt tagesBeginn() beziehungsweise alsTagesschluessel(); die Tests belegen gleiche Ergebnisse bei weniger Code. |
| Typfehler nach der Migration | Der Prisma-Client muss nach jeder Schemaänderung neu erzeugt werden. npm run pruefen tut das inzwischen selbst, wenn er fehlt — hier war er nur veraltet. |
Eigene Fehler, die ich korrigiert habe
- Im Demo-Seed stand versehentlich ein kyrillisches Wort als Titel eines Meilensteins. Ersetzt durch „Seite ist online".
- Ein Test erwartete, dass 481 Minuten Aufwand erlaubt sind; die Grenze liegt bei 480. Der Test war falsch, nicht der Code — jetzt prüft er die Grenze selbst und den ersten Wert darüber.
Offene Punkte
- Kein Wiederholungsrhythmus für Aufgaben und keine Verknüpfung von Aufgaben untereinander. Beides steht bewusst unter „Ideen für später" und wartet darauf, ob es im Alltag wirklich fehlt.
- Die Aufgabenliste lädt bis zu 500 Einträge auf einmal. Für den erwarteten Bestand reicht das; eine Blätterfunktion kommt, wenn es eng wird.
Commits
63c3c15feat(aufgaben): Datenmodell und Regeln für Aufgaben und Projekteb3ca1c9feat(aufgaben): Server Actions und Abfrageschichta4a889ffeat(aufgaben): Inbox, Aufgabenliste, Ideen und Projekte7c8fe10test: Erfassungsweg und Eindeutigkeit der Quellen absichern144457bfix(aufgaben): Ideen aus Aufgabenliste und Inbox heraushalten4d0f83fchore: Demo-Daten für die neuen Seiten erweiternd01c49bdocs: Handbuchseiten zu Aufgaben, Ideen und Projekten
2026-09-10 – M1: Fundament
Dauer: 161 Minuten (12:06–14:47), Art claude.
Ziel der Sitzung: Das Projekt von Grund auf aufsetzen: Gerüst, Datenbank, Anmeldung, Rollen, Audit, Instanzsteuerung, Betrieb und Dokumentation.
Erledigt
- Projektgerüst mit Next 16, React 19, TypeScript strict, Tailwind 4, alle Versionen exakt gepinnt
npm run pruefenals Sammelprüfung: Prettier, ESLint,tsc, Vitest, ShellCheck- Datenmodell und erste Migration: Benutzer mit Rolle, OIDC-Konten, Passkeys, WebAuthn-Herausforderungen, Bereiche, Audit
- Anmeldung über OIDC, Passkey und Passwort, je Instanz zuschaltbar
- Rollenprüfung in Seite und Server Action, Audit je Schreibaktion
- Instanzsteuerung mit 404 statt Hinweis
- Bereiche verwalten, Protokollseite, Startseite, Einstellungen
/api/health, Dockerfile, Compose-Stapel mit Migrationsdienstbackup.sh,update.sh,scripts/release.sh,scripts/zeit.sh- Unit-, Integrations-, Migrations- und E2E-Tests
- Screenshot-Kette mit eigener Wegwerf-Instanz
- README, Schnellstart, Handbuch, ROADMAP, CHANGELOG
Entscheidungen mit Begründung
Prisma 7.10.0 statt der notierten Fassung 6. Fassung 6 ist überholt,
Fassung 8 liegt nur als RC auf latest. Prisma 7 nimmt die Verbindung
nicht mehr im Schema entgegen — sie steht in prisma.config.ts, der
Client bekommt den pg-Treiberadapter.
TypeScript 6.0.3 statt 7.0.2. TS 7 ist zwar latest, aber
typescript-eslint@8.70 verlangt typescript >=4.8.4 <6.1.0. Mit TS 7
wäre die Lint-Kette kaputt gewesen.
ESLint 9.39.5 statt 10. Das in eslint-config-next@16 gebündelte
eslint-plugin-react ruft noch context.getFilename(), das ESLint 10
entfernt hat. Der Lint-Lauf brach reproduzierbar ab.
Eigener Auth.js-Adapter. Der mitgelieferte @auth/prisma-adapter
greift fest auf prisma.user und prisma.account zu; unsere Modelle
heißen Benutzer und Konto. Der Adapter ist schmal und ersparte es,
die Fachbegriffe der Konvention zu opfern.
Passkeys direkt über SimpleWebAuthn 14. Der WebAuthn-Provider von
Auth.js ist als experimentell markiert und auf SimpleWebAuthn 9
festgelegt. Direkt angebunden bleibt die Fassung aktuell und der Ablauf
nachvollziehbar. Für die npm-Auflösung brauchte es overrides, weil
next-auth die älteren Fassungen als optionale Peers deklariert.
Sitzungen als JWT, Rolle trotzdem aus der Datenbank. Credentials-
Provider verlangen JWT. Damit eine Sperre nicht bis zum Ablauf des Tokens
wirkungslos bleibt, liest der session-Callback Rolle und Status bei
jeder Anfrage frisch. Die Tabellen sitzung und verifizierungs_token
wären damit tot und wurden wieder entfernt, solange es nur eine einzige
Migration gab.
Migrationen als eigener Compose-Dienst. Der Versuch, nur die
Prisma-CLI ins schlanke Laufzeitabbild zu kopieren, scheiterte an ihren
transitiven Modulen (effect über @prisma/config). Alle mitzunehmen
hätte das Abbild aufgebläht. Ein Einmaldienst aus der Bau-Stufe läuft vor
der App und muss sauber durchlaufen.
Aufgabe, Zeitblock und Kalenderkonto bewusst nicht modelliert. Sie kommen mit M2, M4 und M3 — dann mit den Feldern, die der jeweilige Provider wirklich braucht, statt heute geraten.
Probleme und Lösungen
| Problem | Lösung |
|---|---|
prisma validate lehnte url im Schema ab |
Prisma 7 erwartet die Verbindung in prisma.config.ts und einen Treiberadapter im Client. |
| Integrationstest brach nach der ersten erwarteten Verletzung ab | In PostgreSQL bricht ein fehlgeschlagener Befehl die ganze Transaktion ab. Der Test läuft jetzt ohne Sammeltransaktion; alle Testdaten tragen eine Kennung und werden am Ende entfernt. |
prisma migrate diff kannte --from-url nicht |
In Prisma 7 heißen die Flaggen anders. Der Test vergleicht jetzt die eingespielte Wegwerf-Datenbank über --from-config-datasource gegen das Schema — und braucht damit keine Schattendatenbank. |
| Nach der Anmeldung ging es nicht zum ursprünglichen Ziel zurück | Das Layout leitet um, bevor die Seite drankommt, und kennt seinen Pfad nicht. Ein schlanker Proxy reicht ihn als Kopfzeile durch — ohne Anmeldelogik, weil er im Edge-Umfeld läuft. |
| Der Docker-Bau brach beim Sammeln der Seitendaten ab | Prisma-Client und Auth.js-Konfiguration wurden beim Laden des Moduls ausgewertet und brauchten Umgebungsvariablen, die es zur Bauzeit nicht gibt. Beide entstehen jetzt faul. |
Ein Typfehler in scripts/screenshots.ts blockierte den Abbild-Bau |
Entwicklungsskripte gehören nicht in den Docker-Kontext. .dockerignore schließt sie aus; geprüft werden sie von npm run pruefen. |
ESLint beanstandete setState im Effekt |
Der Hydratations-Trick im Farbschema-Knopf war unnötig: next-themes setzt die dark-Klasse vor der Hydratation, das erledigt CSS ohne Zustand. |
Eigene Fehler, die ich korrigiert habe
- Die ersten vier Commits lagen direkt auf
mainstatt auf einem Meilenstein-Zweig und ohne Attributionszeile. Historie umgebaut:mainträgt nur noch einen leeren Wurzel-Commit, die Arbeit liegt aufm1-fundament. - Ein
git add -Azog die Proxy-Umbenennung in den Doku-Commit. Wieder getrennt. scripts/screenshots.tswurde geschrieben, ohne danachnpm run pruefenlaufen zu lassen; der Typfehler fiel erst im Docker-Bau auf.
Offene Punkte
- Die
origin-URL fehlt noch. Push und Merge stehen ohnehin erst nach der Freigabe an. - Der Schnellstart endet beim Passkey statt beim ersten Kalenderkonto und der ersten Aufgabe — beides gibt es erst mit M3 und M2.
- Der Demo-Seed enthält noch keine erfundenen Termine; einen Demo-Kalenderprovider gibt es erst mit M3.
- Lokal weichen die Ports ab (Datenbank 5435, App 3001), weil 5432 und
3000 auf diesem Rechner belegt sind. In
.env.examplestehen die üblichen Werte.
Commits
de1f923chore: Projektgerüst und Werkzeugkette einrichten748d032feat(datenbank): Datenmodell für Anmeldung, Bereiche und Audite2b00f4feat(instanz): Instanzsteuerung mit serverseitigem Riegel19ca082feat(zeit): Zeitrechnung und Anzeigeformate in Europe/Berlin4ff67c1feat(oberflaeche): Grundlayout, Navigation und Farbschemacc013fdfeat(anmeldung): OIDC, Passkeys und Passwort mit Auth.js v5e9a2b4bfeat(bereiche): Bereiche verwalten mit Rollenprüfung und Audit1876ae4feat(betrieb): Health-Endpunkt und Seed für eine frische Datenbank24a799dchore: tsconfig-Anpassungen von Next 16 übernehmen126ad93test: Integrations-, Migrations- und E2E-Tests ergänzenf91d015feat(betrieb): Abbild, Compose-Stapel und Betriebsskripte56b5356docs: README, Schnellstart, Handbuch, Fahrplan und CHANGELOGe3390c8refactor: middleware in proxy umbenennena882731chore: Screenshot-Kette und Demo-Seedcf28ec1test: Rollentrennung end-to-end absichern6d04bc2fix(oberflaeche): Kopfleiste einzeilig halten, Demo-Uhr feststellen688d9f5docs: Handbuchbilder aufnehmen