Files
planer/devlog/DEVLOG.md
T
2026-09-13 15:21:00 +02:00

45 KiB
Raw Permalink Blame History

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
M6 claude 733 12,22
M7 claude 886 14,77
M8 claude 69 1,15
Gesamt 2521 42,02

2026-09-13 – M8: MCP

Dauer: 69 Minuten (14:10–15:19), Art claude.

Der kürzeste Meilenstein — weil das Fundament schon stand. Tokens, Rechte und Protokoll kamen mit M7, der Kalenderschreibweg mit M4. Übrig blieb das Zusammenstecken.

Ziel der Sitzung: Ein Sprachmodell soll den Alltag erleichtern können — ohne einen eigenen, weicheren Zugang zu bekommen als jede andere fremde Anwendung.

Erledigt

  • MCP-Server unter /api/mcp über @modelcontextprotocol/sdk
  • Sechs Werkzeuge: suchen, Woche ansehen, Kalender auflisten, anlegen, abhaken, einplanen
  • Das Einplanen aus der Serveraktion herausgelöst, damit Oberfläche und MCP dieselbe Stelle benutzen
  • 6 neue E2E-Tests, Beschreibung in docs/MCP.md

Entscheidungen mit Begründung

Dieselben Tokens, dieselben Rechte wie die Schnittstelle. Ein Sprachmodell ist aus Sicht des Planers eine fremde Anwendung wie jede andere. Ein eigener Zugangsweg wäre ein zweiter Satz Regeln — und der zweite Satz ist immer der, den man vergisst zu pflegen.

Die Werkzeugliste richtet sich nach den Rechten. Was ein Token nicht darf, taucht gar nicht erst auf. Ein Modell soll nicht gegen eine Tür laufen, die für es verschlossen ist; und eine Fehlermeldung als Antwort auf einen Werkzeugaufruf ist verschwendeter Kontext.

Wenige, grob geschnittene Werkzeuge. Ein Modell, das zwischen zwanzig fast gleichen wählen muss, wählt falsch. Jedes weitere Werkzeug ist zudem eine zusätzliche Möglichkeit, versehentlich etwas zu ändern.

Der Server kann nichts löschen. Abhaken ja, wegräumen nein. Auch keine Bereiche, Projekte oder Einstellungen anfassen. Ein Modell soll den Alltag erleichtern, nicht die Ordnung umbauen.

Das Einplanen liegt jetzt an einer gemeinsamen Stelle — und die liegt bewusst nicht in einer Datei mit 'use server'. Dort wird jede exportierte Funktion zu einem Endpunkt, den der Browser aufrufen kann; eine Funktion, die den Akteur als Parameter nimmt, wäre dann ein offenes Tor.

Zustandslos. Der Planer hat keine langlaufenden Vorgänge, und eine Sitzungsverwaltung wäre Zustand, den mehrere Container nicht teilen könnten.

Probleme und Lösungen

Problem Lösung
Der SDK-Transport erwartet Node-Streams, Next liefert Request Das SDK bringt seit 1.30 WebStandardStreamableHTTPServerTransport mit — handleRequest(Request): Promise<Response> passt direkt.
ESLint beanstandete das Auslesen der Formularfelder FormData.get kann auch eine Datei liefern; die würde beim Stringify zu [object Object]. Ein Helfer nimmt jetzt nur Text.

Offene Punkte

  • Der Server ist gegen die eigenen Tests geprüft, nicht gegen einen echten Client. Ein Lauf mit Claude Code gegen die laufende Instanz wäre der Beweis, dass Handschlag und Werkzeugbeschreibungen auch dort taugen.
  • Die Handbuchbilder sind unverändert aus M7 — der MCP-Server hat keine Oberfläche, es gibt nichts abzubilden.

Commits

  • 5f45dff refactor: Einplanen aus der Serveraktion herauslösen
  • bdc4260 feat: MCP-Server über denselben Token wie die API
  • (docs) MCP beschreiben, Fahrplan abschließen

2026-09-13 – M7: Schnellerfassung und API

