# Das Manifest

Das Manifest ist die einzige verbindliche Beschreibung einer App: `defineAppManifest` im SDK, die Prüfung beim Laden und die Ablage im Katalog lesen exakt dieselbe Form. Die Tabellen nennen jedes Feld mit Pflicht, Format und Wirkung; Beispiele stehen darunter.

> **Typ:** nachschlagen
> **Für wen:** Entwickler, die ein Manifest schreiben oder ein bestehendes lesen.
> **Voraussetzung:** das SDK `@szenera/apps-sdk` aus dem Quellbaum der Plattform (nicht öffentlich veröffentlicht).

## Die Felder

| Feld | Pflicht | Format | Wirkung |
|---|---|---|---|
| `id` | ja | Kleinbuchstaben mit Bindestrichen (`^[a-z][a-z0-9]*(-[a-z0-9]+)*$`); `core` ist reserviert und wird abgelehnt | Kanonische Kennung der App; muss dem Katalog-Eintrag entsprechen. |
| `version` | ja | Semver (`1.0.0`) | Die Fassung, die ein Release einfriert. |
| `displayName` | ja | 1–60 Zeichen | Name im Store und im Portal. |
| `description` | ja | max. 300 Zeichen | Kurzbeschreibung im Store. |
| `category` | ja | `zeit` · `information` · `web-video` · `gestaltung` · `geschaeft` | Die eine Einordnung: Filter im Store und Abschnitt im App-Bereich des Szenen-Editors. Widgets tragen keine eigene. |
| `tier` | Vorgabe `standard` | `standard` · `pro` · `premium` · `addon` | Klasse: `standard` trägt im Store keine Marke, `pro` die Marke **Pro**, `addon` die Marke **Add-on** und wird zugebucht; `premium` trägt heute keine App. |
| `vertical` | nein | `pharmacy` | Branchen-Facette des Store; übergreifende Apps lassen es weg. |
| `publisher` | nein | `{ name: 1–80 Zeichen }` | Herausgeber, den das Store-Listing zeigt. |
| `schemaName` | ja | `^apps_[a-z][a-z0-9_]+$` | Name der App-Datenbank: `apps_<id>`, Bindestriche der Kennung werden zu Unterstrichen (`qr-code` → `apps_qr_code`). Pflicht auch ohne Datenbank. |
| `apiCompatibility` | ja | `{ appsSdk: Semver-Bereich, apiVersion?: string, rendererHost?: Semver-Bereich }` | `appsSdk` gilt dem Server-Teil: Der Loader hält die laufende SDK-Version dagegen, passt sie nicht, lädt die App nicht. `rendererHost` nennt den Host-Vertrag der Anzeige, den runtime- und authoring-Bündel brauchen, und gehört in jedes Manifest mit Widget: Anzeige und Szenen-Editor laden ein Bündel nur, wenn ihre Vertragsfassung im Bereich liegt. Gesetzt wird es aus `RENDERER_HOST_RANGE` des SDK, nie von Hand, damit es mit dem Vertrag wandert. |
| `widgets` | ja | Liste von Widget-Definitionen | Die Widgets der App — nach der Regel der Plattform höchstens eines, bei Tor-Apps keines. Wie ihr `configSchema` im Katalog herauskommt, steht unter [Widgets konfigurieren](/de/apps-sdk/configure-widgets). |
| `resources` | nein | Liste von Ressourcentypen (`id`, `appId`, `slug`, `name`, `description`, drei JSON-Schemas, `listEndpoint`, `authoringPath`) | Datentypen, die die Organisation im Portal pflegt; `slug` ist das Adress-Segment des App-Bereichs. |
| `permissions` | ja | `{ tenant: [], platform: [] }`, je Eintrag `key`, `label`, `description`, `defaultRoles` | Rechte, die die Installation in den Rechte-Katalog der Organisation einträgt; `platform` mit leeren `defaultRoles` ist reine Betreiberarbeit. |
| `models` | nein | Liste von Modellnamen | Allowlist der Modelle, die der Server-Teil in der App-Datenbank ansprechen darf. |
| `critical` | Vorgabe `false` | boolean | Nur Berichtszweck in der Betriebsdiagnose; ob ein Ladefehler den Start abbricht, entscheidet die Server-Konfiguration, nicht das Manifest. |
| `criticalForTenant` | Vorgabe `false` | boolean | Das Portal warnt vor der Deinstallation: **Diese App gehört zur Grundausstattung**. |
| `signing` | Vorgabe `optional` | `optional` · `required` | Ohne Wirkung auf die Auslieferung: Das Gerät prüft die Signatur jeder Fassung immer. Der Wert bleibt, weil er in den Manifesten veröffentlichter Fassungen steht. |
| `preview` | ja | `{ iconUrl }` | Das Icon fürs Listing. Die Aufnahmen leitet der Store aus den Varianten der Widgets ab und liefert sie aus dem Artefakt der Fassung; das frühere Feld `screenshots` liest er nicht mehr, ein neues Manifest lässt es weg. |
| `store` | nein | `tagline` (≤ 140), `longDescription` (≤ 3000), `highlights` (≤ 8), `setupSteps` (≤ 8, optional mit `resourceType`) | Ausführliche Store-Inhalte; das Portal rendert sie nur. |
| `dataAccess` | nein | `{ reads[], writes[], external[], retention }` | Die Auskunft vor der Installation; fehlt der Block, gilt die zurückhaltendste Lesart. |
| `unlocks` | nein, max. 8 | Schlüssel kleingeschrieben mit Bindestrichen, ≤ 40 Zeichen | Portal-Funktionen, die die Installation freischaltet; jeder Schlüssel muss im Katalog der Plattform stehen. |
| `connects` | nein, max. 4 | Schlüssel kleingeschrieben mit Bindestrichen, ≤ 40 Zeichen | Speicher-Anbieter, deren Verbindungs-Verwaltung die App auf ihre Seite holt. |
| `legal` | nein | siehe unten | Bedingungen, die Zustimmung und Hinweis im Widget auslösen. |
| `compatibility` | ja (im SDK) | `{ hosts, orientations, offline }`, siehe unten | Wo die App läuft. Der Store zeigt es unter „Läuft auf“, die Zuweisung an ein Display warnt, wenn dessen Wirt fehlt. Im Format optional, damit Manifeste früherer Fassungen lesbar bleiben. |
| `translations` | nein (für die mitgelieferten Apps: `en-US` Pflicht) | je Sprache ein Block mit den Texten, siehe unten | Die Texte in weiteren Sprachen. Ohne Block zeigt der Store Deutsch. |

