# ADR 0010: plugin-owned frontend device workflows

- Status: accepted
- Date: 2026-07-17
- Extends: ADR 0003, ADR 0004 and ADR 0009

## Context

The device UI registry already mounted a model-specific `device.connection`
component, but the XGRIDS implementation still lived below
`apps/control-station/src/device-plugins`. Its 600-line connection view mixed
BLE discovery, Wi-Fi provisioning, acquisition/replay controls, diagnostics and
metrics. The boundary was logical, yet the plugin source and styles were still
physically owned by the generic application tree.

Different device families need different enrollment, connection and acquisition
workflows. Making those workflows generic would either produce a lowest-common-
denominator wizard or restore vendor branches in the Control Station.

## Decision

Device-specific operator workflows are frontend plugin contributions. The
XGRIDS source, API adapter, runtime provider, observation mapping and scoped CSS
are physically located under:

```text
plugins/xgrids-k1/frontend/src/
  plugin.ts
  projectName.ts
  automaticSourceStart.ts
  XgridsK1Connection.tsx
  components/
    K1ProvisioningPipeline.tsx
    K1AcquisitionPipeline.tsx
    K1SpatialControls.tsx
    K1Diagnostics.tsx
    K1Metrics.tsx
```

The generic host exposes one reviewed frontend surface through
`@mission-core/plugin-sdk`, implemented by
`apps/control-station/src/core/device-plugins/frontendSdk.ts`. A plugin may use
manifest/model/slot contracts, the generic Mission Runtime bridge, React and the
NODE.DC UI kit. It may not import Control Station implementation paths.

`apps/control-station/src/composition/devicePlugins.ts` is the only Core file
which imports the concrete XGRIDS frontend. The host owns model selection,
safe deactivation, the `device.connection` slot, navigation, the spatial scene,
saved-session UX and global layout. A contribution may additionally expose one
optional `SpatialControlsView`, which the generic scene mounts beside the
viewport without learning its fields or lifecycle wording. The plugin owns BLE
candidates, exact-profile attestation, credentials-in-memory form state, K1
endpoint state, connection instructions, project-name validation, live/replay
acquisition controls and its scene-level status/stop/telemetry block.

The host surface includes `activateAutomaticSpatialSource`. The name refers to
viewer-source ownership, not automatic equipment control. The plugin calls it
only after live or adapter replay start returns success and then opens the scene;
a failed start leaves the existing scene/source intact. The action clears the
earlier manual/archive selection so backend state can supply the successful new
source. A generic fail-closed acquisition guard blocks saved-session switching,
persisted replay reattach and manual source input/apply/reset for every
nonterminal state, cleanup-pending terminal state or unknown acquisition state.
The internal successful-start action is not an operator source switch and
performs no implicit stop.

K1 project names are required, NFKC-normalized and trimmed, rejected for control
or surrogate characters or more than 96 Unicode characters, and sent to the
backend as local display metadata. At the time of this ADR the active profile
had `vendor_writes_enabled=false`, so its
buttons and scene control say that they prepare/stop local reception. Before an
acquisition exists, device-start wording is gated by active-control capability;
after start, calibration/device-stop wording additionally requires a
plugin-commanded acquisition mode.

The application-control portion is superseded by ADR 0012 and plugin v0.5.0;
the plugin ownership and host-boundary decisions remain unchanged.

Plugin CSS is imported with the contribution and scoped below
`.xgrids-k1-plugin`. A plugin cannot add global navigation, route ownership or
unscoped global selectors.

Frontend loading remains static and reviewed at build time. This ADR does not
introduce arbitrary remote JavaScript, an npm marketplace, signatures or
process/browser-realm isolation.

## Acceptance gate

The frontend boundary test builds two unrelated synthetic device plugins with
different connection components and proves that the unchanged registry resolves
the correct component for each model. It also fails when:

- XGRIDS implementation files return to the generic Control Station source;
- generic Core files other than the composition root contain XGRIDS/Lixel/K1
  implementation knowledge;
- plugin source imports Control Station implementation paths;
- provisioning, acquisition, optional scene controls, diagnostics or
  scoped-style modules disappear;
- a nonterminal acquisition can be replaced by a manual or recorded spatial
  source, including persisted replay reattach.

The accepted local gate is the frontend unit suite, TypeScript strict checking
and a Vite production build. Backend behavior and the physical K1 protocol are
unchanged by this source/UI decomposition; protocol command authorization is a
separate backend/physical gate.

## Consequences

A second device family can provide a completely different connection pipeline
and optional scene controls while reusing the same model catalog, lifecycle
transition, observation scene and saved-session host. Adding it requires its own
plugin-owned frontend contribution and one reviewed composition import; the
generic host changes only when a genuinely cross-device slot/action is added,
not for vendor fields or protocol-specific workflow steps.

The remaining isolation gap is deployment-level: frontend contributions are
still compiled into one signed application build, and backend plugins still run
inside the control-plane process. ADR 0011 adds a fail-closed backend runtime
descriptor/handshake and replaceable control-plane transport seam, but not a
subprocess. Signed bundles, frontend loading/realm policy and process/restart
containment remain later deployment milestones.
