docs(phase-2): Phase 2 in acht Meilensteine schneiden (#56)
This commit was merged in pull request #56.
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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, '<laufId>'`; 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
+10
-2
@@ -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** |
|
||||
|
||||
Reference in New Issue
Block a user