Drei Aufgaben: Loeschweg end-to-end sichtbar (Tracer), Formular- und Ladeweg nachziehen, Aktionsknoepfe der SUPER_ADMIN-Zeile fuer ADMIN nicht anbieten. Texte ueber next-intl in de/en, Serverpruefung unangetastet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
30 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, estimate, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | estimate | must_haves | |||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| quick-260921-a1d | 01 | execute | 1 |
|
true |
|
|
|
Seit Quick 260914-ebg (WINDOWS #29) ist dieser Fall im Alltag erreichbar: die Zeile des SUPER_ADMIN steht in
der Benutzerliste eines ADMIN, und Aendern oder Loeschen darauf liefert jetzt 403
(apps/api/src/user/user.controller.ts, Zielrollen-Riegel in update und remove).
Zwei Haelften, beide verbindlich:
- Das Scheitern sichtbar machen — mit dem Text aus dem Antwortrumpf, wo der Server einen liefert, sonst mit einer uebersetzten Ersatzmeldung.
- Das Unmoegliche gar nicht erst anbieten — fuer einen ADMIN entfallen Bearbeiten und Loeschen in der Zeile des SUPER_ADMIN. Der 403 ist danach das Sicherungsnetz, nicht der Regelweg.
Purpose: Die Benutzerverwaltung gibt bei jeder abgewiesenen Aktion eine Antwort, die man lesen kann. Output: Sichtbare Fehlermeldungen in Formular, Loeschdialog und Listenkopf; rollenrichtige Aktionsknoepfe; eine neue Vitest-Datei, die beides nachweist.
Entscheidungen, die in diesen Plan eingeflossen sind
- D-01 (gesetzt): Alle neuen Texte laufen ueber next-intl in
de.jsonUNDen.json. Keine fest verdrahteten Zeichenketten — Quick 260701-abc hat genau diese Fehlerklasse schon einmal beseitigt. - D-02 (gesetzt): Deutsche Oberflaechentexte in der Sie-Form, wie der restliche Katalog.
- D-03 (gesetzt): Der Text des Servers hat Vorrang, wenn die Antwort einen traegt; sonst greift eine uebersetzte Ersatzmeldung. Kein Fall endet ohne Rueckmeldung.
- D-04 (gesetzt): Die Serverpruefung wird nicht angefasst.
apps/apisteht nicht in der Dateiliste. Das Ausblenden eines Knopfes ist eine Schicht OBERHALB der Serverpruefung, niemals ihr Ersatz (siehe<threat_model>, T-A1D-02). - D-05 (gesetzt): Aenderung bleibt in der Benutzerseite und den beiden Katalogen. Vorhandene Muster
werden wiederverwendet statt neu erfunden — gemessen:
apps/web/src/app/(portal)/admin/groups/page.tsxhat bereits ein Fehlerbanner,apps/web/src/lib/tender-radar-api.tsbereits eine Rumpf-Auswertung. - D-06 (gesetzt): Keine Umformatierung der Datei, keine Versionsspruenge.
Gemessene Ausgangslage (2026-09-21, vor der Planung geprueft)
apps/web/src/app/(portal)/admin/users/page.tsx: 428 Zeilen. Drei Stellen verschlucken still:fetchUsers(Zeile 60-73),handleSubmit(107-142),handleDelete(144-157).- Dritte Fundstelle ist IN SCOPE.
fetchUserswird mitbehandelt: scheitert das Laden, zeigt die Seite heute "Keine Benutzer gefunden" — eine falsche Aussage, dieselbe Fehlerfamilie, dieselbe Datei, sehr geringe Zusatzkosten. Nichts wird ausgelassen. - Serverantworten fuer die 403-Wege, woertlich gemessen in
apps/api/src/user/user.controller.ts:Cannot modify a SUPER_ADMIN user(Z. 199),Cannot delete a SUPER_ADMIN user(Z. 262),Cannot modify users from other tenants(Z. 188),Cannot delete users from other tenants(Z. 255),Cannot delete your own account(Z. 247),Cannot assign SUPER_ADMIN role(Z. 151/204). Es gibt keinen eigenen ExceptionFilter inapps/api/src(geprueft), also gilt die Standardform von NestJS:{ statusCode, message, error }, bei 500 lautetmessageschlichtInternal server error. - Bekannte Eigenheit, ausdruecklich NICHT Teil dieses Plans: diese Servertexte sind englisch. Nach D-03
werden sie angezeigt wie sie sind, eingefasst in einen deutschen Rahmensatz. Eine Uebersetzung der
Servertexte waere eine Aenderung an
apps/apiund damit eine eigene Aufgabe — hier bewusst nicht getan, damit der Plan die Serverantwort nicht anfasst (D-04). - Testbestand
apps/web, gemessen mitpnpm --filter @tessera/web test: 65 Dateien, 447 Tests, alle gruen. Erwartung nach diesem Plan: 66 Dateien, 447 + neue Tests. pnpm --filter @tessera/web type-check: Exit 0.pnpm lint(Wurzel, Biome 2.5.0 ueber alle fuenf Workspaces): 5 von 5 erfolgreich. Bestehende Warnungen blockieren nicht, jede NEUE Meldung im Fehlerrang faerbt den Lauf rot.- Der Waechter
apps/web/src/messages/umlaut-guard.spec.tsprueft dreierlei: keine Ersatzschreibung im Deutschen, kein neues Wort mit ae/oe/ue/ss ausserhalb der Erlaubnisliste, und Schluesselgleichheit von de.json und en.json ueber den gesamten Katalog. Ein Schluessel nur in einer Sprache faellt sofort durch.
<execution_context>
@/.claude/gsd-core/workflows/execute-plan.md
@/.claude/gsd-core/templates/summary.md
</execution_context>
Zweitens die Auswertung des Antwortrumpfs. In `page.tsx` auf Modulebene (ausserhalb der Komponente,
unterhalb der Schnittstellen-Deklarationen) eine Funktion `readApiMessage` mit der Signatur
`(res: Response) => Promise<string | null>` anlegen. Sie liest `await res.json()`, gibt `body.message`
zurueck, wenn es eine nicht-leere Zeichenkette ist, verbindet ein Feld von Zeichenketten mit
`, ` (so liefert NestJS Pruefmeldungen aus class-validator), und gibt in jedem anderen Fall sowie bei
einer Ausnahme aus `res.json()` `null` zurueck. Formvorlage ist `extractErrorMessage` in
`apps/web/src/lib/tender-radar-api.ts`; bewusst lokal kopiert statt importiert, weil jene Datei zum
Modul Ausschreibungs-Radar gehoert und die Verwaltungsseite nicht davon abhaengen soll. Ausdruecklich
NUR das Feld `message` lesen — niemals `res.text()` des ganzen Rumpfes, damit eine fremde HTML-
Fehlerseite eines vorgelagerten Dienstes nicht in die Oberflaeche geraet (T-A1D-01).
Drittens der Loeschweg. Einen Zustand `deleteError` vom Typ `string | null` ergaenzen. In `handleDelete`
zu Beginn auf `null` setzen; im Sonst-Zweig zu `res.ok` den Text ueber `readApiMessage` holen und
`t('errors.serverRejected', { detail })` setzen, wenn ein Text kam, sonst `t('errors.generic')`. Im
Fang-Zweig — dessen Rumpf bisher nur einen Kommentar enthaelt — `t('errors.network')` setzen; der
Kommentar entfaellt ersatzlos. Beim Oeffnen des Dialogs (`setDeleteConfirm(user.id)`) ebenfalls auf
`null` zuruecksetzen, damit eine alte Meldung nicht an einer neuen Zeile klebt.
Viertens die Meldungsflaeche. Im Loeschdialog oberhalb der Knopfreihe ein Banner rendern, wenn
`deleteError` gesetzt ist, mit `role="alert"` und exakt den Klassen aus
`apps/web/src/app/(portal)/admin/groups/page.tsx`:
`rounded-md border border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive`. Der Text wird
als React-Kind gerendert, niemals ueber `dangerouslySetInnerHTML` (T-A1D-03). Anders als die
Gruppenseite wird weder `res.status` noch der Rohrumpf angezeigt — nur der gerahmte Servertext.
Die uebrige Datei bleibt unangetastet: keine Umformatierung, keine Umsortierung der Einfuhren, keine
Aenderung an `apps/api`.
cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/web test 'admin/users' 2>&1 | tail -8 # erwartet: 2 Dateien, alle Tests gruen (Ausgangslage war 1 Datei / 8 Tests)
cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/web test 'messages/umlaut-guard' 2>&1 | tail -8 # erwartet: 3 Tests gruen — belegt Schluesselgleichheit de/en und saubere Umlaute
cd /home/vicolab/projects/tessera-ctl && node -e "const d=require('./apps/web/src/messages/de.json'),e=require('./apps/web/src/messages/en.json');const k=o=>Object.keys(o.admin.users.errors).sort().join(',');if(k(d)!=='generic,loadFailed,network,serverRejected')throw new Error('de errors keys: '+k(d));if(k(e)!==k(d))throw new Error('en weicht ab: '+k(e));console.log('OK',k(d))"
Ein mit 403 abgewiesener Loeschvorgang zeigt den Servertext im Loeschdialog; der Dialog bleibt offen.
Eine Antwort ohne verwertbaren Rumpf und ein Verbindungsfehler zeigen jeweils ihre uebersetzte
Ersatzmeldung. Der Erfolgsfall schliesst den Dialog und laedt die Liste neu wie bisher. Vier Schluessel
unter `admin.users.errors` in beiden Katalogen, Umlaut-Waechter gruen.
Zusaetzlicher Zustand und ein Banner in einer Datei — ruecknehmbar durch Zuruecksetzen der Datei.
Aufgabe 2: Formularweg und Listenladen auf dieselbe Rueckmeldung heben
apps/web/src/app/(portal)/admin/users/page.tsx, apps/web/src/app/(portal)/admin/users/users-page.test.tsx
apps/web/src/app/(portal)/admin/users/page.tsx (Zeilen 60-73 fetchUsers, 83-105 openCreate/openEdit, 107-142 handleSubmit, 178-196 Kopfbereich und Ladezustand, 281-391 Formulardialog)
Weitere Tests in `users-page.test.tsx`, gleiche Bauart wie in Aufgabe 1:
- Test 5 (Aendern wird abgewiesen): Liste laedt, Klick auf Bearbeiten, Absenden des Formulars; fetch
antwortet `{ ok: false, status: 403, json: () => Promise.resolve({ message: 'Cannot modify a
SUPER_ADMIN user' }) }`. Erwartet: "Der Server hat die Aktion abgelehnt: Cannot modify a SUPER_ADMIN
user" steht im Dokument UND das Formular ist weiterhin offen (das Feld Benutzername ist noch da).
- Test 6 (Pruefmeldungen als Feld): Rumpf `{ message: ['username must be longer', 'email must be an
email'] }`. Erwartet: beide Teiltexte erscheinen, mit `, ` verbunden, im Rahmensatz.
- Test 7 (Verbindung scheitert beim Speichern): fetch wirft. Erwartet: die Netzmeldung steht im
Dokument, das Formular bleibt offen.
- Test 8 (Liste laedt nicht): der erste fetch antwortet `{ ok: false, status: 500, json: () =>
Promise.reject(new Error('not json')) }`. Erwartet: "Die Benutzerliste konnte nicht geladen werden."
steht im Dokument und der Text "Keine Benutzer gefunden" steht NICHT im Dokument.
- Test 9 (Meldung ueberdauert nicht): nach einem abgewiesenen Speichern das Formular schliessen und
erneut Bearbeiten oeffnen — die alte Meldung ist verschwunden.
Zwei weitere Zustaende ergaenzen: `formError` und `loadError`, beide `string | null`.
`handleSubmit`: zu Beginn `formError` auf `null` setzen. Sonst-Zweig zu `res.ok` und Fang-Zweig genau
wie in Aufgabe 1 fuer den Loeschweg aufgebaut — `readApiMessage` befragen, bei Text
`t('errors.serverRejected', { detail })`, sonst `t('errors.generic')`, im Fang-Zweig
`t('errors.network')`. Der Rumpf des Fang-Zweigs enthaelt danach echte Zuweisung statt eines
Kommentars. Zusaetzlich in `openCreate` und `openEdit` `formError` auf `null` setzen, damit eine
Meldung nicht in den naechsten Dialogaufruf hinueberwandert.
Meldungsflaeche im Formulardialog: unterhalb des Rollenfeldes und oberhalb der Knopfreihe
(Abbrechen/Speichern) ein Banner mit `role="alert"` und denselben Klassen wie in Aufgabe 1. Die
Platzierung innerhalb des `<form>` ist bewusst: die Meldung muss dort stehen, wo der Blick nach dem
Klick auf Speichern ohnehin ist.
`fetchUsers`: dieser dritte Fall ist ausdruecklich Teil dieses Plans und wird NICHT ausgelassen. Zu
Beginn `loadError` auf `null` setzen, im Sonst-Zweig zu `res.ok` und im Fang-Zweig
`t('errors.loadFailed')` setzen. Weil `fetchUsers` in `useCallback` mit leerer Abhaengigkeitsliste
steckt und `t` aus `useTranslations` stammt: `t` der Abhaengigkeitsliste hinzufuegen, damit die
Biome-Regel `useExhaustiveDependencies` keine neue Meldung erzeugt (sie steht auf Warnung, aber der
Rueckstand soll nicht wachsen). `setLoading(false)` im `finally` bleibt unveraendert.
Meldungsflaeche fuer `loadError`: direkt unter dem Seitenkopf, vor der Tabelle — dieselbe Stelle und
dieselben Klassen wie das Fehlerbanner in `apps/web/src/app/(portal)/admin/groups/page.tsx`. Solange
`loadError` gesetzt ist, darf der Hinweis "Keine Benutzer gefunden" nicht erscheinen: die leere Liste
ist dann keine Aussage ueber den Datenbestand, sondern Folge des gescheiterten Ladens.
Weiterhin nichts an `apps/api`, keine Umformatierung, keine Aenderung an der Erfolgslogik.
cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/web test 'admin/users' 2>&1 | tail -8 # erwartet: 2 Dateien gruen, Testzahl gegenueber Aufgabe 1 gestiegen
cd /home/vicolab/projects/tessera-ctl && grep -v '^\s*//' "apps/web/src/app/(portal)/admin/users/page.tsx" | grep -c 'role="alert"' # erwartet: 3 (Loeschdialog, Formular, Listenkopf)
cd /home/vicolab/projects/tessera-ctl && test "$(grep -c 'silently fail' "apps/web/src/app/(portal)/admin/users/page.tsx")" = "0" && echo OK # kein still verschluckender Fang-Zweig mehr
Alle drei bisher stillen Wege melden sich: abgewiesenes Speichern im Formular, abgewiesenes Loeschen im
Dialog, gescheitertes Laden im Listenkopf. Bei gescheitertem Laden erscheint nicht mehr faelschlich
"Keine Benutzer gefunden". Meldungen ueberdauern das Schliessen eines Dialogs nicht.
Reine Ergaenzung in einer Datei.
Aufgabe 3: Aktionsknoepfe der SUPER_ADMIN-Zeile einem ADMIN nicht anbieten, Gesamtlauf
apps/web/src/app/(portal)/admin/users/page.tsx, apps/web/src/app/(portal)/admin/users/users-page.test.tsx
apps/web/src/app/(portal)/admin/users/page.tsx (Zeilen 221-275, Tabellenzeile mit der Knopfreihe)
apps/api/src/user/user.controller.ts (Zeilen 196-205 und 258-264, Zielrollen-Riegel — nur lesen, nicht aendern)
Weitere Tests in `users-page.test.tsx`, Liste enthaelt drei Zeilen: ein SUPER_ADMIN (`u0`), die
angemeldete Person selbst (`u1`) und ein gewoehnlicher Benutzer (`u2`).
- Test 10 (ADMIN sieht keine Knoepfe an der obersten Rolle): angemeldet als ADMIN. Erwartet: in der
Zeile des SUPER_ADMIN gibt es weder einen Knopf "Bearbeiten" noch "Löschen"; der Knopf "Details"
ist weiterhin da. In der Zeile von `u2` gibt es beide Knoepfe. Die Zuordnung Zeile-zu-Knopf ueber
`within(...)` auf der Tabellenzeile pruefen, nicht ueber Gesamtzaehlungen.
- Test 11 (SUPER_ADMIN sieht beide Knoepfe): angemeldet als SUPER_ADMIN. Erwartet: in der Zeile des
SUPER_ADMIN sind Bearbeiten und Löschen vorhanden.
- Test 12 (Selbstloeschung bleibt wie bisher): angemeldet als ADMIN `u1`. Erwartet: der Loeschknopf in
der eigenen Zeile ist vorhanden und gesperrt (`toBeDisabled`) — dieses Verhalten wird nicht
veraendert.
Eine Hilfsfunktion innerhalb der Komponente anlegen, zum Beispiel `canManageRow`, die genau die
Serverbedingung spiegelt: eine Zeile ist gesperrt, wenn `user.role === 'SUPER_ADMIN'` und
`currentUser?.role !== 'SUPER_ADMIN'`. Dieselbe Bedingung steht serverseitig in
`apps/api/src/user/user.controller.ts` in `update` und in `remove`; sie wird hier gespiegelt, nicht
ersetzt (D-04, T-A1D-02).
In der Knopfreihe der Tabellenzeile: Bearbeiten und Löschen nur rendern, wenn die Zeile nicht gesperrt
ist. Gesperrte Zeilen zeigen gar keinen dieser Knoepfe — nicht einen gesperrten, sondern keinen. Das
ist die Anforderung aus der Fehlerliste, und ein sichtbarer, aber gesperrter Knopf wuerde denselben
Ratespielraum lassen wie heute. "Details" bleibt in jeder Zeile stehen, weil der lesende Zugriff auf
die Zugriffsuebersicht davon nicht betroffen ist.
Die vorhandene Sperre des Loeschknopfes fuer die eigene Zeile (`disabled={user.id === currentUser?.id}`
samt ihren Klassen) bleibt unveraendert bestehen — sie ist ein anderer Fall und nicht Teil von
WINDOWS #36.
Keine Aenderung an `apps/api`. Keine Aenderung an der Rollenauswahl im Formular (die Einschraenkung der
Option SUPER_ADMIN dort besteht bereits und bleibt, wie sie ist).
cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/web test 'admin/users' 2>&1 | tail -8 # erwartet: 2 Dateien, alle Tests gruen
cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/web test 2>&1 | tail -6 # erwartet: 66 Dateien (Ausgangslage 65), mindestens 447 Tests, 0 Fehler
cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/web type-check 2>&1 | tail -3; echo "EXIT=${PIPESTATUS[0]}" # erwartet: EXIT=0 wie in der Ausgangslage
cd /home/vicolab/projects/tessera-ctl && pnpm lint 2>&1 | tail -4 # erwartet: 5 successful, 5 total — keine NEUE Meldung im Fehlerrang
cd /home/vicolab/projects/tessera-ctl && ST=$(git status --porcelain) || { echo "git fehlgeschlagen"; exit 1; }; case "$ST" in *apps/api/*) echo "FEHLER: apps/api wurde angefasst"; exit 1;; *) echo "OK: apps/api unberuehrt";; esac # T-A1D-02, kein Pipe: der Status von git wird zuerst gesichert
Ein ADMIN bekommt in der Zeile des SUPER_ADMIN keine Aktionsknoepfe mehr angeboten, ein SUPER_ADMIN
schon; die Sperre gegen Selbstloeschung ist unveraendert. Gesamter Testbestand von apps/web gruen
(66 Dateien), `type-check` mit Exit 0, `pnpm lint` in allen fuenf Workspaces erfolgreich, und
`apps/api` ist unberuehrt.
Sichtbarkeitsbedingung in einer Zeile der Tabelle — ruecknehmbar.
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| API → Browser (Antwortrumpf) | Text aus der Serverantwort wird neu in die Oberflaeche uebernommen und angezeigt. Bisher wurde er verworfen. |
| Browser → API (Aktionsaufruf) | Unveraendert: jeder Aendern-/Loeschaufruf laeuft weiterhin durch Wache, Rollenpruefung und Zielrollen-Riegel der API. |
| Rolle des Anmeldenachweises → Darstellung | currentUser.role steuert ab jetzt zusaetzlich, welche Knoepfe eine Zeile anbietet. Ein Wert, den der Browser haelt — daher niemals eine Berechtigungsgrenze. |
STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|---|---|---|---|---|---|
| T-A1D-01 | Information Disclosure | readApiMessage in apps/web/src/app/(portal)/admin/users/page.tsx |
low | mitigate | Vor der Planung gemessen: saemtliche Ausnahmen der Benutzer-Endpunkte in apps/api/src/user/user.controller.ts werfen feste englische Zeichenketten ohne Benutzernamen, Kennungen oder Aufrufspuren; in apps/api/src existiert kein eigener ExceptionFilter, es gilt also die NestJS-Standardform { statusCode, message, error } und ein 500 traegt nur Internal server error. Zusaetzlich liest die Auswertung ausschliesslich das Feld message — niemals res.text() des ganzen Rumpfes. Eine fremde HTML-Fehlerseite (Nginx Proxy Manager davor) liefert damit null und fuehrt zur uebersetzten Ersatzmeldung statt zu fremdem Inhalt in der Oberflaeche. |
| T-A1D-02 | Elevation of Privilege | Sichtbarkeit der Aktionsknoepfe in der Benutzertabelle | high | mitigate | Das Ausblenden ist ausschliesslich Ergonomie und liegt OBERHALB der Serverpruefung. Der Zielrollen-Riegel in update und remove (apps/api/src/user/user.controller.ts) bleibt unveraendert und bleibt die einzige wirksame Grenze; wer den Aufruf direkt absetzt, bekommt weiterhin 403. apps/api steht nicht in files_modified, und Aufgabe 3 prueft das mit einem eigenen Gatter (git status --porcelain darf keinen Pfad unter apps/api/ zeigen). |
| T-A1D-03 | Tampering | Darstellung des Servertextes im Banner | medium | mitigate | Der Text wird als React-Kind gerendert und damit maskiert; dangerouslySetInnerHTML ist fuer diese Flaechen ausgeschlossen. Damit kann ein manipulierter Antwortrumpf kein Markup in die Seite bringen. |
| T-A1D-04 | Information Disclosure | Meldung Cannot modify users from other tenants |
low | accept | Diese Meldung bestaetigt einem ADMIN die Existenz einer Kennung in einem fremden Mandanten. Das ist bestehendes Serververhalten: die Antwort erreicht den Browser schon heute vollstaendig, nur wird sie verworfen. Das Anzeigen aendert nichts daran, wer sie lesen kann. Abstellen hiesse die Serverantwort aendern, was D-04 ausschliesst — als Beobachtung fuer die Fehlerliste vermerkt, nicht in diesem Lauf behandelt. |
| T-A1D-05 | Denial of Service | Ersatzmeldungen bei fehlender Antwort | low | mitigate | Jeder Zweig endet in einer Meldung: Servertext, generic, network oder loadFailed. Kein Pfad laesst die Oberflaeche ohne Rueckmeldung stehen — genau das war der Befund von WINDOWS #36. |
| </threat_model> |
pnpm --filter @tessera/web test— erwartet 66 Testdateien (Ausgangslage 65) und mindestens 447 Tests, 0 Fehler. Eine niedrigere Zahl oder ein roter Lauf ist ein Rueckschritt gegenueber der Ausgangsmessung.pnpm --filter @tessera/web type-check— Exit 0 (Ausgangslage: Exit 0).pnpm lint— 5 von 5 Workspaces erfolgreich. Bestehende Warnungen bleiben zulaessig, jede neue Meldung im Fehlerrang faerbt den Lauf rot und ist zu beheben.git status --porcelainzeigt ausschliesslich Pfade unterapps/web/src/— kein Pfad unterapps/api/.
Von Hand, ausdruecklich nicht automatisiert (optional, die Tests decken das Verhalten bereits ab; hier geht es allein um Platzierung und Lesbarkeit): in der laufenden Anwendung als ADMIN die Benutzerverwaltung oeffnen. Sichtprobe a — in der Zeile des SUPER_ADMIN stehen nur noch "Details". Sichtprobe b — eine abgewiesene Aktion (etwa ueber die Entwicklerwerkzeuge erzwungen) zeigt das rote Banner an der erwarteten Stelle, im Formular oberhalb der Knopfreihe und im Loeschdialog oberhalb der Knopfreihe.
<success_criteria>
- Keiner der drei bisher stillen Wege in
page.tsxendet noch ohne sichtbare Rueckmeldung. - Bei vorhandenem Servertext steht dieser im Rahmensatz; sonst greift die passende uebersetzte Ersatzmeldung.
- Ein ADMIN bekommt an der SUPER_ADMIN-Zeile keine Aktionsknoepfe angeboten; ein SUPER_ADMIN schon.
- Alle Texte liegen in beiden Katalogen mit gleichem Schluesselsatz; der Umlaut-Waechter ist gruen.
- 66 Testdateien gruen,
type-checkExit 0,pnpm lint5 von 5 erfolgreich. apps/apiist unveraendert; die 403-Antworten sind exakt dieselben wie vorher. </success_criteria>
In der Zusammenfassung festhalten:
- gemessene Testzahlen vorher (65 Dateien / 447 Tests) und nachher,
- dass
fetchUsersbewusst mitbehandelt wurde (dritte Fundstelle, in scope), - dass die Servertexte englisch bleiben und eine Uebersetzung serverseitig waere (Beobachtung fuer die Fehlerliste, nicht Teil dieses Laufs — siehe T-A1D-04 und den Hinweis in der Ausgangslage),
- dass WINDOWS #36 als geschlossen zu markieren ist, waehrend #28 und #32 derselben Familie offen bleiben.