Florian WildeandClaude Opus 5 5b1e9981e4 test: Wochenansicht und Ausplanen absichern
Drei E2E-Tests: der Hinweis ohne verbundenen Kalender, ein Zeitblock im
Raster samt Ausplanen, und das Blättern durch die Wochen.

Der mittlere Test läuft bewusst mit einem Kalenderkonto ohne gültige
Tokens. Der Schreibweg zum Anbieter scheitert dann — und genau das ist
prüfenswert: Die Planung muss auch überleben, wenn der Kalender nicht
erreichbar ist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 23:17:22 +02:00
2026-09-10 18:34:51 +02:00
2026-09-10 20:41:59 +02:00

Planer

Aufgaben, Ideen, Projekte und Zeitblöcke an einem Ort.

Die Kernidee: Kalender aus Microsoft 365 (Firma, Gewerbe) und Google (privat) lesen und Aufgaben per Ziehen und Ablegen als Zeitblöcke direkt in diese Kalender schreiben — inklusive Rückweg, wenn ein Block im Kalender verschoben wird.

Stand: Meilenstein 3 von 8. Aufgaben, Projekte und das Lesen der Kalender stehen: der Planer weiß jetzt, wann überhaupt Platz ist. Das Schreiben der Zeitblöcke per Ziehen und Ablegen kommt mit M4. Der Fahrplan sagt, was wann folgt.

  • Schnellstart – in zehn Minuten von git clone zur ersten Anmeldung
  • Handbuch – Anleitung für den Alltag
  • Fahrplan – was geplant ist und was bewusst nicht
  • Änderungen – was sich je Version getan hat

Funktionsumfang

Heute

  • Schnellerfassung mit einem einzigen Feld; alles ohne Bereich sammelt sich in der Inbox und wird später einsortiert
  • Aufgaben mit Typ, Status, Priorität, Aufwand und Fälligkeit, sortiert nach Dringlichkeit und filterbar über die Adresszeile
  • Ideen getrennt von Aufgaben — ohne Termin, per Klick übernehmbar
  • Projekte mit Arbeitspaketen, Meilensteinen und Fortschritt
  • Kalender lesen aus Microsoft 365 und Google; die Startseite zeigt je Bereich, wie voll der heutige Tag schon ist
  • Anmeldung über einen OIDC-Anbieter, über Passkeys oder mit E-Mail und Passwort — je Instanz einzeln zuschaltbar
  • Rollen Benutzer und Verwaltung; jede schreibende Aktion wird serverseitig geprüft und hinterlässt einen Protokolleintrag
  • Bereiche mit Farbe, Arbeitsfenster und Tageskapazität je Wochentag
  • Hell und Dunkel, Oberfläche durchgehend auf Deutsch
  • Betrieb über Docker Compose, mit Sicherung, Aktualisierung und Healthcheck

Geplant

Zeitblöcke in den Kalender schreiben, Wochenplanung per Ziehen und Ablegen, Benachrichtigungen, Integrationen, Schnellerfassung vom Handy und ein MCP-Server. Siehe ROADMAP.md.

Bildergalerie

Hell Dunkel
Anmelden Anmeldeseite, hell Anmeldeseite, dunkel
Start Startseite, hell Startseite, dunkel
Inbox Inbox, hell Inbox, dunkel
Aufgaben Aufgaben, hell Aufgaben, dunkel
Projekte Projekte, hell Projekte, dunkel
Ideen Ideen, hell Ideen, dunkel
Kalender Kalenderkonten, hell Kalenderkonten, dunkel
Bereiche Bereiche, hell Bereiche, dunkel
Einstellungen Einstellungen, hell Einstellungen, dunkel
Protokoll Protokoll, hell Protokoll, dunkel

Alle Bilder entstehen mit npm run screenshots in einer Wegwerf-Instanz mit ausschließlich erfundenen Daten — niemals aus einer Instanz mit echten Daten.

Die zwei Instanzen

Ein Abbild, zwei Deployments, gesteuert über INSTANZ. Die Umschaltung wird serverseitig erzwungen: Was eine Instanz nicht darf, liefert dort 404 und lädt seine Konfiguration gar nicht erst.

