# Befehle für den Plattformbetrieb

Die Befehle des Werkzeugs `szenera-ops`, die nur mit einem Profil vom Publikum `operator` laufen — einem Betreiber-Konto oder einem Betreiber-Schlüssel `opk_…`. Sie betreffen Kundinnen und Kunden der Plattform nicht und gehören nicht zur Kommandozeile `szenera`. Sie stehen hier, weil `whoami`, `device`, `audit`, `player`, `playback`, `permission`, `api-key` und `invoice` je nach Profil beide Seiten bedienen.

> **Typ:** nachschlagen
> **Für wen:** Wer die Plattform betreibt — Geräte-Software freigibt, Störungen führt, Warteschlangen und Organisationen verwaltet
> **Voraussetzung:** der Quellbaum der Plattform, ein Profil mit `audience: "operator"` und die Rechte `platform.*` aus der Spalte „Recht". Die Spalte „Schlüssel" sagt, ob ein Betreiber-Schlüssel `opk_…` das Recht tragen darf.

Das Werkzeug liegt im Quellbaum unter `tools/ops-cli` und wird nicht ausgeliefert. Es läuft mit `pnpm -s szenera-ops <befehl>` und führt alle 230 Befehle der Plattform. Profile, Umgebungsvariablen, Ausgabe und Exit-Codes sind dieselben wie bei `szenera`. Dazu kennt es das Publikum `operator` (Profilfeld `audience`, Umgebungsvariable `SZENERA_AUDIENCE`), seine eigene Profil-Datei `.secrets/szenera-profile.json` im Quellbaum und den Rückfall auf die Umgebungen in `.secrets/umgebungen.json`. Ein Befehl, der nur einer Seite dient, weist das Profil der anderen ab, bevor ein Aufruf hinausgeht.

Die Tabellen haben dieselbe Form wie unter [Befehle für Ihre Organisation](/de/cli/commands-organization): Befehl, Zweck, Route und Recht, ob ein Schlüssel das Recht tragen darf, ob der Befehl etwas verändert. Betreiber-Schlüssel laufen 1 bis 365 Tage (Vorgabe 90). Welche Rechte ein solcher Schlüssel überhaupt führen darf, zeigt `szenera-ops permission catalog --key-eligible` unter einem Betreiber-Profil — es sind die lesenden Betriebsrouten, die Geräte-Software und das Lesen der Vermarktung. Alles andere bleibt Menschen mit Zwei-Faktor-Anmeldung vorbehalten. Von den 230 Befehlen des Werkzeugs stehen 136 unter [Befehle für Ihre Organisation](/de/cli/commands-organization), es sind die Befehle von `szenera`. Die übrigen 94 laufen nur unter einem Betreiber-Profil und stehen hier. 21 der 136 dienen beiden Seiten: 14 davon nehmen unter einem Betreiber-Profil einen eigenen Weg (`whoami`, `device list`, `device get`, `audit list`, `audit get`, `player get-program`, `playback timeline`, `permission catalog`, die vier Befehle `api-key …`, `invoice list` und `invoice get`), sieben laufen unverändert (`health`, `health public` und die fünf Befehle des Notausgangs `api …`).

## Gesundheit

| Befehl | Zweck | Route und Recht | Schlüssel | Verändert |
|---|---|---|---|---|
| `health deep` | Ein Urteil aus fünf Quellen, der Befehl nach dem Aufspielen. Eine rote Quelle macht den Befehl rot, eine stumme steht als `unknown` | `GET /health/ready`, `GET /v1/platform/ops/platform-health`, `GET …/ops/jobs/health`, `GET …/ops/cache/health`, `GET …/ops/events/dispatcher/health`, `GET …/ops/audit/health` · `platform.ops.health.read`, `platform.jobs.health.read`, `platform.cache.stats.read`, `platform.events.dispatcher.read`, `platform.audit.health.read` | teils — Cache und Prüfprotokoll nur mit Konto | nein |

Mit `--json` trägt die Antwort das Urteil unter `verdict` (`green` oder `yellow`) und je Quelle unter `sources` die Felder `name`, `status` und `hint`. Ein rotes Urteil endet stattdessen mit Exit 11. Die Gegenprobe von außen ist `health public`: dieselbe Auskunft, die die Statusseite zeigt, ohne Ausweis und ohne Betreiber-Profil — sie beantwortet „Was sehen unsere Kunden gerade?“ und nicht „Wie steht es wirklich?“. Der Befehl steht unter [Befehle für Ihre Organisation](/de/cli/commands-organization).

## Geräte der Flotte

