# NODEDC MISSION CORE · Control Station

Универсальный браузерный пункт управления Mission Core на React 19, TypeScript и
Vite. Приложение задаёт общую операторскую оболочку для аппаратов, сенсоров,
наблюдения, миссий и записей. XGRIDS/LixelKity K1 является первым реальным
device adapter, но структура интерфейса от него не зависит.

Внутри пространственной рабочей поверхности встроен открытый Rerun Web Viewer.
Это self-hosted frontend-компонент из npm-пакета `@rerun-io/web-viewer`, а не
переход во внешний облачный интерфейс. Для K1 live и adapter file-replay backend
сам создаёт Rerun gRPC/proxy source. Saved observation sessions открывают
проверенную digest-bound RRD generation через same-origin HTTP; ручной адрес
нужен только для другого совместимого Rerun source.

## Текущее состояние

| Контур | Состояние | Что это означает |
| --- | --- | --- |
| Mission Core fixed shell | Реализован | Header, навигация по разделам, рабочая поверхность, окна и инспекторы работают в одном приложении. |
| Device plugin registry | Реализован, v1alpha1 + v1alpha2 | До выбора модели provider остаётся inert и не делает I/O. v1alpha1 сохраняет одну модель; v1alpha2 допускает одну или несколько моделей и требует profile coverage каждой. Custom `device.connection` UI key и backend factory подключаются одним reviewed import в composition root. |
| Plugin-owned device UI | Реализован | XGRIDS provisioning, project/acquisition/replay, diagnostics, metrics, optional spatial controls и scoped styles физически находятся в `plugins/xgrids-k1/frontend`; generic Control Station предоставляет frontend SDK, model catalog и host slots. |
| Локальный control plane | Реализован | React получает состояние и выполняет операции через FastAPI REST и WebSocket на loopback. |
| K1 BLE → Wi-Fi | Реализован | Реальный BLE-поиск всех видимых устройств и одна подтверждённая provisioning-запись выбранному устройству. |
| K1 live/replay MQTT | Реализован | Read-only приём, обязательное локальное имя проекта, raw-first сохранение, декодирование облака/позы и device-reported scan time/distance/speed. |
| Автоматический MQTT → Rerun | Реализован | Первый live/adapter file-replay поднимает process-wide `RecordingStream` и gRPC/proxy на TCP 9876; следующие такие сессии переиспользуют его. Saved observation replay использует отдельный immutable HTTP RRD path. |
| Встроенный Rerun Viewer | Реализован | Self-hosted npm-компонент автоматически открывает текущий gRPC source внутри Control Station; внешний viewer не используется. |
| Контролы сцены → Rerun | Реализованы для текущей геометрии | Работают размер и видимость точек, атрибут цвета, палитра, окно накопления, траектория, сетка, host timeline и сохранение/восстановление spatial layout. Проекция и семантические слои ещё не подключены. |
| Сохранённые observation sessions | Реализованы для point/pose/device metrics | Три последние сессии, background preparation, capture-clock-bound cache v9, generation-bound RRD, atomic admission, autoplay, play/pause/seek и controlled switching больших записей. |
| K1 camera preview и archive | Live реализован; recorded contract реализован | Обе RTSP/H.264 камеры физически приняты в live UI. Новые acquisition-owned fMP4 archives не зависят от browser windows; recorded player подключён, но point-cloud + one-camera archived playback ещё не прошёл physical acceptance. |
| Legacy Foxglove module | Только regression | Модуль и тесты сохранены для сравнения декодирования. Текущий live/replay runtime не запускает Foxglove WebSocket и не использует TCP 8765. |
| Карты и миссии | Интерфейсный каркас | Реальные map/mission backends и vehicle control ещё не подключены. |

Приложение не генерирует демонстрационное облако, траекторию, кадры или
метрики. Если реальных данных нет, область сцены остаётся пустой, а числовые поля
показывают `—`.

## Архитектура данных

```text
K1 MQTT :1883, read-only
└── raw .k1mqtt + metadata + durable clock origin сохраняются первыми
    ├── injected ModelingReport observer -> device scan time/distance/speed
    └── bounded latest-wins visual preview queue (4 сообщения)
        └── явно внедрённый K1 protobuf/LZ4 point/pose normalizer
            └── transport-neutral DecodedPointCloudView / DecodedPoseView
                └── Rerun Points3D + Transform3D + LineStrips3D
                    └── gRPC/proxy TCP 9876
                        └── rerun_grpc_url в REST/WebSocket state
                            └── встроенный @rerun-io/web-viewer
                                └── пространственная сцена Mission Core

Mission Core Control Station ←→ REST /api/v1/device-plugins/*
                         + plugin-scoped WebSocket events
                         FastAPI на 127.0.0.1:8000
                         CoreBluetooth + live/replay runtime

sealed/recovered observation session
└── SQLite catalog + bounded background preparation
    └── capture-clock-bound atomic RRD cache v9 + recorded-media manifest v2
        └── generation-bound same-origin HTTP
            └── aggregate admission
                └── Rerun native receiver + recorded fMP4 player
```