`unlocks` und `connects` werden **ausgelassen statt leer** gesetzt: Ein leeres Feld verschöbe den kanonischen Hash jeder bestehenden Fassung, ohne dass sich etwas geändert hat.

## Der Block compatibility

`defineAppManifest` verlangt den Block im Typ und zur Laufzeit: Ohne ihn wirft es mit einem Satz, der die drei Felder nennt. Jede Liste nennt einen Wert höchstens einmal.

| Feld | Werte | Bedeutung |
|---|---|---|
| `hosts` | `szenera-os` · `fire-tv` · `tizen` · `webos` · `web-display`, mindestens einer | Die Wirte, auf denen die App läuft: SzeneraOS auf dem eigenen Gerät, die App für Fire TV und Android, die Apps für Samsung Tizen und LG webOS, ein Browser ohne eigenen Wirt. |
| `orientations` | `landscape` · `portrait`, mindestens eine | Die Lagen des Bildschirms, quer und hochkant, in denen die App taugt. |
| `offline` | `full` · `last-known` · `none` | Was die App ohne Netz zeigt: `full` weiter alles, weil sie allein aus ihrer Konfiguration und dem Vorrat des Geräts rendert (im Store „statisch“), `last-known` den Stand, den der Server zuletzt in den Schnappschuss des Displays gelegt hat, `none` nichts, weil sie live aus dem Netz lädt. |

