Skip to content

feat(web): EYT-113 Admin-/Mitarbeiter-Shell-Grenze mit serverseitigem Feld-Gate - #97

Merged
DYAI2025 merged 8 commits into
masterfrom
feat/eyt-113-shell-grenze
Aug 28, 2026
Merged

feat(web): EYT-113 Admin-/Mitarbeiter-Shell-Grenze mit serverseitigem Feld-Gate#97
DYAI2025 merged 8 commits into
masterfrom
feat/eyt-113-shell-grenze

Conversation

@DYAI2025

@DYAI2025 DYAI2025 commented Aug 28, 2026

Copy link
Copy Markdown
Owner

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

  • Route Group (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).
  • /feld mit Server-Gate: Abgemeldete → /anmelden; Nichtwissen → ehrliche Fehlerfläche statt Shell. Die Feld-Shell nutzt dasselbe domänenfreie AppShell-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. BottomNavigation bewusst nicht gebaut (kein realer Konsument in diesem Inkrement).
  • Start-Dispatch: / leitet reine member serverseitig nach /feld; der Login leitet anhand des frisch vom Server gelieferten SessionDto (Leitungsrollen wie bisher nach /kosten).
  • Import-Grenze feld-shell-boundary als 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 in app/feld/page.tsx
architecture.test.ts rot mit apps/web/app/feld/page.tsx:4 [feld-shell-boundary] … → Mutation vollständig zurückgenommen (git diff auf die Datei: 0 Zeilen) → Suite wieder 47/47 grün.

Lokale Evidenz (Ausführung, kein Replay)

Gate Ergebnis
turbo run lint --force Tasks: 6 successful · Cached: 0 cached
turbo run typecheck --force Tasks: 10 successful · Cached: 0 cached
turbo run test --force Tasks: 10 successful · Cached: 0 cached
env -u EASYTREE_API_PROXY_TARGET turbo run build --force Tasks: 6 successful · Cached: 0 cached
Prettier (tracked set) exit 0
auth-journey (lokaler Supabase-Stack, echte API dist/main.js) 4 passed — A-Regression, B-ausgesperrt, EYT-113-Feld-Reise, Unauthenticated-Redirect; Teardown restzeilen=0
web-smoke (playwright test, ohne API) 28 passed

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 /planung Forbidden, GET /planung/fenster und GET /kosten/mitarbeiter antworten 403 ohne Datenpreisgabe.

Bewusst NICHT in diesem Inkrement (bleibt an EYT-113/EYT-81 offen)

  • Heute/Woche/Melden/Zeiten/Ich-Funktionalität — der Employee-Read getMySchedule (GET /einsatz/plan) existiert nur als Vertrag (NOT_YET_IMPLEMENTED), Implementierung ist EYT-81-Facharbeit.
  • Kosten-Navigation „nur nach verifiziertem costs.read laden" (Bundle-Splitting), Zustandsverträge Stale/Offline-read-only, 1440/1920-Nachweise für neue Flächen, Dokumentation der Grenzen in Confluence.
  • Keine DB-Migration, keine Auth-Architekturänderung, keine neuen Env-Variablen, keine Domainregel-Änderung.

