Die Beschreibung sagt auch, was die Schnittstelle bewusst nicht kann — Bereich setzen, Aufgaben loeschen, Zeitbloecke anfassen. Das ist eine Entscheidung und kein Versehen, und wer davorsteht, soll es erfahren, statt es fuer eine Luecke zu halten. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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 4 von 8. Die Kernidee steht: Aufgaben lassen sich per Ziehen und Ablegen als Zeitblöcke in den Kalender schreiben, und Verschiebungen von dort kommen zurück. Es folgen Benachrichtigungen, Integrationen, Schnellerfassung vom Handy und ein MCP-Server. Der Fahrplan sagt, was wann folgt.
- Schnellstart – in zehn Minuten von
git clonezur 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
- Wochenplanung per Ziehen und Ablegen: Aufgaben werden zu echten Kalenderterminen, Verschiebungen im Kalender kommen zurück
- Benachrichtigungen per Web-Push, E-Mail und Telegram: Tagesüberblick, fällige Aufgaben, Vorlauf vor einem Zeitblock — mit Ruhezeiten und ab Werk still
- Integrationen: Zammad-Tickets werden zu Aufgaben, Telegram erfasst und hakt ab, und der Spiegel bringt dienstliche Belegung nach Hause — ohne Titel
- Schnellerfassung vom Handy, die auch im Funkloch nichts verliert; der Planer lässt sich als App auf dem Startbildschirm ablegen
- Öffentliche Schnittstelle mit Tokens je Anwendung, Rechten je Token und jeder schreibenden Nutzung im Protokoll (Beschreibung)
- Anmeldung über einen OIDC-Anbieter, über Passkeys oder mit E-Mail und Passwort — je Instanz einzeln zuschaltbar
- Rollen
BenutzerundVerwaltung; 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
Ein MCP-Server. Siehe ROADMAP.md.
Bildergalerie
| Hell | Dunkel | |
|---|---|---|
| Anmelden | ![]() |
![]() |
| Start | ![]() |
![]() |
| Inbox | ![]() |
![]() |
| Woche | ![]() |
![]() |
| Aufgaben | ![]() |
![]() |
| Projekte | ![]() |
![]() |
| Ideen | ![]() |
![]() |
| Kalender | ![]() |
![]() |
| Bereiche | ![]() |
![]() |
| Einstellungen | ![]() |
![]() |
| Meldungen | ![]() |
![]() |
| Integrationen | ![]() |
![]() |
| Protokoll | ![]() |
![]() |
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 |
| 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).
Benachrichtigungen
Beides ist freiwillig. Fehlt ein Satz, bleibt der Weg in den Einstellungen ausgegraut, und der Planer läuft ohne ihn weiter.
| Variable | Standard | Bedeutung |
|---|---|---|
VAPID_OEFFENTLICH |
(leer) | Öffentlicher Push-Schlüssel, geht an den Browser |
VAPID_PRIVAT |
(leer) | Privater Push-Schlüssel, bleibt auf dem Server |
VAPID_KONTAKT |
(leer) | Kontakt für den Push-Dienst, mailto: oder https: |
SMTP_HOST |
(leer) | Mailserver. Ohne ihn kein Mailversand |
SMTP_PORT |
587 |
Port des Mailservers |
SMTP_BENUTZER |
(leer) | Anmeldename. Leer lassen für einen Server ohne Anmeldung |
SMTP_PASSWORT |
(leer) | Passwort dazu |
SMTP_ABSENDER |
(leer) | Absenderadresse, z. B. Planer <planer@example.de> |
SMTP_TLS |
false |
true bei Port 465 (SMTPS), false bei 587 mit STARTTLS |
Push braucht alle drei VAPID-Werte, Mail mindestens SMTP_HOST und
SMTP_ABSENDER. Die Schlüssel entstehen einmalig mit:
npx web-push generate-vapid-keys
Wird das Schlüsselpaar später getauscht, müssen sich alle Geräte neu
anmelden — die alten Abos sind dann wertlos. Push setzt außerdem HTTPS
voraus; http://localhost gilt dem Browser als sicher, jede andere
Klartextadresse nicht.
Was der Planer wann meldet, steht im Handbuch.
Integrationen
Welche Integration eine Instanz überhaupt darf, entscheidet INSTANZ — nicht
die Konfiguration. Ein Zammad-Token in der Instanz daheim bleibt wirkungslos.
| Variable | Standard | Bedeutung |
|---|---|---|
ZAMMAD_URL |
(leer) | Adresse der Zammad-Instanz, nur firma |
ZAMMAD_TOKEN |
(leer) | Token aus Profil → Token-Zugriff |
ZAMMAD_ABFRAGE |
owner_id:me AND … |
Suchausdruck; was darauf passt, wird zur Aufgabe |
TELEGRAM_TOKEN |
(leer) | Bot-Token von @BotFather, nur daheim |
SPIEGEL_GEHEIMNIS |
(leer) | Gemeinsames Geheimnis beider Instanzen |
SPIEGEL_ZIEL |
(leer) | Adresse der empfangenden Instanz; nur auf der sendenden Seite |
Zammad liest nur. Ein Ticket schließt man im Ticketsystem — sonst weiß die Kollegin nicht, warum es plötzlich zu ist. Umgekehrt schließt der Planer eine Aufgabe, sobald Zammad das Ticket als erledigt meldet.
Telegram braucht keinen Webhook und damit keine von außen erreichbare Adresse. Welche Chats mitreden dürfen, wird in der Anwendung unter Integrationen gekoppelt: Knopf drücken, Code an den Bot schicken, fertig. Nachrichten aus unbekannten Chats werden verworfen, ohne zu antworten.
Der Spiegel geht in eine Richtung: firma sendet, daheim empfängt.
Hinüber gehen Belegungen ohne Titel — daheim ist zu sehen, dass die Zeit
weg ist, nicht woran gearbeitet wird — und jene Aufgaben, die einzeln zum
Spiegeln markiert wurden. Jede Sendung ist mit dem gemeinsamen Geheimnis
signiert und älter als fünf Minuten wertlos.
Das Geheimnis erzeugst du wie das Sitzungsgeheimnis:
openssl rand -base64 32
Was der Alltag damit anfängt, steht im Handbuch.
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)
- https://entra.microsoft.com → Identität → Anwendungen → App-Registrierungen → Neue Registrierung.
- Name frei wählen, etwa „Planer".
- Unterstützte Kontotypen: Nur Konten in diesem Organisationsverzeichnis
(dann
GRAPH_TENANT_IDauf die Tenant-ID setzen) oder Konten in einem beliebigen Organisationsverzeichnis (dannorganizationsbelassen). - Umleitungs-URI, Plattform Web:
<APP_URL>/api/kalender/rueckleitung/graph - Nach dem Anlegen: Anwendungs-ID (Client) nach
GRAPH_CLIENT_ID, Verzeichnis-ID (Mandant) nachGRAPH_TENANT_ID. - Zertifikate & Geheimnisse → Neuer geheimer Clientschlüssel. Den Wert
sofort nach
GRAPH_CLIENT_SECRETkopieren — er wird nur einmal angezeigt. - 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
- https://console.cloud.google.com → Projekt anlegen oder wählen.
- APIs & Dienste → Bibliothek → Google Calendar API aktivieren.
- OAuth-Zustimmungsbildschirm einrichten. Für den Eigengebrauch genügt Extern mit Veröffentlichungsstatus Test und der eigenen Adresse als Testbenutzer.
- Bereiche (Scopes) hinzufügen:
.../auth/calendar.eventsund.../auth/calendar.readonly. - Anmeldedaten → Anmeldedaten erstellen → OAuth-Client-ID, Anwendungstyp Webanwendung.
- Autorisierter Weiterleitungs-URI:
<APP_URL>/api/kalender/rueckleitung/google - Client-ID und Clientschlüssel nach
GOOGLE_CLIENT_IDundGOOGLE_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.

























