# Ihr erstes Modul

> Sie bauen ein kleines NXM-Modul von Grund auf, mit einem Fachobjekt, einer Liste, einem Dialog, einer eigenen Seite und Beispieldaten. Dann paketieren Sie es, installieren es in einem Mandanten und liefern ein Update mit Datenmigration aus.

Brandt & Söhne verleiht Laptops, Monitore und Telefone an die eigenen Leute. Die Liste dazu steckt heute in einer Tabelle, der niemand traut: Wer hat das MacBook, und bis wann? In diesem Tutorial bauen Sie **Leihgeräte**, ein NXM-Modul mit einem eigenen Fachobjekt für Geräte, einer Liste mit Filtern, einem Dialog zum Ausleihen, einer Seite mit allen fälligen Rückgaben und Werkzeugen für den KI-Assistenten. Zum Schluss liefern Sie Version 1.1.0 aus und sehen zu, wie nexxus die gespeicherten Daten umstellt.

Sie schreiben fünf Dateien, packen sie in eine `.nxm`-Datei und laden sie hoch. Alles, was das Modul zeigt, steht in diesen Dateien. In nexxus selbst läuft kein Code des Moduls.

<Flow
  steps={[
    { icon: 'pen-line', title: 'Schreiben', text: 'module.json, ein Changelog, zwei HTML-Ansichten, Beispieldaten.' },
    { icon: 'hammer', title: 'Bauen', text: 'Das Paketier-Skript macht aus dem Ordner eine .nxm-Datei.' },
    { icon: 'upload', title: 'Hochladen', text: 'nexxus prüft das Paket und zeigt, was es mitbringt.', tone: 'cyan' },
    { icon: 'package-check', title: 'Installieren', text: 'Sie stimmen zu, vergeben Rollen, und das Modul steht im Menü.', tone: 'lavender' },
    { icon: 'rocket', title: 'Aktualisieren', text: 'Version 1.1.0 benennt einen Wert um und ergänzt ein Feld.' },
  ]}
  caption="Das ganze Tutorial auf einen Blick. Ab dem Hochladen läuft alles, ohne dass nexxus neu starten muss."
/>

## Bevor Sie beginnen

- **Ein Administratorkonto** in einer nexxus-Instanz. Nur Administratoren laden Pakete hoch und installieren sie.
- **Eine Instanz, die Ihr Paket annimmt.** Das Paket aus diesem Tutorial ist nicht signiert. Instanzen mit dem Spring-Profil `development`, `local` oder `demo` nehmen unsignierte Pakete an. Anderswo schaltet ein Administrator des Mandanten im Reiter <Ui>Herausgeber</Ui> <Ui>Unsignierte Pakete zulassen</Ui> ein, oder Sie signieren das Paket. Beides steht unter [Paketieren, installieren und aktualisieren](/de/modules/publish/).
- **Node.js** für das Paketier-Skript `module-package.mjs`. Es liegt in den nexxus-Quellen unter `backend/scripts/` und braucht keine weiteren Pakete. Kopieren Sie es neben Ihren Paketordner.
- **Einen Texteditor.**

## Das Modul planen

Bevor Sie die erste Zeile schreiben, legen Sie vier Dinge fest. Sie landen im Manifest und lassen sich später nur schwer ändern.

| Entscheidung | Wahl in diesem Tutorial | Warum |
|---|---|---|
| Schlüssel | `acme.leihgeraete` | `<herausgeber>.<name>`, klein geschrieben. Der Schlüssel ändert sich nie. Rechte, Werkzeugnamen und Routen leiten sich davon ab. |
| Bereich | `projekte` | Der Fachbereich des Moduls. `projekte` erscheint in der linken Leiste unter **Organisation**. |
| Fachobjekte | eines, `geraet` | Jedes Fachobjekt bekommt eine eigene Liste und eigene Datensatzseiten. |
| Wer darf was | Rollen `verwaltung` (verwalten und verleihen) und `einsicht` (nur lesen) | Die Rollen entstehen bei der Installation. Niemand hat sie, bis ein Administrator sie vergibt. |

## Den Ordner anlegen

Jede Version bekommt einen eigenen Ordner, und der Ordner heißt wie die Version. Sonst meldet das Skript `Hinweis: Der Ordner heißt nicht wie die Version 1.0.0.`

<FileTree>
- acme.leihgeraete/
  - 1.0.0/
    - module.json das Manifest
    - CHANGELOG.md
    - ui/
      - ausleihen.html Dialog zum Ausleihen
      - faellig.html Seite mit fälligen Rückgaben
    - demo/
      - geraet.json Beispieldaten, auf Wunsch installiert
</FileTree>

## Das Manifest schreiben

`module.json` beschreibt alles: das Fachobjekt mit seinen Feldern, Rechte, Rollen, Ansichten, Menüeinträge, Aktionen und was der KI-Assistent tun darf. Legen Sie die Datei mit diesem Inhalt an.