| Befehl | Zweck | Route und Recht | Schlüssel | Verändert |
|---|---|---|---|---|
| `device list` | Die Geräte aller Organisationen, auch die ohne Organisation. Dazu `--tenant=<tnt_…>`, `--all-tenants`, `--silent-only` (seit über einer Viertelstunde stumm) | `GET /v1/platform/ops/devices` · `platform.ops.device.read` | ja | nein |
| `device get <dev_…>` | Ein Gerät in der Betriebssicht: Organisation und Seriennummer | `GET /v1/platform/ops/devices/{deviceId}` · `platform.ops.device.read` | ja | nein |
| `device lookup --serial=<text>` | Ein Gerät über die Hardware-Seriennummer finden | `GET /v1/platform/ops/devices/lookup` · `platform.ops.device.read` | ja | nein |
| `device get-command <dev_…> <cmd_…>` | Was aus einem Befehl an ein Gerät wurde. `--wait` wartet bis zum Ende (Exit 11 bei `failed`/`timeout`, Exit 12 beim Zeitlimit) | `GET /v1/platform/ops/devices/{id}/commands/{commandId}` · `platform.ops.device.command-read` | ja | nein |
| `device release <dev_…> --reason=reconnect\|factory\|removed --yes` | Ein Gerät freigeben, der Grund ist Pflicht. `reconnect` und `removed` lassen WLAN und Fernzugang stehen, `factory` nimmt beides mit. Alle Displays des Geräts gehen mit | `POST /v1/platform/ops/devices/{deviceId}/release` · `platform.ops.device.reset` | nein | ja |
| `device block <dev_…> --reason=<text> --yes` | Ein Gerät sperren: Es verliert seine Organisation, alle Displays gehen mit, jedes Zertifikat wird gesperrt, und jede Anmeldung weist die API mit `409 Device.Blocked` ab. Der Grund (1 bis 500 Zeichen) ist Pflicht | `POST /v1/platform/ops/devices/{deviceId}/block` · `platform.ops.device.reset` | nein | ja |
| `device unblock <dev_…>` | Die Sperre aufheben. Das Gerät steht danach ungekoppelt und lässt sich neu koppeln | `POST /v1/platform/ops/devices/{deviceId}/unblock` · `platform.ops.device.reset` | nein | ja |
| `device check-ota <dev_…>` | Ein Gerät sofort nach einem neuen OS-Stand sehen lassen. Antwort 202, `--wait` braucht zusätzlich `platform.ops.device.command-read` | `POST /v1/platform/ops/devices/{id}/ota-check` · `platform.ops.device.ota-check` | ja | ja |
| `device channel --channel=beta\|stable\|canary --device=<dev_…> --yes` | Den Channel mehrerer Geräte in einem Aufruf wechseln, `--device` darf sich bis zu 100-mal wiederholen. Jedes Gerät bekommt sein Ergebnis, ein Gerät, das scheitert, hält die übrigen nicht auf | `POST /v1/platform/ops/devices/channel` · `platform.ops.device.channel` | nein | ja |
| `player get-program <ply_…>` | Was einem Display zugewiesen ist — über alle Organisationen, protokolliert | `GET /v1/platform/ops/devices/{playerId}/program` · `platform.ops.device.playback-read` | ja | nein |
| `playback timeline <ply_…>` | Die aufgelöste Abspiel-Timeline eines Displays — über alle Organisationen, protokolliert | `GET /v1/platform/ops/devices/{playerId}/timeline` · `platform.ops.device.playback-read` | ja | nein |

Jeder Aufruf von `device get-command` steht im Prüfprotokoll der betroffenen Organisation. Ein Betreiber-Profil kann mit `device send-command` und `device screenshot` nichts anfangen: Diese Routen verlangen `fleet.player.command.issue` und weisen eine Betreiber-Identität ab. Das Werkzeug sagt das vor dem Aufruf.

## Störungen und Wartungsfenster

| Befehl | Zweck | Route und Recht | Schlüssel | Verändert |
|---|---|---|---|---|
| `incident list` | Die Störungsmeldungen, mit `--id=<inc_…>` eine einzelne samt Chronik | `GET /v1/platform/ops/incidents` · `platform.ops.incident.list`, mit `--id` `GET …/incidents/{id}` · `platform.ops.incident.read` | nein | nein |
| `incident create --title=… --severity=sev-1…sev-4 --scope=platform\|tenant\|device-fleet` | Eine Störung eröffnen. `--public` stellt sie zugleich auf die Statusseite, ohne die Flagge bleibt sie intern | `POST /v1/platform/ops/incidents` · `platform.ops.incident.open` | nein | ja |
| `incident update <inc_…> --message=…` | Einen Eintrag in die Chronik schreiben. `--status` wandert zugleich weiter, `--public` stellt die Störung auf die Statusseite, `--internal` nimmt sie herunter | `PATCH /v1/platform/ops/incidents/{id}` · `platform.ops.incident.update` | nein | ja |
| `incident resolve <inc_…>` | Eine Störung abschließen, dazu `--postmortem=<url>` | `POST /v1/platform/ops/incidents/{id}/resolve` · `platform.ops.incident.resolve` | nein | ja |
| `maintenance-window list` | Die geplanten Wartungsfenster, mit `--id=<mtw_…>` ein einzelnes | `GET /v1/platform/ops/maintenance-windows` · `platform.ops.maintenance.list`, mit `--id` `platform.ops.maintenance.read` | nein | nein |
| `maintenance-window create --title=… --start=… --end=… --scope=…` | Ein Wartungsfenster planen. `--banner`, `--notify-days=<tage>`, `--block-mutations` hält schreibende Aufrufe der Organisationen an | `POST /v1/platform/ops/maintenance-windows` · `platform.ops.maintenance.create` | nein | ja |
| `maintenance-window update <mtw_…>` | Ein Fenster verschieben oder umschreiben | `PATCH /v1/platform/ops/maintenance-windows/{id}` · `platform.ops.maintenance.update` | nein | ja |
| `maintenance-window cancel <mtw_…> --reason=…` | Ein Fenster absagen. Der Grund wandert in die Benachrichtigung | `DELETE /v1/platform/ops/maintenance-windows/{id}` · `platform.ops.maintenance.cancel` | nein | ja |

