feat(web): EYT-113 Admin-/Mitarbeiter-Shell-Grenze mit serverseitigem Feld-Gate - #97
Conversation
Das Wurzel-Layout rendert nur noch Dokumentrahmen und Provider; die bestehende Admin-/Werkbank-Shell wandert in das Layout der Route Group (werkbank). URLs bleiben unverändert, die Shell-Grenze für die kommende Feld-Shell (EYT-113) ist damit gezogen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tung leseServerSitzung fragt die echte API (GET /auth/session) mit den Cookies der eingehenden Anfrage ueber das Laufzeit-Proxyziel (aktuellesProxyziel, die eine erlaubte Umgebungslesestelle) und unterscheidet fail-closed angemeldet/abgemeldet/unbekannt — Nichtwissen ist nicht abgemeldet. startShellFuer leitet die zulaessige Start-Shell rein aus den verifizierten Mitgliedschaftsrollen ab: ohne Leitungsrolle Feld, sonst Werkbank. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Neuer Teilbaum /feld: das Server-Layout prueft die Session gegen die echte API (Cookies der eingehenden Anfrage), leitet Abgemeldete zur Anmeldung und zeigt bei Nichtwissen eine ehrliche Fehlerflaeche statt der Shell. Die Feld-Shell nutzt dasselbe domaenenfreie AppShell-Geruest und die Basisdesign-v2-Tokens, traegt aber eine eigene Informationsarchitektur: keine Werkbank-Navigation, nur reale Sessiondaten (Organisation, Rolle) und echtes Abmelden. Nicht existierende Feld-Funktionen erscheinen nicht als Attrappen; der Leerzustand benennt die Abwesenheit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Die Startseite entscheidet serverseitig aus der real verifizierten Session: reine member-Konten werden nach /feld geleitet, Leitungsrollen sehen die Werkbank unveraendert. Der Login leitet anhand des frisch vom Server gelieferten SessionDto in dieselbe Richtung — Leitungsrollen wie bisher nach /kosten, member in die Feld-App, deren Server-Gate die Session erneut prueft. Der Startseiten-Inhalt wandert in eine synchrone Komponente, damit die a11y-Suite ihn weiter in jsdom rendern kann. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Die Feld-Shell (app/feld, components/feld, lib/feld) darf react, next, @easytree/ui und @easytree/contracts importieren sowie relativ in die Feld-Verzeichnisse und die geteilte lib/ zeigen — Werkbank-Komponenten und fremde Pakete sind Verstoesse (Allowlist, dieselbe Lehre wie domain-allowlist). Dazu die Scope-Zusicherung 'ueberwacht JEDE Datei der Feld-Shell' und ein Rot-Fall im synthetischen Baum. Gegenmutation im echten Baum ausgefuehrt: ein KostenAnsicht-Import in app/feld/page.tsx macht architecture.test.ts rot mit Regelnennung; danach vollstaendig zurueckgenommen (git diff auf die Datei leer). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d-Smoke auth-journey: ein realer member (Leihgabe wie 9c5/9g2) landet nach dem echten Login in der Feld-Shell, sieht Organisation und Rolle aus der Session, keine Werkbank-Navigation; die Startseite leitet ihn serverseitig ins Feld; /planung zeigt Forbidden und die API lehnt Planungs- wie Kosten-Reads mit 403 ab. Abnahme der Feld-Shell auf 320/375 px (axe, Reflow, Tastatur, sichtbarer Fokus, Touchziel >= 40 px). Ohne Sitzung fuehrt /feld zur Anmeldung. Reisender B ohne Mitgliedschaft landet jetzt ebenfalls im Feld und sieht den ehrlichen Leerzustand. shell-smoke: /feld ohne API zeigt die Fehlerflaeche des Server-Gates statt Shell und leitet NICHT zur Anmeldung um — Nichtwissen ist nicht abgemeldet. Lokal gemessen: auth-journey 4 passed, web-smoke 28 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reviewer's GuideDas Inkrement führt eine echte Admin-/Mitarbeiter-Shell-Grenze ein: Werkbank-Routen behalten ihre URLs und Shell, während authentifizierte Nutzer ohne Leitungsrolle serverseitig in eine eigenständige, mobile Feld-Shell gelangen. Die Session wird über die API und Request-Cookies verifiziert, Nichtwissen bleibt von Abmeldung unterschieden, eine Import-Allowlist schützt die Bundle-Grenze, und umfassende Tests belegen Routing, Zugriffsschutz, Accessibility sowie das Verhalten bei API-Ausfällen. Sequence diagram for server-verified shell dispatchsequenceDiagram
participant Browser
participant NextServer
participant SessionAPI
participant FeldShell
participant Werkbank
Browser->>NextServer: GET /
NextServer->>SessionAPI: GET /auth/session with request cookies
SessionAPI-->>NextServer: SessionDto or 401
alt authenticated without owner or manager role
NextServer-->>Browser: redirect /feld
Browser->>NextServer: GET /feld
NextServer->>SessionAPI: GET /auth/session with request cookies
SessionAPI-->>NextServer: SessionDto
NextServer-->>FeldShell: render FeldShell
FeldShell-->>Browser: organisation, role, logout
else authenticated with owner or manager role
NextServer-->>Werkbank: render existing workbench
Werkbank-->>Browser: workbench shell
else 401
NextServer-->>Browser: render public start page
else API unavailable or invalid response
NextServer-->>Browser: render start page without redirect
end
State diagram for the field session gatestateDiagram-v2
[*] --> SessionCheck
SessionCheck --> Unauthenticated: API returns 401
SessionCheck --> Authenticated: 200 + valid SessionDto
SessionCheck --> Unknown: API failure, non-200, invalid body, or proxy error
Unauthenticated --> Login: redirect /anmelden
Authenticated --> FeldShell: no owner or manager role
Authenticated --> Workbench: owner or manager role
Unknown --> ErrorSurface: honest session-not-checkable error
ErrorSurface --> SessionCheck: retry or later request
Flow diagram for member access boundariesflowchart TD
Login["User signs in"] --> SessionDto["Fresh server-provided SessionDto"]
SessionDto --> RoleCheck{"owner or manager role?"}
RoleCheck -->|yes| Kosten["/kosten\nWorkbench shell"]
RoleCheck -->|no| Feld["/feld\nField shell"]
Feld --> FieldContent["Organisation and role\nHonest empty state"]
Feld --> Logout["Real logout"]
Feld --> Planning["/planung"]
Planning --> Forbidden["403 Forbidden\nNo protected data"]
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 1 issue
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="apps/api/test/architecture.test.ts" line_range="135-139" />
<code_context>
);
});
+ it("Regel feld-shell-boundary ueberwacht JEDE Datei der Feld-Shell", () => {
+ // Dieselbe Lehre wie bei ui-dependency-allowlist: `>= 1` unterscheidet
+ // nicht zwischen "bewacht die Shell" und "bewacht eine Datei". Die linke
+ // Seite kommt aus den Praefixen ueber die eingesammelten Importe, die
+ // rechte aus `inScope` der Regel — beide Wege muessen dieselbe Menge
+ // ergeben. Importfreie Dateien tauchen auf keiner Seite auf.
+ const feldPraefixe = ["apps/web/app/feld/", "apps/web/components/feld/", "apps/web/lib/feld/"];
+ const ausDemPraefix = new Set(
+ refs
+ .filter((ref) => feldPraefixe.some((praefix) => ref.from.startsWith(praefix)))
+ .map((ref) => ref.from),
+ );
+ expect(ausDemPraefix.size).toBeGreaterThan(3);
+ expect([...(scopeCounts.get("feld-shell-boundary") ?? [])].sort()).toEqual(
</code_context>
<issue_to_address>
**issue (testing):** Die Scope-Zusicherung überwacht keine importfreien Dateien der Feld-Shell. `ausDemPraefix` wird ausschließlich aus den bereits eingesammelten Importreferenzen gebildet, und `scopeCounts` wird mit genau derselben Referenzmenge befüllt; der Vergleich ist daher tautologisch und bleibt grün, wenn eine neue Datei unter `app/feld/`, `components/feld/` oder `lib/feld/` keinerlei Imports enthält und von der Regel überhaupt nicht geprüft wird.
**Triggers:** Wenn eine neue importfreie Datei in einem Feld-Shell-Verzeichnis angelegt wird.
**Suggested fix:** Ermittle die Dateien der drei Feld-Shell-Verzeichnisse unabhängig von den Importreferenzen und vergleiche diese Menge mit den von `inScope` erfassten Dateien; alternativ muss der Scanner auch importfreie Dateien an die Regelpipeline übergeben.
```suggestion
const ausDemPraefix = new Set(
files.filter((file) => feldPraefixe.some((praefix) => file.startsWith(praefix))),
);
```
</issue_to_address>Sourcery assessment
Needs a human reviewer. 1 finding to address first, and the server-side session gate and role-based shell dispatch change authentication-dependent routing and the boundary between the Feld and Werkbank experiences. A mistaken session classification or role policy could expose the wrong authenticated surface or deny the intended one; reverting removes the behavior, but any incorrect access or disclosure occurring before the revert would already have happened.
Blocking findings: apps/api/test/architecture.test.ts:139
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
…haerten Sourcery-Befund an PR #97: der refs-basierte Mengenvergleich ist blind fuer Dateien, die der Scanner nie einsammelt — sie fehlen auf beiden Seiten. Neu: ein unabhaengiges Verzeichnislisting der drei Feld-Verzeichnisse muss exakt der Collector-Menge entsprechen. Gegenmutation ausgefuehrt: eine Quelldatei unter app/feld/dist/ (SKIP_DIRS) macht den Fall rot (6 eingesammelt vs. 7 auf der Platte); danach entfernt, Suiten wieder gruen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
EYT-113 Inkrement 1 — echte Admin-/Mitarbeiter-Shell-Grenze
Kleinster vertikaler Durchstich der Zwei-Client-Architektur: ein real authentifizierter Mitarbeiter (
member) wird aufgrund seiner serverseitig verifizierten Session in eine eigenständige Mobile-Feld-Shell (/feld) geführt; Admins behalten die bestehende Werkbank. Die Route verleiht kein Recht. Grundlage für EYT-81 (Heute/Woche).Was dieses Inkrement liefert
(werkbank): die bestehende Admin-Shell gilt nur noch für Werkbank-Routen; URLs unverändert. Das Wurzel-Layout rendert nur Dokumentrahmen + Provider.lib/feld/sitzung-server.ts: Server Components fragen die echte API (GET /auth/session) mit den Cookies der eingehenden Anfrage über das Laufzeit-Proxyziel (aktuellesProxyziel(), die eine erlaubte Umgebungslesestelle). Fail-closed:angemeldet/abgemeldet/unbekannt— Nichtwissen ist nicht abgemeldet.lib/feld/start-shell.ts: reine Ableitung der Start-Shell aus den Mitgliedschaftsrollen (keine Leitungsrolle → Feld)./feldmit Server-Gate: Abgemeldete →/anmelden; Nichtwissen → ehrliche Fehlerfläche statt Shell. Die Feld-Shell nutzt dasselbe domänenfreieAppShell-Primitive und die Basisdesign-v2-Tokens, trägt aber eine eigene Informationsarchitektur: keine Werkbank-Navigation, nur reale Sessiondaten (Organisation, Rolle) und echtes Abmelden. Keine Fake-Funktionen: nicht existierende Feld-Ansichten erscheinen nirgends als Buttons; der Leerzustand benennt die Abwesenheit.BottomNavigationbewusst nicht gebaut (kein realer Konsument in diesem Inkrement)./leitet reine member serverseitig nach/feld; der Login leitet anhand des frisch vom Server geliefertenSessionDto(Leitungsrollen wie bisher nach/kosten).feld-shell-boundaryals neunte Regel der Architektur-Suite (Allowlist: react, next, @easytree/ui, @easytree/contracts, relative Pfade in Feld-Verzeichnisse +lib/), mit Scope-Zusicherung („überwacht JEDE Datei der Feld-Shell") und Rot-Fall im synthetischen Baum.Ausgeführte Gegenmutation (Guard-Beweis)
import { KostenAnsicht } from "../../components/kosten-ansicht"temporär inapp/feld/page.tsx→architecture.test.tsrot mitapps/web/app/feld/page.tsx:4 [feld-shell-boundary] …→ Mutation vollständig zurückgenommen (git diffauf die Datei: 0 Zeilen) → Suite wieder 47/47 grün.Lokale Evidenz (Ausführung, kein Replay)
turbo run lint --forceTasks: 6 successful·Cached: 0 cachedturbo run typecheck --forceTasks: 10 successful·Cached: 0 cachedturbo run test --forceTasks: 10 successful·Cached: 0 cachedenv -u EASYTREE_API_PROXY_TARGET turbo run build --forceTasks: 6 successful·Cached: 0 cacheddist/main.js)restzeilen=0playwright test, ohne API)Die Feld-Reise misst auf 320/375 px: axe (wcag2a/aa, 21a/aa, best-practice) = 0 Verstöße, Reflow ≤ 1 px, Tastatur ohne Falle, sichtbarer Fokus, Touchziel „Abmelden" ≥ 40 px (Basisdesign v2.0 §2.3). Cross-Shell: member erhält auf
/planungForbidden,GET /planung/fensterundGET /kosten/mitarbeiterantworten 403 ohne Datenpreisgabe.Bewusst NICHT in diesem Inkrement (bleibt an EYT-113/EYT-81 offen)
Heute/Woche/Melden/Zeiten/Ich-Funktionalität — der Employee-ReadgetMySchedule(GET /einsatz/plan) existiert nur als Vertrag (NOT_YET_IMPLEMENTED), Implementierung ist EYT-81-Facharbeit.Verhaltensänderung, dokumentiert: Reisender B (ohne Mitgliedschaft) landet nach Login jetzt in
/feld(vorher/kostenmit Forbidden-Fläche) — die Journey-Zusicherung wurde entsprechend nachgeführt und um den ehrlichen Leerzustand ergänzt.Kein Merge durch den Autor — Review erbeten.
🤖 Generated with Claude Code
Summary by Sourcery
Separate the admin workbench from the mobile field experience and route users through the appropriate shell based on their server-verified session.
New Features:
Bug Fixes:
Enhancements:
Tests: