Jede Seite erklärt nicht nur, wo man klickt, sondern warum es so gebaut ist: warum die Schnellerfassung nur ein Feld hat, warum die Inbox kein eigener Zustand ist, warum Ideen getrennt liegen und warum Verworfenes nicht in den Fortschritt zählt. README und Schnellstart auf den Stand von M2 gebracht; der Schnellstart endet jetzt mit der ersten erfassten Aufgabe. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
278 lines
13 KiB
Markdown
278 lines
13 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 2 von 8. Erfassen und Wiederfinden steht: Aufgaben,
|
||
> Inbox, Ideen und Projekte, dazu Anmeldung, Rollen, Protokoll und der Betrieb.
|
||
> Das Lesen der Kalender kommt mit M3, die Wochenplanung per Ziehen und Ablegen
|
||
> 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
|
||
- 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
|
||
|
||
Kalender lesen und 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 |  |  |
|
||
| 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`.
|
||
|
||
### 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` |
|
||
|
||
## 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.
|