Globaler Tenant-Kontext (IDEA-027)

Architektur-Entscheidung fuer Alexium Pages / Alexium Studio: jede View, jeder Controller, jedes Repository operiert implizit auf einem aktiven Tenant pro Session.

Kern-Idee

Statt dass jede View einen eigenen Tenant-Dropdown hat und Sysadmin-Spezialbehandlung baut, lebt der aktive Tenant in $_SESSION['active_tenant_id']. Sysadmin und Sysdesigner koennen ihn per Topbar-Switcher wechseln; alle anderen Rollen sind an ihren primaeren Tenant gepinnt.

Service-API

Service\Context\TenantContext (statisch):

MethodeZweck
active(): ?intAktueller Tenant aus Session (null fuer ungesetzten Sysadmin)
setActive(int $tid)Switcher schreibt; Pinned-Sessions werfen LogicException
pin(int $tid)Tenant-/Unit-Rollen werden an ihren Tenant gepinnt (Login)
isPinned(): booltrue wenn Pin gesetzt (kein Switch erlaubt)
clear()Bei Logout

Pin-Verhalten beim Login

RolleVerhalten
system_admin, system_designerKein Pin. active() startet null, Switcher setzt
tenant_*pin($user['tenant_id'])
unit_*pin($user['tenant_id']) (Tenant der primaeren Unit)

Was die Migration aufraeumt

Vorher (heute):

if ($user['isSystemAdmin']) {
    $rows = $repo->findAllAcrossTenants();
} else {
    $rows = $repo->findByTenant($user['tenant_id']);
}

Nachher:

$rows = $repo->findByTenant(TenantContext::active());

Sysadmin sieht “alles” nicht mehr durch Branching, sondern durch Switchen.

Inventur (Stand 2026-04-26)

  • 12 von 19 Partial-Views mit Tenant-Filter
  • 7 Controller mit Sysadmin-Branching (~30 Stellen allein im WorkflowController)
  • 38 Repo-Methoden mit tenantId-Param
  • 16 JS-Tenant-Dropdowns (12 entfallen, 4 bleiben fachlich)
  • 10 tenant-agnostische Views (Audit-Log, Host-Bindings, System-Settings, Profile etc.)

Detail: project/superpowers/specs/2026-04-26-tenant-context-inventory.md

Stand (2026-04-26): Sprint 3 abgeschlossen

Vollstaendig umgesetzt im Branch feature/tenant-context:

  • Service-API: TenantContext (Session) + TenantContextLookup (DB-Helper) + TenantContextBootstrap (Per-Request-Validation)
  • Migration 063: account.last_active_tenant_id mit FK + ON DELETE SET NULL
  • Topbar-Switcher-UI: Pill rechts vom Brand mit Dropdown (Sysadmin) bzw. read-only Badge (Tenant-User)
  • Smart-Default: beim Sysadmin-Login wird last_active_tenant_id mit Fallback auf kleinste tenant_id gesetzt
  • Controller-Sweep: WorkflowController (30 Stellen), UserController, MediaController, EventController
  • View-Sweep: Editor-Views ueber EditorContextResolver, 4 Workflow-Partials, Users/Units/Events
  • JS-Sweep: Tenant-Selects entfernt, JS liest aktiven Tenant aus root.dataset.tenantId oder boot.data.tenant_active
  • Sicherheit: EventController::resolveContextTenantId akzeptiert kein $_GET['tenant_id']-Override mehr

34 Unit-Tests gruen, 20/20 Stage-Smoke-Views 200 OK.

Kritische Stellen (Vorsicht)

  • WorkflowController::resolveTeamTenantId — Drei Quellen (Boot, Override, Template-Fallback). Nicht 1:1 ersetzbar.
  • step-config/*.php — Step-Tenant ist eine eigene Dimension, nicht Session-Tenant. Nicht anfassen.
  • EventController::resolveContextTenantId — Heute GET-Override fuer Sysadmin-Debug. Entfaellt nach Migration; Support-Workflows aendern sich.
  • Host-Bindings, Audit-Log, System-Settings — tenant-agnostisch, Switcher ausgrauen wenn aktiv.

Zusammenhaenge