### Description
Spec and implementation of scene triggers and scene actions dec…lared by external integrations, so Gladys can be extended without a core update. The spec is `docs/specs/external-integrations/capabilities/scene-triggers-and-actions.md` (a self-contained capability file of the living spec); the code follows it.
> Built on top of #3109 (dashboard widgets, `provider` type, now merged): both capabilities sit side by side in the manifest validator, the vendored schema, the supervisor, the install screen and the i18n files, and `scene_triggers` / `scene_actions` join `CAPABILITY_MANIFEST_FIELDS`, so a `provider` integration whose whole contract is its scene declarations (an events-only bridge) is accepted, in code and in the schema (`provider-type.md` table updated).
**Manifest** (`server/lib/external-integration/externalIntegration.validateManifest.js`, vendored `manifest.schema.json`): two optional top-level fields, `scene_triggers` and `scene_actions` (1–20 each), declarable by every integration type (`provider` included). Triggers carry `fields` (filters, the `config_schema` flat format with a restricted type list, no `boolean`) and `variables` (the whitelist of the event data exposed to the scene). Actions carry `fields` (`boolean` allowed), `timeout_seconds` (5–120, default 30) and `outputs` (scalars only). `secret`/`oauth2`/`account_link` and `{{port:<name>}}` are refused.
**Scene engine**: one generic trigger type `external-integration.scene-event` and one generic action type `external-integration.scene-action`, carrying `integration` (selector), `trigger_key` / `action_key` and a nested `fields` object validated by Joi for its shape only (the manifest is consulted at execution time, never at save time, so a scene loads and saves when its integration is gone). The matcher compares the stored filters with the event's `filters` (equality, `multi_select` membership, empty = wildcard, stale keys skipped) and returns the reduced trigger event `{ type, integration, trigger_key, data }`; `checkTrigger` now takes an object return as the `triggerEvent` (a boolean keeps the raw event, every existing checker is unchanged). The action handler resolves the proxy service through `service.getService(selector)`, relays the stored fields with a `render` callback bound to the scope, and writes the whitelisted outputs to the scope at the action path (replacing a previous run, never merging).
**Supervisor**: `POST /api/integration/v1/scene/event` `{ key, data }` builds `filters` (declared fields minus sections) and `data` (declared variables) with coercion by declared type, `404` on an undeclared key, `400` on nested data, 300 events/min on a counter separate from the states. `runSceneAction` reads the current `t_service` row on every run (like `runAction`, so an update is live without a restart), strips stale keys and empty optionals, applies defaults, renders the declared `string` fields only, validates with the config engine (`source: "devices"` included), reserves an in-flight slot (10 per integration) before the connection wait, and relays `external-integration.scene-action.run` under a single deadline covering the wait and the ack. The proxy service carries `scene.runAction` on every external integration and is re-registered by `update`. `GET /api/v1/external_integration/scene` (every authenticated user, literal route before `:selector`) feeds the editor. The MCP scene schemas accept the two types.
**Frontend**: a dynamic "Integrations" category in both type pickers (one entry per declaration, the integration name on its own line, options top-aligned so long labels line up across columns), trigger and action cards rendered with the shared `ConfigField` engine (`components/integration/ConfigSchemaForm`, string action fields use the variables-aware input), declared variables and outputs exposed to the variable picker and named to the user by label only (one muted line of badges per card, no `{{…}}` path ever shown), a required filter left empty blocks the save with a message naming the field, the declaration catalog kept in three states (unknown / empty / loaded: a pending or failed request shows a loading line and refuses the save instead of posing as "nothing installed"; a save retries the request once and validates against what it returns), `fields.*` string values rewritten on reorder, orphan (uninstalled / key removed) and stopped cards flagged honestly and never edited behind the user's back, one disclosure line on the install screen next to the widgets one. `getLocalizedText` moved to `front/src/utils/`. Strings in en/fr/de.
**Tests**: manifest (provider with scene declarations only included), host API (auth battery, whitelists, coercion, tenant isolation, rate limit), trigger matching (reduced scope, stale keys, wildcards, membership, strict equality), action handler, supervisor (order of the pipeline, slot reserved before the wait, single deadline, outputs, current declaration after an update), proxy capability, scene model, management API (route order, non-admin), MCP schemas, and a Cypress spec (`front/cypress/e2e/routes/scene/SceneExternalIntegration.cy.js`) run locally against a real server, together with the widget spec of #3109 on the merged front. The whole flow was also exercised end to end with a test integration process (manifest with two triggers and two actions, WebSocket + host API): event published → scene triggered → action relayed with rendered fields → outputs read by the following message action. Field-tested on the forum with two community integrations (ISS passes, fuel prices).
Not in this PR (other repositories): the indexer schema of `integration-store`, the SDK members `publishSceneEvent` / `onSceneAction`, the template demo declarations and the developer documentation page, as listed in §10 of the spec.
### Related request
Forum: https://community.gladysassistant.com/t/10870
### Checklist
- [x] If a forum topic or GitHub issue exists, the description links it (`Forum: https://community.gladysassistant.com/t/...` or `Closes #...`)
- [x] Tests pass: `cd server && npm run coverage` (Codecov requires 100% coverage on changed lines) and Cypress (`npm run cypress:run`) if the UI changed
- [x] Linter and prettier pass on both front and server (`npm run eslint`, `npm run prettier`)
- [x] No undocumented breaking change
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01SzdCt7PkgNaicRRsFLnzkJ