## Warteschlangen, Cache, Ereignisse und Verbindungen

| Befehl | Zweck | Route und Recht | Schlüssel | Verändert |
|---|---|---|---|---|
| `job queues` | Die Warteschlangen samt Kennzahlen und Zustand — aus drei Quellen, eine Tabelle | `GET …/ops/jobs/queues`, `GET …/ops/jobs/metrics`, `GET …/ops/jobs/health` · `platform.jobs.queues.read`, `platform.jobs.metrics.read`, `platform.jobs.health.read` | ja | nein |
| `job pause <name>` | Eine Warteschlange pausieren. Was läuft, läuft zu Ende | `POST /v1/platform/ops/jobs/queues/{queueName}/pause` · `platform.jobs.queues.pause` | nein | ja |
| `job resume <name>` | Eine angehaltene Warteschlange fortsetzen | `POST /v1/platform/ops/jobs/queues/{queueName}/resume` · `platform.jobs.queues.resume` | nein | ja |
| `job failed` | Die endgültig gescheiterten Jobs, mit `--not-replayed`, `--queue=<name>`, `--id=<kennung>` | `GET /v1/platform/ops/jobs/dead-letter`, `GET …/dead-letter/{id}` · `platform.jobs.dead-letter.read` | ja | nein |
| `job replay --id=<kennung>\|--all --reason=…` | Einen gescheiterten Job — oder viele — erneut einstellen. `--max` (Vorgabe 100) deckelt | `POST …/jobs/dead-letter/{id}/replay`, `POST …/jobs/dead-letter/bulk-replay` · `platform.jobs.replay` | nein | ja |
| `job kill <jobId> --queue=<name> --reason=… --yes` | Einen laufenden Job abbrechen, Antwort 202 | `POST /v1/platform/ops/jobs/{queueName}/{jobId}/force-kill` · `platform.jobs.force-kill` | nein | ja |
| `job cleanup --yes` | Alte Dead-Letter-Einträge räumen | `POST /v1/platform/ops/jobs/cleanup/run` · `platform.jobs.cleanup` | nein | ja |
| `cache stats` | Zustand, Trefferquote und die heißen Themen, dazu `--window=1m\|5m\|1h` | `GET …/ops/cache/health`, `GET …/ops/cache/stats`, `GET …/ops/cache/topics` · `platform.cache.stats.read` | nein | nein |
| `cache invalidate --topic=<thema>\|--pattern=<glob> --reason=… --yes` | Ein Thema oder ein Muster verwerfen | `POST …/ops/cache/invalidate`, `POST …/ops/cache/invalidate-pattern` · `platform.cache.invalidate` | nein | ja |
| `event pending` | Was noch nicht ausgeliefert ist. `--metrics` hängt Zahlen und Gesundheit des Verteilers an | `GET /v1/platform/ops/events/outbox/pending` · `platform.events.outbox.read` | ja | nein |
| `event failed` | Was endgültig nicht ausgeliefert wurde | `GET /v1/platform/ops/events/outbox/dead-letter` · `platform.events.outbox.read` | ja | nein |
| `event get <evt_…>` | Ein Ereignis samt Nutzlast — genau das, was verschickt wurde | `GET /v1/platform/ops/events/outbox/{eventId}` · `platform.events.outbox.read` | ja | nein |
| `event replay --id=<evt_…>\|--all --reason=…` | Ein Ereignis — oder viele — erneut zustellen, dazu `--event-type`, `--since`, `--max` | `POST …/events/outbox/{eventId}/replay`, `POST …/events/outbox/replay-bulk` · `platform.events.outbox.replay` | nein | ja |
| `connection list` | Die offenen Echtzeit-Verbindungen: Display, Portal-Sitzung oder Renderer | `GET /v1/platform/realtime/connections` · `platform.realtime.read` | nein | nein |
| `connection disconnect <id> --yes` | Eine Verbindung trennen. Der Client verbindet sich danach neu | `POST /v1/platform/realtime/connections/{id}/disconnect` · `platform.realtime.disconnect` | nein | ja |

Der Grund bei `job replay`, `job kill`, `cache invalidate` und `event replay` ist Pflicht und mindestens zehn Zeichen lang.