```json title="acme.leihgeraete/1.0.0/module.json"
{
  "key": "acme.leihgeraete",
  "kind": "package",
  "publisher": "acme",
  "name": {"de": "Leihgeräte", "en": "Loan equipment"},
  "description": {"de": "Geräte, die das Team ausleiht: wer hat was, bis wann.", "en": "Equipment the team borrows: who has what, until when."},
  "icon": "devices",
  "area": "projekte",
  "version": "1.0.0",
  "platform": ">=0.2.0 <1.0.0",
  "permissions": [
    {"key": "verleihen", "label": {"de": "Geräte verleihen", "en": "Lend equipment"},
     "description": {"de": "Darf Geräte ausleihen und zurücknehmen.", "en": "May lend equipment and take it back."}}
  ],
  "roles": [
    {"key": "verwaltung", "label": {"de": "Geräteverwaltung", "en": "Equipment management"},
     "permissions": ["geraet.read", "geraet.write", "verleihen"]},
    {"key": "einsicht", "label": {"de": "Einsicht", "en": "View"}, "permissions": ["geraet.read"]}
  ],
  "entities": [
    {
      "key": "geraet",
      "label": {"de": "Gerät", "en": "Device"},
      "labelPlural": {"de": "Geräte", "en": "Devices"},
      "icon": "devices",
      "titleField": "bezeichnung",
      "searchFields": ["bezeichnung", "inventarnummer"],
      "fields": [
        {"key": "inventarnummer", "type": "string", "sequence": "GER-{n:4}", "label": {"de": "Inventarnummer", "en": "Inventory no."}},
        {"key": "bezeichnung", "type": "string", "required": true, "maxLength": 120, "label": {"de": "Bezeichnung", "en": "Name"}},
        {"key": "kategorie", "type": "enum", "indexed": true, "default": "laptop", "label": {"de": "Kategorie", "en": "Category"},
         "values": [
           {"key": "laptop", "label": {"de": "Laptop", "en": "Laptop"}},
           {"key": "monitor", "label": {"de": "Monitor", "en": "Monitor"}},
           {"key": "telefon", "label": {"de": "Telefon", "en": "Phone"}},
           {"key": "sonstiges", "label": {"de": "Sonstiges", "en": "Other"}}]},
        {"key": "status", "type": "enum", "indexed": true, "readonly": true, "default": "verfuegbar", "label": {"de": "Status", "en": "Status"},
         "values": [
           {"key": "verfuegbar", "label": {"de": "Verfügbar", "en": "Available"}},
           {"key": "verliehen", "label": {"de": "Verliehen", "en": "On loan"}}]},
        {"key": "ausgeliehen_an", "type": "ref", "target": "core:user", "label": {"de": "Ausgeliehen an", "en": "Lent to"}},
        {"key": "rueckgabe_am", "type": "date", "indexed": true, "label": {"de": "Rückgabe am", "en": "Due back"}},
        {"key": "notiz", "type": "text", "label": {"de": "Notiz", "en": "Note"}}
      ]
    }
  ],
  "views": [
    {"key": "geraet.list", "type": "list", "entity": "geraet", "label": {"de": "Geräte", "en": "Devices"},
     "columns": [{"field": "inventarnummer"}, {"field": "bezeichnung"}, {"field": "kategorie"},
                 {"field": "status", "display": "badge"}, {"field": "ausgeliehen_an"}, {"field": "rueckgabe_am"}],
     "filters": ["kategorie", "status", "ausgeliehen_an"], "sort": {"field": "inventarnummer", "dir": "asc"}, "create": true},
    {"key": "geraet.record", "type": "record", "entity": "geraet", "label": {"de": "Daten", "en": "Data"},
     "sections": [
       {"key": "geraet", "label": {"de": "Gerät", "en": "Device"}, "fields": ["inventarnummer", "bezeichnung", "kategorie", "status"]},
       {"key": "ausleihe", "label": {"de": "Ausleihe", "en": "Loan"}, "fields": ["ausgeliehen_an", "rueckgabe_am", "notiz"]}],
     "actions": ["ausleihen-dialog", "zurueckgeben"]},
    {"key": "ausleihen-dialog", "type": "custom", "slot": "record.action", "target": "geraet",
     "html": "ui/ausleihen.html", "label": {"de": "Ausleihen", "en": "Lend"}, "icon": "assignment_ind",
     "when": {"field": "status", "in": ["verfuegbar"]}},
    {"key": "faellig", "type": "custom", "slot": "page", "html": "ui/faellig.html",
     "label": {"de": "Fällige Rückgaben", "en": "Due returns"}}
  ],
  "menu": [
    {"key": "geraete", "label": {"de": "Geräte", "en": "Devices"}, "icon": "devices", "view": "geraet.list", "order": 1},
    {"key": "faellig", "label": {"de": "Fällige Rückgaben", "en": "Due returns"}, "icon": "assignment_late", "view": "faellig", "order": 2}
  ],
  "actions": [
    {"key": "ausleihen", "entity": "geraet", "type": "update", "label": {"de": "Ausleihen", "en": "Lend"}, "icon": "assignment_ind",
     "permission": "verleihen", "when": {"field": "status", "in": ["verfuegbar"]},
     "set": {"status": "verliehen", "ausgeliehen_an": "{{payload.ausgeliehen_an}}", "rueckgabe_am": "{{payload.rueckgabe_am}}"}},
    {"key": "zurueckgeben", "entity": "geraet", "type": "update", "label": {"de": "Zurückgeben", "en": "Return"}, "icon": "assignment_return",
     "permission": "verleihen", "when": {"field": "status", "in": ["verliehen"]},
     "confirm": {"de": "Gerät als zurückgegeben buchen?", "en": "Book the device as returned?"},
     "set": {"status": "verfuegbar", "ausgeliehen_an": null, "rueckgabe_am": null}, "aiTool": true}
  ],
  "aiTools": {"entities": ["geraet"]},
  "assistant": {
    "instructions": {"de": "Du hilfst bei den Leihgeräten des Teams. Den Status ändern nur die Aktionen „Ausleihen“ und „Zurückgeben“.",
                     "en": "You help with the team's loan equipment. Only the actions Lend and Return change the status."},
    "prompts": [
      {"view": "geraet.list", "label": {"de": "Wer hat welches Gerät?", "en": "Who has which device?"},
       "prompt": {"de": "Liste alle verliehenen Geräte mit Person und Rückgabedatum.", "en": "List every device on loan with person and due date."}},
      {"view": "faellig", "label": {"de": "Erinnerung entwerfen", "en": "Draft a reminder"},
       "prompt": {"de": "Entwirf eine freundliche Erinnerung an alle, deren Rückgabe fällig ist.", "en": "Draft a friendly reminder to everyone whose return is due."}}
    ]
  }
}
```