firma daheim
Anmeldung OIDC Entra ID OIDC authentik
Kalenderkonten Graph (Firmen-Tenant) Graph (Gewerbe-Tenant), Google
Mail Graph Firmenpostfach Graph Gewerbepostfach, Gmail
Integrationen Zammad, Wartungsplaner, Spiegel senden Telegram, Spiegel empfangen

Ein falsch geschriebener Wert bricht den Start ab, statt still auf einen Standard zu fallen — in der Firma dürfen keine Provider anlaufen, die dort nichts zu suchen haben.

Umgebungsvariablen

Alle Werte stehen in .env. Vorlage: .env.example. .env gehört niemals ins Git.

Pflicht

Variable Beispiel Bedeutung
INSTANZ daheim firma oder daheim
APP_URL https://planer.example.de Öffentliche Adresse, ohne Schrägstrich am Ende
DATABASE_URL postgresql://planer:…@db:5432/planer?schema=public Verbindung zur Datenbank
AUTH_SECRET (32 Byte Base64) Signiert Sitzungen. openssl rand -base64 32
WEBAUTHN_RP_ID planer.example.de Nackte Domain, ohne Schema und Port
WEBAUTHN_ORIGIN https://planer.example.de Exakt die Herkunft im Browser

Datenbank im Compose-Stapel

Variable Standard Bedeutung
POSTGRES_USER planer Benutzer der Datenbank
POSTGRES_PASSWORD (keiner) Pflicht. Muss zu DATABASE_URL passen
POSTGRES_DB planer Name der Datenbank
POSTGRES_PORT 5432 Nach außen veröffentlichter Port
APP_PORT 3000 Nach außen veröffentlichter Port der Anwendung

Anmeldung

Variable Standard Bedeutung
AUTH_TRUST_HOST false Hinter einem Reverse Proxy auf true
ANMELDUNG_PASSWORT false Anmeldung mit E-Mail und Passwort erlauben
OIDC_NAME (leer) Beschriftung des Anmeldeknopfs
OIDC_ISSUER (leer) Issuer-URL des Anbieters
OIDC_CLIENT_ID (leer) Client-ID
OIDC_CLIENT_SECRET (leer) Client-Secret
WEBAUTHN_RP_NAME Planer Name, den das Gerät beim Passkey anzeigt

OIDC ist aktiv, sobald Issuer, Client-ID und Secret gesetzt sind. Der Rückleit-URI beim Anbieter lautet <APP_URL>/api/auth/callback/oidc.

Kalenderanbieter

Variable Standard Bedeutung
GRAPH_CLIENT_ID (leer) Anwendungs-ID der App-Registrierung in Entra ID
GRAPH_CLIENT_SECRET (leer) Geheimnis dieser Registrierung
GRAPH_TENANT_ID organizations Tenant-ID, oder organizations für jeden Arbeitsplatz-Tenant
GOOGLE_CLIENT_ID (leer) OAuth-Client-ID aus der Google Cloud Console
GOOGLE_CLIENT_SECRET (leer) Geheimnis dieses Clients

Ein Anbieter erscheint erst, wenn Kennung und Geheimnis gesetzt sind — und nur, wenn die Instanz ihn erlaubt (Google nur daheim).

Einrichtung und Sonstiges

Variable Standard Bedeutung
ADMIN_EMAIL (leer) Erstes Verwaltungskonto, wird vom Seed angelegt
ADMIN_PASSWORT (leer) Mindestens 12 Zeichen. Ohne den Wert entsteht ein Konto, das nur OIDC oder Passkey kennt
TZ Europe/Berlin Zeitzone für Anzeigen und Tagesgrenzen
DEMO_MODUS false Nur für die Screenshot-Instanz. Im Betrieb niemals true

Kalenderanbieter einrichten

Beides ist einmalige Handarbeit beim Anbieter. Der Planer fragt Lese- und Schreibrechte an: ab M4 schreibt er Zeitblöcke zurück, und eine zweite Zustimmungsrunde für jedes Konto wäre lästiger als eine etwas breitere Berechtigung von Anfang an.