## Fehler- und Rechte-Katalog

| Befehl | Zweck | Route und Recht | Schlüssel | Verändert |
|---|---|---|---|---|
| `error catalog` | Alle Fehlercodes der API samt HTTP-Status, dazu `--search=<text>` | `GET /v1/platform/ops/errors/catalog` · `platform.errors.catalog.read` | nein | nein |
| `permission catalog` | Der Rechte-Katalog. `--key-eligible` zeigt, was ein Betreiber-Schlüssel führen darf | `GET /v1/permission-catalog`, nur Anmeldung | nicht belegt | nein |
| `permission drift` | Was der Bestand gegenüber dem Katalog vermissen lässt. Ändert nichts | `GET /v1/platform/ops/permissions/catalog-drift` · `platform.permissions.catalog.read` | nein | nein |
| `permission reload` | Den Katalog neu einlesen — nötig, wenn App-Rechte dazukommen | `POST /v1/platform/ops/permissions/catalog/reload` · `platform.permissions.catalog.reload` | nein | ja |
| `permission backfill` | Die Rollen der Organisationen an den Katalog nachziehen, dazu `--tenant=<tnt_…>`. `--dry-run` rechnet nur, `--reconcile` nimmt auch weg. Betreiber-Konten brauchen das nicht: Ihre Rechte folgen der Rolle | `POST …/ops/permissions/roles/backfill` · `platform.permissions.role.backfill` | nein | ja |
| `permission flush-cache --yes` | Den Cache der wirksamen Rechte verwerfen | `POST /v1/platform/ops/permissions/cache/flush` · `platform.permissions.cache.flush` | nein | ja |

## Organisationen

| Befehl | Zweck | Route und Recht | Schlüssel | Verändert |
|---|---|---|---|---|
| `tenant list` | Die Organisationen der Plattform (`active`, `frozen`, `suspended`, `deleted`) | `GET /v1/platform/tenants` · `platform.tenant.list` | nein | nein |
| `tenant get <tnt_…>` | Eine Organisation in der Betriebssicht | `GET /v1/platform/tenants/{id}` · `platform.tenant.read` | nein | nein |
| `tenant provision --slug=… --name=… --owner-email=… --address=…` | Organisation, Abonnement, Inhaber und Einladung in einem Zug, dazu `--licenses`, `--set=<k=v>` | `POST /v1/platform/tenants/provision` · `platform.tenant.provision`, mit `--set` `PATCH …/tenants/{tenantId}/feature-entitlements` · `platform.billing.override` | nein | ja |
| `tenant overview <tnt_…>` | Die Betriebsakte: Flotte, Inhalte, Ansprechpartner | `GET /v1/platform/tenants/{tenantId}/overview` · `platform.tenant.read` | nein | nein |
| `tenant suspend <tnt_…> --reason=… --yes` | Eine Organisation sperren — kaskadiert auf ihre Konten | `POST /v1/platform/tenants/{id}/suspend` · `platform.tenant.suspend` | nein | ja |
| `tenant resume <tnt_…>` | Eine Sperre aufheben | `POST /v1/platform/tenants/{id}/resume` · `platform.tenant.resume` | nein | ja |
| `tenant delete <tnt_…> --slug=… --yes` | Eine Organisation löschen, nicht umkehrbar. Der Kurzname ist die Bestätigung. Ihre Geräte werden freigegeben und zeigen danach einen Kopplungscode | `DELETE /v1/platform/tenants/{id}` · `platform.tenant.delete` | nein | ja |
| `tenant entitlements <tnt_…>` | Vertragliche Fähigkeiten lesen und mit `--set=<k=v>` setzen (Schalter `on` oder `off`) | `GET …/tenants/{tenantId}/feature-entitlements` · `platform.billing.invoice.read`, `PATCH …` · `platform.billing.override` | nein | ja |
| `tenant licensing <tnt_…>` | Vertragslage: Lizenzen, Artikel, Sonderkonditionen | `GET /v1/platform/tenants/{tenantId}/licensing` · `platform.billing.invoice.read` | nein | nein |
| `tenant price-override <tnt_…> --sku=<code>` | Eine Sonderkondition setzen (`--price` oder `--discount`, nie beides) oder mit `--clear` zurücknehmen | `PUT …/tenants/{tenantId}/licensing/items/{skuCode}/override` · `platform.billing.override` | nein | ja |
| `tenant sso-recovery <tnt_…> --email=… --reason=… --yes` | SSO-Notfallzugang für ein Konto ausstellen | `POST /v1/platform/tenants/{tenantId}/sso-recovery` · `platform.tenant.sso-recovery` | nein | ja |
| `self-registration set` | Selbst-Registrierung lesen, mit `--on`/`--off --reason=…` schalten | `GET`/`PUT /v1/platform/self-registration` · `platform.self-registration.manage` | nein | ja |

Die Einladungs-Adresse aus `tenant provision` steht im Klartext in der Ausgabe (`--json`: `inviteUrl`) — die einzige Stelle, an der dieses Token die API verlässt.

