# Apps programmieren

> Die Referenz für Entwickler, die eine App selbst schreiben: das .nxa-Paket und sein Manifest, das .nxd-Dokumentformat, was eine App in der Sandbox darf, das Nachrichtenprotokoll zwischen App und nexxus, wie der KI-Assistent und Mitbearbeitende Ihre Dokumente ändern, und wie Sie testen und veröffentlichen.

:::note[Ohne Code?]
Um Dokumente anzulegen, eine .nxa-Datei zu installieren oder den KI-Assistenten eine App erstellen zu lassen, lesen Sie [Apps ohne Code erstellen](/de/apps/create/). Diese Seite ist für Entwickler, die das HTML einer App selbst schreiben.
:::

Eine App besteht aus einer HTML-Datei und einer Beschreibungsdatei. Das HTML zeichnet ein Dokument und meldet, was die Person darin ändert. Alles andere erledigt nexxus: Dokument laden und speichern, Versionen, Freigaben, gemeinsames Bearbeiten in Echtzeit, der KI-Assistent, die Rechte. Ihre App berührt nie einen Speicher und sieht nie das Netz. Das hält Apps klein, und deshalb lässt nexxus jeden Apps installieren.

Diese Seite ist der Vertrag. Sie beschreibt, was heute in nexxus ausgeliefert wird.

<Hub
  inputsLabel="nexxus"
  inputs={[
    { icon: 'folder-open', title: 'Dateien', text: 'Lädt und speichert das .nxd-Dokument, führt seine Versionen' },
    { icon: 'users', title: 'Mitbearbeitende', text: 'Ihre Änderungen kommen an, während die App offen ist' },
    { icon: 'sparkles', title: 'KI-Assistent', text: 'Schreibt gegen das Schema geprüfte Änderungen in die Daten' },
  ]}
  core={{
    title: 'Ihre App',
    text: 'app.html in einem abgeschotteten Rahmen, ohne Netz, ohne Speicher',
    items: ['nxd:init', 'nxd:apply', 'nxd:change', 'nxd:export'],
  }}
  outputsLabel="Zurück an nexxus"
  outputs={[
    { icon: 'pencil', title: 'Änderungen', text: 'Die vollständigen neuen Daten bei jeder abgeschlossenen Änderung' },
    { icon: 'download', title: 'Dateien zum Speichern', text: 'Ein Bild, eine HTML-Seite, ein JSON-Export' },
    { icon: 'message-square-text', title: 'Fragen an den KI-Assistenten', text: 'Das Element, auf das die Person zeigt' },
  ]}
  caption="Die App zeichnet nur Daten und meldet Änderungen. Alle Nachrichten laufen über einen privaten Kanal zwischen nexxus und dem Rahmen."
/>

## Das Paket (.nxa)

Eine App wird als ZIP-Datei mit der Endung `.nxa` ausgeliefert. nexxus liest daraus zwei Einträge, gefunden über ihren Dateinamen, egal wo im Archiv:

<FileTree>
- hello.nxa ein ZIP-Archiv
  - manifest.json ID, Version, Name, Symbol, Inhaltstyp, Datenschema
  - app.html die Oberfläche, alles eingebettet
</FileTree>

Andere Einträge ignoriert nexxus. Bauen Sie das Paket mit einem beliebigen ZIP-Werkzeug, zum Beispiel:

```bash
zip -j hello.nxa manifest.json app.html
```

Die Hilfe in der App enthält ein funktionierendes Beispiel: Unter <MenuPath to="Start > Apps" /> öffnet <Ui>Hilfe</Ui> eine Erklärung, und <Ui>Beispiel-App herunterladen</Ui> speichert die Datei `hello.nxa`. Jeder Download trägt eine frische ID wie `hello-ab12c`, so lässt sich das Beispiel mehrmals in einem Mandanten installieren.

<Screenshot src={shot_apps_build_help_example} url="/dms-apps" alt={"Der Hilfe-Dialog mit dem Paketaufbau und dem Beispiel zum Herunterladen"} caption={"Die Hilfe erklärt das Paket und bietet eine funktionierende Beispiel-App an."} />

## Das Manifest

`manifest.json` beschreibt die App. nexxus prüft es vollständig, bevor etwas gespeichert wird: beim Hochladen, bei einer neuen Version und wenn der KI-Assistent eine App registriert.

So sieht das Manifest der Beispiel-App aus, wie nexxus es erzeugt:

```json
{
  "id": "hello-ab12c",
  "version": 1,
  "label": "Hallo-App",
  "icon": "waving_hand",
  "contentType": "application/json",
  "dataKind": "json",
  "schema": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "text": { "type": "string", "description": "Der angezeigte Text." }
    },
    "required": ["text"]
  }
}
```

### Felder

| Feld | Typ | Pflicht | Bedeutung |
|---|---|---|---|
| `id` | String | ja | Die ID der App und der Wert `$app` ihrer Dokumente. 2 bis 40 Zeichen, Kleinbuchstaben, Ziffern und Bindestriche, am Anfang ein Buchstabe (`[a-z][a-z0-9-]{1,39}`). Eindeutig je Mandant. `board` und `markdown` sind für die eingebauten Apps reserviert. |
| `label` | String | ja | Der Name auf der Karte, im Menü <Ui>Neu</Ui> der Dateien und auf der Dokumentseite. |
| `contentType` | String | ja | Wie die Daten beschrieben sind, etwa `application/json`, `text/markdown`, `text/plain` oder `application/xml`. |
| `dataKind` | `"json"` oder `"text"` | ja | `json`: `data` ist ein JSON-Wert, geprüft gegen `schema`. `text`: `data` ist ein String. |
| `schema` | Objekt | bei `json`-Apps | Ein JSON Schema (Draft 2020-12) für `data`. nexxus kompiliert es beim Hochladen und prüft jedes Dokument dagegen, das der KI-Assistent anlegt oder ändert. Text-Apps bekommen `{"type": "string"}`. |
| `description` | String | nein | Steht auf der Karte der App und geht an den KI-Assistenten. |
| `icon` | String | nein | Ein Material-Icon-Name für die Karte. Standard `apps`. |
| `color` | String | nein | Die Farbe der Karte als Hex-Wert. Standard `#1565C0`. |
| `version` | Ganzzahl | nein | Die Version der App, Standard `1`. Jeder neue Upload muss eine höhere Zahl tragen. |
| `defaultData` | beliebig | nein | Der Inhalt eines neuen Dokuments. Ohne das Feld beginnt ein neues Dokument mit `{}` (json) oder einem leeren String (text), und Ihre App muss das darstellen. |
| `aiInstructions` | String | nein | Hinweise für den KI-Assistenten, wie ein gutes Dokument aussieht, höchstens 8000 Zeichen. |
| `changelog` | Array | nein | Was jede Version geändert hat, zu sehen vor einer Veröffentlichung und in der Versionshistorie. |
| `capabilities` | Objekt | nein | Wünsche an den Rahmen um die App: `tabs` und `assistant`. |
| `contributes` | Objekt | nein | Menüeinträge, die die App in die Leiste über sich legt. Höchstens 256.000 Bytes. |

`app.html` darf nicht leer sein und höchstens 2.000.000 Bytes (UTF-8) groß.

### Das Datenschema

Das Schema ist der Vertrag zwischen Ihrer App, nexxus und dem KI-Assistenten. Daraus folgt dreierlei:

- nexxus lehnt jedes Dokument vom KI-Assistenten ab, das nicht passt. Der KI-Assistent bekommt die Fehlermeldung und kann seine Änderung korrigieren.
- Der KI-Assistent lernt Ihr Datenmodell aus dem Schema. Geben Sie jeder Eigenschaft eine `description`. Dort liest der KI-Assistent, was ein Feld bedeutet, welche Einheit es hat und welche Werte sinnvoll sind.
- Dokumente überleben App-Versionen. Fügen Sie neue Eigenschaften als optional hinzu, damit Dokumente aus einer älteren Version gültig bleiben.

Halten Sie `defaultData` leer oder knapp. Soll der KI-Assistent in einem neuen Dokument etwas bauen, wird ihm gesagt, unveränderte Startinhalte zu ersetzen. Ein Demo-Board, das nie verschwindet, ist trotzdem die häufigste Klage über Startinhalte. Gehört eine Einführung dazu, zeigen Sie sie im leeren Zustand Ihrer App. Beim Hochladen prüft nexxus `defaultData` nicht gegen das Schema, das Werkzeug des KI-Assistenten zum Anlegen aber schon: Passt `defaultData` nicht zum Schema, scheitert er an neuen Dokumenten.

### aiInstructions

Das Schema sagt, was gültig ist. `aiInstructions` sagt, was gut ist: Layoutregeln, Größen, Konventionen, die Fehler, die Modelle mit Ihren Daten gern machen. nexxus gibt den Text zusammen mit dem Schema an den KI-Assistenten, sobald jemand an einem Ihrer Dokumente arbeitet, und liefert ihn mit dem Werkzeug, das die Apps auflistet. Auf der Seite der App kann der Besitzer für seinen Mandanten eine Ergänzung hinzufügen; der KI-Assistent bekommt zuerst Ihren Text, dann die Ergänzung.

### changelog

Ein Eintrag je Version, der neueste zuerst. `version` ist eine Ganzzahl ab 1, `changes` eine nicht leere Liste nicht leerer Strings, `date` optional im Format `JJJJ-MM-TT`. Weitere Schlüssel ignoriert nexxus. Das sind die beiden neuesten Einträge der Whiteboard-App:

```json
"changelog": [
  {
    "version": 7,
    "date": "2026-10-04",
    "changes": [
      "Die App bringt einen Changelog mit. Er erscheint beim Hochladen einer neuen Version und in der Versionshistorie.",
      "Die Hinweise für den KI-Assistenten erklären jetzt auch Standardgrößen, wie das Board überlappende Elemente nachbessert, Frames als Spalten und typische Fehler wie doppelte ids."
    ]
  },
  {
    "version": 6,
    "date": "2026-10-03",
    "changes": [
      "Hell und Dunkel folgen dem Modus der Anwendung, in der das Board läuft.",
      "Wer in der App selbst umschaltet, behält diese Wahl für das Whiteboard."
    ]
  }
]
```

Lädt jemand eine neue Version hoch, zeigt der Dialog jeden Eintrag, der neuer ist als die installierte Version, bis zur Version des Pakets. Die Versionshistorie auf der Seite der App nimmt den Eintrag jeder Version aus dem neuesten hochgeladenen Manifest, das einen hat. Ein vollständiger Changelog in einer neuen Version füllt also auch ältere Versionen auf. Schreiben Sie die Punkte für die Menschen, die die App benutzen: eine konkrete Änderung je Punkt.

### capabilities

| Schlüssel | Typ | Standard | Wirkung |
|---|---|---|---|
| `tabs` | Boolean | `false` | nexxus zeigt über der App eine Reihe von Tabs. Das geöffnete Dokument ist der erste Tab, <Ui>+</Ui> öffnet eine Auswahl mit den übrigen Dokumenten derselben App und <Ui>Leeres Dokument</Ui>. Ein Tabwechsel lädt die App mit dem anderen Dokument neu. |
| `assistant` | Boolean | `true` | `false` erklärt, dass die Dokumente der App nicht für den KI-Assistenten gedacht sind. Der Schalter <Ui>Assistent für diese App</Ui> auf der Seite der App bleibt dann aus und lässt sich erst mit einer Version einschalten, die es erlaubt. |

Unbekannte Schlüssel ignoriert nexxus. Was fehlt, ist aus, außer bei `assistant`.

### contributes

Eine App kann Befehle in die Leiste über sich legen, entweder in das stehende Menü <Ui>Datei</Ui> oder in eigene Menüs. Ein Klick geht als `nxd:command` mit der ID des Befehls an die App. Das ist der Block des BPMN-Prozess-Editors:

```json
"contributes": {
  "commands": [
    { "id": "process.validate", "title": "BPMN prüfen", "category": "Prozess" },
    { "id": "process.export.svg", "title": "Als SVG exportieren", "category": "Datei" }
  ],
  "menus": {
    "datei": [ { "command": "process.export.svg", "group": "export" } ],
    "menubar": [
      { "id": "prozess", "title": "Prozess", "items": [ { "command": "process.validate" } ] }
    ]
  }
}
```

- `commands` legt Befehle mit `id` und `title` fest. Die erste Angabe einer ID gilt. Die IDs `__proto__`, `constructor` und `prototype` lehnt nexxus ab.
- `menus.datei` hängt Einträge an das Menü <Ui>Datei</Ui> an, gruppiert nach `group` und sortiert nach `order`. `datei` ist das einzige stehende Menü, das eine App erweitern kann.
- `menus.menubar` fügt eigene Menüs hinzu, jedes mit `id`, `title`, `items` und optional `order`. Angezeigt werden höchstens 8 Menüs mit je 24 Einträgen.
- An einem fehlerhaften Menüeintrag scheitert kein Manifest. nexxus lässt weg, was es nicht auflösen kann (einen unbekannten Befehl, ein nicht erlaubtes Menü), und warnt in der Browser-Konsole. Weitergereicht werden nur Befehls-IDs, die diese Prüfung bestanden haben.

## Das Dokument (.nxd)

Ein Dokument ist eine JSON-Datei in den Dateien, verpackt in einen kleinen Umschlag:

```json
{
  "$app": "board",
  "$v": 1,
  "contentType": "application/json",
  "meta": { "title": "Messe 2026" },
  "data": {
    "columns": [ { "id": "todo", "title": "To-do" } ],
    "cards": []
  }
}
```

| Feld | Typ | Bedeutung |
|---|---|---|
| `$app` | String | Die ID der App, die das Dokument öffnet. Pflicht: Ohne das Feld zeigt nexxus <Ui>Ungültige .nxd-Datei: Feld "$app" fehlt.</Ui> |
| `$v` | Zahl | Version des Umschlagformats, derzeit `1`. |
| `contentType` | String | Aus dem Manifest übernommen. |
| `meta.title` | String | Der Titel des Dokuments. Er ist auch Teil des Dateinamens. |
| `data` | Objekt oder String | Der Inhalt Ihrer App: was `schema` bei json-Apps beschreibt, ein String bei Text-Apps. |

Der Dateiname lautet `<Titel>.<App-ID>.nxd`, zum Beispiel `Messe 2026.board.nxd`. Beim Anlegen liest nexxus die App-ID aus dem Dateinamen und findet darüber die Dokumente einer App. Welche App ein Dokument öffnet, entscheidet `$app`, nicht der Name. Eine Datei, die kein gültiges JSON ist, zeigt <Ui>Ungültige .nxd-Datei: kein gültiges JSON.</Ui>

Ihre App sieht immer nur `data` (und den Titel in `meta`). Der Umschlag ist Sache von nexxus.

## In der Sandbox

nexxus lädt `app.html` in einen iframe mit `sandbox="allow-scripts"`: Ihr Code läuft, aber ohne die Origin von nexxus, also ohne Cookies, ohne Sitzung und ohne Zugriff auf die Seite drumherum. Vor Ihr HTML setzt nexxus diese Content-Security-Policy:

```text
default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data: blob:;
font-src data:; media-src data: blob:; form-action 'none'; base-uri 'none'
```

Was das für Ihren Code bedeutet:

| Geht | Geht nicht |
|---|---|
| Eingebettetes `<script>` und `<style>`, alles in der einen HTML-Datei | Jeder Netzwerkzugriff: `fetch`, WebSocket, externe Skripte, Stylesheets, Webfonts, Bilder von einer URL |
| Bilder und Schriften als `data:`- oder `blob:`-URL | `localStorage`, `sessionStorage`, Cookies (der Rahmen hat keine Origin) |
| `<input type="file">`, Einfügen und Drag and Drop | `alert`, `confirm`, `prompt` (in der Sandbox gesperrt): Bauen Sie eigene Dialoge |
| Canvas, SVG, alles, was der Browser zeichnet | Eine Datei direkt herunterladen: Geben Sie sie mit `nxd:export` an nexxus |

Die Daten des Dokuments sind Ihr einziger Zustand. Bilder, die eine Person hinzufügt, gehören komprimiert in `data`. Große Mengen Rasterdaten gehören als eigene Dokumente in die Dateien.

## Das Protokoll

App und nexxus sprechen über einen privaten `MessageChannel`. Der Handshake hat drei Schritte, danach laufen Nachrichten über den Port in beide Richtungen.

<Sequence
  actors={[
    { name: 'Ihre App', icon: 'app-window' },
    { name: 'nexxus-Seite', icon: 'monitor' },
    { name: 'nexxus-Server', icon: 'server' },
    { name: 'Mitbearbeiterin', icon: 'users' },
  ]}
  steps={[
    { from: 0, to: 1, label: 'nxd:ready (an das Elternfenster)' },
    { from: 1, to: 0, label: 'nxd:connect mit einem MessagePort' },
    { from: 1, to: 0, label: 'nxd:init: Daten, Titel, readonly' },
    { from: 0, to: 0, label: 'Die Person ändert etwas' },
    { from: 0, to: 1, label: 'nxd:change mit den vollständigen Daten' },
    { from: 1, to: 2, label: 'Live an die anderen, gespeichert nach etwa 1,5 s' },
    { from: 2, to: 3, label: 'Die Änderung kommt bei der Mitbearbeiterin an' },
    { from: 3, to: 2, label: 'Die Mitbearbeiterin ändert etwas' },
    { from: 2, to: 1, label: 'Weitergereicht an diese Seite' },
    { from: 1, to: 0, label: "nxd:apply mit origin 'peer'" },
  ]}
  caption="Nach dem Handshake geht jede Nachricht über den Port. Ihre App spricht nie selbst mit dem Server."
/>

### Nachrichten

| Richtung | Nachricht | Bedeutung |
|---|---|---|
| App an nexxus (Fenster) | `{type: 'nxd:ready'}` | Die App ist gestartet und möchte ihren Kanal. |
| nexxus an App (Fenster) | `{type: 'nxd:connect'}` mit einem `MessagePort` in `ports[0]` | Behalten Sie den Port, über ihn läuft alles Weitere. |
| nexxus an App | `{type: 'nxd:init', doc, meta, readonly, hostCapabilities}` | Die `data` des Dokuments als `doc`, `meta` mit dem Titel, `readonly` und was dieser Host kann, heute `{assist: true}` oder `{assist: false}`. |
| nexxus an App | `{type: 'nxd:apply', doc, origin}` | Jemand anderes hat das Dokument geändert. `origin` ist `'ai'` für den KI-Assistenten und `'peer'` für Mitbearbeitende. Behandeln Sie ein fehlendes `origin` wie `'peer'`. |
| App an nexxus | `{type: 'nxd:change', doc}` | Die Person hat etwas geändert. Schicken Sie die vollständigen neuen Daten. Bei schreibgeschützten Dokumenten verworfen. |
| nexxus an App | `{type: 'nxd:command', id}` | Die Person hat einen Ihrer Menüeinträge angeklickt. |
| App an nexxus | `{type: 'nxd:export', filename, mime, data}` | Eine Datei für die Person speichern. |
| App an nexxus | `{type: 'nxd:assist', context, message}` | Das Element, auf das die Person zeigt, an den KI-Assistenten geben. |
| App an nexxus | `{type: 'nxd:presenting', on}` | Eine Vollbild-Vorführung beginnt (`true`) oder endet (`false`). |
| nexxus an App | `{type: 'nxd:theme', mode, pref}` | Hell oder dunkel: `mode` ist der Modus von nexxus, `pref` die Wahl dieser Person für Ihre App. |
| App an nexxus | `{type: 'nxd:theme-pref', pref}` | Die Person hat den Hell-Dunkel-Schalter Ihrer App benutzt. |

