From 6402601d6d07713dc0c2995fb7fc3beb48910382 Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Thu, 16 Jul 2026 01:20:09 +0300 Subject: [PATCH] feat: build NODE.DC control station shell --- README.md | 64 +- apps/k1-viewer/README.md | 243 +++- apps/k1-viewer/index.html | 4 +- apps/k1-viewer/package-lock.json | 20 +- apps/k1-viewer/package.json | 4 +- apps/k1-viewer/public/nodedc-logo.svg | 8 + apps/k1-viewer/public/nodedc-mark.svg | 4 + apps/k1-viewer/src/App.tsx | 1237 +++++++---------- apps/k1-viewer/src/api.ts | 8 +- .../k1-viewer/src/components/LandingStage.tsx | 74 + apps/k1-viewer/src/components/MetricCard.tsx | 22 + .../src/components/RerunViewport.tsx | 126 ++ apps/k1-viewer/src/main.tsx | 2 +- apps/k1-viewer/src/messages.ts | 20 + apps/k1-viewer/src/presentation.ts | 90 ++ apps/k1-viewer/src/productModel.ts | 758 ++++++++++ apps/k1-viewer/src/sceneSettings.ts | 31 + apps/k1-viewer/src/styles.css | 976 +------------ apps/k1-viewer/src/styles/base.css | 293 ++++ apps/k1-viewer/src/styles/device.css | 420 ++++++ apps/k1-viewer/src/styles/responsive.css | 278 ++++ apps/k1-viewer/src/styles/shell.css | 253 ++++ apps/k1-viewer/src/styles/spatial.css | 347 +++++ apps/k1-viewer/src/styles/workspaces.css | 628 +++++++++ apps/k1-viewer/src/useK1Console.ts | 8 +- .../src/workspaces/DeviceWorkspace.tsx | 541 +++++++ apps/k1-viewer/src/workspaces/Workspaces.tsx | 587 ++++++++ apps/k1-viewer/vite.config.ts | 6 +- src/k1link/viewer/runtime.py | 18 +- src/k1link/web/app.py | 18 +- 30 files changed, 5296 insertions(+), 1792 deletions(-) create mode 100644 apps/k1-viewer/public/nodedc-logo.svg create mode 100644 apps/k1-viewer/public/nodedc-mark.svg create mode 100644 apps/k1-viewer/src/components/LandingStage.tsx create mode 100644 apps/k1-viewer/src/components/MetricCard.tsx create mode 100644 apps/k1-viewer/src/components/RerunViewport.tsx create mode 100644 apps/k1-viewer/src/messages.ts create mode 100644 apps/k1-viewer/src/presentation.ts create mode 100644 apps/k1-viewer/src/productModel.ts create mode 100644 apps/k1-viewer/src/sceneSettings.ts create mode 100644 apps/k1-viewer/src/styles/base.css create mode 100644 apps/k1-viewer/src/styles/device.css create mode 100644 apps/k1-viewer/src/styles/responsive.css create mode 100644 apps/k1-viewer/src/styles/shell.css create mode 100644 apps/k1-viewer/src/styles/spatial.css create mode 100644 apps/k1-viewer/src/styles/workspaces.css create mode 100644 apps/k1-viewer/src/workspaces/DeviceWorkspace.tsx create mode 100644 apps/k1-viewer/src/workspaces/Workspaces.tsx diff --git a/README.md b/README.md index 20a1273..08e0a45 100644 --- a/README.md +++ b/README.md @@ -55,45 +55,57 @@ uv run k1link doctor uv run pytest ``` -## Live console and Foxglove +## NODE.DC Control Station and visualization adapters -Build the browser control surface once, then launch the whole local stand from -the repository root: +The browser application is now a universal NODE.DC Control Station rather than +a K1-specific Foxglove launcher. Its fixed shell contains six architectural +sections — Center, Fleet, Observation, Missions, Data and System — while the K1 +BLE/Wi-Fi/live workflow remains isolated as the first real device adapter. + +Install, type-check, build and serve the complete local application from the +repository root: ```bash +uv sync --group dev cd apps/k1-viewer npm install +npm run typecheck npm run build cd ../.. uv run k1link serve ``` -Open `http://127.0.0.1:8000`. The API, credential form and Foxglove WebSocket -bind to loopback only. The console supports: +Open `http://127.0.0.1:8000`. The static application, REST/WebSocket control +plane and credential endpoint bind to loopback only. The current K1 adapter +still provides real CoreBluetooth discovery, one operator-triggered reviewed +BLE Wi-Fi provisioning write, read-only MQTT live capture, native `.k1mqtt` and +reviewed-TSV replay, raw-first evidence storage and measured preview metrics. +Physical K1 scanning is still started and stopped by the verified double-click; +the connector publishes no modeling command. -- real CoreBluetooth K1 discovery; -- one operator-triggered reviewed BLE Wi-Fi provisioning write; -- live read-only MQTT capture with raw-first evidence storage; -- replay of native `.k1mqtt` evidence and the reviewed four-column TSV capture; -- `/k1/points`, `/k1/pose`, `/k1/trajectory` and `/k1/metrics` for Foxglove; -- measured Mac-side MQTT-receive-to-Foxglove-publish latency and bounded - latest-wins preview dropping. +The Observation spatial workspace embeds the open-source Rerun Web Viewer +inside the NODE.DC shell. It can open a compatible RRD file over HTTP(S) or a +Rerun gRPC/proxy source such as `rerun+http://127.0.0.1:9876/proxy`. It does not +use an external hosted viewer UI. -For live mode, start the session in the console and use the verified physical -double-click on K1 to start or stop scanning. The connector deliberately does -not publish modeling commands yet. For the recorded proof, replay this ignored -local file at `1x` with loop enabled: +There is one important current boundary: the automatic K1 MQTT → Rerun +RRD/gRPC contract is **not implemented yet**. Starting a K1 live or replay +session updates real state and metrics, but it does not by itself populate the +embedded Rerun viewport. The scene controls for point size, gradients, +accumulation, layers, timeline and layout are presently a product UI contract; +their Rerun Blueprint/playback adapter is also still pending. No synthetic +point cloud, trajectory, camera frame or latency value is generated to conceal +these missing links. -```text -sessions/20260715T122850Z_live_power_cycle/captures/mqtt_scan_full_payloads_03.tsv -``` - -Foxglove remains the full 3D workspace rather than being reimplemented in the -React console. In its 3D panel, select `/k1/points`, color by `intensity` (or -`z`/``) and choose Turbo, Rainbow or a custom gradient. Add -`/k1/trajectory` for the cyan path and `/k1/pose` for the current pose. See the -[live viewer runbook](docs/06_K1_LIVE_VIEWER.md) for timing semantics and current -limitations. +The existing Python MQTT → Foxglove pipeline remains in place as the verified +legacy visualization adapter. It still publishes `/k1/points`, `/k1/pose`, +`/k1/trajectory` and `/k1/metrics`, and preserves the measured +MQTT-receive-to-Foxglove-publish latency plus bounded latest-wins preview +dropping. It is retained for evidence, regression and diagnostics, not as the +target Control Station interface. The historical +[live viewer runbook](docs/06_K1_LIVE_VIEWER.md) records that proven path and its +timing semantics; the current frontend contract is documented in +[`apps/k1-viewer/README.md`](apps/k1-viewer/README.md). `doctor` is intentionally non-invasive. It checks the local Python environment and reports external tools; it does not request Bluetooth permission, scan the diff --git a/apps/k1-viewer/README.md b/apps/k1-viewer/README.md index c7780a6..35e931b 100644 --- a/apps/k1-viewer/README.md +++ b/apps/k1-viewer/README.md @@ -1,51 +1,224 @@ -# K1 Live Console +# Пункт управления NODE.DC -React 19 + TypeScript + Vite frontend for the local K1 bridge. The console handles -connection setup, source selection, transport status, latency metrics and handoff to -the Foxglove 3D viewer. It does not emulate scanner operations or generate placeholder -telemetry. +Универсальный браузерный пункт управления NODE.DC на React 19, TypeScript и +Vite. Приложение задаёт общую операторскую оболочку для аппаратов, сенсоров, +наблюдения, миссий и записей. XGRIDS/LixelKity K1 является первым реальным +device adapter, но структура интерфейса от него не зависит. -## Run locally +Внутри пространственной рабочей поверхности встроен открытый Rerun Web Viewer. +Это self-hosted frontend-компонент из npm-пакета `@rerun-io/web-viewer`, а не +переход во внешний облачный интерфейс. Сам браузерный компонент, однако, должен +получить совместимый источник RRD или Rerun gRPC. + +## Текущее состояние + +| Контур | Состояние | Что это означает | +| --- | --- | --- | +| NODE.DC fixed shell | Реализован | Header, навигация по разделам, рабочая поверхность, окна и инспекторы работают в одном приложении. | +| Локальный control plane | Реализован | React получает состояние и выполняет операции через FastAPI REST и WebSocket на loopback. | +| K1 BLE → Wi-Fi | Реализован | Реальный BLE-поиск всех видимых устройств и одна подтверждённая provisioning-запись выбранному устройству. | +| K1 live/replay MQTT | Реализован | Read-only приём, raw-first сохранение, декодирование облака точек и позы, реальные метрики. | +| Legacy Foxglove adapter | Реализован | Текущий Python runtime публикует декодированные K1 данные в локальный Foxglove WebSocket. Это проверенный диагностический backend, а не продуктовый UI. | +| Встроенный Rerun Viewer | Реализован | Открывает назначенный RRD по HTTP(S) или совместимый Rerun gRPC/proxy source внутри Control Station. | +| Автоматический MQTT → Rerun | **Не реализован** | Запуск K1 live/replay пока не создаёт RRD и не подаёт данные во встроенный Rerun viewport. Нужен отдельный stream adapter. | +| Контролы сцены → Rerun Blueprint | **Не реализованы** | Размер точек, палитра, накопление, слои, проекция, timeline и компоновка пока являются честным UI-контрактом и состоянием текущего сеанса. | +| Камеры, карты и миссии | Интерфейс готов | Серверная логика и реальные каналы для этих рабочих поверхностей ещё не подключены. | + +Приложение не генерирует демонстрационное облако, траекторию, кадры или +метрики. Если реальных данных нет, область сцены остаётся пустой, а числовые поля +показывают `—`. + +## Архитектура данных + +```text +NODE.DC Control Station в браузере +├── control plane +│ └── REST /api/* + WebSocket /api/events +│ └── FastAPI на 127.0.0.1:8000 +│ ├── CoreBluetooth: discovery и reviewed Wi-Fi provisioning +│ └── live/replay session runtime +│ +├── текущий K1 preview path — legacy adapter +│ └── K1 MQTT :1883, read-only +│ ├── raw .k1mqtt + metadata + SHA-256 сохраняются первыми +│ └── protobuf/LZ4 decoder +│ └── Foxglove bridge → локальный WebSocket +│ +└── продуктовая пространственная сцена + └── встроенный @rerun-io/web-viewer + └── RRD URL или Rerun gRPC/proxy source +``` + +Между декодером K1 и Rerun Viewer сейчас нет автоматической стрелки. Поля +`foxglove_ws_url` и `foxglove_viewer_url` ещё присутствуют в API только потому, +что Python runtime продолжает поднимать проверенный legacy adapter. Пункт +управления не использует их как основную поверхность визуализации. + +## Фиксированная оболочка + +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. Это ещё не означает, что все продуктовые контролы уже связаны +с API Rerun: текущая граница явно показана статусом `Интерфейс готов`. + +## Архитектурные разделы + +Навигация описана данными в `src/productModel.ts`, а не зашита в разметку каждой +страницы. + +| Раздел | Назначение | +| --- | --- | +| Центр | Оперативный обзор, состояние контура и активность оператора. | +| Парк | Аппараты, текущее устройство, сенсоры и конфигурации борта. | +| Наблюдение | Пространственная сцена, камеры, карта, объекты, телеметрия и время. | +| Миссии | Планировщик, маршруты, сценарии и исполнение. Командный backend отключён. | +| Данные | Сессии, потоки, сущности, playback и экспорт доказательств. | +| Система | Модули, интеграции, сеть, аудит и настройки платформы. | + +Карточки возможностей имеют четыре честных уровня: работает сейчас, готово к +источнику, интерфейсный контракт и последующий этап. Каталог не следует читать +как утверждение, что для каждой карточки уже существует backend. + +## Установка, проверка, сборка и запуск + +Требуются Node.js 20 или новее, `uv` и соседний checkout +`NODEDC_DESIGN_GUIDELINE`: зависимости `@nodedc/*` подключены к нему через +локальные `file:` пути. Python устанавливается только в `.venv` репозитория. + +Канонический путь от корня репозитория: + +```bash +cd /Users/dcconstructions/Downloads/mnt/NODEDC/NDC_xgrids-k1-connector +uv sync --group dev + +cd apps/k1-viewer +npm install +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. + +Для разработки интерфейса после запуска backend: ```bash cd apps/k1-viewer -npm install npm run dev ``` -The development server listens on `http://127.0.0.1:5173` and proxies `/api` to -`http://127.0.0.1:8000`. Set `VITE_API_TARGET` in a local `.env` file to use a -different backend address. Production artifacts are built with `npm run build`. +Vite слушает `http://127.0.0.1:5173` и проксирует `/api` и `/api/events` на +`http://127.0.0.1:8000`. Другой локальный backend можно указать переменной +`VITE_API_TARGET`. Preview production-сборки запускается командой +`npm run preview` на `http://127.0.0.1:4173`. -The NODE.DC UI packages are consumed from the sibling `NODEDC_DESIGN_GUIDELINE` -checkout through `file:` dependencies. Their source is not copied into this app. +## Операторский путь для текущего K1 adapter -## Backend contract +1. Запустить `uv run k1link serve` и открыть Control Station. +2. Выбрать **Парк → Локальное устройство**. +3. Включить K1, дождаться стабильного индикатора и подтвердить это в форме. +4. Нажать **Показать все BLE-устройства**. Интерфейс показывает полный результат + шестисекундного поиска; метка совместимости является подсказкой, выбор делает + оператор. +5. Ввести SSID и пароль существующей сети и явно запустить подключение. Это одна + reviewed provisioning-запись без автоматических повторов. +6. Запустить live-приём по определённому адресу K1 либо replay локального + `.k1mqtt`/проверенного TSV. Физическое сканирование K1 запускается и + останавливается подтверждённым двойным нажатием кнопки устройства. +7. Состояние, частота, число точек, задержка и пропуски preview появятся только + после прихода реальных данных. -| Method | Route | Body / purpose | +Этот путь запускает существующий MQTT/Foxglove runtime. Чтобы увидеть тот же +живой K1 поток во встроенной пространственной сцене, нужно сначала реализовать и +запустить MQTT→Rerun stream adapter; одного запуска live-сессии сейчас +недостаточно. + +## Источники Rerun + +В **Наблюдение → Пространственная сцена → Источник** принимается 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 хранится только в состоянии текущей страницы; +- ошибка запуска показывается в viewport, без подстановки фиктивных данных. + +После готовности Viewer выбор Rerun entity возвращает `entityPath` и имя view в +оболочку. Продуктовые настройки отображения, custom timeline, переключатель +2D/3D/карты и сохранение RBL пока не вызывают Blueprint/playback API. Черновик +компоновки фиксируется только в памяти текущей страницы и не записывается на +диск. + +## Контракт локального API + +| Метод | Route | Тело / назначение | | --- | --- | --- | -| `GET` | `/api/health` | Local service health | -| `GET` | `/api/state` | Authoritative console state | -| `POST` | `/api/ble/scan` | `{ "duration_seconds": 6 }` | -| `POST` | `/api/connect` | `{ "device_id", "ssid", "password" }` | -| `POST` | `/api/session/live` | Optional `{ "host", "duration_seconds" }` | -| `POST` | `/api/session/replay` | `{ "path", "speed", "loop" }` | -| `POST` | `/api/session/stop` | Stop the active source | -| `WS` | `/api/events` | State snapshots for live UI updates | +| `GET` | `/api/health` | Проверка локального сервиса. | +| `GET` | `/api/state` | Авторитетный snapshot состояния. | +| `POST` | `/api/ble/scan` | `{ "duration_seconds": 6 }`. | +| `POST` | `/api/connect` | `{ "device_id", "ssid", "password" }`. | +| `POST` | `/api/session/live` | Опционально `{ "host", "duration_seconds" }`. | +| `POST` | `/api/session/replay` | `{ "path", "speed", "loop" }`. | +| `POST` | `/api/session/stop` | Остановка активного источника. | +| `WS` | `/api/events` | Периодические snapshots для live UI. | -`/api/state` and state-changing responses may return the snapshot directly or as -`{ "state": { ... } }`. A snapshot exposes `phase`, `message`, `devices`, -`selected_device_id`, `k1_ip`, `foxglove_ws_url`, `foxglove_viewer_url`, -`source_mode` and `metrics`. +`/api/state` и изменяющие состояние ответы могут вернуть snapshot напрямую или +как `{ "state": { ... } }`. Текущие поля включают `phase`, `message`, `devices`, +`selected_device_id`, `k1_ip`, `source_mode`, `metrics` и legacy-поля +`foxglove_ws_url`/`foxglove_viewer_url`. OpenAPI доступен по `/api/docs`. -The Wi-Fi password remains only in React memory, is sent in the JSON POST body, and is -cleared after the backend acknowledges a successful connection. It is never written to -local storage or included in a URL. +## Карта исходников frontend -## Explicit unavailable states +| Файл | Ответственность | +| --- | --- | +| `src/App.tsx` | Fixed shell, выбор разделов и окна source/display/layers/layout. | +| `src/productModel.ts` | Архитектурные разделы, рабочие поверхности и уровни готовности. | +| `src/workspaces/DeviceWorkspace.tsx` | Реальный K1 BLE/Wi-Fi/live/replay adapter UI. | +| `src/workspaces/Workspaces.tsx` | Оперативный обзор, spatial viewport и остальные продуктовые поверхности. | +| `src/components/RerunViewport.tsx` | Жизненный цикл встроенного Rerun Web Viewer и selection events. | +| `src/api.ts` | REST/WebSocket контракт с FastAPI. | +| `src/useK1Console.ts` | Состояние backend, polling, события и действия оператора. | +| `src/sceneSettings.ts` | Типизированный UI-профиль пространственной сцены. | +| `src/presentation.ts` | Русские подписи фаз и форматирование реальных метрик. | +| `src/messages.ts` | Обезличивание и локализация технических сообщений в пользовательском интерфейсе. | +| `src/styles.css`, `src/styles/*` | Компоновка shell и рабочих поверхностей. | -- Missing metrics render as an em dash; no synthetic values are used. -- BLE scanning remains a real API action and reports HTTP/network errors visibly. -- The Foxglove button is disabled until the backend supplies - `foxglove_viewer_url`. -- Replay cannot start without a backend-local capture path. +## Safety и чувствительные данные + +- Control Station и credential endpoint доступны только на loopback. +- Пароль Wi-Fi находится только в React memory, передаётся в JSON POST body, + очищается после успешного ответа и не сохраняется в URL/local storage. +- BLE-подключение выполняет только отдельно рассмотренную provisioning-запись; + случайные GATT writes и автоматические повторы запрещены. +- MQTT live/replay не публикует команды устройству. Запуск и остановка + физического сканирования остаются за кнопкой K1. +- Live-сессии сначала сохраняют сырые сообщения, затем формируют preview. При + перегрузке preview может быть отброшен, raw evidence сохраняется. +- `sessions/` игнорируется Git и может содержать адреса, идентификаторы, + траекторию и карту помещения. В репозиторий попадают только redacted manifests + и безопасная документация. diff --git a/apps/k1-viewer/index.html b/apps/k1-viewer/index.html index 0395819..2fc70f4 100644 --- a/apps/k1-viewer/index.html +++ b/apps/k1-viewer/index.html @@ -5,10 +5,10 @@ - K1 · Облако точек в реальном времени · NODE.DC + NODE.DC · Пункт управления
diff --git a/apps/k1-viewer/package-lock.json b/apps/k1-viewer/package-lock.json index 623fbf6..0154174 100644 --- a/apps/k1-viewer/package-lock.json +++ b/apps/k1-viewer/package-lock.json @@ -11,6 +11,7 @@ "@nodedc/tokens": "file:../../../NODEDC_DESIGN_GUIDELINE/packages/tokens", "@nodedc/ui-core": "file:../../../NODEDC_DESIGN_GUIDELINE/packages/ui-core", "@nodedc/ui-react": "file:../../../NODEDC_DESIGN_GUIDELINE/packages/ui-react", + "@rerun-io/web-viewer": "0.34.1", "react": "^19.1.0", "react-dom": "^19.1.0" }, @@ -20,7 +21,8 @@ "@types/react-dom": "^19.1.0", "@vitejs/plugin-react": "^4.6.0", "typescript": "^5.8.3", - "vite": "^7.0.0" + "vite": "^7.0.0", + "vite-plugin-wasm": "^3.6.0" }, "engines": { "node": ">=20" @@ -844,6 +846,12 @@ "resolved": "../../../NODEDC_DESIGN_GUIDELINE/packages/ui-react", "link": true }, + "node_modules/@rerun-io/web-viewer": { + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@rerun-io/web-viewer/-/web-viewer-0.34.1.tgz", + "integrity": "sha512-2Oq9Mw3qOs765XArGq4e/0pAIksPFKlAqkgo+PDsRkPd77/KdnQhb835suRmYQ6tuqlbPeVCFhlNIXT0+TjmTg==", + "license": "MIT" + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -1880,6 +1888,16 @@ } } }, + "node_modules/vite-plugin-wasm": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/vite-plugin-wasm/-/vite-plugin-wasm-3.6.0.tgz", + "integrity": "sha512-mL/QPziiIA4RAA6DkaZZzOstdwbW5jO4Vz7Zenj0wieKWBlNvIvX5L5ljum9lcUX0ShNfBgCNLKTjNkRVVqcsw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "vite": "^2 || ^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/apps/k1-viewer/package.json b/apps/k1-viewer/package.json index e7777d8..0a30468 100644 --- a/apps/k1-viewer/package.json +++ b/apps/k1-viewer/package.json @@ -13,6 +13,7 @@ "@nodedc/tokens": "file:../../../NODEDC_DESIGN_GUIDELINE/packages/tokens", "@nodedc/ui-core": "file:../../../NODEDC_DESIGN_GUIDELINE/packages/ui-core", "@nodedc/ui-react": "file:../../../NODEDC_DESIGN_GUIDELINE/packages/ui-react", + "@rerun-io/web-viewer": "0.34.1", "react": "^19.1.0", "react-dom": "^19.1.0" }, @@ -22,7 +23,8 @@ "@types/react-dom": "^19.1.0", "@vitejs/plugin-react": "^4.6.0", "typescript": "^5.8.3", - "vite": "^7.0.0" + "vite": "^7.0.0", + "vite-plugin-wasm": "^3.6.0" }, "engines": { "node": ">=20" diff --git a/apps/k1-viewer/public/nodedc-logo.svg b/apps/k1-viewer/public/nodedc-logo.svg new file mode 100644 index 0000000..8a68666 --- /dev/null +++ b/apps/k1-viewer/public/nodedc-logo.svg @@ -0,0 +1,8 @@ + diff --git a/apps/k1-viewer/public/nodedc-mark.svg b/apps/k1-viewer/public/nodedc-mark.svg new file mode 100644 index 0000000..836bf0e --- /dev/null +++ b/apps/k1-viewer/public/nodedc-mark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/k1-viewer/src/App.tsx b/apps/k1-viewer/src/App.tsx index fcbdb58..ee54567 100644 --- a/apps/k1-viewer/src/App.tsx +++ b/apps/k1-viewer/src/App.tsx @@ -1,783 +1,554 @@ -import { useEffect, useMemo, useState, type ReactNode } from "react"; +import { useMemo, useState } from "react"; import { + AdminNavigationPanel, AppHeader, + ApplicationPanel, ApplicationShell, Button, - Checker, - GlassSurface, + ColorField, + ControlRow, HeaderAvatar, HeaderNavigation, HeaderProfile, HeaderProfileButton, + HeaderWorkspace, Icon, - SegmentedControl, + Inspector, + RangeControl, + Select, StatusBadge, + Switch, TextField, - type StatusTone, + Window, + WindowFooterActions, + useApplicationWorkspace, + type ApplicationPanelUtilityAction, } from "@nodedc/ui-react"; -import type { BleDevice, ConsoleState, K1Metrics } from "./api"; -import { useK1Console, type BackendStatus } from "./useK1Console"; +import { LandingStage } from "./components/LandingStage"; +import { + rootById, + roots, + workspaceById, + workspacesForRoot, + type RootId, +} from "./productModel"; +import { backendLabel, phaseLabel, phaseTone } from "./presentation"; +import { localizeRuntimeMessage } from "./messages"; +import { + defaultSceneSettings, + type PointColorMode, + type PointPalette, + type SceneSettings, +} from "./sceneSettings"; +import { useK1Console } from "./useK1Console"; +import { DeviceWorkspace } from "./workspaces/DeviceWorkspace"; +import { WorkspaceRenderer } from "./workspaces/Workspaces"; -type ConsoleSection = "monitor" | "connect" | "session"; -type SessionIntent = "live" | "replay"; +const colorModeOptions: Array<{ value: PointColorMode; label: string; description: string }> = [ + { value: "intensity", label: "Интенсивность", description: "Значение отражённого сигнала" }, + { value: "height", label: "Высота Z", description: "Градиент по вертикальной координате" }, + { value: "distance", label: "Расстояние", description: "Дальность точки от сенсора" }, + { value: "rgb", label: "RGB", description: "Цвет, если он присутствует в данных" }, + { value: "class", label: "Класс", description: "Категория внешнего модуля восприятия" }, +]; -const sectionItems = [ - { value: "monitor", label: "Монитор" }, - { value: "connect", label: "Подключение" }, - { value: "session", label: "Поток" }, -] as const; - -const sessionItems = [ - { value: "live", label: "Сканер в реальном времени" }, - { value: "replay", label: "Повтор записи" }, -] satisfies Array<{ value: SessionIntent; label: string }>; - -const phaseLabels: Record = { - idle: "Ожидание", - scanning: "Поиск по BLE", - device_selected: "K1 выбран", - provisioning: "Передача настроек Wi‑Fi", - connecting: "Подключение", - connected: "K1 подключён", - starting_live: "Запуск потока", - live: "Поток в реальном времени", - replay: "Повтор записи", - stopping: "Остановка", - error: "Ошибка", -}; - -function phaseLabel(phase: string | null | undefined): string { - if (!phase) return "Нет состояния"; - return phaseLabels[phase] ?? "Неизвестное состояние"; -} - -function phaseTone(phase: string | null | undefined): StatusTone { - if (!phase) return "neutral"; - if (phase === "error") return "danger"; - if (["connected", "live", "replay"].includes(phase)) return "success"; - if (["scanning", "provisioning", "connecting", "starting_live", "stopping"].includes(phase)) { - return "accent"; - } - return "neutral"; -} - -function backendLabel(status: BackendStatus): string { - return { - checking: "Проверка API", - online: "API работает", - degraded: "API работает частично", - offline: "API недоступен", - }[status]; -} - -function eventStatusLabel(status: string): string { - return { - connecting: "подключение", - open: "подключён", - closed: "закрыт", - error: "ошибка", - }[status] ?? "неизвестно"; -} - -function backendTone(status: BackendStatus): StatusTone { - if (status === "online") return "success"; - if (status === "degraded" || status === "checking") return "warning"; - return "danger"; -} - -function finiteMetric(value: number | null | undefined): number | null { - return typeof value === "number" && Number.isFinite(value) ? value : null; -} - -function pipelineLatency(metrics: K1Metrics | undefined): number | null { - if (!metrics) return null; - const direct = finiteMetric(metrics.pipeline_ms ?? metrics.end_to_end_ms); - if (direct !== null) return direct; - - const segments = [ - finiteMetric(metrics.mqtt_to_decode_ms), - finiteMetric(metrics.decode_ms), - finiteMetric(metrics.publish_ms), - ].filter((value): value is number => value !== null); - - return segments.length ? segments.reduce((total, value) => total + value, 0) : null; -} - -function formatNumber(value: number | null, digits = 1): string { - if (value === null) return "—"; - return value.toLocaleString("ru-RU", { - maximumFractionDigits: digits, - minimumFractionDigits: digits, - }); -} - -function MetricCard({ - eyebrow, - value, - unit, - detail, - featured = false, -}: { - eyebrow: string; - value: string; - unit?: string; - detail: string; - featured?: boolean; -}) { - return ( - - {eyebrow} -
- {value} - {unit ? {unit} : null} -
-

{detail}

-
- ); -} - -function DetailRow({ label, children }: { label: string; children: ReactNode }) { - return ( -
-
{label}
-
{children}
-
- ); -} - -function WizardStep({ - number, - title, - status, - tone = "neutral", - children, -}: { - number: string; - title: string; - status: string; - tone?: StatusTone; - children: ReactNode; -}) { - return ( -
- -
-
-

{title}

- {status} -
- {children} -
-
- ); -} - -function DeviceRow({ - device, - selected, - onSelect, -}: { - device: BleDevice; - selected: boolean; - onSelect: () => void; -}) { - return ( -
-
-
-
- {finiteMetric(device.rssi) === null ? "RSSI —" : `${device.rssi} дБм`} - -
-
- ); -} - -function LatencyTrace({ values }: { values: number[] }) { - const ceiling = Math.max(16, ...values); - - return ( -
- {values.length ? ( - values.map((value, index) => ( - - )) - ) : ( -

Измерений пока нет. График появится после получения первых данных от K1.

- )} -
- ); -} - -function FoxglovePanel({ state }: { state: ConsoleState | null }) { - const viewerUrl = state?.foxglove_viewer_url?.trim(); - - return ( - -