Microsoft 365 (Entra ID)

  1. https://entra.microsoft.com → Identität → Anwendungen → App-Registrierungen → Neue Registrierung.
  2. Name frei wählen, etwa „Planer".
  3. Unterstützte Kontotypen: Nur Konten in diesem Organisationsverzeichnis (dann GRAPH_TENANT_ID auf die Tenant-ID setzen) oder Konten in einem beliebigen Organisationsverzeichnis (dann organizations belassen).
  4. Umleitungs-URI, Plattform Web: <APP_URL>/api/kalender/rueckleitung/graph
  5. Nach dem Anlegen: Anwendungs-ID (Client) nach GRAPH_CLIENT_ID, Verzeichnis-ID (Mandant) nach GRAPH_TENANT_ID.
  6. Zertifikate & Geheimnisse → Neuer geheimer Clientschlüssel. Den Wert sofort nach GRAPH_CLIENT_SECRET kopieren — er wird nur einmal angezeigt.
  7. API-Berechtigungen → Berechtigung hinzufügen → Microsoft Graph → Delegierte Berechtigungen: Calendars.ReadWrite, offline_access, openid, email.

Ist im Tenant die Zustimmung durch Endbenutzer abgeschaltet, muss ein Administrator einmalig Administratorzustimmung erteilen klicken.

Google Kalender

  1. https://console.cloud.google.com → Projekt anlegen oder wählen.
  2. APIs & Dienste → Bibliothek → Google Calendar API aktivieren.
  3. OAuth-Zustimmungsbildschirm einrichten. Für den Eigengebrauch genügt Extern mit Veröffentlichungsstatus Test und der eigenen Adresse als Testbenutzer.
  4. Bereiche (Scopes) hinzufügen: .../auth/calendar.events und .../auth/calendar.readonly.
  5. Anmeldedaten → Anmeldedaten erstellen → OAuth-Client-ID, Anwendungstyp Webanwendung.
  6. Autorisierter Weiterleitungs-URI: <APP_URL>/api/kalender/rueckleitung/google
  7. Client-ID und Clientschlüssel nach GOOGLE_CLIENT_ID und GOOGLE_CLIENT_SECRET.

Bleibt das Projekt im Status Test, laufen die Aktualisierungstoken nach sieben Tagen ab und die Konten müssen neu verbunden werden. Für den Dauerbetrieb den Zustimmungsbildschirm veröffentlichen.

Nach dem Eintragen: docker compose up -d und im Planer unter Kalender verbinden. Die Anleitung für den Alltag steht im Handbuch.

Betrieb

Starten und stoppen

docker compose up -d          # starten
docker compose logs -f app    # Protokoll mitlesen
docker compose down           # stoppen, Daten bleiben

Der Stapel besteht aus drei Diensten: db, ein Einmaldienst migration, der die Migrationen einspielt und sich beendet, und app.

Healthcheck

curl https://planer.example.de/api/health
{
  "status": "ok",
  "version": "0.1.0",
  "instanz": "daheim",
  "datenbank": "ok",
  "zeit": "2026-03-17T08:20:00.000Z"
}

Die Auskunft ist bewusst ohne Anmeldung erreichbar und enthält deshalb keine Zählstände, Namen oder Adressen. Bei "status": "fehler" antwortet der Endpunkt mit HTTP 503.

Aktualisieren

./update.sh                    # auf den neuesten Tag
./update.sh --nur-pruefen      # nur zeigen, was passieren würde
./update.sh --version v0.2.0   # auf eine bestimmte Version
./update.sh --ja               # ohne Rückfrage

Das Skript prüft die Voraussetzungen, zeigt die Änderungen aus dem CHANGELOG, legt eine Sicherung an, wechselt auf den Ziel-Tag, baut, startet und wartet auf einen gesunden /api/health mit der erwarteten Version. Klappt das nicht, rollt es auf die vorherige Version zurück und nennt den Pfad der Sicherung. Jeder Lauf wird nach logs/update-<Datum>.log protokolliert.

Die Datenbank wird beim Zurückrollen nicht automatisch zurückgespielt — das wäre bei einem Teilfehler gefährlicher als hilfreich. Das Skript nennt den Befehl dafür.

Sicherung und Wiederherstellung

