# 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 | | M4 | claude | 167 | 2,78 | | M5 | claude | 420 | 7,00 | | **Gesamt** | | **833** | **13,88** | --- ## 2026-09-11 – M5: Benachrichtigungen **Dauer:** 420 Minuten (06:35–13:35), Art `claude`. **Ziel der Sitzung:** Sagen, was ansteht — und dabei so selten stören, dass niemand auf die Idee kommt, alles abzuschalten. ### Erledigt - Regeln als reine Logik: Anlässe, Ruhezeit, Wiederholungssperre (30 Tests) - Web-Push über VAPID samt Service Worker, An- und Abmeldung je Browser - Mailversand über nodemailer, als Text und als HTML - Vier Anlässe: Tagesüberblick, Fälligkeit, Vorlauf vor einem Zeitblock, Abgleichfehler - Bedienung in den Einstellungen mit Probemeldung und Geräteliste - Ein Lauf alle zehn Minuten, Nachweise räumt die Nachtrunde weg - 2 neue E2E-Tests, 7 neue Integrationsprüfungen, Handbuchseite ### Entscheidungen mit Begründung **Ab Werk meldet der Planer gar nichts.** Kein Weg ist eingeschaltet. Wer beim ersten Start mit Meldungen überschüttet wird, schaltet alles ab — und dann auch das Nützliche. Die Einrichtung ist ein bewusster Handgriff, und das ist der Preis dafür, dass die Meldungen später ernst genommen werden. **Der Tagesüberblick ist von der Ruhezeit ausgenommen.** Sonst bekäme ihn nie, wer ihn auf 06:00 stellt — die voreingestellte Ruhezeit läuft bis 07:00. Die Ruhezeit schützt vor Störungen, nicht vor der Nachricht, um die ausdrücklich gebeten wurde. **Der Nachweis wird vor dem Senden geschrieben, nicht danach.** Bricht der Versand mittendrin ab, gilt die Meldung als zugestellt und kommt nicht wieder. Andersherum würde ein hängender Mailserver dieselbe Nachricht alle zehn Minuten erneut versuchen. Lieber eine verpasste Nachricht als eine, die im Minutentakt klopft. **Was in die Ruhezeit fällt, wird nicht nachgeholt.** Eine Erinnerung an einen Zeitblock, der längst begonnen hat, ist nur der Beweis, dass die Anwendung zu spät kommt. **Abgleichfehler erst ab dem dritten Versuch in Folge.** Ein Netz hustet, ein Anbieter hat Wartung. Beim ersten Fehlschlag zu melden hieße, für Dinge zu wecken, die sich von selbst erledigen. Nach dem dritten Mal ist es dagegen dringend — solange steht die Wochenansicht auf altem Stand. **Der Tagesüberblick entfällt, wenn es nichts zu sagen gibt.** „Heute steht nichts an" ist keine Information, sondern Lärm. **Tote Abos putzen sich selbst weg.** 404 und 410 heißen endgültig, das Gerät verschwindet sofort; andere Fehler zählen hoch und führen nach fünf Versuchen zum selben Ergebnis. Eine Liste, die von Hand gepflegt werden muss, wird nicht gepflegt. **Push-Schlüssel liegen verschlüsselt, Endpunkte nie im Protokoll.** Ein Endpunkt genügt, um jemandem Nachrichten zu schicken — er gehört damit in dieselbe Klasse wie ein Token. ### Probleme und Lösungen | Problem | Lösung | | --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `pushManager.subscribe` lehnte den VAPID-Schlüssel als `Uint8Array` ab | Der Puffer wird jetzt ausdrücklich als `ArrayBuffer` angelegt; ein geteilter Puffer ist dort nicht erlaubt. | | Der Integrationstest brach mit einem Fremdschlüsselfehler ab | Die neuen Prüfungen standen hinter dem Löschen des Testbenutzers. Jetzt davor — und danach wird geprüft, dass Geräte und Nachweise mit ihm verschwinden. | | E2E-Läufe scheiterten unter Last immer wieder beim Anmelden | Ein Klick, der vor der Hydratation ankommt, verpufft folgenlos. Der Anmeldehelfer liegt jetzt einmal in `e2e/hilfe.ts` und fasst nach, bis die Startseite da ist. | | Die Karte meldete gleichzeitig „Browser blockiert" und „Server nicht eingerichtet" | Zwei Aussagen, von denen nur eine zählt. Der Serverzustand geht jetzt vor. Gefunden nur durch das Ansehen des aufgenommenen Bildes. | | Das erfundene Push-Gerät in den Demo-Daten wäre beim ersten Versandlauf gelöscht worden | Seine Schlüssel lagen als Klartext-Platzhalter, und unlesbare Schlüssel führen zum Entfernen. Die Demo-Daten enthalten jetzt kein Gerät mehr — die Bilder zeigen den Zustand davor. | | Im Bild meldet der Browser „blockiert", obwohl die Erlaubnis erteilt wurde | Chromium im Kopflosbetrieb antwortet auf `Notification.permission` immer mit `denied`; `grantPermissions` ändert daran nichts. Das Handbuch schreibt es jetzt unter das Bild. | | Ein Docker-Bau brauchte 14 Minuten, ein E2E-Lauf 18 | Systemlast, kein Codeproblem: dieselben Läufe danach in 1,5 beziehungsweise 0,4 Minuten. Ein vergessener Testserver auf Port 3100 hat kräftig mitgeholfen. | ### Offene Punkte - Der Versandweg ist gegen die Logik geprüft, nicht gegen echte Dienste: ohne VAPID-Schlüssel und SMTP-Zugang geht lokal nichts hinaus. Das braucht einen Lauf auf dem Server. - Mehrere Anlässe erscheinen als mehrere Nachrichten. Ob eine Sammelmeldung fehlt, zeigt erst der Alltag. ### Commits - 24f613d feat: Datenmodell und Umgebung für Benachrichtigungen - ddb13d7 feat: Regeln für Benachrichtigungen als reine Logik - e0e11c8 feat: Versand per Web-Push und E-Mail - b622c1b feat: Benachrichtigungen in den Einstellungen bedienen - 047a4a1 feat: Meldungen alle zehn Minuten prüfen und verschicken - 5bbbdd5 chore: Demo-Daten und Bildaufnahme für Benachrichtigungen - f03ea73 docs: Handbuchseite und README-Abschnitt zu Benachrichtigungen - 6e1f279 test: Benachrichtigungen end-to-end und in der Datenbank absichern - 7bef435 fix: Serverzustand geht der Browsermeldung vor - 35ecb2f chore: Screenshot-Instanz mit eingerichteten Meldewegen - bb7b189 docs: Handbuchbilder für M5 neu aufnehmen --- ## 2026-09-10 – M4: Wochenplanung und Zeitblöcke **Dauer:** 167 Minuten (20:42–23:29), Art `claude`. **Ziel der Sitzung:** Die Kernidee des Projekts — Aufgaben per Ziehen und Ablegen in echte Kalendertermine verwandeln, mit Rückweg. ### Erledigt - `Zeitblock` samt Migration und Schleifenschutz (30 Tests) - Schreibweg zu Graph und Google mit `If-Match`, Rückweg im Abgleich - Rasterlogik für die Wochenansicht (21 Tests) - Wochenansicht mit `@dnd-kit/core`: einplanen, verschieben, ausplanen - Kapazität und Belegung je Wochentag - 3 neue E2E-Tests, Handbuchseite ### Entscheidungen mit Begründung **Der Fingerabdruck geht nur über Beginn und Ende.** Titel und Beschreibung ändern sich auch aus harmlosen Gründen — etwa weil der Aufgabentitel bearbeitet wurde. Für die Planung zählt allein, wann der Block liegt; alles andere in den Fingerabdruck zu nehmen hieße, den Anbieter grundlos zu behelligen. **Der Kalender gewinnt, ausnahmslos.** Wer im Kalender etwas verschiebt, meint es so. Säße der Planer am längeren Hebel, zöge er dem Benutzer den Termin wieder weg — und der wüsste nicht, warum. Bei HTTP 412 verwirft der Planer sogar seinen eigenen Fingerabdruck, damit der nächste Abgleich die fremde Änderung sicher als solche erkennt. **Scheitert das Schreiben, überlebt die Planung.** Der Zeitblock bleibt mit vermerktem Fehler stehen. Ihn zu verwerfen hieße, die Planung des Benutzers wegen eines Netzproblems zu vergessen; das nächste Verschieben trägt ihn nach. Beim Löschen ist es umgekehrt: Da verschwindet der Zeitblock auch bei unerreichbarem Kalender, sonst wäre die Planung blockiert. **Die Dauer kommt aus dem geschätzten Aufwand.** Wer schätzt, bekommt die passende Blockgröße geschenkt. Das belohnt genau die Gewohnheit, die die Planung überhaupt erst tragfähig macht. **Ziehen beginnt erst nach acht Pixeln.** Ohne diese Schwelle löst jeder Klick einen Zug aus und die Knöpfe im Block wären nicht mehr bedienbar. **Überbuchte Tage werden rot, aber nicht gesperrt.** Der Planer ist ein Werkzeug, kein Aufpasser. Manchmal muss ein Tag überbucht sein. ### Probleme und Lösungen | Problem | Lösung | | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | | Ein Zeitblock steht auch im Terminspiegel und erschien doppelt — einmal als eigene Planung, einmal als fremder Termin | Die Wochenabfrage filtert Termine heraus, deren `extId` zu einem Zeitblock gehört. | | Ein Build lief 32 Minuten statt Sekunden | Systemlast durch einen gleichzeitigen Docker-Bau. Ein sauberer Neubau danach: 46 Sekunden. Kein Codeproblem. | | Sich überschneidende Termine lagen unsichtbar übereinander | `spurenVerteilen` legt sie nebeneinander und gibt eine Spur wieder frei, sobald sie zu Ende ist. | ### Offene Punkte - **Der Schreibweg ist nicht gegen die echten APIs geprüft.** Wie in M3 fehlen die Zugangsdaten. Die Logik ist getestet, der erste echte Schreib- und Rückweg ist Florians. - Die Dauer lässt sich nicht am Balken ziehen; wer eine andere will, ändert den Aufwand der Aufgabe. - Ein Zeitblock landet immer im ersten verbundenen Kalender. Das Standard-Kalenderkonto je Bereich ist im Datenmodell vorbereitet, aber noch nicht in der Oberfläche. ### Commits - 5791047 feat(zeitbloecke): Datenmodell und Schleifenschutz - 9c1c916 feat(zeitbloecke): Schreibweg in den Kalender und Rückweg zurück - bcb4b8e feat(woche): Rasterlogik für die Wochenansicht - fada445 feat(woche): Wochenansicht mit Ziehen und Ablegen - 5b1e998 test: Wochenansicht und Ausplanen absichern - 0974695 docs: Handbuchseite zur Wochenplanung --- ## 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 `