diff --git a/.gitignore b/.gitignore index 42e9a5c..253c276 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ Thumbs.db .vscode/* !.vscode/extensions.json .idea/ +screenshot-lauf.log diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b662902 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,49 @@ +# Änderungen + +Alle nennenswerten Änderungen an diesem Projekt stehen hier. +Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), +die Versionierung [Semantic Versioning](https://semver.org/lang/de/). + +## [Unveröffentlicht] + +### Hinzugefügt + +- Projektgerüst mit Next 16 (App Router, `output: "standalone"`), React 19, + TypeScript strict, Tailwind 4 und shadcn-Primitiven. +- `npm run pruefen` bündelt Prettier, ESLint, `tsc --noEmit`, Vitest und + ShellCheck. ShellCheck läuft über das Docker-Abbild, wenn es lokal fehlt. +- Datenmodell für Benutzer mit Rolle, OIDC-Konten, Passkeys, Bereiche und + Audit-Einträge; PostgreSQL 16 über Prisma 7 mit pg-Treiberadapter. +- Anmeldung über einen generischen OIDC-Anbieter, über Passkeys + (SimpleWebAuthn) und wahlweise über E-Mail und Passwort. +- Rollen `BENUTZER` und `ADMIN`. Jede schreibende Aktion prüft die Rolle + serverseitig in der Seite **und** noch einmal in der Server Action. +- Audit-Protokoll für jede schreibende Aktion, mit Protokollseite für die + Verwaltung. Details werden vor dem Speichern gesäubert. +- Instanzsteuerung über `INSTANZ=firma|daheim`. Was eine Instanz nicht darf, + liefert dort 404 und lädt seine Konfiguration gar nicht erst. +- Bereiche mit Farbe, Arbeitsfenster und Tageskapazität je Wochentag. +- Zeitrechnung durchgehend in Europe/Berlin, mit Tagesgrenzen, Wochenbeginn + am Montag und deutschen Anzeigeformaten. +- `/api/health` mit Status, Version, Instanz und Erreichbarkeit der Datenbank. +- Hell und Dunkel, Navigation mit Rollenfilter, Oberfläche auf Deutsch. +- Dockerfile in drei Stufen und Compose-Stapel mit eigenem Migrationsdienst. +- `backup.sh` (pg_dump im Custom-Format, `.env` und Uploads), + `update.sh` (mit Rückrollpfad), `scripts/release.sh` und `scripts/zeit.sh`. +- Tests: Unit mit Vitest, Integrationstest und Migrationstest gegen eine + echte Datenbank, E2E-Rauchtest mit Playwright. +- `npm run screenshots` nimmt die Handbuchbilder in einer Wegwerf-Instanz mit + ausschließlich erfundenen Daten auf. +- README, Schnellstart, Handbuch, ROADMAP und DEVLOG. + +### Sicherheit + +- Anmeldung, Passwort und Passkey antworten bei Fehlschlag immer gleich, + damit sich nicht abfragen lässt, wer ein Konto hat. +- Sitzungen als JWT, aber Rolle und Sperrstatus werden bei jeder Anfrage + frisch aus der Datenbank gelesen — eine Sperre wirkt sofort. +- Die WebAuthn-Herausforderung wird nach einem Versuch verworfen, gleich ob + er geglückt ist. +- Der Demo-Seed verweigert den Dienst ohne `DEMO_MODUS=true`. +- `allowDangerousEmailAccountLinking` ist bewusst aktiv: je Instanz gibt es + genau einen vertrauenswürdigen OIDC-Anbieter, der die Adresse selbst prüft. diff --git a/README.md b/README.md new file mode 100644 index 0000000..71244c4 --- /dev/null +++ b/README.md @@ -0,0 +1,267 @@ +# 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 1 von 8. Die Grundlage steht: Anmeldung, Rollen, +> Protokoll, Bereiche und der Betrieb. Aufgaben kommen mit M2, das Lesen der +> Kalender mit M3, die Wochenplanung 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 + +- 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 + +Aufgaben und Projekte, 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 | ![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) | +| 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 `/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-.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. diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..4a3bbd7 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,124 @@ +# Fahrplan + +Acht Meilensteine. Jeder endet mit einer lauffähigen Version auf `main`, +grünen Prüfungen, aktueller Dokumentation und einem Eintrag im DEVLOG. + +| Meilenstein | Ziel | Status | +| --------------------------------------- | ---------------------------- | --------- | +| [M1](#m1--fundament) | Fundament | in Arbeit | +| [M2](#m2--aufgaben-und-projekte) | Aufgaben und Projekte | geplant | +| [M3](#m3--kalender-lesen) | Kalender lesen | geplant | +| [M4](#m4--wochenplanung-und-zeitblöcke) | Wochenplanung und Zeitblöcke | geplant | +| [M5](#m5--benachrichtigungen) | Benachrichtigungen | geplant | +| [M6](#m6--integrationen) | Integrationen | geplant | +| [M7](#m7--schnellerfassung-und-api) | Schnellerfassung und API | geplant | +| [M8](#m8--mcp) | MCP | geplant | + +--- + +## M1 – Fundament + +**Ziel:** Ein angemeldeter, betreibbarer Rumpf. Noch keine Aufgaben, kein +Kalender — aber alles, worauf die späteren Meilensteine aufsetzen. + +- [x] Projektgerüst: Next 16, React 19, TypeScript strict, Tailwind 4 +- [x] `npm run pruefen`: Prettier, ESLint, tsc, Vitest, ShellCheck +- [x] Datenmodell für Anmeldung, Passkeys, Bereiche und Audit +- [x] Anmeldung über OIDC, Passkey und wahlweise Passwort +- [x] Rollen `BENUTZER` und `ADMIN`, serverseitig in Seite **und** Server Action +- [x] Audit-Eintrag je schreibender Aktion, Details werden gesäubert +- [x] Instanzsteuerung `INSTANZ=firma|daheim` mit 404 statt Hinweis +- [x] Bereiche verwalten: Farbe, Arbeitsfenster, Tageskapazität +- [x] Protokollseite für die Verwaltung +- [x] `/api/health` mit Status, Version und Instanz +- [x] Dockerfile, Compose-Stapel mit eigenem Migrationsdienst +- [x] `backup.sh`, `update.sh`, `scripts/release.sh`, `scripts/zeit.sh` +- [x] Unit-, Integrations-, Migrations- und E2E-Tests +- [x] Screenshot-Kette mit eigener Wegwerf-Instanz +- [x] README, Schnellstart, Handbuch, CHANGELOG, DEVLOG + +**Bewusst verschoben:** `Aufgabe` und `Zeitblock` kommen mit M2 und M4, +`Kalenderkonto` mit M3 — jeweils mit den Feldern, die der Provider dann +wirklich braucht, statt heute geraten. + +## M2 – Aufgaben und Projekte + +**Ziel:** Alles erfassen und wiederfinden, noch ohne Kalender. + +- [ ] `Aufgabe` mit Typ, Status, Priorität, Aufwand, Fälligkeit, Quelle +- [ ] Eindeutigkeit `[quelle, quelleRef]`, Integrationen upserten +- [ ] Inbox: schnell erfassen, später einsortieren +- [ ] Ideen getrennt von Aufgaben +- [ ] Projekte mit Arbeitspaketen und Meilensteinen +- [ ] Listen mit Filtern nach Bereich, Status und Fälligkeit +- [ ] Handbuchseiten und Bilder + +## M3 – Kalender lesen + +**Ziel:** Termine aus Microsoft 365 und Google sehen. + +- [ ] `Kalenderkonto` mit Anbieter, Tokens und Delta-Verweis +- [ ] OAuth-Verbindung je Instanz: Graph immer, Google nur daheim +- [ ] Graph-Delta-Sync und Google-Sync über `fetch`, Antworten mit zod geprüft +- [ ] Ganztägige Termine zählen nicht zur Belegung +- [ ] Anonymisierte Beispielantworten in `test/fixtures/` +- [ ] Hintergrundjob mit node-cron + +## M4 – Wochenplanung und Zeitblöcke + +**Ziel:** Aufgaben per Ziehen und Ablegen in den Kalender schreiben — und +Verschiebungen von dort zurückholen. + +- [ ] `Zeitblock` an genau einer Aufgabe und einem Kalenderkonto +- [ ] Wochenansicht mit `@dnd-kit/core` +- [ ] Schreiben in den Kalender, Rückweg beim Verschieben +- [ ] Schleifenschutz über `hash = sha256(startISO|endeISO)` +- [ ] Konflikte: der Kalender gewinnt (HTTP 412 auf If-Match) +- [ ] Belegung gegen die Tageskapazität des Bereichs + +## M5 – Benachrichtigungen + +- [ ] Web-Push mit `web-push` +- [ ] Tagesüberblick per E-Mail über nodemailer +- [ ] Erinnerungen an Fälligkeiten, Jobs über node-cron +- [ ] Je Benutzer einstellbar, mit Ruhezeiten + +## M6 – Integrationen + +- [ ] Zammad und Wartungsplaner-API (nur `firma`) +- [ ] Telegram (nur `daheim`) +- [ ] Spiegel: `firma` sendet, `daheim` empfängt +- [ ] Jede Integration upsertet über `[quelle, quelleRef]` + +## M7 – Schnellerfassung und API + +- [ ] Erfassung vom Handy, auch offline +- [ ] Öffentliche API mit Token je Anwendung +- [ ] Rechte je Token, jede Nutzung im Protokoll + +## M8 – MCP + +- [ ] MCP-Server über `@modelcontextprotocol/sdk` +- [ ] Lesen und Anlegen von Aufgaben und Zeitblöcken +- [ ] Rechte wie bei der API, jede Nutzung im Protokoll + +--- + +## Ideen für später + +- Auswertung: wie viel Zeit ging tatsächlich in welchen Bereich +- Wiederkehrende Aufgaben mit eigenem Rhythmus +- Vorlagen für wiederkehrende Projekte +- Verknüpfung von Aufgaben untereinander (blockiert, gehört zu) +- Export der eigenen Daten als JSON + +## Bewusst nicht geplant + +- **Wiederkehrende Wartungsaufgaben mit Nachweispflicht.** Die bleiben im + Wartungsplaner. Der Planer liest nur Fälligkeiten über dessen API. +- **Mehrbenutzerbetrieb mit geteilten Aufgaben.** Der Planer ist für eine + Person gedacht; Kollegen melden sich in der Firmeninstanz nur an ihren + eigenen Daten an. +- **Eigene Kalenderdarstellung als Ersatz für Outlook.** Der Planer schreibt + in die vorhandenen Kalender, er ersetzt sie nicht. +- **Zeiterfassung zur Abrechnung.** Dafür gibt es andere Werkzeuge. diff --git a/docs/SCHNELLSTART.md b/docs/SCHNELLSTART.md new file mode 100644 index 0000000..4651e79 --- /dev/null +++ b/docs/SCHNELLSTART.md @@ -0,0 +1,155 @@ +# Schnellstart + +Von `git clone` bis zur ersten Anmeldung in höchstens zehn Minuten. +Jeder Befehl ist zum Kopieren, jeder Schritt nennt das erwartete Ergebnis. + +## Voraussetzungen + +| Werkzeug | Fassung | Prüfen mit | +| ------------------ | ---------------- | ------------------------ | +| Docker mit Compose | 25 oder neuer | `docker compose version` | +| Git | 2.40 oder neuer | `git --version` | +| Node.js | 22.12 oder neuer | `node -v` | + +Node wird nur für die Entwicklung gebraucht. Für den reinen Betrieb genügen +Docker und Git. + +## 1. Holen und Verzeichnis wechseln + +```bash +git clone planer +cd planer +``` + +**Erwartet:** Das Verzeichnis `planer` enthält unter anderem `package.json`, +`docker-compose.yml` und `.env.example`. + +## 2. Konfiguration anlegen + +```bash +cp .env.example .env +``` + +Jetzt die Pflichtwerte setzen. Die Geheimnisse werden erzeugt, nicht erfunden: + +```bash +# Signiert die Sitzungen. Ohne diesen Wert startet die Anwendung nicht. +openssl rand -base64 32 + +# Ein Passwort für die Datenbank. +openssl rand -hex 16 +``` + +Unter Windows ohne OpenSSL geht auch: + +```bash +node -e "console.log(require('crypto').randomBytes(32).toString('base64'))" +``` + +Diese Werte in `.env` eintragen: + +| Variable | Beispiel | Bedeutung | +| ------------------- | -------------------------------------------------------------------- | ------------------------------------------------------- | +| `INSTANZ` | `daheim` | `firma` oder `daheim` – bestimmt, was es überhaupt gibt | +| `APP_URL` | `http://localhost:3000` | Adresse ohne Schrägstrich am Ende | +| `AUTH_SECRET` | _(erzeugt)_ | Signatur der Sitzungen | +| `POSTGRES_PASSWORD` | _(erzeugt)_ | Passwort der Datenbank | +| `DATABASE_URL` | `postgresql://planer:@localhost:5432/planer?schema=public` | muss dasselbe Passwort enthalten | +| `WEBAUTHN_RP_ID` | `localhost` | nackte Domain, ohne Schema und Port | +| `WEBAUTHN_ORIGIN` | `http://localhost:3000` | exakt wie im Browser, mit Port | +| `ADMIN_EMAIL` | `du@example.de` | erstes Verwaltungskonto | +| `ADMIN_PASSWORT` | _(mindestens 12 Zeichen)_ | nur nötig, wenn du dich ohne OIDC anmelden willst | + +> **Sind die Ports 5432 oder 3000 auf deinem Rechner belegt?** +> Dann `POSTGRES_PORT` beziehungsweise `APP_PORT` in `.env` auf einen freien +> Wert setzen. `DATABASE_URL`, `APP_URL` und `WEBAUTHN_ORIGIN` müssen denselben +> Port nennen. + +**Erwartet:** `.env` liegt im Projektverzeichnis. Sie gehört niemals ins Git — +`.gitignore` sorgt dafür. + +## 3. Starten + +```bash +docker compose up -d --build +``` + +Beim ersten Mal dauert der Bau einige Minuten. Der Ablauf ist: Datenbank +starten, Migrationen einspielen, Anwendung starten. + +**Erwartet:** + +```bash +docker compose ps +``` + +zeigt `db` als `healthy` und `app` als `Up`. Der Dienst `migration` taucht +nicht mehr auf — er hat seine Arbeit getan und sich beendet. + +## 4. Prüfen, ob alles läuft + +```bash +curl http://localhost:3000/api/health +``` + +**Erwartet:** + +```json +{ "status": "ok", "version": "0.1.0", "instanz": "daheim", "datenbank": "ok" } +``` + +Steht dort `"datenbank": "fehler"`, stimmt `DATABASE_URL` nicht mit +`POSTGRES_PASSWORD` überein. + +## 5. Erste Bereiche und das Verwaltungskonto anlegen + +```bash +docker compose run --rm migration node node_modules/prisma/build/index.js db seed +``` + +**Erwartet:** Die Ausgabe nennt die vier angelegten Bereiche (Firma, Gewerbe, +Privat, Homelab) und bestätigt das Verwaltungskonto. + +## 6. Anmelden + +Im Browser öffnen. + +**Erwartet:** Die Anmeldeseite. Mit `ADMIN_EMAIL` und `ADMIN_PASSWORT` +anmelden — danach steht oben „Guten Tag" und darunter die vier Bereiche. + +> Ist OIDC eingerichtet, erscheint zusätzlich ein Knopf mit dem Namen aus +> `OIDC_NAME`. Der Rückleit-URI, den du beim Anbieter hinterlegen musst, +> lautet `/api/auth/callback/oidc`. + +## 7. Einen Passkey hinterlegen + +Auf **Einstellungen** gehen, unter _Passkeys_ einen Namen für das Gerät +eintragen (etwa „Arbeitsrechner") und auf **Anlegen** klicken. Das Gerät +fragt nach Fingerabdruck, Gesicht oder PIN. + +**Erwartet:** Der Passkey steht in der Liste. Ab jetzt genügt zum Anmelden die +E-Mail-Adresse und **Mit Passkey anmelden**. + +> Hinterlege am besten zwei Passkeys auf verschiedenen Geräten. Sonst sperrst +> du dich aus, wenn ein Gerät verlorengeht. + +## 8. Aufräumen + +Wenn `ADMIN_PASSWORT` nur zur Einrichtung gedient hat, kannst du es jetzt +zusammen mit `ADMIN_EMAIL` aus `.env` entfernen. Das Konto bleibt bestehen. + +## Wie es weitergeht + +- [Handbuch](handbuch/) – Anleitung für den Alltag +- [README](../README.md) – alle Umgebungsvariablen, Betrieb, Sicherung +- [ROADMAP](../ROADMAP.md) – was in den nächsten Meilensteinen kommt + +## Wenn etwas klemmt + +| Symptom | Ursache und Abhilfe | +| ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `INSTANZ fehlt in .env` beim Start | `INSTANZ` ist leer. Erlaubt sind nur `firma` und `daheim`. | +| `Die Konfiguration ist unvollständig` | Die Meldung nennt die fehlende Variable. Sie nennt nie den Wert. | +| `port is already allocated` | Ein anderer Dienst hält den Port. `APP_PORT` oder `POSTGRES_PORT` ändern. | +| Passkey lässt sich nicht anlegen | `WEBAUTHN_ORIGIN` muss exakt der Adresse im Browser entsprechen, inklusive Port. `WEBAUTHN_RP_ID` ist die nackte Domain, ohne `https://`. | +| Anmeldung schlägt immer fehl | Bewusst gibt es nur eine Meldung für alle Fälle. Prüfe im Protokoll der Anwendung: `docker compose logs app`. | diff --git a/docs/handbuch/README.md b/docs/handbuch/README.md new file mode 100644 index 0000000..cdbc4f8 --- /dev/null +++ b/docs/handbuch/README.md @@ -0,0 +1,36 @@ +# Handbuch + +Anleitung für den Alltag mit dem Planer. Eine Seite je Thema. + +## Erste Schritte + +1. **[Anmelden](anmelden.md)** – die drei Wege hinein, Passkeys einrichten, + was tun, wenn nichts mehr geht +2. **[Bereiche einrichten](bereiche.md)** – Farbe, Arbeitsfenster und + Tageskapazität festlegen + +## Verwaltung + +3. **[Protokoll](protokoll.md)** – nachsehen, wer wann was geändert hat + +## Was noch kommt + +Diese Seiten entstehen mit den nächsten Meilensteinen: + +| Thema | Meilenstein | +| ------------------------------- | ----------- | +| Aufgaben erfassen, Inbox, Ideen | M2 | +| Projekte und Arbeitspakete | M2 | +| Kalenderkonten verbinden | M3 | +| Wochenplanung und Zeitblöcke | M4 | +| Benachrichtigungen | M5 | +| Integrationen | M6 | +| Schnellerfassung vom Handy, API | M7 | +| MCP | M8 | + +Der [Fahrplan](../../ROADMAP.md) sagt, was in welchem Meilenstein steckt. + +--- + +Zur Einrichtung einer neuen Instanz: [Schnellstart](../SCHNELLSTART.md). +Für Betrieb, Sicherung und alle Umgebungsvariablen: [README](../../README.md). diff --git a/docs/handbuch/anmelden.md b/docs/handbuch/anmelden.md new file mode 100644 index 0000000..acef851 --- /dev/null +++ b/docs/handbuch/anmelden.md @@ -0,0 +1,83 @@ +# Anmelden + +Es gibt drei Wege in den Planer. Welche davon zu sehen sind, hängt davon ab, +was für deine Instanz eingerichtet wurde. + +![Die Anmeldeseite](../bilder/anmelden-hell-schreibtisch.png) + +| Weg | Wann sinnvoll | +| -------------------------------- | --------------------------------------------------------------------------------------------------- | +| **Unternehmensanmeldung** (OIDC) | In der Firma der Normalfall. Ein Klick, die Anmeldung übernimmt Entra ID beziehungsweise authentik. | +| **Passkey** | Der bequemste Weg am eigenen Gerät: Fingerabdruck, Gesicht oder Geräte-PIN, kein Passwort. | +| **E-Mail und Passwort** | Der Rückfallweg. Oft nur daheim freigeschaltet. | + +## Mit Passkey anmelden + +1. E-Mail-Adresse eintragen. +2. Auf **Mit Passkey anmelden** klicken. +3. Das Gerät fragt nach Fingerabdruck, Gesicht oder PIN. + +Fertig — du landest auf der Startseite. + +> **Warum erst die E-Mail-Adresse?** Damit der Planer weiß, welche Passkeys er +> anbieten soll. Er antwortet dabei für unbekannte Adressen genauso wie für +> bekannte, damit sich von außen nicht abfragen lässt, wer hier ein Konto hat. + +## Einen Passkey einrichten + +Das geht erst, wenn du angemeldet bist — also beim ersten Mal über OIDC oder +über E-Mail und Passwort. + +1. Oben rechts auf **Einstellungen**. +2. Im Abschnitt _Passkeys_ einen Namen für das Gerät eintragen, etwa + „Arbeitsrechner" oder „Diensthandy". +3. Auf **Anlegen** klicken und die Abfrage des Geräts bestätigen. + +![Passkeys in den Einstellungen](../bilder/einstellungen-hell-schreibtisch.png) + +Der Passkey steht danach in der Liste, mit Anlegedatum und letzter Benutzung. + +> **Lege zwei Passkeys an, auf zwei verschiedenen Geräten.** Sonst sperrst du +> dich aus, sobald ein Gerät verlorengeht oder zurückgesetzt wird. + +Unter jedem Eintrag steht, ob der Passkey **geräteübergreifend** ist — dann +liegt er im Schlüsselbund von Apple, Google oder deinem Passwortmanager und ist +auf deinen anderen Geräten ebenfalls verfügbar. Steht dort **nur dieses +Gerät**, ist er an genau dieses Gerät gebunden. + +## Einen Passkey entfernen + +In den Einstellungen auf das Papierkorb-Symbol neben dem Eintrag. Der Passkey +ist sofort ungültig; auf dem Gerät selbst bleibt ein Rest zurück, den du dort +im Schlüsselbund löschen kannst. + +## Abmelden + +Oben rechts auf das Symbol mit dem Pfeil aus der Tür. + +## Wenn es klemmt + +| Was du siehst | Was dahintersteckt | +| -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| „Anmeldung nicht möglich. Bitte Angaben prüfen." | Diese eine Meldung gilt für alle Fälle: falsches Passwort, unbekannte Adresse, gesperrtes Konto. Das ist Absicht — sonst ließe sich abfragen, wer hier ein Konto hat. | +| Es passiert nichts nach dem Klick auf **Mit Passkey anmelden** | Du hast den Systemdialog abgebrochen. Einfach noch einmal klicken. | +| „Dieses Gerät ist hier schon hinterlegt." | Für dieses Gerät gibt es bereits einen Passkey. Ein zweiter bringt nichts. | +| Der Passkey-Knopf tut gar nichts | Der Browser unterstützt WebAuthn nicht, oder die Seite läuft nicht über HTTPS. Passkeys brauchen eine sichere Verbindung — `localhost` ausgenommen. | +| Passkey lässt sich nicht anlegen | Meist stimmt die Konfiguration nicht: `WEBAUTHN_ORIGIN` muss exakt der Adresse im Browser entsprechen, inklusive Port. Siehe [README](../../README.md). | + +## Ausgesperrt? + +Wenn kein Weg mehr hineinführt, hilft nur jemand mit Zugriff auf den Server. +Dort lässt sich mit `ADMIN_EMAIL` und `ADMIN_PASSWORT` in der `.env` und einem +erneuten Seed-Lauf wieder ein Passwort setzen — siehe +[Schnellstart, Schritt 5](../SCHNELLSTART.md). + +## Rollen + +Unten in der Fußzeile und in den Einstellungen steht, welche Rolle du hast. + +- **Benutzer** – sieht und plant die eigenen Sachen. +- **Verwaltung** – darf zusätzlich Bereiche ändern und das + [Protokoll](protokoll.md) einsehen. + +Die Rolle vergibt die Verwaltung; sie lässt sich nicht selbst hochstufen. diff --git a/docs/handbuch/bereiche.md b/docs/handbuch/bereiche.md new file mode 100644 index 0000000..136af55 --- /dev/null +++ b/docs/handbuch/bereiche.md @@ -0,0 +1,80 @@ +# Bereiche einrichten + +Ein **Bereich** ist ein Lebensbereich: Firma, Gewerbe, Privat, Homelab. Jede +Aufgabe und jeder Zeitblock gehört später zu genau einem davon. + +Der Bereich bestimmt drei Dinge: + +- **die Farbe**, an der du im Kalender sofort erkennst, worum es geht, +- **das Arbeitsfenster** — die Tageszeit, in der für diesen Bereich überhaupt + geplant wird, +- **die Tageskapazität** — wie viele Minuten je Wochentag höchstens + hineingehen. + +![Die Bereichsseite](../bilder/bereiche-hell-schreibtisch.png) + +> Bereiche ändern darf nur die **Verwaltung**. Als Benutzer siehst du sie, +> aber ohne die Formulare. + +## Einen Bereich anlegen + +1. Im Menü auf **Bereiche**. +2. Ganz unten unter _Neuen Bereich anlegen_ die Felder ausfüllen. +3. Auf **Bereich anlegen** klicken. + +| Feld | Bedeutung | +| ---------------------------- | ----------------------------------------------------------------------------------------------- | +| **Name** | Zwei bis 40 Zeichen, muss eindeutig sein | +| **Farbe** | Wird zum Farbpunkt in Listen und später im Kalender | +| **Arbeitsfenster von / bis** | Zwischen diesen Uhrzeiten wird geplant. Muss nach dem Beginn enden | +| **Tageskapazität** | Minuten je Wochentag. 0 heißt: an diesem Tag gar nicht | +| **In Benutzung** | Abgehakt lassen. Ohne Haken bleibt der Bereich erhalten, taucht aber nicht mehr zur Auswahl auf | +| **Reihenfolge** | Kleinere Zahlen stehen weiter oben | + +## Was eine sinnvolle Kapazität ist + +Nicht die Zeit, die der Tag hergibt — die Zeit, die **wirklich für geplante +Arbeit** übrig bleibt, nach Besprechungen, Störungen und Pausen. + +Ein brauchbarer Anfang: + +| Bereich | Montag bis Donnerstag | Freitag | Wochenende | +| ------- | --------------------- | ------- | --------------- | +| Firma | 360 min (6 h) | 240 min | 0 | +| Gewerbe | 90 min | 60 min | Samstag 180 min | +| Privat | 60 min | 120 min | je 240 min | +| Homelab | 0 bis 60 min | 0 | Samstag 120 min | + +Wenn am Ende der Woche regelmäßig etwas liegenbleibt, ist die Kapazität zu +hoch angesetzt, nicht der Tag zu kurz. + +> Die Tageskapazität darf nie länger sein als das Arbeitsfenster. Ein Fenster +> von 08:00 bis 17:00 fasst höchstens 540 Minuten; trägst du mehr ein, weist +> das Formular es ab. + +## Einen Bereich ändern + +Auf **Bearbeiten** unter dem Bereich klicken, ändern, **Speichern**. Mit +**Schließen** verwirfst du, was du noch nicht gespeichert hast. + +## Einen Bereich loswerden + +Zwei Wege, und meistens ist der erste der richtige: + +1. **Stilllegen** – den Haken bei _In Benutzung_ entfernen und speichern. Alles + Bisherige bleibt erhalten, der Bereich taucht nur nicht mehr zur Auswahl auf. +2. **Löschen** – der rote Knopf. Endgültig. + +> Ab M2 hängen Aufgaben an Bereichen. Dann ist Stilllegen fast immer die +> richtige Wahl, weil beim Löschen auch die Zuordnung verlorenginge. + +Jede Änderung landet im [Protokoll](protokoll.md). + +## Typische Fehler + +| Meldung | Was zu tun ist | +| --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| „Diesen Namen gibt es schon." | Namen sind eindeutig. Vielleicht existiert der Bereich stillgelegt schon. | +| „Das Arbeitsfenster muss nach seinem Beginn enden." | Von-Zeit liegt hinter der Bis-Zeit. Über Mitternacht hinweg geht nicht — dafür lieber zwei Bereiche. | +| „Keine Tageskapazität darf länger sein als das Arbeitsfenster." | Entweder die Kapazität senken oder das Fenster weiten. | +| „Dazu fehlt dir die Berechtigung." | Du bist als Benutzer angemeldet, nicht als Verwaltung. | diff --git a/docs/handbuch/protokoll.md b/docs/handbuch/protokoll.md new file mode 100644 index 0000000..65be422 --- /dev/null +++ b/docs/handbuch/protokoll.md @@ -0,0 +1,57 @@ +# Protokoll + +Jede schreibende Aktion im Planer hinterlässt einen Eintrag: wer, wann, was, +und woran. Einträge lassen sich weder ändern noch löschen. + +Die Seite steht nur der **Verwaltung** offen. Als Benutzer siehst du den +Menüpunkt gar nicht erst; wer die Adresse direkt aufruft, landet auf einem +Hinweis. + +![Die Protokollseite](../bilder/protokoll-hell-schreibtisch.png) + +## Was dort steht + +| Spalte | Bedeutung | +| ------------- | --------------------------------------------------------------------- | +| **Zeitpunkt** | Datum und Uhrzeit in Europe/Berlin | +| **Wer** | Die E-Mail-Adresse zum Zeitpunkt der Aktion | +| **Was** | Die Aktion, in der Form `objekt.verb` | +| **Objekt** | Die Art des betroffenen Objekts, dahinter die fachlichen Einzelheiten | + +Angezeigt werden die letzten 100 Einträge, die neuesten zuerst. + +## Die Aktionen + +| Aktion | Bedeutung | +| ------------------- | --------------------------------------------------- | +| `bereich.angelegt` | Ein neuer Bereich ist entstanden | +| `bereich.geaendert` | Name, Farbe, Fenster oder Kapazität wurden geändert | +| `bereich.geloescht` | Ein Bereich wurde entfernt | +| `passkey.angelegt` | Jemand hat ein Gerät hinterlegt | +| `passkey.entfernt` | Ein Gerät wurde entfernt | + +Mit jedem Meilenstein kommen Aktionen hinzu. + +## Was dort bewusst **nicht** steht + +- Keine Passwörter, Hashes oder Tokens. +- Keine Termin-Betreffs und keine Mailinhalte. +- Keine Zugangsdaten der Kalenderkonten. + +Das ist doppelt abgesichert: Solche Felder werden gar nicht erst übergeben, +und vor dem Speichern ersetzt der Planer zusätzlich jedes Feld, dessen Name +nach einem Geheimnis aussieht, durch `[entfernt]`. Wenn du dort `[entfernt]` +liest, hat dieser zweite Riegel gegriffen. + +## Warum die E-Mail-Adresse und nicht nur die Kennung + +Wird ein Benutzer gelöscht, verschwinden seine Passkeys und verknüpften +Konten — der Protokolleintrag bleibt. Die Verknüpfung zum Konto wird gelöst, +die Adresse bleibt als Nachweis stehen. Sonst wäre das Protokoll nach einer +Löschung wertlos. + +## Aufbewahrung + +Einträge werden nicht automatisch entfernt. Die Tabelle wächst langsam — ein +Eintrag je Änderung, nicht je Seitenaufruf. Sollte sie einmal stören, gehört +das Aufräumen in die Sicherungsroutine und nicht in die Anwendung.