./backup.sh                    # Datenbank, .env und Uploads
./backup.sh --tage 90          # längere Aufbewahrung
./backup.sh --nur-datenbank    # nur der Dump

Je Lauf entstehen zwei Dateien in sicherungen/: ein pg_dump im Custom-Format und ein tar.gz mit .env und uploads.

Wiederherstellen:

# Datenbank
docker compose exec -T db pg_restore -U planer -d planer --clean --if-exists \
  < sicherungen/planer-db-2026-03-17_02-00-00.dump

# Dateien
tar -xzf sicherungen/planer-dateien-2026-03-17_02-00-00.tar.gz

Die Sicherung enthält .env und damit alle Geheimnisse. Sie gehört so geschützt wie die Datenbank selbst.

Nächtlich per cron:

0 2 * * * cd /opt/planer && ./backup.sh --tage 30 >> logs/backup.log 2>&1

Entwicklung

npm install
docker compose up -d db         # nur die Datenbank
npm run db:migrieren            # Migrationen einspielen
npm run db:seed                 # Bereiche und Verwaltungskonto
npm run dev                     # Entwicklungsserver

Prüfungen

npm run pruefen           # Prettier, ESLint, tsc, Vitest, ShellCheck
npm run test:integration  # gegen eine echte Datenbank
npm run test:migration    # Migrationen gegen eine Wegwerf-Datenbank
npm run test:e2e          # Playwright gegen die gebaute Anwendung
npm run screenshots       # Handbuchbilder neu aufnehmen

npm run pruefen muss vor jedem Commit grün sein. Fehlt shellcheck lokal, wird das Docker-Abbild koalaman/shellcheck benutzt.

Aufbau

prisma/            Schema, Migrationen, Seed, Demo-Seed
scripts/           Prüf-, Test- und Betriebsskripte
src/app/           Seiten und API-Routen; (app) = angemeldeter Bereich
src/components/    Bausteine; ui/ enthält die Primitiven
src/lib/           Reine Logik ohne DB und ohne Systemzeit, je Datei ein Test
src/server/        DB-nahe Dienste, Server Actions, Provider
e2e/               Playwright-Rauchtests
screenshots/       Aufnahme der Handbuchbilder
docs/              Schnellstart, Handbuch, Bilder
devlog/            Entwicklungstagebuch und Zeiterfassung

Reine Funktionen bekommen die aktuelle Zeit als Parameter, damit sie ohne Tricks prüfbar bleiben. Die Tests laufen bewusst in UTC — so fällt sofort auf, wenn irgendwo die Ortszeit des Servers durchschlägt statt Europe/Berlin.

Bekannte Meldungen

npm audit meldet vier Funde in transitiven Abhängigkeiten der prisma-CLI (deepmerge-ts über @prisma/config, mysql2). Beide sind Entwicklungsabhängigkeiten und erreichen das Laufzeitabbild nicht; MySQL wird ohnehin nie geladen. Der angebotene Fix wäre ein Rückschritt auf Prisma 6.

Sicherheit

  • Anmeldung, Passwort und Passkey antworten bei Fehlschlag immer gleich, damit sich nicht abfragen lässt, wer hier ein Konto hat.
  • Sitzungen sind JWT, aber Rolle und Sperrstatus werden bei jeder Anfrage frisch aus der Datenbank gelesen — eine Sperre wirkt sofort.
  • Jede schreibende Aktion prüft die Rolle in der Seite und noch einmal in der Server Action. Die Oberfläche blendet Knöpfe nur zusätzlich aus.
  • Audit-Details werden vor dem Speichern gesäubert: Felder, deren Name nach Passwort, Token, Betreff oder Mailinhalt aussieht, werden ersetzt.
  • Keine Geheimnisse, Tokens, Mailinhalte oder Termin-Betreffs in Protokollen.

Gefundene Schwachstellen bitte nicht als Issue anlegen, sondern direkt melden.

Lizenz

Privates Projekt, keine Lizenz zur Weiterverwendung.

S
Description
No description provided
Readme
32 MiB
Languages
TypeScript 95.5%
Shell 2.6%
JavaScript 1.3%
CSS 0.4%
Dockerfile 0.2%