From b688e2ebc8c01a9d0b49e8402419a4ac551a82ba Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Sun, 4 Oct 2026 16:20:42 +0000 Subject: [PATCH] docs(phase-2): Phase 2 in acht Meilensteine schneiden (#56) --- 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** |