Ein Abschnitt oder ein einzelner Schritt erscheint nur, wenn seine Bedingung
zutrifft — abhaengig von einer Variable des Laufs oder vom Zustand eines
anderen Schritts. Der echte Fall steht im Seed: "Mitarbeiter-Onboarding" traegt
beide Konto-Abschnitte, fuenf AD-Schritte und vier fuer Entra ID, und
`konto_typ` sagt beim Start, welcher gilt. Bisher klickte ein Techniker die
anderen neun bei jedem Onboarding weg.
Entscheidungen, alle im Issue begruendet:
- Laufend ausgewertet, nicht beim Start festgeschrieben. Fuer die
Abteilungsabschnitte (#031) ist Festschreiben richtig; fuer Bedingungen nicht
— die Antwort auf "ist Pruefen fehlgeschlagen?" entsteht erst waehrend der
Arbeit. Ein struktureller Test prueft, dass alle drei Schichten
(Abschlussregel, Abarbeitung, Druck) die Auswertung rufen.
- Nicht entscheidbar heisst: gilt. Ein Lauf ohne konto_typ zeigt beide
Abschnitte — genau wie vorher. Ein Abschnitt zu viel ist sichtbar und
korrigierbar, ein fehlender faellt niemandem auf.
- OFFEN ist eine Antwort, keine Luecke: waere es unentscheidbar, waere ein
bedingter Abschnitt von Anfang an sichtbar.
- Eine Bedingung, kein Ausdrucksbaum. Kein und, kein oder.
- Verwiesen wird ueber Titel-Pfade, nicht ueber Kennungen: vier Wege schreiben
eine Struktur neu und geben neue Kennungen.
Zwei stille Datenverluste gefunden, der zweite aelter als dieser Meilenstein:
- Der Import verwarf `nurFuerAbteilungen`. Der Export schreibt es seit #031, das
Schema nimmt es an, der Import setzte es nicht — eine exportierte Vorlage
verlor ihre Abteilungsabschnitte und galt dort fuer alle.
- Die tiefe Kopie verwarf es ebenfalls, womit jedes "Bearbeiten" einer Vorlage
alle Einschraenkungen verlor. Ursache war ein von Hand getippter Zwischentyp,
der weniger Felder nannte als die Abfrage liefert; jetzt aus Prisma
abgeleitet, beide Kopierwege teilen eine Stelle, und ein Test vergleicht Kopie
und Original Feld fuer Feld.
Gefunden hat beides die Klickprobe am gebauten Stand, nicht die Suite: im Editor
stand "Bedingung gesetzt", nach "Bearbeiten" war sie weg. Dreizehn Gegenproben
liefen, alle wurden rot — und der teuerste Befund lag ausserhalb von allen.
1499 Vitest-Tests (vorher 1457), 115 E2E-Ablaeufe gruen.
Closes#39
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Vier Meilensteine fuer v0.3.0, 34 h geschaetzt: bedingte Schritte und
Abschnitte (M23), Vier-Augen-Freigabe (M24), PDF-Uebergabeprotokoll (M25),
weitere Schritt-Typen (M26). Die Reihenfolge ist begruendet: M23 zuerst, weil
es als einziges am Datenmodell und an der Abschlussregel arbeitet; M26 zuletzt,
weil UNTERSCHRIFT erst im PDF einen Zweck hat.
Jedes Issue nennt die Entscheidungen, die zu treffen sind, mit Empfehlung und
Begruendung. Bei M23 ist die entscheidende: eine Bedingung auf einer Variable
liesse sich beim Start festschreiben wie bei #031, eine auf einem
Schrittergebnis nicht — die Antwort entsteht erst waehrend der Arbeit.
Beim Nummernvergeben einen Fund: vier Codestellen, sechs Changelog-Zeilen und
der Doku-Pruefer nannten dreistellig die Tracker-Nummern 39 und 40 — gemeint
waren die Issue-Dateien 025 und 026. Solange es diese Dateien nicht gab, fiel es
niemandem auf; mit Phase 3 waere es eine falsche Spur geworden, und zwar in
Kommentaren, die eine Codestelle begruenden.
Behoben, und pruefe:doku prueft es ab jetzt: jeder dreistellige Verweis muss auf
eine Issue-Datei zeigen. Gegenprobe mit einer erfundenen Nummer schlaegt an.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Version auf 0.2.0, Changelog-Abschnitt geschlossen, Phasenzusammenfassung im
Devlog. Der Tag selbst folgt getrennt.
Gemessen, nicht uebernommen: 40.781 Zeilen unter src/ (ohne erzeugten
Prisma-Client), 25 Tabellen, 13 Aufzaehlungen, 14 Migrationen, 56 namentlich
aufgezaehlte Datenbankregeln, 1457 Vitest-Tests in 61 Dateien, 115
Ende-zu-Ende-Ablaeufe in 16 Dateien, 38 Issue-Dateien. Der Seed ist unveraendert
bei 7 Vorlagen mit 155 Schritten.
38 h 35 min fuer zwoelf Issues, geschaetzt waren 90 h. Acht davon waren
Meilensteine (M15-M22); zwei der nuetzlichsten (#036, #038) stammen aus keinem
Plan, sondern aus der Durchsicht.
Was offen bleibt, steht in der Zusammenfassung: #024 (Proxmox auf einem echten
Host) und zwei benannte Grenzen — gegen einen echten Entra-Mandanten und gegen
echte SCIM-Anbieter ist nichts gelaufen, die Formen in den Tests stammen aus der
Dokumentation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#038: Die E2E-Ablaeufe laufen ab jetzt vor jedem Merge nach main, nicht vor
jedem Commit. Gemessen: Bau 14 s, Start 7 s, Ablaeufe 256 s — zu den vier
Minuten des bestehenden Tores. Neun Minuten vor jedem Commit ist ein Tor, das
umgangen wird. Vor jedem Merge waeren es in Phase 2 acht Laeufe gewesen, und
jeder der vier Befunde aus #027 waere am Tag seiner Entstehung aufgefallen.
Geprueft wurde jeder der 16 Ablaeufe gegen die Frage, ob es ein
Vitest-Gegenstueck gibt. Neun Zusagen haengen ausschliesslich dort, und keine
laesst sich holen: alle brauchen einen echten Browser. Die Liste steht in
CLAUDE.md.
Der eigentliche Fund dabei war keine Testluecke, sondern eine Konvention ohne
Pruefer: "jede Seite prueft selbst" steht seit #007 in CLAUDE.md, und geprueft
hat es bis hierher nur ein Mensch, der nachsieht. Der Review von Phase 2 hat
alle 27 Seiten durchgesehen — ein Ergebnis, das fuer genau einen Tag gilt.
tests/zugang.test.ts liest sie jetzt, samt jedem Route Handler; die Ausnahmen
(Auth, Healthcheck) stehen namentlich mit Begruendung, und ein Test prueft auch
die Gegenrichtung.
#037: Das Lesemodell der Anhaenge liegt in queries/ und engt selbst ein — vorher
hing das daran, dass beide Aufrufer den Lauf vorher pruefen. /meine-geraete wird
bei 390 px gemessen, mit einem echten MITARBEITER-Konto aus dem E2E-Vorlauf,
dazu ein Test, dass dort kein Link in die Verwaltung angeboten wird.
Dabei selbst einen Fehler gemacht, der die neue Regel begruendet: fuer das
zweite Konto brauchte `anmelden` eine Variante, die nicht auf / wartet, und ich
habe das Warten fuer alle gelockert. Das machte zwei unverwandte Ablaeufe
unzuverlaessig — sichtbar nur im vollen Durchgang, einzeln waren beide gruen.
Ein gemeinsamer Helfer in einer Testsuite ist geteilter Zustand. Ohne vollen
Durchgang haette ich zwei rote Ablaeufe gemergt.
1457 Vitest-Tests (vorher 1454), 115 E2E-Ablaeufe (vorher 113). Offen bleibt nur
noch #024, und das verlangt einen echten Proxmox-Host.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Acht Meilensteine lagen ohne fremden Blick auf main. Ein Review mit eigener
Datenbank, Gegenproben und vollem Suitenlauf hat sie gegen die
Akzeptanzkriterien und gegen CLAUDE.md geprueft.
Die Kernregeln halten: fuenf von fuenf Gegenproben gegen Versionierung,
Mandantengrenze, Abschlussregel und Selbstauskunft wurden rot. Alle Befunde
lagen am Rand — und die drei wertvollsten waren Gegenproben, die gruen blieben.
Schwer:
- Der Link in der Mail "Verzeichnis-Abgleich fehlgeschlagen" war ein 404
(/verwaltung/verzeichnis statt /admin/verzeichnis). Die eine Meldung, die an
alle Administratoren geht, im Moment eines Ausfalls. Ein Test prueft jetzt
jeden Meldungspfad gegen die vorhandenen Routen.
- Der Export eines Entwurfs mit Baustein lieferte einen leeren Abschnitt unter
dem Platzhalter "wird ersetzt" — der Zweck des Formats ist, eine Vorlage in
eine zweite Installation zu bringen. Bausteine werden jetzt aufgeloest.
Mittel:
- Der Anhang-Handler hatte keinen Test; die ganze Rechtepruefung liess sich
durch einen hartcodierten ADMIN ersetzen, Suite gruen. Es ist die einzige
Stelle, die rohe Dateibytes ausliefert. tests/anhang-route.test.ts faehrt ihn
jetzt echt, acht Pruefungen.
- /api/scim in der Proxy-Allowlist stand in keinem Test. OEFFENTLICH ist
exportiert, der Test vergleicht beide Richtungen: eine Liste, die nur in einer
Richtung geprueft wird, laeuft dem Code hinterher.
- Ein offener Ereignisstrom ueberlebte Abmelden, Deaktivieren und
Sitzungswiderruf. Der Herzschlag prueft die Sitzung jetzt nach. Dass es kein
Test zeigte, lag an einer Attrappe `sitzung: {}` — eine Attrappe ohne die Form
der Wirklichkeit prueft etwas anderes als die Wirklichkeit.
- README kannte MITARBEITER nicht und behauptete, unbekannte Benutzer wuerden
angelegt. Wer auf v0.2.0 hebt und nur das README liest, haette genau diese
Leute ausgesperrt.
- sicherung.md nannte einen Anhaenge-Pfad, den es nirgends gibt — als
Rueckfallwert in einem Skript mit rm -rf. Jetzt bricht es ab statt zu raten.
- Attributnamen wurden nur in der Server Action geprueft, waehrend der
Graph-Leser sich im Kommentar darauf berief.
Dazu #027: die Ende-zu-Ende-Ablaeufe raeumen jetzt zentral auf, vor und nach
dem Lauf, und ein struktureller Test bewacht es. Beim ersten echten Lauf seit
dem 26. September waren sieben Ablaeufe rot, vier Ursachen, keine ein
Produktfehler — unbemerkt, weil pnpm test:e2e in keinem Tor steht. Das ist jetzt
#038 und keine Fussnote.
Ein Befund des Reviews war falsch (der Zustands-Handler habe keinen Test; er hat
acht direkte Aufrufe). Nachgeprueft, bevor etwas geaendert wurde.
1454 Vitest-Tests (vorher 1432), 113 E2E-Ablaeufe gruen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bisher holt Laufzettel Personen aus dem Verzeichnis. Das setzt Lesezugriff und
einen Netzweg dorthin voraus, und beides ist nicht ueberall gegeben. Mit der Art
SCIM ruft stattdessen der Anbieter an — sofort, nicht beim naechsten Intervall.
Umgesetzt ist, was authentik, Entra ID und Okta wirklich schicken: Users mit dem
Filter userName eq, POST/GET/PUT/PATCH/DELETE, dazu ServiceProviderConfig,
Schemas und ResourceTypes. Jeder Handler prueft sein Token selbst.
Ein Token je Quelle, argon2-gehasht, einmal angezeigt — es ist der einzige
Mandantenbeweis, den eine Zustellung mitbringt. DELETE deaktiviert und loescht
nichts. Eine SCIM-Quelle wird nicht abgeglichen, an zwei Stellen gehalten: ein
Abgleich, der dort nichts liest und ERFOLG meldet, haette ueber die
Abschaltregel die ganze Belegschaft deaktiviert.
Zwei Befunde kamen aus Gegenproben, nicht aus Tests:
- PATCH remove wirkte gar nicht. Der Schalter nurGesetzte las bei PATCH jedes
null als "nicht mitgeschickt"; eine beim Anbieter entfernte Abteilung stand
hier weiter. Noetig war er nie — patchAnwenden liefert immer den
vollstaendigen Zielzustand.
- Entra ID schickt active als "True"/"False". Beim Deaktivieren kommt mit einem
reinen value === true zufaellig das Richtige heraus, beim Aktivieren nicht:
ein wieder freigeschaltetes Konto waere fuer immer inaktiv geblieben.
Dazu ein Fehler aus #028: in verbindungTestenAktion stand zweimal dieselbe
Bedingung, der zweite Zweig war seit dem ersten Tag unerreichbar.
1432 Tests (vorher 1373). Klickprobe am gebauten Stand mit einer echten
Zustellung. Gegen echte Mandanten ist nichts gelaufen — die Anfrageformen
stammen aus der Dokumentation, und das steht so in der Testdatei.
Closes#49
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ein Haus ohne lokales Active Directory hat kein LDAP, auf das sich
zeigen liesse. Eine Verzeichnisquelle hat jetzt eine **Art** — LDAP oder
GRAPH —, und beide laufen durch **denselben** Abgleich: anlegen,
aktualisieren, deaktivieren, nie loeschen. Nur das Holen der Eintraege
unterscheidet sich; zwei Abgleiche nebeneinander wuerden
auseinanderdriften, und die Zusage „es wird nichts geloescht“ gaelte
dann fuer eine Art und fuer die andere nicht mehr ganz.
Gelesen wird mit dem Client-Credentials-Flow, seitenweise ueber
@odata.nextLink, der Vorgesetzte ueber $expand=manager($select=id). Das
Client-Secret wird verschluesselt gespeichert wie das Bind-Passwort und
erreicht keine Antwort und keinen Protokolleintrag.
Die Art ist **unveraenderlich**, und das steht nicht im Issue: an ihr
haengt, was `externeId` bedeutet. Im AD eine objectGUID, in Entra ID
eine andere GUID — nach einem Wechsel wuerde kein einziger Mitarbeiter
wiedererkannt, der naechste Abgleich legte die ganze Belegschaft neu an
und setzte die alten Zeilen auf aktiv = false, mit allen Laeufen und
Geraeten daran.
Der teuerste Fund beim Bauen: „in Entra ID deaktiviert“ als
*uebersprungen* zu melden haette die Leute **aktiv** gelassen.
Uebersprungen heisst im Abgleich „nicht zuordenbar“, und solche
Eintraege werden seit #016 absichtlich nicht abgeschaltet. Gefiltert
wird deshalb serverseitig mit `$filter=accountEnabled eq true`, als
Gegenstueck zum LDAP-Standardfilter. Gefunden hat es der Test — aber nur,
weil der Doppelgaenger den Filter wirklich auswertet statt ihn bloss zu
empfangen.
{{ou}} bleibt bei einer Graph-Quelle leer: Entra ID hat keine OU, und
etwas hineinzudeuten waere ein Wert, den kein PowerShell-Befehl brauchen
kann — er saehe nur richtig aus. Steht an der Abteilung eine Ziel-OU
gepflegt, greift die.
Gegen einen echten Entra-Mandanten ist das **nicht** gelaufen. Geprueft
ist es gegen einen HTTP-Doppelgaenger mit aufgezeichneten Antworten; die
Pruefliste fuer den ersten echten Lauf steht am Ende von
docs/betrieb/entra-id.md.
Closes#48
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
„Was ist eigentlich mit meinem Notebook passiert?“ musste bisher ein
Techniker nachsehen. Ein Mitarbeiter kann die Arbeiten an **seinen**
Geraeten jetzt selbst ansehen — mit der fuenften Rolle MITARBEITER,
zugeordnet beim Anmelden ueber den UPN.
Was er sieht, sind Titel, Zeitraum und Status. Keine Schritte, keine
Notizen, keine Kommentare, keine Anhaenge: in einem Lauf stehen
Arbeitsnotizen ueber Menschen und Befehle mit Platzhaltern fuer
Zugangsdaten. Eine Selbstauskunft ist keine Akteneinsicht, und das haelt
nicht die Seite fest, sondern das Lesemodell — eine Seite, die filtert,
ist eine Seite, die es vergessen kann.
`selbstauskunft` ist ein **Ersatz** fuer `lesen` und kein Zusatz; auch
ein ADMIN hat es nicht. Ein zweiter Weg zu denselben Daten waere eine
zweite Stelle, an der jemand die Einschraenkung auf die eigene Person
vergisst. Die Datenbank verlangt das sogar: ein CHECK haelt fest, dass
Rolle und Zuordnung zusammengehoeren, in beide Richtungen.
Ein mehrdeutiger UPN wird abgewiesen und protokolliert. Zwei Kunden
koennen dieselbe Person fuehren; ein findFirst entschiede nach
Sortierung darueber, wessen Geraete jemand sieht.
**Verhaltensaenderung:** wer sich anmeldet und in keiner Rollengruppe
steht, bekommt nicht mehr automatisch LESER — also nicht mehr
Lesezugriff auf alle Laeufe des Hauses. Das greift nur mit
konfigurierten OIDC_GROUP_*; ohne sie bleibt alles wie vorher, denn ohne
abgebildete Gruppen laesst sich Techniker nicht von Mitarbeiter
unterscheiden.
Nebenbei behoben: `staerksteRolle` benutzte LESER als Startwert und
damit als Untergrenze — mit einer Rolle darunter lieferte sie mehr
Rechte als uebergeben. Gefunden von einem Test, der seit #007
unveraendert dasteht.
Closes#55
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ein zugewiesener Lauf fiel bisher nur auf, wenn jemand die Uebersicht
oeffnete; ein ueberfaelliger ueberhaupt nicht. Vier Anlaesse gehen jetzt
hinaus — zugewiesen, ueberfaellig, abgeschlossen und Verzeichnisabgleich
fehlgeschlagen —, je Person abschaltbar. Niemand bekommt Post ueber die
eigene Tat: das ist der haeufigste Grund, Benachrichtigungen ganz
abzuschalten, und deshalb faellt der Handelnde immer aus der
Empfaengerliste.
Eingereiht wird im Anfrage-Pfad, gesendet wird es nicht. Ein haengender
Mailserver darf kein Abhaken blockieren, also schreibt der Dienst eine
Zeile in `Zustellung` und ein eigener Zeitplan schickt sie. Betreff und
Rumpf stehen fertig darin: eine Meldung sagt, was *damals* war.
In keiner Nachricht steht ein Schrittinhalt, und damit auch kein
erzeugtes Passwort. Darueber wacht src/lib/meldungen.ts — die Funktionen
dort nehmen nur Kopfdaten, der Typ laesst nichts anderes zu. Geprueft
wird es zusaetzlich ueber alle Rumpftexte, die wirklich verschickt
wurden, so wie #012 es fuer die Druckansicht tat.
Zwei Dinge haetten ohne Datenbank nicht gehalten. Die Einmaligkeit der
Ueberfaelligkeitsmeldung liegt in einem partiellen eindeutigen Index:
der Zeitplan sieht jede Minute nach, und ohne ihn bekaeme der
Zustaendige fuer einen Lauf, der ueber das Wochenende steht, 2880
Mails — jede fuer sich korrekt, kein Test rot. Und die Sperre der
Warteschlange liegt in derselben Tabelle wie die des Abgleichs, denn
hinter einem Lastverteiler laufen zwei Instanzen.
Eine stille Warteschlange ist schlimmer als keine: Verwaltung →
Zustellungen zeigt mit Grund, was hinausging und was nicht.
Geprueft gegen einen echten SMTP-Faenger (mailpit) statt gegen eine
abgefangene Funktion — und `mailpit` gehoert damit ins Tor vor dem
Commit, sonst ueberspringt die Suite 3 Tests und sieht gruen aus.
Closes#54
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Die Abarbeitungsansicht fragte alle zehn Sekunden nach dem Zustand. Jetzt
melden vier Datenbank-Trigger `NOTIFY lauf_geaendert, '<laufId>'`, eine
LISTEN-Verbindung je Prozess hoert zu, und der Route Handler schiebt
weiter. Gemessen: der Haken des Kollegen ist nach 485 ms beim Zuschauer
(am Handy 487 ms), vorher bis zu zehn Sekunden. Zwanzig offene Laschen
kosten nichts mehr statt zwanzig Abfragen je zehn Sekunden.
Das Ereignis kommt aus der Datenbank und nicht aus dem Dienst, aus
demselben Grund, aus dem auch die Regeln dort stehen: ein Trigger sieht
auch den Seed, eine Migration und die Reparatur per psql. Und NOTIFY
erreicht jede Instanz der Anwendung, ohne einen zweiten Dienst.
Welche Tabellen melden, haengt an dem, was `laufZustand` liefert — nicht
daran, was das Issue vorgeschlagen hat. Mit Triggern nur auf
Schrittergebnis und Lauf waere der Strom ein **Rueckschritt** gewesen:
das Polling verglich den ganzen Stand und sah deshalb auch Kommentare,
ein Strom sieht nur, was gemeldet wird. Der Kommentar des Kollegen waere
nie angekommen, und die Seite haette ausgesehen wie immer. Es melden
jetzt vier Tabellen, und `laufZustand` traegt `anzahlAnhaenge` — ein
Ereignis ohne Entsprechung im Zustand loest nichts aus.
Das Polling bleibt als Rueckfall an **derselben** Adresse, unterschieden
nur am Accept-Kopf: eine Rechtepruefung, ein Lesemodell, eine Nutzlast.
Ein zweiter Endpunkt waere eine zweite Stelle, an der jemand
rechtErforderlich vergessen kann. Bei erreichter Obergrenze antwortet
der Handler gewoehnlich mit JSON; EventSource meldet darauf einen Fehler
und versucht es nicht wieder, also faellt der Client von sich aus
zurueck — ohne Sonderfall.
Nebenbei zwei Fehler in docs/betrieb/reverse-proxy.md behoben, beide aus
#52: `client_max_body_size 8m` bei `ANHANG_MAX_MB=10` haette ein
10-MB-Foto mit 413 abgewiesen, bevor die eigene Meldung zum Zuge kommt.
Closes#53
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Zwei Wege in den Betrieb, beide so weit gefahren, wie es hier moeglich war.
deploy/install.sh richtet Debian 13 in neun Schritten ein: PostgreSQL 17 hoert
nur am Unix-Socket, der Dienstnutzer hat kein Login, die Geheimnisse entstehen
im Zielsystem und liegen 0600 bei root. Die Umgebungsdatei wird nie
ueberschrieben -- auch nicht beim Aktualisieren, sonst enden alle Sitzungen und
die verschluesselten Bind-Passwoerter waeren unlesbar.
deploy/proxmox/laufzettel.sh legt den Container an und ruft install.sh darin
auf. Der pct-Teil ist ungetestet, weil kein PVE-Host zur Verfuegung stand; die
Gegenprobe steht als Pruefliste in docs/betrieb/proxmox.md, das Folge-Issue ist
Nummer 38.
Die Abnahme gegen alle 61 Kriterien hat drei Zusagen ohne Deckung gefunden, alle
behoben: der Healthcheck des Webdienstes konnte nie bestehen (busybox loest
localhost nach ::1 auf, Next lauscht nur auf IPv4), die CI-Pipeline konnte in
keinem Job gruen werden (kein LDAP-Dienst, kein gestarteter Server, kein
shellcheck), und der README nannte fuer die Entwicklungsdatenbank den falschen
Port.
Closes#19
Der Einstieg am Schreibtisch und das Gedaechtnis der Anwendung. Die Uebersicht
sortiert nach Dringlichkeit — ueberfaellig zuerst, und das ist das Einzige in
Warnfarbe; alles andere auf der Seite ist Auskunft.
Das Archiv sucht ueber Titel, Notizen, erfasste Werte und Kommentare in einer
SQL-Anweisung: `Schrittergebnis.wert` ist ein Json, und nur `("wert")::text`
trifft den Trigramm-Index aus #006. Vor allem aber gehoert die Fundstelle zum
Treffer — ein Treffer ohne sichtbaren Grund ist fuer den Suchenden wertlos.
Die Druckansicht ist ein eigenes Lesemodell und kein Schalter an
`laufZumAbarbeiten`: dort entsteht beim Fuellen der Befehle ein Passwort, und auf
einem Blatt, das jemand abheftet, darf keines stehen. Vom Mitarbeiter holt sie
nur Name, UPN und Abteilung — nicht alles und dann gefiltert.
Der unabhaengige Review fand zwei Blocker, beide im Dashboard: „ueberfaellig“
verglich mit dem Augenblick statt mit dem Tagesbeginn, und der Schnellstart
zaehlte Laeufe fremder Kunden mit. Was „ueberfaellig“ heisst, steht seitdem in
`src/lib/fristen.ts` — eine Regel fuer Lesemodell und Anzeige.
Closes#15
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Der Einstieg, fuer den die Anwendung gebaut ist: scannen, sehen was zu tun ist,
loslegen. `/t/<tagCode>` kennt drei Faelle — registriertes Geraet, vorgedruckter
Tag, unbekannter Code —, die Geraeteseite zeigt Zuweisungen, Schnellstart und
Historie, und beides ist am Handy bedienbar.
Im QR steht ausschliesslich `{APP_URL}/t/{tagCode}`: kein Name, keine
Seriennummer, kein Besitzer. Ein Aufkleber haengt jahrelang sichtbar am Geraet
und wird von jedem Telefon gelesen; was darauf steht, ist veroeffentlicht. Die
Masse sind gemessen, nicht behauptet: ein echtes PDF liefert 62 x 29 mm und A4,
und ein bei 300 dpi gerastertes Etikett wird mit demselben Decoder
zurueckgelesen, den auch der Scanner benutzt.
Drei Defekte zeigte erst der gebaute Stand, alle an den Naehten zwischen den
parallel gebauten Teilen: „Starten“ setzte IN_ARBEIT und sprang nicht, „Vorlage
zuweisen“ bot Vorlagen an, die der Dienst danach ablehnte, und die Geraeteliste
verlinkte nichts. Der unabhaengige Review fand drei Blocker — darunter, dass der
Scanner nie die native BarcodeDetector-API benutzte und dass ein Unterpfad in
APP_URL aus dem gedruckten QR herausfiel.
Closes#14
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ein Lauf wird vorbereitet, abgearbeitet, abgeschlossen und archiviert. Der
Archiv-Schreibschutz sitzt in genau einer Funktion und wird vor jeder Aenderung
an Kopfdaten, Schrittergebnis und Kommentar gerufen; abgeschlossen bleibt
aenderbar, archiviert nicht.
Die Abarbeitungsansicht speichert jeden Schritt einzeln, frischt sich alle zehn
Sekunden auf und pausiert im Hintergrund. Sie traegt in der Polling-Antwort
keine Befehle und laedt sich auch nicht von selbst neu, solange ein generiertes
Passwort auf der Seite steht: beides tauschte es dem Techniker zwischen Lesen
und Einfuegen aus.
Der unabhaengige Review fand zwei Blocker: ein Befehl konnte zwei verschiedene
Passwoerter tragen (Konto bekommt A, Benutzer erfaehrt B), und „nicht
zutreffend“ liess sich nicht zuruecknehmen, waehrend die Oberflaeche
behauptete, nichts sei gewaehlt. Beides behoben und in Tests festgehalten.
Closes#13
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Zwei Betriebsmodi, ein Codepfad. src/server/mandant.ts ist die eine Stelle,
an der die Mandantenregel steht.
Der Review fand drei Blocker: die Lesemodelle wandten die Mandantensicht nicht
an, der Protokolleintrag des Moduswechsels stand außerhalb der Transaktion, und
Prüfung und Vollzug lagen so weit auseinander, dass zwei gleichzeitige Wechsel
beide durchliefen. Alle drei geschlossen, der letzte mit einem Advisory Lock.
336 Tests.
Closes#8
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
OIDC mit Discovery, PKCE, state und nonce; lokaler Login mit argon2 als
Notfallzugang; Rechte in einer Tabelle, Prüfung ausschließlich serverseitig,
Rolle und Aktiv-Status pro Anfrage aus der Datenbank.
Ein unabhängiger Review fand eine offene Weiterleitung: die Präfix-Prüfung des
Rücksprungziels ließ /<TAB>/fremd.example durch, woraus der Browser
//fremd.example macht. Das Ziel wird jetzt aufgelöst und die Herkunft
verglichen. Dazu acht weitere Befunde, alle behoben.
274 Tests.
Closes#7
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Vollständiges Datenmodell in deutscher Benennung: 20 Tabellen, 9 Aufzählungen,
eine Migration, dazu die framework-freien Helfer für tagCodes und Geheimnisse.
Regeln stehen zusätzlich in der Datenbank, nicht nur im Service: 12 CHECKs,
2 partielle Eindeutigkeitsindizes, 4 zusammengesetzte Fremdschlüssel. Die
Mandantengrenze ist damit strukturell erzwungen. Schrittergebnisse, Kommentare
und Protokolleinträge kaskadieren bewusst nicht — das Archiv ist das Gedächtnis
der Anwendung.
106 Tests, davon 42 gegen die echten Datenbankregeln.
Closes#6
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Legt die Grundlage, auf der alle weiteren Meilensteine nur noch Fachlichkeit
ergänzen: Next 16 mit App Router und TypeScript strict, Tailwind 4, Prisma 7,
Vitest 5, Playwright, ESLint, Prettier und die Hooks für Lint und Commits.
Zwei Dinge sind bewusst so gebaut und durch Tests festgehalten:
Die Datenbank ist im Betriebsstand von außen nicht erreichbar. Sie hat kein
ports:-Mapping und hängt allein im Netz "intern", das ohne Gateway läuft; nur
"web" kommt zusätzlich nach draußen. tests/betriebsstand.test.ts schlägt an,
falls ein Port-Mapping zurückkehrt. Geprüft, nicht behauptet: docker compose up
startet den Stack, 5432 ist vom Host aus zu, aus "web" offen.
pnpm pruefe:doku hält Dokumentation und Code zusammen — Version im Changelog,
Issue-Dateien gegen ihre Übersicht, keine Verweise auf fehlende Dateien, keine
fehlenden Bilder. Das Skript wurde gegen vier eingebaute Fehler gegengeprüft.
Drei Versions-Obergrenzen sind nötig und in CLAUDE.md begründet: eslint bleibt
auf 9.x, weil eslint-plugin-react keinen Nachfolger für eslint 10 hat und das
Regelwerk sonst beim Laden abbricht; typescript bleibt auf 6.0.x, weil
typescript-eslint TypeScript 7 noch nicht unterstützt; prisma ist exakt auf
7.10.0 gepinnt, weil sein latest-Tag auf einen 8.0-RC zeigt, der von
@prisma/client aber nicht.
Auth.js entfällt entgegen der ursprünglichen Vorgabe: v5 steht dauerhaft auf
Beta, das Projekt ist im Wartungsmodus und sein Prisma-Adapter deckt Prisma 7
nicht ab. Stattdessen openid-client und jose, wie in ProxLedger.
Dieser erste Commit geht direkt auf main, weil es noch keinen Branch gibt, von
dem abzuzweigen wäre. Ab #006 läuft jedes Issue über einen eigenen Branch.
Closes#5
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>