From 79911eb3169195a53be238e0ce85cf76765fc90f Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Sat, 5 Sep 2026 18:35:57 +0300 Subject: [PATCH] feat(node): consolidate board system views and plan fleet pairing --- apps/node-agent/README.md | 7 + apps/node-agent/_qa/main.go | 4 +- apps/node-agent/packaging/build_deb.py | 2 +- apps/node-agent/ui/src/BoardSummary.tsx | 19 ++ apps/node-agent/ui/src/InventoryViews.tsx | 4 +- apps/node-agent/ui/src/NodeOverview.tsx | 15 +- apps/node-agent/ui/src/SetupView.tsx | 14 -- apps/node-agent/ui/src/TailnetAccess.tsx | 34 +--- apps/node-agent/ui/src/main.tsx | 14 +- apps/node-agent/ui/src/nodeModel.ts | 20 +- apps/node-agent/ui/src/useTailscaleStatus.ts | 36 ++++ docs/node/02_NODE_DESKTOP_SURFACE.md | 3 + .../03_SYSTEM_AND_VEHICLE_PAIRING_SURFACE.md | 179 ++++++++++++++++++ 13 files changed, 278 insertions(+), 73 deletions(-) create mode 100644 apps/node-agent/ui/src/BoardSummary.tsx delete mode 100644 apps/node-agent/ui/src/SetupView.tsx create mode 100644 apps/node-agent/ui/src/useTailscaleStatus.ts create mode 100644 docs/node/03_SYSTEM_AND_VEHICLE_PAIRING_SURFACE.md diff --git a/apps/node-agent/README.md b/apps/node-agent/README.md index 3f76d56..656333c 100644 --- a/apps/node-agent/README.md +++ b/apps/node-agent/README.md @@ -1,5 +1,12 @@ # Mission Core Node — Ubuntu system configuration candidate +0.3.1 consolidates host inventory, USB, Tailscale and SSH under «Система». +«Обзор БК» contains host facts, the board name and an observed connectivity +summary. The separate configuration page and redundant Node health badge are +removed. «Устройства» is reserved for driver-backed devices and remains disabled +until a real device workflow exists. The admitted Node/Core pairing and fleet +surface plan is in `docs/node/03_SYSTEM_AND_VEHICLE_PAIRING_SURFACE.md`. + 0.3.0 adopts the canonical Mission Core shell, navigation, system views and a GUI list of trusted SSH devices. ResourceRow is shared from Design Guideline; see `docs/node/02_NODE_DESKTOP_SURFACE.md` for composition and acceptance. diff --git a/apps/node-agent/_qa/main.go b/apps/node-agent/_qa/main.go index aa20fab..1137ce0 100644 --- a/apps/node-agent/_qa/main.go +++ b/apps/node-agent/_qa/main.go @@ -15,11 +15,11 @@ import ( ) func main() { - dir := "/private/tmp/mc-node-ui-030-qa" + dir := "/private/tmp/mc-node-ui-031-qa" store, err := node.OpenStore(dir); if err != nil { log.Fatal(err) } assets, _ := fs.Sub(web.Assets, "dist") memory, available := uint64(8388608), uint64(5242880) - app := &node.Server{Store: store, Assets: assets, Origin: "http://127.0.0.1:8780", Version: "0.3.0-qa", Inventory: func() node.Inventory { + app := &node.Server{Store: store, Assets: assets, Origin: "http://127.0.0.1:8780", Version: "0.3.1-qa", Inventory: func() node.Inventory { return node.Inventory{CollectedAt: time.Now().UTC().Format(time.RFC3339), Hostname:"qa-board", OS:"Ubuntu 24.04.4 LTS", Architecture:"amd64", CPUs:8, MemoryKiB:&memory, AvailableKiB:&available, Networks: []node.Network{{Name:"ethernet-qa", Up:true, Addresses:[]string{"192.0.2.10/24"}}}, USB:[]node.USB{{Port:"2-1", Vendor:"8086", ProductID:"0b5c", Product:"Intel RealSense D455 · QA", Speed:"5000"}}, USBReadable:true, Warnings:[]string{}} diff --git a/apps/node-agent/packaging/build_deb.py b/apps/node-agent/packaging/build_deb.py index eee50c1..ee1e630 100644 --- a/apps/node-agent/packaging/build_deb.py +++ b/apps/node-agent/packaging/build_deb.py @@ -13,7 +13,7 @@ import tarfile ROOT = Path(__file__).resolve().parents[1] -VERSION = "0.3.0" +VERSION = "0.3.1" BRAND_SHA256 = "8bfee8ca9f98e0db48d98aae3af4b32493b8593e18b064a0239d513d824182af" diff --git a/apps/node-agent/ui/src/BoardSummary.tsx b/apps/node-agent/ui/src/BoardSummary.tsx new file mode 100644 index 0000000..6c5d537 --- /dev/null +++ b/apps/node-agent/ui/src/BoardSummary.tsx @@ -0,0 +1,19 @@ +import { ActivityIndicator, Button, Icon, ResourceList, ResourceRow, SettingsCard, StatusBadge } from "@nodedc/ui-react"; +import type { Status } from "./api"; +import type { ViewId } from "./nodeModel"; +import { useAccess } from "./useAccess"; +import { tailscaleLabel, useTailscaleStatus } from "./useTailscaleStatus"; + +export function BoardSummary({ value, failure, openView }: { value: Status; failure: (error: unknown) => void; openView: (id: ViewId) => void }) { + const { access, loading } = useAccess(value.host.collected_at, failure); + const tailnet = useTailscaleStatus(failure, value.host.collected_at); + const networks = value.host.networks.filter(item => item.name !== "lo" && item.name !== "lo0" && item.up && item.addresses.length > 0); + return + +
  • } title="Сеть" description="Включённые интерфейсы с назначенными адресами" status={{networks.length}} actions={} />
  • +
  • } title="USB-устройства" description="Обнаружены операционной системой" status={{value.host.usb_readable ? value.host.usb.length : "Нет сведений"}} actions={} />
  • +
  • : } title="Tailscale" description="Частная сеть" status={{tailscaleLabel(tailnet.value, tailnet.checked)}} actions={} />
  • +
  • : } title="SSH" description="Удалённое обслуживание БК" metadata={access ? `Разрешено ключей: ${access.keys.length}` : undefined} status={{loading ? "Проверяем" : !access ? "Нет сведений" : access.ssh_ready ? "Отвечает локально" : "Не отвечает"}} actions={} />
  • +
    +
    ; +} diff --git a/apps/node-agent/ui/src/InventoryViews.tsx b/apps/node-agent/ui/src/InventoryViews.tsx index 27c0df5..1376fef 100644 --- a/apps/node-agent/ui/src/InventoryViews.tsx +++ b/apps/node-agent/ui/src/InventoryViews.tsx @@ -1,11 +1,11 @@ import { Button, Icon, ResourceList, ResourceRow, SettingsCard, StatusBadge } from "@nodedc/ui-react"; import type { Status } from "./api"; export function NetworkView({ value }: { value: Status }) { - return
    + return
    {value.host.networks.length === 0 ?

    Сетевые интерфейсы не обнаружены.

    : {value.host.networks.map(network =>
  • } title={network.name} description={network.addresses.join(" · ") || "Нет назначенного адреса"} status={{network.up ? "Включён" : "Выключен"}} />
  • )}
    }

    Наличие адреса не подтверждает доступность другого компьютера или устройства.

    ; } -export function DevicesView({ value }: { value: Status }) { +export function USBView({ value }: { value: Status }) { return
    {value.host.usb.length}}> {!value.host.usb_readable ?

    Не удалось получить список устройств. Повторите обновление.

    : value.host.usb.length === 0 ?

    Подключите устройство к USB, затем обновите список.

    : {value.host.usb.map(device =>
  • } title={device.product || `USB ${device.vendor}:${device.product_id}`} description={`Порт ${device.port} · ${device.speed_mbps ? `${device.speed_mbps} Мбит/с` : "Скорость недоступна"}`} metadata={`${device.vendor}:${device.product_id}`} status={Обнаружено} />
  • )}
    }

    Обнаружение USB ещё не означает готовность к съёмке.

    ; diff --git a/apps/node-agent/ui/src/NodeOverview.tsx b/apps/node-agent/ui/src/NodeOverview.tsx index 2d92a77..d1c716a 100644 --- a/apps/node-agent/ui/src/NodeOverview.tsx +++ b/apps/node-agent/ui/src/NodeOverview.tsx @@ -1,9 +1,11 @@ import { useEffect, useState } from "react"; -import { Button, SettingsCard, StatusBadge, TextField } from "@nodedc/ui-react"; +import { Button, SettingsCard, TextField } from "@nodedc/ui-react"; import { request, type Status } from "./api"; +import type { ViewId } from "./nodeModel"; +import { BoardSummary } from "./BoardSummary"; const memory = (value: number | null) => value === null ? "Недоступно" : `${(value / 1048576).toFixed(1)} ГиБ`; -export function NodeOverview({ value, refresh, failure }: { value: Status; refresh: () => Promise; failure: (error: unknown) => void }) { +export function NodeOverview({ value, refresh, failure, openView }: { value: Status; refresh: () => Promise; failure: (error: unknown) => void; openView: (id: ViewId) => void }) { const [name, setName] = useState(value.name); const [saving, setSaving] = useState(false); useEffect(() => setName(value.name), [value.name]); @@ -12,16 +14,17 @@ export function NodeOverview({ value, refresh, failure }: { value: Status; refre try { await request("/api/name", "PUT", { name }); await refresh(); } catch (error) { failure(error); } finally { setSaving(false); } } return
    - Node работает}> +
    Операционная система
    {value.host.os}
    Архитектура
    {value.host.architecture}
    Логических процессоров
    {value.host.cpus}
    Оперативная память
    {memory(value.host.memory_kib)}
    Доступно памяти
    {memory(value.host.available_kib)}
    Mission Core Node
    {value.version}
    - +
    - setName(event.target.value)} autoComplete="off" /> + setName(event.target.value)} autoComplete="off" /> -
    ID ноды
    {value.node_id}
    +
    ID БК
    {value.node_id}
    +

    Сведения обновлены {new Date(value.host.collected_at).toLocaleString("ru-RU")}.

    ; } diff --git a/apps/node-agent/ui/src/SetupView.tsx b/apps/node-agent/ui/src/SetupView.tsx deleted file mode 100644 index 9473b49..0000000 --- a/apps/node-agent/ui/src/SetupView.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { ActivityIndicator, Button, Icon, ResourceList, ResourceRow, StatusBadge } from "@nodedc/ui-react"; -import { useAccess } from "./useAccess"; -export function SetupView({ revision, failure, openSSH, openTailnet }: { revision: string; failure: (error: unknown) => void; openSSH: () => void; openTailnet: () => void }) { - const { access, loading } = useAccess(revision, failure); - return
    -

    Компоненты для работы с этим компьютером и его обслуживания.

    - -
  • } title="Mission Core Node" description="Локальная служба и приложение" status={Работает} />
  • -
  • : } title="OpenSSH Server" description="Доступ для обслуживания компьютера" status={{loading ? "Проверяем" : !access ? "Нет сведений" : access.ssh_ready ? "Отвечает локально" : "Не отвечает"}} actions={} />
  • -
  • } title="Tailscale" description="Подключение к частной сети" actions={} />
  • -
    -

    OpenSSH устанавливается вместе с Node. Ответ локального сервера не подтверждает подключение с другого компьютера.

    -
    ; -} diff --git a/apps/node-agent/ui/src/TailnetAccess.tsx b/apps/node-agent/ui/src/TailnetAccess.tsx index 7f883fb..f04dfb2 100644 --- a/apps/node-agent/ui/src/TailnetAccess.tsx +++ b/apps/node-agent/ui/src/TailnetAccess.tsx @@ -1,40 +1,16 @@ import { useEffect, useState } from "react"; import { ActivityIndicator, Button, SettingsCard, StatusBadge } from "@nodedc/ui-react"; -import { APIError, desktopAction, networkSetupAvailable, request } from "./api"; +import { desktopAction, networkSetupAvailable } from "./api"; +import { tailscaleLabel, useTailscaleStatus } from "./useTailscaleStatus"; -interface NetworkStatus { installed: boolean; state: string; online: boolean; addresses: string[] } interface NetworkResult { action: string; ok: boolean; error?: string; browser_opened?: boolean } -const states: Record = { - not_installed: "Не установлен", unavailable: "Служба недоступна", NeedsLogin: "Требуется вход", - NeedsMachineAuth: "Ожидаем разрешения администратора сети", Stopped: "Отключён", Starting: "Подключаемся", NoState: "Запускается", -}; - export function TailnetAccess({ failure, revision: hostRevision }: { failure: (error: unknown) => void; revision: string }) { - const [checked, setChecked] = useState(false); - const [value, setValue] = useState(null); const [pending, setPending] = useState(null); const [notice, setNotice] = useState(""); const [revision, setRevision] = useState(0); - useEffect(() => { - let active = true; - let timer: ReturnType; - async function update() { - try { - const next = await request("/api/network/tailscale"); - if (active) { setValue(next); if (next.online) setNotice(""); } - } catch (error) { - if (active) { - setValue(null); - // Service upgrades invalidate local sessions. Use the existing - // application login surface instead of hiding 401 as provider failure. - if (error instanceof APIError && error.status === 401) failure(error); - } - } finally { if (active) { setChecked(true); timer = setTimeout(() => void update(), 5000); } } - } - void update(); - return () => { active = false; clearTimeout(timer); }; - }, [revision, hostRevision, failure]); + const { value, checked } = useTailscaleStatus(failure, hostRevision, revision); + useEffect(() => { if (value?.online) setNotice(""); }, [value]); useEffect(() => { function completed(event: Event) { const result = (event as CustomEvent).detail; @@ -54,7 +30,7 @@ export function TailnetAccess({ failure, revision: hostRevision }: { failure: (e setPending(null); failure(new Error("Откройте установленное приложение Mission Core Node из меню Ubuntu.")); } } - const label = !checked ? "Проверяем подключение" : !value ? "Состояние недоступно" : value.state === "Running" ? value.online ? "В сети" : "Нет связи с координатором" : states[value.state] ?? "Состояние неизвестно"; + const label = tailscaleLabel(value, checked); const canConnect = value?.installed && ["NeedsLogin", "Stopped", "unavailable"].includes(value.state); const supported = networkSetupAvailable(); return
    {label}}> diff --git a/apps/node-agent/ui/src/main.tsx b/apps/node-agent/ui/src/main.tsx index b01ef8d..c9f26ed 100644 --- a/apps/node-agent/ui/src/main.tsx +++ b/apps/node-agent/ui/src/main.tsx @@ -8,9 +8,8 @@ import { desktopLogin } from "./api"; import { useNode } from "./useNode"; import { roots, views, type RootId, type ViewId } from "./nodeModel"; import { NodeOverview } from "./NodeOverview"; -import { DevicesView, DiagnosticsView, NetworkView } from "./InventoryViews"; +import { USBView, DiagnosticsView, NetworkView } from "./InventoryViews"; import { SystemAccess } from "./SystemAccess"; -import { SetupView } from "./SetupView"; import { TailnetAccess } from "./TailnetAccess"; import "./node.css"; @@ -26,17 +25,16 @@ function App() { const currentRoot = roots.find(item => item.id === root)!; const currentView = views.find(item => item.id === workspace.activeView); function openView(id: ViewId) { setAdding(false); setRoot(views.find(item => item.id === id)!.root); workspace.openView(id); } - function selectRoot(id: RootId) { openView(roots.find(item => item.id === id)!.first); } - const content = !value ? null : workspace.activeView === "overview" ? + function selectRoot(id: RootId) { const first = roots.find(item => item.id === id)!.first; if (first) openView(first); } + const content = !value ? null : workspace.activeView === "overview" ? : workspace.activeView === "network" ? - : workspace.activeView === "usb" ? + : workspace.activeView === "usb" ? : workspace.activeView === "diagnostics" ? - : workspace.activeView === "setup" ? openView("ssh")} openTailnet={() => openView("tailscale")} /> : workspace.activeView === "tailscale" ? : workspace.activeView === "ssh" ? setAdding(false)} /> : null; return <> } brandLabel="Mission Core Node" - center={<> ({ value: item.id, label: item.label, disabled: !value }))} onChange={selectRoot} />} + center={<> ({ value: item.id, label: item.label, disabled: !value || !item.first }))} onChange={selectRoot} />} right={ void refresh() }, { id: "theme", label: theme === "dark" ? "Светлая тема" : "Тёмная тема", icon: "eye", onSelect: () => { const next = theme === "dark" ? "light" : "dark"; setTheme(next); localStorage.setItem("node-theme", next); } }]} />} />} navigationOpen={!!value && workspace.navigationOpen} contentOpen={!!value && workspace.contentOpen} contentExpanded={workspace.contentExpanded} navigation={ setAdding(true) }] : []), { label: "Обновить сведения", icon: "refresh", disabled: pending, onClick: () => void refresh() }]}>{content}} stage={
    - {value ?
    {roots.map(item => )}
    : + {value ?
    {roots.map(item => )}
    : {pending ? :

    {locked ? "Подтвердите доступ в системном окне Ubuntu." : "Не удалось связаться со службой. Повторите подключение."}

    }
    } diff --git a/apps/node-agent/ui/src/nodeModel.ts b/apps/node-agent/ui/src/nodeModel.ts index 5c7ffa8..1332546 100644 --- a/apps/node-agent/ui/src/nodeModel.ts +++ b/apps/node-agent/ui/src/nodeModel.ts @@ -1,17 +1,15 @@ import type { IconName } from "@nodedc/ui-react"; -export type RootId = "system" | "devices" | "remote"; -export type ViewId = "overview" | "network" | "setup" | "diagnostics" | "usb" | "tailscale" | "ssh"; -export const roots: { id: RootId; label: string; first: ViewId }[] = [ - { id: "system", label: "Состояние системы", first: "overview" }, - { id: "devices", label: "Устройства", first: "usb" }, - { id: "remote", label: "Удалённый контроль", first: "tailscale" }, +export type RootId = "system" | "devices"; +export type ViewId = "overview" | "network" | "diagnostics" | "usb" | "tailscale" | "ssh"; +export const roots: { id: RootId; label: string; first: ViewId | null }[] = [ + { id: "system", label: "Система", first: "overview" }, + { id: "devices", label: "Устройства", first: null }, ]; export const views: { id: ViewId; root: RootId; label: string; icon: IconName }[] = [ - { id: "overview", root: "system", label: "Обзор компьютера", icon: "activity" }, + { id: "overview", root: "system", label: "Обзор БК", icon: "activity" }, { id: "network", root: "system", label: "Сеть", icon: "network" }, - { id: "setup", root: "system", label: "Конфигурация системы", icon: "settings" }, + { id: "usb", root: "system", label: "USB-устройства", icon: "camera" }, + { id: "tailscale", root: "system", label: "Tailscale", icon: "globe" }, + { id: "ssh", root: "system", label: "SSH · доверенные устройства", icon: "key" }, { id: "diagnostics", root: "system", label: "Диагностика", icon: "clipboard" }, - { id: "usb", root: "devices", label: "USB-устройства", icon: "camera" }, - { id: "tailscale", root: "remote", label: "Tailscale", icon: "globe" }, - { id: "ssh", root: "remote", label: "SSH · доверенные устройства", icon: "key" }, ]; diff --git a/apps/node-agent/ui/src/useTailscaleStatus.ts b/apps/node-agent/ui/src/useTailscaleStatus.ts new file mode 100644 index 0000000..b03206b --- /dev/null +++ b/apps/node-agent/ui/src/useTailscaleStatus.ts @@ -0,0 +1,36 @@ +import { useEffect, useState } from "react"; +import { APIError, request } from "./api"; + +export interface NetworkStatus { installed: boolean; state: string; online: boolean; addresses: string[] } +const states: Record = { + not_installed: "Не установлен", unavailable: "Служба недоступна", NeedsLogin: "Требуется вход", + NeedsMachineAuth: "Ожидаем разрешения администратора сети", Stopped: "Отключён", Starting: "Подключаемся", NoState: "Запускается", +}; +export function tailscaleLabel(value: NetworkStatus | null, checked: boolean): string { + return !checked ? "Проверяем подключение" : !value ? "Состояние недоступно" : value.state === "Running" ? value.online ? "В сети" : "Нет связи с координатором" : states[value.state] ?? "Состояние неизвестно"; +} + +// Shared by the overview and provider page so session expiry and network states +// retain the same meaning. Only the mounted view owns a polling loop. +export function useTailscaleStatus(failure: (error: unknown) => void, hostRevision: string, revision = 0) { + const [checked, setChecked] = useState(false); + const [value, setValue] = useState(null); + useEffect(() => { + let active = true; + let timer: ReturnType; + async function update() { + try { + const next = await request("/api/network/tailscale"); + if (active) setValue(next); + } catch (error) { + if (active) { + setValue(null); + if (error instanceof APIError && error.status === 401) failure(error); + } + } finally { if (active) { setChecked(true); timer = setTimeout(() => void update(), 5000); } } + } + void update(); + return () => { active = false; clearTimeout(timer); }; + }, [revision, hostRevision, failure]); + return { value, checked }; +} diff --git a/docs/node/02_NODE_DESKTOP_SURFACE.md b/docs/node/02_NODE_DESKTOP_SURFACE.md index 2149d91..65e7926 100644 --- a/docs/node/02_NODE_DESKTOP_SURFACE.md +++ b/docs/node/02_NODE_DESKTOP_SURFACE.md @@ -1,5 +1,8 @@ # Node 0.3 — структура настольного приложения +История композиции 0.3.0. Последующее уточнение владельца, перенос системных +функций в 0.3.1 и план сопряжения: [03_SYSTEM_AND_VEHICLE_PAIRING_SURFACE.md](03_SYSTEM_AND_VEHICLE_PAIRING_SURFACE.md). + Решение владельца от 2026-09-05: привести доступные функции Node к визуальной системе действующего Mission Core, повторно использовать шапку, навигацию и длинные строки AI Inference. Это согласование состава вкладок и композиции. diff --git a/docs/node/03_SYSTEM_AND_VEHICLE_PAIRING_SURFACE.md b/docs/node/03_SYSTEM_AND_VEHICLE_PAIRING_SURFACE.md new file mode 100644 index 0000000..83f516e --- /dev/null +++ b/docs/node/03_SYSTEM_AND_VEHICLE_PAIRING_SURFACE.md @@ -0,0 +1,179 @@ +# Система БК и добавление аппарата в Mission Core + +Уточнение владельца от 2026-09-05 к MISSIONCOR-76. Базовое тело Ops не +переписывается. Этот документ фиксирует согласованную композицию следующего +приращения и порядок реализации; наличие описанного действия не означает, +что его backend уже реализован. История 0.3.0 остаётся в документе 02. + +## Задача и границы сущностей + +Оператор настраивает бортовой компьютер (БК), видит фактическое состояние его +системы и связывает его с аппаратом в Core. Это настройка при первом запуске +и обслуживание, а не лабораторный отчёт. После сопряжения оператор открывает +аппарат в парке и работает с его реально зарегистрированными устройствами. + +БК — вычислительный корень подключений этого аппарата. USB-контроллер, hub, +клавиатура и найденная камера относятся к системной инвентаризации. Запись USB +не является регистрацией сенсора, доказательством его identity, набором +capabilities или готовностью к съёмке. Устройства — объекты драйверов, +подключённые и подтверждённые через принятый контракт Plugin SDK v0alpha2. + +## Node: согласованное размещение + +- Верхний раздел «Система» вместо «Состояние системы». +- «Обзор БК»: ОС, архитектура, CPU/RAM, имя, сводка фактических подключений. + Название редактируется в блоке «Название БК». Самоподтверждающий зелёный + статус «Node работает» удаляется. Сведения имеют время получения. +- «Сеть»: интерфейсы и адреса БК. +- «USB-устройства»: вся системная USB-инвентаризация перенесена сюда. +- «Tailscale» и «SSH · доверенные устройства» принадлежат «Системе». + Отдельного верхнего «Удалённого контроля» больше нет. +- «Mission Core»: выбранное место для создания приглашения и обслуживания + привязки к Core. Подключение Core отделено от настройки сетевого транспорта. +- «Диагностика»: существующий очищенный отчёт и замечания инвентаризации. + Это ограниченная диагностика, не полный support bundle всех будущих модулей. +- Отдельная «Конфигурация системы» удаляется. Состояния SSH/Tailscale и + оборудования входят в сводку; операции остаются в своих подразделах. +- Верхние «Устройства» предназначены для драйверных K1, D455 и следующих + сенсоров. Системный USB-список там не дублируется. В этом приращении + вкладка недоступна до появления рабочего подключения через драйвер; + пустой экран и фиктивные зарегистрированные устройства не создаются. + +Отдельный раздел телеметрии и VPN-профили сейчас не вводятся: владелец оставил +их дальнейшую композицию на следующий этап. Текущий обзор показывает +измеренные сведения, не имитирует будущую телеметрию аппарата. + +Выбрана системная композиция вместо трёх прежних верхних разделов: установка +сети и доступ относятся к одному БК, а сенсоры имеют отдельный жизненный цикл. +Альтернатива «всё внутри Сети» отклонена для сопряжения: частная достижимость +и доверенная привязка к Core являются разными фактами. Альтернатива переноса +всех операций в обзор также отклонена: обзор остаётся компактной сводкой и +ведёт к соответствующей настройке. Это изменение композиции класса B, +прямо согласованное владельцем в текущем сообщении, без нового общего контрола. + +## Обзор БК: состояния и доказательства + +Обзор сохраняет сведения об ОС, архитектуре, CPU/RAM и отдельную форму имени. +Сводка содержит сеть (включённые интерфейсы с адресами, без loopback), число +обнаруженных USB-устройств, фактическое состояние Tailscale, локальный ответ +SSH и количество разрешённых ключей. Действия открывают нужный подраздел. +Количество ключей не является числом online операторов. Ответ локального +SSH не обещает удалённый вход. Наличие сетевого адреса не обещает связь с Core. + +Во время запроса — загрузка, при ошибке — отсутствие актуальных сведений, +а не зелёная готовность. Tailscale использует один общий hook статуса с той же +проверкой истёкшего сеанса и polling, что и его страница. Старый сеанс +возвращает на штатный вход Node. Диагностические сведения остаются snapshot +с временем получения и ручным обновлением; новая телеметрия не объявляется. + +## Core: Парк → Аппараты + +Название «Аппараты» сохраняется. Текущий catalog в productModel.ts с текстами +о готовности/контрактах заменяется настоящим реестром вместе с backend. +«Состояние контура» не поглощает реестр. Прежнее «Подключение» K1 остаётся +отдельным legacy-путём и не используется скрыто для устройств удалённого БК. + +В шапке реестра плюс «Добавить аппарат». Канонический modal Window содержит +выбор варианта, от которого зависит состав формы: + +1. С бортовым компьютером — название аппарата и код приглашения, созданный + в Node. Успешное подтверждение связывает аппарат с устойчивой identity БК. +2. Прямое подключение / управление с пульта — регистрация через отдельный + реально поддерживаемый адаптер и его поля. Приглашение Node для этого + варианта не требуется; вымышленный БК не создаётся. Телеметрия и позиция + появляются только если их предоставляет данный адаптер. + +Упомянутые владельцем «автономный» и «с пульта» фиксируют разные сценарии +подключения. Само наличие БК не доказывает автономное движение: аппарат с БК +может управляться дистанционно. Наземный беспилотный аппарат (UGV), воздушный +аппарат и другие классы описывают платформу; способ подключения и возможности +автономности не подменяют этот класс. Первая реализуемая форма — «С бортовым +компьютером»; неподдерживаемая прямая интеграция не выдаётся за рабочую. + +После успешного сопряжения аппарат появляется длинной строкой ResourceRow. +Глазик с подписью «Открыть аппарат» открывает постоянную конфигурацию выбранного +аппарата в ApplicationPanel, а не длинную модальную форму. Детали показывают +название/класс, привязанный БК, актуальность связи, состав устройств и доступные +действия. При потере сети строка не исчезает: конфигурация сохранена, +последние данные помечены временем, недоступные команды заблокированы. + +Дерево первой интеграции: аппарат → БК → реально зарегистрированные D455/K1. +Конкретные устройства отображаются по факту появления и регистрации, а не +заранее как два обязательных online-объекта. Открытие сенсора ведёт в +предметную конфигурацию и принятые слои данных, не выполняет команду съёмки. + +## Сопряжение: обязательный полный сценарий P1 + +Node: Система → Mission Core → создать приглашение → скопировать код через UI. +Показываются срок действия и возможность отменить приглашение. Уже привязанный +БК показывает соответствующий Core и состояние связи; повторная привязка +проходит явный сценарий. Секрет не сохраняется в истории URL, логах и отчёте. + +Core: Парк → Аппараты → плюс → с БК → вставить код → подтвердить сведения +выбранного БК → связать. Запись в реестре и подтверждение доверия должны +согласованно переживать обрыв, повторный запрос, рестарт и отмену. + +Сохраняются базовые одноразовость, expiry, pin публичной identity, проверка +версии, ограничение окна сопряжения, один владелец и подтверждённая привязка. +Ни IP/hostname, ни членство в Tailscale не являются identity или authority. +Код не является адресом для произвольного сетевого запроса: private endpoint +и перенаправления проверяются до соединения. После bootstrap Node использует +исходящий аутентифицированный канал с mTLS; UI не передаёт команды через SSH. + +Обязательны replay/expiry/conflict, отмена, partial binding recovery, +revoke/re-pair, ротация сертификатов и запрет потери identity при ремонте. +Жизненные циклы enrollment/connectivity/acquisition, revisions, operations, +capabilities, streams и evidence используют семантику Plugin SDK v0alpha2; +параллельная runtime-онтология для нового экрана не вводится. + +## Устройства и authority + +Управление K1 из Core идёт через runtime выбранного БК, его Linux BLE/network +adapter и K1 Bridge в общей с БК локальной сети. Близость к операторскому Mac +не нужна. Включённый K1 должен быть достижим для БК и подтвердить identity и +совместимый профиль. Состояние «рядом и включён» само по себе не даёт права +команды. Не происходит скрытого fallback в legacy runtime Mac. + +Локальный драйвер владеет command session и сырой записью. Открытие глазика +или reconnect не повторяют START/STOP. Unknown result сохраняется как +неизвестный результат до разрешённого восстановления. Локальная запись +продолжается независимо от окна, Core и WAN. Preview/WebRTC не заменяет raw. + +## Порядок реализации и приёмка + +1. Текущее приращение: перенос существующих страниц в «Систему», обзор БК, + переименование полей, устранение отдельной конфигурации и self-status. + Сборка .deb и GUI-проверка на физическом Mini. +2. Закрыть оставшиеся системные проверки P1 по предыдущему отчёту: + чистая установка, GUI upgrade/repair, reboot и SSH lifecycle без консоли. +3. Реализовать backend сопряжения и реестр Core; одновременно выпустить + рабочие Node «Mission Core», форму добавления и детали аппарата. Проверить + позитивные и негативные сценарии через два интерфейса. +4. P2: драйвер D455, фактические profiles/options/streams, raw recording, + live-слои и открываемая в Core запись. Затем P3: K1 Linux Bridge. +5. Далее совместный P4 и полный P5 восстановления. Вариант прямого аппарата + включается после появления конкретного адаптера и собственной приёмки. + +Используемые exports: AppHeader/HeaderNavigation, ApplicationShell/Panel, +AdminNavigationPanel, SettingsCard, ResourceRow/List, Button/IconButton, Icon, +Select/TextField/TextAreaField, Window/ConfirmationModal, StatusBadge, +ActivityIndicator, ToastStack. Новых визуальных примитивов и копий DG нет. + +Для текущей приёмки: USB доступен из «Системы»; прежних remote/setup маршрутов +нет; имя БК сохраняется; сводка не объявляет подключение камеры по USB готовым +драйвером; Tailscale/SSH открываются из сводки; плюс SSH работает после переноса; +темы/узкое окно и recovery сеанса не нарушены. Для будущего сопряжения: +добавленный аппарат сохраняется после reload/restart и открывает именно +свой БК/устройства, без дубликатов и зависимости от legacy-коннектора. + +## Проверка приращения 0.3.1 + +Через интерфейс изолированной QA-ноды с production API проверены сохранение +нового имени БК и его отображение в shell; USB в «Системе»; переходы из сводки +в USB/Tailscale; недоступность Tailscale и восстановление «В сети»; SSH и его +плюс; открытие формы и Escape с возвратом фокуса. Проверены светлая/тёмная темы, +сводка и её действия в окне 720×800. После рестарта QA-службы именно из обзора +приложение возвращается на штатный вход, без ложного статуса Tailscale. +Typecheck, production build и существующий UI boundary прошли. Это GUI-проверка +перестановки функций, не закрытие приёмки чистой установки или сопряжения. +Аппаратная установка и её результат фиксируются отдельно в отчёте Ops.