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>
This commit is contained in:
Florian Wilde
2026-09-10 18:12:05 +02:00
co-authored by Claude Opus 5
parent df50627a20
commit a8b2ff0235
8 changed files with 303 additions and 25 deletions
+15
View File
@@ -59,6 +59,21 @@ WEBAUTHN_RP_NAME=Planer
# Muss exakt der Herkunft im Browser entsprechen, inklusive Port. # Muss exakt der Herkunft im Browser entsprechen, inklusive Port.
WEBAUTHN_ORIGIN=http://localhost:3000 WEBAUTHN_ORIGIN=http://localhost:3000
# --- Kalenderanbieter ------------------------------------------------------
# Graph gibt es in beiden Instanzen, Google nur daheim. Ein Anbieter erscheint
# erst, wenn Kennung UND Geheimnis gesetzt sind.
# Einrichtung siehe README, Abschnitt "Kalenderanbieter einrichten".
# Rueckleit-URIs:
# <APP_URL>/api/kalender/rueckleitung/graph
# <APP_URL>/api/kalender/rueckleitung/google
GRAPH_CLIENT_ID=
GRAPH_CLIENT_SECRET=
# Tenant-ID, oder "organizations" fuer jeden Arbeitsplatz-Tenant.
GRAPH_TENANT_ID=organizations
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# --- Sonstiges ------------------------------------------------------------- # --- Sonstiges -------------------------------------------------------------
# Zeitzone fuer Anzeigen und Tagesgrenzen. # Zeitzone fuer Anzeigen und Tagesgrenzen.
TZ=Europe/Berlin TZ=Europe/Berlin
+28
View File
@@ -6,6 +6,34 @@ die Versionierung [Semantic Versioning](https://semver.org/lang/de/).
## [Unveröffentlicht] ## [Unveröffentlicht]
### Hinzugefügt
- Kalenderkonten für Microsoft 365 (Graph) und Google verbinden, umbenennen,
von Hand abgleichen und trennen. Google gibt es nur in der Instanz
`daheim`; in `firma` liefert die Route 404.
- Termine werden örtlich gespiegelt: 7 Tage zurück bis 90 Tage voraus.
Gelesen werden nur Beginn, Ende, Betreff, ganztägig und "zeigt an als" —
keine Teilnehmer, Beschreibungen, Anhänge oder Orte.
- Die Startseite zeigt je Bereich, wie voll der heutige Tag ist: Termine aus
allen verbundenen Kalendern gegen die Tageskapazität.
- Hintergrundjobs: alle 15 Minuten abgleichen, nachts alte Termine und
abgelaufene WebAuthn-Herausforderungen entfernen.
- Handbuchseite zu den Kalenderkonten, Einrichtungsanleitung für Entra ID
und die Google Cloud Console in der README.
### Sicherheit
- Zugangs- und Aktualisierungstoken liegen verschlüsselt in der Datenbank
(AES-256-GCM, Schlüssel aus AUTH_SECRET abgeleitet). Wer eine Sicherung in
die Hände bekommt, kann damit ohne AUTH_SECRET nichts anfangen.
- Der Zustandswert im OAuth-Ablauf ist signiert und trägt die
Benutzerkennung; ohne das ließe sich ein fremdes Kalenderkonto an ein
beliebiges Konto hängen. Er verfällt nach 30 Minuten.
- Fehlermeldungen nennen nie den Antwortkörper des Token-Endpunkts — der
enthält das Client-Secret oft im Echo.
- Protokolliert werden beim Abgleich nur Zahlen, nie Kontonamen oder
Termininhalte.
## [0.2.0] – 2026-09-10 ## [0.2.0] – 2026-09-10
### Hinzugefügt ### Hinzugefügt
+75 -6
View File
@@ -7,10 +7,10 @@ lesen und Aufgaben per Ziehen und Ablegen als Zeitblöcke direkt in diese
Kalender schreiben — inklusive Rückweg, wenn ein Block im Kalender verschoben Kalender schreiben — inklusive Rückweg, wenn ein Block im Kalender verschoben
wird. wird.
> **Stand:** Meilenstein 2 von 8. Erfassen und Wiederfinden steht: Aufgaben, > **Stand:** Meilenstein 3 von 8. Aufgaben, Projekte und das Lesen der
> Inbox, Ideen und Projekte, dazu Anmeldung, Rollen, Protokoll und der Betrieb. > Kalender stehen: der Planer weiß jetzt, wann überhaupt Platz ist. Das
> Das Lesen der Kalender kommt mit M3, die Wochenplanung per Ziehen und Ablegen > Schreiben der Zeitblöcke per Ziehen und Ablegen kommt mit M4. Der
> mit M4. Der [Fahrplan](ROADMAP.md) sagt, was wann folgt. > [Fahrplan](ROADMAP.md) sagt, was wann folgt.
- **[Schnellstart](docs/SCHNELLSTART.md)** – in zehn Minuten von `git clone` - **[Schnellstart](docs/SCHNELLSTART.md)** – in zehn Minuten von `git clone`
zur ersten Anmeldung zur ersten Anmeldung
@@ -28,6 +28,8 @@ wird.
nach Dringlichkeit und filterbar über die Adresszeile nach Dringlichkeit und filterbar über die Adresszeile
- **Ideen** getrennt von Aufgaben — ohne Termin, per Klick übernehmbar - **Ideen** getrennt von Aufgaben — ohne Termin, per Klick übernehmbar
- **Projekte** mit Arbeitspaketen, Meilensteinen und Fortschritt - **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 - Anmeldung über einen OIDC-Anbieter, über Passkeys oder mit E-Mail und
Passwort — je Instanz einzeln zuschaltbar Passwort — je Instanz einzeln zuschaltbar
- Rollen `Benutzer` und `Verwaltung`; jede schreibende Aktion wird - Rollen `Benutzer` und `Verwaltung`; jede schreibende Aktion wird
@@ -38,20 +40,21 @@ wird.
### Geplant ### Geplant
Kalender lesen und schreiben, Wochenplanung per Ziehen und Ablegen, Zeitblöcke in den Kalender schreiben, Wochenplanung per Ziehen und Ablegen,
Benachrichtigungen, Integrationen, Schnellerfassung vom Handy und ein Benachrichtigungen, Integrationen, Schnellerfassung vom Handy und ein
MCP-Server. Siehe [ROADMAP.md](ROADMAP.md). MCP-Server. Siehe [ROADMAP.md](ROADMAP.md).
## Bildergalerie ## Bildergalerie
| | Hell | Dunkel | | | Hell | Dunkel |
| ------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------- | | ------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| Anmelden | ![Anmeldeseite, hell](docs/bilder/anmelden-hell-schreibtisch.png) | ![Anmeldeseite, dunkel](docs/bilder/anmelden-dunkel-schreibtisch.png) | | 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) | | 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) | | 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) | | 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) | | 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) | | 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) | | 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) | | 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) | | Protokoll | ![Protokoll, hell](docs/bilder/protokoll-hell-schreibtisch.png) | ![Protokoll, dunkel](docs/bilder/protokoll-dunkel-schreibtisch.png) |
@@ -117,6 +120,19 @@ Alle Werte stehen in `.env`. Vorlage: [`.env.example`](.env.example).
OIDC ist aktiv, sobald Issuer, Client-ID und Secret gesetzt sind. Der OIDC ist aktiv, sobald Issuer, Client-ID und Secret gesetzt sind. Der
Rückleit-URI beim Anbieter lautet `<APP_URL>/api/auth/callback/oidc`. 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 ### Einrichtung und Sonstiges
| Variable | Standard | Bedeutung | | Variable | Standard | Bedeutung |
@@ -126,6 +142,59 @@ Rückleit-URI beim Anbieter lautet `<APP_URL>/api/auth/callback/oidc`.
| `TZ` | `Europe/Berlin` | Zeitzone für Anzeigen und Tagesgrenzen | | `TZ` | `Europe/Berlin` | Zeitzone für Anzeigen und Tagesgrenzen |
| `DEMO_MODUS` | `false` | Nur für die Screenshot-Instanz. Im Betrieb niemals `true` | | `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 ## Betrieb
### Starten und stoppen ### Starten und stoppen
+9 -7
View File
@@ -7,7 +7,7 @@ grünen Prüfungen, aktueller Dokumentation und einem Eintrag im DEVLOG.
| --------------------------------------- | ---------------------------- | -------- | | --------------------------------------- | ---------------------------- | -------- |
| [M1](#m1--fundament) | Fundament | erledigt | | [M1](#m1--fundament) | Fundament | erledigt |
| [M2](#m2--aufgaben-und-projekte) | Aufgaben und Projekte | erledigt | | [M2](#m2--aufgaben-und-projekte) | Aufgaben und Projekte | erledigt |
| [M3](#m3--kalender-lesen) | Kalender lesen | geplant | | [M3](#m3--kalender-lesen) | Kalender lesen | erledigt |
| [M4](#m4--wochenplanung-und-zeitblöcke) | Wochenplanung und Zeitblöcke | geplant | | [M4](#m4--wochenplanung-und-zeitblöcke) | Wochenplanung und Zeitblöcke | geplant |
| [M5](#m5--benachrichtigungen) | Benachrichtigungen | geplant | | [M5](#m5--benachrichtigungen) | Benachrichtigungen | geplant |
| [M6](#m6--integrationen) | Integrationen | geplant | | [M6](#m6--integrationen) | Integrationen | geplant |
@@ -63,14 +63,16 @@ später" und wartet darauf, ob es im Alltag wirklich fehlt.
## M3 – Kalender lesen ## M3 – Kalender lesen
**Erledigt am 10.09.2026 mit Version 0.3.0.**
**Ziel:** Termine aus Microsoft 365 und Google sehen. **Ziel:** Termine aus Microsoft 365 und Google sehen.
- [ ] `Kalenderkonto` mit Anbieter, Tokens und Delta-Verweis - [x] `Kalenderkonto` mit Anbieter, Tokens und Delta-Verweis
- [ ] OAuth-Verbindung je Instanz: Graph immer, Google nur daheim - [x] OAuth-Verbindung je Instanz: Graph immer, Google nur daheim
- [ ] Graph-Delta-Sync und Google-Sync über `fetch`, Antworten mit zod geprüft - [x] Graph-Delta-Sync und Google-Sync über `fetch`, Antworten mit zod geprüft
- [ ] Ganztägige Termine zählen nicht zur Belegung - [x] Ganztägige Termine zählen nicht zur Belegung
- [ ] Anonymisierte Beispielantworten in `test/fixtures/` - [x] Anonymisierte Beispielantworten in `test/fixtures/`
- [ ] Hintergrundjob mit node-cron - [x] Hintergrundjob mit node-cron
## M4 – Wochenplanung und Zeitblöcke ## M4 – Wochenplanung und Zeitblöcke
+3 -2
View File
@@ -16,10 +16,12 @@ Anleitung für den Alltag mit dem Planer. Eine Seite je Thema.
4. **[Ideen](ideen.md)** – festhalten, was noch keine Aufgabe ist 4. **[Ideen](ideen.md)** – festhalten, was noch keine Aufgabe ist
5. **[Projekte](projekte.md)** – mehrere Aufgaben unter einem Ziel, mit 5. **[Projekte](projekte.md)** – mehrere Aufgaben unter einem Ziel, mit
Fortschritt Fortschritt
6. **[Kalenderkonten](kalenderkonten.md)** – Microsoft 365 und Google
verbinden, Belegung verstehen
## Verwaltung ## Verwaltung
6. **[Protokoll](protokoll.md)** – nachsehen, wer wann was geändert hat 7. **[Protokoll](protokoll.md)** – nachsehen, wer wann was geändert hat
## Was noch kommt ## Was noch kommt
@@ -27,7 +29,6 @@ Diese Seiten entstehen mit den nächsten Meilensteinen:
| Thema | Meilenstein | | Thema | Meilenstein |
| ------------------------------- | ----------- | | ------------------------------- | ----------- |
| Kalenderkonten verbinden | M3 |
| Wochenplanung und Zeitblöcke | M4 | | Wochenplanung und Zeitblöcke | M4 |
| Benachrichtigungen | M5 | | Benachrichtigungen | M5 |
| Integrationen | M6 | | Integrationen | M6 |
+116
View File
@@ -0,0 +1,116 @@
# Kalenderkonten verbinden
Der Planer liest deine Termine, um zu wissen, **wann überhaupt Platz ist**.
Ab M4 schreibt er Zeitblöcke in denselben Kalender zurück.
![Die Seite Kalenderkonten](../bilder/kalenderkonten-hell-schreibtisch.png)
## Was der Planer liest — und was nicht
| Liest er | Nutzt er wofür |
| --------------------------------- | ------------------------------------------------ |
| Beginn und Ende | Belegung gegen die Tageskapazität |
| Betreff | Anzeige in der Wochenansicht |
| Ganztägig ja/nein | Ganztägige Termine zählen **nicht** zur Belegung |
| „Zeigt an als" (frei, gebucht, …) | Als frei markierte Termine zählen nicht |
**Nicht gelesen** werden Teilnehmer, Beschreibungstexte, Anhänge und Orte.
Betreffs stehen in der Datenbank, damit du sie in der Ansicht siehst — sie
landen aber **niemals in Protokollen**.
Gelesen wird ein Fenster von **7 Tagen zurück bis 90 Tage voraus**. Was aus
diesem Fenster herauswandert, wird nachts wieder entfernt.
## Einen Kalender verbinden
1. Im Menü auf **Kalender**.
2. Auf **Microsoft 365 verbinden** beziehungsweise **Google verbinden**.
3. Beim Anbieter anmelden und der Berechtigung zustimmen.
4. Du landest wieder hier, und der erste Abgleich läuft sofort.
> **Warum fragt der Planer auch nach Schreibrechten?** Weil er ab M4
> Zeitblöcke in den Kalender schreibt. Würde er jetzt nur Leserechte
> anfordern, müsstest du später jedem Konto erneut zustimmen — und genau
> daran scheitert Einrichtung erfahrungsgemäß.
### Welche Anbieter es gibt
Das hängt an der Instanz und ist nicht verhandelbar:
| | Firma | Daheim |
| ------------- | -------- | ------ |
| Microsoft 365 | ja | ja |
| Google | **nein** | ja |
In der Firmeninstanz gibt es die Google-Seite nicht — der Aufruf endet dort
mit „Diese Seite gibt es nicht". Das ist Absicht, kein Fehler.
## Der Abgleich
Alle 15 Minuten läuft der Abgleich von selbst. Mit **Jetzt abgleichen**
stößt du ihn von Hand an.
Beide Anbieter liefern nur die **Änderungen** seit dem letzten Mal, nicht
jedes Mal alle Termine. Läuft dieser Merker ab — das passiert nach längerer
Pause —, liest der Planer den ganzen Zeitraum neu ein. Davon merkst du außer
einem etwas längeren Abgleich nichts.
## Die Belegung
Auf der Startseite zeigt jeder Bereich, wie voll der heutige Tag schon ist:
Termine aus **allen** verbundenen Kalendern gegen die Tageskapazität dieses
Bereichs.
> **Warum zählen alle Kalender gegen jeden Bereich?** Weil ein Termin die
> Zeit ohnehin blockiert, ganz gleich, zu welchem Lebensbereich er gehört.
> Wer um 14 Uhr beim Zahnarzt sitzt, kann zur selben Zeit nichts für die
> Firma tun.
Gezählt wird nur, was **ins Arbeitsfenster** des Bereichs fällt. Ein Termin
um 22 Uhr verkleinert die Kapazität eines Bereichs nicht, dessen Fenster um
17 Uhr endet.
Überlappende Termine werden zusammengelegt — eine Doppelbuchung zählt nur
einmal.
## Ein Konto umbenennen
Die Bezeichnung ist frei wählbar. „Firma Outlook" oder „Gewerbe" liest sich
besser als die Mailadresse, gerade wenn du mehrere Konten desselben
Anbieters hast.
## Ein Konto trennen
Der rote Knopf, mit Rückfrage. Die gespiegelten Termine verschwinden dabei —
**im Kalender selbst bleibt alles unangetastet**. Der Planer hat bis M4
ohnehin nur gelesen.
Zusätzlich solltest du die Berechtigung beim Anbieter selbst zurückziehen:
- Microsoft: <https://myapps.microsoft.com> → _Zugriff verwalten_
- Google: <https://myaccount.google.com/permissions>
## Wenn es klemmt
| Was du siehst | Was dahintersteckt |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| „Der Anbieter hat kein Aktualisierungstoken herausgegeben." | Meist bei Google, wenn du schon einmal zugestimmt hattest. Berechtigung im Google-Konto entziehen und neu verbinden. |
| „Die Anfrage war ungültig oder zu alt." | Zwischen Klick und Zustimmung lagen mehr als 30 Minuten. Einfach neu beginnen. |
| „Kein Aktualisierungstoken hinterlegt." | Das Konto muss neu verbunden werden. Das passiert auch, wenn `AUTH_SECRET` gewechselt wurde. |
| „Der Anbieter antwortete mit 401." | Die Berechtigung wurde beim Anbieter entzogen. Neu verbinden. |
| „unerwartete Zeitzone …, erwartet wurde UTC" | Ein Konfigurationsfehler auf Serverseite. Bitte melden — der Planer bricht hier bewusst ab, statt Termine um Stunden zu verschieben. |
| Die Belegung bleibt leer | Kein Kalender verbunden, oder der erste Abgleich lief noch nicht. Die Karte zeigt die Belegung erst, wenn wirklich ein Kalender dranhängt. |
## Sicherheit
Die Zugangstoken liegen **verschlüsselt** in der Datenbank (AES-256-GCM).
Wer eine Sicherung in die Hände bekommt, kann damit nichts anfangen, solange
er nicht auch `AUTH_SECRET` hat.
Wird `AUTH_SECRET` gewechselt, müssen alle Kalenderkonten neu verbunden
werden. Das ist gewollt: mit dem alten Geheimnis sind auch alle Sitzungen
ungültig.
Für die Einrichtung auf Serverseite — was in Entra ID und in der Google
Cloud Console anzulegen ist — siehe
[README, Abschnitt Kalenderanbieter](../../README.md#kalenderanbieter-einrichten).
+46
View File
@@ -327,6 +327,52 @@ async function main(): Promise<void> {
}) })
} }
// --- Kalenderkonto und erfundene Termine ---------------------------------
// Die Tokens sind absichtlich Unsinn: die Screenshot-Instanz spricht mit
// keinem Anbieter, sie soll die Seite nur nicht leer zeigen.
await db.kalenderkonto.deleteMany({})
const kalenderkonto = await db.kalenderkonto.create({
data: {
benutzerId: benutzer.id,
anbieter: 'GRAPH',
anzeigename: 'Firma Outlook',
extKontoId: 'martina.beispiel@firma.invalid',
letzterAbgleich: new Date(DEMO_JETZT.getTime() - 6 * 60_000),
},
})
const TERMINE = [
{ betreff: 'Wochenbesprechung', vonStunde: 8, bisStunde: 9 },
{ betreff: 'Kundengespräch Sonnenberg', vonStunde: 10, bisStunde: 11 },
{ betreff: 'Mittagspause', vonStunde: 11, bisStunde: 12, frei: true },
{ betreff: 'Abnahme Baustelle', vonStunde: 13, bisStunde: 15 },
]
for (const eintrag of TERMINE) {
await db.termin.create({
data: {
kalenderkontoId: kalenderkonto.id,
extId: `demo-${eintrag.betreff.toLowerCase().replace(/[^a-z]+/g, '-')}`,
betreff: eintrag.betreff,
beginn: new Date(Date.UTC(2026, 2, 17, eintrag.vonStunde - 1, 0)),
ende: new Date(Date.UTC(2026, 2, 17, eintrag.bisStunde - 1, 0)),
zeigtAls: eintrag.frei ? 'FREI' : 'GEBUCHT',
},
})
}
await db.termin.create({
data: {
kalenderkontoId: kalenderkonto.id,
extId: 'demo-brueckentag',
betreff: 'Brückentag',
beginn: new Date(Date.UTC(2026, 2, 20, 0, 0)),
ende: new Date(Date.UTC(2026, 2, 21, 0, 0)),
ganztaegig: true,
zeigtAls: 'ABWESEND',
},
})
// Eine Aufgabe, die von außen kam – zeigt die Herkunft in der Liste. // Eine Aufgabe, die von außen kam – zeigt die Herkunft in der Liste.
await db.aufgabe.create({ await db.aufgabe.create({
data: { data: {
+1
View File
@@ -34,6 +34,7 @@ const AUFNAHMEN: Aufnahme[] = [
{ name: 'projekte', pfad: '/projekte' }, { name: 'projekte', pfad: '/projekte' },
{ name: 'ideen', pfad: '/ideen' }, { name: 'ideen', pfad: '/ideen' },
{ name: 'bereiche', pfad: '/bereiche' }, { name: 'bereiche', pfad: '/bereiche' },
{ name: 'kalenderkonten', pfad: '/kalenderkonten' },
{ name: 'einstellungen', pfad: '/einstellungen' }, { name: 'einstellungen', pfad: '/einstellungen' },
{ name: 'protokoll', pfad: '/verwaltung/protokoll' }, { name: 'protokoll', pfad: '/verwaltung/protokoll' },
] ]