### Eine vollständige Minimal-App

Das ist die `app.html` der Beispiel-App. Sie zeigt ein Textfeld, meldet jeden Tastendruck und übernimmt Änderungen von anderen:

```html
<!doctype html>
<html lang="de"><head><meta charset="utf-8"><style>
  body{font:15px/1.5 -apple-system,system-ui,sans-serif;margin:0;display:grid;place-items:center;height:100vh}
  input{font-size:20px;padding:10px 14px;border:1px solid #cbd5e1;border-radius:10px}
</style></head><body>
  <input id="t" placeholder="Text eingeben">
  <script>
    var port=null, state={text:""};
    var t=document.getElementById("t");
    function render(){ if(t.value!==(state.text||"")) t.value=state.text||""; }
    function emit(){ if(port) port.postMessage({type:"nxd:change", doc: state}); }
    t.addEventListener("input", function(){ state.text=t.value; emit(); });
    function handle(m){ if(!m) return;
      if(m.type==="nxd:init"){ state=m.doc||{text:""}; t.readOnly=!!m.readonly; render(); }
      if(m.type==="nxd:apply"){ state=m.doc||{text:""}; render(); } }
    window.addEventListener("message", function(e){
      if(e.data && e.data.type==="nxd:connect" && e.ports[0]){ port=e.ports[0]; port.onmessage=function(ev){handle(ev.data);}; } });
    (window.parent||window).postMessage({type:"nxd:ready"}, "*");
  </script>
</body></html>
```

### Regeln, an die sich jede App hält

- **`readonly` respektieren.** Blenden Sie bei `true` alle Bearbeitungselemente aus: Paletten, Griffe, Löschknöpfe. Eine CSS-Klasse am `<body>` lässt sich leichter vollständig halten als eine Abfrage je Knopf. Lesen, Suchen, Zoomen und Exportieren bleiben. nexxus erzwingt die Schreibrechte auf dem Server und verwirft `nxd:change` aus einem schreibgeschützten Dokument. Bei `readonly` geht es darum, nichts anzubieten, was scheitern würde.
- **Jedes `nxd:init` als Neuanfang behandeln.** Setzen Sie Ihren Zustand daraus neu. Hosts können es mehr als einmal schicken.
- **Bei `nxd:apply` keine laufende Eingabe überschreiben.** Kommt die Änderung einer Mitbearbeiterin, während die Person in einem Feld tippt, behalten Sie deren Text in diesem Feld. `render()` im Beispiel schreibt das Feld nur, wenn sich der Wert unterscheidet.
- **`nxd:change` bei jeder abgeschlossenen Änderung schicken**, mit den vollständigen Daten. Fassen Sie schnelle Tastenfolgen zusammen.
- **`hostCapabilities` großzügig lesen.** Fehlt ein Eintrag, kann der Host es nicht. Bieten Sie eine Funktion nur an, wenn der Host sie ankündigt.

### Dateien speichern: nxd:export

Die Sandbox kann keine Dateien herunterladen, also gibt Ihre App sie an nexxus:

```js
port.postMessage({ type: 'nxd:export', filename: 'board.svg', mime: 'image/svg+xml', data: svgString });
```

`data` darf ein String, ein `ArrayBuffer`, ein typisiertes Array oder ein `Blob` sein, übergeben als Bytes und nie als Base64. nexxus schaut nicht auf das Format. Es prüft:

- Der Export muss auf einen echten Klick in Ihrer App folgen. Ein Export ohne vorherigen Klick wird verworfen.
- Höchstens 50 MB.
- Mindestens eine Sekunde zwischen zwei Exporten, höchstens 20 je Minute. Sonst sieht die Person <Ui>Zu viele Exporte in kurzer Zeit. Bitte einen Moment warten.</Ui>
- `mime` muss die Form `typ/untertyp` haben. Den Dateinamen kürzt nexxus auf einen einfachen Namen mit höchstens 200 Zeichen (keine Pfade, keine Steuerzeichen, kein Punkt am Anfang). Eine ungültige Nachricht zeigt <Ui>Export fehlgeschlagen: Die App hat ungültige Daten geliefert.</Ui>
- Auf einem öffentlichen Link, der Downloads verbietet, sieht die Person <Ui>Für diesen Link sind Downloads nicht erlaubt.</Ui>

### Den KI-Assistenten zu einem Element fragen: nxd:assist

Ihre App erreicht kein Modell. Sie kann aber sagen, worauf die Person zeigt:

