# Matheforscher postMessage-Protokoll

Dokumentation für Entwickler:innen, die ihre Webapp in die [Matheforscher-Plattform](https://matheforscher-prod.web.app) 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?](#worum-gehts)
2. [Quickstart (5 Zeilen)](#quickstart-5-zeilen)
3. [Vollständiger Auto-Init (alles in einem)](#vollständiger-auto-init-alles-in-einem)
4. [Plattform-Erkennung](#plattform-erkennung)
5. [iframe-Berechtigungen](#iframe-berechtigungen)
6. [Konfiguration via Query-Parameter](#konfiguration-via-query-parameter)
7. [App-Manifest beim ready-Event](#app-manifest-beim-ready-event)
8. [Event-Typen](#event-typen)
9. [Schema-Beschreibung für den App-Pool](#schema-beschreibung-für-den-app-pool)
10. [Best Practices](#best-practices)
11. [Komplettbeispiel: Mini-Rechenfeld](#komplettbeispiel-mini-rechenfeld)
12. [Browser-Kompatibilität & Stolpersteine](#browser-kompatibilität--stolpersteine)
13. [Testen + Debugging](#testen--debugging)
14. [Datenschutz](#datenschutz)
15. [Versions-History](#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`](https://developer.mozilla.org/en-US/docs/Web/API/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":

```js
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](/dev/forscher-sdk.md) 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:

```js
// 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

```js
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:

```js
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:

```js
// ❌ 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:

```html
<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`:**

```js
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:**

```markdown
## 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

```js
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: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:

```js
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.

```js
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.

```js
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 `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).

**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.

```js
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.

```js
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.

```js
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:**
```json
{ "type": "matheforscher:hello", "host": "matheforscher", "version": 1 }
```

**Empfänger:**
```js
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.

```js
// 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):**

```js
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):**

```js
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.

```js
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.

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

**Antwort:** Sende ein normales [`matheforscher:ready`](#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**.

```js
{
  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.

```js
// 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`](#matheforscher-set-config), nachdem die neue Konfiguration in der App angewandt wurde.

```js
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:**
```js
{
  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):**
```js
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:
```js
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:**
```js
{
  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):**
```js
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:
```js
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

```markdown
## 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

```markdown
## 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

```markdown
## 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.

```js
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:

```js
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

```html
<!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:

```markdown
## 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](#vollständiger-auto-init-alles-in-einem) 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:

```js
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)

```js
{
  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:

```js
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

```js
// 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.

---

## 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.
