From 41778529fc44442ece5bfe99ae1c0f88f2eaa8f3 Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Sun, 4 Oct 2026 18:20:12 +0200 Subject: [PATCH] docs(phase-2): Phase 2 in acht Meilensteine schneiden Phase 2 stand bisher nur als acht Stichpunkte im Epic. Jetzt je Meilenstein eine Datei mit Ziel, Aufgaben und Akzeptanzkriterien, dazu im Tracker gespiegelt: #48 bis #55, zusammen 76 h geschaetzt. Die Reihenfolge ist nicht beliebig. M15 fuehrt die Quellenart ein, die M16 braucht; M17 legt die Zusammensetzung fest, auf der M18 aufsitzt. Die anderen vier haengen an nichts und koennen jederzeit dazwischen. Drei Entscheidungen fallen vor dem Code und stehen je Meilenstein als erste Aufgabe mit Empfehlung: - Wie ein Baustein die Unveraenderlichkeit einer veroeffentlichten Version ueberlebt (Verweis im Entwurf, tiefe Kopie beim Veroeffentlichen). Ein Lauf sieht weiterhin konkrete Schritte und muss nichts lernen. - Wo Anhaenge liegen (Dateisystem, Metadaten in der Datenbank) -- mit der Folge, dass Compose-Stand, native Installation und sicherung.md ein Volume brauchen. Eine Sicherung, die nur die Datenbank mitnimmt, verliert ab dort Daten. - Was eine Selbstauskunft zeigt: Titel, Status, Zeitraum, Geraet, aber keine Schritte, Notizen, Kommentare oder Anhaenge. In einem Lauf stehen Arbeitsnotizen ueber Menschen; eine Selbstauskunft ist keine Akteneinsicht. Refs #2 --- docs/issues/002-epic-phase-2.md | 52 ++++++++++++++++----- docs/issues/028-graph-quelle.md | 55 ++++++++++++++++++++++ docs/issues/029-scim-endpunkt.md | 56 +++++++++++++++++++++++ docs/issues/030-bausteine.md | 59 ++++++++++++++++++++++++ docs/issues/031-abteilungsabschnitte.md | 49 ++++++++++++++++++++ docs/issues/032-anhaenge.md | 54 ++++++++++++++++++++++ docs/issues/033-sse-live.md | 52 +++++++++++++++++++++ docs/issues/034-benachrichtigungen.md | 57 +++++++++++++++++++++++ docs/issues/035-selbstauskunft.md | 61 +++++++++++++++++++++++++ docs/issues/README.md | 12 ++++- 10 files changed, 494 insertions(+), 13 deletions(-) create mode 100644 docs/issues/028-graph-quelle.md create mode 100644 docs/issues/029-scim-endpunkt.md create mode 100644 docs/issues/030-bausteine.md create mode 100644 docs/issues/031-abteilungsabschnitte.md create mode 100644 docs/issues/032-anhaenge.md create mode 100644 docs/issues/033-sse-live.md create mode 100644 docs/issues/034-benachrichtigungen.md create mode 100644 docs/issues/035-selbstauskunft.md diff --git a/docs/issues/002-epic-phase-2.md b/docs/issues/002-epic-phase-2.md index 9a9b7a2..6415208 100644 --- a/docs/issues/002-epic-phase-2.md +++ b/docs/issues/002-epic-phase-2.md @@ -1,24 +1,54 @@ # #002 Phase 2: Verzeichnis-Alternativen, Bausteine, Anhänge -**Status:** offen +**Status:** in Arbeit **Meilenstein:** v0.2.0 **Labels:** phase-2 -**Geschätzt:** – **Tatsächlich:** – +**Geschätzt:** 76 h **Tatsächlich:** – ## Ziel -Nicht im MVP umsetzen. Hier gesammelt, damit das Datenmodell nichts verbaut. +Was der MVP bewusst offen gelassen hat, in acht Meilensteinen. Die Reihenfolge ist nicht beliebig: +M15 legt die Weiche, die M16 braucht, und M17 die Zusammensetzung, auf der M18 aufsitzt. + +## Meilensteine + +| # | Meilenstein | Geschätzt | Hängt an | +|---|---|---|---| +| M15 | [#028 Quellenarten und Microsoft-Graph-Quelle](028-graph-quelle.md) | 10 h | – | +| M16 | [#029 SCIM-Endpunkt als Alternative zum Abgleich](029-scim-endpunkt.md) | 12 h | M15 | +| M17 | [#030 Wiederverwendbare Bausteine](030-bausteine.md) | 12 h | – | +| M18 | [#031 Abteilungsspezifische Zusatzabschnitte](031-abteilungsabschnitte.md) | 8 h | M17 | +| M19 | [#032 Anhänge und Fotos an Schritten](032-anhaenge.md) | 10 h | – | +| M20 | [#033 Live-Aktualisierung per SSE statt Polling](033-sse-live.md) | 6 h | – | +| M21 | [#034 Benachrichtigungen per Mail und Webhook](034-benachrichtigungen.md) | 10 h | – | +| M22 | [#035 Mitarbeiter-Selbstauskunft](035-selbstauskunft.md) | 8 h | – | ## Aufgaben -- [ ] SCIM-Endpunkt als Alternative zum Pull-Abgleich -- [ ] Microsoft-Graph-Quelle für reine Entra-ID-Umgebungen ohne LDAP -- [ ] Wiederverwendbare Bausteine: Abschnitte, die mehrere Vorlagen teilen -- [ ] Abteilungsspezifische Zusatzabschnitte (Fachsoftware je Abteilung) -- [ ] Anhänge und Fotos an Schritten -- [ ] Live-Aktualisierung per SSE statt Polling -- [ ] Benachrichtigungen per Mail und Webhook -- [ ] Mitarbeiter-Selbstauskunft: ein Mitarbeiter sieht die Läufe zu seinen Geräten, nur lesend +- [x] SCIM-Endpunkt als Alternative zum Pull-Abgleich → M16 +- [x] Microsoft-Graph-Quelle für reine Entra-ID-Umgebungen ohne LDAP → M15 +- [x] Wiederverwendbare Bausteine: Abschnitte, die mehrere Vorlagen teilen → M17 +- [x] Abteilungsspezifische Zusatzabschnitte (Fachsoftware je Abteilung) → M18 +- [x] Anhänge und Fotos an Schritten → M19 +- [x] Live-Aktualisierung per SSE statt Polling → M20 +- [x] Benachrichtigungen per Mail und Webhook → M21 +- [x] Mitarbeiter-Selbstauskunft: ein Mitarbeiter sieht die Läufe zu seinen Geräten, nur lesend → M22 + +Die Haken bedeuten **in einen Meilenstein geschnitten**, nicht umgesetzt. Der Stand je Meilenstein +steht in seiner eigenen Datei. + +## Drei Entscheidungen, die vor dem Code fallen müssen + +Sie stehen je Meilenstein als erste Aufgabe, mit Empfehlung. Zwei davon greifen weit: + +1. **Wie ein Baustein die Unveränderlichkeit überlebt** (M17). Empfehlung: Verweis im Entwurf, tiefe + Kopie beim Veröffentlichen. Ein Lauf sieht weiterhin konkrete Schritte und muss nichts lernen. +2. **Wo Anhänge liegen** (M19). Empfehlung: Dateisystem, Metadaten in der Datenbank — mit der Folge, + dass Compose-Stand, native Installation und `sicherung.md` ein Volume brauchen. Eine Sicherung, + die nur die Datenbank mitnimmt, verliert ab dort Daten. +3. **Was eine Selbstauskunft zeigt** (M22). Empfehlung: Titel, Status, Zeitraum, Gerät — **keine** + Schritte, Notizen, Kommentare oder Anhänge. In einem Lauf stehen Arbeitsnotizen über Menschen; + eine Selbstauskunft ist keine Akteneinsicht. ## Vorleistung im MVP diff --git a/docs/issues/028-graph-quelle.md b/docs/issues/028-graph-quelle.md new file mode 100644 index 0000000..9ea5a4c --- /dev/null +++ b/docs/issues/028-graph-quelle.md @@ -0,0 +1,55 @@ +# #028 M15 – Quellenarten und Microsoft-Graph-Quelle + +**Status:** offen +**Im Tracker:** [#48](https://git.wilde-server.de/florian.wilde/Laufzettel/issues/48) +**Meilenstein:** v0.2.0 +**Labels:** phase-2, backend, frontend +**Geschätzt:** 10 h **Tatsächlich:** – + +## Ziel + +Ein Haus ohne lokales AD hat kein LDAP, auf das sich zeigen ließe. Für reine Entra-ID-Umgebungen +braucht es eine zweite Art von Verzeichnisquelle, die über Microsoft Graph liest — mit denselben +Regeln wie der LDAP-Abgleich: anlegen, aktualisieren, deaktivieren, **nie löschen**. + +Dieser Meilenstein legt dazu die Weiche, die auch [#029](029-scim-endpunkt.md) braucht: +`Verzeichnisquelle` bekommt eine **Art**. Heute ist jede Quelle implizit LDAP. + +## Zu entscheiden + +- [ ] **Ein Enum `QuellenArt {LDAP GRAPH SCIM}` auf `Verzeichnisquelle`** — Empfehlung. Die Felder + `url`, `bindDn` und `basisDn` sind dann je Art unterschiedlich belegt; was eine Art braucht, + prüft der Service, nicht die Datenbank. Die Alternative wäre eine Tabelle je Art mit + gemeinsamer Oberklasse — mehr Struktur, als drei Arten rechtfertigen. +- [ ] **Wie `{{ou}}` in Entra ID aussieht.** Es gibt keinen DN und keine OU. Ehrlich ist, den + Platzhalter bei einer Graph-Quelle **leer** zu lassen, statt etwas hineinzudeuten — und das + in der Anleitung zu sagen. Wer OU-abhängige Befehle fährt, braucht ein lokales AD. + +## Aufgaben + +- [ ] `QuellenArt` als Enum, Migration mit `LDAP` für alle bestehenden Zeilen +- [ ] Graph-Zugang: `tenantId`, `clientId`, `clientSecret` (verschlüsselt wie das Bind-Passwort, nie + in einer Antwort), Token über Client-Credentials-Flow, Zwischenspeicher bis kurz vor Ablauf +- [ ] Lesen über `GET /v1.0/users` mit `$select`, Seitenabruf über `@odata.nextLink`; der + Vorgesetzte über `$expand=manager($select=id)` +- [ ] Attributzuordnung mit Graph-Vorgaben (`givenName`, `surname`, `displayName`, `mail`, + `userPrincipalName`, `onPremisesSamAccountName`, `department`, `jobTitle`, `accountEnabled`) +- [ ] `externeId` ist die Entra-`id` (GUID). Sie ist stabil und übersteht eine Umbenennung +- [ ] „Verbindung testen“ wie bei LDAP: die ersten zehn zugeordneten Treffer zeigen +- [ ] Der Abgleich läuft durch **denselben** Dienst wie LDAP — nur das Holen der Einträge + unterscheidet sich. Sonst driften die Regeln auseinander +- [ ] Admin → Verzeichnis zeigt die Art je Quelle und bietet beim Anlegen die Wahl +- [ ] Anleitung Kapitel 06 und `docs/betrieb/`: was in Entra ID einzurichten ist + (App-Registrierung, `User.Read.All`, `Directory.Read.All`, Administratoreinwilligung) + +## Akzeptanzkriterien + +- [ ] Test gegen einen **abspielenden HTTP-Doppelgänger** mit aufgezeichneten Graph-Antworten: erster + Abgleich legt an, zweiter aktualisiert ohne Dubletten, ein `accountEnabled: false` setzt + `aktiv = false` +- [ ] Test: der Seitenabruf folgt `@odata.nextLink` über mindestens drei Seiten +- [ ] Test: `clientSecret` erscheint in keiner Antwort und in keinem Protokolleintrag +- [ ] Test: eine Graph-Quelle lässt `{{ou}}` leer, statt einen Wert zu erfinden +- [ ] Der LDAP-Weg verhält sich unverändert — alle Tests aus #016 bleiben grün +- [ ] **Offen und benannt:** gegen einen echten Entra-Mandanten ist das nicht gelaufen; die + Gegenprobe gehört in eine Prüfliste wie bei [#024](024-proxmox-echtlauf.md) diff --git a/docs/issues/029-scim-endpunkt.md b/docs/issues/029-scim-endpunkt.md new file mode 100644 index 0000000..a5147f3 --- /dev/null +++ b/docs/issues/029-scim-endpunkt.md @@ -0,0 +1,56 @@ +# #029 M16 – SCIM-Endpunkt als Alternative zum Abgleich + +**Status:** offen +**Im Tracker:** [#49](https://git.wilde-server.de/florian.wilde/Laufzettel/issues/49) +**Meilenstein:** v0.2.0 +**Labels:** phase-2, backend +**Geschätzt:** 12 h **Tatsächlich:** – + +## Ziel + +Der Abgleich holt heute. Ein Identitätsanbieter kann auch **schicken** — authentik, Entra ID und +Okta sprechen dafür SCIM 2.0. Das ist der Weg für Häuser, die Laufzettel keinen Lesezugriff auf ihr +Verzeichnis geben wollen oder aus einem Netz heraus betreiben, das das Verzeichnis nicht erreicht. + +Setzt [#028](028-graph-quelle.md) voraus: die Quellenart `SCIM` braucht die Weiche. + +## Zu entscheiden + +- [ ] **Ein Token je Quelle, als Hash gespeichert** — Empfehlung, dasselbe Verfahren wie beim + lokalen Passwort (argon2). Es wird **einmal** angezeigt und ist danach nicht mehr lesbar. Das + Token identifiziert die Quelle und damit den Kunden; es ist der einzige Mandantenbeweis, den + ein Push mitbringt. +- [ ] **Was bei `DELETE /Users/{id}` geschieht:** `aktiv = false`, nicht löschen. Das ist die Regel + des ganzen Projekts, und ein Identitätsanbieter, der löschen will, bekommt `204` — aber die + Zeile bleibt, weil Läufe an ihr hängen. Das muss in der Anleitung stehen, sonst wirkt es wie + ein Fehler. + +## Aufgaben + +- [ ] Route Handler unter `src/app/api/scim/v2/`: `Users` (GET mit Filter `userName eq`, POST), + `Users/{id}` (GET, PUT, PATCH, DELETE), dazu `ServiceProviderConfig`, `Schemas`, + `ResourceTypes` — ohne die drei weigern sich manche Anbieter, überhaupt anzufangen +- [ ] **Jeder dieser Handler prüft selbst**: das Token, und daraus die Quelle und den Kunden. Die + Hülle unter `(app)` liegt hier nicht darüber (CLAUDE.md) +- [ ] Zuordnung: `externalId` → `externeId`, `userName` → `upn`, `name.givenName`/`familyName`, + `emails[primary]` → `mail`, `active` → `aktiv`, `urn:ietf:params:scim:schemas:extension:enterprise:2.0:User` + für `department` und `manager` +- [ ] Antwortform nach RFC 7644: `schemas`, `id`, `meta.resourceType`, `meta.location`, bei Listen + `totalResults`/`itemsPerPage`/`startIndex`; Fehler als `urn:ietf:params:scim:api:messages:2.0:Error` +- [ ] `PATCH` mit `op: add|replace|remove` auf den Pfaden, die die Anbieter wirklich schicken — + nicht der ganze Standard +- [ ] Idempotenz: zweimal dasselbe `POST` ergibt `409`, nicht eine zweite Person +- [ ] Protokolleintrag je Vorgang, mit der Quelle als Urheber (Systemlauf, kein Benutzer) +- [ ] Admin → Verzeichnis: Token erzeugen, einmal anzeigen, widerrufen; letzte Zustellung sichtbar +- [ ] Anleitung Kapitel 06: Einrichtung in authentik und in Entra ID + +## Akzeptanzkriterien + +- [ ] Test gegen **aufgezeichnete Anfragen** von authentik und Entra ID (Form, nicht erfunden): + anlegen, aktualisieren, deaktivieren, wieder aktivieren +- [ ] Test: ohne Token `401`, mit fremdem Token kein Zugriff auf die Personen einer anderen Quelle +- [ ] Test: `DELETE` setzt `aktiv = false` und löscht nichts +- [ ] Test: zweimal dasselbe `POST` ergibt `409` und keine zweite Zeile +- [ ] Test: das Token steht nur als Hash in der Datenbank und in keiner Antwort +- [ ] Test: eine Quelle der Art `SCIM` wird vom Zeitplan **nicht** abgeglichen — sie bekommt + geschickt diff --git a/docs/issues/030-bausteine.md b/docs/issues/030-bausteine.md new file mode 100644 index 0000000..b7d070c --- /dev/null +++ b/docs/issues/030-bausteine.md @@ -0,0 +1,59 @@ +# #030 M17 – Wiederverwendbare Bausteine + +**Status:** offen +**Im Tracker:** [#50](https://git.wilde-server.de/florian.wilde/Laufzettel/issues/50) +**Meilenstein:** v0.2.0 +**Labels:** phase-2, backend, frontend +**Geschätzt:** 12 h **Tatsächlich:** – + +## Ziel + +„Datensicherung prüfen“ steht in vier der sieben Vorlagen, wortgleich. Wer den Ablauf ändert, ändert +ihn heute viermal — oder dreimal und vergisst den vierten. Ein **Baustein** ist ein Abschnitt, den +mehrere Vorlagen teilen. + +## Woran es hängt + +Eine veröffentlichte `VorlagenVersion` ist **unveränderlich**. Das trägt die Nachvollziehbarkeit des +ganzen Archivs: ein Lauf zeigt auch in zehn Jahren genau die Schritte, die damals abgearbeitet +wurden. Ein Baustein, der sich ändert, darf das nicht antasten. + +## Zu entscheiden + +- [ ] **Baustein mit eigener Version, beim Veröffentlichen tief kopiert** — Empfehlung. Ein + Entwurf **verweist** auf einen Baustein; beim Veröffentlichen löst der Dienst den Verweis in + echte Abschnitte und Schritte auf. Damit bleibt alles, was danach kommt, unberührt: `Lauf`, + `Schrittergebnis`, Druckansicht und Archiv sehen weiterhin konkrete Schritte und müssen nichts + lernen. + Die Alternative — Läufe verweisen zur Laufzeit auf den Baustein — hieße, dass die Unveränder- + lichkeit an einer zweiten Stelle durchgesetzt werden muss, und dass jede Abfrage über Schritte + zwei Wege kennen muss. Das ist der Preis für eine Bequemlichkeit, die niemand verlangt hat. +- [ ] **Was „Baustein aktualisieren“ für laufende Arbeiten bedeutet:** nichts. Eine neue + Baustein-Version wirkt erst in der **nächsten** veröffentlichten Vorlagenversion. Das muss die + Oberfläche sagen, sonst erwartet man Durchschlag. + +## Aufgaben + +- [ ] Modelle `Baustein` und `BausteinVersion` mit denselben Regeln wie bei Vorlagen: höchstens ein + Entwurf, veröffentlichte Version unveränderlich (Trigger, nicht nur Dienst) +- [ ] `VorlagenAbschnitt` kann statt eigener Schritte einen Baustein nennen — nur im **Entwurf** +- [ ] Veröffentlichen löst auf: tiefe Kopie der Baustein-Abschnitte und -Schritte in die Version, + mit Vermerk, aus welchem Baustein und welcher Version sie stammen +- [ ] Editor: Baustein einfügen, als Einheit anzeigen, nicht Schritt für Schritt bearbeitbar; ein + Hinweis nennt Baustein und Version +- [ ] Eigene Verwaltung für Bausteine, Recht `vorlagenBearbeiten` (wer Vorlagen pflegt, pflegt + Bausteine) +- [ ] „Wo wird dieser Baustein benutzt“ — vor dem Ändern will man das wissen +- [ ] Anleitung Kapitel 04 + +## Akzeptanzkriterien + +- [ ] Test: eine neue Baustein-Version ändert **keine** veröffentlichte Vorlagenversion und **keinen** + laufenden Lauf +- [ ] Test: die Druckansicht eines archivierten Laufs ist nach einer Baustein-Änderung Zeichen für + Zeichen dieselbe +- [ ] Test: die nächste veröffentlichte Vorlagenversion enthält die neue Baustein-Version +- [ ] Test: ein Baustein lässt sich nicht löschen, solange eine Vorlage ihn im Entwurf nennt +- [ ] Test: Export und Import einer Vorlage mit Baustein ergeben eine inhaltsgleiche Vorlage — die + Datei trägt die aufgelösten Schritte, keinen Verweis auf einen Baustein, den die andere + Installation nicht hat diff --git a/docs/issues/031-abteilungsabschnitte.md b/docs/issues/031-abteilungsabschnitte.md new file mode 100644 index 0000000..f9e7f81 --- /dev/null +++ b/docs/issues/031-abteilungsabschnitte.md @@ -0,0 +1,49 @@ +# #031 M18 – Abteilungsspezifische Zusatzabschnitte + +**Status:** offen +**Im Tracker:** [#51](https://git.wilde-server.de/florian.wilde/Laufzettel/issues/51) +**Meilenstein:** v0.2.0 +**Labels:** phase-2, backend, frontend +**Geschätzt:** 8 h **Tatsächlich:** – + +## Ziel + +Beim Onboarding in der Buchhaltung gehören drei Schritte dazu, die sonst niemand braucht: DATEV +einrichten, Drucker für Formulare, Zugang zum Bankportal. Heute heißt die Wahl: drei Vorlagen je +Abteilung pflegen, oder Schritte, die für die meisten „nicht zutreffend“ sind. + +Ein Abschnitt soll sagen können: **nur für diese Abteilungen.** + +## Zu entscheiden + +- [ ] **Die Auswahl wird beim Start festgeschrieben** — Empfehlung. Der Lauf merkt sich, welche + Abschnitte für ihn gelten, statt sie bei jedem Aufruf aus `Lauf.abteilungId` neu zu berechnen. + Grund: ein archivierter Lauf muss genau die Schritte zeigen, die damals abgearbeitet wurden. + Wechselt jemand später die Abteilung des Laufs, dürfen Schritte nicht verschwinden, an denen + schon eine Notiz hängt. +- [ ] **Was bei einem Lauf ohne Abteilung gilt:** nur die Abschnitte ohne Einschränkung. Ein + Firewall-Rollout hat keine Abteilung, und das ist kein Mangel. + +## Aufgaben + +- [ ] `VorlagenAbschnitt.nurFuerAbteilungen` — Liste von Abteilungsnamen, nicht von Kennungen: der + Abschnitt gehört zur Version und muss ein Umbenennen oder Archivieren einer Abteilung + überleben, ohne ins Leere zu zeigen. Dieselbe Entscheidung wie bei `Mitarbeiter.abteilung` +- [ ] Beim Start: die geltenden Abschnitte ermitteln und am Lauf festhalten +- [ ] Die Abarbeitungsansicht zeigt nur geltende Abschnitte; die Nummerierung (`01 ·`) zählt nur sie +- [ ] **Die Abschlussregel zählt nur geltende Pflichtschritte.** Ein ausgefilterter Pflichtschritt + darf einen Lauf nicht blockieren — sonst ist der Abschluss unerreichbar +- [ ] Druckansicht und Archiv zeigen dieselbe Auswahl wie die Abarbeitung +- [ ] Editor: je Abschnitt eine Mehrfachauswahl über die gepflegten Abteilungen, mit Freitext für + eine, die es noch nicht gibt +- [ ] Der Start-Dialog sagt vorher, wie viele Abschnitte die gewählte Abteilung hinzunimmt +- [ ] Anleitung Kapitel 04 und 02 + +## Akzeptanzkriterien + +- [ ] Test: ein Lauf für „Buchhaltung“ enthält den Buchhaltungs-Abschnitt, einer für „Vertrieb“ nicht +- [ ] Test: ein Lauf ohne Abteilung enthält nur die uneingeschränkten Abschnitte +- [ ] Test: die Abteilung eines laufenden Laufs ändern ändert **nicht** seine Abschnitte +- [ ] Test: ein ausgefilterter Pflichtschritt blockiert den Abschluss nicht +- [ ] Test: die Druckansicht zeigt genau die Abschnitte der Abarbeitung — nicht mehr und nicht weniger +- [ ] Am Handy kein horizontales Scrollen diff --git a/docs/issues/032-anhaenge.md b/docs/issues/032-anhaenge.md new file mode 100644 index 0000000..2d748d7 --- /dev/null +++ b/docs/issues/032-anhaenge.md @@ -0,0 +1,54 @@ +# #032 M19 – Anhänge und Fotos an Schritten + +**Status:** offen +**Im Tracker:** [#52](https://git.wilde-server.de/florian.wilde/Laufzettel/issues/52) +**Meilenstein:** v0.2.0 +**Labels:** phase-2, backend, frontend, infra +**Geschätzt:** 10 h **Tatsächlich:** – + +## Ziel + +„Seriennummer fotografiert“, „Typenschild“, „Zustand bei Übergabe“, „Rechnung abgelegt“ — manches +lässt sich nicht abhaken, es muss gezeigt werden. Ein Schritt soll Dateien und Fotos aufnehmen +können, am Handy direkt aus der Kamera. + +## Zu entscheiden + +- [ ] **Dateien im Dateisystem, Metadaten in der Datenbank** — Empfehlung. Ein Pfad aus der + Umgebung (`ANHAENGE_PFAD`), darunter nach Kunde und Lauf gegliedert. Gegen `bytea` in Postgres + spricht die Sicherung: ein `pg_dump` mit Fotos ist nach einem Jahr kein `pg_dump` mehr, das + jemand einspielt. Gegen S3 spricht, dass diese Anwendung selbst gehostet läuft und keinen + Objektspeicher voraussetzen soll. + **Folge, die mitgeplant werden muss:** der Compose-Stand und die native Installation brauchen + ein Volume, und `docs/betrieb/sicherung.md` muss beides nennen. Eine Sicherung, die nur die + Datenbank mitnimmt, verliert ab hier Daten — das ist genau die Art Zusage, die dieses Projekt + sonst teuer bezahlt hat. +- [ ] **Keine Verkleinerung auf dem Server im ersten Schritt.** Ein Handyfoto hat heute 3–6 MB; eine + Obergrenze (etwa 10 MB je Datei, 20 Dateien je Lauf) ist ehrlicher als eine Umwandlung, die + Metadaten verliert und eine Bildbibliothek ins Projekt zieht. + +## Aufgaben + +- [ ] Modell `Anhang`: `schrittergebnisId`, `kundeId`, Dateiname, Medientyp, Größe, Prüfsumme, + `erstelltVonId`, `erstelltAm`; Datei daneben im Dateisystem +- [ ] Hochladen über einen Route Handler mit `rechtErforderlich` — kein Weg daran vorbei, und + **jeder** Abruf prüft Recht und Mandant, auch der des Bildes selbst +- [ ] Grenzen: Medientypen auf Bilder und PDF, Größe je Datei und Anzahl je Lauf, beides in der + Umgebung einstellbar; eine abgewiesene Datei nennt den Grund +- [ ] Am Handy direkt aus der Kamera (`capture="environment"`), am Rechner aus dem Dateiwähler +- [ ] **Der Archiv-Schreibschutz gilt auch hier**: an einem archivierten Lauf kommt kein Anhang mehr + dazu und keiner weg. `laufMussVeraenderbarSein` sitzt an genau einer Stelle — dort anschließen +- [ ] Druckansicht: Bilder als Miniaturen mit Dateinamen, PDF nur als Zeile +- [ ] `docs/betrieb/sicherung.md`, `docker-compose.yml`, `deploy/install.sh`: Volume, Rechte, + Sicherung **und** Rückspielung +- [ ] Anleitung Kapitel 02 und 08 + +## Akzeptanzkriterien + +- [ ] Test: Anhang an einem Schritt hochladen, in der Abarbeitung und im Druck sichtbar +- [ ] Test: an einem archivierten Lauf wird das Hochladen abgewiesen +- [ ] Test: ein Anhang eines fremden Kunden ist über seine Adresse **nicht** abrufbar +- [ ] Test: eine zu große Datei und ein falscher Medientyp werden mit Grund abgewiesen +- [ ] Test: `docker-compose.yml` hat ein Volume für die Anhänge, und `sicherung.md` nennt es (wie + `tests/betriebsstand.test.ts` es für die Datenbank schon prüft) +- [ ] Sicherung und **Rückspielung** einmal wirklich gefahren, nicht nur beschrieben diff --git a/docs/issues/033-sse-live.md b/docs/issues/033-sse-live.md new file mode 100644 index 0000000..50b592c --- /dev/null +++ b/docs/issues/033-sse-live.md @@ -0,0 +1,52 @@ +# #033 M20 – Live-Aktualisierung per SSE statt Polling + +**Status:** offen +**Im Tracker:** [#53](https://git.wilde-server.de/florian.wilde/Laufzettel/issues/53) +**Meilenstein:** v0.2.0 +**Labels:** phase-2, backend, frontend +**Geschätzt:** 6 h **Tatsächlich:** – + +## Ziel + +Die Abarbeitungsansicht fragt alle zehn Sekunden nach dem Zustand. Das reicht für zwei Techniker an +einem Lauf, kostet aber bei zwanzig offenen Tabs zwanzig Abfragen je zehn Sekunden — und zehn +Sekunden sind lang, wenn zwei Leute dieselbe Liste abhaken. + +Die Vorleistung steht: das Polling liegt in genau einem Route Handler +(`/api/laeufe/[id]/zustand`) und lässt sich tauschen. + +## Zu entscheiden + +- [ ] **`LISTEN`/`NOTIFY` in Postgres als Signal** — Empfehlung. Ein Trigger auf `Schrittergebnis` + und `Lauf` schickt `NOTIFY lauf_geaendert, ''`; der Route Handler hält eine Verbindung + und schiebt weiter. Damit funktioniert es auch mit mehreren Instanzen der Anwendung, ohne + einen zweiten Dienst. Die Alternative — der Server fragt selbst alle zwei Sekunden — wäre + dasselbe Polling, nur an anderer Stelle. +- [ ] **Polling bleibt als Rückfall**, nicht als zweite Wahrheit: derselbe Handler antwortet weiter + auf eine gewöhnliche Anfrage. Wer kein `EventSource` hat oder hinter einem Proxy sitzt, der + Ströme puffert, bekommt den alten Weg. Ein Rückfall, der nie geprüft wird, ist keiner — + deshalb ein Test je Weg. + +## Aufgaben + +- [ ] Trigger und `NOTIFY` in einer handgeschriebenen Migration, mit dem üblichen Vermerk (Prisma + kennt Trigger nicht und will sie löschen) +- [ ] Route Handler liefert `text/event-stream`, schickt alle 25 s einen Kommentar als Herzschlag + (sonst schließen Proxys die Verbindung) und beendet sauber bei `abort` +- [ ] **Der Handler prüft Recht und Mandant selbst** — ein offener Strom ist ein Leseweg wie jeder + andere (CLAUDE.md: Route Handler deckt die Hülle nicht ab) +- [ ] Client: `EventSource` mit Wiederverbindung; bei versteckter Lasche pausieren wie heute; bei + `error` auf Polling zurückfallen und das **einmal** melden, nicht bei jedem Versuch +- [ ] `docs/betrieb/reverse-proxy.md`: `proxy_buffering off` für nginx, `flush_interval -1` für + Caddy — ohne das kommt kein Ereignis an, und die Seite sieht aus, als sei sie hängen geblieben +- [ ] Obergrenze für gleichzeitige Ströme je Instanz, damit ein Browser mit vierzig Tabs die + Verbindungen nicht aufbraucht + +## Akzeptanzkriterien + +- [ ] Test: zwei Sitzungen am selben Lauf — hakt die eine ab, sieht die andere es in unter einer + Sekunde, ohne neu zu laden +- [ ] Test: ohne Recht wird der Strom abgewiesen, bevor das erste Ereignis fließt +- [ ] Test: ein Lauf eines fremden Kunden liefert keinen Strom +- [ ] Test: der Rückfall auf Polling funktioniert — gefahren mit abgeschaltetem `EventSource` +- [ ] Der Herzschlag kommt an: eine Verbindung überlebt 60 s ohne Änderung diff --git a/docs/issues/034-benachrichtigungen.md b/docs/issues/034-benachrichtigungen.md new file mode 100644 index 0000000..7b2b6d9 --- /dev/null +++ b/docs/issues/034-benachrichtigungen.md @@ -0,0 +1,57 @@ +# #034 M21 – Benachrichtigungen per Mail und Webhook + +**Status:** offen +**Im Tracker:** [#54](https://git.wilde-server.de/florian.wilde/Laufzettel/issues/54) +**Meilenstein:** v0.2.0 +**Labels:** phase-2, backend, frontend, infra +**Geschätzt:** 10 h **Tatsächlich:** – + +## Ziel + +Ein zugewiesener Lauf fällt heute nur auf, wenn jemand die Übersicht öffnet. Ein überfälliger +überhaupt nicht. Vier Ereignisse sind es wert, gemeldet zu werden: **zugewiesen**, **überfällig**, +**abgeschlossen**, **Verzeichnisabgleich fehlgeschlagen**. + +## Zu entscheiden + +- [ ] **Nur diese vier Ereignisse, je Empfänger abschaltbar** — Empfehlung. Ein allgemeiner + Benachrichtigungsbaukasten ist eine Phase für sich; vier Ereignisse decken ab, was im Betrieb + wirklich fehlt. +- [ ] **Mail über SMTP, keine Dienstanbindung.** Eine selbst gehostete Anwendung soll an den + Mailserver des Hauses können, und das ist SMTP. Zugangsdaten verschlüsselt wie das + LDAP-Bind-Passwort. +- [ ] **Zustellung in einer Warteschlange in der Datenbank**, nicht im Anfrage-Pfad. Ein hängender + Mailserver darf kein Abhaken blockieren. Dieselbe Sperre wie beim Abgleich + (`AbgleichSperre`) verhindert, dass zwei Instanzen dasselbe zweimal schicken. + +## Aufgaben + +- [ ] Modell `Zustellung`: Ereignis, Empfänger, Weg (MAIL | WEBHOOK), Versuche, letzter Fehler, + Zustand; mit Index auf das, was der Zeitplan abholt +- [ ] SMTP-Einstellungen unter Admin → Einstellungen: Host, Port, TLS, Benutzer, Passwort + (verschlüsselt, nie in einer Antwort), Absender, „Testmail schicken“ +- [ ] Webhook: Ziel-URL, Geheimnis, **HMAC-SHA256 über den Rumpf** im Kopf `X-Laufzettel-Signatur`, + dazu ein Zeitstempel gegen Wiedereinspielen +- [ ] Wiederholung mit wachsendem Abstand (1, 5, 30, 120 Minuten), dann aufgeben und sichtbar + machen. Eine stille Warteschlange ist schlimmer als keine +- [ ] Überfälligkeit erkennt der Zeitplan, der schon läuft (`node-cron` im Prozess) — kein zweiter + Dienst +- [ ] Je Benutzer abschaltbar; ein Benutzer ohne Mailadresse bekommt keine Mail und keinen Fehler +- [ ] Admin: die letzten Zustellungen mit Zustand und Grund +- [ ] **`{{generiertes_passwort}}` kommt in keine Nachricht.** Eine Mail mit dem Lauf-Inhalt wäre + genau die Lücke, die das Projekt an jeder anderen Stelle geschlossen hat. Die Nachricht + enthält Titel, Status und einen Link — **keine Schrittinhalte** +- [ ] `docs/betrieb/`: SMTP einrichten, Webhook prüfen, Signatur nachrechnen +- [ ] Anleitung Kapitel 07 + +## Akzeptanzkriterien + +- [ ] Test gegen einen echten SMTP-Fänger im Container (mailpit): zugewiesen, überfällig, + abgeschlossen, Abgleich fehlgeschlagen — vier Mails, richtige Empfänger +- [ ] Test gegen einen lokalen Empfänger: die Signatur lässt sich mit dem Geheimnis nachrechnen, mit + einem falschen nicht +- [ ] Test: eine fehlgeschlagene Zustellung wird wiederholt und ist danach sichtbar +- [ ] Test: zwei gleichzeitige Durchgänge schicken nichts zweimal +- [ ] Test: in **keiner** Nachricht steht ein erzeugtes Passwort — über alle Rumpftexte geprüft, wie + in #012 +- [ ] Test: ein Benutzer mit abgeschalteter Benachrichtigung bekommt nichts diff --git a/docs/issues/035-selbstauskunft.md b/docs/issues/035-selbstauskunft.md new file mode 100644 index 0000000..b6d83d8 --- /dev/null +++ b/docs/issues/035-selbstauskunft.md @@ -0,0 +1,61 @@ +# #035 M22 – Mitarbeiter-Selbstauskunft + +**Status:** offen +**Im Tracker:** [#55](https://git.wilde-server.de/florian.wilde/Laufzettel/issues/55) +**Meilenstein:** v0.2.0 +**Labels:** phase-2, backend, frontend +**Geschätzt:** 8 h **Tatsächlich:** – + +## Ziel + +„Was ist eigentlich mit meinem Notebook passiert?“ fragt heute jemand im Ticket, und ein Techniker +sieht nach. Ein Mitarbeiter soll die Läufe zu **seinen** Geräten und zu **seiner** Person selbst +ansehen können — nur lesend. + +## Woran es hängt + +Ein `Mitarbeiter` ist kein `Benutzer`. Die Anwendung kennt vier Rollen für Leute, die **arbeiten**; +ein Mitarbeiter ist Gegenstand der Arbeit, nicht ihr Träger. Es gibt heute keinen Weg, von einer +Anmeldung zu einer `Mitarbeiter`-Zeile zu kommen. + +## Zu entscheiden + +- [ ] **Anmeldung über OIDC, Zuordnung über den UPN, neue Rolle `MITARBEITER`** — Empfehlung. Das + Haus hat seine Leute ohnehin im Identitätsanbieter; wer sich anmeldet und keiner + Rollengruppe angehört, wird über `upn` einer `Mitarbeiter`-Zeile zugeordnet und bekommt genau + ein Recht: `selbstauskunft`. `Benutzer.mitarbeiterId` hält die Zuordnung fest. + Die Alternative — ein Link mit Token je Person — braucht keine Anmeldung, aber jeder + weitergeleitete Link ist dann ein Datenleck, und widerrufen müsste man je Person. +- [ ] **Was der Mitarbeiter sieht:** Titel, Status, Zeitraum und Gerät eines Laufs. **Nicht** die + Schritte, nicht die Notizen, nicht die Kommentare, nicht die Anhänge. In einem Lauf stehen + Arbeitsnotizen über Menschen und Zugangsdaten-Platzhalter; eine Selbstauskunft ist keine + Akteneinsicht. Das ist die wichtigste Entscheidung dieses Meilensteins und gehört in die + Anleitung, nicht nur in den Code. +- [ ] **Kein Zugang ohne Zuordnung.** Wer sich anmeldet und zu keiner `Mitarbeiter`-Zeile passt, + bekommt `/kein-zugriff` mit Begründung — nicht eine leere Seite, die wie ein Fehler aussieht. + +## Aufgaben + +- [ ] `Rolle` um `MITARBEITER` erweitern; `RECHTE` bekommt `selbstauskunft: ["MITARBEITER", …]`. + **Jede** Rollenprüfung in `tests/rollen.test.ts` wächst um eine Spalte — die Matrix ist + ausgeschrieben und nicht berechnet, genau damit das auffällt +- [ ] `Benutzer.mitarbeiterId` mit Mandantengrenze als zusammengesetzter Fremdschlüssel +- [ ] Zuordnung beim Login über `upn`, unabhängig von Groß- und Kleinschreibung; mehrdeutig ist ein + Fehler und kein Zufallstreffer +- [ ] Eigene Seite `/meine-geraete`: meine Geräte, je Gerät die Läufe chronologisch, Titel und Status +- [ ] **Jede Abfrage schränkt auf die eigene Person ein** — nicht die Seite. Ein Lesemodell trägt + seine Sicht selbst (CLAUDE.md) +- [ ] Die Navigation zeigt einem `MITARBEITER` nur diese Seite; alles andere ist weg, nicht grau +- [ ] Anleitung: ein eigenes kurzes Kapitel, an Mitarbeiter gerichtet und nicht an Techniker + +## Akzeptanzkriterien + +- [ ] Test: ein `MITARBEITER` sieht die Läufe seiner Geräte und keinen anderen +- [ ] Test: er sieht **keine** Schritte, Notizen, Kommentare oder Anhänge — geprüft über die Antwort, + nicht über die Seite +- [ ] Test: er kann nichts schreiben — jede Server Action weist ihn ab (`tests/zugang.test.ts` + erkennt eine ungeprüfte Aktion ohnehin) +- [ ] Test: ohne passende `Mitarbeiter`-Zeile führt die Anmeldung auf `/kein-zugriff` mit Begründung +- [ ] Test: ein mehrdeutiger UPN wird abgewiesen und protokolliert +- [ ] Test: die Rollenmatrix ist vollständig — jede der fünf Rollen gegen jedes Recht +- [ ] Am Handy kein horizontales Scrollen diff --git a/docs/issues/README.md b/docs/issues/README.md index f892f58..9423453 100644 --- a/docs/issues/README.md +++ b/docs/issues/README.md @@ -6,7 +6,7 @@ Diese Dateien sind die Quelle; `pnpm issues:sync` spiegelt sie in den Gitea-Trac | Nr. | Titel | Status | Meilenstein | Geschätzt | Tatsächlich | |---|---|---|---|---|---| | #001 | [Phase 1: MVP](001-epic-phase-1-mvp.md) | in Arbeit | v0.1.0 | – | – | -| #002 | [Phase 2: Verzeichnis-Alternativen, Bausteine, Anhänge](002-epic-phase-2.md) | offen | v0.2.0 | – | – | +| #002 | [Phase 2: Verzeichnis-Alternativen, Bausteine, Anhänge](002-epic-phase-2.md) | in Arbeit | v0.2.0 | 76 h | – | | #003 | [Phase 3: Protokoll, bedingte Schritte, Freigabe](003-epic-phase-3.md) | offen | v0.3.0 | – | – | | #004 | [Phase 4: Aktionen, Connectors, REST-API](004-epic-phase-4.md) | offen | v0.4.0 | – | – | | #005 | [M0 – Gerüst, Prozess, Design-Token](005-geruest-prozess-design.md) | erledigt | v0.1.0 | 6 h | 1 h 25 min | @@ -32,4 +32,12 @@ Diese Dateien sind die Quelle; `pnpm issues:sync` spiegelt sie in den Gitea-Trac | #025 | [Healthcheck gibt die rohe Datenbankfehlermeldung heraus](025-gesundheit-fehlermeldung.md) | erledigt | v0.2.0 | 1 h | 25 min | | #026 | [Zwei Konventionen präzisieren, die strenger klingen als sie gemeint sind](026-konventionen-praezisieren.md) | erledigt | v0.2.0 | 1 h | 20 min | | #027 | [Die Editor-Abläufe lassen ihre Vorlagen stehen](027-e2e-reste-vorlagen.md) | offen | v0.2.0 | 1 h | – | -| | **Summe** | | | **153 h** | **41 h 20 min** | +| #028 | [M15 – Quellenarten und Microsoft-Graph-Quelle](028-graph-quelle.md) | offen | v0.2.0 | 10 h | – | +| #029 | [M16 – SCIM-Endpunkt als Alternative zum Abgleich](029-scim-endpunkt.md) | offen | v0.2.0 | 12 h | – | +| #030 | [M17 – Wiederverwendbare Bausteine](030-bausteine.md) | offen | v0.2.0 | 12 h | – | +| #031 | [M18 – Abteilungsspezifische Zusatzabschnitte](031-abteilungsabschnitte.md) | offen | v0.2.0 | 8 h | – | +| #032 | [M19 – Anhänge und Fotos an Schritten](032-anhaenge.md) | offen | v0.2.0 | 10 h | – | +| #033 | [M20 – Live-Aktualisierung per SSE statt Polling](033-sse-live.md) | offen | v0.2.0 | 6 h | – | +| #034 | [M21 – Benachrichtigungen per Mail und Webhook](034-benachrichtigungen.md) | offen | v0.2.0 | 10 h | – | +| #035 | [M22 – Mitarbeiter-Selbstauskunft](035-selbstauskunft.md) | offen | v0.2.0 | 8 h | – | +| | **Summe** | | | **229 h** | **41 h 20 min** |