Verhaltensänderung, dokumentiert: Reisender B (ohne Mitgliedschaft) landet nach Login jetzt in /feld (vorher /kosten mit 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:

  • Introduce a server-verified mobile field shell for authenticated users without leadership roles, including organization and role information, honest empty states, and logout.

Bug Fixes:

  • Prevent unauthenticated users from accessing the field shell and preserve an explicit error state when session verification is unavailable.

Enhancements:

  • Scope the existing workbench shell to its route group and dispatch authenticated users between field and workbench experiences based on verified membership roles.
  • Enforce a field-shell import allowlist to prevent dependencies on workbench UI and unauthorized packages.

Tests:

  • Add unit, architecture, accessibility, smoke, and end-to-end coverage for session verification, shell dispatch, field behavior, import boundaries, and cross-shell authorization.

BenPerro and others added 7 commits August 28, 2026 14:31
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>
@sourcery-ai

sourcery-ai Bot commented Aug 28, 2026

Copy link
Copy Markdown

Reviewer's Guide

Das 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 dispatch

sequenceDiagram
    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
Loading

State diagram for the field session gate

stateDiagram-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
Loading

Flow diagram for member access boundaries

flowchart 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"]
Loading

File-Level Changes

Change Details Files
Die Web-App trennt Werkbank- und Feld-Shell über Next.js-Route-Groups und eigene Layouts, ohne bestehende Werkbank-URLs zu ändern.
  • Root-Layout auf Dokumentrahmen und Provider reduziert
  • Admin-Routen in (werkbank) verschoben und dort AppShell angewendet
  • Eigene mobile Feld-Shell mit separater Informationsarchitektur, Sessionbereich und Logout ergänzt
  • Feld-Startfläche zeigt nur Organisationen/Rollen und einen ehrlichen Leerzustand ohne nicht existente Fachnavigation
apps/web/app/layout.tsx
apps/web/app/(werkbank)/layout.tsx
apps/web/app/(werkbank)/page.tsx
apps/web/app/(werkbank)/anmelden/page.tsx
apps/web/app/(werkbank)/kosten/page.tsx
apps/web/app/(werkbank)/kosten/stundensaetze/page.tsx
apps/web/app/(werkbank)/planung/page.tsx
apps/web/app/feld/layout.tsx
apps/web/app/feld/page.tsx
apps/web/components/feld/feld-shell.tsx
apps/web/components/feld/feld-start.tsx
apps/web/components/start-inhalt.tsx
apps/web/app/globals.css
Die Shell-Auswahl und der Schutz von /feld basieren auf einer serverseitig verifizierten API-Session statt auf Route oder Client-Zustand.
  • GET /auth/session mit Request-Cookies gegen das Laufzeit-Proxyziel implementiert
  • Sessionantwort strikt validiert und in angemeldet/abgemeldet/unbekannt mit fail-closed-Verhalten überführt
  • Reine member sowie Nutzer ohne Leitungsrolle serverseitig nach /feld geleitet; owner/manager bleiben in der Werkbank
  • Abgemeldete zu /anmelden umgeleitet, unbekannte Sitzungen erhalten eine Fehlerfläche
  • Login-Weiterleitung verwendet das frisch gelieferte Session-DTO
apps/web/lib/feld/sitzung-server.ts
apps/web/lib/feld/start-shell.ts
apps/web/app/(werkbank)/page.tsx
apps/web/app/feld/layout.tsx
apps/web/components/login-form.tsx
Eine Architekturregel verhindert, dass die Feld-Shell Werkbank-Oberflächen oder nicht freigegebene Pakete importiert.
  • Allowlist für Feld-Pakete und auflösbare relative Importe eingeführt
  • Importfreie beziehungsweise nicht erfasste Feld-Dateien durch Scope-Abgleich abgesichert
  • Synthetischer Rot-Fall prüft sowohl Werkbank-Importe als auch fremde Pakete
apps/api/test/architecture/rules.ts
apps/api/test/architecture.test.ts
apps/api/test/architecture-red-case.test.ts
Die neue Grenze und das Feld-Verhalten werden durch Unit-, Komponenten-, Architektur- und End-to-End-Tests abgesichert.
  • Server-Sitzungslesung und Shell-Ableitung mit Grenzfällen getestet
  • Feld-Shell auf Sessiondaten, Logout, Accessibility und fehlende Werkbank-Links geprüft
  • Auth-Journey für member, fehlende Mitgliedschaft, Server-Gate und Cross-Shell-403 ergänzt
  • Mobile Abnahme bei 320/375 px sowie API-Ausfall ohne falsche Umleitung getestet
  • Bestehende Werkbank-Tests auf die Route-Group-Pfade angepasst
apps/web/test/sitzung-server.test.ts
apps/web/test/start-shell.test.ts
apps/web/test/feld-shell.test.tsx
apps/web/test/a11y.test.tsx
apps/web/test/planung-page.test.tsx
apps/web/test/helpers/werkbank.tsx
apps/web/e2e/auth-journey/journey.pwtest.ts
apps/web/e2e/shell-smoke.spec.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread apps/api/test/architecture.test.ts
…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>
@DYAI2025
DYAI2025 merged commit 38b3275 into master Aug 28, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants