/* * SBKIM — Modul 16 — SBKIM-Siegel * * Self-inscribing-Selbst-Zertifikat einer PWA-Zelle. Bau-Sitzung 16 * (2026-05-24) implementiert alle vier Sub-Bereiche aus Spec-Sitzung 16: * * Sub (a) Selbst-Prüfung — Surface-Check für sieben Pflicht-Module * (01/02/03/04/05/07/15) via typeof globalThis[globalName] + * typeof ns[surfaceFn]==="function". Vier Status-Werte: * "ok" / "deferred" / "missing" / "broken". Snapshot zur * init()-Zeit, gecacht. Re-Init no-op. * Sub (b) Badge-Rendering — rundes 40-px-Medaillon Edel-Gold auf * Bronze-Ink, drei Hyphen-Bögen + Knoten-Punkt-Glyph, * 600 ms First-Boot-Animation einmalig pro Session. * Anti-Greenwashing binär: Element wird gar nicht im DOM * angelegt, wenn isCertified()===false. * Sub (c) Erklärungs-Modal — eigenständig in document.body (analog * Modul 15), Titel "SBKIM-Siegel — was bedeutet das?", * Datum + Modul-Liste + Aspekte + zwei Zeilen Aussteller- * Klärung. Backdrop/Esc/✕ schließen. * Sub (d) ZERTIFIKAT_ASPEKTE — Start-Eintrag „Grund-Siegel-Bezeugung * 2026-05-24" verbindlich; künftige Sicherheits-Module * ergänzen ihre Aspekt-Einträge per Pflege-PR. * * DOM-Mount-Variante: Option β (Bau-Sitzung-16-Entscheidung). Der * `badgeSelector` zeigt auf einen CONTAINER (Default `.lamps`); der * Badge-Span wird via JS darin erzeugt, ausschließlich wenn * isCertified()===true. Damit ist die Anti-Greenwashing-Klausel * binär erfüllt: kein DOM-Element überhaupt im Negativ-Fall. Wenn * der Selektor ein bereits-bestehendes Element matcht (z.B. * `#sbkim-siegel-badge` vor-injiziert), wird dieses Element als * Anker genutzt; sonst wird darin der Badge-Span erzeugt. * * Modul 16 ist NICHT protokoll-aktiv: kein Netz, keine Signatur, kein * Embedding, kein Handshake. Rein lokales Render-Modul. KEINE * benannten Error-Klassen — alle Fehlerpfade fail-soft via * console.warn (analog Modul 15). * * Public surface (registered on window.SbkimSiegel): * init(options?) -> Promise * isCertified() -> boolean (sync) * getExplanation() -> ExplanationSnapshot (sync, defensive Kopie) * getCertifiedModules() -> string[] (sync, defensive Kopie) * getAspects() -> Aspect[] (sync, defensive Kopie) * _meta -> Read-Anker für Tests * * options-Form (init): * { badgeSelector?: string, // Default '.lamps' (Container, Option β) * visible?: "visible"|"hidden", // Default "visible" * mountModal?: boolean, // Default true * repoUrl?: string | null, // Default null → Auto-Erkennung * ribbonText?: string, // Band-Text im Wappen unten (SELF-INSCRIBING). * // Ohne Wert bleibt das Band OFFEN (leer) + * // ein console.info-Vermerk; kein Auto-Label. * // Host graviert seinen Namen via ribbonText. * andockTool?: boolean } // Default false. true → optionaler * // „Fremden Knoten andocken"-Knopf im Modal, * // öffnet Modul-18-Wizard (KI-unabhängiger * // Handshake). „🔑"-Pfad bleibt unberührt. * * Self-check: emits a console.info line on script load (synchronous, * before any call). Siehe INTERFACES.md §1 Modul 16 und * docs/components/16_siegel.md. */ (function (global) { "use strict"; // ---- Pflicht-Modul-Liste (Karte 16 § Sub (a), INTERFACES § 1 Modul 16) ---- // // Sieben Pflicht-Einträge. Code-versioniert, KEINE Runtime-API zum // Setzen (Karte 16 § Strikte Tabus). Aktualisierung NUR per Pflege-PR. var PFLICHT_MODULE = [ { id: "01", name: "Storage", globalName: "SbkimStorage", surfaceFn: "init", lazy: false }, { id: "02", name: "Spore", globalName: "SbkimSpore", surfaceFn: "getOwnSpore", lazy: false }, { id: "03", name: "Embedding", globalName: "SbkimEmbedding", surfaceFn: "embedPassage", lazy: true }, { id: "04", name: "Match", globalName: "SbkimMatch", surfaceFn: "match", lazy: false }, { id: "05", name: "Anastomose", globalName: "SbkimAnastomose", surfaceFn: "handshake", lazy: false }, // 05b — aufgenommen 2026-08-16 auf Klaus' Wort, nach einem Befund an // seinem Tablet: das Netz-Panel meldete „Kein Nostr-Relais-Client (Modul // 05b) verfügbar", der Raum liess sich nicht lesen — und das SIEGEL LEUCHTETE // TROTZDEM. Es prüfte die sieben unten, und 05b war nicht darunter. // // Ein Siegel, das goldenes Vertrauen zeigt, während der Knoten den Raum // gar nicht lesen kann, sagt die Unwahrheit. Genau davor soll die // Anti-Greenwashing-Klausel (Karte 16) schützen — sie griff hier nicht, // weil die Liste unvollstaendig war, nicht weil die Prüfung schwach war. // // Gemessen vor der Aufnahme (alle 22 Knoten-Repos): JEDER laedt 05b. // Die Aufnahme loescht also nirgends ein Siegel — sie verhindert nur, dass // eines leuchtet, wo der Raum tot ist. // // `subscribe` ist die richtige Fläche: daran entscheidet sich das Lesen des // Raums. `publish` allein wuerde einen Knoten durchgehen lassen, der senden, // aber nichts empfangen kann. { id: "05b", name: "Relais-Client", globalName: "SbkimNostrRelay", surfaceFn: "subscribe", lazy: false }, { id: "07", name: "Apoptose", globalName: "SbkimApoptose", surfaceFn: "prepareSelfApoptose", lazy: false }, { id: "15", name: "Membran", globalName: "SbkimMembrane", surfaceFn: "init", lazy: false }, ]; // ---- Aspekte-Liste (Karte 16 § Sub (d), INTERFACES § 1 Modul 16) ---- // // Code-versioniert. Jedes spätere Sicherheits-Modul (10/11/12/14/ // künftige 15.B-Erweiterungen) MUSS in seiner Bau-/Pflege-Sitzung // hier einen Eintrag am Listen-Ende ergänzen (aktuelles Datum + // Modul-ID + ein-Satz-Beschreibung; siehe Karte 16 § Sub (d) // Pflicht-Konvention). var ZERTIFIKAT_ASPEKTE = [ { since: "2026-05-24", module: "16", aspect: "Grund-Siegel-Bezeugung (Protokoll)", description: "Grundlage jedes SBKIM-Siegels: Beim Start prüft die App selbst, ob die Pflicht-Module 01/02/03/04/05/07/15 geladen sind — nur dann entsteht das Siegel. Protokoll-Baustein, kein app-eigenes Datum (was DIESE App gerade lädt, steht oben unter „Pflicht-Module“).", }, { since: "2026-05-25", module: "15", aspect: "Sub (a) Read-API + Sub (b) postMessage-Brücke", description: "Finale Bedien-Pfade: MembraneSnapshot mit Siegel-Hook, vier op-Werte (sporeRef/query/hint/queryResult) mit Nonce-Pflicht, fail-soft Allowlist, Rate-Limit-Hook für Modul 11.", }, { since: "2026-05-25", module: "17", aspect: "Floating-Widget mit Vier-Slot-Live-Status", description: "Live-Status-Dashboard (LEBT/VERKEHR/FREMD/SIEGEL) als Endknoten-Standard; macht den SBKIM-Lauf sichtbar ohne Navleisten-Mount-Pflicht. Render-Schicht ohne Protokoll-Eingriff.", }, // Aspekt 4 — Mycel-Aktivität (Karte 16 § Sub (e), // Spec-Erweiterung 2026-05-26). Dynamisch sichtbar: // _meta.mycelConnected === false → Modal rendert „pending"-Marker // statt Datum; _meta.mycelConnected === true → Modal rendert Datum. // Aktivierung über window-Event sbkim:handshake outcome:"established" // (Modul 05 Bau 17). RAM-only — Tab-Reload startet wieder Bronze. { since: "2026-05-26", module: "16", aspect: "Mycel-Aktivität (erster Hyphen-Verkehr)", description: "Diese Zelle hatte in dieser Sitzung mindestens einen Hyphen-Verkehr (erfolgreicher Cross-Knoten-Handshake) — Leben im Mycel. SIEGEL-Stufe Gold.", }, { since: "2026-06-07", module: "16", aspect: "Semantische Selbst-Beschreibung im Siegel", description: "Direkt im Siegel lässt sich die App in eigenen Worten (oder per eingefügter README) beschreiben; der Text wird per Modul 03 (e5-small, 384-dim, L2-normalisiert) zum Domain-Vektor und mit dem vorhandenen Schlüssel neu in die Spore signiert — gleiche nodeId, treffenderer verified-match. Ein einziger, sauberer Identitäts-/Andock-Pfad ohne Modul-18-Verweis.", }, { since: "2026-06-20", module: "20", aspect: "Schlüssel-Tresor (Identitäts-Sicherung)", description: "Die SBKIM-Identität (nodeId + privater Knotenschlüssel + Spore) wird lokal verschlüsselt gesichert (Modul-02-Krypto-Kern: PBKDF2-SHA256 ≥600k + AES-GCM-256), mit Shamir-Recovery 2 von 3 über das Passwort — gegen Identitäts-Verlust/-Wandern. Nur Identität/Schlüssel, kein PII, nie übers Netz.", }, { since: "2026-07-01", module: "15", aspect: "KI-Richter im Cross-Knoten-Antwort-Pfad (opt-in)", description: "Der op:\"query\"-Empfänger (Membran Sub b) kann eingehende Fremd-Anfragen optional durch den KI-Richter (Modul 04 queryLocalJudged, BYOK) nach Bedeutung beurteilen und sortieren, statt nur nach rohem Cosinus. Default AUS (roher Vorfilter), Schlüssel RAM-only/nie im Code, fail-soft; der 0.80-Andock-Riegel bleibt unberührt.", }, { since: "2026-07-11", module: "16", aspect: "Ehrliche Aspekt-Darstellung (protokoll-weit, eingeklappt)", description: "Die Aspekte-Historie wird jetzt eingeklappt gezeigt und ausdrücklich als gemeinsame, netzweite SBKIM-Protokoll-Historie beschriftet — NICHT als app-eigenes Zertifizierungsdatum (Klaus-Befund: eine frisch zertifizierte App darf keine fremden Bau-Daten als ihre eigene Historie ausgeben). Die app-eigene, aktuelle Aussage bleibt die Pflicht-Modul-Selbstprüfung oben plus der lokale Start-Zeitpunkt.", }, { since: "2026-07-11", module: "20", aspect: "Verschlüsselte Geheimnis-Ablage im Safe (putSecret/getSecret)", description: "Der Safe kann jetzt beliebige kleine Geheimnisse (z.B. einen KI-Richter-API-Schlüssel) verschlüsselt ablegen — PBKDF2-SHA256 600k → AES-GCM-256, frisches Salt/IV je Geheimnis, kein Klartext in localStorage/IndexedDB. So überlebt ein Schlüssel Reload/App-Schließen, ohne dass eine andere App auf der geteilten Adresse ihn lesen kann (Fremdnutzer-/Marktplatz-Leitsatz). Falsches Passwort/Manipulation → fail-soft (null).", }, { since: "2026-07-29", module: "23", aspect: "Echtheit der Karten im gemeinsamen Raum", description: "Jede Visitenkarte im Rendezvous-Raum wird jetzt geprüft, bevor sie angezeigt wird: die Karte muss ihre eigene Spore tragen (spore.id === nodeId) und deren Ed25519-Signatur muss über Modul 02 (verifyForeignSpore) halten. Damit kann sich niemand mehr unter fremder Identität ins Brett hängen. Dazu Mengen-Deckel je Durchlauf (200 Karten) und je Nostr-Absender (3 Identitäten) gegen Flutung. Fehlt der Prüfer, läuft der Raum weiter, meldet die Karten aber ehrlich als UNGEPRÜFT statt sie still durchzuwinken. Reine Vor-Prüfung — der 0.80-Andock-Riegel bleibt unberührt.", }, { since: "2026-08-01", module: "15", aspect: "Nachvollziehbares Fremdzugriff-Protokoll", description: "Anlass war ein echter Fund im Feld: die FREMD-Lampe stand auf Rot, das Fenster sagte nur „ignored“ — die Membran hatte etwas abgewiesen, aber WER gesendet hatte, war nicht zu erkennen. Jeder Eintrag führt jetzt mit, welcher der vier gleich aussehenden Abweis-Gründe zutraf (fremder Typ / nicht erlaubt / fehlende Kennung / unbekannter Wunsch / gedrosselt), wer abgeschickt hat (Fenster, eingebetteter Rahmen, öffnendes Fenster), wofür sich die Nachricht ausgab, wie lange nach dem Laden sie kam und ob der Tab dabei vorn war. Das Fenster erklärt das in ganzen Sätzen. PII-Tabu bleibt hart: keine Werte aus fremden Objekten, nur Feld-Namen, Text-Auszug gekappt und Ziffernfolgen maskiert, alles RAM-only und nie übers Netz.", }, { since: "2026-08-14", module: "15", aspect: "Fachworte der App beim Antworten (abschaltbar, standardmäßig aus)", description: "Ehrlich vorweg: das ist keine Schutz-Verbesserung, sondern eine Erweiterung am Antwort-Pfad der Membran — sie steht hier, weil jede Änderung an einem Schutz-Modul sichtbar bleiben soll, auch die harmlose. Fragt ein anderer Knoten etwas an, konnte bisher nur die reine Bedeutungs-Suche antworten; wer nach „Faktura“ fragte, fand nichts über „Rechnung“. Eine App kann jetzt ihre eigenen Fachworte mitgeben, dann wird die Frage vor dem Suchen aufgefächert und zusätzlich nach Wörtern gesucht. Herkunft: BookLedgerPro hatte das seit dem 2026-07-11 in seiner eigenen Kopie stehen — an der falschen Stelle, weil eine Kopie nicht geändert werden darf; die Mechanik ist deshalb in den Kanon gewandert, die Fachworte bleiben bei der App. Standardmäßig AUS: wer nichts einstellt, bekommt exakt den bisherigen Weg. Es werden nur zusätzliche Treffer AUFGENOMMEN — der 0.80-Andock-Riegel bleibt unberührt, hier wird geantwortet, nicht angedockt.", }, { since: "2026-08-16", module: "05b", aspect: "Relais-Client gehört zur Selbst-Prüfung", description: "Das Siegel prüft ab jetzt auch den Relais-Client (Modul 05b). Vorher konnte es golden leuchten, während der Knoten den gemeinsamen Raum gar nicht lesen konnte. Geprüft wird die Fläche subscribe: wer nur senden, aber nichts empfangen kann, nimmt nicht teil.", }, { since: "2026-10-14", module: "23", aspect: "Echtheit im Raum wirkt wieder, Antworten sind unterschrieben", description: "Ein Befund vom 2026-10-14: die Prüfung der Visitenkarten im gemeinsamen Raum las die Antwort von Modul 02 falsch und ließ jede Karte durch, auch eine mit gefälschter Unterschrift. Das ist behoben, und die Prüfung steht jetzt vor dem Zusammenfassen nach Namen, damit eine gefälschte Karte die echte nicht mehr verdrängt. Ein Name bleibt ungeprüft; trägt eine andere Kennung denselben Namen, steht das an der Karte. Antworten auf eine Frage tragen jetzt die Spore des Antworters und seine Unterschrift; genommen wird nur die des gefragten Knotens. Eine Antwort ohne Unterschrift (ältere Fassung) erscheint als „unbestätigt“ und geht nicht an den KI-Richter.", }, { since: "2026-10-15", module: "23", aspect: "Fragen sind unterschrieben (S1)", description: "Eine Frage an einen anderen Knoten trägt jetzt die Spore des Fragers und seine Unterschrift, wie die Antworten seit dem 2026-10-14. Der gefragte Knoten prüft sie: eine gefälschte oder nachträglich veränderte Frage wird nicht beantwortet. Eine Frage ohne Unterschrift (ältere Fassung) wird weiter beantwortet, aber höchstens zwei solche Fragen je Minute; unterschriebene Fragen haben Vorrang.", }, { since: "2026-10-15", module: "23", aspect: "Die Bedeutung wird nachgerechnet (S2)", description: "Jeder Knoten sagt mit einem Bedeutungs-Vektor, worum es bei ihm geht. Bisher ließ sich dieser Vektor von einem anderen Knoten abschreiben, ohne dass es auffiel. Jetzt rechnet der Empfänger ihn aus der Beschreibung der Karte nach, sobald sein Sprachmodell ohnehin geladen ist. Passt er nicht, steht an der Karte „Bedeutung passt nicht zur Beschreibung“, und der Knoten bekommt beim Suchen keinen Vorrang; sichtbar bleibt er. Ist der Vektor aus den Inhalten der App gerechnet, lässt er sich nicht nachrechnen und wird so benannt, ohne Abzug. Ohne geladenes Sprachmodell steht dort „nicht geprüft“, nie still ein Haken.", }, { since: "2026-10-15", module: "23", aspect: "Knoten können füreinander bürgen (S5)", description: "Ein Knoten kann für einen anderen unterschreiben: „Diese Kennung kenne ich.“ Vorher vergleichen beide eine Sicherheitsnummer auf einem zweiten Weg, etwa am Telefon. Eine Bürgschaft zählt bei dir nur, wenn du dem Bürgen schon traust: dem Netzadmin deines Netzes oder einem eigenen Kontakt, höchstens zwei Schritte weit und befristet. Ein Bürge kann zurückziehen. Im offenen Mycel ist Sage-Protokol der Netzadmin, in einem eigenen Netz dessen Betreiber. Bei einer Frage stehen verbürgte Karten vorn; unverbürgte bleiben sichtbar und werden weiter gefragt.", }, { since: "2026-10-16", module: "23", aspect: "Fluten wird teurer (S3)", description: "Wer eine Visitenkarte in den gemeinsamen Raum hängt oder einem anderen Knoten eine Frage stellt, rechnet vorher eine kleine Prüfsummen-Aufgabe. Für eine einzelne Karte dauert das am Gerät nur einen Augenblick; wer den Raum mit Hunderten Karten fluten will, muss für jede rechnen. Abgewiesen wird nichts: Ist der Raum voll, verdrängt eine Karte mit Rechenaufwand eine ohne, und Fragen ohne Aufwand bekommen höchstens vier der sechs Antworten je Minute. Gegen einen Angreifer mit schnellem Rechner ist das nur eine kleine Hürde; dafür gibt es die Unterschriften und das Bürgen.", }, { since: "2026-10-16", module: "23", aspect: "Gesperrte Knoten verschwinden aus dem Netz (S4)", description: "Der Netzadmin eines Netzes kann eine Kennung sperren; im offenen Mycel ist das Sage-Protokol. Die Sperrliste ist öffentlich und unterschrieben und reist auf den Visitenkarten im Raum mit, jede App prüft die Unterschrift selbst. Eine gesperrte Kennung erscheint nicht mehr auf der Karte, ihre Fragen werden nicht beantwortet und ihre Antworten verworfen. Eine Liste ohne gültige Unterschrift des Netzadmins zählt nicht. Ein Gesperrter sieht, dass er gesperrt ist, und kann sich eine neue Kennung machen; dagegen helfen der Rechenaufwand und das Bürgen.", }, { since: "2026-10-16", module: "23", aspect: "Bürgen für fremde Apps: befristet, mit Grund, je App (S5b)", description: "Eine Bürgschaft gilt jetzt höchstens 180 Tage; 30 Tage vor dem Ablauf zeigt die verbürgte App einen Hinweis, und verlängern kann nur der Bürge. Wer eine Bürgschaft zurückzieht, muss einen Grund angeben, der mit unterschrieben wird; ein Widerruf ohne Grund zählt nicht. Fällt eine verbürgte App auf oder wird gesperrt, verfällt nur ihre eigene Bürgschaft, die übrigen des Bürgen gelten weiter. Jede App merkt sich gesehene Widerrufe, auch wenn der Bürge nicht mehr im Raum ist. Neu ist die Einstellung „Nur Verbürgten antworten“: Fragen von Knoten, die bei dir weder Netzadmin noch Kontakt noch verbürgt sind, bleiben dann ohne Antwort.", }, { since: "2026-10-16", module: "23", aspect: "Empfehlungen kommen von selbst an (S5b Teil 2)", description: "„Bürgen“ heißt in der Oberfläche jetzt „Empfehlen“. Eine Empfehlung muss nicht mehr als Text weitergegeben werden: die Karte dessen, der empfiehlt, trägt sie im Raum mit, und die empfohlene App holt sie beim nächsten „Wer ist im Raum?“ selbst ab. Abgeholt wird nur, was gültig unterschrieben ist und von jemandem kommt, der bei der App schon zählt (Netzadmin, eigener Kontakt oder selbst empfohlen); ein Unbekannter kann so niemandes Vorrat füllen. Die Netz-Verwaltung ist eine eigene Seite im Studio: Apps im Mycel, eigene Empfehlungen, Widerrufe mit Grund und die Sperrliste, mit Knöpfen zum Empfehlen, Verlängern, Widerrufen und Zurücknehmen.", }, ]; // ---- Aspekt-4-Anker (Karte 16 § Sub (e) dynamische Render-Variante) ---- // // Eindeutige Identifikation des „Mycel-Aktivitäts"-Aspekts // in der ZERTIFIKAT_ASPEKTE-Liste; das Modal rendert ihn in Bronze mit // „pending"-Marker statt Datum. var ASPEKT_4_SINCE = "2026-05-26"; var ASPEKT_4_MODULE = "16"; var ASPEKT_4_TITLE_PREFIX = "Mycel-Aktivität"; function isAspect4(a) { return a && a.since === ASPEKT_4_SINCE && a.module === ASPEKT_4_MODULE && typeof a.aspect === "string" && a.aspect.indexOf(ASPEKT_4_TITLE_PREFIX) === 0; } // ---- Konstanten ---- /* ── SPRACHE (Klaus 2026-09-14) ──────────────────────────────────────── * * WARUM. Nach dem Modul-23-UI-Rollout stand Klaus' Seite auf Englisch — und * das Siegel daneben auf Deutsch. Ein Voll-Knoten war damit * gemischtsprachig: das Verbinden-Fenster englisch, die Lampe „siegel" und * das ganze Modal dahinter deutsch. * * WIE. Byte-gleiches Verfahren wie Modul 23 UI: SCHLUESSELLOS. `T("…")` * nimmt den deutschen Satz und gibt die Uebersetzung zurueck, wenn es eine * gibt. Kein Schluessel-System — ein Schluessel und sein Text laufen * auseinander, sobald einer von beiden sich bewegt, und dann steht im Modal * ein Schluessel statt eines Satzes. * * FAIL-SOFT UND RUECKWAERTSKOMPATIBEL: fehlt ein Eintrag, bleibt es deutsch. * Das ist der Zustand vor dieser Aenderung — neunzehn Apps tragen dieses * Modul byte-1:1, und keine davon darf davon etwas merken, solange sie * nichts einstellt. * * ⚠ DIE ASPEKTE-LISTE BLEIBT DEUTSCH IM CODE. `ZERTIFIKAT_ASPEKTE` ist * netzweiter Protokoll-Bestand; ihr deutscher Wortlaut ist die Urkunde. * Uebersetzt wird erst an der ANZEIGE-Stelle (T(a.aspect) / * T(a.description)). Wer einen Aspekt ERGAENZT (CLAUDE.md § Sicherheits- * Module pflegen Aspekte), traegt seine englische Fassung hier mit ein — * `tests/smoke_bau1617_sprache.mjs` besteht darauf. * * ⚠ KEIN ZERTIFIKAT_ASPEKTE-EINTRAG FUER DIESE AENDERUNG, obwohl 16 ein * Schutz-Modul ist und die Konvention sonst greift: eine Uebersetzung ist * Render-Schicht. Sie ruehrt weder die Selbst-Pruefung noch die * Anti-Greenwashing-Klausel noch die Bronze/Gold-Stufung an. Ein Eintrag * „spricht jetzt Englisch" behauptete einen Sicherheits-Fortschritt, den es * nicht gibt — und verwaesserte die Liste, die Sicherheits-Updates sichtbar * machen soll. Begruendung ausgeschrieben in INTERFACES § Modul 16 SPRACHE. */ var TEXTE = { en: { /* Die zwei Anzeigetexte im WAPPEN (Klaus 2026-09-14). „SEAL" ist mit * Modul 17 abgestimmt (dort heisst der Lampen-Slot ebenso) — * zwei Fassungen desselben Wortes liefen sonst auseinander. */ "OFFIZIELLE BESTÄTIGUNG": "OFFICIAL ATTESTATION", "SIEGEL": "SEAL", /* Modal-Rahmen */ "SBKIM-Siegel — was bedeutet das?": "The SBKIM seal — what does it mean?", "Schließen": "Close", "Pflicht-Module": "Required modules", "SBKIM-Protokoll — Sicherheits-Aspekte (gemeinsam, netzweit)": "SBKIM protocol — security aspects (shared, network-wide)", "Historie der SBKIM-Sicherheits-Bausteine — für alle Knoten gleich. Das sind Protokoll-Daten, keine app-eigenen Zertifizierungs-Daten dieser App.": "History of the SBKIM security building blocks — the same for every node. These are protocol dates, not this app's own certification dates.", "pending": "pending", /* Badge */ "SBKIM-Siegel · im Mycel, ruhend": "SBKIM seal · in the mycelium, dormant", "SBKIM-Siegel · im Mycel, aktiv": "SBKIM seal · in the mycelium, active", /* Bezeugt-Zeile. „ Uhr." hat im Englischen keine Entsprechung — dort * endet der Satz schlicht mit einem Punkt. */ "Bezeugt seit ": "Attested since ", " Uhr.": ".", "Bezeugt: —": "Attested: —", /* Status-Etiketten der Pflicht-Modul-Liste */ "bereit": "ready", "bereit (lazy)": "ready (lazy)", "fehlt": "missing", "defekt": "broken", /* Die deutschen Modul-NAMEN aus PFLICHT_MODULE. Storage, Spore, * Embedding und Match stehen nicht hier: sie sind in beiden Sprachen * dasselbe Wort, und eine „Uebersetzung", die eine Kopie des Deutschen * ist, ist keine. */ "Relais-Client": "Relay client", "Anastomose": "Anastomosis", "Apoptose": "Apoptosis", "Membran": "Membrane", /* Bronze-Hinweis */ "Im Mycel · ruhend": "In the mycelium · dormant", " — diese Zelle trägt das SBKIM-Siegel und ist damit Teil des Mycels, so klein sie auch ist. In dieser Sitzung gab es noch keinen Hyphen-Verkehr (Handshake). Über den Knopf „🔑 Eigene Identität & Spore erzeugen / verwalten →“ oben knüpfst du aktiv einen Faden zu einem Geschwister-Knoten.": " — this cell carries the SBKIM seal and is therefore part of the mycelium, however small it may be. There has been no hyphal traffic (handshake) in this session yet. Use the “🔑 Create / manage your own identity & spore →” button above to actively spin a thread to a sibling node.", /* Aussteller-Klaerung */ "Dieses Siegel ist ": "This seal is ", "self-inscribing": "self-inscribing", ": die App hat sich beim Boot selbst geprüft.": ": the app checked itself at boot time.", "Vertrauen kommt vom Repo, in dem sie gehostet ist: ": "Trust comes from the repository it is hosted in: ", /* Andock-Block */ "Fremden Knoten verbinden — ohne KI, direkt im Browser: Repo-/App-URL eingeben → Spore prüfen → Match → Handshake.": "Connect a foreign node — no AI, straight in the browser: enter the repo/app URL → check the spore → match → handshake.", "🔌 Fremden Knoten andocken →": "🔌 Dock a foreign node →", "Andock-Werkzeug (Modul 18 SbkimToolPwa) ist nicht geladen — bitte src/modules/18_tool_pwa.js einbinden und SbkimToolPwa.init({…}) aufrufen.": "The docking tool (module 18 SbkimToolPwa) is not loaded — please include src/modules/18_tool_pwa.js and call SbkimToolPwa.init({…}).", "Andock-Werkzeug nicht bereit: ": "Docking tool not ready: ", /* ── ZERTIFIKAT_ASPEKTE ─────────────────────────────────────────── * Die Daten oben bleiben deutsch; hier steht ihre englische Fassung. * Wer einen Aspekt ergaenzt, ergaenzt ihn HIER MIT. */ "Grund-Siegel-Bezeugung (Protokoll)": "Foundational seal attestation (protocol)", "Grundlage jedes SBKIM-Siegels: Beim Start prüft die App selbst, ob die Pflicht-Module 01/02/03/04/05/07/15 geladen sind — nur dann entsteht das Siegel. Protokoll-Baustein, kein app-eigenes Datum (was DIESE App gerade lädt, steht oben unter „Pflicht-Module“).": "The basis of every SBKIM seal: at start-up the app checks for itself whether the required modules 01/02/03/04/05/07/15 are loaded — only then does the seal come into being. A protocol building block, not a date of this app's own (what THIS app is loading right now is listed above under “Required modules”).", "Sub (a) Read-API + Sub (b) postMessage-Brücke": "Sub (a) read API + sub (b) postMessage bridge", "Finale Bedien-Pfade: MembraneSnapshot mit Siegel-Hook, vier op-Werte (sporeRef/query/hint/queryResult) mit Nonce-Pflicht, fail-soft Allowlist, Rate-Limit-Hook für Modul 11.": "Final operating paths: MembraneSnapshot with a seal hook, four op values (sporeRef/query/hint/queryResult) requiring a nonce, a fail-soft allowlist, and a rate-limit hook for module 11.", "Floating-Widget mit Vier-Slot-Live-Status": "Floating widget with four-lamp live status", "Live-Status-Dashboard (LEBT/VERKEHR/FREMD/SIEGEL) als Endknoten-Standard; macht den SBKIM-Lauf sichtbar ohne Navleisten-Mount-Pflicht. Render-Schicht ohne Protokoll-Eingriff.": "A live status dashboard (ALIVE/TRAFFIC/FOREIGN/SEAL) as the end-node standard; it makes SBKIM visibly running without requiring a nav-bar mount. Render layer, no interference with the protocol.", "Mycel-Aktivität (erster Hyphen-Verkehr)": "Mycelium activity (first hyphal traffic)", "Diese Zelle hatte in dieser Sitzung mindestens einen Hyphen-Verkehr (erfolgreicher Cross-Knoten-Handshake) — Leben im Mycel. SIEGEL-Stufe Gold.": "In this session this cell had at least one instance of hyphal traffic (a successful cross-node handshake) — life in the mycelium. Seal tier: gold.", "Semantische Selbst-Beschreibung im Siegel": "Semantic self-description inside the seal", "Direkt im Siegel lässt sich die App in eigenen Worten (oder per eingefügter README) beschreiben; der Text wird per Modul 03 (e5-small, 384-dim, L2-normalisiert) zum Domain-Vektor und mit dem vorhandenen Schlüssel neu in die Spore signiert — gleiche nodeId, treffenderer verified-match. Ein einziger, sauberer Identitäts-/Andock-Pfad ohne Modul-18-Verweis.": "The app can be described in its own words (or by pasting in a README) straight inside the seal; module 03 (e5-small, 384-dim, L2-normalised) turns that text into the domain vector and signs it back into the spore with the existing key — same nodeId, a more accurate verified match. One single clean identity/docking path, with no reference to module 18.", "Schlüssel-Tresor (Identitäts-Sicherung)": "Key safe (identity backup)", "Die SBKIM-Identität (nodeId + privater Knotenschlüssel + Spore) wird lokal verschlüsselt gesichert (Modul-02-Krypto-Kern: PBKDF2-SHA256 ≥600k + AES-GCM-256), mit Shamir-Recovery 2 von 3 über das Passwort — gegen Identitäts-Verlust/-Wandern. Nur Identität/Schlüssel, kein PII, nie übers Netz.": "The SBKIM identity (nodeId + private node key + spore) is backed up locally in encrypted form (module 02 crypto core: PBKDF2-SHA256 ≥600k + AES-GCM-256), with 2-of-3 Shamir recovery over the password — against losing or drifting the identity. Identity and keys only, no personal data, never over the network.", "KI-Richter im Cross-Knoten-Antwort-Pfad (opt-in)": "AI judge in the cross-node reply path (opt-in)", "Der op:\"query\"-Empfänger (Membran Sub b) kann eingehende Fremd-Anfragen optional durch den KI-Richter (Modul 04 queryLocalJudged, BYOK) nach Bedeutung beurteilen und sortieren, statt nur nach rohem Cosinus. Default AUS (roher Vorfilter), Schlüssel RAM-only/nie im Code, fail-soft; der 0.80-Andock-Riegel bleibt unberührt.": "The op:\"query\" receiver (membrane sub b) can optionally have incoming foreign requests judged and ranked by meaning through the AI judge (module 04 queryLocalJudged, BYOK) instead of by raw cosine alone. Off by default (raw pre-filter), the key stays in RAM and never in the code, fail-soft; the 0.80 docking bar is untouched.", "Ehrliche Aspekt-Darstellung (protokoll-weit, eingeklappt)": "Honest presentation of the aspects (protocol-wide, collapsed)", "Die Aspekte-Historie wird jetzt eingeklappt gezeigt und ausdrücklich als gemeinsame, netzweite SBKIM-Protokoll-Historie beschriftet — NICHT als app-eigenes Zertifizierungsdatum (Klaus-Befund: eine frisch zertifizierte App darf keine fremden Bau-Daten als ihre eigene Historie ausgeben). Die app-eigene, aktuelle Aussage bleibt die Pflicht-Modul-Selbstprüfung oben plus der lokale Start-Zeitpunkt.": "The aspect history is now shown collapsed and labelled explicitly as the shared, network-wide SBKIM protocol history — NOT as a certification date of this app's own (Klaus's finding: a freshly certified app must not present other people's build dates as its own history). What this app itself currently states remains the required-module self-check above plus the local start time.", "Verschlüsselte Geheimnis-Ablage im Safe (putSecret/getSecret)": "Encrypted secret storage in the safe (putSecret/getSecret)", "Der Safe kann jetzt beliebige kleine Geheimnisse (z.B. einen KI-Richter-API-Schlüssel) verschlüsselt ablegen — PBKDF2-SHA256 600k → AES-GCM-256, frisches Salt/IV je Geheimnis, kein Klartext in localStorage/IndexedDB. So überlebt ein Schlüssel Reload/App-Schließen, ohne dass eine andere App auf der geteilten Adresse ihn lesen kann (Fremdnutzer-/Marktplatz-Leitsatz). Falsches Passwort/Manipulation → fail-soft (null).": "The safe can now store arbitrary small secrets (an AI-judge API key, say) in encrypted form — PBKDF2-SHA256 600k → AES-GCM-256, a fresh salt/IV per secret, no plain text in localStorage/IndexedDB. A key therefore survives a reload or closing the app, without another app on the shared address being able to read it (the outside-user/marketplace principle). A wrong password or tampering → fail-soft (null).", "Echtheit der Karten im gemeinsamen Raum": "Authenticity of the cards in the shared room", "Jede Visitenkarte im Rendezvous-Raum wird jetzt geprüft, bevor sie angezeigt wird: die Karte muss ihre eigene Spore tragen (spore.id === nodeId) und deren Ed25519-Signatur muss über Modul 02 (verifyForeignSpore) halten. Damit kann sich niemand mehr unter fremder Identität ins Brett hängen. Dazu Mengen-Deckel je Durchlauf (200 Karten) und je Nostr-Absender (3 Identitäten) gegen Flutung. Fehlt der Prüfer, läuft der Raum weiter, meldet die Karten aber ehrlich als UNGEPRÜFT statt sie still durchzuwinken. Reine Vor-Prüfung — der 0.80-Andock-Riegel bleibt unberührt.": "Every calling card in the rendezvous room is now checked before it is shown: the card has to carry its own spore (spore.id === nodeId) and that spore's Ed25519 signature has to hold up under module 02 (verifyForeignSpore). Nobody can hang themselves on the board under a foreign identity any more. On top of that there are volume caps per pass (200 cards) and per Nostr sender (3 identities) against flooding. If the verifier is missing, the room carries on but honestly reports the cards as UNCHECKED instead of waving them through in silence. A pure pre-check — the 0.80 docking bar is untouched.", "Nachvollziehbares Fremdzugriff-Protokoll": "A foreign-access log you can actually follow", "Anlass war ein echter Fund im Feld: die FREMD-Lampe stand auf Rot, das Fenster sagte nur „ignored“ — die Membran hatte etwas abgewiesen, aber WER gesendet hatte, war nicht zu erkennen. Jeder Eintrag führt jetzt mit, welcher der vier gleich aussehenden Abweis-Gründe zutraf (fremder Typ / nicht erlaubt / fehlende Kennung / unbekannter Wunsch / gedrosselt), wer abgeschickt hat (Fenster, eingebetteter Rahmen, öffnendes Fenster), wofür sich die Nachricht ausgab, wie lange nach dem Laden sie kam und ob der Tab dabei vorn war. Das Fenster erklärt das in ganzen Sätzen. PII-Tabu bleibt hart: keine Werte aus fremden Objekten, nur Feld-Namen, Text-Auszug gekappt und Ziffernfolgen maskiert, alles RAM-only und nie übers Netz.": "The trigger was a real finding in the field: the FOREIGN lamp stood red, the window said only “ignored” — the membrane had turned something away, but WHO had sent it was impossible to tell. Every entry now carries which of the four identical-looking rejection reasons applied (foreign type / not allowed / missing identifier / unknown request / throttled), who sent it (window, embedded frame, opening window), what the message claimed to be, how long after loading it arrived, and whether the tab was in the foreground at the time. The window explains all that in whole sentences. The ban on personal data stays hard: no values out of foreign objects, field names only, text excerpts truncated and digit sequences masked, everything RAM-only and never over the network.", "Fachworte der App beim Antworten (abschaltbar, standardmäßig aus)": "The app's own terminology when answering (switchable, off by default)", "Ehrlich vorweg: das ist keine Schutz-Verbesserung, sondern eine Erweiterung am Antwort-Pfad der Membran — sie steht hier, weil jede Änderung an einem Schutz-Modul sichtbar bleiben soll, auch die harmlose. Fragt ein anderer Knoten etwas an, konnte bisher nur die reine Bedeutungs-Suche antworten; wer nach „Faktura“ fragte, fand nichts über „Rechnung“. Eine App kann jetzt ihre eigenen Fachworte mitgeben, dann wird die Frage vor dem Suchen aufgefächert und zusätzlich nach Wörtern gesucht. Herkunft: BookLedgerPro hatte das seit dem 2026-07-11 in seiner eigenen Kopie stehen — an der falschen Stelle, weil eine Kopie nicht geändert werden darf; die Mechanik ist deshalb in den Kanon gewandert, die Fachworte bleiben bei der App. Standardmäßig AUS: wer nichts einstellt, bekommt exakt den bisherigen Weg. Es werden nur zusätzliche Treffer AUFGENOMMEN — der 0.80-Andock-Riegel bleibt unberührt, hier wird geantwortet, nicht angedockt.": "Honestly, up front: this is not an improvement in protection but an extension to the membrane's reply path — it is listed here because every change to a protective module should stay visible, the harmless ones included. When another node asks something, only the pure meaning search could answer until now; ask for “invoicing” and you found nothing filed under “invoice”. An app can now supply its own terminology, and the question is then fanned out before the search and additionally searched for by word. Origin: BookLedgerPro had this in its own copy since 2026-07-11 — in the wrong place, because a copy must not be modified; the mechanism has therefore moved into the canon while the terminology stays with the app. Off by default: change nothing and you get exactly the previous path. Only ADDITIONAL hits are taken in — the 0.80 docking bar is untouched, this is about answering, not about docking.", "Relais-Client gehört zur Selbst-Prüfung": "The relay client belongs to the self-check", "Das Siegel prüft ab jetzt auch den Relais-Client (Modul 05b). Vorher konnte es golden leuchten, während der Knoten den gemeinsamen Raum gar nicht lesen konnte. Geprüft wird die Fläche subscribe: wer nur senden, aber nichts empfangen kann, nimmt nicht teil.": "From now on the seal also checks the relay client (module 05b). Before, it could shine gold while the node could not read the shared room at all. The surface checked is subscribe: anyone who can only send but receive nothing is not taking part.", "Echtheit im Raum wirkt wieder, Antworten sind unterschrieben": "Authenticity in the room works again, answers are signed", "Ein Befund vom 2026-10-14: die Prüfung der Visitenkarten im gemeinsamen Raum las die Antwort von Modul 02 falsch und ließ jede Karte durch, auch eine mit gefälschter Unterschrift. Das ist behoben, und die Prüfung steht jetzt vor dem Zusammenfassen nach Namen, damit eine gefälschte Karte die echte nicht mehr verdrängt. Ein Name bleibt ungeprüft; trägt eine andere Kennung denselben Namen, steht das an der Karte. Antworten auf eine Frage tragen jetzt die Spore des Antworters und seine Unterschrift; genommen wird nur die des gefragten Knotens. Eine Antwort ohne Unterschrift (ältere Fassung) erscheint als „unbestätigt“ und geht nicht an den KI-Richter.": "A finding from 2026-10-14: the check of the calling cards in the shared room misread the reply of module 02 and let every card through, including one with a forged signature. That is fixed, and the check now runs before cards are merged by name, so a forged card can no longer push out the real one. A name stays unverified; if another identifier carries the same name, the card says so. Answers to a question now carry the answering node's spore and its signature; only the answer of the node that was asked is taken. An answer without a signature (older version) appears as “unconfirmed” and does not go to the AI judge.", "Fragen sind unterschrieben (S1)": "Questions are signed (S1)", "Eine Frage an einen anderen Knoten trägt jetzt die Spore des Fragers und seine Unterschrift, wie die Antworten seit dem 2026-10-14. Der gefragte Knoten prüft sie: eine gefälschte oder nachträglich veränderte Frage wird nicht beantwortet. Eine Frage ohne Unterschrift (ältere Fassung) wird weiter beantwortet, aber höchstens zwei solche Fragen je Minute; unterschriebene Fragen haben Vorrang.": "A question to another node now carries the asker's spore and signature, like the answers since 2026-10-14. The node being asked checks it: a forged or afterwards altered question is not answered. A question without a signature (older version) is still answered, but at most two such questions per minute; signed questions take precedence.", "Die Bedeutung wird nachgerechnet (S2)": "The meaning is recomputed (S2)", "Jeder Knoten sagt mit einem Bedeutungs-Vektor, worum es bei ihm geht. Bisher ließ sich dieser Vektor von einem anderen Knoten abschreiben, ohne dass es auffiel. Jetzt rechnet der Empfänger ihn aus der Beschreibung der Karte nach, sobald sein Sprachmodell ohnehin geladen ist. Passt er nicht, steht an der Karte „Bedeutung passt nicht zur Beschreibung“, und der Knoten bekommt beim Suchen keinen Vorrang; sichtbar bleibt er. Ist der Vektor aus den Inhalten der App gerechnet, lässt er sich nicht nachrechnen und wird so benannt, ohne Abzug. Ohne geladenes Sprachmodell steht dort „nicht geprüft“, nie still ein Haken.": "Every node states what it is about with a meaning vector. Until now another node could copy that vector without anyone noticing. Now the receiver recomputes it from the card's description whenever its language model is loaded anyway. If it does not match, the card says “Meaning does not match the description”, and the node gets no priority in searches; it stays visible. If the vector was computed from the app's content, it cannot be recomputed and is labelled as such, without any penalty. Without a loaded language model the card says “not checked”, never a silent tick.", "Knoten können füreinander bürgen (S5)": "Nodes can vouch for each other (S5)", "Ein Knoten kann für einen anderen unterschreiben: „Diese Kennung kenne ich.“ Vorher vergleichen beide eine Sicherheitsnummer auf einem zweiten Weg, etwa am Telefon. Eine Bürgschaft zählt bei dir nur, wenn du dem Bürgen schon traust: dem Netzadmin deines Netzes oder einem eigenen Kontakt, höchstens zwei Schritte weit und befristet. Ein Bürge kann zurückziehen. Im offenen Mycel ist Sage-Protokol der Netzadmin, in einem eigenen Netz dessen Betreiber. Bei einer Frage stehen verbürgte Karten vorn; unverbürgte bleiben sichtbar und werden weiter gefragt.": "A node can sign for another one: “I know this identifier.” Before that, both compare a safety number over a second channel, for example by phone. A vouch only counts for you if you already trust the voucher: the network admin of your network or an own contact, at most two steps away and for a limited time. A voucher can withdraw. In the open mycelium Sage-Protokol is the network admin; in an own network it is whoever runs it. When you ask, vouched cards come first; cards without a vouch stay visible and are still asked.", "Fluten wird teurer (S3)": "Flooding costs more (S3)", "Wer eine Visitenkarte in den gemeinsamen Raum hängt oder einem anderen Knoten eine Frage stellt, rechnet vorher eine kleine Prüfsummen-Aufgabe. Für eine einzelne Karte dauert das am Gerät nur einen Augenblick; wer den Raum mit Hunderten Karten fluten will, muss für jede rechnen. Abgewiesen wird nichts: Ist der Raum voll, verdrängt eine Karte mit Rechenaufwand eine ohne, und Fragen ohne Aufwand bekommen höchstens vier der sechs Antworten je Minute. Gegen einen Angreifer mit schnellem Rechner ist das nur eine kleine Hürde; dafür gibt es die Unterschriften und das Bürgen.": "Whoever posts a calling card to the shared room or asks another node a question first solves a small checksum puzzle. For a single card this takes only a moment on the device; whoever wants to flood the room with hundreds of cards has to compute each one. Nothing is rejected: when the room is full, a card with computing effort pushes out one without, and questions without effort get at most four of the six answers per minute. Against an attacker with a fast computer this is only a small hurdle; signatures and vouching are there for that.", "Gesperrte Knoten verschwinden aus dem Netz (S4)": "Blocked nodes disappear from the network (S4)", "Der Netzadmin eines Netzes kann eine Kennung sperren; im offenen Mycel ist das Sage-Protokol. Die Sperrliste ist öffentlich und unterschrieben und reist auf den Visitenkarten im Raum mit, jede App prüft die Unterschrift selbst. Eine gesperrte Kennung erscheint nicht mehr auf der Karte, ihre Fragen werden nicht beantwortet und ihre Antworten verworfen. Eine Liste ohne gültige Unterschrift des Netzadmins zählt nicht. Ein Gesperrter sieht, dass er gesperrt ist, und kann sich eine neue Kennung machen; dagegen helfen der Rechenaufwand und das Bürgen.": "The network admin of a network can block an identifier; in the open mycelium that is Sage-Protokol. The block list is public and signed and travels on the calling cards in the room; every app checks the signature itself. A blocked identifier no longer appears on the map, its questions are not answered and its answers are discarded. A list without a valid signature of the network admin does not count. A blocked node can see that it is blocked and can make itself a new identifier; computing effort and vouching help against that.", "Bürgen für fremde Apps: befristet, mit Grund, je App (S5b)": "Vouching for other apps: limited, with a reason, per app (S5b)", "Eine Bürgschaft gilt jetzt höchstens 180 Tage; 30 Tage vor dem Ablauf zeigt die verbürgte App einen Hinweis, und verlängern kann nur der Bürge. Wer eine Bürgschaft zurückzieht, muss einen Grund angeben, der mit unterschrieben wird; ein Widerruf ohne Grund zählt nicht. Fällt eine verbürgte App auf oder wird gesperrt, verfällt nur ihre eigene Bürgschaft, die übrigen des Bürgen gelten weiter. Jede App merkt sich gesehene Widerrufe, auch wenn der Bürge nicht mehr im Raum ist. Neu ist die Einstellung „Nur Verbürgten antworten“: Fragen von Knoten, die bei dir weder Netzadmin noch Kontakt noch verbürgt sind, bleiben dann ohne Antwort.": "A vouch now lasts at most 180 days; 30 days before it expires the vouched app shows a notice, and only the voucher can renew it. Whoever withdraws a vouch has to give a reason, which is signed along; a revocation without a reason does not count. If a vouched app is flagged or blocked, only its own vouch lapses; the voucher's other vouches stay valid. Every app remembers revocations it has seen, even when the voucher is no longer in the room. New is the setting “Answer only vouched nodes”: questions from nodes that are neither the network admin, nor a contact, nor vouched for you then get no answer.", "Empfehlungen kommen von selbst an (S5b Teil 2)": "Recommendations arrive by themselves (S5b part 2)", "„Bürgen“ heißt in der Oberfläche jetzt „Empfehlen“. Eine Empfehlung muss nicht mehr als Text weitergegeben werden: die Karte dessen, der empfiehlt, trägt sie im Raum mit, und die empfohlene App holt sie beim nächsten „Wer ist im Raum?“ selbst ab. Abgeholt wird nur, was gültig unterschrieben ist und von jemandem kommt, der bei der App schon zählt (Netzadmin, eigener Kontakt oder selbst empfohlen); ein Unbekannter kann so niemandes Vorrat füllen. Die Netz-Verwaltung ist eine eigene Seite im Studio: Apps im Mycel, eigene Empfehlungen, Widerrufe mit Grund und die Sperrliste, mit Knöpfen zum Empfehlen, Verlängern, Widerrufen und Zurücknehmen.": "“Vouching” is now called “recommending” in the interface. A recommendation no longer has to be passed on as text: the card of the one who recommends carries it in the room, and the recommended app picks it up itself at the next “Who is in the room?”. Only what is validly signed and comes from someone who already counts for the app (network admin, own contact or recommended itself) is picked up; a stranger cannot fill anyone's store this way. The network admin is a separate page in the studio: apps in the mycel, own recommendations, revocations with a reason and the block list, with buttons to recommend, extend, revoke and take back.", } }; /* Die gewaehlte Sprache. null = „nicht gesetzt" → entscheidet. */ var optLang = null; function sprache() { if (optLang === "de" || optLang === "en") return optLang; try { var d = global.document; var l = String((d && d.documentElement && d.documentElement.lang) || "").slice(0, 2).toLowerCase(); if (l === "en") return "en"; } catch (_e) { /* nb */ } return "de"; } /* Bei JEDEM Aufruf neu nachsehen, nicht einmal beim Laden merken: wer die * Sprache umschaltet und danach das Modal oeffnet, bekaeme sonst die alte. */ function T(de) { if (sprache() !== "en") return de; var w = TEXTE.en; return (w && Object.prototype.hasOwnProperty.call(w, de)) ? w[de] : de; } var DEFAULT_BADGE_SELECTOR = ".lamps"; // Container; Option β var BADGE_ID = "sbkim-siegel-badge"; var MODAL_ID = "sbkim-siegel-modal"; var MODAL_TITLE = "SBKIM-Siegel — was bedeutet das?"; var FIRST_BOOT_ANIMATION_MS = 600; var MOUNT_OBSERVER_TIMEOUT_MS = 10000; // Band-Text im Wappen (unteres Ribbon, SELF-INSCRIBING). DEFAULT_RIBBON_TEXT // ist der im inlined WAPPEN_SVG eingebackene Wert ("SAGE OBSERVATORIUM") — // er dient als (a) Sages explizite Marke und (b) No-Replace-Sentinel // (byte-identisch). Der Laufzeit-Default OHNE init({ribbonText}) ist NICHT // dieser Wert, sondern ein OFFENES (leeres) Band (Klaus-Entscheidung // 2026-06-20): kein geratenes Auto-Label, der Host graviert seinen Namen // bewusst ein. Befund-Anlass 2026-06-19: Rezeptbuch/Mixarium trugen statisch // "MEIN-TRESOR", weil die SVG-Datei kopiert + nie angepasst wurde. var DEFAULT_RIBBON_TEXT = "SAGE OBSERVATORIUM"; // Eindeutiger Anker im WAPPEN_SVG (genau ein Vorkommen, im Ribbon-textPath). var RIBBON_MARKER = ">" + DEFAULT_RIBBON_TEXT + ""; // Die zwei ANZEIGETEXTE im Wappen (Klaus 2026-09-14, entschieden: das Wappen // spricht mit). Sie standen auf englischen Seiten deutsch da — gemessen an // drei echten Seiten mit , und es war genau Klaus' Befund // vom selben Tag: ein deutsches Wort zwischen englischen Zeilen. // // ⚠ NICHT DIESELBE FRAGE WIE DIE ZERTIFIKAT_ASPEKTE. Deren deutscher // Wortlaut ist die Urkunde und steht nur im CODE; uebersetzt wird dort an // der Anzeige-Stelle. Diese zwei standen IN DER ANZEIGE. // // ⚠ "SBKIM" steht bewusst NICHT hier: ein Eigenname wird nicht uebersetzt. // Der Band-Text laeuft ueber RIBBON_MARKER, nicht hierueber — den graviert // der Host, und er ist keine Uebersetzungs-Frage (§ ribbonText). var WAPPEN_TEXTE = ["OFFIZIELLE BESTÄTIGUNG", "SIEGEL"]; // Sub (e) Bronze/Gold-Stufung (Spec-Erweiterung 2026-05-26). var STUFE_BRONZE = "bronze"; var STUFE_GOLD = "gold"; var STUFENWECHSEL_ANIMATION_MS = 600; var HANDSHAKE_EVENT = "sbkim:handshake"; var ARIA_LABEL_BRONZE = "SBKIM-Siegel · im Mycel, ruhend"; var ARIA_LABEL_GOLD = "SBKIM-Siegel · im Mycel, aktiv"; // ---- Wappen-SVG (Akkretions-Disk-Korona + Auszeichnungs-Siegel) ---- // // Inlined aus `assets/sbkim-siegel-wappen.svg` (source of truth — bei // Änderungen dort UND hier nachziehen, Konvention dokumentiert in // Karte 16 § Bauzustand). Korona-Ring (zwölf Arc-Segmente + // feGaussianBlur stdDeviation=22) folgt dem `.bh-disk`-Vorbild der // Sage-Page-Schwarzes-Loch-Karte (`index.html:498` conic-gradient // orange → gold → magenta → blau → türkis → orange). Im Badge // skaliert das SVG via viewBox-Attribute auf die CSS-Box (`width:40px; // height:40px`). aria-hidden auf dem SVG — das `title`/`aria-label` // am Badge-Span trägt das a11y-Label. var WAPPEN_SVG = '\n'; // ---- Modul-Zustand (Closure) ---- var ready = false; var badgeSelector = DEFAULT_BADGE_SELECTOR; var visibleMode = "visible"; var mountModalFlag = true; var repoUrlOverride = null; var ribbonText = DEFAULT_RIBBON_TEXT; // true sobald init({ribbonText}) einen expliziten Wert gesetzt hat. // Ohne expliziten Wert wird der Band-Text aus dem Repo-/Pages-Namen // automatisch abgeleitet (deriveRibbonFromRepo), damit jeder Endknoten // ohne Config seinen EIGENEN Namen trägt und nie ein mitkopiertes // Fremd-Label zeigt (Mein-Rezeptbuch-Bitte 2026-06-20). var ribbonTextExplicit = false; // Optionaler Andock-Knopf im Modal (opt-in, Default aus). Wenn true, // hängt mountSiegelModal() einen „Fremden Knoten andocken"-Knopf ins // Modal, der den KI-unabhängigen Modul-18-Wizard (SbkimToolPwa. // openAndockTab) öffnet. Der bestehende „🔑"-Identitäts-Pfad bleibt // unberührt (Klaus 2026-06-19: Andocken als ZUSÄTZLICHE Option). var andockToolEnabled = false; var moduleStatuses = null; // Array<{id, name, globalName, surfaceFn, lazy, status}> var certifiedFlag = false; var certifiedAt = null; var firstBootShown = false; var badgeElement = null; var badgeCreatedByModule = false; var badgeClickHandler = null; var modalRoot = null; var modalMounted = false; var modalOpen = false; var modalKeydownHandler = null; var mountObserver = null; var mountObserverTimeoutId = null; // Sub (e) Bronze/Gold-Stufe (Karte 16 § Sub (e)). RAM-only: Tab-Reload // startet wieder Bronze — gewollt (Karte 16 § Sub (e) Persistenz). var mycelConnected = false; var mycelConnectedAt = null; var handshakeListener = null; var stufenwechselTimeoutId = null; // ---- Hilfsfunktionen ---- function warn(message, cause) { if (typeof console !== "undefined" && console.warn) { if (cause !== undefined) console.warn("[SbkimSiegel] " + message, cause); else console.warn("[SbkimSiegel] " + message); } } function nowIso() { return new Date().toISOString(); } // ---- Sub (e) Bronze/Gold-Stufe (Karte 16 § Sub (e)) ---- // // siegelStufe() ist closure-intern — KEIN export auf public surface // (Brief Block 1 B). _meta.mycelConnected ist der publizierte Anker. function siegelStufe() { if (mycelConnected === true) return STUFE_GOLD; return STUFE_BRONZE; } // Setzt data-stufe + aria-label auf das Badge-Element. KEIN title- // Attribut (Konvention aus Pflege 17 Tooltips, Doppel-Tooltip-Problem // auf DeX-Chrome — aria-label trägt vollen Text). function applyStufeToBadge() { if (!badgeElement) return; var stufe = siegelStufe(); try { badgeElement.setAttribute("data-stufe", stufe); badgeElement.setAttribute( "aria-label", T(stufe === STUFE_GOLD ? ARIA_LABEL_GOLD : ARIA_LABEL_BRONZE), ); if (typeof badgeElement.removeAttribute === "function") { badgeElement.removeAttribute("title"); } } catch (err) { warn("data-stufe / aria-label konnte nicht gesetzt werden.", err); } } // Stufenwechsel-Animation (600 ms .stufenwechsel-gold-Klasse). function playStufenwechselAnimation() { if (!badgeElement || !badgeElement.classList) return; try { badgeElement.classList.add("stufenwechsel-gold"); if (stufenwechselTimeoutId !== null) { clearTimeout(stufenwechselTimeoutId); } stufenwechselTimeoutId = setTimeout(function () { if (badgeElement) { try { badgeElement.classList.remove("stufenwechsel-gold"); } catch (_e) { /* nb */ } } stufenwechselTimeoutId = null; }, STUFENWECHSEL_ANIMATION_MS); } catch (err) { warn("Stufenwechsel-Animation konnte nicht starten.", err); } } // window-Event-Listener-Handler für sbkim:handshake (Modul 05 Bau 17). // Idempotent + fail-soft (Karte 16 § Sub (e) Modul-16-Listener). function onHandshakeEvent(event) { var outcome = event && event.detail && event.detail.outcome; if (outcome !== "established") return; // no-op (fail-soft) if (mycelConnected === true) return; // idempotent mycelConnected = true; mycelConnectedAt = nowIso(); applyStufeToBadge(); playStufenwechselAnimation(); if (modalOpen) { try { renderModalContents(); } catch (err) { warn("Modal-Refresh nach Stufenwechsel fehlgeschlagen.", err); } } } function registerHandshakeListener() { if (handshakeListener) return; // idempotent if (typeof global.addEventListener !== "function") return; handshakeListener = onHandshakeEvent; try { global.addEventListener(HANDSHAKE_EVENT, handshakeListener); } catch (err) { warn("Handshake-Listener-Registrierung fehlgeschlagen.", err); handshakeListener = null; } } function escapeAttr(str) { // Modal/Badge-Title-Strings sind statisch oder kommen aus // ZERTIFIKAT_ASPEKTE / PFLICHT_MODULE — beide code-versioniert. // textContent reicht im Modal; für `title`-Attribute genügt es, // potenzielle Doublequotes als " auszugeben. if (typeof str !== "string") return ""; return str.replace(/&/g, "&").replace(/"/g, """).replace(//g, ">"); } // ---- Surface-Check (Karte 16 § Sub (a) Surface-Check-Form) ---- function checkModuleSurface(entry) { var ns; try { ns = global[entry.globalName]; } catch (_e) { ns = undefined; } if (ns === undefined) { return entry.lazy ? "deferred" : "missing"; } if (ns === null || typeof ns !== "object") { return "broken"; } if (typeof ns[entry.surfaceFn] !== "function") { return "broken"; } return "ok"; } function buildModuleStatuses() { var out = []; for (var i = 0; i < PFLICHT_MODULE.length; i++) { var entry = PFLICHT_MODULE[i]; out.push({ id: entry.id, name: entry.name, globalName: entry.globalName, surfaceFn: entry.surfaceFn, lazy: entry.lazy, status: checkModuleSurface(entry), }); } return out; } function deriveCertified(statuses) { if (!statuses || statuses.length === 0) return false; for (var i = 0; i < statuses.length; i++) { var s = statuses[i].status; if (s !== "ok" && s !== "deferred") return false; } return true; } function collectFailedIds(statuses) { var failed = []; for (var i = 0; i < statuses.length; i++) { var s = statuses[i].status; if (s === "missing" || s === "broken") { failed.push(statuses[i].id + " (" + s + ")"); } } return failed; } // ---- Repo-URL (Karte 16 § Sub (c) Repo-URL-Quelle) ---- function defaultRepoUrl() { var origin = ""; var path = "/"; try { if (global.location && typeof global.location.origin === "string") { origin = global.location.origin; } if (global.location && typeof global.location.pathname === "string") { path = global.location.pathname; } } catch (_e) { /* nb */ } var segments = path.split("/"); var firstSegment = null; for (var i = 0; i < segments.length; i++) { if (segments[i].length > 0) { firstSegment = segments[i]; break; } } if (firstSegment) return origin + "/" + firstSegment + "/"; return origin + "/"; } function resolveRepoUrl() { if (typeof repoUrlOverride === "string" && repoUrlOverride.length > 0) { // Sanity-Check: muss mit http(s):// oder / anfangen, sonst // fail-soft auf Auto-Erkennung. if (/^(https?:\/\/|\/)/.test(repoUrlOverride)) { return repoUrlOverride; } warn("repoUrl-Override ist keine gültige URL — Auto-Erkennung als Fallback: " + repoUrlOverride); } return defaultRepoUrl(); } // ---- Snapshot-Bau (defensive Kopie pro Aufruf) ---- function buildExplanationSnapshot() { var modulesCopy = []; if (moduleStatuses) { for (var i = 0; i < moduleStatuses.length; i++) { var m = moduleStatuses[i]; modulesCopy.push({ id: m.id, name: m.name, globalName: m.globalName, surfaceFn: m.surfaceFn, lazy: m.lazy, status: m.status, }); } } var certifiedIds = []; for (var j = 0; j < modulesCopy.length; j++) { if (modulesCopy[j].status === "ok" || modulesCopy[j].status === "deferred") { certifiedIds.push(modulesCopy[j].id); } } var aspectsCopy = []; for (var k = 0; k < ZERTIFIKAT_ASPEKTE.length; k++) { var a = ZERTIFIKAT_ASPEKTE[k]; aspectsCopy.push({ since: a.since, module: a.module, aspect: a.aspect, description: a.description, }); } return { certifiedAt: certifiedAt, isCertified: certifiedFlag, repoUrl: resolveRepoUrl(), modules: modulesCopy, certifiedModules: certifiedIds, aspects: aspectsCopy, }; } function emptySnapshot() { return { certifiedAt: null, isCertified: false, repoUrl: resolveRepoUrl(), modules: [], certifiedModules: [], aspects: [], }; } // ---- Badge-Mount (Option β) ---- function resolveBadgeAnchor() { if (!badgeSelector || typeof badgeSelector !== "string") return null; try { var doc = global.document; if (!doc || typeof doc.querySelector !== "function") return null; return doc.querySelector(badgeSelector); } catch (err) { warn("badgeSelector ist kein gültiger CSS-Selektor: " + badgeSelector, err); return null; } } // XML-Text-Escaping für den Band-Text (defensiv — Forker-Eingabe). function escapeXmlText(s) { return String(s) .replace(/&/g, "&") .replace(//g, ">"); } // Effektiver Band-Text. Bewusst KEINE Auto-Ableitung aus dem Repo-Namen // (Klaus-Entscheidung 2026-06-20): das untere Ribbon ist das SELF- // INSCRIBING-Element des Siegels — ein geratener Repo-Slug wirkt auf einer // Auszeichnung schnell falsch. Ohne expliziten `ribbonText` bleibt das Band // OFFEN (leer); ein Vermerk (console.info einmalig + Doku) bittet den Host, // seinen Namen via init({ribbonText}) einzugravieren. Der explizite Wert // übersteuert; ein mitkopiertes Fremd-Label entsteht so nie. function effectiveRibbonText() { return ribbonTextExplicit ? ribbonText : ""; } // Liefert den WAPPEN_SVG mit dem effektiven Band-Text. Entspricht der // effektive Text der inlined Konstante (Sage: "SAGE OBSERVATORIUM"), // bleibt das SVG byte-identisch; sonst wird der Ribbon-Text ersetzt // (leerer Wert → offenes Band). function renderWappenSvg() { var svg = WAPPEN_SVG; // Die zwei Anzeigetexte durch T(). AUF DEUTSCH GIBT T() DEN SATZ // UNVERAENDERT ZURUECK, die Ersetzung entfaellt und das SVG bleibt // BYTE-IDENTISCH — dieselbe Bauart wie der Band-Text eine Zeile weiter // unten, und damit bleibt „OHNE EINSTELLUNG AENDERT SICH NICHTS" gewahrt. // // ⚠ GESUCHT WIRD ">TEXT<", NICHT DER TEXT ALLEIN. Beide Woerter kommen im // SVG ein zweites Mal ausserhalb eines Textknotens vor; eine Ersetzung am // blossen Wort traefe Markup statt Anzeige. Gemessen: als ">TEXT<" ist // jeder von beiden GENAU EINMAL da. for (var i = 0; i < WAPPEN_TEXTE.length; i++) { var de = WAPPEN_TEXTE[i]; var uebersetzt = T(de); if (uebersetzt === de) continue; svg = svg.replace(">" + de + "<", ">" + escapeXmlText(uebersetzt) + "<"); } var eff = effectiveRibbonText(); if (eff === DEFAULT_RIBBON_TEXT) return svg; return svg.replace( RIBBON_MARKER, ">" + escapeXmlText(eff) + "", ); } function buildBadgeElement() { var doc = global.document; var span = doc.createElement("span"); span.id = BADGE_ID; span.setAttribute("role", "button"); span.setAttribute("tabindex", "0"); // aria-label wird in applyStufeToBadge() je nach Sub-(e)-Stufe gesetzt // (Bronze/Gold). KEIN title-Attribut — Pflege 17 Tooltips 2026-05-26: // Doppel-Tooltip-Problem auf DeX-Chrome. span.setAttribute("aria-label", T(ARIA_LABEL_BRONZE)); // SVG-Wappen: Ritterschild-Siegel mit Akkretions-Disk-Korona // (source of truth: `assets/sbkim-siegel-wappen.svg`; inlined als // `WAPPEN_SVG`-Konstante oben im Modul). Skaliert via viewBox auf // die 40-px-Badge-Box; bei 40 px sind Text-Bänder mikro-klein, das // Medaillon ist als visueller Anker erkennbar. aria-hidden auf dem // SVG — `title`/`aria-label` am Span trägt das a11y-Label. // renderWappenSvg() setzt den konfigurierten Band-Text (init ribbonText). span.innerHTML = renderWappenSvg(); return span; } function mountBadge() { // Option β: kein Render bei not-certified, kein DOM-Element. if (!certifiedFlag) return; if (visibleMode === "hidden") return; if (badgeElement) return; // idempotent var anchor = resolveBadgeAnchor(); if (!anchor) { setupBadgeMountObserver(); return; } var doc = global.document; if (!doc) return; // Wenn der Selektor bereits ein Element mit BADGE_ID gematcht // hat (vor-injiziert), benutzen wir dieses Element direkt. // Sonst (Standardfall: Container wie `.lamps`) erzeugen wir // den Badge-Span darin. if (anchor.id === BADGE_ID) { badgeElement = anchor; badgeCreatedByModule = false; // Wenn das bereits-bestehende Element leer ist, füllen wir // das Wappen nach; sonst lassen wir den Inhalt unangetastet. if (!anchor.firstChild) { anchor.innerHTML = buildBadgeElement().innerHTML; } if (!anchor.getAttribute("role")) anchor.setAttribute("role", "button"); if (!anchor.getAttribute("tabindex")) anchor.setAttribute("tabindex", "0"); // aria-label setzt applyStufeToBadge() je nach Sub-(e)-Stufe. // KEIN title-Attribut (Pflege 17 Tooltips, Doppel-Tooltip-Problem). } else { var span = buildBadgeElement(); anchor.appendChild(span); badgeElement = span; badgeCreatedByModule = true; } applyStufeToBadge(); // Sub (e) Bronze/Gold (Karte 16 § Sub (e)). attachBadgeClickHandler(); playFirstBootAnimation(); } function setupBadgeMountObserver() { if (mountObserver) return; var doc = global.document; if (!doc || typeof MutationObserver === "undefined") return; if (!doc.body) { if (typeof doc.addEventListener === "function") { var onReady = function () { doc.removeEventListener("DOMContentLoaded", onReady); mountBadge(); }; doc.addEventListener("DOMContentLoaded", onReady); } return; } mountObserver = new MutationObserver(function () { var anchor = resolveBadgeAnchor(); if (anchor) { disconnectMountObserver(); mountBadge(); } }); try { mountObserver.observe(doc.body, { childList: true, subtree: true }); } catch (err) { warn("MutationObserver konnte nicht starten — späten Badge-Mount aufgeben.", err); mountObserver = null; return; } mountObserverTimeoutId = setTimeout(function () { if (mountObserver) { disconnectMountObserver(); warn( 'badgeSelector "' + badgeSelector + '" auch nach ' + MOUNT_OBSERVER_TIMEOUT_MS + ' ms nicht gefunden — Badge-Mount übersprungen.', ); } }, MOUNT_OBSERVER_TIMEOUT_MS); } function disconnectMountObserver() { if (mountObserver) { try { mountObserver.disconnect(); } catch (_e) { /* nb */ } mountObserver = null; } if (mountObserverTimeoutId !== null) { clearTimeout(mountObserverTimeoutId); mountObserverTimeoutId = null; } } function attachBadgeClickHandler() { if (!badgeElement) return; if (badgeClickHandler) return; // idempotent badgeClickHandler = function () { if (!mountModalFlag) return; if (modalOpen) closeModal(); else openModal(); }; try { badgeElement.addEventListener("click", badgeClickHandler); // Tastatur-A11y: Enter / Space am fokussierten Badge öffnet das Modal. badgeElement.addEventListener("keydown", function (ev) { if (!mountModalFlag) return; if (ev && (ev.key === "Enter" || ev.key === " " || ev.key === "Spacebar")) { ev.preventDefault(); if (modalOpen) closeModal(); else openModal(); } }); } catch (err) { warn("Badge-Click-Handler konnte nicht registriert werden.", err); } } function playFirstBootAnimation() { if (firstBootShown) return; if (!badgeElement) return; try { badgeElement.classList.add("first-boot"); setTimeout(function () { if (badgeElement) { try { badgeElement.classList.remove("first-boot"); } catch (_e) { /* nb */ } } }, FIRST_BOOT_ANIMATION_MS); } catch (err) { warn("First-Boot-Animation konnte nicht starten.", err); } firstBootShown = true; } // ---- Modal-Mount (Karte 16 § Sub (c)) ---- // ---- Optionaler Andock-Knopf (opt-in, Modul 18 wiederverwendet) ---- // // KI-unabhängiger Handshake: öffnet den Modul-18-Wizard // (SbkimToolPwa.openAndockTab: Repo-URL → Spore holen → verifyForeignSpore // → Match → Handshake via Modul 05). Reiner Browser-Pfad (WebCrypto+fetch), // keine Claude-Sitzung nötig. Fail-soft: fehlt Modul 18, zeigt der Knopf // einen Hinweis statt zu werfen. Der „🔑"-Identitäts-Pfad bleibt unberührt. function showAndockHint(text) { if (!modalRoot) return; var hint = modalRoot.querySelector("[data-siegel-andock-tool-hint]"); if (!hint) return; hint.style.display = "block"; hint.textContent = text; } function onAndockClick() { var tp = global.SbkimToolPwa; if (!tp || typeof tp.openAndockTab !== "function") { showAndockHint(T("Andock-Werkzeug (Modul 18 SbkimToolPwa) ist nicht geladen — bitte src/modules/18_tool_pwa.js einbinden und SbkimToolPwa.init({…}) aufrufen.")); warn("Andock-Knopf geklickt, aber Modul 18 (SbkimToolPwa) ist nicht geladen."); return; } var hint = modalRoot ? modalRoot.querySelector("[data-siegel-andock-tool-hint]") : null; if (hint) { hint.style.display = "none"; hint.textContent = ""; } try { var p = tp.openAndockTab(); // Siegel-Modal schließen, damit der Modul-18-Wizard sichtbar ist: // der Wizard hat einen niedrigeren z-index (10000) als das Siegel- // Modal (99998) und läge sonst dahinter (Befund Klaus 2026-06-20: // „Knopf da, Klick öffnet nichts" — Wizard war verdeckt). closeModal(); if (p && typeof p.catch === "function") { p.catch(function (err) { warn("Andock-Wizard-Start fehlgeschlagen.", err); }); } } catch (err) { warn("Andock-Werkzeug-Start fehlgeschlagen.", err); showAndockHint(T("Andock-Werkzeug nicht bereit: ") + (err && err.message ? err.message : String(err))); } } function buildAndockBlock(doc) { var block = doc.createElement("div"); block.setAttribute("data-siegel-andock-tool", ""); block.style.cssText = [ "margin:0 0 1.2rem", "padding:0.85rem 0.9rem", "background:rgba(201,169,97,0.08)", "border:1px solid var(--siegel-line, rgba(201,169,97,0.45))", "border-radius:8px", ].join(";"); var lead = doc.createElement("p"); lead.style.cssText = [ "margin:0 0 0.6rem", "font-family:'Geist', system-ui, sans-serif", "font-size:0.86rem", "line-height:1.5", "color:rgba(245,245,255,0.86)", ].join(";"); lead.textContent = T("Fremden Knoten verbinden — ohne KI, direkt im Browser: Repo-/App-URL eingeben → Spore prüfen → Match → Handshake."); var btn = doc.createElement("button"); btn.type = "button"; btn.setAttribute("data-siegel-andock-tool-btn", ""); btn.textContent = T("🔌 Fremden Knoten andocken →"); btn.style.cssText = [ "display:inline-block", "background:var(--siegel-gold, #C9A961)", "color:#1A1306", "border:none", "border-radius:8px", "padding:0.5rem 0.9rem", "font-family:'Geist', system-ui, sans-serif", "font-size:0.9rem", "font-weight:600", "cursor:pointer", ].join(";"); var hint = doc.createElement("p"); hint.setAttribute("data-siegel-andock-tool-hint", ""); hint.style.cssText = [ "display:none", "margin:0.55rem 0 0", "font-size:0.8rem", "line-height:1.45", "color:rgba(245,245,255,0.6)", ].join(";"); block.appendChild(lead); block.appendChild(btn); block.appendChild(hint); btn.addEventListener("click", onAndockClick); return block; } function mountSiegelModal() { if (modalMounted) return; if (!mountModalFlag) return; if (!certifiedFlag) return; // Modal nur wenn Bezeugung grün var doc = global.document; if (!doc || !doc.body) return; var root = doc.createElement("div"); root.id = MODAL_ID; root.setAttribute("aria-hidden", "true"); root.style.cssText = [ "position:fixed", "inset:0", "z-index:99998", // unter Modul-15-Modal-z-Index (99999), Reihenfolge ist konventionsfrei "display:none", "align-items:center", "justify-content:center", ].join(";"); var backdrop = doc.createElement("div"); backdrop.setAttribute("data-siegel-backdrop", ""); backdrop.style.cssText = [ "position:absolute", "inset:0", "background:rgba(0,0,0,0.62)", ].join(";"); var panel = doc.createElement("div"); panel.setAttribute("role", "dialog"); panel.setAttribute("aria-modal", "true"); panel.setAttribute("aria-labelledby", MODAL_ID + "-title"); // Bronze-Ink-Untergrund + Edel-Gold-Rahmen (Karte 16 § Sub (c) Modal-Form). panel.style.cssText = [ "position:relative", "background:var(--siegel-ink, #1A1306)", "color:#F5F5FF", "border:1px solid var(--siegel-line, rgba(201,169,97,0.45))", "border-radius:10px", "padding:1.4rem 1.6rem", "min-width:320px", "max-width:min(560px, 92vw)", "max-height:80vh", "overflow:auto", "box-shadow:0 24px 64px rgba(0,0,0,0.7)", "font-family:'Geist', system-ui, sans-serif", ].join(";"); var header = doc.createElement("div"); header.style.cssText = "display:flex;align-items:center;gap:0.8rem;margin-bottom:1rem;"; var title = doc.createElement("h2"); title.id = MODAL_ID + "-title"; title.textContent = T(MODAL_TITLE); // Serif für Titel (Karte 16 § Sub (c) wertigere Typografie). title.style.cssText = [ "margin:0", "font-family:'Spectral','Georgia','Times New Roman',serif", "font-size:1.25rem", "font-weight:500", "letter-spacing:0.01em", "color:var(--siegel-gold, #C9A961)", "flex:1", ].join(";"); var closeBtn = doc.createElement("button"); closeBtn.type = "button"; closeBtn.setAttribute("data-siegel-close", ""); closeBtn.setAttribute("aria-label", T("Schließen")); closeBtn.textContent = "✕"; closeBtn.style.cssText = [ "background:transparent", "color:#F5F5FF", "border:1px solid var(--siegel-line, rgba(201,169,97,0.45))", "border-radius:8px", "padding:0.25rem 0.6rem", "cursor:pointer", "font-size:1rem", ].join(";"); header.appendChild(title); header.appendChild(closeBtn); // Sub (e) Bronze-Hinweis-Block (Karte 16 § Sub (e) Klick-Verhalten in // Bronze). Sichtbar nur wenn _meta.mycelConnected === false. Reiner // Hinweis-Text; der Andock-/Identitäts-Pfad ist der „🔑 …"-Knopf, den // der Host (Sage-Page / Endknoten) oben ins Modal einhängt (Pflege // 2026-06-07: kein Modul-18-Verweis mehr, ein Pfad statt mehrerer). var bronzeHinweisBlock = doc.createElement("div"); bronzeHinweisBlock.setAttribute("data-siegel-bronze-hinweis", ""); bronzeHinweisBlock.style.cssText = [ "display:none", "margin:0 0 1rem", "padding:0.75rem 0.9rem", "background:rgba(140,110,47,0.12)", "border:1px solid var(--siegel-bronze-glow, rgba(140,110,47,0.45))", "border-radius:8px", "font-family:'Geist', system-ui, sans-serif", "font-size:0.86rem", "line-height:1.5", "color:rgba(245,245,255,0.88)", ].join(";"); var dateLine = doc.createElement("p"); dateLine.setAttribute("data-siegel-date", ""); dateLine.style.cssText = "margin:0 0 1rem;font-size:0.86rem;color:rgba(245,245,255,0.78);"; var modulesHeader = doc.createElement("h3"); modulesHeader.textContent = T("Pflicht-Module"); modulesHeader.style.cssText = [ "margin:0 0 0.5rem", "font-family:'Geist', system-ui, sans-serif", "font-size:0.92rem", "font-weight:500", "color:rgba(245,245,255,0.78)", "text-transform:uppercase", "letter-spacing:0.06em", ].join(";"); var modulesList = doc.createElement("ul"); modulesList.setAttribute("data-siegel-modules", ""); modulesList.style.cssText = [ "list-style:none", "padding:0", "margin:0 0 1.2rem", "font-family:'Geist', system-ui, sans-serif", "font-size:0.86rem", ].join(";"); // Aspekte-Sektion: standardmäßig EINGEKLAPPT (
ohne "open") und // ehrlich als PROTOKOLL-weite, gemeinsame Historie gerahmt — NICHT als // app-eigene Zertifizierungs-Daten. Die since-Daten stammen aus der // netzweiten SBKIM-Bau-Historie und sind in jeder App identisch; die // app-eigene, aktuelle Aussage ist die „Pflicht-Module"-Liste oben + // die Bezeugt-seit-Zeile (lokaler Start-Zeitpunkt DIESER App). var aspectsDetails = doc.createElement("details"); aspectsDetails.setAttribute("data-siegel-aspects-details", ""); aspectsDetails.style.cssText = "margin:0 0 1.2rem;"; var aspectsSummary = doc.createElement("summary"); aspectsSummary.textContent = T("SBKIM-Protokoll — Sicherheits-Aspekte (gemeinsam, netzweit)"); aspectsSummary.style.cssText = modulesHeader.style.cssText + ";cursor:pointer;"; var aspectsIntro = doc.createElement("p"); aspectsIntro.textContent = T("Historie der SBKIM-Sicherheits-Bausteine — für alle Knoten gleich. Das sind Protokoll-Daten, keine app-eigenen Zertifizierungs-Daten dieser App."); aspectsIntro.style.cssText = "margin:0.5rem 0 0.7rem;font-size:0.8rem;color:rgba(245,245,255,0.6);line-height:1.5;"; var aspectsList = doc.createElement("ul"); aspectsList.setAttribute("data-siegel-aspects", ""); aspectsList.style.cssText = [ "list-style:none", "padding:0", "margin:0", "font-family:'Geist', system-ui, sans-serif", "font-size:0.86rem", ].join(";"); aspectsDetails.appendChild(aspectsSummary); aspectsDetails.appendChild(aspectsIntro); aspectsDetails.appendChild(aspectsList); var ausstellerBlock = doc.createElement("div"); ausstellerBlock.setAttribute("data-siegel-aussteller", ""); // Serif für die Aussteller-Klärung (Karte 16 § Sub (c)). ausstellerBlock.style.cssText = [ "padding-top:0.9rem", "border-top:1px solid var(--siegel-line, rgba(201,169,97,0.45))", "font-family:'Spectral','Georgia','Times New Roman',serif", "font-size:0.92rem", "line-height:1.55", "color:rgba(245,245,255,0.86)", ].join(";"); panel.appendChild(header); panel.appendChild(bronzeHinweisBlock); // Sub (e) — sichtbar nur in Bronze panel.appendChild(dateLine); // Optionaler Andock-Knopf (opt-in via init({andockTool:true})). Nur // dann im DOM — Default-Render trägt KEIN [data-siegel-andock-tool]. if (andockToolEnabled) { panel.appendChild(buildAndockBlock(doc)); } panel.appendChild(modulesHeader); panel.appendChild(modulesList); panel.appendChild(aspectsDetails); panel.appendChild(ausstellerBlock); root.appendChild(backdrop); root.appendChild(panel); doc.body.appendChild(root); backdrop.addEventListener("click", closeModal); closeBtn.addEventListener("click", closeModal); modalRoot = root; modalMounted = true; } function statusMarkerDot(status) { var color; if (status === "ok") color = "#16A34A"; else if (status === "deferred") color = "var(--siegel-gold, #C9A961)"; else color = "#DC2626"; return ( '' ); } function statusLabel(status) { if (status === "ok") return "bereit"; if (status === "deferred") return "bereit (lazy)"; if (status === "missing") return "fehlt"; if (status === "broken") return "defekt"; return status; } // Sub (e) Bronze-Hinweis-Block-Render (Karte 16 § Sub (e) Klick- // Verhalten in Bronze). Wird aus renderModalContents() bei jedem // Modal-Render aufgerufen. function renderBronzeHinweisBlock(modalRoot) { var block = modalRoot.querySelector("[data-siegel-bronze-hinweis]"); if (!block) return; var doc = global.document; block.textContent = ""; // In Gold-Stufe: Block ausblenden. if (siegelStufe() === STUFE_GOLD) { block.style.display = "none"; return; } block.style.display = "block"; var p = doc.createElement("p"); p.style.cssText = "margin:0;"; var strong = doc.createElement("strong"); strong.textContent = T("Im Mycel · ruhend"); strong.style.cssText = "color:var(--siegel-bronze, #8C6E2F);font-weight:600;"; p.appendChild(strong); // EINE Zeile, keine Konkatenation: der Woerterbuch-Schluessel ist der // GANZE Satz, und ein zusammengesetzter Ausdruck steht nirgends als // Zeichenkette in der Datei. Der Waechter faende ihn dann nicht und // meldete den Eintrag zu Recht als tot. p.appendChild(doc.createTextNode(T(" — diese Zelle trägt das SBKIM-Siegel und ist damit Teil des Mycels, so klein sie auch ist. In dieser Sitzung gab es noch keinen Hyphen-Verkehr (Handshake). Über den Knopf „🔑 Eigene Identität & Spore erzeugen / verwalten →“ oben knüpfst du aktiv einen Faden zu einem Geschwister-Knoten."))); block.appendChild(p); } function renderModalContents() { if (!modalRoot) return; var doc = global.document; var snap = buildExplanationSnapshot(); // Sub (e) Bronze-Hinweis-Block — vor allen anderen Render-Schritten. renderBronzeHinweisBlock(modalRoot); var dateLine = modalRoot.querySelector("[data-siegel-date]"); if (dateLine) { if (snap.certifiedAt) { // Pflege Modal-Local-Time 2026-05-26 (Sub-(e)-Folge-Pflege 3/3): // certifiedAt ist UTC-ISO ("2026-05-24T18:42:31.123Z"). Vor der // Pflege wurden die ISO-Slices direkt angezeigt — Klaus' Befund // DeX-Chrome (MESZ, UTC+2): „Datum/Uhrzeit ist nicht aktuell, // ich vermute nicht Mitteleuropäische Zeit, eher Amerikan." // Fix: lokale Date-Methoden (getFullYear/getMonth/getDate/ // getHours/getMinutes) statt UTC-ISO-Split. ISO-Datum-Format // (YYYY-MM-DD) bleibt, weil Klaus' Doku-Stil das überall nutzt // (Aspekte-since-Feld, Übergabeprotokoll-Dateinamen). var date = new Date(snap.certifiedAt); if (isNaN(date.getTime())) { dateLine.textContent = T("Bezeugt seit ") + snap.certifiedAt; } else { var yyyy = date.getFullYear(); var mm = String(date.getMonth() + 1).padStart(2, "0"); var dd = String(date.getDate()).padStart(2, "0"); var HH = String(date.getHours()).padStart(2, "0"); var MM = String(date.getMinutes()).padStart(2, "0"); dateLine.textContent = T("Bezeugt seit ") + yyyy + "-" + mm + "-" + dd + ", " + HH + ":" + MM + T(" Uhr."); } } else { dateLine.textContent = T("Bezeugt: —"); } } var modulesList = modalRoot.querySelector("[data-siegel-modules]"); if (modulesList) { modulesList.textContent = ""; for (var i = 0; i < snap.modules.length; i++) { var m = snap.modules[i]; var li = doc.createElement("li"); li.style.cssText = "padding:0.32rem 0;border-bottom:1px solid rgba(255,255,255,0.05);"; // Status-Punkt + ID/Name + Status-Label. Punkt-HTML ist // module-eigen und statisch — kein User-Input. li.innerHTML = statusMarkerDot(m.status) + '' + escapeAttr(m.id) + '' + ' · ' + '' + escapeAttr(T(m.name)) + '' + ' · ' + '' + escapeAttr(T(statusLabel(m.status))) + ''; modulesList.appendChild(li); } } var aspectsList = modalRoot.querySelector("[data-siegel-aspects]"); if (aspectsList) { aspectsList.textContent = ""; // Chronologisch aufsteigend, Tie-Breaker module-ID aufsteigend // (Karte 16 § Sub (d) Reihenfolge). var sorted = snap.aspects.slice().sort(function (a, b) { if (a.since < b.since) return -1; if (a.since > b.since) return 1; if (a.module < b.module) return -1; if (a.module > b.module) return 1; return 0; }); for (var k = 0; k < sorted.length; k++) { var a = sorted[k]; var aLi = doc.createElement("li"); aLi.style.cssText = "padding:0.5rem 0;border-bottom:1px solid rgba(255,255,255,0.05);"; // Sub (e) Aspekt-4-Pending-Marker (Karte 16 § Sub (d) Sonderfall // dynamische Render-Variante): in Bronze-Stufe rendert das Modal // Aspekt 4 mit Marker „pending" statt Datum, mit grauer Farbe. var isPendingAspect4 = isAspect4(a) && mycelConnected !== true; var head = doc.createElement("div"); head.style.cssText = "display:flex;gap:0.5rem;align-items:baseline;margin-bottom:0.2rem;"; var since = doc.createElement("span"); since.textContent = isPendingAspect4 ? T("pending") : a.since; since.style.cssText = isPendingAspect4 ? "font-family:'Geist Mono',ui-monospace,monospace;color:rgba(245,245,255,0.45);font-size:0.82rem;font-style:italic;" : "font-family:'Geist Mono',ui-monospace,monospace;color:var(--siegel-gold, #C9A961);font-size:0.82rem;"; var moduleId = doc.createElement("span"); moduleId.textContent = "· " + a.module; moduleId.style.cssText = "font-family:'Geist Mono',ui-monospace,monospace;color:rgba(245,245,255,0.62);font-size:0.82rem;"; var aspect = doc.createElement("span"); aspect.textContent = "· " + T(a.aspect); aspect.style.cssText = "color:#F5F5FF;font-size:0.9rem;flex:1;"; head.appendChild(since); head.appendChild(moduleId); head.appendChild(aspect); var desc = doc.createElement("p"); desc.textContent = T(a.description); desc.style.cssText = "margin:0;font-size:0.82rem;color:rgba(245,245,255,0.7);line-height:1.5;"; aLi.appendChild(head); aLi.appendChild(desc); aspectsList.appendChild(aLi); } } var ausstellerBlock = modalRoot.querySelector("[data-siegel-aussteller]"); if (ausstellerBlock) { ausstellerBlock.textContent = ""; var doc2 = global.document; var p1 = doc2.createElement("p"); p1.style.cssText = "margin:0 0 0.4rem;"; // Verbindlicher Wortlaut (Karte 16 § Sub (c) Aussteller-Klärung). // textContent statt innerHTML → KEINE HTML-Interpretation, dafür // "self-inscribing" als Wort, das im Text betont aussieht durch // den Serif-Font des umgebenden Blocks. p1.appendChild(doc2.createTextNode(T("Dieses Siegel ist "))); var strong = doc2.createElement("strong"); strong.textContent = T("self-inscribing"); strong.style.cssText = "font-weight:500;color:var(--siegel-gold, #C9A961);"; p1.appendChild(strong); p1.appendChild(doc2.createTextNode(T(": die App hat sich beim Boot selbst geprüft."))); var p2 = doc2.createElement("p"); p2.style.cssText = "margin:0;"; p2.appendChild(doc2.createTextNode(T("Vertrauen kommt vom Repo, in dem sie gehostet ist: "))); var link = doc2.createElement("a"); link.href = snap.repoUrl; link.textContent = snap.repoUrl; link.target = "_blank"; link.rel = "noopener noreferrer"; link.style.cssText = "color:var(--siegel-gold, #C9A961);text-decoration:underline;"; p2.appendChild(link); p2.appendChild(doc2.createTextNode(".")); ausstellerBlock.appendChild(p1); ausstellerBlock.appendChild(p2); } } function openModal() { if (!mountModalFlag) return; if (!certifiedFlag) return; if (!modalMounted) mountSiegelModal(); if (!modalRoot) return; renderModalContents(); modalRoot.style.display = "flex"; modalRoot.setAttribute("aria-hidden", "false"); modalOpen = true; if (!modalKeydownHandler) { modalKeydownHandler = function (event) { if (event && event.key === "Escape" && modalOpen) { closeModal(); } }; try { global.document.addEventListener("keydown", modalKeydownHandler); } catch (_e) { /* nb */ } } } function closeModal() { if (!modalOpen) return; if (modalRoot) { modalRoot.style.display = "none"; modalRoot.setAttribute("aria-hidden", "true"); } modalOpen = false; if (modalKeydownHandler) { try { global.document.removeEventListener("keydown", modalKeydownHandler); } catch (_e) { /* nb */ } modalKeydownHandler = null; } } // ---- init() ---- async function init(options) { if (ready) { // Idempotent: keine Re-Check, kein Re-Mount, kein Doppel-Listener. // Wenn der Selektor erst nach init() im DOM erscheint, kann ein // Aufrufer `mountBadge()` indirekt via wiederholten init() NICHT // erzwingen — das ist Karte 16 § Sub (a) "idempotent" verbindlich. return; } var opts = (options && typeof options === "object") ? options : {}; if (typeof opts.badgeSelector === "string" && opts.badgeSelector.length > 0) { badgeSelector = opts.badgeSelector; } if (opts.visible === "hidden") { visibleMode = "hidden"; } else if (opts.visible === "visible") { visibleMode = "visible"; } mountModalFlag = (opts.mountModal !== false); if (typeof opts.repoUrl === "string" && opts.repoUrl.length > 0) { repoUrlOverride = opts.repoUrl; } else if (opts.repoUrl === null) { repoUrlOverride = null; } // Band-Text im Wappen (Forker gravieren ihren Knoten-Namen ein). Fail- // soft: leerer/Nicht-String-Wert lässt das Band OFFEN (kein Auto-Label). if (typeof opts.ribbonText === "string" && opts.ribbonText.trim().length > 0) { ribbonText = opts.ribbonText.trim(); ribbonTextExplicit = true; } else if (typeof console !== "undefined" && console.info) { // Vermerk (einmalig, init ist idempotent): Band bleibt bewusst offen. console.info( "[SbkimSiegel] Band offen gelassen — init({ ribbonText: \"DEIN-KNOTEN\" }) " + "setzen, um den eigenen Namen ins Siegel zu gravieren (kein Auto-Label).", ); } // Sprache (2026-09-14). Nur "de"/"en" werden uebernommen; alles andere // laesst optLang auf null, und dann entscheidet . Kein // Zuruecksetzen auf "de" bei einem unbekannten Wert — sonst schluege eine // Falscheingabe die Seiten-Sprache, statt fail-soft daneben zu liegen. if (opts.lang === "de" || opts.lang === "en") optLang = opts.lang; // Optionaler Andock-Knopf (KI-unabhängiger Handshake via Modul 18). // Strikt boolean true → opt-in; alles andere lässt den Default (aus). if (opts.andockTool === true) { andockToolEnabled = true; } // Surface-Check: Snapshot zur init()-Zeit. moduleStatuses = buildModuleStatuses(); certifiedFlag = deriveCertified(moduleStatuses); if (!certifiedFlag) { // Fail-Modus binär (Karte 16 § Sub (a) Fail-Modus): // EINE console.warn-Zeile mit ID-Liste; KEIN Badge, KEIN Modal. var failed = collectFailedIds(moduleStatuses); warn( "kein Render: Pflicht-Module fehlen/defekt — " + failed.join(", ") + ". Siehe docs/components/16_siegel.md § Sub (a).", ); ready = true; return; } certifiedAt = nowIso(); // Badge-Mount (Option β: kein DOM-Element bei not-certified). if (visibleMode !== "hidden") { mountBadge(); } // Modal-Mount (default true). Modal hängt unabhängig vom Badge — // bei `visible:"hidden"` UND `mountModal:true` ist das Modal trotzdem // ansprechbar, aber ohne Click-Trigger (Aufrufer öffnet via JS). if (mountModalFlag) { try { mountSiegelModal(); } catch (err) { warn("Modal-Mount fehlgeschlagen", err); } } // Sub (e) Bronze/Gold-Stufung (Karte 16 § Sub (e), 2026-05-26): // window-Event-Listener für sbkim:handshake outcome:"established" // registrieren. Erst NACH Badge-Mount, damit applyStufeToBadge auf // einem existierenden Element arbeiten kann. Idempotent + // fail-soft im Handler. registerHandshakeListener(); ready = true; // Bau 17: sbkim:siegel-certified-Custom-Event NUR bei // isCertified()===true (Anti-Greenwashing-Klausel binär). Wird // genau einmal pro Sitzung gefeuert — `ready`-Flag oben schützt // gegen Re-Init. Modul 17 (LEBT-Slot ... Pardon, SIEGEL-Slot) // hängt davon ab, dass dieser Event auf `window` ankommt. try { if (typeof global.dispatchEvent === "function" && typeof global.CustomEvent === "function") { global.dispatchEvent(new global.CustomEvent("sbkim:siegel-certified", { detail: { certifiedAt: certifiedAt, repoUrl: resolveRepoUrl(), }, bubbles: false, cancelable: false, })); } } catch (_e) { // fail-soft — Render-Schicht (Modul 17) ist optional. } } // ---- Öffentliche API ---- function isCertified() { return certifiedFlag === true; } function getExplanation() { if (!ready) return emptySnapshot(); return buildExplanationSnapshot(); } function getCertifiedModules() { if (!ready || !moduleStatuses) return []; var out = []; for (var i = 0; i < moduleStatuses.length; i++) { var s = moduleStatuses[i].status; if (s === "ok" || s === "deferred") out.push(moduleStatuses[i].id); } return out; } function getAspects() { var out = []; for (var i = 0; i < ZERTIFIKAT_ASPEKTE.length; i++) { var a = ZERTIFIKAT_ASPEKTE[i]; out.push({ since: a.since, module: a.module, aspect: a.aspect, description: a.description, }); } return out; } // PFLICHT_MODULE-Spec-Kopie für _meta (defensive Kopie zur Skript-Lade- // Zeit; KEINE Runtime-Mutation, KEINE Aufrufer-Manipulation). function snapshotPflichtModuleSpec() { var out = []; for (var i = 0; i < PFLICHT_MODULE.length; i++) { var e = PFLICHT_MODULE[i]; out.push({ id: e.id, name: e.name, globalName: e.globalName, surfaceFn: e.surfaceFn, lazy: e.lazy, }); } return out; } // Sub (e) Test-Brücke (Karte 16 § Sub (e), Panel 16 Knopf 12): // setzt _meta.mycelConnected zurück auf false, re-rendert Badge. // KEIN public-Use; ausschließlich Test-Brücke (Convention analog // Modul 08 _clearOutbox, Modul 15 clear). Tab-Reload erreicht das // gleiche, aber Panel-Test braucht Reset ohne Reload. function _resetMycelConnectedForTest() { mycelConnected = false; mycelConnectedAt = null; if (stufenwechselTimeoutId !== null) { clearTimeout(stufenwechselTimeoutId); stufenwechselTimeoutId = null; } if (badgeElement && badgeElement.classList) { try { badgeElement.classList.remove("stufenwechsel-gold"); } catch (_e) { /* nb */ } } applyStufeToBadge(); if (modalOpen) { try { renderModalContents(); } catch (err) { warn("Modal-Refresh nach _resetMycelConnectedForTest fehlgeschlagen.", err); } } } var SbkimSiegel = { init: init, isCertified: isCertified, getExplanation: getExplanation, getCertifiedModules: getCertifiedModules, getAspects: getAspects, _resetMycelConnectedForTest: _resetMycelConnectedForTest, _meta: { badgeId: BADGE_ID, get lang() { return sprache(); }, get langKeys() { return Object.keys(TEXTE.en).length; }, modalId: MODAL_ID, defaultSelector: DEFAULT_BADGE_SELECTOR, get ready() { return ready; }, get firstBootShown() { return firstBootShown; }, get certifiedAt() { return certifiedAt; }, get pflichtModuleSpec() { return snapshotPflichtModuleSpec(); }, get badgeMounted() { return badgeElement !== null; }, get badgeCreatedByModule() { return badgeCreatedByModule; }, get modalMounted() { return modalMounted; }, get modalOpen() { return modalOpen; }, get visibleMode() { return visibleMode; }, get mountModalFlag() { return mountModalFlag; }, get badgeSelector() { return badgeSelector; }, get ribbonText() { return effectiveRibbonText(); }, get andockToolEnabled() { return andockToolEnabled; }, // Sub (e) Bronze/Gold-Stufung (Karte 16 § Sub (e)). get mycelConnected() { return mycelConnected; }, get mycelConnectedAt() { return mycelConnectedAt; }, get siegelStufe() { return siegelStufe(); }, }, }; global.SbkimSiegel = SbkimSiegel; // Self-check (synchron, beim Skript-Laden — vor jedem Aufruf). if (typeof console !== "undefined" && console.info) { console.info( "MODUL 16 SIEGEL bereit, Funktionen: init/isCertified/getExplanation/getCertifiedModules/getAspects", ); } })(typeof window !== "undefined" ? window : globalThis);