## Der Block translations

Die Texte des Manifests stehen auf Deutsch, der Pflichtsprache. Jede weitere Sprache steht unter
`translations` als eigener Block, der nur Texte trägt: `displayName` und `description`, `store`
(Einzeiler, Langtext, Funktionen und Einrichtungsschritte in der Reihenfolge der deutschen),
`widgets` je Kennung mit Name, Beschreibung und `variants` je Ansicht, `resources` je Kennung,
`permissions` je Schlüssel, `dataAccess.external` in der Reihenfolge der Dienste und
`legal.disclaimerSummary`. Kennungen, Werte und Regeln stehen nie in einem Block.

Ein Block ist vollständig oder gar nicht da: `defineAppManifest` prüft ihn mit
`appManifestTranslationIssues()` und wirft beim Bauen, mit der Stelle beim Namen. Der Store
antwortet in der Sprache, die das Portal erbittet, und fällt auf Deutsch zurück. Englische Begriffe
folgen den Portalen: `display`, nie `screen`, die Leinwand heißt `video wall`. `pnpm app:check
<id>` prüft die Texte beider Sprachen nach der Hausregel, in der Zeile **Texte**.

## Der legal-Block

| Feld | Pflicht | Bedeutung |
|---|---|---|
| `disclaimerKey` | ja | Schlüssel der Fassung, auf die sich eine Zustimmung bezieht (`<gruppe>.bedingungen.v1`). Eine inhaltliche Änderung des Hinweises hebt ihn (`v1` → `v2`). |
| `disclaimerSummary` | ja, max. 500 Zeichen | Kurzfassung in Klartext; Installationsdialog, Store-Seite und Widget-Fußzeile zeigen sie unverändert. Sie stellt die Fassung dar, eine neue Formulierung bei gleichem Schlüssel verlangt keine Zustimmung. |
| `termsPath` | nein | Pfad des Volltexts auf der öffentlichen Website (`^\/[a-z0-9-]+(\/[a-z0-9-]+)*$`, ≤ 200 Zeichen, z. B. `/app-bedingungen/notdienst`). Ein Pfad, keine Adresse — das Portal setzt seine Basis davor. |
| `requireAcceptanceAt` | Vorgabe `install` | `install`: Zustimmung bei der Installation und danach mit jeder neuen Fassung des Hinweises. `upgrade`: Zustimmung bei jeder neuen Fassung der App, auch bei gleichem Hinweis. `never`: keine Zustimmung. |

Ein neuer `disclaimerKey` macht ein Release zustimmungspflichtig: Sobald die neue Fassung als `stable` veröffentlicht ist, steht die Installation auf **Update wartet**. Wer den Hinweis inhaltlich ändert, hebt deshalb den Schlüssel, im selben Arbeitsgang wie Kurzfassung, Volltext und Fassung der App. Eine neu formulierte Kurzfassung bei gleichem Schlüssel übernimmt die Plattform still. Eine Manifest-Änderung ohne Versionssprung erreicht bestehende Installationen nicht.

## Was nicht im Manifest steht

- **Fetcher und Jobs.** Sie deklariert eine App als Decorator an Klassen ihres Server-Teils (`@Fetcher`, `@JobHandler`, `@WidgetResolver`); die Plattform entdeckt sie beim Start. Das Manifest bleibt reines Datenformat.
- **Ob ein Ladefehler den Start abbricht.** Das ist eine Eigenschaft der Installation und steht in der Server-Konfiguration des Betriebs; `critical` im Manifest ist nur Bericht.

## Beispiel: das Manifest der Notdienst-App (gekürzt)