```js
port.postMessage({
  type: 'nxd:assist',
  context: {
    focus: { kind: 'block', id: 'b-7', label: 'Tabelle',
             path: 'Abschnitt „Liste“ › Tabelle', data: node },
    summary: 'Auftragstabelle mit 4 Spalten',
    excerpt: '<table class="orders">...</table>'
  },
  message: 'Diese Tabelle ist zu breit.'
});
```

nexxus zeigt das Element als Fokus des Gesprächs, öffnet den KI-Assistenten und legt `message` ins Eingabefeld, ohne es abzuschicken: Modell und Formulierung wählt weiterhin die Person. Der Fokus reist mit jeder folgenden Nachricht mit, bis die Person weiterzieht. `kind` legen Sie fest (`block`, `slide`, `entity`). Schicken Sie in `excerpt` Markup, statt ein Bild zu beschreiben: nexxus kann Ihren Rahmen nicht abfotografieren, und Markup sagt einem Modell mehr.

Alles, was die Sandbox-Grenze überquert, gilt als ungeprüft und wird gekürzt: `label` auf 120 Zeichen, `kind` auf 40, `path` und `summary` auf 300, `excerpt` auf 2000 und `data` auf 4000 Zeichen JSON.

Bieten Sie die Übergabe nur an, wenn `nxd:init` `hostCapabilities.assist === true` mitgebracht hat. Der Wert ist `false`, wo kein KI-Assistent bereitsteht, etwa wenn er für Ihre App abgeschaltet ist. Zeigen Sie dann den Kontext zum Kopieren an, statt eines Knopfs, der nichts tut.

### Hell und dunkel: nxd:theme

Die Bedienelemente Ihrer App (Leisten, Seitenpanels, Dialoge) sollten nexxus folgen, bis die Person in Ihrer App selbst umschaltet. Der Inhalt behält sein eigenes Aussehen. nexxus schickt `nxd:theme` nach jedem `nxd:init` und immer, wenn sich einer der beiden Werte ändert:

```js
const look = { host: 'light', pref: 'auto' };
const dark = () => (look.pref === 'auto' ? look.host : look.pref) === 'dark';
// bei nxd:theme:   look.host = m.mode; look.pref = m.pref; apply()
// eigener Schalter: const next = dark() ? 'light' : 'dark';
//                   look.pref = next === look.host ? 'auto' : next;
//                   port.postMessage({ type: 'nxd:theme-pref', pref: look.pref }); apply()
```

nexxus bewahrt die Wahl je Person und App in den Einstellungen der Person auf und schickt sie mit dem nächsten `nxd:theme` zurück. Speichern Sie sie nie in `data`: Sie gehört der Person, nicht dem Dokument. Zurück zum Modus von nexxus heißt `pref: 'auto'`.

### Vorführen: nxd:presenting

Schicken Sie `{type: 'nxd:presenting', on: true}`, wenn Ihre App eine Vollbild-Vorführung beginnt, und `on: false`, wenn sie endet. nexxus blendet solange die Zeiger der anderen aus und meldet den Zeiger der vorführenden Person nicht weiter. Die Live-Zeiger zeichnet nexxus selbst über Ihre App; dafür müssen Sie sonst nichts tun.

## Wie der KI-Assistent Ihre Dokumente ändert

Öffnet jemand den KI-Assistenten an einem Ihrer Dokumente, sagt nexxus ihm einmal zu Beginn des Gesprächs:

- dass es ein App-Dokument ist, welche App es ist und dass die aktuellen Daten mit jeder Nachricht mitkommen;
- Ihr Schema (direkt, wenn es höchstens 20.000 Zeichen hat, sonst holt der KI-Assistent es mit dem Werkzeug, das die Apps auflistet);
- Ihre `aiInstructions`, gefolgt von der Ergänzung des Mandanten;
- dass er das Dokument mit `nx_dmsapp_update` ändern soll, statt die Änderung zu beschreiben, unveränderte Startinhalte ersetzen soll und nie Nebendateien anlegen darf.

Mit jeder Nachricht bekommt der KI-Assistent die Daten so, wie die Person sie gerade sieht, samt ungespeicherten Änderungen und denen der Mitbearbeitenden. Über 100.000 Zeichen werden die Daten gekürzt, und der KI-Assistent soll statt der vollständigen Daten einen JSON Patch (RFC 6902) mit nur den geänderten Pfaden schicken:

```json
[
  { "op": "replace", "path": "/slides/0/boxes/1/text", "value": "Neu" }
]
```

Auf dem Server wendet nexxus die Änderung an, prüft das Ergebnis gegen Ihr Schema, prüft das Bearbeitungsrecht der Person, speichert eine neue Version und schickt die Daten mit `nxd:apply` und `origin: 'ai'` an jede offene Ansicht des Dokuments.

Ihre App darf nachbessern, was ein Modell nicht sehen kann, etwa Text, der aus einem Kasten läuft, oder neue Elemente, die alte überdecken. Tun Sie das nur bei `origin: 'ai'` und schicken Sie das Ergebnis als eigenes `nxd:change`. Die Änderung einer Mitbearbeiterin (`'peer'`) wurde schon so gezeichnet, wie sie es wollte: Lassen Sie sie, wie sie ist.

