Files
planer/README.md
Florian WildeandClaude Opus 5 a8b2ff0235 docs: Handbuchseite und Einrichtung der Kalenderanbieter
Die Handbuchseite erklärt, was der Planer liest und was ausdrücklich nicht,
warum alle Kalender gegen jeden Bereich zählen und was beim Trennen mit den
Terminen passiert.

Die README bekommt eine Schritt-für-Schritt-Anleitung für Entra ID und die
Google Cloud Console — mit den genauen Rückleit-URIs und Berechtigungen.
Dazu der Hinweis, dass ein Google-Projekt im Status "Test" die
Aktualisierungstoken nach sieben Tagen verfallen lässt; das kostet sonst
einen halben Abend Fehlersuche.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 18:12:05 +02:00

347 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](ROADMAP.md) sagt, was wann folgt.
- **[Schnellstart](docs/SCHNELLSTART.md)** – in zehn Minuten von `git clone`
zur ersten Anmeldung
- **[Handbuch](docs/handbuch/)** – Anleitung für den Alltag
- **[Fahrplan](ROADMAP.md)** – was geplant ist und was bewusst nicht
- **[Änderungen](CHANGELOG.md)** – 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](ROADMAP.md).
## Bildergalerie
| | Hell | Dunkel |
| ------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| Anmelden | ![Anmeldeseite, hell](docs/bilder/anmelden-hell-schreibtisch.png) | ![Anmeldeseite, dunkel](docs/bilder/anmelden-dunkel-schreibtisch.png) |
| Start | ![Startseite, hell](docs/bilder/start-hell-schreibtisch.png) | ![Startseite, dunkel](docs/bilder/start-dunkel-schreibtisch.png) |
| Inbox | ![Inbox, hell](docs/bilder/inbox-hell-schreibtisch.png) | ![Inbox, dunkel](docs/bilder/inbox-dunkel-schreibtisch.png) |
| Aufgaben | ![Aufgaben, hell](docs/bilder/aufgaben-hell-schreibtisch.png) | ![Aufgaben, dunkel](docs/bilder/aufgaben-dunkel-schreibtisch.png) |
| Projekte | ![Projekte, hell](docs/bilder/projekte-hell-schreibtisch.png) | ![Projekte, dunkel](docs/bilder/projekte-dunkel-schreibtisch.png) |
| Ideen | ![Ideen, hell](docs/bilder/ideen-hell-schreibtisch.png) | ![Ideen, dunkel](docs/bilder/ideen-dunkel-schreibtisch.png) |
| Kalender | ![Kalenderkonten, hell](docs/bilder/kalenderkonten-hell-schreibtisch.png) | ![Kalenderkonten, dunkel](docs/bilder/kalenderkonten-dunkel-schreibtisch.png) |
| Bereiche | ![Bereiche, hell](docs/bilder/bereiche-hell-schreibtisch.png) | ![Bereiche, dunkel](docs/bilder/bereiche-dunkel-schreibtisch.png) |
| Einstellungen | ![Einstellungen, hell](docs/bilder/einstellungen-hell-schreibtisch.png) | ![Einstellungen, dunkel](docs/bilder/einstellungen-dunkel-schreibtisch.png) |
| Protokoll | ![Protokoll, hell](docs/bilder/protokoll-hell-schreibtisch.png) | ![Protokoll, dunkel](docs/bilder/protokoll-dunkel-schreibtisch.png) |
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.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](docs/handbuch/kalenderkonten.md).
## Betrieb
### Starten und stoppen
```bash
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
```bash
curl https://planer.example.de/api/health
```
```json
{
"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
```bash
./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
```bash
./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:
```bash
# 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:
```cron
0 2 * * * cd /opt/planer && ./backup.sh --tage 30 >> logs/backup.log 2>&1
```
## Entwicklung
```bash
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
```bash
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.