Commit Graph

597 Commits

Author SHA1 Message Date
schalli 2ef9b8638c feat(16-02): standard group handoff building block (D-06)
- DEFAULT_GROUP_NAME extracted as shared constant between
  ensureDefaultGroup() and the new reassignDefaultBeforeDelete()
- reassignDefaultBeforeDelete(tenantId, groupId) moves the default
  marker deterministically (DEFAULT_GROUP_NAME first, else oldest
  other group by createdAt asc), never deletes, never throws
- 6 test cases covering handoff, fallback ordering, no-other-group,
  non-default no-op, cross-tenant no-op, and P2002 race
2026-08-06 15:55:53 +02:00
schalli 1b19876c32 docs(16-01): complete AD group import tracer plan 2026-08-06 15:49:26 +02:00
schalli b4844557af docs(16-01): add plan summary 2026-08-06 15:45:41 +02:00
schalli 626e29659d feat(16-01): apply Group.internalName/ldapObjectGuid migration to local DB
- Migration 20260806133916_add_group_internal_name_and_object_guid applied
  against the local Postgres container (baselined 24 prior migrations first
  — _prisma_migrations was missing, unrelated to this task's DDL)
- New describe block in migration-sql.spec.ts pins internalName,
  ldapObjectGuid, and the (tenantId, ldapObjectGuid) unique index
- Full API test suite green (40 files, 526 tests)
2026-08-06 15:43:19 +02:00
schalli 3523e43a13 feat(16-01): tracer — select and import AD groups end-to-end
Task 1 checkpoint resolved: approve-both, granted 2026-08-06 by the
project owner (D-04 one-way schema extension: Group.internalName +
Group.ldapObjectGuid, both nullable, one versioned migration).

Adds the Phase 16 tracer slice through every layer:
- Prisma schema: Group.internalName, Group.ldapObjectGuid,
  @@unique([tenantId, ldapObjectGuid]) (Prisma client regenerated;
  the versioned migration itself is Task 3, separately blocking).
- LdapService: listGroups() now reads objectGUID via
  explicitBufferAttributes and flags alreadyImported per tenant;
  new importGroupsByDn() creates a Group per checked DN with
  name/ldapDn/ldapObjectGuid, reject-with-report on name collision
  (P2002 on name -> nameCollisions, P2002 on ldapObjectGuid ->
  skipped), never aborts the batch on one DN's error; new static
  escapeLdapFilterBuffer() for Plan 16-03's later existence sweep.
- DTO/controller: ImportGroupsDto, POST /ldap/groups/import
  (ADMIN/SUPER_ADMIN), listGroups route now tenant-scoped.
- Frontend: new "AD-Gruppen importieren" section in /admin/ldap,
  own discovery/import handlers with a visible error state
  (Owner decision 2026-08-06 — no silent catch{} for these two
  handlers), i18n keys in de.json/en.json.
- Tests: 8 new cases covering the full <behavior> list plus
  listGroups sort order and alreadyImported.

Flagged assumption (RESEARCH.md A1/A2): objectGUID rename-stability
and the binary filter syntax are unverified against a real AD —
this plan only WRITES the GUID, Plan 16-03 reads it back live.
2026-08-06 15:09:35 +02:00
schalli c4a25511f3 docs(16): create phase plan 2026-08-06 14:42:44 +02:00
schalli 3dfa671ec9 docs(16): create phase plan — 5 plans, 4 waves, tracer-first 2026-08-06 14:38:45 +02:00
schalli 5c7d4ad128 docs(16): reassign PERM-02 to phase 16 with import semantics 2026-08-06 14:10:53 +02:00
schalli 1c658a8ed7 docs(16): UI design contract 2026-08-06 13:52:33 +02:00
schalli 092f4f6068 docs(16): UI design contract 2026-08-06 10:34:48 +02:00
schalli c5ce07afab docs(16): add validation strategy 2026-08-05 16:49:26 +02:00
schalli 9a494b1afd docs(16): research AD-Gruppen-Synchronisation phase 2026-08-05 16:48:12 +02:00
schalli fca4e3c3b5 docs(state): record phase 16 context session 2026-08-05 16:26:34 +02:00
schalli 8728716824 docs(16): capture phase context 2026-08-05 16:26:34 +02:00
schalli d502941767 docs: mark DOE URL todo as post-phase-16
Tessera CI/CD / Lint & Type Check (push) Successful in 48s
Tessera CI/CD / Tests (push) Successful in 47s
Tessera CI/CD / Build & Publish Images (push) Successful in 7s
2026-08-05 16:13:15 +02:00
schalli 324d578fe4 docs: diagnose DOE tender URL defect (API uri instead of notice page) 2026-08-05 16:11:44 +02:00
schalli 13ae48f0c1 docs: capture todo - Ausschreibungsportal falsche URL 2026-08-05 16:07:30 +02:00
schalli ff70b4efba docs(quick-260805-fok): Standardgruppe bei Mandanten-Anlage + Startup-Reparatur
Tessera CI/CD / Lint & Type Check (push) Successful in 48s
Tessera CI/CD / Tests (push) Successful in 49s
Tessera CI/CD / Build & Publish Images (push) Successful in 28s
2026-08-05 11:37:43 +02:00
schalli 0d7d8a597e feat(260805-fok): beide Mandanten-Entstehungspfade verdrahten + Startup-Reparatur
- TenantService.create ruft nach prisma.tenant.create ensureDefaultGroup
  auf; Fehler werden protokolliert, nicht propagiert (Muster aus
  UserService.create)
- tenant.module.ts importiert GroupsModule (keine Zirkularitaet, wie
  UserModule bereits vormacht)
- AdminSeedService.onApplicationBootstrap besteht jetzt aus zwei
  sequenziellen await-Schritten: seedAdmin() (bisheriger Rumpf, plus
  ensureDefaultGroup nach dem Tenant-Upsert und VOR user.create), dann
  ensureDefaultGroupsForAllTenants() als abschliessende Reparatur ueber
  ALLE Mandanten — laeuft unabhaengig von seedAdmin()s fruehen
  Rueckkehrpfaden (fehlende ENV / Admin existiert bereits) und ist je
  Mandant sowie insgesamt try/catch-gekapselt, blockiert den API-Start nie
- Reparatur sitzt bewusst NICHT als eigener onApplicationBootstrap-Hook
  in GroupsModule (Ordering-Falle aus tender-scheduler.service.ts)
- tenant.service.spec.ts, admin-seed.service.spec.ts (neu): Reihenfolge,
  beide fruehen Rueckkehrpfade, Fehlerisolation je Mandant, Idempotenz
  ueber zwei Bootstrap-Laeufe
2026-08-05 11:30:49 +02:00
schalli 9d1254cd78 feat(260805-fok): GroupsService.ensureDefaultGroup(tenantId)
- Neue Methode ensureDefaultGroup: legt fuer einen Mandanten ohne jede
  Gruppe die Standardgruppe 'Alle Benutzer' (isDefault:true) an, nimmt
  alle Bestandsbenutzer als MANUAL-Mitglieder auf und erzeugt Grants
  fuer alle aktiven Module — derselbe Endzustand wie die drei
  Backfill-INSERTs der Migration 20260804130130
- Waechter prueft ausschliesslich group.count === 0, niemals die
  fehlende isDefault-Markierung (D-13)
- P2002 aus dem partiellen Index Group_one_default_per_tenant wird
  abgefangen und liefert null statt zu werfen (Race-Sicherheit)
- groups.service.spec.ts: Fake erweitert um group.count,
  tenantModuleActivation, moduleGrant.findMany/createMany,
  $transaction mit Callback-Form, plus voller ensureDefaultGroup-Testblock
2026-08-05 11:28:16 +02:00
schalli c2e9f67a7d docs(quick-260805-d0r): Gruppenmitgliedschaften im Benutzer-Detail
Tessera CI/CD / Lint & Type Check (push) Successful in 50s
Tessera CI/CD / Tests (push) Successful in 52s
Tessera CI/CD / Build & Publish Images (push) Successful in 4m5s
2026-08-05 09:44:11 +02:00
schalli 8ce374818c feat(260805-d0r): membership-origin badge on chips (D-19/D-20)
- Reuses admin.groups.members.sourceManual/.sourceLdap -- no new i18n key
- Badge markup copied verbatim from GroupMembersModal.tsx (blue for LDAP,
  neutral gray otherwise), same visual language on both surfaces
2026-08-05 09:37:26 +02:00
schalli 73122b5676 test(260805-d0r): add failing test for MANUAL/LDAP origin badge on chips 2026-08-05 09:36:45 +02:00
schalli f8ff74bd3f feat(260805-d0r): UserAccessModal chips render from data.groups (D-16)
- Chips come from the response's groups (actual GroupMembership rows),
  not the union of row.viaGroups -- closes the reproduced defect where
  revoking a group's last grant hid an otherwise-unchanged membership
- React key is the group id, not the name
- Test file: moved the LDAP/MANUAL badge assertion out of this commit,
  it belongs to Task 2 which reuses admin.groups.members
2026-08-05 09:35:54 +02:00
schalli 771f680034 test(260805-d0r): rewrite modal test fixtures to { groups, modules } shape
- Regression: group without any module grant stays visible as a chip
- New: mixed empty-modules + populated-groups case, LDAP/MANUAL badge case
2026-08-05 09:34:42 +02:00
schalli ecadf69e14 feat(260805-d0r): getUserAccess returns groups from GroupMembership (D-16)
- New groupMembership.findMany query, tenant-scoped via group.tenantId
  (GroupMembership has no own tenantId column)
- Response shape changes from an array to { groups, modules }; modules
  entries stay field-identical to before
- Group without any module grant now stays visible, closing the
  reproduced defect
2026-08-05 09:33:51 +02:00
schalli b6d4e4acb6 test(260805-d0r): add failing tests for groups in getUserAccess
- Regression: user in a group without any module grant stays visible
- Cross-tenant: membership in a foreign tenant's group is excluded
- Origin (MANUAL/LDAP), empty-modules case, stable alpha sort
2026-08-05 09:33:22 +02:00
schalli be1183a61c docs(roadmap): add Phase 16 AD group synchronisation 2026-08-05 08:54:55 +02:00
schalli 8e70f55c8c docs(15): add pattern map from planning
Tessera CI/CD / Lint & Type Check (push) Successful in 42s
Tessera CI/CD / Tests (push) Successful in 50s
Tessera CI/CD / Build & Publish Images (push) Successful in 2m46s
2026-08-04 19:54:13 +02:00
schalli d15475d6d7 docs(15-08): complete Serverseitige Modulsperre und Marketplace-Sperr-Badge plan 2026-08-04 19:53:13 +02:00
schalli 2e7daa481e feat(15-08): Marketplace-Karte mit drittem Zustand "Kein Zugriff"
- marketplace/page.tsx und marketplace/[slug]/page.tsx auf GET
  /modules/catalog umgestellt (ein Aufruf statt zwei), beide Statusflags
  (isActiveForTenant, hasAccess) kommen in einer Antwort -> kein
  Zwischenzustand, in dem eine Karte kurzzeitig ohne Sperr-Badge
  anklickbar erscheint
- MarketplaceCard bekommt hasAccess-Prop: drittes Badge (Bernstein,
  "Kein Zugriff") bei isActive && !hasAccess, Karte opacity-60/
  cursor-not-allowed, Klick loest Toast statt Navigation aus; bei
  Zugriff navigiert der Klick zu /marketplace/[slug]; Badge-Reihe
  bekommt flex-wrap gegen Overflow bei langen Namen
- [Rule 2] Marketplace-Ansicht war zuvor komplett isAdmin-gated
  (Zugriff verweigert fuer USER) - das widersprach D-08 ("Katalog
  bleibt Schaufenster fuer jeden authentifizierten Benutzer") und
  haette das neue Sperr-Badge fuer USER nie sichtbar gemacht. isAdmin
  gated jetzt nur noch die Aktivieren/Deaktivieren-Aktion (canManage),
  nicht mehr die gesamte Seite
- bestehende Marketplace-Tests auf einaufrufiges Catalog-Mock
  umgestellt, "access-denied fuer non-admin"-Test durch "Karten
  sichtbar, aber ohne Manage-Button" ersetzt
2026-08-04 19:49:46 +02:00
schalli 43c7fa200b feat(15-08): serverseitige Modulsperre mit 403-Seite
- checkModuleAccess (module-access-actions.ts) fragt GET /modules/active
  serverseitig ab, Cookie-Weiterleitung nach fetchCurrentUser-Muster,
  schliesst im Zweifel (fehlendes Cookie, nicht-ok, Fehler -> false)
- page.tsx zur async Server Component umgebaut, rendert bei fehlender
  Freigabe das 403-Markup direkt (kein notFound(), kein Redirect, D-07)
- bisheriger Client-Inhalt (Whitelist-Pruefung, Nicht-gefunden-Zustand,
  Ruecknavigation) unveraendert nach module-shell.tsx ausgelagert
- 5 Tests in module-access.test.tsx: 403-Zustand, Shell-Rendering,
  fehlendes Cookie, nicht-ok-Antwort, unregistrierter Slug trotz Zugriff
2026-08-04 19:41:25 +02:00
schalli c6086ba750 docs(15-07): complete Freigabe-Matrix, Aktivierungsdialog und Benutzer-Detail-Grants plan 2026-08-04 19:28:18 +02:00
schalli 050d070340 feat(15-07): Benutzer-Detail mit geerbten Rechten und Direkt-Freigaben
- Neue UserAccessModal.tsx: laedt einmal GET /module-grants/users/:userId
  und rendert daraus zwei Abschnitte -- Gruppenmitgliedschaften (read-only
  Chip-Liste, dedupliziert aus allen viaGroups-Namen; Bearbeitung bleibt
  ausschliesslich unter /admin/groups, D-16) und Modul-Zugriff (Modul |
  erbende Gruppen als Chips oder "–" | Direkt-Checkbox)
- Direkt-Checkbox verhaelt sich identisch zur Matrix-Zelle: optimistisches
  Toggle via POST/DELETE /module-grants mit moduleId+userId, Rollback samt
  sichtbarer Fehlermeldung bei Fehlschlag (T-15-25), aria-label pro Zeile
  aus admin.users.grants.directCheckboxLabel
- admin/users/page.tsx: vierter Aktionsbutton "Details" je Zeile oeffnet
  das Modal
- user-access-modal.test.tsx: 5 Tests (Chip-Liste + Leerzustand, Modultabelle
  mit geerbtem/nicht-geerbtem Modul, Rollback bei Fehler, Hinweistext ohne
  aktive Module, aria-label je Checkbox)
2026-08-04 19:25:15 +02:00
schalli 3cd6d997cf feat(15-07): Aktivierungsdialog mit drei Aktionen in /admin/modules
- Neue ActivateModuleDialog.tsx: Abbrechen / "Spaeter konfigurieren" (nur
  POST /modules/:id/activate) / "Sofort freigeben" (POST .../activate
  gefolgt von POST /module-grants fuer die als Standard markierte Gruppe) --
  zwei getrennte Aufrufe, kein neuer kombinierter Endpoint (D-10)
- Laedt GET /groups beim Oeffnen; ohne markierte Standardgruppe ist "Sofort
  freigeben" disabled mit Hinweistext (D-13)
- admin/modules/page.tsx: toggleModule-Klick verzweigt -- Deaktivierung
  bleibt direkt, Aktivierung oeffnet den Dialog statt sofort zu aktivieren;
  Erfolg aktualisiert Aktivierungs-Map + Sidebar-Refresh wie bisher
- grants-matrix.test.tsx erweitert um 3 Tests fuer den Dialog (alle drei
  Buttons vorhanden, Sofort-freigeben deaktiviert ohne Standardgruppe,
  Aufrufreihenfolge activate->grant)
2026-08-04 19:21:25 +02:00
schalli 6b2a6f1ce4 feat(15-07): Freigabe-Matrix Module x Gruppen unter /admin/modules/grants
- Neue Client-Komponente admin/modules/grants/page.tsx: laedt GET /module-grants/matrix
  einmal, rendert Module x Gruppen mit sticky erster Spalte/Kopfzeile, Kategorie-
  Gruppierung, Suchfeld und Admin-Bypass-Fussnote (D-03/D-15, PERM-03)
- Jede Zelle togglet sofort optimistisch (POST/DELETE /module-grants); Fehlschlag
  springt die Checkbox zurueck und zeigt die Fehlermeldung im bestehenden error-Div
  (T-15-25) -- identisches Muster zu AdminModulesPage.toggleModule
- aria-label pro Checkbox aus admin.groups.grants.matrixCheckboxLabel beschreibt die
  bevorstehende Aktion (freigeben/entziehen), nicht den aktuellen Zustand
- admin/modules/page.tsx: neuer Header-Button "Freigaben-Matrix" verlinkt auf die
  Unterseite, kein siebter Sidebar-Eintrag
- grants-matrix.test.tsx: 5 Tests (befuellte Matrix, leerer Zustand, Rollback bei
  Fehler, Suchfilter, aria-label je Checkbox)
2026-08-04 19:18:30 +02:00
schalli a3e8d0a158 docs(15-06): complete Gruppenverwaltung im Admin-UI plan 2026-08-04 19:06:54 +02:00
schalli 4985e43412 feat(15-06): group member management + delete dialog with concrete impact numbers
- GroupMembersModal.tsx: chip list of current members with source badge
  (MANUAL/LDAP); LDAP-sourced chips carry a disabled remove button with a
  "managed via AD sync" tooltip (D-19) instead of an active one; second
  section adds manual members via GET /users + POST /groups/:id/members,
  already-member candidates shown disabled (upsert on the API is
  folgenlos, no special-case needed)
- DeleteGroupDialog.tsx: loads GET /groups/:id/impact and interpolates
  memberCount/grantCount into the confirmation text (D-17); deliberately
  breaks from the project's silent-delete-failure precedent -- stays open
  and shows a visible error on a failed DELETE, since a silent failure
  here would leave an admin believing a group (and its grants) is gone
  while it still grants access (T-15-24)
- page.tsx: wires both dialogs in, refetches the group list after any
  member/delete mutation so member counts and badges stay current
- groups-page.test.tsx: disabled-vs-active remove button by membership
  source, delete text shows both numbers, visible error + dialog stays
  open on failed delete
2026-08-04 19:02:11 +02:00
schalli c3ba1f74ea feat(15-06): /admin/groups table + create/rename modal with AD radio-select binding
- page.tsx: sixth admin route, table (Name/AD-Bindung/Standardgruppe/
  Mitglieder/Aktionen), empty state matching AdminUsersPage's noUsers
  pattern, optimistic default-group star toggle (PATCH /groups/:id
  isDefault) with rollback + visible error div on failure, full refetch
  on success since setting one group default unsets all others server-side
  (D-13 transaction)
- GroupFormModal.tsx: create/rename dialog; AD binding section reuses
  GET /ldap/groups (D-18) with a radio list (D-05: exactly one AD group
  per Tessera group) instead of the LDAP page's checkbox multi-select;
  visible discoverError/noResults states instead of a silent-empty list
  (UI-SPEC backstop); create-with-binding does POST then a second PATCH
  since CreateGroupDto only accepts `name`
- groups-page.test.tsx: empty state, populated table, star-toggle
  optimistic PATCH + rollback-on-failure
2026-08-04 18:57:43 +02:00
schalli 90dc3981d5 feat(15-06): phase-wide i18n keys + sixth admin nav entry for groups
- de.json/en.json: admin.groups.* (incl. ldapBind, members, deleteConfirm,
  grants sub-namespaces), admin.users.grants.*, adminModules.grantsLink +
  grants.* + activationDialog.*, modules.accessDenied.*, marketplace
  statusNoAccess/toastNoAccess, header.admin.groups -- covers this plan's
  /admin/groups surface plus the Wave 4 surfaces (permission matrix,
  user-detail grants, activation dialog, 403 page, marketplace badge) so
  15-07/15-08 can run in parallel without touching the translation files
- admin-sidebar.tsx: sixth nav entry "Gruppen" -> /admin/groups with a
  roster/list icon (Lucide list glyph, distinct from the users icon)
2026-08-04 18:50:28 +02:00
schalli 09abb2b323 docs(15-03): complete modul-freigaben-schreibseite plan 2026-08-04 18:43:45 +02:00
schalli 1c32543f58 feat(15-03): GET /modules/catalog — beide Statusflags in einer Antwort
- ModuleAccessService.getCatalogFlags(tenantId, userId, role) liefert je
  aktivem Modul isActiveForTenant + hasAccess in einer Auflösung
- ModuleRegistryController.findCatalog (GET /modules/catalog), erreichbar
  für jeden authentifizierten Benutzer wie GET /modules (D-08)
- ADMIN/SUPER_ADMIN: hasAccess immer wahr für aktive Module (D-03)
- 4 neue Tests für getCatalogFlags
2026-08-04 18:41:23 +02:00
schalli 072fb7f62f feat(15-03): ModuleGrantsController und Einbindung in GroupsModule
- GET /module-grants/matrix, GET /module-grants/users/:userId,
  POST /module-grants, DELETE /module-grants — alle vier rollengeschützt
  (RolesGuard + Roles ADMIN/SUPER_ADMIN)
- matrix vor users/:userId deklariert (Beschattungsfehler-Vermeidung)
- GroupsModule bindet ModuleGrantsController/-Service ein; kein Import
  von ModuleRegistryModule nötig, da der Service nur PrismaService braucht
2026-08-04 18:41:17 +02:00
schalli 5e256db01d feat(15-03): ModuleGrantsService — Freigaben setzen/entziehen mit Mandanten-Gegenprüfung
- assertTargetBelongsToTenant prüft groupId/userId aus dem Request-Body
  gegen tenantId aus dem JWT (T-15-01), vor jedem Grant-Insert
- grant: Entweder-oder-Regel (D-04), aktive TenantModuleActivation (D-02),
  P2002 als Erfolg (Doppelklick-Schutz)
- getMatrix (D-15) und getUserAccess (D-16) für Matrix-Seite und
  Benutzer-Detail, jeweils sortiert und mandantengescoped
- 20 Tests inkl. adjacency/empty/ordering/idempotency/concurrency
2026-08-04 18:41:12 +02:00
schalli 614de2815a feat(15-04): AD-Gruppenmitgliedschafts-Abgleich im bestehenden LDAP-Sync
- LdapService.syncGroupMembershipsForTenant (neu, privat): pro AD-gebundener
  Group (ldapDn gesetzt) ein memberOf-Reverse-Query je Base-DN, nie ein
  Attribut-Lesen (Range-Retrieval-Pitfall). GroupMembership(source: LDAP)
  wird per createMany/skipDuplicates angelegt (lässt bestehende MANUAL-Zeilen
  unangetastet, D-19/D-20) und per deleteMany(source: 'LDAP', notIn: [...])
  bereinigt. Jede Gruppe läuft in eigenem try/catch, ein Fehler landet als
  "Gruppe <name>: <message>" in result.errors, die Schleife läuft weiter.
- Aufruf in syncUsersForTenant nach der Deaktivierungsschleife (Schritt 5)
  und vor lastSyncAt (Schritt 6) — hinter dem bestehenden Base-DN-No-Op-Wächter,
  kein separater Job, kein zweiter Button (D-21).
- LdapSyncResult um groupMembershipsAdded/groupMembershipsRemoved erweitert.
- ldap.service.spec.ts: neuer describe-Block mit 13 Tests (adjacency, empty,
  encoding, ordering, idempotency, concurrency/backstop) plus Anpassung der
  drei bestehenden Prisma-Fixtures und einer Ergebnis-Assertion an die
  erweiterte LdapSyncResult-Form.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 15:50:42 +02:00
schalli 2a79d4ca1f docs(15-05): complete dashboard-widget-modulfilterung plan 2026-08-04 15:39:48 +02:00
schalli 0ff46acd75 feat(15-05): getWidgets filters via ModuleAccessService (D-22, PERM-07)
- DashboardModule imports ModuleRegistryModule to inject ModuleAccessService
- getWidgets(userId, tenantId, role) runs the existing findMany unchanged
  first, then calls getAccessibleModuleIds exactly once — only if a loaded
  widget's type is in WIDGET_MODULE_MAP (currently always empty, so no
  lookup runs today); unresolved module slugs fail closed
- DashboardController.getWidgets forwards tenantId + role from the JWT
- dashboard.service.spec.ts (8 tests, TDD-GREEN): covers every <behavior>
  case incl. D-03 ADMIN bypass, adjacency/empty/ordering/idempotency, and
  fail-closed on an unresolved Module slug
- pnpm --filter @tessera/api test: 457/457 green; type-check clean
- manual e2e against local API + DB container: empty WIDGET_MODULE_MAP
  leaves an existing user's widget count unchanged (2/2 clock+search
  survived the filter), throwaway verification user/rows removed after
2026-08-04 15:37:28 +02:00
schalli d0ff6f0bc0 test(15-05): add failing test for module-filtered getWidgets
- covers every <behavior> case from 15-05-PLAN.md task 2, including the
  adjacency/empty/ordering/idempotency edge-probe categories and the
  fail-closed unresolved-slug case
- RED confirmed: 4/8 fail against the current 1-arg getWidgets(userId)
2026-08-04 15:31:35 +02:00
schalli 954cd6e171 feat(15-05): static widget-to-module registration table
- WIDGET_MODULE_MAP + getModuleSlugForWidgetType (apps/api/src/dashboard/widget-module-map.ts)
- table intentionally empty at end of phase: all 8 existing widget types are module-free platform widgets (D-22)
- code constant chosen over a WidgetInstance schema column — no migration for a field empty on every row
2026-08-04 15:30:51 +02:00
schalli c4d7b7ded6 docs(15-02): complete Gruppenverwaltung & Standardgruppen-Mitgliedschaft plan 2026-08-04 15:27:48 +02:00