Dauer: 886 Minuten (10:15–01:01), Art claude.

Wie schon bei M6 sagt die Zahl mehr über den Rechner als über die Arbeit: Fünf hängende Docker-Bauten, einer davon mit einem einzelnen Schritt über drei Stunden. Die Bildaufnahme musste achtmal angestoßen werden.

Ziel der Sitzung: Etwas festhalten können, wo man gerade steht — und andere Anwendungen hereinlassen, ohne ihnen die Schlüssel zum ganzen Haus zu geben.

Erledigt

  • Regeln der Schnittstelle als reine Logik: Rechte, Tokenaufbau, Eingaben (30 Tests)
  • /api/v1: Aufgaben lesen, anlegen, ändern, abhaken
  • Tokens mit Rechten und wahlweise Ablauf, verwaltbar in der Anwendung
  • Schnellerfassung fürs Handy mit Warteschlange in IndexedDB
  • Manifest und Symbol: der Planer lässt sich als App ablegen
  • 10 neue E2E-Tests, 6 neue Integrationsprüfungen, Handbuchseite, API-Doku

Entscheidungen mit Begründung

Von einem Token liegt nur der Hash in der Datenbank. Ein Token ist ein Passwort; es gibt keinen Grund, es zurücklesen zu können, und jeden Grund, es nicht zu können. SHA-256 ohne Salz ist hier richtig — der Wert hat 32 Byte Zufall, gegen so etwas hilft kein Wörterbuch, und die Prüfung muss bei jeder Anfrage schnell sein.

Kein Recht schließt ein anderes ein. „Erfassen" darf nicht lesen. Ein Dienst, der nur meldet „hier ist etwas Neues", braucht den Bestand nicht zu sehen — und das ist der häufigste Fall.

Der sichtbare Anfang heißt planer_. Wer so einen Wert versehentlich in ein öffentliches Verzeichnis schiebt, soll ihn beim Durchsehen erkennen.

/selbst braucht kein Recht. Sonst müsste man beim Einrichten raten, ob der Token falsch ist oder nur zu wenig darf.

Die Schnittstelle kann Bereich und Projekt nicht setzen. Was von außen kommt, landet in der Inbox. Die Einordnung ist Sache dessen, der plant, nicht dessen, der meldet. Löschen kann sie auch nicht: Ein Fehler im fremden Skript soll keine Planung vernichten.

Erst anstellen, dann senden. Die Schnellerfassung schreibt zuerst in die Warteschlange des Geräts und versucht dann zu senden. Andersherum wäre der Eintrag weg, wenn der Browser mitten im Senden geschlossen wird.

IndexedDB statt localStorage. Das sind echte Daten des Benutzers. Ein Eintrag, der beim Aufräumen des Speichers verschwindet, ist eine verlorene Aufgabe — genau das, was der Planer verhindern soll.

Der Dienstarbeiter hält genau eine Seite vor. Ein halb veralteter Planer wäre schlimmer als gar keiner; ein leeres Eingabefeld dagegen veraltet nicht. Und ohne die Seite nützte die Warteschlange niemandem: Man käme gar nicht erst hin.

Die Navigation wurde geteilt statt weiter gedehnt. Nach M6 hatte ich die Leiste verbreitert; mit dem dreizehnten Eintrag verschluckte sie erneut Punkte. Das war die falsche Antwort: Das Problem ist nicht die Breite, sondern die Vermischung. Alltag steht in der Leiste, Einrichtung und Verwaltung liegen hinter „Mehr".

Probleme und Lösungen

Problem Lösung
Die Erfassungsseite zählte die Inbox anders als die Kopfleiste Sie zählte Ideen mit. Jetzt benutzt sie dieselbe Abfrage. Derselbe Fehlertyp wie in M2 — und wieder nur im Bild aufgefallen, im Code sah jede Seite für sich richtig aus.
Die Leiste verschluckte mit dem dreizehnten Eintrag wieder Punkte Aufgeteilt statt verbreitert, siehe oben. Zwei E2E-Tests greifen den neuen Weg über „Mehr" auf.
Ein E2E-Test brach mit ERR_ABORTED ab Die Erfolgsmeldung kommt, bevor das Neuladen der Liste fertig ist; wer dann navigiert, bricht es ab. Der Test wartet jetzt darauf.
exactOptionalPropertyTypes und Prismas Update-Typen vertragen kein undefined Das Datenobjekt wird Feld für Feld gebaut statt per Spread. Ein gesetztes undefined ist etwas anderes als ein fehlendes Feld.
Ein Devanagari-Zeichen war in ein Token-Präfix der Demo-Daten geraten Entfernt. Beim Schreiben über die Shell passiert so etwas; die Datei danach durchsucht.
Fünf hängende Docker-Bauten, einer über drei Stunden Kein Codeproblem: Nach Abbruch und Neustart lief derselbe Bau in Minuten durch. Zweimal scheiterte zusätzlich das Anlegen des Compose-Netzes.

Offene Punkte

  • Die Bilder der Erfassungsseite zeigen die alte Inbox-Zahl. Der Bildlauf ließ sich nach der Korrektur nicht mehr durchbringen. Ein Lauf, wenn Docker wieder mitspielt, räumt das auf.
  • Keine Begrenzung der Aufrufzahl je Token. Bei einer Handvoll eigener Tokens wäre sie mehr Zeremonie als Schutz — bei einem Token, der außer Haus geht, sähe das anders aus.
  • Die Warteschlange kennt keinen Abbruch: Ein Eintrag, der dauerhaft scheitert, wird ewig erneut versucht. Bisher gibt es keinen Fall, in dem das passieren könnte, ohne dass gleichzeitig die Anmeldung fehlt.

Commits

  • d5c7974 feat: Datenmodell und Regeln für die öffentliche Schnittstelle
  • 6d3ebd0 feat: API v1 für Aufgaben
  • b17c42a feat: Tokens verwalten
  • 9565b9a feat: Schnellerfassung vom Handy, auch ohne Netz
  • 10b49cf test: Die Tür der Schnittstelle scharf prüfen
  • 2e57713 docs: Handbuchseite zum Erfassen und Beschreibung der Schnittstelle
  • 7919149 fix: Navigation aufteilen, statt sie weiter zu dehnen
  • f9a616e docs: Handbuchbilder für M7 aufnehmen

2026-09-12 – M6: Integrationen

Dauer: 733 Minuten (20:47–09:00), Art claude.

Die Zahl täuscht. Ein großer Teil davon war Warten auf Docker-Bauten, die in der Nacht dreimal hängen blieben — einmal 39 Minuten ohne eine Zeile Ausgabe. Die eigentliche Arbeit war knapper.

Ziel der Sitzung: Aufgaben dort abholen, wo sie entstehen — ohne dass die Planung darunter leidet, die hier passiert.

Erledigt

  • Gemeinsame Aufnahmestelle für alle Integrationen (21 Tests)
  • Zammad, lesend, mit Suchausdruck aus der Konfiguration
  • Telegram: erfassen, melden, abhaken — mit Kopplung über Einmalcode
  • Spiegel zwischen den Instanzen: Belegung ohne Titel, Aufgaben nur markiert
  • Seite „Integrationen" mit Zustand, Fehler und Knopf für den Sofortlauf
  • Gespiegelte Belegung in der Wochenansicht
  • 4 neue E2E-Tests, 9 neue Integrationsprüfungen, Handbuchseite

Entscheidungen mit Begründung

Das Ursprungssystem führt Titel, Notiz, Verweis und Fälligkeit — mehr nicht. Bereich, Projekt, Priorität und geschätzter Aufwand gehören der Planung. Wer ein Ticket einsortiert und schätzt, muss sich darauf verlassen können, dass der nächste Abgleich das nicht wegräumt. Ohne diese Trennung wäre die Wochenplanung nach jedem Lauf kaputt.

Zammad wird nur gelesen. Ein Ticket schließt man im Ticketsystem, sonst weiß die Kollegin nicht, warum es plötzlich zu ist. Umgekehrt darf Zammad hier abhaken — dort ist die Wahrheit über den Zustand des Tickets.