## Wie gemeinsames Bearbeiten funktioniert

- Jedes `nxd:change` wird etwa anderthalb Sekunden später gespeichert und sofort an alle anderen geschickt, die das Dokument offen haben. Dort kommt es als `nxd:apply` mit `origin: 'peer'` an. Jede Nachricht trägt die vollständigen Daten; eine Nachricht darf höchstens 4 MB groß sein.
- Wer das Dokument öffnet, während andere daran arbeiten, bekommt zuerst deren aktuellen Stand.
- Jedes Speichern nennt den Stand, auf dem es beruht. Hat dazwischen jemand anderes gespeichert, holt nexxus dessen Stand, führt ihn mit den Änderungen dieser Person zusammen und speichert erneut. Das Zusammenführen arbeitet auf Ihrem JSON: Objekte Schlüssel für Schlüssel, Arrays, deren Einträge alle eine eindeutige `id` tragen, Eintrag für Eintrag. Legen zwei Personen je eine Haftnotiz an, bleiben beide. Wo beide denselben Wert unterschiedlich geändert haben, gilt der Wert dieser Person, und der andere Stand wird als eigene Version gesichert. Steht eine Löschung auf der einen Seite gegen eine Änderung auf der anderen, bleibt der geänderte Eintrag.
- Eine Version für den Verlauf entsteht, wenn jemand das Dokument verlässt, oder nach zehn ruhigen Minuten.

Für Ihre App heißt das: Rechnen Sie jederzeit mit `nxd:apply`, auch mitten in einem Ziehvorgang oder während die Person tippt, und geben Sie jedem Eintrag einer Liste eine stabile, eindeutige `id`. Listen ohne IDs lassen sich nur als Ganzes zusammenführen.

## Die App testen

Das ganze Protokoll lässt sich ohne nexxus testen. Diese Seite spielt den Host: Sie setzt dieselbe Content-Security-Policy vor Ihre App, führt den Handshake aus, schickt `nxd:init` und protokolliert alles, was die App zurückschickt.

<FileTree>
- meine-app
  - manifest.json
  - app.html Ihre App
  - host.html der Test-Host unten
</FileTree>

```html
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>DMS-App test host</title>
<iframe id="app" sandbox="allow-scripts" style="width:100%;height:60vh;border:1px solid #ccc"></iframe>
<pre id="log"></pre>
<script>
  // The same policy nexxus puts in front of every app: no network at all.
  const CSP = "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data: blob:; " +
              "font-src data:; media-src data: blob:; form-action 'none'; base-uri 'none'";
  const seal = (html) => {
    const meta = `<meta http-equiv="Content-Security-Policy" content="${CSP}">`;
    const doctype = /^\s*(?:<!--[\s\S]*?-->\s*)*<!doctype[^>]*>/i.exec(html);
    return doctype ? doctype[0] + meta + html.slice(doctype[0].length) : meta + html;
  };

  const frame = document.getElementById('app');
  const log = (text) => { document.getElementById('log').textContent += text + '\n'; };
  let port = null;

  window.addEventListener('message', (e) => {
    if (e.source !== frame.contentWindow || e.data?.type !== 'nxd:ready') return;
    const channel = new MessageChannel();
    port = channel.port1;
    port.onmessage = (ev) => log('from app: ' + JSON.stringify(ev.data));
    frame.contentWindow.postMessage({type: 'nxd:connect'}, '*', [channel.port2]);
    port.postMessage({type: 'nxd:init', doc: {text: 'Hallo'}, meta: {title: 'Test'}, readonly: false, hostCapabilities: {assist: false}});
  });

  fetch('app.html').then((r) => r.text()).then((html) => { frame.srcdoc = seal(html); });
</script>
</html>
```

<Steps>

1. Stellen Sie den Ordner über einen beliebigen statischen Webserver bereit, zum Beispiel `python3 -m http.server 8000`, und öffnen Sie `http://localhost:8000/host.html`. Direkt von der Festplatte geöffnet geht es nicht, weil die Seite `app.html` mit `fetch` lädt.

2. Ändern Sie etwas in der App. Jede Änderung erscheint im Protokoll als `from app: {"type":"nxd:change",...}`.

3. Spielen Sie in der Browser-Konsole eine Mitbearbeiterin oder den KI-Assistenten:

   ```js
   port.postMessage({type: 'nxd:apply', doc: {text: 'Von außen'}, origin: 'ai'})
   ```

4. Setzen Sie in `host.html` `readonly: false` auf `true`, laden Sie neu und prüfen Sie, dass alle Bearbeitungselemente verschwunden sind.

5. Öffnen Sie die Netzwerkansicht des Browsers. Sie muss leer bleiben, solange die App läuft: Was die App zu laden versucht, wird hier genauso gesperrt wie in nexxus.

</Steps>

