← mathe.digital
⬇ Markdown

Tipp: Mit Cmd+P (Mac) oder Ctrl+P (Windows/Linux) erscheint der Druckdialog → „Als PDF speichern" wählen.

Matheforscher postMessage-Protokoll

Dokumentation für Entwickler:innen, die ihre Webapp in die Matheforscher-Plattform einbinden möchten.

Kurz und gut: Deine Webapp läuft im iframe. Mit ein paar Zeilen JavaScript meldet sie der Plattform, was das Kind tut. Die Plattform nutzt das für Lehrkraft-Dashboard, automatische Lösungs-Erkennung und KI-gestützte Lernbegleitung.


Inhalt

  1. Worum geht's?
  2. Quickstart (5 Zeilen)
  3. Vollständiger Auto-Init (alles in einem)
  4. Plattform-Erkennung
  5. iframe-Berechtigungen
  6. Konfiguration via Query-Parameter
  7. App-Manifest beim ready-Event
  8. Event-Typen
  9. Schema-Beschreibung für den App-Pool
  10. Best Practices
  11. Komplettbeispiel: Mini-Rechenfeld
  12. Browser-Kompatibilität & Stolpersteine
  13. Testen + Debugging
  14. Datenschutz
  15. Protokoll v3.2 — KI-Kanal (zentrale KI des Hosts)
  16. Versions-History

Worum geht's?

Die Matheforscher-Plattform bettet Webapps per <iframe> in Forscheraufträge ein. Apps müssen nichts „wissen" — sie funktionieren auch ohne Anbindung. Optional kann eine App aber per window.postMessage an das Parent-Fenster Events senden. Diese Events ermöglichen u.a. folgende Funktionen:

Event Plattform-Reaktion
matheforscher:progress Lehrkraft-Dashboard zeigt Zwischenschritte; Live-Feedback im Schüler-UI als Chip
matheforscher:complete Arbeitsmittel meldet „Ziel erreicht" — zählt als Bestätigung, wenn das Kind „Fertig – schau nach!" drückt (hakt nicht selbst ab, ab Doku 3.3)
matheforscher:configured Bestätigt eine Live-Re-Konfiguration der App — vermeidet iframe-Reload
matheforscher:error Plattform zeigt Fehler-Hinweis (selten benötigt)
matheforscher:action Lernverlauf wird gespeichert; KI kann Schritte nachvollziehen
matheforscher:state Aktueller App-Zustand wird gespeichert; KI kann ihn auf Anfrage interpretieren
matheforscher:highlight Plattform → App: hebt deklarierte Elemente hervor (sprachbegleitender KI-Hinweis)
matheforscher:perform Plattform → App: führt deklarierte Aktionen vor (demo) oder aus (execute)

Alle Events sind optional und additiv — du kannst klein anfangen (nur complete) und später ausbauen.


Quickstart (5 Zeilen)

Minimal-Implementierung, die der Plattform nur sagt „Aufgabe gelöst":

function notifyComplete() {
  window.parent.postMessage({ type: 'matheforscher:complete' }, '*');
}
// im UI dann: button.onclick = notifyComplete;

Das war's. Dein App ist jetzt „matheforscher-aware".

Für die Reichhaltigkeit empfehlen wir, zusätzlich state und action zu senden — siehe unten.

Noch schneller mit dem SDK: Statt das Protokoll von Hand zu implementieren, kannst du das offizielle Mini-SDK einbinden (<script src="https://mathe.digital/sdk/forscher-protokoll.js">, zero-deps, ~8 KB). Es übernimmt Embedding-Detection, hello/ready-Handshake, set-config-Acks und State-Debouncing komplett — siehe SDK-Doku bzw. die Demo unter /dev/sdk-demo.html.


Vollständiger Auto-Init (alles in einem)

Wenn du ohne Umweg gleich alle Plattform-Mechanik bedienen willst, kopiere diesen Block in deine App. Er deckt Erkennung, Hello-Antwort, Send-Helper und Screenshot-on-Request in einem ab — alle Browser, ohne weitere Konfiguration:

// 1) Plattform-Erkennung (drei redundante Signale)
let inMatheforscher =
  new URLSearchParams(location.search).get('matheforscher') === '1' ||
  location.hash.includes('matheforscher=1') ||
  window.self !== window.top;

// 2) Sicherer Send-Helper (no-op, wenn nicht eingebettet)
const send = (msg) => inMatheforscher && window.parent.postMessage(msg, '*');

// 3) Embedded-CSS-Klasse — eigene Navigation/Header ggf. ausblenden
if (inMatheforscher) document.body.classList.add('embedded');

// 4) Aktuelle Konfiguration aus URL-Parametern lesen (Beispiel — passe an)
const params = new URLSearchParams(location.search);
const config = {
  zr: parseInt(params.get('zr') || '20', 10),
  modus: params.get('modus') || 'frei'
};
const supportedParams = {
  zr:    { type: 'number', default: 20, values: [10, 20, 100], label: 'Zahlenraum' },
  modus: {
    type: 'string', default: 'frei', values: ['frei', 'zerlegung', 'darstellung'],
    label: 'Übungsmodus',
    description: 'frei = offenes Legen, zerlegung = Zahlzerlegungen finden, darstellung = Darstellungswechsel üben'
  }
};

// 5) Plattform-Events empfangen
window.addEventListener('message', async (e) => {
  const t = e.data?.type;
  if (typeof t !== 'string' || !t.startsWith('matheforscher:')) return;

  // 5a) Hello → mit Ready antworten (zeigt der Plattform: App ist bereit + Config)
  if (t === 'matheforscher:hello') {
    inMatheforscher = true;
    document.body.classList.add('embedded');
    e.source?.postMessage(
      { type: 'matheforscher:ready', payload: { config, supportedParams } },
      e.origin || '*'
    );
    return;
  }

  // 5b) Screenshot-Request → DOM oder Canvas der App ablichten
  if (t === 'matheforscher:request-screenshot') {
    const canvas = document.querySelector('canvas');
    let dataUrl;
    if (canvas) {
      dataUrl = canvas.toDataURL('image/png');
    } else {
      // html2canvas-pro lokal bündeln (npm i html2canvas-pro), nicht von CDN —
      // strenge CSPs blockieren CDN-Imports. Wenn deine App keine CSP setzt,
      // funktioniert auch: const html2canvas = (await import('https://cdn.jsdelivr.net/npm/html2canvas-pro@2/+esm')).default;
      const html2canvas = (await import('html2canvas-pro')).default;
      const c = await html2canvas(document.body, { useCORS: true, scale: 1 });
      dataUrl = c.toDataURL('image/png');
    }
    e.source?.postMessage(
      { type: 'matheforscher:screenshot', payload: { dataUrl } },
      e.origin || '*'
    );
  }
});

Damit funktioniert deine App in der Plattform ohne weitere Setup-Schritte — du musst nur noch deine fachlichen Events (progress, action, state, complete) per send(...) an den passenden Stellen im UI-Code aufrufen.


Plattform-Erkennung

Die Plattform sendet drei redundante Signale, sodass deine App die Einbettung auch dann erkennen kann, wenn z. B. ein Client-Side-Router (history.replaceState) den Query-String beim Bootstrap entfernt. Du brauchst nur eines davon zu prüfen — empfohlen: alle drei zusammen, dann ist es bombensicher.

Die drei Signale

Signal Wann Wo prüfen
1. Query-Parameter ?matheforscher=1 Beim ersten Laden new URLSearchParams(location.search).get('matheforscher')
2. Hash-Fragment #matheforscher=1 Beim ersten Laden, überlebt SPA-Routing location.hash.includes('matheforscher')
3. matheforscher:hello-postMessage Wiederholt nach iframe-load-Event (6× alle 500 ms) window.addEventListener('message', …)

Empfohlene Detection-Funktion

const inMatheforscher =
  new URLSearchParams(location.search).get('matheforscher') === '1' ||
  location.hash.includes('matheforscher=1') ||
  // Fallback: Wir sind in irgendeinem Iframe — kein Cross-Origin-Read nötig
  window.self !== window.top;

if (inMatheforscher) {
  document.body.classList.add('embedded');
  // ggf. eigene Navigation ausblenden, FullScreen-Mode aktivieren etc.
}

// Zusätzlich: hello-postMessage abfangen, damit auch nach Routing erkannt wird
window.addEventListener('message', (e) => {
  if (e.data?.type === 'matheforscher:hello') {
    document.body.classList.add('embedded');
    // Antworten — siehe `matheforscher:ready` weiter unten
  }
});

window.self !== window.top ist der zuverlässigste lokale Check und erlaubt — anders als window.parent.location o. ä. — keinen Cross-Origin-Read, löst also keine SecurityError-Warnung im Browser aus.

Sicher senden — nur wenn eingebettet, postMessage rufen:

function send(payload) {
  if (!inMatheforscher) return;
  window.parent.postMessage(payload, '*');
}

Hinweis zum targetOrigin: Wir empfehlen '*' für maximale Kompatibilität. Die Plattform validiert auf Empfänger-Seite, dass der Sender (event.origin) zur registrierten App-URL passt.

Anti-Pattern: kein direkter Parent-DOM-Zugriff

Diese Zeile löst in Cross-Origin-Iframes (Standard-Fall) eine SecurityError-Warnung in der Browser-Konsole aus und gibt nichts Brauchbares zurück:

// ❌ NICHT MACHEN
const isHidden = window.parent.frameElement?.style.display === 'none';
const parentUrl = window.parent.location.href;
const parentDoc = window.top.document;

Browser blockieren jeden direkten Zugriff auf Properties eines Fremd-Origin-Fensters. Verwende stattdessen postMessage und/oder die Signale oben — die funktionieren ohne Sicherheitswarnung.


iframe-Berechtigungen

Die Plattform bettet deine App so ein:

<iframe
  src="…?matheforscher=1#matheforscher=1"
  allow="microphone; camera; clipboard-write; fullscreen"
></iframe>

Daraus folgt für dich konkret:

Funktion Funktioniert in deiner App?
getUserMedia für Mikrofon ✅ ja, mit User-Geste
getUserMedia für Kamera ✅ ja, mit User-Geste
navigator.clipboard.writeText(...) ✅ ja
element.requestFullscreen() ✅ ja
localStorage / sessionStorage / Cookies (auf deiner Origin) ✅ ja, normal nutzbar
fetch zu deiner eigenen Origin ✅ ja
fetch zu Drittanbieter-APIs ✅ ja, sofern CORS passt
Cross-Origin-Zugriff aufs Parent-Fenster ❌ nein (Same-Origin-Policy)
display-capture (Screen-Sharing), payment, usb, geolocation etc. ❌ nicht freigeschaltet