## Betreiber-Konten und Betreiber-Schlüssel

| Befehl | Zweck | Route und Recht | Schlüssel | Verändert |
|---|---|---|---|---|
| `operator list` | Die Betreiber-Konten, mit `--role=owner\|manager\|support\|viewer`, `--status`, `--search` und `--with-permission=<platform.…>` (Konten, deren Rolle das Recht trägt) | `GET /v1/platform/operators` · `platform.operator.list` | nein | nein |
| `operator get <ops_…>` | Ein Betreiber-Konto samt Rolle und der Stufe der Rolle je Bündel | `GET /v1/platform/operators/{id}` · `platform.operator.list` | nein | nein |
| `operator create --email=… --name=… --role=manager\|support\|viewer` | Ein Betreiber-Konto anlegen. Die Rolle ist Pflicht, `owner` gibt es hier nicht. Die Zwei-Faktor-Einrichtung erscheint genau einmal | `POST /v1/platform/operators` · `platform.operator.create` | nein | ja |
| `operator set-role <ops_…> --role=manager\|support\|viewer --yes` | Die Rolle ersetzen. Nur der Owner, nie die eigene, nie die des Owners. Die neuen Rechte gelten mit der nächsten Anfrage des Kontos | `PATCH /v1/platform/operators/{id}/role` · `platform.operator.create` | nein | ja |
| `operator transfer-owner <ops_…> --totp-code=<123456>` | Den Owner übertragen, Schritt 1: Der Owner bietet dem Konto an. Das Angebot gilt sieben Tage, ein neues ersetzt das frühere. `--accept --totp-code=…` (Schritt 2, unter dem Profil des Ziels) nimmt an, danach ist das Ziel Owner und der bisherige Owner Manager. `--status` zeigt das offene Angebot | `POST /v1/platform/operators/{id}/owner-transfer`, `POST …/operators/owner-transfer/accept`, `GET …/operators/owner-transfer` · `platform.operator.create`, bei Annahme und Stand `platform.operator.list` | nein | ja |
| `operator suspend <ops_…> --reason=…` | Ein Betreiber-Konto sperren | `POST /v1/platform/operators/{id}/suspend` · `platform.operator.suspend` | nein | ja |
| `operator reactivate <ops_…>` | Die Sperre aufheben | `POST /v1/platform/operators/{id}/reactivate` · `platform.operator.suspend` | nein | ja |
| `operator reset-mfa <ops_…> --reason=… --yes` | Zwei-Faktor neu einrichten. Der bisherige Authenticator gilt nicht mehr | `POST /v1/platform/operators/{id}/mfa-reset` · `platform.operator.mfa-reset` | nein | ja |
| `operator resend-onboarding <ops_…>` | Die Einrichtungs-Mail erneut verschicken. Sie gilt 72 Stunden | `POST /v1/platform/operators/{id}/onboarding-resend` · `platform.operator.mfa-reset` | nein | ja |
| `api-key list` | Die Betreiber-Schlüssel des Hauses | `GET /v1/platform/api-keys` · `platform.operator.api-key.list` | nein | nein |
| `api-key issue --name=… --permissions=<a,b,c>` | Einen Betreiber-Schlüssel `opk_…` ausstellen. `--days=<1–365>` (Vorgabe 90), `--list-allowed` zeigt die freigegebenen Rechte | `POST /v1/platform/api-keys` · `platform.operator.api-key.create` | nein | ja |
| `api-key rotate <oak_…>` | Einen Betreiber-Schlüssel erneuern, dazu `--grace-hours=<0–72>`, `--days` | `POST /v1/platform/api-keys/{id}/rotate` · `platform.operator.api-key.create` | nein | ja |
| `api-key revoke <oak_…> --yes` | Einen Betreiber-Schlüssel zurückziehen — sofort und endgültig | `DELETE /v1/platform/api-keys/{id}` · `platform.operator.api-key.revoke` | nein | ja |

## Gerätesoftware

Der Baustein ist immer das erste Wort hinter dem Befehl: `os`, `agent`, `renderer`, `host-webos` oder `host-tizen`. Eine Version nennt der Befehl mit der Bau-Kennung (zwölf Hex-Stellen), `list` zeigt sie.