```ts
import { defineAppManifest, RENDERER_HOST_RANGE } from '@szenera/apps-sdk/manifest';

export const manifest = defineAppManifest({
  id: 'pharmacies-on-duty',
  version: '1.3.0',
  displayName: 'Notdienst',
  description:
    'Zeigt die Apotheken im Notdienst rund um Ihren Standort, regelmäßig aktualisiert für ganz Deutschland.',
  tier: 'pro',
  category: 'geschaeft',
  vertical: 'pharmacy',
  schemaName: 'apps_pharmacies_on_duty',
  apiCompatibility: { appsSdk: '^0.2.0', rendererHost: RENDERER_HOST_RANGE },
  widgets: [PharmaciesOnDutyDisplayWidget],
  resources: [
    { ...PharmaciesOnDutyLocationResource, slug: 'locations' },
    { ...PharmaciesOnDutyScheduleResource, slug: 'schedules' },
  ],
  permissions: {
    tenant: [
      {
        key: 'pharmacies-on-duty.location.read',
        label: 'Standorte lesen',
        description: 'Hinterlegte Apotheken-Standorte ansehen, für die Notdienstpläne geladen werden.',
        defaultRoles: ['OWNER', 'MANAGER', 'MEMBER'],
      },
      // … weitere Rechte für das Team …
    ],
    platform: [
      {
        key: 'pharmacies-on-duty.crawler.config',
        label: 'Crawler konfigurieren',
        description: 'Betreiber-Recht: Quellen, Abrufintervalle und Parser des Notdienst-Crawlers einstellen.',
        defaultRoles: [],
      },
    ],
  },
  publisher: { name: 'Szenera' },
  signing: 'required',
  legal: {
    disclaimerKey: 'notdienst.bedingungen.v1',
    disclaimerSummary: 'Notdienstangaben stammen aus öffentlichen Verzeichnissen … und werden ohne Gewähr angezeigt …',
    termsPath: '/app-bedingungen/notdienst',
    requireAcceptanceAt: 'install',
  },
  preview: { iconUrl: '/apps/pharmacies-on-duty/icon.svg' },
  dataAccess: {
    external: [
      {
        name: 'Öffentliche Notdienst-Verzeichnisse (u. a. aponet.de)',
        purpose: 'Wir rufen die Dienstpläne für Ihre Standorte ab und halten sie aktuell.',
        sends: 'Postleitzahl und Umkreis der Standorte, die Sie angelegt haben.',
      },
    ],
  },
  // Die Dienstpläne holt der Server, das Display behält den letzten Stand im Schnappschuss.
  compatibility: {
    hosts: ['szenera-os', 'fire-tv', 'tizen', 'webos', 'web-display'],
    orientations: ['landscape', 'portrait'],
    offline: 'last-known',
  },
});
```

## Formprüfung und Querregeln

`defineAppManifest` prüft die Form gegen das Schema, wendet Vorgabewerte an (`tier`, `critical`, `criticalForTenant`, `signing`), verlangt den Block `compatibility` und wirft bei jedem Formfehler. Drei Regeln, die sich aus mehreren Feldern zugleich ergeben, prüft erst die Plattform beim Laden: Das `appId` jedes Widgets muss der `id` des Manifests gleichen, jede Ressource muss das Präfix der App tragen, und keine zwei Widgets dürfen dieselbe Kennung haben. Ein Manifest, das dem Schema widerspricht, weist die Release-Anlage mit `Manifest.ValidationFailed` (HTTP 422) ab.

## Weiterführendes

- [Widget, Konfiguration und Vorschau](/de/apps-sdk/widget-and-preview) — die Widget-Definition im Feld `widgets`.
- [Datenzugriff, Rechte und Bedingungen](/de/apps-sdk/data-access) — `dataAccess`, `permissions`, `unlocks`, `connects` und `legal` im Detail.
- [Signieren und der Weg in den Katalog](/de/apps-sdk/signing-and-catalog) — was aus `signing` beim Release und am Gerät folgt.