### Warum das Manifest so aussieht

Einige Entscheidungen in dieser Datei lohnen sich auch für Ihre eigenen Module:

- **`status` ist `readonly`.** Niemand setzt ein Gerät über das Formular auf „verliehen“. Nur die beiden Aktionen ändern den Status, und jede prüft auf dem Server ihr Recht `verleihen` und ihre `when`-Bedingung. Ein Formular, das den Wert unverändert zurückschickt, speichert trotzdem.
- **Ausleihen braucht Eingaben, Zurückgeben nicht.** Die Aktion `ausleihen` braucht eine Person und ein Datum. Deshalb läuft sie über die Dialog-Ansicht `ausleihen-dialog`, die beides abfragt und dann die Aktion aufruft. Die Aktion `zurueckgeben` braucht nichts und steht direkt in der Aktionsleiste des Datensatzes, mit einer Rückfrage vorab. Welche Einträge die Leiste zeigt, legt die Liste `actions` der Datensatz-Ansicht fest.
- **`inventarnummer` ist eine `sequence`.** Wird ein Gerät ohne Nummer angelegt, vergibt nexxus die nächste: `GER-0001`, `GER-0002` und so weiter. Ein Sequenzfeld ist per Definition eindeutig und schreibgeschützt, und Sie können danach sortieren.
- **Jedes Filter- und Sortierfeld ist indiziert.** `kategorie`, `status` und `rueckgabe_am` tragen `"indexed": true`. Verweise wie `ausgeliehen_an` sind automatisch indiziert. Wer nach einem nicht indizierten Feld filtert oder sortiert, bekommt das Paket beim Hochladen zurück.
- **Der KI-Assistent kann mit Geräten arbeiten.** `aiTools.entities` gibt ihm Werkzeuge zum Suchen und Lesen von `geraet` und die Aktionen zum Anlegen und Ändern (`acme.leihgeraete:geraet.create`, `acme.leihgeraete:geraet.update`). `"aiTool": true` an `zurueckgeben` ergänzt die Aktion `acme.leihgeraete:zurueckgeben`. Ausführen tut der KI-Assistent keine Aktion selbst: Er schlägt sie als Karte im Chat vor, und erst der Klick der Person führt sie aus.

Alle Felder und Optionen finden Sie unter [Paketaufbau](/de/modules/structure/).

## Den Changelog ergänzen

Jedes Paket bringt einen Changelog mit einer Überschrift für die eigene Version mit. nexxus zeigt ihn auf der Paketseite und im Dialog zum Aktualisieren.

```markdown title="acme.leihgeraete/1.0.0/CHANGELOG.md"
# Changelog: Leihgeräte

## [1.0.0] - 2026-10-12

- Geräte mit Inventarnummer, Kategorie und Status; ausleihen und zurückgeben mit Rückgabedatum.
- Seite „Fällige Rückgaben“.
```

## Die Dialog-Ansicht schreiben

`ui/ausleihen.html` ist eine eigene Ansicht im Slot `record.action`. nexxus zeigt sie als Dialog auf der Seite eines Geräts und zeichnet den Kopf des Dialogs selbst. Die Ansicht zeichnet nur die Felder und ihre Schaltfläche. Sie läuft abgeschottet und spricht mit nexxus über das View-SDK `window.nx`, das schon geladen ist, wenn das erste Skript der Ansicht startet.

```html title="acme.leihgeraete/1.0.0/ui/ausleihen.html"
<!doctype html>
<html>
<head><meta charset="utf-8"></head>
<body>
  <div class="nx-stack">
    <label class="nx-field">
      <span class="nx-field__label" id="l-person"></span>
      <select class="nx-select" id="person"></select>
    </label>
    <label class="nx-field">
      <span class="nx-field__label" id="l-datum"></span>
      <input class="nx-input" type="date" id="datum">
    </label>
    <div class="nx-row"><span class="nx-spacer"></span><button class="nx-btn nx-btn--primary" id="ok"></button></div>
  </div>
  <script>
    (async () => {
      const ctx = await nx.ready();
      const $ = id => document.getElementById(id);
      $('l-person').textContent = nx.t({de: 'Ausleihen an', en: 'Lend to'});
      $('l-datum').textContent = nx.t({de: 'Rückgabe am', en: 'Due back'});
      $('ok').textContent = nx.t({de: 'Ausleihen', en: 'Lend'});
      for (const person of await nx.lookup('core:user', {})) {   // die ersten 20; mit {q} suchen
        const option = document.createElement('option');
        option.value = String(person.id);
        option.textContent = person.label;
        $('person').append(option);
      }
      $('ok').onclick = async () => {
        try {
          const {record} = await nx.invoke('ausleihen', {
            recordId: ctx.record.id,
            payload: {ausgeliehen_an: Number($('person').value), rueckgabe_am: $('datum').value || null},
          });
          nx.dialog.close(record);
        } catch (e) {
          nx.ui.toast(e.message, {kind: 'error'});
        }
      };
    })();
  </script>
</body>
</html>
```

