feat: introduce device plugin runtime boundary

This commit is contained in:
DCCONSTRUCTIONS
2026-07-16 13:49:10 +03:00
parent 9225227421
commit 27bf7527df
45 changed files with 3748 additions and 1062 deletions
+23 -41
View File
@@ -1,39 +1,38 @@
import type { StatusTone } from "@nodedc/ui-react";
import type { K1Metrics } from "./api";
import type { BackendStatus } from "./useK1Console";
import type {
BackendStatus,
RuntimePhase,
SourceMode,
StreamMetrics,
} from "./core/runtime/contracts";
const phaseLabels: Record<string, string> = {
const phaseLabels: Record<RuntimePhase, string> = {
unconfigured: "Модель не выбрана",
idle: "Ожидание",
scanning: "Поиск Bluetooth",
device_selected: "Устройство выбрано",
provisioning: "Передача настроек Wi‑Fi",
connecting: "Подключение",
configuring: "Настройка устройства",
connected: "Устройство подключено",
starting_live: "Запуск потока",
live: "Поток в реальном времени",
replay: "Повтор записи",
starting: "Запуск потока",
streaming: "Поток в реальном времени",
replaying: "Повтор записи",
stopping: "Остановка",
error: "Ошибка",
};
export function phaseLabel(phase: string | null | undefined): string {
if (!phase) return "Нет состояния";
return phaseLabels[phase] ?? "Неизвестное состояние";
export function phaseLabel(phase: RuntimePhase | null | undefined): string {
return phase ? phaseLabels[phase] : "Нет состояния";
}
export function phaseTone(phase: string | null | undefined): StatusTone {
if (!phase) return "neutral";
export function phaseTone(phase: RuntimePhase | null | undefined): StatusTone {
if (!phase || phase === "unconfigured" || phase === "idle") 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";
if (["connected", "streaming", "replaying"].includes(phase)) return "success";
return "accent";
}
export function backendLabel(status: BackendStatus): string {
return {
unconfigured: "Модель не выбрана",
checking: "Проверка контура",
online: "Контур доступен",
degraded: "Контур ограничен",
@@ -44,33 +43,16 @@ export function backendLabel(status: BackendStatus): string {
export function backendTone(status: BackendStatus): StatusTone {
if (status === "online") return "success";
if (status === "degraded" || status === "checking") return "warning";
if (status === "unconfigured") return "neutral";
return "danger";
}
export function eventStatusLabel(status: string): string {
return {
connecting: "подключение",
open: "подключён",
closed: "закрыт",
error: "ошибка",
}[status] ?? "неизвестно";
}
export function finiteMetric(value: number | null | undefined): number | null {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
export 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.publish_ms),
].filter((value): value is number => value !== null);
return segments.length === 2 ? segments.reduce((total, value) => total + value, 0) : null;
export function pipelineLatency(metrics: StreamMetrics | undefined): number | null {
return finiteMetric(metrics?.latencyMs);
}
export function formatNumber(value: number | null, digits = 1): string {
@@ -81,7 +63,7 @@ export function formatNumber(value: number | null, digits = 1): string {
});
}
export function sourceModeLabel(mode: string | null | undefined): string {
export function sourceModeLabel(mode: SourceMode | null | undefined): string {
if (mode === "live") return "Реальное время";
if (mode === "replay") return "Повтор записи";
if (mode === "idle") return "Ожидание";