# 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 - `Kalenderkonto` und `Termin` samt 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 - 79e6c0d feat(kalender): Datenmodell für Kalenderkonten und Termine - 6fda2a5 feat(kalender): Antworten von Graph und Google lesen, Belegung rechnen - 9946e7c feat(kalender): OAuth und Abgleich für Graph und Google - 0c67b73 feat(kalender): Seite für die Kalenderkonten - 42c98c8 feat(kalender): Hintergrundjobs für Abgleich und Aufräumen - d8d3227 feat(kalender): Belegung des Tages auf der Startseite - df50627 test: Verschlüsselung und Kalendermodelle absichern - a8b2ff0 docs: 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 `Aufgabe` und `Projekt` samt 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 `