| Befehl | Zweck | Route und Recht | Schlüssel | Verändert |
|---|---|---|---|---|
| `device-software list <baustein>` | Die Versionen in der Ablage, neueste zuerst: Anzeigename mit Welt (`0.6.1-prod`), Bau, Stand, die Channels, in denen sie gilt, die Zahl der Geräte und die Größe | `GET /v1/platform/ops/device-software/{component}/builds` · `platform.ops.device-software.read` | ja | nein |
| `device-software releases <baustein>` | Was in den Channels gilt, und der Verlauf der Freigaben, mit `--limit` und `--cursor` | `GET …/device-software/{component}/releases` · `platform.ops.device-software.read` | ja | nein |
| `device-software devices <baustein>` | Die Geräte, die den Baustein fahren, mit Channel, Fassung, Bau und Stand zur Freigabe (`current`, `updating`, `rolled-back`, `outdated`, `no-release`). Dazu `--channel`, `--build`, `--state` | `GET …/device-software/{component}/devices` · `platform.ops.device-software.read` | ja | nein |
| `device-software activity` | Der Verlauf über alle Bausteine: Freigaben, Importe, Löschungen, Anfragen und die Rücknahmen, die Geräte meldeten. Dazu `--component=<baustein>` | `GET …/device-software/activity` · `platform.ops.device-software.read` | ja | nein |
| `device-software release <baustein> <bau> --channel=… --yes` | Eine Version in einem Channel gelten lassen. `--pull-along=beta,canary` zieht tiefere Channels im selben Schritt nach, `--note`, `--security`. Verlangt die Plattform eine zweite Person (`releaseApproval.deviceSoftware`), entsteht nach stable eine Anfrage, `--reason=…` ist dann Pflicht | `POST …/device-software/{component}/releases` · `platform.ops.device-software.release` | nein | ja |
| `device-software promote <baustein> --from=… --to=… --yes` | Den Bau, der heute im tieferen Channel gilt, im höheren freigeben, sonst wie `release` | `GET` und `POST …/device-software/{component}/releases` · `platform.ops.device-software.release` | nein | ja |
| `device-software delete <baustein> <bau> --yes` | Die Bytes einer Version löschen, der Datensatz bleibt als Grabstein. Geschützt ist eine Version, die in einem Channel gilt oder auf die eine Anfrage zeigt (`DeviceSoftware.BuildInUse`, Exit 6) | `DELETE …/device-software/{component}/builds/{build}` · `platform.ops.device-software.delete` | nein | ja |
| `device-software cleanup <baustein> --dry-run\|--yes` | Die Versionen jenseits der neuesten `--keep-newest=<n>` (ohne Angabe `retention.deviceSoftware.keepNewest`) löschen, die sich löschen lassen. `--dry-run` nennt nur, was gelöscht würde | `POST …/device-software/{component}/builds/cleanup` · `platform.ops.device-software.delete` | nein | ja |
| `device-software nudge <baustein> <dsr_…>` | Die Geräte des Channels sofort an die geltende Freigabe erinnern | `POST …/device-software/{component}/releases/{id}/nudge` · `platform.ops.device-software.release` | nein | ja |
| `device-software image <bau>` | Die kurzlebige Bezugsadresse des SD-Karten-Abbilds zu einer Version von SzeneraOS | `GET …/device-software/os/builds/{build}/image` · `platform.ops.device-software.read` | ja | nein |

Einen Bau holt das Werkzeug nicht: Die Umgebung gleicht sich alle zehn Minuten mit der Registry ab, die CI beschleunigt es mit `POST …/builds/import-from-registry` (`platform.ops.device-software.import`, mit Betreiber-Schlüssel). `release` und `promote` verlangen `--yes`, weil eine Freigabe die Geräte des Channels erreicht.

## Anfragen an eine zweite Person

Für Gerätesoftware und Apps gleich. Ob ein Release nach stable eine zweite Person braucht, stellt der Owner mit `settings set releaseApproval.deviceSoftware|apps stable`.

| Befehl | Zweck | Route und Recht | Schlüssel | Verändert |
|---|---|---|---|---|
| `approval list` | Die Anfragen, ohne `--state` die offenen. Dazu `--state=open\|approved\|withdrawn\|expired`, `--kind=device-software\|app`, `--package=<baustein oder app>` | `GET /v1/platform/ops/approvals` · `platform.ops.device-software.read` oder `platform.app-catalog.list` | ja | nein |
| `approval approve <rap_…>` | Eine Anfrage als zweite Person bestätigen und damit ausführen. Die anfragende Person selbst bekommt einen Satz und Exit 4 (`ReleaseApproval.SamePerson`) | `POST /v1/platform/ops/approvals/{id}/approve` · `platform.ops.release.approve` | nein | ja |
| `approval withdraw <rap_…>` | Eine Anfrage zurückziehen, als andere Person ablehnen. `--reason=…` ist freiwillig | `POST /v1/platform/ops/approvals/{id}/withdraw` · `platform.ops.release.approve` oder `platform.ops.device-software.release` oder `platform.app-catalog.promote` | nein | ja |

## Plattform-Einstellungen

| Befehl | Zweck | Route und Recht | Schlüssel | Verändert |
|---|---|---|---|---|
| `settings get` | Alle Plattform-Einstellungen vollständig, eine nicht gesetzte mit ihrer Vorgabe | `GET /v1/platform/settings` · `platform.settings.read` | nein | nein |
| `settings set <einstellung> <wert> --yes` | Eine Einstellung ändern, zum Beispiel `releaseApproval.apps stable` oder `retention.deviceSoftware.keepNewest 3`. Der Wert wird gegen den Vertrag geprüft, bevor etwas hinausgeht | `PATCH /v1/platform/settings` · `platform.settings.manage` (nur der Owner) | nein | ja |

Die Einstellungen sind `releaseApproval.deviceSoftware` und `releaseApproval.apps` (`off` oder `stable`, Vorgabe `off`) sowie `retention.deviceSoftware.keepNewest` (1 bis 100, Vorgabe 5) und `retention.deviceSoftware.auto` (Vorgabe `false`). Dazu kommen `retention.apps.keepNewest` (1 bis 100, Vorgabe 5), `retention.apps.unreferencedMonths` (1 bis 120, Vorgabe 12) und `retention.apps.auto` (Vorgabe `false`). Jede Änderung steht mit altem und neuem Wert im Prüfprotokoll (`platform.settings.changed`).

## App-Katalog

| Befehl | Zweck | Route und Recht | Schlüssel | Verändert |
|---|---|---|---|---|
| `app-release list` | Die Releases des App-Katalogs, mit `--app=<app-id>`, `--channel=canary\|beta\|stable\|deprecated\|retired` | `GET /v1/platform/app-catalog/apps`, `GET …/apps/{appId}/releases` · `platform.app-catalog.list` | nein | nein |
| `app-release set-notes <arl_…> --app=<app-id> --notes=…` | Die Release-Notizen ändern (max. 2000 Zeichen) | `PATCH …/apps/{appId}/releases/{id}` · `platform.app-catalog.update` | nein | ja |
| `app-release promote <arl_…> --app=<app-id> --channel=beta\|stable` | Nach beta oder stable heben, immer ein Schritt. Nach stable hebt der Befehl sofort, wenn die Plattform keine zweite Person verlangt (`releaseApproval.apps = off`), sonst legt er eine Anfrage an, und `--reason=…` (10 bis 500 Zeichen) ist dann Pflicht | `POST …/apps/{appId}/releases/{id}/promote` · `platform.app-catalog.promote` | nein | ja |
| `app-release restore <arl_…> --app=<app-id> --reason=…` | Die Rücknahme einer veralteten oder abgelösten Fassung nach stable. Je nach Einstellung sofort oder als Anfrage, die eine zweite Person mit `approval approve` bestätigt. Die bis dahin geltende Fassung geht dabei nach deprecated | `POST …/apps/{appId}/releases/{id}/restore` · `platform.app-catalog.promote` | nein | ja |
| `app-release demote <arl_…> --app=<app-id> --channel=deprecated` | Ein Release aus stable nach deprecated zurückstufen, der einzige Schritt rückwärts | `POST …/apps/{appId}/releases/{id}/demote` · `platform.app-catalog.promote` | nein | ja |
| `app-release retire <arl_…> --app=<app-id> --reason=… --grace=regular\|security\|immediate` | Ein Release außer Betrieb nehmen. `--grace` wählt die Auslauffrist und hat keine Vorgabe: `regular` drei Monate, `security` einen Monat, dann ist der Sicherheitsvermerk `--advisory=…` für die Organisationen Pflicht, `immediate` keine Frist, die Plattform stellt sofort auf die nächsthöhere Fassung in stable um. Zu `regular` und `immediate` gibt es keinen Vermerk | `POST …/apps/{appId}/releases/{id}/retire` · `platform.app-catalog.retire` | nein | ja |
| `app-release grace-period <arl_…> --app=<app-id>` | Die Auslauffrist verlängern, um `--months=<n>` ab dem bisherigen Ende oder bis zum Ende eines Tages mit `--until=<jjjj-mm-tt>`, oder mit `--immediately --reason=…` sofort beenden. Genau eine Wahl | `POST …/releases/{id}/extend-grace`, `POST …/releases/{id}/force-retire` · `platform.app-catalog.retire` | nein | ja |
| `app-release delete-artifact <arl_…> --app=<app-id> --yes` | Die Bytes einer Fassung löschen, Zeile, Manifest und Historie bleiben. Geschützt ist eine Fassung in einem Kanal, mit Installation, Szene oder offener Anfrage (`AppRelease.ArtifactInUse`, Exit 6) | `DELETE …/apps/{appId}/releases/{id}/artifact` · `platform.app-catalog.delete` | nein | ja |
| `app-release cleanup --app=<app-id> --dry-run\|--yes` | Die Bytes aller Fassungen jenseits der neuesten `--keep-newest=<n>` (ohne Angabe `retention.apps.keepNewest`) löschen, die sich löschen lassen. `--dry-run` nennt nur, was gelöscht würde | `POST …/apps/{appId}/releases/cleanup` · `platform.app-catalog.delete` | nein | ja |
| `app-status list` | Die Apps mit ihrem Betreiber-Status, Installationen und Ausnahmen | `GET /v1/platform/app-catalog/managed-apps` · `platform.app-catalog.list` | nein | nein |
| `app-status set <app> --status=active\|beta\|disabled --reason=…` | Eine App plattformweit setzen | `PATCH /v1/platform/app-catalog/managed-apps/{appId}` · `platform.app-catalog.status.manage` | nein | ja |
| `app-status override <app>` | Ausnahmen je Organisation lesen, mit `--tenant` und `--status` setzen, mit `--clear` entfernen | `GET …/managed-apps/{appId}/overrides` · `platform.app-catalog.list`, `PUT …/overrides/{tenantId}`, `POST …/overrides/{tenantId}/remove` · `platform.app-catalog.status.manage` | nein | ja |