Was hier passiert:

- `nx.ready()` liefert den Kontext. In einem Dialog an einem Datensatz steht in `ctx.record` das Fachobjekt und die Id des Geräts.
- `nx.lookup('core:user', {})` listet Personen des Mandanten, ohne Suchbegriff die ersten 20. Dienst- und Systemkonten fehlen in der Liste. Ein echter Dialog sucht mit `{q}`, während die Person tippt.
- `nx.invoke('ausleihen', ...)` führt die Aktion als die Person aus, die geklickt hat. nexxus prüft Recht und `when`-Bedingung noch einmal. Lehnt der Server ab, erscheint die Meldung als Hinweis.
- `nx.dialog.close(record)` schließt den Dialog. Die Datensatzseite liest das Gerät neu und zeigt den neuen Stand.
- Die Beschriftungen kommen aus `nx.t`, die Ansicht spricht also die Sprache der Person. Text kommt über `textContent` in die Seite, nie über `innerHTML`.
- Die Klassen `nx-*` stammen aus dem UI-Kit, das nexxus in jede Ansicht einsetzt. So sieht der Dialog aus wie der Rest der Anwendung, im hellen wie im dunklen Modus.

Alle Funktionen des SDK stehen unter [Ansichten und das View-SDK](/de/modules/views/).

## Die Seiten-Ansicht schreiben

`ui/faellig.html` ist eine eigene Seite (Slot `page`). Der Menüeintrag „Fällige Rückgaben“ führt dorthin. Sie sucht verliehene Geräte, deren Rückgabe heute oder früher fällig ist.

```html title="acme.leihgeraete/1.0.0/ui/faellig.html"
<!doctype html>
<html>
<head><meta charset="utf-8"><style>body { padding: 16px; }</style></head>
<body>
  <p class="nx-muted" id="hint"></p>
  <div class="nx-list" id="list"></div>
  <div class="nx-empty" id="empty" hidden><div class="nx-empty__title" id="empty-title"></div></div>
  <script>
    (async () => {
      await nx.ready();
      const $ = id => document.getElementById(id);
      $('hint').textContent = nx.t({de: 'Verliehene Geräte, deren Rückgabe heute oder früher fällig ist.', en: 'Devices on loan due back today or earlier.'});
      $('empty-title').textContent = nx.t({de: 'Alles zurück.', en: 'Everything is back.'});
      const today = new Date().toLocaleDateString('sv-SE');   // JJJJ-MM-TT in Ortszeit
      const {items} = await nx.records.search('geraet', {
        filter: {status: 'verliehen', rueckgabe_am: {to: today}},
        sort: 'rueckgabe_am', size: 100,
      });
      $('empty').hidden = items.length > 0;
      for (const g of items) {
        const item = document.createElement('button');
        item.className = 'nx-list__item';
        item.textContent = `${g.title} · ${g.refs?.ausgeliehen_an?.label ?? '-'} · ${g.data.rueckgabe_am}`;
        item.onclick = () => nx.nav.open({entity: 'geraet', id: g.id});
        $('list').append(item);
      }
      nx.assistant.setContext({
        summary: nx.t({de: `${items.length} fällige Rückgaben`, en: `${items.length} due returns`}),
        records: items.map(g => ({entity: 'geraet', id: g.id})),
      });
    })();
  </script>
</body>
</html>
```

Die Suche filtert auf zwei indizierte Felder. Ein Datumsbereich nur mit `to` schließt diesen Tag ein. `g.title` ist die `bezeichnung` des Geräts (sein `titleField`), und `g.refs` enthält den Anzeigenamen jeder verwiesenen Person. `nx.nav.open` öffnet die Seite des Geräts. `nx.assistant.setContext` sagt dem KI-Assistenten, welche Geräte die Person gerade sieht. Eine Bitte wie „Erinnere sie“ trifft dann die richtigen Leute.

## Beispieldaten ergänzen

Beispieldaten sind freiwillig. Setzt der Administrator bei der Installation den Haken <Ui>Beispieldaten anlegen</Ui>, legt nexxus diese Zeilen an. Platzhalter füllen Werte ein, die es erst im Zielmandanten gibt.

```json title="acme.leihgeraete/1.0.0/demo/geraet.json"
[
  {"key": "thinkpad", "data": {"bezeichnung": "ThinkPad T14", "kategorie": "laptop", "status": "verfuegbar"}},
  {"key": "macbook", "data": {"bezeichnung": "MacBook Air", "kategorie": "laptop", "status": "verliehen",
                              "ausgeliehen_an": {"$currentUser": true}, "rueckgabe_am": {"$date": "-2wd"}}},
  {"key": "monitor", "data": {"bezeichnung": "Monitor 27 Zoll", "kategorie": "monitor", "status": "verliehen",
                              "ausgeliehen_an": {"$currentUser": true}, "rueckgabe_am": {"$date": "+1w"}}}
]
```

- `{"$currentUser": true}` wird zum Administrator, der installiert.
- `{"$date": "-2wd"}` liegt zwei Werktage vor dem Installationstag, `"+1w"` eine Woche danach. Das MacBook ist also immer überfällig, wenn jemand das Modul ausprobiert, der Monitor nie.
- Beispieldaten dürfen `status` setzen, obwohl das Feld schreibgeschützt ist: Datendateien schreibt der Installer, keine Person.

Alle Platzhalter beschreibt [Daten und Aktualisierungen](/de/modules/data-and-upgrades/).

## Das Paket bauen

<Steps>

1. Öffnen Sie ein Terminal in dem Ordner, in dem `acme.leihgeraete/` und `module-package.mjs` liegen.

2. Bauen Sie das Archiv:

   ```sh
   node module-package.mjs build acme.leihgeraete/1.0.0
   ```

   Das Skript prüft, ob `module.json` die Angaben `kind`, `key` und `version` hat und ob `CHANGELOG.md` die Überschrift `## [1.0.0]` enthält. Es schreibt `acme.leihgeraete-1.0.0.nxm` samt einer Datei `MANIFEST.sha256` mit dem Hash jeder Datei und meldet Dateiname, `5 Dateien`, Größe und `unsigniert`.

3. Wenn Sie möchten, lesen Sie das Archiv zur Kontrolle zurück:

   ```sh
   node module-package.mjs verify acme.leihgeraete-1.0.0.nxm
   ```

   Geprüft werden die Hashes und die Changelog-Überschrift. Den Inhalt des Manifests prüft erst nexxus beim Hochladen.

</Steps>

Derselbe Ordner ergibt immer dasselbe Archiv, Byte für Byte.

## Hochladen und installieren

<Steps>

1. Melden Sie sich als Administrator an und öffnen Sie <MenuPath to="Administration > Betrieb > Module" />. Wechseln Sie auf den Reiter <Ui>Pakete</Ui>.

2. Klicken Sie auf <Ui>Paket hochladen (.nxm)</Ui> und wählen Sie `acme.leihgeraete-1.0.0.nxm`. nexxus lädt die Datei hoch und prüft sie. Danach erscheint der Hinweis <Ui>Leihgeräte 1.0.0 hochgeladen.</Ui> und die Paketseite öffnet sich.

3. Lesen Sie die Karte <Ui>Was das Paket mitbringt</Ui>. Für Leihgeräte steht dort:

   | Zeile | Inhalt |
   |---|---|
   | <Ui>Fachobjekte</Ui> | Gerät |
   | <Ui>Menüeinträge</Ui> | Geräte, Fällige Rückgaben |
   | <Ui>Ansichten</Ui> | Geräte · Liste, Daten · Datenreiter, Ausleihen · Dialog am Datensatz, Fällige Rückgaben · Seite |
   | <Ui>Rollen</Ui> | Geräteverwaltung, Einsicht, jeweils mit ihren Rechten |
   | <Ui>Daten außerhalb des Pakets</Ui> | keine |
   | <Ui>Externe Dienste</Ui> | keine |
   | <Ui>Werkzeuge für den Assistenten</Ui> | `nx_acme_leihgeraete_geraet_search`, `_get`, `acme.leihgeraete:geraet.create`, `.update` und `acme.leihgeraete:zurueckgeben` |
   | <Ui>Rechte</Ui> | Geräte lesen (`acme.leihgeraete.geraet.read`), Geräte bearbeiten (`acme.leihgeraete.geraet.write`), Geräte verleihen (`acme.leihgeraete.verleihen`) |

   Weil das Paket nicht signiert ist, steht darüber ein gelber Hinweis: <Ui>Nicht signiert. Prüfen Sie die Herkunft, bevor Sie es installieren.</Ui>

   <Screenshot src={shot_nxm_30} url="/modules/pakete/acme.leihgeraete?version=1.0.0" alt={"Die Paketseite des Tutorial-Pakets nach dem Hochladen"} caption={"Was das Tutorial-Paket mitbringt, gelesen aus seinem Manifest."} />

4. Klicken Sie in der Aktionsleiste auf <Ui>Installieren</Ui>. Der Dialog <Ui>Leihgeräte installieren</Ui> öffnet sich. Unter <Ui>Ihre Zustimmung</Ui> steht <Ui>Das Paket nutzt keine Daten außerhalb seiner eigenen.</Ui>, denn Leihgeräte fragt weder nach Kerndaten noch nach einem externen Dienst.

5. Wählen Sie unter <Ui>Rollen zuweisen</Ui> die Personen für jede Rolle und bestätigen Sie jede Auswahl mit <Ui>Anwenden</Ui>. Bei Brandt & Söhne bekommt Tom Keller aus der IT die Rolle <Ui>Geräteverwaltung</Ui> und Mia Schulz die Rolle <Ui>Einsicht</Ui>. Sie können beide Felder leer lassen und die Rollen später im Rolleneditor vergeben.

6. Setzen Sie den Haken bei <Ui>Beispieldaten anlegen</Ui> und klicken Sie auf <Ui>Installieren</Ui>. Der Hinweis <Ui>Leihgeräte ist installiert.</Ui> bestätigt es.

   <Screenshot src={shot_nxm_31} url="/modules/pakete/acme.leihgeraete?version=1.0.0" alt={"Das Tutorial-Paket installieren, mit Rollen und Beispieldaten"} caption={"Keine Daten außerhalb der eigenen, also gibt es nichts zuzustimmen."} />

