Wie ein Screen an die CORE-Shell andockt — eine Steckdose, kein iframe · verankert aus PLAT#09
TGV_MODULES.define("buchhalter", {
mount(outlet, ctx) { /* PFLICHT — Modul rendert in outlet */ },
unmount() { /* optional — Shell bricht ctx.signal ohnehin ab */ },
onScopeChange(s) { /* optional — fehlt er, re-mountet die Shell */ },
});
| Methode | Pflicht | Semantik |
|---|---|---|
mount(outlet, ctx) | ✓ | Modul rendert in outlet (leeres <div>, gehört dem Modul). ctx ist eingefroren. |
unmount() | – | Aufräumen. Die Shell ruft vorher ctx.signal.abort(). Fehlt die Methode, reicht der Abort. |
onScopeChange(scope) | – | Benachrichtigung. Fehlt sie, re-mountet die Shell (= heutiges Verhalten). |
ctx bekommt (lesen, nie setzen)| Feld | Zweck |
|---|---|
ctx.scope · ctx.period | Aktiver Scope / Periode — frozen. Lesen. Nie setzen. |
ctx.locale | Aktive Sprache (Shell besitzt i18n global — Module bringen keine eigene). |
ctx.session · ctx.params | Supabase-Session {email, account} · Route-Parameter, beide frozen. |
ctx.signal | AbortSignal — jeder fetch reicht ihn durch. Wird beim Unmount abgebrochen. |
ctx.restGet(q) | PostgREST mit User-JWT → RLS greift. Der bevorzugte Datenpfad. |
ctx.skillFetch(n, b) | /api/skill/<n>. ⚠️ serverseitig service_role → RLS umgangen (s.u.). |
ctx.postToChat(p) | Modul → Chat-Zone über den Shell-Event-Bus. |
ctx.requestScope(cand) | Modul erfragt einen Scope-Wechsel. Die Shell entscheidet. |
restGet läuft über RLS (Scope = harmloser View-Filter) · skillFetch läuft über service_role (RLS umgangen — die App-Schicht ist die einzige Grenze). Ein Modul, das den nutzer-editierbaren localStorage-Scope an einen service_role-Endpoint reicht, wäre das Leck.Strukturell verriegelt: ctx ist Object.freezed ohne Setter · requestScope prüft gegen authorizedScopes() und delegiert an die Shell · assertContract() weist ein Modul ab, das setScope/setStore exponiert (fail-rot).
authorizedScopes() ist heute ein UX-Guard, keine echte Autorisierung — mandant ist serverseitig hart "jcbinetti". Vor dem ersten externen Mandanten blockierend: jeder /api/skill/*-Endpoint MUSS den Client-Scope serverseitig gegen die Memberships schneiden (Intersection, fail-closed).TGV_MODULES.adaptLegacy("dokumente", secDokumente); // kein Rewrite
adaptLegacy(id, renderFn) hüllt eine bestehende render(outlet, params)-Funktion in den Vertrag — ohne Rewrite, ohne eigenes onScopeChange (die Shell re-mountet bei Scope-Wechsel wie heute). Der Weg eines Cockpits ist dreistufig, jede Stufe rückrollbar: Deep-Link-Karte (Vollbild) → adaptLegacy (Render-Fn tritt ein) → define (natives Modul mit onScopeChange + ctx.signal).
assertContractNach dem TGV-Prinzip „regressions-anfällige Regeln → aktive Guards, nicht nur Doku" wirft define() fail-rot, wenn: mount fehlt/keine Funktion · unmount/onScopeChange existieren, aber keine Funktionen sind · das Modul setScope/setStore exponiert (Scope-Setter-Verbot). Zusätzlich prüft die Shell nach jedem mount, dass ctx.scope unverändert ist.
01_ITERATIONS/PLAT_09_CORE_FRONTEND_APPSHELL/MODULE_CONTRACT.md · Implementierung = TGV_MODULES/assertContract in 06_DASHBOARD/core_shell.html. Diese Seite macht ihn im Kompendium auffindbar. Im Bau gebunden über den Seitenbau-Kanon (Composer-Skill, Gate D): ein Fach-Screen, der als Modul lebt, MUSS diesen Vertrag erfüllen — sonst weist assertContract ihn ab.Quellen: PLAT#09 MODULE_CONTRACT.md · core_shell.html (TGV_MODULES, adaptLegacy, assertContract) · IDENTITY_RLS_SSOT_DECISION.md §4 (service_role RLS-bypassed) · MANDANT#01 (offene Auth-Naht).