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>
347 lines
17 KiB
Markdown
347 lines
17 KiB
Markdown
# 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 |  |  |
|
||
| Start |  |  |
|
||
| Inbox |  |  |
|
||
| Aufgaben |  |  |
|
||
| Projekte |  |  |
|
||
| Ideen |  |  |
|
||
| Kalender |  |  |
|
||
| Bereiche |  |  |
|
||
| Einstellungen |  |  |
|
||
| 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 |
|
||
| 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.
|