</Steps>

## Ausprobieren

<Steps>

1. Öffnen Sie in der linken Leiste den Bereich **Organisation**. Unter den eingebauten Abschnitten steht jetzt ein neuer Abschnitt <Ui>Leihgeräte</Ui> mit den Einträgen <Ui>Geräte</Ui> und <Ui>Fällige Rückgaben</Ui>. Neu laden müssen Sie dafür nicht.

   <Screenshot src={shot_nxm_32} url="/x/acme.leihgeraete/geraet" alt={"Der neue Menüabschnitt des Moduls"} caption={"Das Modul erscheint ohne Neuladen im Menü."} />

2. Klicken Sie auf <Ui>Geräte</Ui> (Route `/x/acme.leihgeraete/geraet`). Die Liste zeigt die drei Beispielgeräte mit Nummern ab `GER-0001`, das Suchfeld <Ui>Geräte durchsuchen</Ui>, die Menüs <Ui>Filter</Ui> (Kategorie, Status, Person) und <Ui>Sortieren</Ui> und die Schaltfläche <Ui>Gerät anlegen</Ui>.

   <Screenshot src={shot_nxm_24} url="/x/acme.leihgeraete/geraet" alt={"Die Geräteliste des Tutorial-Moduls"} caption={"Die Liste entsteht aus der Listenansicht im Manifest."} />

3. Klicken Sie auf <Ui>Gerät anlegen</Ui>. Der Reiter <Ui>Daten</Ui> zeigt die Abschnitte <Ui>Gerät</Ui> und <Ui>Ausleihe</Ui>. Bei der Inventarnummer steht <Ui>Wird beim Anlegen vergeben</Ui>. Tragen Sie eine Bezeichnung ein und klicken Sie auf <Ui>Anlegen</Ui>.

4. Klicken Sie am neuen Gerät in der Aktionsleiste auf <Ui>Ausleihen</Ui>. Der Dialog aus `ausleihen.html` öffnet sich. Wählen Sie eine Person, etwa Mia Schulz, und ein Datum und klicken Sie auf <Ui>Ausleihen</Ui>. Der Dialog schließt sich, der Status zeigt <Ui>Verliehen</Ui>, und die Aktionsleiste bietet jetzt <Ui>Zurückgeben</Ui> an.

   <Screenshot src={shot_nxm_33} url="/x/acme.leihgeraete/geraet/50/daten" alt={"Ein Gerät im Dialog des Moduls ausleihen"} caption={"Der Dialog ist eine eigene Ansicht; den Kopf zeichnet nexxus."} />

5. Klicken Sie auf <Ui>Zurückgeben</Ui>. nexxus fragt <Ui>Gerät als zurückgegeben buchen?</Ui>. Klicken Sie auf <Ui>Ausführen</Ui>. Der Hinweis lautet <Ui>„Zurückgeben“ ausgeführt.</Ui>

6. Öffnen Sie <Ui>Fällige Rückgaben</Ui> (Route `/x/acme.leihgeraete/p/faellig`). Das Beispiel-MacBook steht dort, weil sein Rückgabedatum zwei Werktage zurückliegt.

   <Screenshot src={shot_nxm_34} url="/x/acme.leihgeraete/p/faellig" alt={"Die Seite mit den fälligen Rückgaben"} caption={"Eine eigene Seite, die Geräte mit fälliger Rückgabe sucht."} />

7. Öffnen Sie den KI-Assistenten auf der Geräteliste. Die Beispiel-Anweisung <Ui>Wer hat welches Gerät?</Ui> erscheint als Chip. Bitten Sie den KI-Assistenten, das MacBook als zurückgegeben zu buchen: Bevor sich etwas ändert, zeigt der Chat eine Aktionskarte mit den drei Prüfungen <Ui>Eingaben</Ui>, <Ui>Zustand</Ui> und <Ui>Berechtigung</Ui> und der Schaltfläche <Ui>Ausführen</Ui>. Geprüft hat der Server schon; eine rote Prüfung nennt, was fehlt, und die Schaltfläche bleibt aus. Je nach Formulierung wählt der KI-Assistent auch eine andere Aktion des Moduls, etwa das Ändern des Geräts; die Karte zeigt dann diese. Bitten Sie um zwei Änderungen auf einmal, trägt eine Karte beide, mit <Ui>Alle 2 ausführen</Ui>.

</Steps>

Wer keine Administratorrolle hat, sieht das Modul erst mit einer seiner Rollen: <Ui>Geräteverwaltung</Ui> für alles, <Ui>Einsicht</Ui> zum Lesen. Mehr dazu unter [Rollen](/de/setup/roles/).

## Version 1.1.0 ausliefern

Brandt & Söhne wünscht sich zwei Änderungen: „Telefon“ soll „Smartphone“ heißen, und jedes Gerät braucht einen Standort. Eine ausgelieferte Version ändert sich nie. Deshalb legen Sie Version 1.1.0 daneben an.

<Steps>

1. Kopieren Sie den Ordner `acme.leihgeraete/1.0.0` nach `acme.leihgeraete/1.1.0`.

2. Setzen Sie in der Kopie von `module.json` den Wert `"version": "1.1.0"`. Ersetzen Sie im Feld `kategorie` den Wert `telefon` durch:

   ```json
   {"key": "smartphone", "label": {"de": "Smartphone", "en": "Smartphone"}}
   ```

