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
- Worum geht's?
- Quickstart (5 Zeilen)
- Vollständiger Auto-Init (alles in einem)
- Plattform-Erkennung
- iframe-Berechtigungen
- Konfiguration via Query-Parameter
- App-Manifest beim ready-Event
- Event-Typen
- Schema-Beschreibung für den App-Pool
- Best Practices
- Komplettbeispiel: Mini-Rechenfeld
- Browser-Kompatibilität & Stolpersteine
- Testen + Debugging
- Datenschutz
- 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 |
Aktive Teilaufgabe wird automatisch als „fertig" markiert |
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] },
modus: { type: 'string', default: 'frei', values: ['frei', 'zerlegung', 'darstellung'] }
};
// 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). |
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:readymitsenden oder nur als Antwort auf einenmatheforscher:discover-Ping liefern — die Plattform akzeptiert beides. - Bestehende Apps, die nur
supportedParamsohnemanifestschicken, 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] },
farbe: { type: 'string', default: 'blau', values: ['blau', 'rot', 'grün'] },
zeigeHilfe: { type: 'boolean', default: false },
modus: { type: 'string', default: 'frei', values: ['frei', 'zerlegung', 'darstellung'] }
}
}
});
Plattform-Effekt:
- Die Plattform speichert
configundsupportedParamsim 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).
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 — 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 Erfolgs-Bedingung der Lehrkraft.
send({
type: 'matheforscher:complete',
payload: { /* optional, z.B. Endstand */ }
});
Plattform-Effekt: Die aktuell aktive Teilaufgabe wird automatisch als „fertig" markiert (Häkchen). Spart dem Kind den manuellen „Fertig"-Klick — aber nur, wenn die App tatsächlich sicher ist.
Hinweis zur plattformseitigen Erfolgs-Erkennung: Lehrkräfte können pro Auftrag eine textuelle Erfolgs-Bedingung definieren (z.B. „Alle elf Zerlegungen der 10 sind dargestellt"). Wenn das Kind auf „✅ Bin ich fertig?" klickt, prüft die KI anhand des aktuellen state und der letzten Aktionen, ob diese Bedingung erfüllt ist — unabhängig davon, ob die App ein complete-Event gesendet hat.
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 |
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.
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
targetsverstehen — alle anderen Felder sind optionale Wünsche, die du honorieren oder ignorieren darfst. - Unbekannte IDs still ignorieren.
styleist 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 (keinstate/completeals Folge). Das Kind macht es danach selbst.mode: 'execute'— wende die Schritte echt an (du darfst danach wie bei Kind-Aktionenstate/actionsenden).- Unbekannten
modegnädig behandeln (am besten alsdemo). Schritte mit unbekannteraction/targetüberspringen, übrige ausführen. executeist NICHT idempotent — die Plattform sendet jedeperform-Message genau einmal (keine Retries).demoist gefahrlos wiederholbar.- Apps ohne Perform-Support ignorieren den unbekannten
type. - Pro Schritt werden nur
action,targetundpayloadü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:
- Du registrierst nur deine App-URL (und optional Default-Parameter) in der Plattform.
- Es gibt keine Authentifizierung, keine Tokens, keinen Handshake-State, der zwischen Plattform und App synchron sein müsste.
- Die Plattform validiert Origins automatisch und ignoriert ungültige Messages — wer falsch sendet, kann die Plattform also nicht in einen kaputten Zustand bringen.
- 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. - 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'
}
}
hellobleibt idempotent (bis zu 6× alle 500 ms, bisreadyeintrifft).profil.idist der stabile Schlüssel;name/emoji/farbesind Anzeige-Attribute, die sich bei gleicher id ändern dürfen (Profil umbenannt → App zieht beim nächstenhellonach).capabilitiesnennt die Events, die der Host versteht — sende z. B.ergebnisnur, 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):
- Internen Benutzer mit
externId == profil.idsuchen — falls keiner existiert, automatisch anlegen (Name/Emoji/Farbe aus dem Profil). - Diesen Benutzer auswählen und die komplette Benutzerverwaltung ausblenden (Auswahl, Anlegen, Wechseln, Löschen).
- 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/reportenthalten keine Personendaten — die Zuordnung zum Kind macht der Host über die Sitzung.
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. |
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.