В момент готовности live/file-replay `RerunBridge` backend публикует адрес вида
`rerun+http://127.0.0.1:9876/proxy`. Frontend автоматически назначает его сцене,
если оператор не указал ручной source. После остановки приёма URL и встроенный
viewer остаются активны, а следующая сессия сбрасывает session-local геометрию,
траекторию и метрики и использует тот же listener. Он закрывается вместе с
процессом `k1link serve`.

Поля `foxglove_ws_url` и `foxglove_viewer_url` пока остаются в API как
совместимость со старым контрактом, но текущий runtime держит их пустыми.

## Фиксированная оболочка

Shell собран из локальных NODE.DC UI packages и сохраняет одну структуру для
всех функциональных модулей:

1. `AppHeader` — марка NODE DC, выбор архитектурного раздела и состояние
   локального backend.
2. `AdminNavigationPanel` — контекст аппарата и список рабочих поверхностей
   выбранного раздела.
3. `LandingStage` — стартовая ситуационная поверхность и быстрые переходы.
4. `ApplicationPanel` — единый контейнер активной рабочей поверхности.
5. `Window` и `Inspector` — источник, отображение, слои и компоновка без
   раскрытия внутренних панелей визуального движка.

Встроенный Rerun Viewer работает как canvas внутри этой оболочки. Его верхняя,
blueprint-, selection- и time-панели скрыты, чтобы продуктовые действия жили в
Control Station. Размер точек, способ окрашивания и палитра, 12-секундное по
умолчанию накопление, видимость облака и траектории и сетка связаны с backend и
Rerun Blueprint. Host timeline управляет сохранённым `session_time`, а spatial
layout сохраняет display settings, tool windows и source-window geometry через
revisioned API. Смена 2D/3D/карты и семантические слои пока остаются
интерфейсным контрактом.

## Архитектурные разделы

Навигация описана данными в `src/productModel.ts`, а не зашита в разметку каждой
страницы.

| Раздел | Назначение |
| --- | --- |
| Центр | Оперативный обзор, состояние контура и активность оператора. |
| Парк | Аппараты, текущее устройство, сенсоры и конфигурации борта. |
| Наблюдение | Пространственная сцена, камеры, карта, объекты, телеметрия и время. |
| Миссии | Планировщик, маршруты, сценарии и исполнение. Командный backend отключён. |
| Данные | Сессии, потоки, сущности, playback и экспорт доказательств. |
| Система | Модули, интеграции, сеть, аудит и настройки платформы. |

Карточки возможностей имеют четыре честных уровня: работает сейчас, готово к
источнику, интерфейсный контракт и последующий этап. Каталог не следует читать
как утверждение, что для каждой карточки уже существует backend.

## Установка, проверка, сборка и запуск

Требуются Node.js 20.19+ либо 22.12+, `uv` и соседний checkout
`NODEDC_DESIGN_GUIDELINE`: зависимости `@nodedc/*` подключены к нему через
локальные `file:` пути. Python устанавливается только в `.venv` репозитория.

Канонический путь от корня репозитория:

```bash
cd /Users/dcconstructions/Downloads/mnt/NODEDC/NODEDC_MISSION_CORE
uv sync --frozen --group dev

cd apps/control-station
npm ci
npm run test:unit
npm run typecheck
npm run build

cd ../..
uv run k1link serve
```

Открыть `http://127.0.0.1:8000`. Команда `serve` отдаёт собранный `dist/` и
локальный API. Она намеренно привязана только к `127.0.0.1`; LAN bind не
предусмотрен, потому что endpoint подключения кратковременно принимает пароль
Wi-Fi.

Этот locked bootstrap повторяем в текущем workspace, но он ещё не является
standalone release install. Локальные `file:` зависимости берутся из соседнего
`NODEDC_DESIGN_GUIDELINE`, а lockfile не фиксирует Git revision или content hash
этого checkout. До CI/portable packaging эти пакеты нужно опубликовать,
завендорить либо проверять по неизменяемой donor revision.

Для разработки интерфейса после запуска backend:

```bash
cd apps/control-station
npm run dev
```

Для полного live-viewer контура Vite должен открыться на
`http://127.0.0.1:5173`: только 5173, production preview 4173 и backend 8000
входят в текущий Rerun CORS allowlist. Если Vite сообщает fallback на 5174 или
выше, освободите 5173 и перезапустите `npm run dev`; REST/WebSocket proxy на
fallback-порту может работать, но прямой browser → Rerun 9876 будет отклонён.
Vite проксирует весь `/api` (включая WebSocket) на `http://127.0.0.1:8000`.
Другой локальный backend можно указать переменной `VITE_API_TARGET`. Preview
production-сборки запускается командой `npm run preview` на
`http://127.0.0.1:4173`.

## Операторский путь для текущего K1 adapter

1. Запустить `uv run k1link serve` и открыть Mission Core Control Station.
2. Выбрать **Парк → Локальное устройство**.
3. В каталоге моделей выбрать **XGRIDS LixelKity K1**. Только после этого
   активируется runtime и монтируется custom UI XGRIDS-плагина.
4. Включить K1, дождаться стабильного индикатора, вручную сверить firmware
   `3.0.2` и direct-LAN топологию, затем подтвердить это в форме. Mission Core
   не читает firmware с устройства; такое подтверждение остаётся
   `operator-attested`, а не device-derived evidence.
5. Нажать **Показать все BLE-устройства**. Интерфейс показывает полный результат
   шестисекундного поиска; метка совместимости является подсказкой, выбор делает
   оператор.
6. Ввести SSID и пароль существующей сети и явно запустить подключение. Это одна
   reviewed provisioning-запись без автоматических повторов.
7. Ввести обязательное название проекта. Оно NFKC-нормализуется, trim-ится,
   проверяется на control/surrogate characters и лимит 96 Unicode characters,
   сохраняется как display metadata и не используется в filesystem path.
8. Запустить локальный live-приём по определённому адресу K1 либо replay
   локального `.k1mqtt`/проверенного TSV. Физическое сканирование K1 запускается
   и останавливается подтверждённым двойным нажатием кнопки устройства.
9. Backend автоматически поднимет Rerun gRPC на TCP 9876 и опубликует адрес в
   state. Ручной source вводить не требуется.
10. Открыть **Наблюдение → Пространственная сцена**. Plugin-owned spatial block
    показывает local acquisition phase, stop local reception и полученные от K1
    scan time/distance/speed; облако, траектория и preview metrics появляются
    только после реальных сообщений.
11. Физически остановить K1, дождаться steady green, затем остановить локальный
    приём из device workflow или spatial block, чтобы запечатать evidence.
12. После нормального stop или recovery открыть **Сохранённые сессии**. Дождаться
    состояния **Готово**, выбрать запись и использовать host timeline. Evidence
    сохраняется автоматически; disk action сохраняет только workspace layout.

В проверочном live-сеансе через этот путь прошло 80 реальных MQTT-сообщений:
38 кадров `lio_pcl`, 42 кадра `lio_pose`, 2 775 точек в последнем облаке и
0 ошибок декодирования.

## Источники Rerun

Live/replay runtime автоматически назначает первый URL из примера. В
**Наблюдение → Пространственная сцена → Источник** можно оставить ручное поле
пустым либо указать другой адрес, который понимает `@rerun-io/web-viewer`
зафиксированной в `package.json` версии:

```text
rerun+http://127.0.0.1:9876/proxy
http://127.0.0.1:8080/recording.rrd
https://example.internal/recording.rrd
```

- `rerun+http://…/proxy` — живой Rerun gRPC source через доступный браузеру
  proxy;
- `http(s)://…/recording.rrd` — RRD-запись по HTTP(S);
- версия RRD должна быть совместима с версией Web Viewer;
- ручной URL хранится только в состоянии текущей страницы и имеет приоритет над
  автоматическим source;
- nonterminal acquisition fail-closed блокирует manual source apply/reset,
  saved session switch и persisted replay reattach до завершения текущего
  приёма; automatic-source action очищает старый source и открывает сцену только
  после successful start, failed start сохраняет текущую сцену, а guard не
  останавливает устройство;
- ошибка запуска показывается в viewport, без подстановки фиктивных данных.

После готовности Viewer выбор Rerun entity возвращает `entityPath` и имя view в
оболочку. Backend принимает и применяет к Rerun размер точки `0.5–12.0`, режимы
цвета `intensity`, `height`, `distance`, `rgb`, `class`, палитры Turbo, Viridis,
Plasma, grayscale и custom, накопление `0–120` секунд, а также видимость облака,
траектории и сетки. Значение по умолчанию — 12 секунд истории реальных кадров.
Для текущего firmware-3 `lio_pcl` доказана только интенсивность из младшего
байта `rgbi`; RGB используется лишь когда он действительно присутствует в
декодированном формате. Custom/class сейчас означает выбранный сплошной цвет,
а не готовую семантическую классификацию.