3. Fügen Sie hinter `status` ein neues Feld ein und nehmen Sie `"standort"` in die `fields` des Abschnitts `geraet` in `geraet.record` auf:

   ```json
   {"key": "standort", "type": "enum", "indexed": true, "default": "buero", "label": {"de": "Standort", "en": "Location"},
    "values": [
      {"key": "buero", "label": {"de": "Büro", "en": "Office"}},
      {"key": "homeoffice", "label": {"de": "Homeoffice", "en": "Home office"}}]}
   ```

4. Sagen Sie nexxus, wie die gespeicherten Daten umziehen. Ergänzen Sie auf oberster Ebene von `module.json`:

   ```json
   "migrations": {
     "1.1.0": {
       "pre":  [{"op": "renameValue", "entity": "geraet", "field": "kategorie", "from": "telefon", "to": "smartphone"}],
       "post": [{"op": "setDefault", "entity": "geraet", "field": "standort", "value": "buero"}]
     }
   }
   ```

   `pre`-Schritte laufen, bevor nexxus auf die neue Version umschaltet, `post`-Schritte danach. `renameValue` macht aus jedem gespeicherten `telefon` ein `smartphone`. `setDefault` gibt jedem vorhandenen Gerät den Standort `buero`.

5. Stellen Sie in `CHANGELOG.md` einen neuen Abschnitt nach oben:

   ```markdown
   ## [1.1.0] - 2026-10-19

   - Kategorie „Telefon“ heißt jetzt „Smartphone“; bestehende Geräte werden umgestellt.
   - Neues Feld Standort (Büro oder Homeoffice); bestehende Geräte stehen im Büro.
   ```

6. Bauen Sie mit `node module-package.mjs build acme.leihgeraete/1.1.0` und laden Sie `acme.leihgeraete-1.1.0.nxm` auf dem Reiter <Ui>Pakete</Ui> hoch.

7. Die Paketseite zeigt den Chip <Ui>Update auf 1.1.0</Ui>. Klicken Sie auf <Ui>Auf 1.1.0 aktualisieren</Ui>. Der Dialog <Ui>Leihgeräte aktualisieren</Ui> zeigt <Ui>Von 1.0.0 auf 1.1.0</Ui>, unter <Ui>Was sich ändert</Ui> den neuen Changelog-Abschnitt und unter <Ui>Datenanpassungen</Ui> einen Probelauf auf den heutigen Daten: `renameValue` auf `geraet.kategorie` (<Ui>vor dem Wechsel</Ui>) und `setDefault` auf `geraet.standort` (<Ui>nach dem Wechsel</Ui>), jeweils mit der Zahl der Datensätze, die sich ändern. Mit nur den Beispieldaten sind das 0 und 3, dazu das Gerät, das Sie selbst angelegt haben. Einen Abschnitt <Ui>Neu zuzustimmen</Ui> gibt es nicht, denn 1.1.0 verlangt keinen neuen Datenzugriff.

   <Screenshot src={shot_nxm_35} url="/modules/pakete/acme.leihgeraete?version=1.1.0" alt={"Das Tutorial-Paket auf 1.1.0 aktualisieren, mit zwei Migrationsschritten"} caption={"Der Probelauf zählt die Datensätze, die jeder Schritt ändert."} />

8. Klicken Sie auf <Ui>Aktualisieren</Ui>. Der Hinweis lautet <Ui>Leihgeräte ist auf 1.1.0 aktualisiert.</Ui>

</Steps>

Nicht jede Änderung ist in einer Nebenversion erlaubt. Hätten Sie `standort` zur Pflicht gemacht, ohne einen Standardwert anzugeben, hätte der Dialog abgelehnt: <Ui>Das neue Pflichtfeld 'geraet.standort' hat keinen Standardwert.</Ui> Die Regeln stehen unter [Daten und Aktualisierungen](/de/modules/data-and-upgrades/).

## Was im Hintergrund passiert

**Beim Hochladen** entpackt nexxus das Archiv und prüft `module.json` gegen das Paketschema. Danach folgen die Regeln, die ein Schema nicht ausdrücken kann: Jede Spalte, jeder Filter, jeder Abschnitt und jede Aktion nennt ein vorhandenes Feld, Menüeinträge zeigen auf Listen oder Seiten, Rollen nennen vorhandene Rechte, und jede Datei, die das Manifest nennt, liegt im Archiv. nexxus meldet alle Probleme auf einmal. Zuletzt prüft es `MANIFEST.sha256` und die Signatur. Gespeichert wird das Paket nur für Ihren Mandanten.

**Bei der Installation** erledigt nexxus in einer Transaktion:

- Es legt die Installation mit dem Stand <Ui>Aktiv</Ui> an und hält Ihre Zustimmung fest.
- Es legt je Manifest-Rolle eine Zugriffsrolle an, benannt nach Modul und Rolle: <Ui>Leihgeräte – Geräteverwaltung</Ui> und <Ui>Leihgeräte – Einsicht</Ui>. Die gewählten Personen bekommen sie sofort.
- Es legt aus `assistant.instructions` einen firmenweiten Skill für den KI-Assistenten an, <Ui>Leihgeräte: Arbeitsweise</Ui>.
- Es schreibt die Beispieldaten, löst die Platzhalter auf und vergibt die Inventarnummern.