Es ist kein sandbox-Attribut gesetzt, deine App läuft also unrestricted. Wenn du eine zusätzliche Permission im allow= brauchst (z. B. geolocation), nimm Kontakt auf — wir ergänzen sie.


Konfiguration via Query-Parameter

Im App-Pool kann die Lehrkraft pro App Default-Parameter hinterlegen (z.B. zr=20, farbe=blau). Diese werden beim Einbetten als Query-Parameter an deine App-URL angehängt — gemeinsam mit dem matheforscher=1-Marker.

Deine App liest sie via URLSearchParams:

const params = new URLSearchParams(location.search);
const zr = parseInt(params.get('zr') || '20', 10);
const farbe = params.get('farbe') || 'blau';
// initialisiere die App entsprechend

Was du dokumentieren solltest:

  • In deiner App-Pool-Beschreibung (Feld „Was kann das Kind in der App tun?"): liste die unterstützten Parameter mit Default-Werten und gültigen Wertebereichen.
  • Beispiel-Block für die App-Pool-Beschreibung:
## Unterstützte URL-Parameter
- `zr` (Zahl, default 20): Zahlraum (10, 20, 100)
- `farbe` (String, default 'blau'): Akzentfarbe ('blau', 'rot', 'grün')
- `zeigeHilfe` (Boolean, default false): blendet Hilfe-Tooltips ein
- `modus` (String, default 'frei'): 'frei' | 'zerlegung' | 'darstellung'

So kann die Lehrkraft beim Auftrag-Erstellen die App passend konfigurieren — und die KI hat im Generator-Modal Kontext, was möglich ist.

Auto-Config beim Start: Damit die Plattform (und die KI im State-Check) weiß, mit welcher Konfiguration deine App läuft, sende beim Start ein matheforscher:ready-Event (siehe nächste Sektion). So muss die Lehrkraft nicht raten, ob ihre Parameter angekommen sind.


App-Manifest beim ready-Event

Eine App kann sich beim ersten matheforscher:ready selbst beschreiben. Die Plattform schlägt die gemeldeten Felder beim Anlegen der App im App-Pool automatisch vor — die Lehrkraft kann jeden Wert vor dem Speichern überschreiben. Bei späteren App-Updates lässt sich das Manifest per Klick neu abrufen; die Plattform vergleicht es per SHA-256-Hash mit dem zuletzt adoptierten Stand und zeigt die geänderten Felder zur Pro-Feld-Übernahme an.

Manifest-Payload

window.parent.postMessage({
  type: 'matheforscher:ready',
  payload: {
    config: { /* aktueller Lauf, wie bisher */ },
    supportedParams: { /* wie bisher — Schema-Definition */ },
    manifest: {                                   // NEU, alles optional
      name: 'Mini-Rechenfeld',
      description: 'Plättchen ins Zwanzigerfeld legen — Zahlzerlegungen sehen.',
      kindActions: 'Plättchen setzen/entfernen, Reset, Anzahl ablesen.',
      categories: ['Arithmetik', 'Zahlzerlegung'],
      stateSchema: 'state.felder als 2D-Array (0=leer, 1=blau)…',
      defaultParams: { zr: '20', modus: 'frei' },
      iconUrl: '/icon.png',                       // relativ oder absolut
      infoUrl: 'https://urff.app/rechenfeld/doku',
      version: '1.4.0',                           // Freitext (semver oder ISO-Datum)
      highlightTargets: [                           // NEU: hervorhebbare Elemente
        { id: 'summe', label: 'die Summenanzeige oben' }
      ],
      performableActions: [                         // NEU: fern-aufrufbare Aktionen
        { action: 'place', target: 'zelle', label: 'eine Zelle füllen', payloadSchema: 'payload {row,col}' },
        { action: 'reset', label: 'das Feld leeren' }
      ]
    }
  }
}, e.origin || '*');

Felder

Feld Typ Bedeutung
name string Anzeigename im App-Pool
description string 1–2 Sätze, was die App didaktisch leistet
kindActions string „Was kann das Kind in der App tun?" — Prosa für Lehrkräfte und KI
categories string[] Themenbereiche (z.B. „Arithmetik", „Stellenwert")
stateSchema string Prosa-Beschreibung des per matheforscher:state gesendeten App-Zustands, damit die KI ihn interpretieren kann
defaultParams Record<string,string> Empfohlene Default-Werte als key=value-Paare
supportedParams ParamSchema Wie heute (kann auch in payload.supportedParams außerhalb von manifest stehen — beide werden akzeptiert)
iconUrl string App-Icon (absolut oder relativ zur App-URL)
infoUrl string Doku-Seite / About-Page für die App
version string Freitext-Version, hilft Entwickler:innen beim Tracking — die Plattform nutzt aber primär einen Inhalts-Hash
highlightTargets HighlightTarget[] Vom Arbeitsmittel hervorhebbare Elemente, je { id, label }. Nicht-leer = App unterstützt matheforscher:highlight.
performableActions PerformableAction[] Fern-aufrufbare Aktionen, je { action, target?, label, payloadSchema? }. Nicht-leer = App unterstützt matheforscher:perform.
didaktik object v2.9 — maschinenlesbare didaktische Selbstauskunft: { klassenstufen?: number[] /* 1–4 */, zahlenraeume?: string[] /* 'ZR10'…'ZR1M' */, darstellungsebenen?: string[] /* 'enaktiv'|'ikonisch'|'symbolisch' */, inhaltsbereiche?: string[], kompetenzen?: string[] }. Speist Auftrags-Generator, App-Passungs-Check und Bibliotheks-Filter.
stateSchemaV2 object v2.9 — strukturiertes State-Schema: { jsonSchema?: object /* echtes JSON-Schema */, prosa?: string /* KI-Interpretationshilfe */, beispiele?: Array<{ state, bedeutung: string }> /* Few-Shot-Paare für die KI, max 5 */ }. Ergänzt (ersetzt nicht) das Prosa-stateSchema.
protocolLevel number v2.9 — selbst-deklariertes Conformance-Level: 0 = nur iframe · 1 = konfigurierbar (ready+supportedParams) · 2 = beobachtbar (state+action) · 3 = KI-integriert (beschreibung, highlight/perform) · 4 = integriert (context, ergebnis, report).
ki object v3.2 — App nutzt den KI-Kanal des Hosts: { beschreibung: 'Tutor-Hinweise am Zahlenstrahl', faehigkeiten: ['text'] }. beschreibung erscheint in der Lehrkraft-Ansicht des Hosts, faehigkeiten nennt, was die App braucht (text, json, stream, vision). Details im Abschnitt Protokoll v3.2.

Regeln

  • Jedes Feld ist optional. Ein nicht-vorhandener Key bedeutet „App macht keine Aussage"; ein leerer Wert bedeutet „App meldet bewusst Leere".
  • Apps können das Manifest beim regulären matheforscher:ready mitsenden oder nur als Antwort auf einen matheforscher:discover-Ping liefern — die Plattform akzeptiert beides.
  • Bestehende Apps, die nur supportedParams ohne manifest schicken, funktionieren unverändert weiter.

Quickstart

Wenn deine App noch gar nichts implementiert hat, reicht dieser Block:

window.addEventListener('message', (e) => {
  if (e.data?.type === 'matheforscher:hello' || e.data?.type === 'matheforscher:discover') {
    e.source?.postMessage({
      type: 'matheforscher:ready',
      payload: {
        manifest: {
          name: 'Deine App',
          description: 'Kurze Beschreibung in 1–2 Sätzen.',
          kindActions: 'Was die Kinder tun können.',
          categories: ['Arithmetik'],
          version: '1.0.0'
        }
      }
    }, e.origin || '*');
  }
});

Event-Typen

Alle Events sind JavaScript-Objekte mit einem type-Feld, das mit 'matheforscher:' beginnt. Andere Felder sind event-spezifisch.

matheforscher:progress

Wann senden: Bei jedem konstruktiven Zwischenschritt, der für die Lehrkraft sichtbar sein sollte.

send({
  type: 'matheforscher:progress',
  teilaufgabeHinweis: '10 = 7 + 3',   // Pflicht: kurze Beschreibung des Fortschritts
  payload: { /* optional, app-spezifisch */ }
});

Plattform-Effekt: Im Schüler-UI erscheint ein kleiner grüner Chip („✓ 10 = 7 + 3"). Im Lehrkraft-Dashboard wird der Hinweis im Aktivitätslog angezeigt.

Beispiele:

  • Eine Zahlzerlegung wurde gefunden: '10 = 7 + 3'
  • Eine Form wurde korrekt gelegt: 'Quadrat aus 4 Plättchen'
  • Eine Aufgabe wurde richtig gerechnet: '17 + 8 = 25'

matheforscher:ready

Wann senden: Genau einmal beim App-Start, sobald deine App ihre Konfiguration aus den URL-Parametern eingelesen und initialisiert hat.

send({
  type: 'matheforscher:ready',
  payload: {
    config: {
      // die aktuelle Konfiguration deiner App, nachdem sie die URL-Parameter angewandt hat
      zr: 20,
      farbe: 'blau',
      modus: 'zerlegung'
    },
    supportedParams: {
      // Schema deiner unterstützten Parameter (gibt der Plattform/KI Kontext für künftige Aufträge)
      zr:           { type: 'number', default: 20, values: [10, 20, 100], label: 'Zahlenraum' },
      anzahl:       { type: 'number', default: 12, min: 1, max: 30, label: 'Plättchen-Anzahl' },
      farbe:        { type: 'string', default: 'blau', values: ['blau', 'rot', 'grün'], label: 'Plättchen-Farbe' },
      zeigeHilfe:   { type: 'boolean', default: false, label: 'Strukturhilfe',
                      description: 'Blendet die Fünfer-Gliederung ein — nimmt die Zählstruktur vorweg' },
      modus:        { type: 'string', default: 'frei', values: ['frei', 'zerlegung', 'darstellung'], label: 'Übungsmodus',
                      description: 'frei = offenes Legen, zerlegung = Zahlzerlegungen finden, darstellung = Darstellungswechsel üben' }
    }
  }
});

Schema-Felder pro Parameter:

Feld Pflicht Bedeutung
type ja 'number' | 'string' | 'boolean'
default empfohlen Wert, mit dem die App ohne Parameter startet
values optional erlaubte Werte (geschlossene Liste)
min / max optional Grenzen für number-Parameter ohne values-Liste
label empfohlen menschenlesbarer Kurzname (erscheint im Konfigurations-UI der Lehrkraft)
description empfohlen was der Parameter fachlich/didaktisch bewirkt — ein Satz genügt

Warum label/description wichtig sind: Die Plattform konfiguriert deine App auch KI-gestützt — beim Erstellen eines Forscherauftrags schlägt die KI passende Parameter-Werte vor (z. B. Zahlenraum hochziehen, Strukturhilfe abschalten, Progression über Teilaufgaben). Die KI sieht dabei genau dieses Schema. Ein Key wie modus allein sagt ihr nichts — erst description macht die didaktische Wirkung verständlich. Je besser deine Parameter beschrieben sind, desto passgenauer wird deine App konfiguriert.

Plattform-Effekt:

  • Die Plattform speichert config und supportedParams im Schüler-Store.
  • Beim KI-Check bekommt die KI die aktuelle App-Config als Kontext — kann das Kind besser einschätzen.
  • Im Lehrkraft-Dashboard kann später angezeigt werden, womit die App konkret läuft (zur Verifikation).
  • Die KI-gestützte Auftrag-Erstellung nutzt das Schema (inkl. label/description/min/max), um deine App passgenau zu konfigurieren.

Wichtig: payload.config und payload.supportedParams sind beide optional, aber empfohlen. Wenn deine App keine Parameter unterstützt: sende { type: 'matheforscher:ready' } ohne Payload — die Plattform weiß dann zumindest, dass die App initialisiert ist.

matheforscher:complete

Semantik: Informativ, nicht autoritativ. Die Plattform erkennt Erfolg primär plattformseitig über state + Aktionen + die von der Lehrkraft definierte Erfolgs-Bedingung bzw. das Ziel eines Material-Schritts — nicht allein über dieses Event. complete ist ein optionaler Hinweis deiner App, kein verbindliches Abschluss-Signal.

Wann senden: Nur wenn deine App selbst mit Sicherheit weiß, dass die Aufgabe abgeschlossen ist (z.B. alle Felder korrekt ausgefüllt, Zielzahl exakt erreicht). In allen anderen Fällen reicht state + action — die Plattform entscheidet über Erfolg anhand der KI und der Vorgaben der Lehrkraft.

send({
  type: 'matheforscher:complete',
  payload: { /* optional, z.B. Endstand */ }
});

Plattform-Effekt (seit Doku 3.3): complete hakt die Teilaufgabe nicht mehr selbst ab. Bei Schritten „Am Material zeigen" drückt das Kind „Fertig – schau nach!"; ist seit Beginn des Schritts ein complete eingetroffen, gilt der Schritt als vom Arbeitsmittel bestätigt:

  • Hat die Lehrkraft keinen eigenen Zielsatz („Am Arbeitsmittel fertig heißt …") hinterlegt, ist der Schritt damit sofort bestanden.
  • Mit eigenem Zielsatz prüft die KI zusätzlich den state — der Zielsatz darf mehr verlangen als dein App-Ziel (z.B. „243 mit mehr als 10 Zehnern").

Boolesches state.erreicht hat Vorrang: Meldet dein aktueller state ein Feld erreicht: true | false, zählt nur dieses — ein älteres complete wird ignoriert, denn das Kind kann das Ziel nach dem complete wieder zerstört haben. Empfehlung: Apps mit selbst geprüftem Ziel senden erreicht in jedem state.

send({ type: 'matheforscher:state', state: { zahl: 243, zehner: 14, erreicht: true }, beschreibung: '243 gelegt, Ziel erreicht' });

Hinweis zur plattformseitigen Erfolgs-Erkennung: Lehrkräfte können pro Auftrag eine textuelle Erfolgs-Bedingung und pro Material-Schritt einen Zielsatz definieren (z.B. „Alle elf Zerlegungen der 10 sind dargestellt"). Die KI prüft anhand des aktuellen state und der Aktionen, ob diese erfüllt sind — unabhängig davon, ob die App ein complete-Event gesendet hat.

Ziel-Parameter (Konvention)

Kann dein Arbeitsmittel ein Ziel selbst prüfen, nimm den Zielwert als URL-Parameter entgegen und deklariere ihn in supportedParams unter einem dieser Namen:

Name Typische Verwendung
ziel Zielzahl oder Zielzustand (z.B. Stellenwerte: ziel=243)
zielzahl Zielzahl, wenn ziel schon anders belegt ist
zielzeit Zielzeit HH:MM (z.B. Klassenzimmeruhr: zielzeit=09:10)
target englische Variante von ziel
goal englische Variante von ziel
karte Forscherkarte mit eingebautem Ziel (z.B. Geobrett: karte=…)

Ist einer dieser Parameter in supportedParams deklariert und für den Schritt gesetzt, zeigt die Plattform der Lehrkraft „Arbeitsmittel prüft selbst" und verlässt sich auf complete bzw. state.erreicht. Melde das Erreichen des Ziels dann zuverlässig (am besten über erreicht im state).

matheforscher:error

Wann senden: Bei App-internen Fehlern, die das Kind nicht selbst beheben kann.

send({
  type: 'matheforscher:error',
  message: 'Konnte Daten nicht laden'
});

Plattform-Effekt: Fehler-Banner im Schüler-UI. Selten nötig — meistens reicht App-internes Error-Handling.

matheforscher:action

Wann senden: Bei jeder bedeutsamen Schüler-Handlung — Plättchen setzen, Karte umdrehen, Würfel werfen, Eingabe machen.

send({
  type: 'matheforscher:action',
  action: 'place',                    // kurzer Verb-Code
  target: 'plättchen',                // optional: was wurde manipuliert
  payload: { row: 3, col: 5, color: 'rot' },  // optional: app-spezifische Details
  beschreibung: 'Kind legt ein rotes Plättchen in Zeile 4, Spalte 6'  // optional, ab v2.9 — siehe unten
});

Plattform-Effekt: Action wird im Schüler-Store gespeichert (max 200 in Memory, letzte 50 in LocalStorage). Wenn das Kind den „🤖 Wie weit bin ich?"-Button drückt, schickt die Plattform die letzten 20 Actions an die KI als Kontext.

beschreibung (optional, empfohlen — semantische Selbstbeschreibung): Ein kurzer deutscher Satz, was die Handlung bedeutet. Deine App kennt ihre Semantik am besten — mit beschreibung muss die Plattform-KI dein Roh-payload nicht mehr deuten. Senden alle Actions eine beschreibung, bekommt die KI die Beschreibungen als Liste statt Roh-JSON (weniger Fehlinterpretationen, besseres Feedback fürs Kind).

Konvention für action-Codes:

Verb Bedeutung Beispiele
place etwas positionieren Plättchen, Stein, Zahl
remove etwas entfernen Plättchen weg, Eingabe löschen
move etwas bewegen Drag&Drop, Schieben
toggle An/Aus, Ja/Nein Häkchen, Markierung
set Wert setzen Eingabefeld, Slider
select etwas auswählen Karte, Option
reset Zurücksetzen Feld leeren
try Probier-Aktion Versuch, Lösungsversuch
check Prüfung anstoßen Eigenes Lösen prüfen
save etwas in eine Sammlung übernehmen (ab Doku 3.3, target: 'archiv') Lösung ins Archiv, Figur sammeln

Halte dich locker an diese Verben — eigene gehen aber auch (z.B. 'roll' beim Würfel).

matheforscher:hello (Plattform → App)

Wann empfangen: Direkt nach dem load-Event deines Iframes, danach bis zu 6× alle 500 ms wiederholt — solange, bis deine App mit matheforscher:ready antwortet (oder die 6 Versuche aufgebraucht sind).

Format:

{ "type": "matheforscher:hello", "host": "matheforscher", "version": 1 }

Empfänger:

window.addEventListener('message', (e) => {
  if (e.data?.type !== 'matheforscher:hello') return;
  // Wir sind sicher in Matheforscher eingebettet — auch wenn URL-Parameter fehlen.
  document.body.classList.add('embedded');
  // Antworten mit `ready` — wichtig, damit das Pingen aufhört und
  // die Plattform unsere Konfiguration kennt.
  e.source?.postMessage({
    type: 'matheforscher:ready',
    payload: {
      config: { /* aktuelle App-Konfiguration */ },
      supportedParams: { /* Schema, siehe matheforscher:ready */ }
    }
  }, e.origin);
});

Warum dieses Event existiert: Manche Apps räumen den Query-String beim Bootstrap weg (Client-Side-Router). Das Hello-postMessage ist die zuverlässigste Erkennung — unabhängig von URL und Routing.

Plattform-Effekt: Sobald deine App matheforscher:ready zurücksendet, stoppt die Plattform das Hello-Pingen und speichert deine Konfiguration im Schüler-Store.

matheforscher:request-screenshot (Plattform → App)

Wann empfangen: Wenn das Kind im Schüler-UI „📸 App-Screenshot jetzt machen" drückt UND deine App nicht same-origin mit der Plattform ist.

// Empfänger in deiner App:
window.addEventListener('message', async (e) => {
  if (e.data?.type !== 'matheforscher:request-screenshot') return;
  // Dein Screenshot-Code, z.B. via html2canvas oder eigenes Canvas-Rendering
  const dataUrl = await dieAppMachtSelberEinenScreenshot();
  e.source.postMessage({
    type: 'matheforscher:screenshot',
    payload: { dataUrl }
  }, e.origin);
});

Implementierungs-Vorlagen:

a) Wenn deine App auf einem <canvas> rendert (z.B. ein eigenes Rechenfeld):

window.addEventListener('message', (e) => {
  if (e.data?.type !== 'matheforscher:request-screenshot') return;
  const canvas = document.querySelector('canvas');
  if (!canvas) return;
  const dataUrl = canvas.toDataURL('image/png');
  e.source.postMessage({
    type: 'matheforscher:screenshot',
    payload: { dataUrl }
  }, e.origin);
});

b) Wenn deine App DOM-basiert rendert (HTML/CSS):

window.addEventListener('message', async (e) => {
  if (e.data?.type !== 'matheforscher:request-screenshot') return;
  // EMPFOHLEN: html2canvas-pro lokal bündeln (`npm i html2canvas-pro`).
  // Strenge Content-Security-Policies (z. B. mit `script-src 'self'`)
  // blockieren sonst den Import von einer fremden CDN.
  const html2canvas = (await import('html2canvas-pro')).default;
  // Alternative ohne Build-Schritt (nur in Apps OHNE strenge CSP):
  // const html2canvas = (await import('https://cdn.jsdelivr.net/npm/html2canvas-pro@2/+esm')).default;
  const canvas = await html2canvas(document.body, { useCORS: true, scale: 1 });
  const dataUrl = canvas.toDataURL('image/png');
  e.source.postMessage({
    type: 'matheforscher:screenshot',
    payload: { dataUrl }
  }, e.origin);
});

c) Wenn deine App eine Mischform ist: kombiniere beide Ansätze. Für Hauptbereiche Canvas, für umrahmende UI html2canvas. Oder nimm einfach immer html2canvas.

Hinweis zu Bildgröße: Default-PNG kann groß werden. Wenn deine App nicht hochauflösend rendert, reicht JPEG mit canvas.toDataURL('image/jpeg', 0.85) — kleinere Datei, schnellerer Upload.

matheforscher:screenshot (App → Plattform)

Format: { type: 'matheforscher:screenshot', payload: { dataUrl: string } }

dataUrl ist ein Standard-Data-URL (data:image/png;base64,...). Die Plattform decodiert und lädt das Bild in Storage hoch.

Plattform-Effekt: Erscheint im Lehrkraft-Dashboard wie ein manueller Upload.

matheforscher:state

Wann senden: Nach jeder relevanten Zustandsänderung — sodass die Plattform jederzeit den aktuellen App-Stand kennt.

send({
  type: 'matheforscher:state',
  state: { /* beliebiges JSON-Objekt, app-spezifisch */ },
  beschreibung: '7 Plättchen gelegt: 3 oben, 4 unten — Zerlegung 3+4 sichtbar'  // optional, ab v2.9
});

Plattform-Effekt: Der zuletzt gesendete State wird im Schüler-Store gehalten. Beim KI-Check wird er an die KI übergeben (max 4 KB JSON).

beschreibung (optional, empfohlen — semantische Selbstbeschreibung): Ein kurzer deutscher Satz, was der aktuelle Zustand bedeutet („Zerlegung 3+4 sichtbar", „Feld ist leer"). Die Plattform stellt ihn der KI vor dem Roh-JSON zur Verfügung — Tutor und Auswertung beziehen sich dann direkt auf das Material, ohne dein State-Format raten zu müssen. Das ist der wirksamste Einzel-Schritt, um deine App „KI-optimal" zu machen.

Wichtig:

  • State muss JSON-serialisierbar sein (keine Funktionen, keine DOM-Knoten, keine Klassen).
  • Halte den State so kompakt wie möglich, aber so vollständig wie nötig, damit die KI ihn verstehen kann.
  • Bei Bedarf zwischenstand komprimieren — z.B. ein Rechenfeld als 2D-Array [[1,1,0],[1,1,0]] statt verbose Cell-Objects.
  • Drosseln: State nur senden, wenn er sich tatsächlich geändert hat. Bei kontinuierlichen Änderungen (Drag) am Ende der Geste senden, nicht bei jedem Pixel.

Baustein „Sammlung" (ab Doku 3.3)

Apps mit Sammel- oder Archivfunktion (das Kind legt gefundene Lösungen in eine Liste) melden die ganze Sammlung als Liste im state — nicht nur den zuletzt gespeicherten Eintrag. Erst damit werden Aufgaben wie „Sammle alle Zerlegungen der 10" oder „Finde drei verschiedene Figuren mit Fläche 4" am Material prüfbar.

  • State: ein Array mit einem Objekt pro Eintrag; pro Eintrag die prüfrelevanten Eigenschaften, nicht nur ein Bild oder eine ID.
    • Vorbild Teiler: state.archiv: [{ zahl: 12, teiler: [1, 2, 3, 4, 6, 12] }, …]
    • Geobrett: state.archiv: [{ figur: 'Dreieck', flaeche: 2, kongruenzSignatur: '…' }, …] — die Signatur macht „anders als" bzw. „keine doppelt" prüfbar.
  • Aktionen: action: 'save' beim Hinzufügen, action: 'remove' beim Entfernen, jeweils mit target: 'archiv' und dem Eintrag im payload:
send({ type: 'matheforscher:action', action: 'save', target: 'archiv', payload: { zahl: 12, teiler: [1, 2, 3, 4, 6, 12] }, beschreibung: 'Teiler von 12 gespeichert' });
send({ type: 'matheforscher:action', action: 'remove', target: 'archiv', payload: { zahl: 12 }, beschreibung: 'Teiler von 12 aus der Sammlung entfernt' });
send({ type: 'matheforscher:state', state: { archiv: [ /* alle Einträge */ ] }, beschreibung: '3 Zahlen gesammelt: 12, 18, 20' });
  • Kompakt bleiben: Die Plattform reicht höchstens ~4 KB State-JSON an die KI weiter. Lieber knappe Eigenschaften je Eintrag als Rohdaten.
  • beschreibung nennen, was in der Sammlung liegt („3 Zahlen gesammelt: 12, 18, 20") — die KI stützt sich zuerst darauf.

matheforscher:discover (Plattform → App)

Wann empfangen: Bei der App-Registrierung (Lehrkraft-Bereich → Apps → „🔄 Konfiguration abrufen"). Die Plattform lädt deine App in einem versteckten Iframe und möchte dein supportedParams-Schema sehen — damit beim Auftrag-Bauen die KI sinnvolle Defaults pro Teilaufgabe vorschlagen kann.

{ type: 'matheforscher:discover', host: 'matheforscher', version: 1 }

Antwort: Sende ein normales matheforscher:ready mit payload.supportedParams zurück. Wenn deine App bereits beim Start matheforscher:ready sendet, ist discover redundant aber unschädlich — antworte einfach noch einmal mit demselben Schema.

Plattform-Effekt: Die Plattform speichert supportedParams auf dem App-Dokument. Bei der nächsten KI-Auftragsgenerierung steht das Schema dem Modell als Kontext zur Verfügung.

matheforscher:set-config (Plattform → App)

Wann empfangen: Beim Wechsel zwischen Teilaufgaben — wenn die Lehrkraft pro Aufgabe unterschiedliche Parameter konfiguriert hat. Anstelle eines Iframe-Reloads (mit State-Verlust) bittet die Plattform deine App, ihre Konfiguration live umzuschalten.

{
  type: 'matheforscher:set-config',
  payload: {
    config: { zr: 100, modus: 'darstellung' },
    reason: 'teilaufgabe-wechsel'  // oder 'auftrag-start'
  }
}

App-Pflicht: Innerhalb von 500 ms mit matheforscher:configured antworten (siehe nächster Abschnitt), sonst macht die Plattform einen Reload mit neuen URL-Parametern.

// Empfänger in deiner App:
window.addEventListener('message', (e) => {
  if (e.data?.type !== 'matheforscher:set-config') return;
  applyConfig(e.data.payload.config);  // deine App-Logik
  e.source.postMessage({
    type: 'matheforscher:configured',
    payload: { config: e.data.payload.config }
  }, e.origin);
});

Tipp: Wenn ein Wechsel der Config einen vollständigen Reset des App-Inhalts erfordert (z.B. anderer Zahlenraum), führe ihn selbst durch — der State-Verlust ist dann erwünscht und nicht schlimm.

matheforscher:configured (App → Plattform)

Wann senden: Als Antwort auf matheforscher:set-config, nachdem die neue Konfiguration in der App angewandt wurde.

e.source.postMessage({
  type: 'matheforscher:configured',
  payload: { config: { zr: 100, modus: 'darstellung' } }
}, e.origin);

Plattform-Effekt: Plattform stoppt den 500-ms-Timer und verzichtet auf den Reload. Der angegebene config-Snapshot wird im Schüler-Store gespeichert (sichtbar im KI-State-Check und Lehrkraft-Dashboard).

Fallback: Apps, die das Event nicht senden (z.B. ältere Apps ohne Live-Reconfigure-Support), bekommen automatisch einen Iframe-Reload mit neuen URL-Parametern — der bisherige Pfad funktioniert unverändert weiter.

matheforscher:highlight (Plattform → App)

Wann empfangen: Wenn die KI sprachbegleitend ein Element hervorheben möchte (z.B. „Schau mal auf die Summenanzeige"). Wird nur gesendet, wenn deine App im Manifest highlightTargets deklariert hat.

Format:

{
  type: 'matheforscher:highlight',
  payload: {
    targets: ['summe'],          // Pflicht (außer bei clear): 1..n deklarierte IDs
    style: 'pulse',              // optional: 'pulse'|'outline'|'arrow'|'spotlight'|'point'|<eigener String>
    label: 'Schau hier',         // optional: kurzer Text, den die App am Element zeigen darf
    durationMs: 4000,            // optional: automatisch nach N ms entfernen (fehlt/0 = bis zum nächsten Befehl)
    intensity: 'normal',         // optional: 'subtle'|'normal'|'strong'
    clearPrevious: true,         // optional: Wunsch, vorherige Hervorhebungen zuerst zu entfernen. Kein Plattform-Default — fehlt das Feld, entscheidet deine App (das Empfänger-Beispiel unten löscht ohnehin immer zuerst).
    reason: 'tutor'              // optional: Herkunft (App-Analytics, ignorierbar)
  }
}

Löschen aller Hervorhebungen: { type: 'matheforscher:highlight', payload: { clear: true } }

Regeln (robust & abwärtskompatibel):

  • Du musst nur targets verstehen — alle anderen Felder sind optionale Wünsche, die du honorieren oder ignorieren darfst.
  • Unbekannte IDs still ignorieren. style ist eine Konvention, kein Enum — mappe sie auf deine eigene Optik.
  • Apps ohne Highlight-Support ignorieren den unbekannten type — nichts geht kaputt.

Style-Empfehlungen:

Style Gedacht für
pulse sanftes Pulsieren (Default-Wahl für „schau hier")
outline Rahmen/Umrandung
spotlight Umgebung abdunkeln, Element hervortreten lassen
arrow Pfeil/Marker, der auf das Element zeigt
point dezenter Punkt/Indikator

Empfänger (DOM-Beispiel):

window.addEventListener('message', (e) => {
  if (e.data?.type !== 'matheforscher:highlight') return;
  const p = e.data.payload || {};
  document.querySelectorAll('.mf-highlight').forEach((el) => el.classList.remove('mf-highlight'));
  if (p.clear) return;
  const applied = [], ignored = [];
  for (const id of (p.targets || [])) {
    const el = document.getElementById(id);            // oder dein eigenes id→Element-Mapping
    if (el) { el.classList.add('mf-highlight'); applied.push(id); } else ignored.push(id);
  }
  if (p.durationMs) setTimeout(() => applied.forEach((id) => document.getElementById(id)?.classList.remove('mf-highlight')), p.durationMs);
  e.source?.postMessage({ type: 'matheforscher:highlighted', payload: { applied, ignored } }, e.origin || '*');
});

Damit das funktioniert, deklariere die hervorhebbaren Elemente im Manifest:

highlightTargets: [{ id: 'summe', label: 'die Summenanzeige oben' }]

matheforscher:perform (Plattform → App)

Wann empfangen: Wenn die KI eine Aktion am Material vorführen (mode: 'demo') oder echt ausführen (mode: 'execute') möchte — z.B. einen ersten Modell-Schritt zeigen. Wird nur gesendet, wenn deine App im Manifest performableActions deklariert hat.

Format:

{
  type: 'matheforscher:perform',
  payload: {
    mode: 'demo',                // 'demo' = vorführen OHNE echten Zustands-Change | 'execute' = echt anwenden
    steps: [                     // Pflicht: 1..n Schritte (Form wie das matheforscher:action-Event)
      { action: 'place', target: 'zelle', payload: { row: 0, col: 0 } },
      { action: 'place', target: 'zelle', payload: { row: 0, col: 1 } }
    ],
    stepDelayMs: 600,            // optional: Pause zwischen Schritten beim Abspielen
    label: 'So könntest du anfangen',  // optional: Begleittext, den die App zeigen darf
    reason: 'tutor'             // optional: Herkunft
  }
}

Semantik & Regeln:

  • mode: 'demo' — zeige die Schritte als Vorschau/Animation, OHNE den fachlichen Zustand zu ändern (kein state/complete als Folge). Das Kind macht es danach selbst.
  • mode: 'execute' — wende die Schritte echt an (du darfst danach wie bei Kind-Aktionen state/action senden).
  • Unbekannten mode gnädig behandeln (am besten als demo). Schritte mit unbekannter action/target überspringen, übrige ausführen.
  • execute ist NICHT idempotent — die Plattform sendet jede perform-Message genau einmal (keine Retries). demo ist gefahrlos wiederholbar.
  • Apps ohne Perform-Support ignorieren den unbekannten type.
  • Pro Schritt werden nur action, target und payload übermittelt — zusätzliche Felder an einem Schritt werden von der Plattform verworfen.

Empfänger (Beispiel):

window.addEventListener('message', async (e) => {
  if (e.data?.type !== 'matheforscher:perform') return;
  const p = e.data.payload || {};
  let applied = 0, skipped = 0;
  for (const step of (p.steps || [])) {
    const fn = appActionMap[step.action];              // dein action→Funktion-Mapping
    if (!fn) { skipped++; continue; }
    await fn(step.target, step.payload, { mode: p.mode || 'demo' });  // App entscheidet demo vs. execute
    applied++;
    if (p.stepDelayMs) await new Promise((r) => setTimeout(r, p.stepDelayMs));
  }
  e.source?.postMessage({ type: 'matheforscher:performed', payload: { mode: p.mode || 'demo', appliedSteps: applied, skippedSteps: skipped } }, e.origin || '*');
});

Deklariere die fern-aufrufbaren Aktionen im Manifest:

performableActions: [
  { action: 'place', target: 'zelle', label: 'eine Zelle füllen', payloadSchema: 'payload {row,col}' },
  { action: 'reset', label: 'das Feld leeren' }
]

matheforscher:highlighted (App → Plattform)

Optional. Bestätigt, welche Elemente hervorgehoben wurden. Nützlich für späteres Dashboard/KI-Feedback; die Plattform funktioniert auch ohne.

Format: { type: 'matheforscher:highlighted', payload: { applied: ['summe'], ignored: [] } }

matheforscher:performed (App → Plattform)

Optional. Bestätigt eine ausgeführte/vorgeführte Sequenz.

Format: { type: 'matheforscher:performed', payload: { mode: 'demo', appliedSteps: 2, skippedSteps: 0 } }


Schema-Beschreibung für den App-Pool

Damit die KI deine state- und action-Daten interpretieren kann, beschreibst du im App-Pool (Lehrkraft-Bereich → App registrieren → „Was sendet die App per postMessage?"), was deine App sendet. Die KI bekommt diese Prosa-Beschreibung mit jedem State-Check.

Empfohlene Struktur

## State-Format
state.{feldname1}: {Typ + Bedeutung}
state.{feldname2}: {Typ + Bedeutung}

## Actions
- action="{verb}" mit target="{ziel}" und payload={schema}: bedeutet …
- action="{verb}" …

## Erfolg / Lösung
Eine Aufgabe gilt als gelöst, wenn …

Beispiel: Rechenfeld

## State-Format
state.felder: 2D-Array, Form [zeilen][spalten].
  Werte: 0 = leer, 1 = blau gefüllt, 2 = rot gefüllt.
  Übliche Größen: 5×5, 10×10 (zwanzigerfeld), 10×10 (hunderterfeld).
state.gesamtAnzahl: Anzahl der gefüllten Felder (Zahl).
state.zerlegung: Wenn Aufgabentyp "Zerlegung", dann { links: 4, rechts: 6 }.

## Actions
- action="place" mit target="plättchen" und payload={row, col, color}:
  Kind setzt ein Plättchen in der genannten Zelle.
- action="remove" mit target="plättchen" und payload={row, col}:
  Kind entfernt ein Plättchen.
- action="reset": Alle Plättchen weg.
- action="check": Kind hat den "Prüfen"-Button gedrückt.

## Erfolg / Lösung
Bei Zerlegungs-Aufgaben: state.zerlegung.links + state.zerlegung.rechts == zielzahl.
Bei Anzahl-Aufgaben: state.gesamtAnzahl == zielzahl.
Es gibt keine globale Erfolgs-Bedingung — die hängt vom Auftrag ab.

Beispiel: Stellenwerttafel

## State-Format
state.bündel: { tausender: 0, hunderter: 2, zehner: 5, einer: 7 }.
state.zahlwert: errechnete Zahl, also tausender*1000 + hunderter*100 + ... = 257.
state.darstellung: 'plättchen' | 'striche' | 'zahlen' — wie das Kind gerade visualisiert.

## Actions
- action="add" mit target="zehner": Eine Zehner-Einheit hinzugefügt.
- action="remove" mit target="zehner": Eine Zehner-Einheit entfernt.
- action="bündeln" mit target="zehner": 10 Einer wurden zu 1 Zehner gebündelt.
- action="entbündeln" mit target="zehner": 1 Zehner wurde zu 10 Einern entbündelt.

## Erfolg / Lösung
Aufgabenabhängig — meist „state.zahlwert == zielzahl".

Tipps für die Schema-Beschreibung

  • Schreib in Prosa. Die KI versteht „2D-Array, 1 = blau" besser als JSON-Schema.
  • Gib Beispiele. Ein konkreter Beispiel-State hilft der KI mehr als abstrakte Definitionen.
  • Erkläre Fachbegriffe. Wenn die App „Bündeln" als spezielle Aktion kennt, beschreib's.
  • Bleib kurz. 200-400 Wörter sind ein guter Richtwert. Mehr lenkt eher ab.

Best Practices

1. Sende nur, wenn eingebettet

Spar dir das Geschick außerhalb der Plattform — keine zusätzlichen Console-Logs, keine Fremd-Origin-Aufrufe.

const isEmbedded = new URLSearchParams(location.search).has('matheforscher');
const send = (msg) => isEmbedded && window.parent.postMessage(msg, '*');

2. Drossel den State

Bei kontinuierlichen Änderungen (Drag, Slider) erst am Ende senden:

let stateTimer = null;
function queueStateUpdate() {
  if (stateTimer) clearTimeout(stateTimer);
  stateTimer = setTimeout(() => send({ type: 'matheforscher:state', state: getCurrentState() }), 250);
}

3. Action vs. Progress vs. Complete

Sender Plattform-UI Use-Case
action unsichtbar (im Hintergrund) Jede einzelne Handlung — Lernverlauf
progress Chip im Schüler-UI Bedeutsamer Zwischenschritt — Lehrkraft soll's sehen
complete Häkchen automatisch (informativ) Nur wenn die App selbst sicher weiß, dass die Aufgabe fertig ist

Beispiel-Sequenz im Rechenfeld bei „Zerlegung der 10":

action: place plättchen     (× viele, im Hintergrund)
state: { gesamtAnzahl: 7 }  (× viele, im Hintergrund)
action: place plättchen
state: { gesamtAnzahl: 10, zerlegung: { links: 7, rechts: 3 } }
progress: '10 = 7 + 3'      (Chip!) ← sichtbar für Lehrkraft + Kind

complete würde man hier NICHT senden, weil der Auftrag „Finde alle Zerlegungen" lautet und 7+3 nur eine ist.

Wann complete senden?

Sende complete nur, wenn deine App selbst mit Sicherheit weiß, dass die Aufgabe abgeschlossen ist — z.B. weil die Zielzahl exakt erreicht wurde oder weil alle Pflichtfelder korrekt ausgefüllt sind. In allen anderen Fällen reicht state + action — die Plattform entscheidet selbst, ob das Ziel der Lehrkraft erreicht ist, anhand der KI-gestützten Erfolgs-Erkennung über die vom der Lehrkraft definierte Erfolgs-Bedingung.

4. JSON-serialisierbar

State und payloads müssen JSON.stringify-fähig sein:

  • ✅ Numbers, strings, booleans, arrays, plain objects, null
  • ❌ Functions, Dates (statt dessen ISO-String), DOM-Nodes, Maps/Sets, undefined

5. Datenschutz: kein PII

Sende nichts, was identifizierbar ist. Insbesondere nichts vom Schüler-Pseudonym oder localStorage-Inhalten — die Plattform kennt das selbst und braucht es nicht von der App.

6. Höre auf das hello-Event statt nur URL zu prüfen

Wenn deine App ein Client-Side-Router-Bootstrap hat, der URL-Parameter wegräumt, ist ?matheforscher=1 schnell weg. Das matheforscher:hello-postMessage kommt nach dem App-Bootstrap und ist damit zuverlässig — siehe Detection-Snippet oben.

7. Niemals Parent-DOM direkt lesen

window.parent.location, window.top.document, window.parent.frameElement.style… etc. lösen in Cross-Origin-Iframes (Standard-Fall) SecurityError aus. Verwende ausschließlich window.postMessage für Kommunikation zur Plattform und window.self !== window.top für die reine Embedding-Erkennung.

8. Plattform → App: aktuelle Events

In der aktuellen Version sendet die Plattform diese Events an deine App: hello, discover, request-screenshot, set-config, highlight und perform. Alle sind optional zu behandeln — deine App ignoriert, was sie nicht unterstützt. Wenn du weitere Use-Cases brauchst, nimm Kontakt auf.


Komplettbeispiel: Mini-Rechenfeld

<!doctype html>
<html lang="de">
<head>
  <meta charset="utf-8">
  <title>Mini-Rechenfeld</title>
  <style>
    body { font-family: system-ui, sans-serif; padding: 16px; }
    .feld { display: grid; grid-template-columns: repeat(10, 28px); gap: 2px; margin: 16px 0; }
    .zelle { width: 28px; height: 28px; border: 1px solid #ccc; cursor: pointer; }
    .zelle.gefüllt { background: #1f5a8c; }
    button { padding: 8px 14px; font: inherit; cursor: pointer; }
  </style>
</head>
<body>
  <h1>Mini-Rechenfeld</h1>
  <div class="feld" id="feld"></div>
  <p>Anzahl gefüllt: <b id="anzahl">0</b></p>
  <button id="reset">Zurücksetzen</button>
  <button id="fertig">Fertig</button>

<script>
const ROWS = 5, COLS = 10;
const isEmbedded = new URLSearchParams(location.search).has('matheforscher');
const send = (msg) => isEmbedded && window.parent.postMessage(msg, '*');

let felder = Array.from({ length: ROWS }, () => Array(COLS).fill(0));

function render() {
  const el = document.getElementById('feld');
  el.innerHTML = '';
  for (let r = 0; r < ROWS; r++) {
    for (let c = 0; c < COLS; c++) {
      const z = document.createElement('div');
      z.className = 'zelle' + (felder[r][c] ? ' gefüllt' : '');
      z.onclick = () => toggle(r, c);
      el.appendChild(z);
    }
  }
  document.getElementById('anzahl').textContent = anzahl();
}

function anzahl() { return felder.flat().filter(Boolean).length; }

function toggle(r, c) {
  const wasFilled = felder[r][c] === 1;
  felder[r][c] = wasFilled ? 0 : 1;
  render();
  send({
    type: 'matheforscher:action',
    action: wasFilled ? 'remove' : 'place',
    target: 'plättchen',
    payload: { row: r, col: c }
  });
  send({
    type: 'matheforscher:state',
    state: { felder, gesamtAnzahl: anzahl() }
  });
  // wenn z.B. genau 10 Plättchen — als Progress melden
  if (anzahl() === 10) {
    send({ type: 'matheforscher:progress', teilaufgabeHinweis: '10 Plättchen gelegt' });
  }
}

document.getElementById('reset').onclick = () => {
  felder = Array.from({ length: ROWS }, () => Array(COLS).fill(0));
  render();
  send({ type: 'matheforscher:action', action: 'reset' });
  send({ type: 'matheforscher:state', state: { felder, gesamtAnzahl: 0 } });
};

document.getElementById('fertig').onclick = () => {
  send({ type: 'matheforscher:complete', payload: { gesamtAnzahl: anzahl() } });
};

render();
</script>
</body>
</html>

Passende Schema-Beschreibung für den App-Pool:

## State-Format
state.felder: 2D-Array Form [5][10] (5 Zeilen, 10 Spalten).
  Werte: 0 = leer, 1 = blau gefüllt.
state.gesamtAnzahl: Anzahl gefüllter Zellen (Zahl 0…50).

## Actions
- action="place" mit target="plättchen" und payload={row, col}: Kind hat ein Plättchen gesetzt.
- action="remove" mit target="plättchen" und payload={row, col}: Kind hat ein Plättchen entfernt.
- action="reset": Alle Plättchen weg.

## Erfolg
Aufgabenabhängig — bei „Lege X Plättchen" gilt state.gesamtAnzahl == X.

Browser-Kompatibilität & Stolpersteine

Das Protokoll nutzt nur seit Jahren etablierte Standard-APIs und funktioniert in allen aktuellen Browsern:

Browser Mindest-Version Status
Chrome / Edge / Opera 90+ ✅ vollständig
Firefox 88+ ✅ vollständig
Safari (macOS) 14+ ✅ vollständig
Safari (iOS / iPadOS) 14+ ✅ vollständig
Samsung Internet 14+ ✅ vollständig

Konkret: window.postMessage, addEventListener('message', …), URLSearchParams, dynamic import(), canvas.toDataURL, navigator.clipboard.writeText sind in allen genannten Versionen verfügbar.

Damit's wirklich „von selbst" läuft

Wenn du den Auto-Init-Block eingebaut hast, brauchst du am Browser nichts weiter zu konfigurieren. Achte aber auf folgende typische Stolpersteine, die sonst zu stillen Ausfällen führen:

Stolperstein Symptom Lösung
Strenge CSP in deiner App (script-src 'self', connect-src 'self') import('https://cdn.jsdelivr.net/...') schlägt fehl, Screenshot-DOM-Fallback funktioniert nicht html2canvas-pro per npm install als lokale Dependency bündeln (siehe Auto-Init-Block)
X-Frame-Options: DENY oder restriktive frame-ancestors-CSP auf deinem Server Iframe lädt gar nicht, leere Fläche im Schüler-UI Auf deinem App-Server X-Frame-Options weglassen oder ALLOWALL setzen; CSP frame-ancestors * (oder explizit https://matheforscher-prod.web.app) erlauben
Tippfehler im type-String (z. B. mathforscher:state ohne e) Kein Eintrag im Lehrkraft-Dashboard, KI-Check liefert nichts Konsole mit Filter „matheforscher" prüfen; type muss exakt matheforscher:<name> lauten
Service Worker entfernt Query-Params App lädt, aber ?matheforscher=1 ist weg Hash-Signal #matheforscher=1 und matheforscher:hello-postMessage fangen das auf — nichts zu tun, solange dein Listener registriert ist
State nicht JSON-serialisierbar (Date, Map, Function, DOM-Node) KI-Check antwortet generisch, weil State leer ankommt Im State nur Plain-Objects/Primitives, Datumswerte als ISO-String
canvas.toDataURL() wirft SecurityError Bilder/Tiles auf dem Canvas kommen von fremder Origin ohne CORS Bilder mit crossorigin="anonymous" laden; Server muss Access-Control-Allow-Origin setzen
Privacy-Erweiterungen (Brave Shields strict, uBlock advanced) Iframe lädt teilweise nicht, postMessage kommt nicht an Tritt im Schul-Setup praktisch nicht auf — beim eigenen Test ggf. Standard-Profil nutzen

Warum „von selbst funktionieren" hier wirklich gilt

Das Protokoll ist bewusst so entworfen, dass deine App kein Plattform-Wissen braucht:

  1. Du registrierst nur deine App-URL (und optional Default-Parameter) in der Plattform.
  2. Es gibt keine Authentifizierung, keine Tokens, keinen Handshake-State, der zwischen Plattform und App synchron sein müsste.
  3. Die Plattform validiert Origins automatisch und ignoriert ungültige Messages — wer falsch sendet, kann die Plattform also nicht in einen kaputten Zustand bringen.
  4. Alle Plattform→App-Events (hello, request-screenshot) sind idempotent und können verloren gehen oder mehrfach kommen, ohne dass die App Buch führen muss.
  5. Alle App→Plattform-Events sind additiv: nichts musst du senden, alles kannst du senden — die Plattform fügt nur dazu, was sie versteht.

Testen + Debugging

Lokal mit der Live-Plattform testen

Während der Entwicklung kannst du deine App lokal hosten (z.B. localhost:5500) und in der Plattform als App registrieren mit URL http://localhost:5500/...?matheforscher=1. Wichtig: dein lokaler Server muss CORS erlauben oder die Plattform muss CORS-tolerant sein (ist sie für eingebettete iframes — du brauchst nichts zu tun, solange dein Server keine restriktiven X-Frame-Options setzt).

Browser-Konsole

In der Schüler-Ansicht öffne DevTools, Tab „Console". Filtere auf matheforscher. Du siehst alle Events, die die Plattform empfängt.

Du kannst manuell senden, um zu testen:

window.parent.postMessage({ type: 'matheforscher:state', state: { test: 1 } }, '*');

State-Check probieren

Nach ein paar Actions/States: drück den „🤖 Wie weit bin ich?"-Button im Schüler-UI. Die KI bekommt deinen aktuellen State + Schema-Beschreibung + Aufgabentext und antwortet. Wenn die Antwort generisch ist, ist meist die Schema-Beschreibung zu mager.


Datenschutz

  • Keine personenbezogenen Daten in Events. Pseudonyme verwaltet die Plattform selbst.
  • Keine externen Server-Calls während eingebettet, wenn vermeidbar — die Plattform protokolliert ihrerseits nicht alle Apps.
  • Audio/Video in der App selbst (Mikrofon, Kamera): das ist deine Verantwortung. Hol dir Permission, wenn nötig, und nutze nichts dauerhaft.
  • Drittanbieter-Tracking (Google Analytics, Hotjar, etc.) sollte in der eingebetteten Variante deaktiviert sein. Nutze den matheforscher=1-Parameter dafür.

Protokoll v3.0 — Integrierte Hosts (Profil-Kontext & Reporting)

Ab v3.0 kann ein Host (z. B. die urff.app-Super-App oder die Matheforscher-Plattform) deiner App Kontext übergeben (aktives Kind-Profil) und strukturierte Ergebnisse entgegennehmen. Alles bleibt additiv und optional.

Erweitertes hello (Host → App)

{
  type: 'matheforscher:hello',
  host: 'urffapp',                    // oder 'matheforscher'
  version: 3,
  capabilities: ['context', 'ergebnis', 'report'],   // was DIESER Host verwertet
  context: {
    profil: { id: 'p_abc', name: 'Lina', emoji: '🦊', farbe: '#e8734a' },  // null = Gast/anonym
    locale: 'de-DE'
  }
}
  • hello bleibt idempotent (bis zu 6× alle 500 ms, bis ready eintrifft).
  • profil.id ist der stabile Schlüssel; name/emoji/farbe sind Anzeige-Attribute, die sich bei gleicher id ändern dürfen (Profil umbenannt → App zieht beim nächsten hello nach).
  • capabilities nennt die Events, die der Host versteht — sende z. B. ergebnis nur, wenn es dort auch verwertet wird.
  • Hosts ohne v3 senden die Felder nicht → deine App verhält sich wie bisher.

Profil-Adoption

Wenn context.profil != null und deine App Adoption unterstützt (Manifest profilAdoption: true):

  1. Internen Benutzer mit externId == profil.id suchen — falls keiner existiert, automatisch anlegen (Name/Emoji/Farbe aus dem Profil).
  2. Diesen Benutzer auswählen und die komplette Benutzerverwaltung ausblenden (Auswahl, Anlegen, Wechseln, Löschen).
  3. Optional bestätigen: matheforscher:context-applied { payload: { profil: { id }, adoption: 'uebernommen' | 'neu-angelegt' } }

Bei profil: null (Gast) verhält sich die App wie bisher. Best Practice: Im eingebetteten Modus (window.self !== window.top) bis max. 1 s auf das erste hello warten, bevor du deine eigene Benutzerauswahl zeigst — so blitzt der Benutzer-Picker nicht auf (das erste hello kommt praktisch sofort nach load).

set-context / context-applied (Laufzeit-Profilwechsel)

Symmetrisch zu set-config/configured: Host sendet { type: 'matheforscher:set-context', payload: { context, reason } }; deine App wendet den Kontext an und antwortet binnen 500 ms mit matheforscher:context-applied, sonst macht der Host einen iframe-Reload.

matheforscher:ergebnis (App → Host)

Ein bewertetes Item — statistisch auswertbar ohne KI. Sende es immer dann, wenn das Kind eine Aufgabe abgeschlossen hat:

send({
  type: 'matheforscher:ergebnis',
  payload: {
    aufgabe: '7 + 5',                 // Pflicht: menschenlesbare Aufgabenstellung
    korrekt: true,                    // Pflicht: true | false | null (nicht bewertbar)
    typ: 'addition-zr20',             // optional: app-interner Aufgabentyp-Code
    antwort: '12', erwartet: '12',    // optional
    dauerMs: 4200,                    // optional: Bearbeitungszeit
    versuche: 1, hilfen: 0,           // optional
    fehlertyp: 'zehnerübergang',      // optional: app-diagnostizierter Fehlertyp
    didaktik: { zahlenraum: 'ZR20', inhaltsbereich: 'Arithmetik' },  // optional, Vokabular wie Manifest-didaktik
    beschreibung: 'Kind rechnet 7+5=12 im ersten Versuch'            // optional, KI-Satz wie bei state/action
  }
});

Abgrenzung: action = jede Handlung (Rohverlauf) · progress = sichtbarer Zwischenschritt · ergebnis = bewertetes Item (Quote, Zeiten, Fehlertypen berechenbar). Payload-Limit: 8 KB.

request-report / report — aggregierte Diagnostik

// Host → App (z. B. beim Sitzungsende):
{ type: 'matheforscher:request-report', payload: { scope: 'sitzung' } }   // 'sitzung' | 'gesamt'

// App → Host (als Antwort ODER unaufgefordert, z. B. nach abgeschlossenem Durchgang):
{ type: 'matheforscher:report', payload: {
    scope: 'sitzung',
    zusammenfassung: '24 Aufgaben im ZR20, 19 richtig. Schwierigkeiten beim Zehnerübergang.',
    kennzahlen: { bearbeitet: 24, korrekt: 19, dauerSec: 540 },   // flaches Key-Value für Statistik
    diagnostik: { /* app-spezifisch, Schema im Manifest (reportSchema) beschrieben */ },
    empfehlung: 'Weiter mit Zehnerübergang im ZR20 üben.'          // optional
} }

Antworte auf request-report zügig (Hosts warten typischerweise unter 1 s). Mehrere Reports pro Sitzung sind erlaubt; der letzte pro Scope gilt. Payload-Limit: 64 KB.

Manifest-Erweiterungen (v3)

Feld Bedeutung
profilAdoption boolean — App kann Profile übernehmen und blendet dann ihre Benutzerverwaltung aus
reporting { ergebnis: true, reportScopes: ['sitzung','gesamt'], reportSchema: 'Prosa…' } — was die App liefert; reportSchema beschreibt das diagnostik-Objekt
protocolLevel: 4 Neues Conformance-Level „integriert" (context + ergebnis/report) über Level 3

Datenschutz (v3)

  • Kontextdaten (Profil) dienen nur der lokalen Zuordnung/Anzeige und dürfen von Apps niemals an eigene Server oder Dritte übertragen werden.
  • Hosts senden nur Anzeigenamen/Pseudonyme, keine Klarnamen-Pflicht.
  • ergebnis/report enthalten keine Personendaten — die Zuordnung zum Kind macht der Host über die Sitzung.

Protokoll v3.2 — KI-Kanal (zentrale KI des Hosts)

Ab v3.2 kann ein Host seinen eingebetteten Apps eine zentrale KI anbieten. Deine App schickt eine anbieterneutrale Chat-Anfrage (messages im OpenAI-Stil) an den Host; der Host wählt Anbieter und Modell (eigener Schlüssel der Lehrkraft, KI auf dem Gerät oder ein zentraler Zugang), führt die Anfrage aus und liefert die Antwort zurück. Die Prompts bleiben in deiner App, der API-Schlüssel bleibt im Host. Alles ist additiv und optional.

Stand der Hosts: urff.app ist der erste Host mit KI-Kanal. mathe.digital selbst sendet aktuell kein 'ki' in capabilities — dort verhalten sich Apps wie bisher (eigener Zugang).

Erweitertes hello (Host → App)

{
  type: 'matheforscher:hello', host: 'urffapp', version: 3,
  capabilities: ['context', 'ergebnis', 'report', 'ki'],   // 'ki' = Host bietet den Kanal an
  context: { /* wie v3.0 */ },
  ki: {
    status: 'bereit',                                     // 'bereit' | 'deaktiviert' | 'eigener-zugang'
    faehigkeiten: ['text', 'json', 'stream', 'vision'],   // nur bei 'bereit'
    anbieter: 'byok'                                      // nur bei 'bereit', informativ: 'byok' | 'zentral' | 'lokal'
  }
}

Normatives Verhalten der App:

Situation Verhalten
standalone / kein 'ki' in capabilities / kein ki-Feld eigener Zugang wie bisher
status: 'eigener-zugang' eigener Zugang wie bisher (Host hat nichts konfiguriert)
status: 'bereit' alle Chat-Anfragen über ki-request; eigene Schlüssel-/Anbieter-UI ausblenden
status: 'deaktiviert' KI-Funktionen ausblenden oder deaktivieren; kein Rückfall auf den eigenen Zugang

Fähigkeiten (faehigkeiten):

Fähigkeit Bedeutung
text immer vorhanden — Textantwort in payload.text
json Host garantiert bei antwortFormat: 'json' ein JSON-Objekt in payload.json
stream Host liefert ki-chunk-Nachrichten vor der ki-response
vision Bilder in content erlaubt

Prüfe eine Fähigkeit, bevor du sie anforderst; fehlt sie, degradiere (z. B. Text statt JSON parsen, Bild weglassen).

matheforscher:ki-request (App → Host)

send({
  type: 'matheforscher:ki-request',
  id: 'k-17',                                   // von der App gewählt, eindeutig pro Sitzung
  payload: {
    messages: [                                 // Pflicht, 1–64 Einträge
      { role: 'system', content: 'Du bist ein Mathe-Tutor für Klasse 2. Antworte in einem Satz.' },
      { role: 'user', content: [
          { type: 'text', text: 'Was zeigt das Bild?' },
          { type: 'image', dataUrl: 'data:image/png;base64,…' }   // nur mit Fähigkeit 'vision'
      ] }
    ],
    zweck: 'tutor-hinweis',                     // optional, ≤ 40 Zeichen: Statistik/Abrechnung
    modell: 'schnell',                          // optional: 'schnell' | 'stark' (Klasse, kein Modellname); Default 'schnell'
    temperature: 0.4,                           // optional, 0–2
    maxTokens: 512,                             // optional, 1–8192
    antwortFormat: 'text',                      // optional: 'text' | 'json'; Default 'text'
    stream: false,                              // optional: true → ki-chunk vor ki-response
    timeoutMs: 30000                            // optional; Host klemmt auf 5 000–120 000; Default 60 000
  }
});

role ist 'system', 'user' oder 'assistant'; content ist ein String oder ein Array aus { type: 'text', text } und { type: 'image', dataUrl }. Alle Antworten des Hosts tragen deine id.

Antworten: ki-chunk / ki-response / ki-error (Host → App)

// nur bei stream: true — beliebig viele, in Reihenfolge
{ type: 'matheforscher:ki-chunk', id: 'k-17', payload: { delta: 'Teil der Antwort' } }

// Erfolg
{ type: 'matheforscher:ki-response', id: 'k-17', payload: {
    text: 'vollständige Antwort',
    json: { /* … */ },                           // nur bei antwortFormat 'json'
    anbieter: 'byok',                            // 'byok' | 'zentral' | 'lokal'
    modell: 'google/gemini-3.5-flash-lite',      // optional
    usage: { eingabe: 812, ausgabe: 97 }         // optional, Tokens
} }

// Fehler
{ type: 'matheforscher:ki-error', id: 'k-17', payload: { code: 'limit', message: 'Warteschlange voll' } }

Pro id kommt genau eine Abschluss-Nachricht (ki-response oder ki-error).

Fehlercodes:

Code Bedeutung Empfohlene Reaktion der App
deaktiviert Lehrkraft hat KI im Host ausgeschaltet KI-Funktion ausblenden; kein Rückfall auf eigenen Zugang
eigener-zugang Host hat keine KI konfiguriert eigenen Zugang wie bisher nutzen
nicht-verfuegbar Anbieter momentan nicht nutzbar (z. B. lokales Modell wird geladen) später erneut versuchen, Hinweis zeigen
offline keine Netzverbindung eigene Offline-Logik (nicht der eigene Online-Schlüssel)
ungueltig Payload verletzt das Schema; message nennt das Feld Programmierfehler in der App beheben
zu-gross Payload über dem Größenlimit Verlauf kürzen, Bild verkleinern
limit Rate-Limit erreicht oder Warteschlange voll kurz warten, dann erneut
timeout Host-Timeout (timeoutMs) überschritten erneut versuchen oder Offline-Logik
abgebrochen durch ki-cancel oder Verlassen der App abgebrochen nichts anzeigen
anbieter HTTP-/Modellfehler beim Anbieter; message enthält den Status Offline-Logik, Fehler dezent melden

matheforscher:ki-cancel (App → Host)

send({ type: 'matheforscher:ki-cancel', id: 'k-17' });

Der Host bricht die Anfrage ab und antwortet mit ki-error { code: 'abgebrochen' }.

matheforscher:ki-status (Host → App, optional)

{ type: 'matheforscher:ki-status', payload: { status: 'deaktiviert' } }   // payload = ki-Objekt wie im hello

Für Hosts, deren KI-Zustand sich zur Laufzeit ändert; behandle es wie ein neues hello.ki. urff.app sendet es nicht (Apps werden beim Verlassen des Lehrkraft-Bereichs ohnehin neu geladen); definiert für mathe.digital und künftige Hosts.

Manifest-Erweiterung (v3.2)

Feld Bedeutung
ki { beschreibung: 'Tutor-Hinweise am Zahlenstrahl', faehigkeiten: ['text'] } — App nutzt KI; beschreibung zeigt die Lehrkraft-Ansicht des Hosts, faehigkeiten nennt, was die App braucht

Grenzen & Datenschutz (v3.2)

  • Host-Limits: Payload ≤ 256 KB (Text) bzw. ≤ 4 MB mit Bildern; pro App-Sitzung max. 2 parallele Anfragen (weitere warten, ab 8 wartenden → limit); 30 Anfragen/Minute.
  • Der Host leitet messages unverändert weiter und ergänzt keine Profil- oder Gerätedaten.
  • Apps dürfen keine Profil-/Klarnamen in Prompts schreiben (v3-Regel gilt fort).
  • Der Host protokolliert nur Metadaten (App, Zweck, Anbieter, Modell, Tokens, Dauer, Erfolg), niemals Prompt- oder Antwortinhalte.
  • Der API-Schlüssel verlässt den Host nie in Richtung App.

Minimalbeispiel: KI-Anfrage aus der App ohne SDK

Promise-Wrapper mit id-Korrelation und Timeout — Antworten werden nur vom Parent-Fenster akzeptiert (e.source === window.parent):

let kiInfo = null;                          // ki-Objekt aus hello bzw. ki-status
const offen = new Map();                    // id → { resolve, reject, timer, onChunk }
let zaehler = 0;

window.addEventListener('message', (e) => {
  if (e.source !== window.parent) return;
  const d = e.data || {};
  if (d.type === 'matheforscher:hello') {
    const hatKi = Array.isArray(d.capabilities) && d.capabilities.includes('ki');
    kiInfo = hatKi && d.ki ? d.ki : { status: 'eigener-zugang' };
    return;
  }
  if (d.type === 'matheforscher:ki-status') { kiInfo = d.payload || kiInfo; return; }

  const o = d.id && offen.get(d.id);
  if (!o) return;
  if (d.type === 'matheforscher:ki-chunk') { o.onChunk?.(d.payload?.delta || ''); return; }
  if (d.type === 'matheforscher:ki-response' || d.type === 'matheforscher:ki-error') {
    clearTimeout(o.timer); offen.delete(d.id);
    if (d.type === 'matheforscher:ki-response') return o.resolve(d.payload);
    const err = new Error(d.payload?.message || d.payload?.code);
    err.code = d.payload?.code;
    o.reject(err);
  }
});

const kiStatus = () => kiInfo?.status || 'eigener-zugang';

function kiAnfrage(payload, onChunk) {
  return new Promise((resolve, reject) => {
    if (kiStatus() !== 'bereit') {
      return reject(Object.assign(new Error('KI-Kanal nicht bereit'), { code: kiStatus() }));
    }
    const id = 'k' + (++zaehler);
    const frist = (payload.timeoutMs || 60000) + 5000;           // etwas länger als der Host
    const timer = setTimeout(() => {
      offen.delete(id);
      window.parent.postMessage({ type: 'matheforscher:ki-cancel', id }, '*');
      reject(Object.assign(new Error('Zeitüberschreitung'), { code: 'timeout' }));
    }, frist);
    offen.set(id, { resolve, reject, timer, onChunk });
    window.parent.postMessage({ type: 'matheforscher:ki-request', id, payload }, '*');
  });
}

// Verwendung:
kiAnfrage({
  messages: [{ role: 'user', content: 'Erkläre 7 + 5 in einem Satz für ein Kind in Klasse 2.' }],
  zweck: 'tutor-hinweis', maxTokens: 120
}).then((antwort) => zeigeHinweis(antwort.text))
  .catch((err) => {
    if (err.code === 'deaktiviert') verbergeKiButton();   // kein Rückfall
    else zeigeHinweis(offlineHinweis());                  // eigene Fallback-Logik
  });

Mit SDK (/sdk/forscher-protokoll.js): app.kiStatus(), app.kiFaehigkeiten(), app.ki(payload, { onChunk }) (Promise), app.kiAbbrechen(id) und die Option onKiStatus(ki) — siehe SDK-Doku, Abschnitt „KI-Kanal (v3.2)".


Versions-History

Version Stand Neuerungen
1.0 Mai 2026 Initial: progress, complete, error
2.0 Mai 2026 + action, state, App-Pool stateSchema-Feld, KI-State-Check
2.1 Mai 2026 erfolgsBedingung in Aufträgen, Plattform-seitige Erfolgs-Erkennung; complete als informativ/optional markiert
2.2 Mai 2026 request-screenshot / screenshot Events für automatischen App-Screenshot
2.3 Mai 2026 + ready-Event mit config/supportedParams, Konfigurations-Doku, Screenshot-Beispiele
2.4 Mai 2026 + hello-Event (Plattform → App, mehrfaches Pingen), zusätzliches #matheforscher=1-Hash-Signal, Anti-Pattern-Hinweis zu Cross-Origin-Parent-Zugriff, robusterer URL-Bau
2.5 Mai 2026 + Vollständiger Auto-Init-Block, iframe-Berechtigungen explizit dokumentiert, Browser-Kompatibilität (Chrome/Firefox/Safari/iOS) + häufige Stolpersteine (CSP, X-Frame-Options, Service Worker, canvas-CORS), html2canvas-Bundling als CSP-Fallback empfohlen
2.6 Mai 2026 + discover-Event (Plattform fragt aktiv nach supportedParams bei der App-Registrierung), + set-config/configured-Eventpaar für Live-Re-Konfiguration der App bei Teilaufgaben-Wechsel ohne Iframe-Reload (Fallback auf URL-Reload, wenn App nicht antwortet)
2.7 Mai 2026 + manifest-Payload im ready-Event mit Selbstbeschreibung der App (Name, Beschreibung, kindActions, Kategorien, stateSchema, defaultParams, iconUrl, infoUrl, version). Plattform fragt das Manifest beim App-Anlegen automatisch ab (1.5 s Debounce auf der URL-Eingabe) und bietet auf Knopfdruck Updates mit Hash-basierter Diff-Anzeige.
2.8 Mai 2026 + highlight/perform (Plattform → App): KI kann deklarierte Elemente hervorheben und Aktionen vorführen/ausführen (mode: 'demo' oder 'execute', Mehrschritt-Sequenz); Manifest-Felder highlightTargets/performableActions; optionale Acks highlighted/performed.
2.9 Juli 2026 + Semantische Selbstbeschreibung: optionales Feld beschreibung auf state- und action-Events — die App erklärt in einem deutschen Satz, was Zustand/Handlung bedeuten; die Plattform-KI (Tutor, Auswertung, State-Check) nutzt Beschreibungen bevorzugt statt Roh-JSON. + Manifest-Felder didaktik (Klassenstufen, Zahlenräume, EIS-Darstellungsebenen, Inhaltsbereiche, Kompetenzen), stateSchemaV2 (JSON-Schema + Prosa + Beispiel-Paare) und protocolLevel (0–3). + Offizielles Mini-SDK /sdk/forscher-protokoll.js (siehe docs/forscher-sdk.md).
3.0 Juli 2026 + Integrierte Hosts: hello mit host/version/capabilities/context (aktives Kind-Profil); Profil-Adoption mit ausgeblendeter Benutzerverwaltung (profilAdoption im Manifest, context-applied-Ack, set-context für Laufzeitwechsel); ergebnis-Event (bewertete Items fürs Lernprozesslog); request-report/report (aggregierte Diagnostik); Manifest-Felder reporting, protocolLevel 4; Datenschutz-Regeln für Kontextdaten. Erster v3-Host: urff.app.
3.1 August 2026 + supportedParams-Schema offiziell erweitert um label, description (menschenlesbare Bedeutung — Grundlage der KI-gestützten App-Konfiguration beim Erstellen von Forscheraufträgen) und min/max (Grenzen für number-Parameter ohne values-Liste). Schema-Felder-Tabelle im ready-Abschnitt.
3.2 September 2026 + KI-Kanal: hello mit Capability 'ki' und Feld ki (status bereit/deaktiviert/eigener-zugang, faehigkeiten, anbieter); ki-request (anbieterneutrale Chat-Anfrage: messages, zweck, modell-Klasse, temperature, maxTokens, antwortFormat, stream, timeoutMs) mit Antworten ki-chunk/ki-response/ki-error (zehn Fehlercodes), ki-cancel und optionales ki-status; Manifest-Feld ki; Host-Limits und Datenschutz-Regeln (nur Metadaten protokolliert, Schlüssel bleibt im Host). SDK: kiStatus(), kiFaehigkeiten(), kiInfo(), ki(), kiAbbrechen(), Option onKiStatus. Erster Host mit KI-Kanal: urff.app; mathe.digital sendet aktuell kein 'ki'.
3.3 September 2026 Nur Doku, kein Breaking Change: „Zeig's am Material" — complete hakt nicht mehr selbst ab, sondern zählt als „Arbeitsmittel bestätigt", wenn das Kind „Fertig – schau nach!" drückt; boolesches state.erreicht hat Vorrang vor complete. Konvention für Ziel-Parameter (ziel, zielzahl, zielzeit, target, goal, karte in supportedParams → „Arbeitsmittel prüft selbst"). Neuer Baustein „Sammlung": ganze Sammlung als Liste im state plus save/remove-Aktionen auf archiv.

Kontakt

Plattform-Maintainer: Christian Urff · matheforscher@urff.app · https://matheforscher-prod.web.app

Source: https://github.com/... (sobald öffentlich)

Lizenz dieser Doku: CC-BY 4.0 — gerne weitergeben und übersetzen.

mathe.digital · postMessage-Protokoll · Diese Seite wird statisch aus docs/postmessage-protokoll.md generiert. Fragen? matheforscher@urff.app

Diese Plattform speichert deine Bearbeitung lokal im Browser. Wenn eine Lehrkraft eine Sammel-Session startet, werden Pseudonym, Lösungsstand, Notizen und ggf. Sprachmemos/Screenshots auf einem deutschen Server (Firebase, europe-west3) gespeichert. Mit dem Klick auf „Verstanden" akzeptierst du das.