Для saved session host timeline вызывает Rerun play/pause/seek только после
полного admission. Accumulation и display settings применяются через отдельный
маленький blueprint RRD и не заменяют основную запись. Versioned
`observation.spatial` layout сохраняется на host с optimistic revision; он не
содержит sensor evidence. Переключатель 2D/3D/карты и семантические слои пока не
вызывают готовый presentation backend.

## Контракт локального API

| Метод | Route | Тело / назначение |
| --- | --- | --- |
| `GET` | `/api/health` | Проверка локального сервиса. |
| `GET` | `/api/v1/device-plugins` | Валидированные manifests установленных device plugins. |
| `GET` | `/api/v1/device-models` | Backend-каталог моделей из валидированных manifests; UI-каталог текущей сборки формируется отдельным static composition root. |
| `POST` | `/api/v1/device-plugins/{pluginId}/actions/{actionId}` | Namespaced действие через host allowlist; тело `{ "input": { ... } }`. |
| `WS` | `/api/v1/device-plugins/{pluginId}/events` | Plugin-scoped snapshots с `pluginId` и монотонным `sequence`. |
| `GET` | `/api/v1/observation-sessions` | Последние cataloged sessions и authoritative preparation state. |
| `POST` | `/api/v1/observation-sessions/{id}/replay` | Verified replay launch либо HTTP 202 preparation handle. |
| `GET` | `/api/v1/observation-sessions/{id}/recording-preparation` | Exact-job polling с `If-Match`. |
| `GET` | `/api/v1/observation-sessions/{id}/recording.rrd` | Immutable generation-bound RRD; arbitrary chunked `send_rrd` не используется. |
| `GET` | `/api/v1/observation-sessions/{id}/media/{artifact}/...` | Manifest/init/segments записанных камер по opaque identifiers. |
| `GET`, `PUT` | `/api/v1/workspace-layouts/observation.spatial` | Versioned host layout с optimistic revision. |

Frontend XGRIDS-плагина использует только v1alpha namespaced routes. Старые
`/api/state`, `/api/events`, `/api/ble/scan`, `/api/connect`, `/api/session/*` и
`/api/viewer/settings` сохранены как deprecated compatibility shims внутри
XGRIDS backend contribution.

В v1alpha1/v1alpha2 backend composition загружается из manifests, а frontend plugins
статически включаются в сборку через `src/composition/devicePlugins.ts`.
Автоматической runtime-сверки двух installed sets пока нет: их соответствие —
проверяемое требование сборки до появления подписанных plugin bundles и startup
compatibility handshake. Поля manifest `permissions`, `mutating` и
`secretFields` пока являются декларативными метаданными; generic host валидирует
их форму и action allowlist, но ещё не реализует на их основе RBAC, подтверждения
оператора или secret-vault substitution.
Точный K1 compatibility profile остаётся неактивным, пока оператор явно не
подтвердит firmware `3.0.2` и direct-LAN топологию. Runtime не получает версию
firmware с устройства и явно помечает основание как `operator-attested`.
Ошибки валидации namespaced action endpoint и deprecated `/api/connect`
возвращаются как общий `422` без исходных значений запроса, поэтому отклонённое
значение credential-поля не отражается клиенту.

Legacy `/api/state` и изменяющие состояние ответы могут вернуть snapshot напрямую или
как `{ "state": { ... } }`. v1alpha2 snapshot отдельно содержит `device_ref`,
`device_session`, `acquisition`, `operations`, compatibility/calibration/sensor
facts и старые presentation-поля `phase`, `message`, `devices`,
`selected_device_id`, `k1_ip`, `source_mode`, `metrics`, `rerun_grpc_url` и
`viewer_settings`. Legacy-поля `foxglove_ws_url`/`foxglove_viewer_url` остаются
пустыми. OpenAPI доступен по `/api/docs`.

## Карта исходников frontend

| Файл | Ответственность |
| --- | --- |
| `src/App.tsx` | Fixed shell, выбор разделов и окна source/display/layers/layout. |
| `src/productModel.ts` | Архитектурные разделы, рабочие поверхности и уровни готовности. |
| `src/core/device-plugins/` | Vendor-neutral manifest parser, registry, lifecycle, plugin host и public frontend SDK surface. |
| `src/core/runtime/` | Нормализованное состояние активного устройства, spatial source и fail-closed source-switch guard. |
| `src/composition/devicePlugins.ts` | Единственный allowlist импортов конкретных device plugins. |
| `src/workspaces/DeviceWorkspace.tsx` | Generic выбор модели и `device.connection` slot. |
| `../../plugins/xgrids-k1/frontend/` | Plugin-owned K1 BLE/Wi-Fi, project/acquisition/replay UI, optional spatial controls, API client, runtime mapper и scoped styles. |
| `src/workspaces/Workspaces.tsx` | Оперативный обзор, spatial viewport и остальные продуктовые поверхности. |
| `src/components/RerunViewport.tsx` | Live/recorded lifecycle WebViewer, native RRD open, atomic admission, playback и selection events. |
| `src/components/ObservationSessionSelect.tsx` | Три последние сессии, состояния `Готово` / `Обработка` / `Ошибка`. |
| `src/components/RecordedFmp4Player.tsx` | Проверенный generation-bound MSE playback архивных камер. |
| `src/core/observation/sessionArchive.ts` | Строгий wire contract catalog/preparation/replay/media API. |
| `src/core/observation/recordedSessionAdmission.ts` | Общий RRD/camera gate до публикации recorded workspace. |
| `src/core/observation/workspaceLayout.ts` | Versioned spatial layout и optimistic revision. |
| `src/sceneSettings.ts` | Типизированный UI-профиль пространственной сцены. |
| `src/presentation.ts` | Vendor-neutral подписи lifecycle и форматирование нормализованных метрик. |
| `src/styles.css`, `src/styles/*` | Компоновка shell и рабочих поверхностей. |

## Safety и чувствительные данные

- Mission Core Control Station и credential endpoint доступны только на loopback.
- Исключение: Rerun gRPC/proxy на TCP 9876 сейчас слушает все сетевые интерфейсы,
  хотя автоматически возвращаемый URL содержит `127.0.0.1`. На нём нет
  connector-level authentication или TLS. Этот порт допустим только в доверенной
  лабораторной LAN; его нельзя пробрасывать в публичный Интернет или cellular
  WAN без аутентифицированного TLS reverse proxy. Listener намеренно остаётся
  активным между сессиями; для его закрытия нужно остановить `k1link serve`.
- Пароль Wi-Fi находится только в React memory, передаётся в JSON POST body,
  очищается после успешного ответа и не сохраняется в URL/local storage.
- Bridge и Direct Connect выполняют только отдельно рассмотренную provisioning-
  запись; Quick Connect не пишет в GATT и передаёт пароль короткоживущему
  CoreWLAN helper только через stdin. Случайные GATT writes и автоматические
  повторы запрещены.
- MQTT data live/replay остаётся subscribe-only. Команды изолированы в
  plugin-owned canonical control session и доступны только через отдельные
  operator-present UI checkpoints.
- Exact start/stop codec, response correlator и device-status state machine не
  владеют transport. Plugin v0.5.0 связывает их отдельным single-owner MQTT
  контуром без wall-clock переходов, reconnect или automatic retry; после STOP
  обязательны live READY и подтверждённый постоянный зелёный индикатор.
- Live-сессии сначала сохраняют сырые сообщения и camera segments, затем
  формируют disposable preview. При перегрузке preview может быть отброшен,
  native evidence сохраняется. MQTT durability имеет bounded group-commit RPO,
  camera durability — текущий незавершённый fragment RPO; это не zero-loss claim.
- Новая session summary hash-bind-ит durable clock origin и атомарно переключает
  pointer с provisional `mqtt.timeline.json` на content-addressed
  `mqtt.timeline.session-<sha256>.json`: только owner-sealed `session` scope
  может рекламировать combined media replay. Cache v9 создаёт реальные
  origin/end RRD rows; provisional `transport` scope fails closed.
- Timeline `capture_time` сохраняет Unix-время приёма сообщения Mac, а окно
  накопления viewer использует session-local `stream_time`. `capture_time` — не
  доказанный timestamp сенсора K1 и не photon-to-screen latency.
- Панорамный камерный поток в MQTT report topics отсутствует; отдельные
  left/right RTSP preview доступны через generic camera windows. LiDAR и camera
  сейчас синхронизированы только по host arrival, не по доказанным sensor clocks.
  Физический archived playback point cloud + одна выбранная камера ещё не принят.
- `.runtime/`, canonical evidence roots и legacy `sessions/` игнорируются Git и
  могут содержать адреса, изображения, идентификаторы, траекторию и карту
  помещения. В репозиторий попадают только код, тесты, redacted manifests и
  безопасная документация.