Nach dem Abschluss stehen Menü, Routen und die Werkzeuge des KI-Assistenten allen zur Verfügung, die die nötigen Rechte haben. Im Rolleneditor erscheinen die Rechte als <Ui>Geräte lesen</Ui>, <Ui>Geräte bearbeiten</Ui> und <Ui>Geräte verleihen</Ui>.

**Im Betrieb** laufen jede Liste, jedes Formular und jede Aktion durch dieselben Prüfungen auf dem Server, egal ob eine Person klickt, eine Ansicht das SDK aufruft oder der KI-Assistent ein Werkzeug benutzt. Die Ansichten laufen abgeschottet, ohne Netzwerkzugriff und ohne Zugriff auf die Seite um sie herum.

**Beim Aktualisieren** laufen die `pre`-Schritte, der Wechsel auf 1.1.0 und die `post`-Schritte in einer Transaktion. Scheitert ein Schritt, wird alles zurückgerollt, und das Modul bleibt auf 1.0.0. Rollen, die es schon in 1.0.0 gab, bekommen die Rechte, die in 1.1.0 neu sind. Rechte, die ein Administrator entfernt hat, bleiben entfernt.

## Wenn etwas schiefgeht

Lehnt nexxus ein Paket ab, zeigt der Katalog <Ui>„acme.leihgeraete-1.0.0.nxm“ wurde nicht angenommen.</Ui> mit dem Grund des Servers und einer Zeile je Problem. Diese Meldungen begegnen Ihnen beim Bauen am häufigsten:

| Meldung | Ursache und Lösung |
|---|---|
| `module.json menu.faellig: nur Listen und Seiten (slot page) gehören ins Menü` | Ein Menüeintrag zeigt auf eine Ansicht, die weder eine `list` noch eine `custom`-Ansicht mit Slot `page` ist. Verweisen Sie auf eine solche Ansicht. |
| `module.json views.geraet.list: Filter 'notiz' ist nicht indexed` | Sie filtern oder sortieren nach einem Feld ohne `"indexed": true`. Ergänzen Sie die Angabe. Felder vom Typ `text` und `json` lassen sich nicht indizieren. |
| `module.json views.geraet.list: sort 'bezeichnung' ist nicht indexed` | Dasselbe für die Sortierung der Liste. |
| `module.json entities.geraet: searchFields 'rueckgabe_am' muss Text sein` | Suchfelder müssen vom Typ `string`, `text` oder `enum` sein. |
| `CHANGELOG.md: keine Überschrift '## [1.0.0]' für die Version aus module.json` | Der Changelog hat keine Überschrift für die Version aus dem Manifest. Ergänzen Sie `## [1.0.0]`. Das Skript bricht schon vorher ab mit `CHANGELOG.md: Überschrift "## [1.0.0]" fehlt.` |
| `module.json: area 'lager' ist kein Menübereich (...)` | `area` muss einer der Fachbereiche sein, die die Meldung aufzählt. |
| `Das Archiv enthält kein module.json.` | `module.json` liegt weder ganz oben im Archiv noch in einem einzigen obersten Ordner. Bauen Sie mit dem Skript oder packen Sie den Versionsordner selbst. |
| `Das Paket ist nicht signiert. Unsignierte Pakete sind auf dieser Plattform nicht zugelassen.` | Die Instanz nimmt keine unsignierten Pakete an. Signieren Sie das Paket, siehe [Paketieren, installieren und aktualisieren](/de/modules/publish/). |
| `acme.leihgeraete 1.0.0 gibt es schon mit anderem Inhalt; eine neue Fassung braucht eine neue Versionsnummer.` | Sie haben Dateien einer Version geändert, die schon hochgeladen war. Ausgelieferte Versionen sind unveränderlich: Erhöhen Sie die Versionsnummer in `module.json` und im Changelog, mit einem neuen Versionsordner oder, in einem Paketordner mit Git-Tags, mit einem neuen Tag. Dieselbe Datei noch einmal hochzuladen ist kein Problem. |

Fehler im Betrieb:

- Mia Schulz hat nur die Rolle <Ui>Einsicht</Ui>, öffnet den Dialog <Ui>Ausleihen</Ui> und bekommt den Hinweis `Keine Berechtigung: Ihnen fehlt das Recht 'acme.leihgeraete.verleihen'.` Eigene Ansichten sieht jeder, der das Fachobjekt lesen darf, und sie kennen die Rechte der Person noch nicht. Der Server lehnt die Aktion ab. Geben Sie Mia Schulz die Rolle <Ui>Geräteverwaltung</Ui>, oder nehmen Sie hin, dass Lesende die Schaltfläche sehen.
- Der Menüabschnitt fehlt: Die Person hat keine Rolle des Moduls, oder das Modul ist deaktiviert. Administratoren sehen ihn immer, solange das Modul aktiv ist. Das Abschalten des Fachbereichs `projekte` verbirgt ihn nicht.

## Wie es weitergeht

<CardGrid>
  <LinkCard title="Paketaufbau" description="Jedes Feld des Manifests, alle Feldtypen, Ansichtstypen und Aktionen." href="/de/modules/structure/" />
  <LinkCard title="Ansichten und das View-SDK" description="Alle Funktionen des SDK, Gestaltung und die Regeln der Abschottung." href="/de/modules/views/" />
  <LinkCard title="Paketieren, installieren und aktualisieren" description="Signieren, der Katalog, Updates und Deinstallieren." href="/de/modules/publish/" />
</CardGrid>