Danach probieren Sie die App in nexxus aus. Sind Sie kein Administrator, wird Ihr Import zu Ihrer eigenen App, die nur Sie und die Administratoren sehen. Das ist ein sicherer Ort zum Testen. Prüfen Sie, dass eine zweite Person an einem freigegebenen Dokument Ihre Änderungen live sieht, dass jemand mit Leserecht eine schreibgeschützte App bekommt und dass der KI-Assistent aus Ihrem Schema ein Dokument bauen kann.

Ihre App ist fertig, wenn sie mit dem Handshake startet, bei jeder Änderung `nxd:change` schickt, ein `nxd:apply` mitten in einer Änderung übersteht, im Nur-Lesen-Modus ihre Bearbeitungselemente ausblendet und nichts aus dem Netz lädt.

## Veröffentlichen

<Steps>

1. Erhöhen Sie `version` in `manifest.json` um eins für jedes Paket, das Sie herausgeben. nexxus lehnt ein Paket ab, dessen Version nicht höher ist als die installierte, und an der Nummer erkennen Sie, welchen Stand ein Mandant hat.

2. Ergänzen Sie einen `changelog`-Eintrag für die neue Version, geschrieben für die Menschen, die die App benutzen.

3. Bauen Sie die `.nxa`: `zip -j meine-app.nxa manifest.json app.html`.

4. Erste Installation: <Ui>App importieren</Ui> unter <MenuPath to="Start > Apps" />. Jede spätere Version: <Ui>Neue Version hochladen</Ui> auf der Seite der App. nexxus prüft das Paket und zeigt <Ui>Was sich ändert</Ui>, bevor jemand bestätigt. Siehe [Apps ohne Code erstellen](/de/apps/create/#eine-neue-version-veröffentlichen).

</Steps>

Neue Versionen lassen jedes vorhandene Dokument unangetastet. Halten Sie neue Schema-Eigenschaften optional, und wenn sich die Bedeutung vorhandener Daten ändern muss, lassen Sie Ihre App auch die alte Form lesen.

### Hochladen per API

Für automatische Veröffentlichungen gibt es dieselben Schritte als HTTP-Endpunkte für einen angemeldeten Benutzer. Ein Paket, das die Prüfung nicht besteht, beantwortet nexxus mit Status 400 und dem Grund als Meldung.

| Methode und Pfad | Was es tut |
|---|---|
| `POST /api/dms-app/import` | Installiert ein Paket (Multipart-Feld `file`). |
| `POST /api/dms-app/{appId}/version/preview` | Prüft ein Paket als nächste Version und liefert `valid`, `error`, `installedVersion`, `packageVersion` und die `changelog`-Einträge, die es mitbringt. Speichert nichts. |
| `POST /api/dms-app/{appId}/version` | Veröffentlicht ein Paket als nächste Version (Multipart-Feld `file`). |
| `GET /api/dms-app/` | Listet die Apps, die der Benutzer sehen darf, ohne ihr HTML. |
| `GET /api/dms-app/{appId}` | Eine App mit HTML, Schema und den wirksamen KI-Anweisungen. |
| `GET /api/dms-app/{appId}/detail` | Die App mit Dokumentanzahl, Versionshistorie und Einstellungen. |
| `PATCH /api/dms-app/{appId}/settings` | Setzt `aiInstructionsAddendum` (String mit höchstens 8000 Zeichen oder `null`) und `assistantDisabled` (Boolean). |
| `DELETE /api/dms-app/{appId}` | Entfernt die App. Ihre Dokumente bleiben. |

Wer was darf, ist dasselbe wie in der Oberfläche: Jeder Angemeldete darf importieren, Administratoren installieren für den Mandanten, und nur der Besitzer der App und Administratoren dürfen Versionen veröffentlichen, Einstellungen ändern oder eine App entfernen.

## Grenzen

| Was | Grenze |
|---|---|
| `app.html` | 2.000.000 Bytes |
| `contributes` | 256.000 Bytes, 8 Menüs, 24 Einträge je Menü |
| `aiInstructions`, Ergänzung | je 8000 Zeichen |
| App-ID | `[a-z][a-z0-9-]{1,39}`, nicht `board` oder `markdown` |
| Eine Nachricht beim gemeinsamen Bearbeiten | 4 MB |
| Daten, die der KI-Assistent vollständig sieht | 100.000 Zeichen, darüber ändert der KI-Assistent per JSON Patch |
| Schema, das der KI-Assistent direkt bekommt | 20.000 Zeichen, darüber holt der KI-Assistent es selbst |
| Export | 50 MB, 1 je Sekunde, 20 je Minute, nur nach einem Klick in der App |

## Wie es weitergeht

<CardGrid>
  <LinkCard title="Apps ohne Code erstellen" description="Importieren, Versionen veröffentlichen und eine App in der Oberfläche einrichten." href="/de/apps/create/" />
  <LinkCard title="NXM-Module" description="Wenn ein Dokument nicht reicht: Fachmodule mit eigenen Datensätzen und Rollen." href="/de/modules/overview/" />
</CardGrid>