Ob nach stable eine Person allein hebt, entscheidet die Einstellung `releaseApproval.apps`. Verlangt die Plattform eine zweite Person und nennt `POST …/promote` mit `stable` keine Begründung, antwortet es `AppRelease.SecondPersonRequired` (Exit 6). Mit Begründung entsteht eine Anfrage, die eine zweite Person mit `approval approve` bestätigt, sie verfällt nach sieben Tagen. Trägt die Prüfliste der Fassung eine Sperre, weisen `promote` (nach beta wie nach stable) und die Bestätigung einer Anfrage nach stable mit `AppRelease.ReviewBlocked` (Exit 6) ab, und der Befehl nennt die gesperrten Punkte: „Neu in dieser Fassung“ fehlt in der Pflichtsprache, oder ein Kundentext trägt ein Semikolon oder ein gesperrtes Wort. „Neu in dieser Fassung“ entsteht auf der Prüfung der Fassung im Betreiber-Portal, `set-notes` setzt nur den Text für den Betrieb.

## Abrechnung und Artikel

| Befehl | Zweck | Route und Recht | Schlüssel | Verändert |
|---|---|---|---|---|
| `subscription set-payment-way <tnt_…> --way=invoice\|stripe\|none` | Den Zahlweg eines Vertrags stellen | `PUT /v1/platform/tenants/{tenantId}/billing/payment-way` · `platform.billing.override` | nein | ja |
| `subscription approve-invoice <tnt_…> --yes` | Den Kauf auf Rechnung freigeben. `--revoke` räumt die Freigabe ab | `PUT /v1/platform/tenants/{tenantId}/billing/payment-way` · `platform.billing.override` | nein | ja |
| `subscription check-stripe <tnt_…>` | Die Bindung eines Vertrags an den Zahlungsdienstleister prüfen. Ändert nichts | `GET /v1/platform/tenants/{tenantId}/billing/stripe-link` · `platform.billing.override` | nein | nein |
| `subscription relink-stripe <tnt_…> --yes` | Einen Vertrag neu an den Zahlungsdienstleister binden. Lebt der Kunde dort noch, geschieht nichts (`unchanged`) | `POST /v1/platform/tenants/{tenantId}/billing/relink-stripe` · `platform.billing.override` | nein | ja |
| `subscription backfill-period-prices --yes` | Die Perioden-Stückpreise des Bestands nachziehen — der Griff, wenn der Lauf beim Start der Anwendung gescheitert ist. Idempotent | `POST /v1/platform/billing/period-unit-prices/backfill` · `platform.billing.plan.manage` | nein | ja |
| `invoice list --tenant=<tnt_…>\|--all-tenants` | Die Rechnungen aller Organisationen | `GET /v1/platform/invoices` · `platform.billing.invoice.read` | nein | nein |
| `invoice get <inv_…>` | Eine Rechnung samt Positionen | `GET /v1/platform/invoices/{id}` · `platform.billing.invoice.read` | nein | nein |
| `invoice mark-paid <inv_…> --yes` | Einen Zahlungseingang buchen | `POST /v1/platform/invoices/{id}/mark-paid` · `platform.billing.invoice.mark-paid` | nein | ja |
| `invoice refund <inv_…> --reason=… --yes` | Eine Rechnung erstatten | `POST /v1/platform/invoices/{id}/refund` · `platform.billing.invoice.refund` | nein | ja |
| `invoice issue-documents <inv_…> --yes` | PDF und E-Rechnung eines Belegs nachholen. Ein Beleg, der seine Dokumente schon trägt, bleibt unangetastet | `POST /v1/platform/invoices/{id}/issue-documents` · `platform.billing.invoice.refund` | nein | ja |
| `catalog list` | Der Artikelkatalog samt gesetzter Sonderkonditionen | `GET /v1/platform/billing/catalog` · `platform.billing.plan.manage`, `GET /v1/platform/billing/overrides` · `platform.billing.override` | nein | nein |
| `catalog set [sku]` | Einen Katalog-Artikel ersetzen (alle fünf Felder) oder mit `--app=<app-id> --price=<cent>` den Monatspreis einer App setzen | `PUT /v1/platform/billing/catalog/{skuCode}`, `PUT /v1/platform/billing/app-prices/{appId}` · `platform.billing.plan.manage` | nein | ja |

Unter einem Betreiber-Profil laufen außerdem `audit list` und `audit get` gegen `GET /v1/platform/audit` und `GET /v1/platform/audit/{id}` (`platform.audit.read`). `--tenant` oder `--all-tenants` ist dort Pflicht.

## Weiterführendes

- [Befehle für Ihre Organisation](/de/cli/commands-organization)
- [Profile und Ausweis](/de/cli/profiles-and-credentials)
