29 KiB
phase, slug, status, shadcn_initialized, preset, created
| phase | slug | status | shadcn_initialized | preset | created |
|---|---|---|---|---|---|
| 15 | modul-berechtigungen-gruppen-user-grants | draft | false | none | 2026-08-04 |
Phase 15 — UI Design Contract
Visual and interaction contract for frontend phases. Generated by gsd-ui-researcher, verified by gsd-ui-checker.
Design System
| Property | Value |
|---|---|
| Tool | none — kein components.json, keine shadcn-CLI im Repo (verifiziert: find liefert keinen Treffer) |
| Preset | not applicable |
| Component library | none — keine Radix-/Base-UI-Abhängigkeit im Projekt (grep radix|lucide|cva|clsx in apps/web/package.json liefert nichts). Alle fünf bestehenden Admin-Seiten (ldap, users, modules, tenants, smtp) sind handgebaute Tailwind-Komponenten mit einem konsistenten, projekteigenen Vokabular (Tabellen, Modal-Dialoge, Toggle-Switches, Badge-Chips) |
| Icon library | none — Icons sind handgeschriebene Inline-<svg> im Lucide-Stil (viewBox="0 0 24 24", stroke="currentColor", strokeWidth="2", strokeLinecap="round", strokeLinejoin="round", kein fill), siehe apps/web/src/components/admin/admin-sidebar.tsx |
| Font | Inter via --font-sans (apps/web/src/app/globals.css:33), system-ui-Fallback |
Shadcn-Gate-Entscheidung: Kein components.json gefunden, Stack ist Next.js — der Gate würde normalerweise fragen, ob shadcn initialisiert werden soll. Diese Frage ist hier bereits beantwortet: der <hard_constraints>-Auftrag verlangt explizit "Reuse existing components and tokens; do not introduce a new component library or a second styling approach", und 14 vorherige Phasen haben ein durchgängiges, funktionierendes handgebautes System etabliert (5 Admin-Seiten, Marketplace, Modul-Shells). shadcn jetzt einzuführen würde einen zweiten, inkonsistenten Styling-Ansatz neben den bestehenden ~40 Seiten schaffen. Tool bleibt none, Registry-Safety-Gate ist damit nicht anwendbar.
Spacing Scale
Declared values (must be multiples of 4), identisch mit dem bereits im Projekt etablierten Raster (verifiziert in allen fünf Admin-Seiten):
| Token | Value | Usage |
|---|---|---|
| xs | 4px | Icon-Text-Abstand in Badges/Chips (gap-1), Checkbox-zu-Label-Feinabstand |
| sm | 8px | Kompakte Element-Abstände (gap-2), Badge-Innenpolsterung (px-2 py-0.5) |
| md | 16px | Standard-Formularabstand (space-y-4), Tabellenzellen-Polsterung (px-4 py-3) |
| lg | 24px | Card-/Section-Innenpolsterung (p-6), Abstand zwischen Seitenabschnitten (space-y-6/space-y-8) |
| xl | 32px | reserviert, in dieser Phase nicht benötigt |
| 2xl | 48px | reserviert, in dieser Phase nicht benötigt |
| 3xl | 64px | reserviert, in dieser Phase nicht benötigt |
Exceptions:
- Icon-only Aktionsbuttons in Tabellenzeilen (Standardgruppen-Stern-Toggle, Mitglied-Entfernen-Button) übernehmen das bestehende Projektmaß
px-2 py-1(kein 44px-Touch-Target) — Admin-Oberflächen sind Desktop-Maus-Bedienung, identisch zu den bereits ausgelieferten "Bearbeiten"/"Löschen"-Buttons inadmin/users/page.tsx. Kein neuer Präzedenzfall, reine Fortführung. - Matrix-Checkboxen (Permission-Matrix-Seite) nutzen die native
<input type="checkbox">-Größe ohne zusätzliches Padding, identisch zum bestehendengroupFilterDns-Checkbox-Muster inadmin/ldap/page.tsx.
Typography
Übernommene, bereits im Projekt etablierte Werte (nicht neu erfunden — verifiziert aus allen fünf Admin-Seiten). Vier Größen und vier Gewichte sind bereits Teil des bestehenden Systems; diese Phase führt keine neue Größe und kein neues Gewicht ein, sondern verwendet ausschließlich die vorhandenen vier Kombinationen:
| Role | Size | Weight | Line Height |
|---|---|---|---|
| Label / Badge | 12px (text-xs) |
500 medium | 1.4 |
| Body / Table / Form | 14px (text-sm) |
400 regular (Fließtext, Tabellenzellen) | 1.5 |
| Section Heading | 18px (text-lg) |
600 semibold | 1.3 |
| Page Heading | 24px (text-2xl) |
700 bold | 1.2 |
OWNER-APPROVED EXCEPTION — genehmigt am 2026-08-04 vom Projekt-Owner.
Die vier Gewichte 400 (Body) / 500 (Label, Badge) / 600 (Section Heading) / 700 (Page Heading) überschreiten das UI-SPEC-Standardlimit von 2 Gewichten. Diese Überschreitung ist explizit vom Projekt-Owner genehmigt, nicht nur durch Prosa-Argumentation begründet:
- Scope der Ausnahme: genau diese vier Werte — 400/500/600/700 — sind vorbestehendes Projektvokabular, etabliert in Phase 2 und seither auf allen ausgelieferten Oberflächen (5 Admin-Seiten, Marketplace, Dashboard, alle Modul-Shells) im Einsatz. Phase 15 führt kein fünftes Gewicht ein.
- Begründung: Phase 15 konsumiert ausschließlich die bestehende Skala, ohne sie zu erweitern. Eine Konsolidierung auf 2 Gewichte würde jede bereits ausgelieferte Seite umgestalten — projektweite Bestandsaufnahme zählt 192
font-medium-, 56font-semibold- und 19font-bold-Vorkommen über 70 Dateien inapps/web/src. Das ist ein Portal-weites Redesign und liegt außerhalb des Scopes einer Berechtigungs-Phase. - Keine Performance-Kosten:
apps/web/src/app/globals.css:33deklariert--font-sans: "Inter", system-ui, …ohnenext/fontund ohne@font-face— es werden keine Font-Dateien nachgeladen, die Gewichtsanzahl verursacht keine Ladekosten. - Bindende Einschränkung für Phase 15: die neuen Oberflächen dieser Phase dürfen kein Gewicht jenseits dieser vier einführen. Formular-Labels, Tabellen-Kopfzeilen und Buttons nutzen
font-medium(500) auf 14px-Basis (z. B.<label className="text-sm font-medium text-foreground">) — dieselbe 500er-Stufe wie bei Labels/Badges, nur auf 14px statt 12px angewendet, kein neuer Wert. Planner/Executor übernehmen ausschließlich die vier genehmigten Größe-Gewicht-Paare, erfinden keine neue Kombination.
Color
| Role | Value | Usage |
|---|---|---|
| Dominant (60%) | --background / --card (OKLCH, hell 0.99 0 0 / 1 0 0, dunkel 0.23 0.01 260 / 0.27 0.01 260) |
Seitenhintergrund, Card-/Section-Flächen |
| Secondary (30%) | --secondary / --muted / --sidebar (OKLCH 0.96 0 0 hell, 0.30 0.01 260 dunkel) |
Tabellenkopf (bg-muted/50), Admin-Sidebar, sekundäre Buttons (border border-border) |
| Accent (10%) | --primary (OKLCH 0.91 0.19 102, gelb) |
ausschließlich: primäre CTA-Buttons ("Gruppe erstellen", "Sofort freigeben", "Speichern"), aktive Toggle-Switch-Füllung (Modul-Aktivierung, Matrix-Häkchen-Fokusring), aktiver Zustand im Admin-Sidebar-Nav-Item (sidebar-accent-foreground), Fokus-Ring (ring-ring) |
| Destructive | --destructive (OKLCH 0.55 0.2 27, rot) |
ausschließlich: "Löschen"/"Entfernen"/"Freigabe entziehen"-Buttons, Bestätigungs-Button in Lösch-Dialogen, Fehlermeldungstext |
Accent reserved for: Primär-Buttons (Gruppe erstellen, Speichern, Sofort freigeben), aktiver Toggle-Zustand, aktives Sidebar-Nav-Item, Fokus-Ring. Niemals für Badges, Tabellenzeilen-Hover oder informative Hinweise — dafür gilt die bestehende informelle Badge-Palette unten.
Bestehende informelle Badge-Palette (kein Token, aber projektweit etabliertes Muster — jede Instanz trägt Light- und Dark-Variante):
| Badge-Zweck | Klassen | Bereits verwendet in |
|---|---|---|
| Positiv/Aktiv | bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400 |
Modul-Status "Aktiviert", User-Status "Aktiv" |
| Neutral/Inaktiv | bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-500 bzw. bg-muted text-muted-foreground |
User-Status "Inaktiv", Marketplace "Verfügbar" |
| Info/System | bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400 |
Rolle ADMIN, "System"-Modul-Badge |
| Kritisch (Rolle) | bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400 |
Rolle SUPER_ADMIN |
| NEU in dieser Phase — Eingeschränkt | bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400 |
Marketplace-Badge "Kein Zugriff" (D-08). Bewusst nicht destructive (rot) — es ist kein Fehlerzustand, sondern ein Informationszustand, konsistent mit der bestehenden Palette-Familie um eine fünfte, bisher ungenutzte Farbe erweitert (amber ist in Tailwinds Standardpalette bereits verfügbar, kein neues Token nötig) |
Copywriting Contract
| Element | Copy |
|---|---|
| Primary CTA — Gruppenverwaltung | "Gruppe erstellen" (admin.groups.create) |
| Primary CTA — Matrix-Zugriff | "Freigaben-Matrix" (Link/Button auf /admin/modules, adminModules.grantsLink) |
| Primary CTA — Modul-Aktivierung mit Grant | "Sofort freigeben" (adminModules.activationDialog.grantNow) |
| Sekundär-CTA — Modul-Aktivierung ohne Grant | "Später konfigurieren" (adminModules.activationDialog.configureLater) |
| Empty state heading — Gruppenliste | "Keine Gruppen vorhanden" (admin.groups.noGroups) |
| Empty state body — Gruppenliste | "Lege deine erste Gruppe an, um Modulzugriff gezielt zu vergeben." (admin.groups.noGroupsBody) |
| Empty state — Matrix ohne aktive Module | "Es sind noch keine Module für diesen Mandanten aktiviert. Aktiviere zuerst ein Modul unter Module." (adminModules.grants.emptyModules, mit Link zu /admin/modules) |
| Empty state — User-Detail ohne Gruppenmitgliedschaft | "Dieser Benutzer ist keiner Gruppe zugeordnet." (admin.users.grants.noGroups) |
| Error state — Grant-Speicherfehler | "Freigabe konnte nicht gespeichert werden. Bitte erneut versuchen." + Statuscode-Anzeige wie im bestehenden adminModules-Fehlermuster (error-Div mit border-destructive/50 bg-destructive/10) |
| 403-Seite Titel (D-07) | "Kein Zugriff auf dieses Modul" (modules.accessDenied.title) |
| 403-Seite Body (D-07) | "Du hast für dieses Modul keine Freigabe. Wende dich an deinen Administrator." (modules.accessDenied.body) — Formulierung ist durch D-07 wörtlich vorgegeben, keine Umformulierung |
| 403-Seite Rücklink | "Zur Startseite" → / (Dashboard), nicht "Zurück zur Kategorie" — der Benutzer hat für dieses Modul keinen Zugriff, ein Rücksprung in die Modul-Kategorie ist kein sinnvoller nächster Schritt (modules.accessDenied.backToDashboard) |
| Marketplace-Badge "Kein Zugriff" (D-08) | "Kein Zugriff" (marketplace.statusNoAccess) |
| Marketplace-Klick-Feedback bei gesperrter Karte | Toast: "Kein Zugriff auf dieses Modul — wende dich an deinen Administrator." (identischer Wortlaut wie 403-Seite, marketplace.toastNoAccess) — nutzt die bestehende Toast.tsx-Komponente |
| Destructive confirmation — Gruppe löschen (D-17, PERM-01) | Titel: "Gruppe löschen"; Body: "Diese Gruppe hat {memberCount} Mitglieder und {grantCount} Modul-Freigaben. Betroffene Benutzer verlieren den Zugriff, sofern sie ihn nicht anderweitig haben. Diese Aktion kann nicht rückgängig gemacht werden." (admin.groups.deleteConfirm.title / .body, Zahlen als next-intl-Interpolationsparameter, keine generische Warnung ohne Zahlen) |
| Destructive confirmation — Freigabe entziehen (Matrix/User-Detail) | Kein Dialog nötig — Häkchen-Toggle ist sofort wirksam und sofort umkehrbar (kein Datenverlust, reine Klick-Umkehrung), analog zum bestehenden Modul-Aktivierungs-Toggle-Verhalten. Kein Bestätigungsdialog für einzelne Grant-Toggles. |
| Destructive confirmation — Mitglied aus Gruppe entfernen | Kein Dialog — sofortiger Klick-Entfernen-Button analog zum bestehenden userExcludeList-Chip-Entfernen-Muster (kein irreversibler Datenverlust, Mitglied kann sofort wieder hinzugefügt werden) |
| AD-Bindung — Bindungs-Auswahl-Hinweis | "Wähle eine AD-Gruppe aus der Liste, um die Mitgliedschaft automatisch zu synchronisieren." (admin.groups.ldapBind.hint) |
| AD-Bindung — bereits gebunden | "Gebunden an: {ldapDn}" mit "Bindung entfernen"-Link (admin.groups.ldapBind.bound / .unbind) |
| Standardgruppen-Markierung | Icon-Button-Tooltip/aria-label: "Als Standardgruppe festlegen" bzw. "Standardgruppe" (wenn bereits gesetzt) — admin.groups.setDefault / admin.groups.isDefault |
| Mitglied entfernen (Icon-only, Mitglieder-Modal) | Icon-Button-Tooltip/aria-label: "Mitglied entfernen" — admin.groups.members.remove. Gilt nur für MANUAL-Mitgliedschaften (Button ist deaktiviert bei LDAP-Herkunft, siehe Surface Contract 1); der deaktivierte Zustand trägt zusätzlich aria-label/Tooltip "Wird über AD-Sync verwaltet" — admin.groups.members.ldapManaged |
| Matrix-Häkchen ohne sichtbaren Text (Permission-Matrix) | aria-label pro Checkbox, dynamisch zusammengesetzt: "{Modulname} für Gruppe {Gruppenname} freigeben" bzw. "… entziehen" je nach Zustand — admin.groups.grants.matrixCheckboxLabel (Modul- und Gruppenname als next-intl-Interpolationsparameter, da die Zelle selbst keinen sichtbaren Text trägt — nur Zeilen-/Spaltenkopf) |
| User-Detail "Direkt"-Checkbox (User-Detail-Modul-Zugriff) | aria-label pro Checkbox: "{Modulname} direkt für {Benutzername} freigeben" bzw. "… entziehen" — admin.users.grants.directCheckboxLabel (gleiches Muster wie Matrix-Checkbox, da auch hier kein sichtbarer Zellentext existiert) |
Surface Contracts
1. /admin/groups — Gruppenverwaltung (PERM-01, D-14, D-17)
Sechster Eintrag im AdminSidebar (apps/web/src/components/admin/admin-sidebar.tsx), zwischen Module und LDAP oder ans Ende — Reihenfolge liegt bei Executor, Icon muss sich vom bestehenden admin.users-Personen-Icon unterscheiden (Vorschlag: ein Roster-/Listen-Icon, 24×24, stroke-width 2, kein Fill — kein bestehendes Icon im Projekt duplizieren).
Seitenaufbau identisch zu admin/users/page.tsx:
h1"Gruppen" (24px/700) + primärer Button "Gruppe erstellen" rechtsbündig im Header (identisches Markup wieopenCreate-Button inAdminUsersPage).- Tabelle (
rounded-md border border-border,thead bg-muted/50,tbody divide-y divide-border) mit Spalten: Name, AD-Bindung (Badge "AD-gebunden" info-blau wennldapDngesetzt, sonst Badge "Manuell" neutral-grau), Standardgruppe (Stern-Icon-Toggle-Button, gefüllt wennisDefault), Mitglieder (Zahl,{count}), Aktionen (Bearbeiten · Mitglieder · Löschen als drei Text-Buttons im bestehendenpx-2 py-1 text-xs-Stil). - Create/Rename-Modal: identisches Overlay-Muster (
fixed inset-0 bg-black/50→max-w-md rounded-lg border bg-card p-6 shadow-lg) wie das User-Create-Modal. Feld "Name" (Pflicht), darunter Abschnitt "AD-Bindung (optional)": Suchfeld + Ergebnisliste im 1:1-Muster der bestehendengroupFilter.discover-Sektion ausadmin/ldap/page.tsx(Zeilen 705–746), aber mit Radio-Auswahl statt Checkbox-Mehrfachauswahl (genau eine AD-Gruppe pro Tessera-Gruppe, D-05). - Mitglieder-Modal (separater Dialog, Button "Mitglieder" aus der Tabellenzeile): zwei Abschnitte — (a) aktuelle Mitglieder als Chip-Liste mit Herkunfts-Badge (
MANUAL/LDAP) und Entfernen-Button, identisches Chip-Markup wieuserExcludeListinadmin/ldap/page.tsx(Zeilen 957–974); LDAP-Mitgliedschaften zeigen keinen Entfernen-Button (D-19: nur der Sync entfernt sie) — stattdessen ein deaktivierter Zustand mit Tooltip "Wird über AD-Sync verwaltet". (b) Suchfeld + Checkbox-Liste bestehender Tenant-Benutzer zum manuellen Hinzufügen, identisches Markup wie dieuserSearch-Sektion (Zeilen 841–877), Ziel ist hier aberGroupMembership, nicht LDAP-Import. - Lösch-Dialog: identisches Overlay-Muster wie
deleteConfirminAdminUsersPage, aber mit den unter Copywriting Contract festgelegten konkreten Zahlen (D-17) statt der generischen Ein-Satz-Warnung.
2. Permission-Matrix — Module × Gruppen (PERM-03, D-15)
Erreichbar über einen Button "Freigaben-Matrix" im Header von /admin/modules (kein siebter Sidebar-Eintrag — bleibt Unterseite von Modules, analog zu dkv-fleet/settings). Route: /admin/modules/grants.
- Suchfeld oben (filtert Modul- und Gruppennamen client-seitig), identisches Eingabefeld-Markup wie
discoverSearch. - Tabelle mit
stickyerster Spalte (Modulname,sticky left-0 bg-card z-10) undstickyKopfzeile (Gruppennamen,sticky top-0 bg-card z-10) in einemoverflow-x-auto overflow-y-auto max-h-[70vh] rounded-md border border-border-Container — löst die Claude's-Discretion-Frage "Scrollverhalten bei vielen Modulen/Gruppen" aus RESEARCH.md. - Zeilen nach Modul-Kategorie gruppiert: volle-Breite-Zwischenzeile
bg-muted/50mit Kategorienamen (identisch zum bestehenden Kategorie-Badge-Text ausAdminModulesPage), darunter die Modulzeilen dieser Kategorie. - Jede Zelle: natives
<input type="checkbox">, sofortiges Toggle bei Klick (optimistisches UI, PATCH im Hintergrund — identisches Verhalten zum bestehenden Modul-Aktivierungs-Switch inAdminModulesPage.toggleModule), kleiner Inline-Spinner nur in der aktiven Zelle während des Requests (kein Vollseiten-Ladezustand). - Fußnote unter der Tabelle (12px,
text-muted-foreground): "ADMIN und SUPER_ADMIN haben immer Zugriff auf alle aktiven Module — diese Matrix betrifft nur die Rolle USER." (D-03 transparent machen, verhindert Verwirrung, warum Admin-Accounts hier nicht auftauchen). - Leerer Zustand (kein aktives Modul im Mandanten): siehe Copywriting Contract "Empty state — Matrix ohne aktive Module".
3. User-Detail-Grants in /admin/users (PERM-03, D-16)
AdminUsersPage bekommt pro Zeile einen vierten Aktions-Button "Details" (neben Bearbeiten/Löschen, gleicher px-2 py-1 text-xs-Stil), der ein breiteres Modal öffnet (max-w-2xl statt max-w-md — mehr Inhalt als das einfache Edit-Formular, aber weiterhin das etablierte Overlay-Muster, kein neues Slide-over-Primitiv).
Modal-Inhalt, Titel "{username} — Zugriff":
- Abschnitt "Gruppenmitgliedschaften" — read-only Chip-Liste (identisches Chip-Markup wie
userExcludeList, aber ohne Entfernen-Button — Bearbeitung geschieht ausschließlich unter/admin/groups, keine zweite CRUD-Oberfläche für dieselbe Beziehung). Leerzustand siehe Copywriting Contract. - Abschnitt "Modul-Zugriff" — Tabelle mit Spalten: Modul | Über Gruppe(n) (Chip-Liste der Gruppennamen, die dieses Modul gewähren, oder "–") | Direkt (Checkbox-Toggle für einen direkten
ModuleGrant, identisches Sofort-Toggle-Verhalten wie die Matrix). Nur mandantenweit aktive Module werden gelistet — inaktive Module sind hier nicht wählbar, da ein Grant ohne Aktivierung wirkungslos ist (D-02).
4. 403-Seite für gesperrtes Modul (PERM-04, D-07)
apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx wird zur async Server Component (Umbau lt. RESEARCH.md Pattern 5); der bisherige Client-Inhalt wandert unverändert in eine neue module-shell.tsx-Client-Component.
Visuelles Muster identisch zum bestehenden "not found"-Zustand derselben Datei (Zeilen 30–65: zentrierter Block, rounded-lg bg-muted p-4-Icon-Container, h2 18px/600, p 14px muted, Link darunter) — nur Icon und Copy ändern sich:
- Icon: Schloss-Symbol (
lock, nicht das bestehende Kreis-mit-Schrägstrich-"nicht gefunden"-Icon — visuelle Unterscheidung zwischen "existiert nicht" und "gesperrt" ist bewusst, verhindert Verwechslung der beiden Zustände). - Titel/Body: siehe Copywriting Contract, wörtlich D-07.
- Kein
notFound(), kein Redirect (D-07 explizit) — die Seite rendert das 403-Markup direkt als Server-Response. - Rücklink führt zur Startseite (
/), nicht zur Modul-Kategorie (siehe Copywriting Contract Begründung).
5. Marketplace-Katalog — "Kein Zugriff"-Badge (PERM-04, D-08)
MarketplaceCard.tsx bekommt einen dritten Status neben isActive/!isActive: hasGrant (nur relevant, wenn isActive === true und Rolle USER). Wenn isActive && !hasGrant:
- Zusätzliches Badge "Kein Zugriff" (amber, siehe Color-Tabelle) direkt neben dem bestehenden Status-Badge in derselben Badge-Reihe (
flex items-center gap-2). - Karte bekommt
opacity-60 cursor-not-allowedauf dem äußerendiv(keinhover:shadow-md/hover:border-primary/30in diesem Zustand — visuelles Feedback, dass die Karte nicht interaktiv ist). - Klick auf die Karte navigiert nicht zum Moduldetail, sondern löst den Toast "Kein Zugriff …" aus (bestehende
Toast.tsx-Komponente aus dem Marketplace-Ordner wiederverwenden, kein neues Toast-System). - ADMIN/SUPER_ADMIN sehen dieses Badge nie (Bypass, D-03) — für sie ist jede aktivierte Karte normal klickbar.
6. Modul-Aktivierungs-Dialog in /admin/modules (PERM-03, D-10)
AdminModulesPage.toggleModule verzweigt: Deaktivierung bleibt unverändert (bestehendes ActivationDialog.tsx-Warnmuster, falls vorhanden/nachgezogen). Aktivierung öffnet neu ein Dialog im identischen Overlay-Stil wie ActivationDialog.tsx (fixed inset-0 bg-black/50 → max-w-sm rounded-lg border bg-card p-6 shadow-lg), aber mit drei Aktionen statt zwei:
- Titel: "Modul aktivieren" + Modulname interpoliert.
- Body: Frage, ob sofort für die Standardgruppe freigegeben werden soll (siehe D-10-Wortlaut in
15-CONTEXT.md). - Buttons, von links nach rechts: "Abbrechen" (Border-Button, bricht komplett ab, Modul bleibt inaktiv) — "Später konfigurieren" (Border-Button, aktiviert das Modul OHNE Grant, Admin konfiguriert Freigaben separat über Matrix/Gruppen) — "Sofort freigeben" (Primary-Button
bg-primary, aktiviert das Modul UND erzeugt einenModuleGrantfür die als Standard markierte Gruppe des Mandanten). - Falls der Mandant keine als Standard markierte Gruppe besitzt (D-13: Markierung kann abgeschaltet sein): "Sofort freigeben" ist
disabledmit Hinweistext darunter ("Keine Standardgruppe markiert — lege zuerst eine unter Gruppen fest oder konfiguriere die Freigabe manuell.").
7. Sidebar (apps/web/src/components/layout/sidebar.tsx)
Explizit keine visuelle Änderung. Die Komponente fetcht weiterhin GET /modules/active unverändert und rendert exakt dasselbe Markup — der Endpoint liefert nach dieser Phase serverseitig weniger Module für nicht-freigegebene Benutzer, das ist reine Datenänderung, keine UI-Änderung. Kein neuer Sidebar-Zustand ("gesperrtes Modul ausgegraut anzeigen" o. ä.) — nicht freigegebene Module fehlen schlicht in der Liste, identisch zum bestehenden Verhalten für nicht-aktivierte Module.
Dashboard-Widgets (PERM-07, D-22)
Ebenfalls keine neue UI-Komponente: DashboardService.getWidgets() filtert serverseitig, das Frontend rendert nur die gefilterte Liste — identisch zum bestehenden Grid-Rendering-Pfad. Kein Platzhalter/"gesperrtes Widget"-Zustand, das Widget verschwindet ersatzlos (D-22 verlangt kein Hinweis-Kachel).
UI Considerations
Applicable state considerations resolved: 17 covered, 5 backstop, 0 unresolved
Quelle:
ui-consideration-probe.cjsüber sieben Surfaces (E1/admin/groups, E2 Permission-Matrix, E3 User-Detail-Modal, E4 403-Seite, E5 Marketplace-Karte, E6 Aktivierungs-Dialog, E7 Sidebar + Dashboard-Grid). Der Prosa-Klassifikator ordnete E5 zunächst alsunclassifiedein; nach manueller Einordnung alslist-collection+interactive-control+static-contentwurden dessen acht Kategorien nachgezogen (Propose-then-confirm, ui-phase §9.5).
| Category | Element(s) | Status | Resolution / Reason |
|---|---|---|---|
| empty | Gruppenliste (/admin/groups) |
✅ covered | Leerer Zustand rendert die dokumentierte "Keine Gruppen vorhanden"-Copy mit CTA, identisches Muster zu AdminUsersPage's noUsers-Zustand |
| empty | Permission-Matrix ohne aktive Module | ✅ covered | Dokumentierte Empty-State-Copy mit Link zu /admin/modules |
| empty | User-Detail ohne Gruppenmitgliedschaft | ✅ covered | Dokumentierte Empty-State-Copy, Chip-Liste einfach leer |
| empty | User-Detail-Modul-Zugriff-Tabelle, wenn Mandant keine aktiven Module hat | 🧪 backstop | Kein explizit dokumentierter Text — Tabelle rendert 0 Zeilen; Verifikation prüft, dass kein leerer <table>-Rahmen ohne jeden Hinweis erscheint |
| loading | Matrix-Zellen-Toggle, Gruppen-Modal-Speichern | ✅ covered | Identisches Inline-Spinner-Muster wie bestehender isToggling-Zustand in AdminModulesPage — kein neues Ladeverhalten |
| error | Grant-Speicherfehler | ✅ covered | Dokumentierte Fehlermeldung im bestehenden error-Div-Muster |
| error | Gruppe löschen schlägt fehl (z. B. Netzwerkfehler) | 🧪 backstop | Kein bestehendes Netzwerkfehler-Muster für Lösch-Dialoge im Projekt (aktuelle Lösch-Dialoge scheitern "silently" laut Code-Kommentar) — Planner muss entscheiden, ob dieser Präzedenzfall (stiller Fail) hier ebenfalls gilt oder eine sichtbare Fehlermeldung nötig ist, da PERM-01 konkrete Zahlen verspricht, die bei Fehlschlag nicht veralten dürfen |
| populated | Matrix mit vielen Modulen/Gruppen | ✅ covered | Sticky Header + sticky erste Spalte + Kategorie-Gruppierung + Suchfeld, siehe Surface Contract 2 |
| overflow | Lange Gruppennamen in der Matrix-Kopfzeile | 🧪 backstop | Kein expliziter Truncate-/Tooltip-Mechanismus dokumentiert — Planner sollte truncate + title-Attribut auf den Spaltenkopf anwenden, analog zum bestehenden truncate-Einsatz in MarketplaceCard.tsx (h3.truncate) |
| zero-one-many | AD-Bindung: Gruppe ohne/mit gebundener AD-Gruppe | ✅ covered | Zwei explizite Zustände dokumentiert (ungebunden zeigt Auswahl-UI, gebunden zeigt Chip + Entfernen-Link) |
| zero-one-many | Standardgruppe: keine/eine markierte Gruppe pro Mandant | ✅ covered | D-13 erzwingt DB-seitig genau 0 oder 1 — UI zeigt Stern-Toggle je Zeile, "Sofort freigeben" wird disabled, wenn keine Standardgruppe existiert (Surface Contract 6) |
| long-text | Modulnamen in der Matrix-ersten-Spalte | ✅ covered | sticky left-0-Spalte erhält feste Mindestbreite + truncate mit title-Attribut, identisch zur Overflow-Behandlung in Zeile darüber |
| partial | Matrix-Zelle (E2) und „Direkt"-Checkbox im User-Detail (E3), wenn der PATCH nach dem optimistischen Toggle fehlschlägt | ✅ covered | Die Checkbox springt auf den Serverzustand zurück, zusätzlich zur dokumentierten Fehlermeldung. Es darf keinen Zustand geben, in dem die Oberfläche eine Freigabe anzeigt, die in der Datenbank nicht existiert — das bestehende toggleModule-Muster in AdminModulesPage setzt beim Fehlerfall ebenfalls zurück |
| partial | Gruppenliste (E1): Mitgliederzahl je Zeile | ✅ covered | Kein Teilzustand möglich — die Zahl kommt im selben Response wie die Gruppenzeile; es gibt keinen Nachlade-Pfad, der eine Zeile ohne Zahl rendern könnte |
| partial | AD-Gruppensuche im Create/Rename-Modal (E1), wenn das LDAP nur einen Teil der Treffer liefert oder der Bind fehlschlägt | 🧪 backstop | Kein dokumentiertes Verhalten für Teilergebnisse der Gruppensuche. Verifikation prüft, dass die Ergebnisliste bei fehlgeschlagener oder unvollständiger Suche nicht stillschweigend leer erscheint, sondern den bestehenden Fehler-Div-Pfad aus admin/ldap/page.tsx nutzt |
| partial | Sidebar und Dashboard-Grid (E7) | ✅ covered | Kein Teilzustand — beide erhalten vom Server die fertig gefilterte Liste; ein teilweise gefiltertes Ergebnis kann strukturell nicht entstehen |
| empty · loading · error · populated · zero-one-many | Marketplace-Katalog (E5) in seinen Bestandszuständen | ✅ covered | Unverändert gegenüber dem ausgelieferten Marketplace — diese Phase ergänzt ausschließlich das Badge und den nicht-klickbaren Zustand einer einzelnen Karte, sie fasst weder Katalog-Laden, Katalog-Fehler noch Leerzustand an |
| long-text · overflow | Marketplace-Karte (E5) mit langem Modulnamen plus zusätzlichem „Kein Zugriff"-Badge | 🧪 backstop | Die Badge-Reihe (flex items-center gap-2) wird durch das zweite Badge breiter; h3.truncate deckt den Titel ab, aber nicht die Badge-Reihe. Verifikation prüft, dass Status-Badge und „Kein Zugriff"-Badge bei schmaler Karte umbrechen statt aus der Karte zu laufen |
| partial | Marketplace-Karte (E5): Karte gerendert, bevor der Freigabe-Status bekannt ist | 🧪 backstop | Nicht dokumentiert, ob Aktivierungs- und Freigabestatus im selben Response kommen. Verifikation prüft, dass keine Karte kurzzeitig ohne „Kein Zugriff"-Badge klickbar erscheint und dann nachträglich sperrt — im Zweifel Badge-Status serverseitig mitliefern |
| empty · loading · error | 403-Seite (E4) und Aktivierungs-Dialog (E6) | ✅ covered | Beide sind reine Zustandsanzeigen ohne eigenen Datenabruf: die 403-Seite rendert serverseitig fertig, der Dialog arbeitet auf bereits geladenen Moduldaten. Fehler beim Aktivieren laufen über den bestehenden Fehler-Div-Pfad von AdminModulesPage |
| long-text | Modulname im Aktivierungs-Dialog (E6) | 🧪 backstop | Der interpolierte Modulname steht in Titel und Fließtext des max-w-sm-Dialogs; kein Truncate dokumentiert. Verifikation prüft, dass ein langer Name den Dialog umbricht statt ihn zu verbreitern |
Registry Safety
| Registry | Blocks Used | Safety Gate |
|---|---|---|
| shadcn official | keine — Tool ist none |
not required |
| Drittanbieter | keine | not required |
Kein Registry-Zugriff in dieser Phase — alle Komponenten sind handgebaut nach den in "Surface Contracts" referenzierten, bereits im Repo vorhandenen Mustern.
Checker Sign-Off
- Dimension 1 Copywriting: PASS
- Dimension 2 Visuals: PASS
- Dimension 3 Color: PASS
- Dimension 4 Typography: PASS
- Dimension 5 Spacing: PASS
- Dimension 6 Registry Safety: PASS
Approval: pending