Telegram holt ab, statt einen Webhook zu bekommen. Ein Webhook verlangt eine von außen erreichbare Adresse mit gültigem Zertifikat. Der Planer soll auch hinter einem Anschluss ohne feste Adresse laufen, und der Preis dafür sind zwei Minuten Verzögerung.

Unbekannte Chats bekommen keine Antwort — auch keine Fehlermeldung. Wer die Kennung des Bots kennt, könnte ihm sonst schreiben und daran erkennen, dass es diesen Planer gibt. Die einzige Ausnahme ist ein gültiger Kopplungscode.

Der Kopplungscode entstand aus einem Widerspruch. Erst sollte die Chatkennung von Hand eingetragen werden. Nur: Um sie zu erfahren, müsste der Bot Unbekannten antworten — genau das, was er nicht darf. Der Code dreht die Richtung um: Er kommt aus der Anwendung, in die man sich anmelden muss.

Der Spiegel schickt Belegung ohne Titel. Daheim soll zu sehen sein, dass die Zeit weg ist, nicht woran gearbeitet wird. Aufgaben gehen nur einzeln markiert mit — ein Schalter für einen ganzen Bereich würde zu leicht Kundennamen nach Hause schicken.

Was der Sender nicht mehr schickt, verschwindet beim Empfänger. Sonst stünde daheim für immer eine Sperre, die es längst nicht mehr gibt. Für Aufgaben gilt das nicht: Sie sind Planung, keine Sperre.

Jede Sendung ist fünf Minuten gültig. Ohne Frist ließe sich ein abgefangenes Paket beliebig oft erneut einspielen. Der Empfänger antwortet auf jeden Fehlschlag gleich — falsches Geheimnis, verstellte Uhr und Unsinn im Inhalt sind von außen nicht zu unterscheiden.

Die Demo-Instanz redet mit niemandem. Sie ist mit erfundenen Daten gefüllt und steht nur für Bilder. Solange DEMO_MODUS gilt, starten die Jobs für Zammad, Telegram und den Spiegel gar nicht erst.

Probleme und Lösungen

Problem Lösung
Die Kopfleiste verschluckte mit dem elften Eintrag „Einstellungen" und „Protokoll" Sie ist jetzt breiter als die Inhaltsspalte. Eine Leiste, die Punkte stillschweigend abschneidet, ist schlimmer als eine, die nicht bündig mit dem Text steht. Gefunden im aufgenommenen Bild.
Der Kartentext erklärte noch den Weg von vor dem Kopplungscode Ersetzt. Ebenfalls erst im Bild aufgefallen — im Code war beides für sich stimmig.
Ein Kürzungstest schlug an der Grenze fehl Nicht der Test war falsch, sondern der Vergleich: Eine Worttrennung bei genau der halben Titellänge wurde abgelehnt. Jetzt >=.
Ein regulärer Ausdruck ging beim Schreiben über die Shell kaputt Das Muster steht jetzt als benannte Konstante im Code, geschrieben mit dem Edit-Werkzeug statt über ein Skript.
Ein unsichtbares Zeichen (U+200B) landete mitten in einem Funktionsnamen Entfernt und die Datei danach abgesucht.
Das Aufräumen im Integrationstest hätte fremde Telegram-Chats treffen können Es trifft jetzt genau die Kennung, die der Lauf selbst angelegt hat.
Docker-Bauten blieben dreimal hängen, einmal 39 Minuten ohne Ausgabe Kein Codeproblem: Nach Abbruch und Neustart lief derselbe Bau in Minuten durch. Die Bilder mussten deshalb viermal angestoßen werden.

Offene Punkte

  • Der Wartungsplaner ist nicht angebunden. Die Aufnahmestelle steht bereit und die Seite führt ihn als sichtbare Lücke; es fehlt allein das Abholen. Dafür muss erst feststehen, was das Produkt an Schnittstelle hergibt.
  • Zammad und Telegram sind gegen die Logik geprüft, nicht gegen die echten Dienste — dafür fehlen Zugangsdaten. Der Spiegel dagegen ist end-to-end geprüft, weil beide Enden dieselbe Anwendung sind.
  • Ein Telegram-Chat lässt sich nicht umbenennen. Fällt das im Alltag auf, ist es ein Einzeiler.

Commits

  • 7f8f505 feat: Datenmodell und Umgebung für Integrationen
  • e4d3547 feat: Aufnahmeregeln für Integrationen als reine Logik
  • 6dad333 feat: Zammad, Telegram und den Spiegel anbinden
  • bc60e5f feat: Integrationen takten und Telegram als dritten Meldeweg
  • 1281c3e feat: Integrationen sichtbar und bedienbar machen
  • 0d0f850 feat: Gespiegelte Belegung in der Wochenansicht zeigen
  • 4da2f3d test: Spiegel-Eingang und Integrationstabellen absichern
  • 3befcc1 feat: Aufgaben einzeln zum Spiegeln freigeben
  • f5eadef feat: Lauf von Hand anstoßen und Spiegelschalter in der Liste
  • 3c7cdbb chore: Demo-Daten, Bildaufnahme und Platz in der Kopfleiste
  • 475b1d5 docs: Handbuchseite, README und Fahrplan für M6
  • 98d170b docs: Handbuchbilder für M6 neu aufnehmen

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<ArrayBufferLike> 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 <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

  • 63c3c15 feat(aufgaben): Datenmodell und Regeln für Aufgaben und Projekte
  • b3ca1c9 feat(aufgaben): Server Actions und Abfrageschicht
  • a4a889f feat(aufgaben): Inbox, Aufgabenliste, Ideen und Projekte
  • 7c8fe10 test: Erfassungsweg und Eindeutigkeit der Quellen absichern
  • 144457b fix(aufgaben): Ideen aus Aufgabenliste und Inbox heraushalten
  • 4d0f83f chore: Demo-Daten für die neuen Seiten erweitern
  • d01c49b docs: 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 pruefen als 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 Migrationsdienst
  • backup.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 main statt auf einem Meilenstein-Zweig und ohne Attributionszeile. Historie umgebaut: main trägt nur noch einen leeren Wurzel-Commit, die Arbeit liegt auf m1-fundament.
  • Ein git add -A zog die Proxy-Umbenennung in den Doku-Commit. Wieder getrennt.
  • scripts/screenshots.ts wurde geschrieben, ohne danach npm run pruefen laufen 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.example stehen die üblichen Werte.

Commits

  • de1f923 chore: Projektgerüst und Werkzeugkette einrichten
  • 748d032 feat(datenbank): Datenmodell für Anmeldung, Bereiche und Audit
  • e2b00f4 feat(instanz): Instanzsteuerung mit serverseitigem Riegel
  • 19ca082 feat(zeit): Zeitrechnung und Anzeigeformate in Europe/Berlin
  • 4ff67c1 feat(oberflaeche): Grundlayout, Navigation und Farbschema
  • cc013fd feat(anmeldung): OIDC, Passkeys und Passwort mit Auth.js v5
  • e9a2b4b feat(bereiche): Bereiche verwalten mit Rollenprüfung und Audit
  • 1876ae4 feat(betrieb): Health-Endpunkt und Seed für eine frische Datenbank
  • 24a799d chore: tsconfig-Anpassungen von Next 16 übernehmen
  • 126ad93 test: Integrations-, Migrations- und E2E-Tests ergänzen
  • f91d015 feat(betrieb): Abbild, Compose-Stapel und Betriebsskripte
  • 56b5356 docs: README, Schnellstart, Handbuch, Fahrplan und CHANGELOG
  • e3390c8 refactor: middleware in proxy umbenennen
  • a882731 chore: Screenshot-Kette und Demo-Seed
  • cf28ec1 test: Rollentrennung end-to-end absichern
  • 6d04bc2 fix(oberflaeche): Kopfleiste einzeilig halten, Demo-Uhr feststellen
  • 688d9f5 docs: Handbuchbilder aufnehmen