diff --git a/apps/control-station/src/App.tsx b/apps/control-station/src/App.tsx index 85e09e5..9936f90 100644 --- a/apps/control-station/src/App.tsx +++ b/apps/control-station/src/App.tsx @@ -26,6 +26,7 @@ import { } from "@nodedc/ui-react"; import { LandingStage } from "./components/LandingStage"; +import { ObservationSessionSelect } from "./components/ObservationSessionSelect"; import { useDevicePluginHost } from "./core/device-plugins/DevicePluginHost"; import { useMissionRuntime } from "./core/runtime/MissionRuntimeContext"; import type { ViewerSettings } from "./core/runtime/contracts"; @@ -752,7 +753,14 @@ export default function App() { ) : null} ) : activeDefinition.kind === "recordings" ? ( - Архив записей + + settleRecordedReplaySwitch(outcome)} + /> ) : activeDefinition.kind === "datasets" ? ( Offline evaluation ) : activeDefinition.kind === "lab-archive" ? ( diff --git a/apps/control-station/src/productModel.ts b/apps/control-station/src/productModel.ts index 544c721..404fcd0 100644 --- a/apps/control-station/src/productModel.ts +++ b/apps/control-station/src/productModel.ts @@ -153,10 +153,10 @@ export const workspaces: WorkspaceDefinition[] = [ { id: "lab-archive", root: "polygon", - label: "Лабораторные работы", - title: "Архив лабораторных работ", + label: "Лабораторные контуры", + title: "Лабораторные контуры", eyebrow: "ТЕСТОВЫЙ КОНТУР / ЛАБОРАТОРИЯ", - description: "Конфигурации, ход эксперимента и визуальные результаты принятых лабораторных работ.", + description: "Профили конфигураций, лабораторные работы и их воспроизводимые результаты.", icon: "clipboard", kind: "lab-archive", groups: [], diff --git a/apps/control-station/src/styles/workspaces.css b/apps/control-station/src/styles/workspaces.css index 6641aba..39eea4d 100644 --- a/apps/control-station/src/styles/workspaces.css +++ b/apps/control-station/src/styles/workspaces.css @@ -2385,18 +2385,17 @@ .recordings-workspace, .lab-archive-workspace, -.recordings-workspace__viewer, .lab-result-surface { display: grid; gap: 0.85rem; } -.recordings-workspace__viewer, -.lab-result-surface { - min-height: 30rem; +.recordings-workspace { + width: 100%; + height: 100%; + min-height: 34rem; } -.recordings-workspace__viewer > header, .lab-result-surface > header { display: flex; align-items: center; @@ -2404,102 +2403,346 @@ padding: 0 0.2rem; } -.recordings-workspace__viewer > header strong, .lab-result-surface > header strong { color: var(--nodedc-text-secondary); font-size: 0.7rem; } -.recordings-workspace__empty { +.lab-archive-workspace { + padding-bottom: 1rem; +} + +.laboratory-selector { display: grid; - min-height: 27rem; - place-items: center; - align-content: center; - gap: 0.5rem; - border-radius: 1rem; - background: rgb(0 0 0 / 0.24); - color: var(--nodedc-text-muted); - text-align: center; -} - -.recordings-workspace__empty strong { - color: var(--nodedc-text-secondary); - font-size: 0.75rem; -} - -.recordings-workspace__empty p { - margin: 0; - font-size: 0.63rem; -} - -.lab-configuration { - display: flex; + grid-template-columns: minmax(0, 1fr) minmax(18rem, 0.38fr); align-items: center; - justify-content: space-between; - gap: 1rem; + gap: 1.4rem; border-radius: 1rem; background: rgb(255 255 255 / 0.025); padding: 1rem; } -.lab-configuration h2, -.lab-configuration p { +.laboratory-selector h2, +.laboratory-selector p { margin: 0; } -.lab-configuration h2 { +.laboratory-selector h2 { margin-top: 0.25rem; color: var(--nodedc-text-primary); font-size: 1rem; } -.lab-configuration p { - max-width: 55rem; +.laboratory-selector p { + max-width: 60rem; margin-top: 0.35rem; color: var(--nodedc-text-muted); - font-size: 0.66rem; + font-size: 0.64rem; line-height: 1.5; } -.lab-primary-result { +.laboratory-selector label { display: grid; + gap: 0.35rem; } -.lab-primary-result button { - display: grid; - grid-template-columns: auto minmax(0, 1fr); - gap: 0.3rem 0.75rem; - border: 0; - border-radius: 0.85rem; - background: rgb(255 255 255 / 0.025); - color: var(--nodedc-text-secondary); - padding: 0.8rem 1rem; - text-align: left; - cursor: pointer; -} - -.lab-primary-result button[data-active="true"] { - background: rgb(255 255 255 / 0.065); -} - -.lab-primary-result button > span { - grid-row: 1 / 3; - align-self: center; - color: var(--nodedc-accent); - font-size: 0.65rem; - font-weight: 700; -} - -.lab-primary-result button > strong { - color: var(--nodedc-text-primary); - font-size: 0.72rem; -} - -.lab-primary-result button > small { +.laboratory-selector label > span { color: var(--nodedc-text-muted); + font-size: 0.55rem; + font-weight: 650; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.laboratory-selector select { + width: 100%; + min-width: 0; + border: 0; + border-radius: 999px; + outline: 0; + background: rgb(255 255 255 / 0.075); + color: var(--nodedc-text-primary); + padding: 0.58rem 0.85rem; + font: inherit; + font-size: 0.65rem; + font-weight: 620; +} + +.laboratory-selector select:focus-visible { + background: rgb(255 255 255 / 0.12); +} + +.laboratory-selector select:disabled { + opacity: 0.45; +} + +.laboratory-work-output { + display: grid; + gap: 0.85rem; +} + +.laboratory-task, +.laboratory-result-summary, +.laboratory-visual-result { + border-radius: 1rem; + background: rgb(255 255 255 / 0.025); + padding: 1rem; +} + +.laboratory-task > header, +.laboratory-result-summary > header, +.laboratory-visual-result > header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1.5rem; +} + +.laboratory-task h2, +.laboratory-task p, +.laboratory-task dl, +.laboratory-result-summary h2, +.laboratory-result-summary p, +.laboratory-visual-result h2, +.laboratory-visual-result p { + margin: 0; +} + +.laboratory-task h2, +.laboratory-result-summary h2, +.laboratory-visual-result h2 { + margin-top: 0.3rem; + color: var(--nodedc-text-primary); + font-size: 1rem; + letter-spacing: -0.025em; +} + +.laboratory-task p, +.laboratory-result-summary > p, +.laboratory-visual-result header p { + max-width: 66rem; + margin-top: 0.38rem; + color: var(--nodedc-text-muted); + font-size: 0.63rem; + line-height: 1.55; +} + +.laboratory-task dl { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 0.4rem; + margin-top: 0.9rem; +} + +.laboratory-task dl > div, +.laboratory-result-metrics > div { + display: grid; + gap: 0.25rem; + border-radius: 0.75rem; + background: rgb(255 255 255 / 0.035); + padding: 0.7rem; +} + +.laboratory-task dt, +.laboratory-task dd { + margin: 0; +} + +.laboratory-task dt, +.laboratory-result-metrics span, +.laboratory-result-metrics small { + color: var(--nodedc-text-muted); + font-size: 0.54rem; +} + +.laboratory-task dd, +.laboratory-result-metrics strong { + overflow: hidden; + color: var(--nodedc-text-primary); + font-size: 0.7rem; + font-weight: 660; + text-overflow: ellipsis; + white-space: nowrap; +} + +.lab-result-surface { + min-height: 36rem; +} + +.lab-result-surface .spatial-workspace { + min-height: 34rem; +} + +.laboratory-result-pending { + display: grid; + min-height: 34rem; + place-items: center; + align-content: center; + gap: 0.5rem; + border-radius: 1rem; + background: #06070a; + color: var(--nodedc-text-muted); + text-align: center; +} + +.laboratory-result-pending strong { + color: var(--nodedc-text-secondary); + font-size: 0.75rem; +} + +.laboratory-result-pending p { + max-width: 34rem; + margin: 0; + font-size: 0.62rem; +} + +.laboratory-visual-result { + display: grid; + min-height: 34rem; + align-content: start; + gap: 1rem; + background: #08090c; +} + +.laboratory-e29-chart { + display: grid; + gap: 0.85rem; + border-radius: 0.9rem; + background: rgb(255 255 255 / 0.025); + padding: 1rem; +} + +.laboratory-e29-chart__bar { + display: flex; + height: 1.1rem; + overflow: hidden; + border-radius: 999px; + background: rgb(255 255 255 / 0.04); +} + +.laboratory-e29-chart__bar i[data-status="agree"], +.laboratory-e29-chart__legend i[data-status="agree"] { + background: rgb(var(--nodedc-success-rgb)); +} + +.laboratory-e29-chart__bar i[data-status="camera"], +.laboratory-e29-chart__legend i[data-status="camera"] { + background: rgb(var(--nodedc-accent-rgb)); +} + +.laboratory-e29-chart__bar i[data-status="conflict"], +.laboratory-e29-chart__legend i[data-status="conflict"] { + min-width: 0.25rem; + background: rgb(var(--nodedc-warning-rgb)); +} + +.laboratory-e29-chart__legend { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 0.5rem; +} + +.laboratory-e29-chart__legend > div { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: 0.25rem 0.45rem; + border-radius: 0.75rem; + background: rgb(255 255 255 / 0.025); + padding: 0.7rem; +} + +.laboratory-e29-chart__legend i { + width: 0.5rem; + height: 0.5rem; + border-radius: 50%; +} + +.laboratory-e29-chart__legend span, +.laboratory-e29-chart__legend small { + color: var(--nodedc-text-muted); + font-size: 0.56rem; +} + +.laboratory-e29-chart__legend strong { + color: var(--nodedc-text-primary); + font-size: 0.85rem; +} + +.laboratory-e29-chart__legend small { + grid-column: 2 / 4; +} + +.laboratory-e29-evidence { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.6rem; +} + +.laboratory-e29-evidence article { + display: grid; + align-content: start; + gap: 0.45rem; + border-radius: 0.9rem; + background: rgb(255 255 255 / 0.025); + padding: 1rem; +} + +.laboratory-e29-evidence strong { + color: var(--nodedc-text-primary); + font-size: 1.4rem; +} + +.laboratory-e29-evidence p { + margin: 0; + color: var(--nodedc-text-muted); + font-size: 0.62rem; + line-height: 1.5; +} + +.laboratory-e29-evidence article > div { + display: grid; + gap: 0.3rem; +} + +.laboratory-e29-evidence article > div span { + border-radius: 0.6rem; + background: rgb(255 255 255 / 0.03); + color: var(--nodedc-text-secondary); + padding: 0.45rem 0.55rem; font-size: 0.58rem; } +.laboratory-result-summary { + display: grid; + gap: 0.85rem; +} + +.laboratory-result-metrics { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 0.4rem; +} + +.laboratory-result-metrics > div { + min-width: 0; +} + +.laboratory-result-metrics strong { + font-size: 0.9rem; +} + +@media (max-width: 1100px) { + .laboratory-selector { + grid-template-columns: minmax(0, 1fr); + } + + .laboratory-task dl, + .laboratory-result-metrics { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + .dataset-entry { display: grid; grid-template-columns: minmax(20rem, 1.2fr) minmax(22rem, 0.8fr); diff --git a/apps/control-station/src/workspaces/LidarQualityWorkspace.tsx b/apps/control-station/src/workspaces/LidarQualityWorkspace.tsx index a9e1954..bb815d3 100644 --- a/apps/control-station/src/workspaces/LidarQualityWorkspace.tsx +++ b/apps/control-station/src/workspaces/LidarQualityWorkspace.tsx @@ -61,9 +61,11 @@ function errorMessage(error: unknown): string { export function LidarQualityWorkspace({ deviceLabel, onOpenObservation, + embedded = false, }: { deviceLabel: string | null; onOpenObservation: () => void; + embedded?: boolean; }) { const [catalog, setCatalog] = useState(null); const [detail, setDetail] = useState(null); @@ -248,29 +250,31 @@ export function LidarQualityWorkspace({ ) : ( <> -
-
- ВЫБРАННЫЙ СЕНСОР - {deviceLabel ?? "Архивный источник"} · LiDAR - - Запись {fieldReview?.sessionId ?? detail.pack.sessionId} - -
-
- Диагностическая запись -

- Данные воспроизводятся и доступны для визуального разбора. - Физическая точность сенсора этой записью не аттестована. -

-
- -
+ {!embedded ? ( +
+
+ ВЫБРАННЫЙ СЕНСОР + {deviceLabel ?? "Архивный источник"} · LiDAR + + Запись {fieldReview?.sessionId ?? detail.pack.sessionId} + +
+
+ Диагностическая запись +

+ Данные воспроизводятся и доступны для визуального разбора. + Физическая точность сенсора этой записью не аттестована. +

+
+ +
+ ) : null}
diff --git a/apps/control-station/src/workspaces/Workspaces.tsx b/apps/control-station/src/workspaces/Workspaces.tsx index b498422..97745f3 100644 --- a/apps/control-station/src/workspaces/Workspaces.tsx +++ b/apps/control-station/src/workspaces/Workspaces.tsx @@ -14,7 +14,6 @@ import { } from "@nodedc/ui-react"; import { - ObservationSessionArchive, type ObservationSessionReplayCallbacks, } from "../components/ObservationSessionSelect"; import { @@ -24,7 +23,11 @@ import { } from "../components/ObservationSources"; import { ObservationTimeline } from "../components/ObservationTimeline"; import { FloatingObservationWindow } from "../components/FloatingObservationWindow"; -import type { ObservationSessionReplayLaunch } from "../core/observation/sessionArchive"; +import type { + ObservationSessionReplayLaunch, + ObservationSessionSummary, +} from "../core/observation/sessionArchive"; +import { useObservationSessions } from "../core/observation/useObservationSessions"; import type { RecordedSessionAdmissionController } from "../core/observation/useRecordedSessionAdmission"; import type { RecordedAdmissionPhase, @@ -1176,84 +1179,447 @@ function CatalogWorkspace({ definition }: WorkspaceRendererProps) { function RecordingsWorkspace(props: WorkspaceRendererProps) { return (
- - {props.recordedReplay ? ( -
-
- ВОСПРОИЗВЕДЕНИЕ - Выбранная пространственная запись -
- -
- ) : ( -
- - Выберите сохранённую сессию -

Viewer появится здесь; эфир в разделе «Наблюдение» останется неизменным.

-
- )} +
); } -function LabArchiveWorkspace(props: WorkspaceRendererProps) { - const [surface, setSurface] = useState<"lidar-e29" | "replay">( - props.recordedReplay ? "replay" : "lidar-e29", +type LaboratoryProfileId = "sensor-fusion" | "published-perception"; +type LaboratoryWorkId = "e28-local-surface" | "e29-camera-geometry" | `session:${string}`; + +interface LaboratoryOption { + id: T; + label: string; +} + +function LaboratorySelector({ + eyebrow, + title, + description, + label, + value, + options, + disabled = false, + onChange, +}: { + eyebrow: string; + title: string; + description: string; + label: string; + value: T; + options: readonly LaboratoryOption[]; + disabled?: boolean; + onChange: (value: T) => void; +}) { + return ( +
+
+ {eyebrow} +

{title}

+

{description}

+
+ +
); - const replayActions: WorkspaceRendererProps["sessionArchive"] = { - ...props.sessionArchive, - onReplayAccepted: async (session, launch) => { - await props.sessionArchive.onReplayAccepted?.(session, launch); - setSurface("replay"); - }, - }; +} + +function LaboratoryTask({ + title, + description, + status, + facts, +}: { + title: string; + description: string; + status: string; + facts: readonly { label: string; value: string }[]; +}) { + return ( +
+
+
+ ОПИСАНИЕ ЛАБОРАТОРНОЙ РАБОТЫ +

{title}

+

{description}

+
+ {status} +
+
+ {facts.map((fact) => ( +
+
{fact.label}
+
{fact.value}
+
+ ))} +
+
+ ); +} + +function E29LaboratoryResult({ rigLabel }: { rigLabel: string }) { + const semanticTotal = 19_625; + const statuses = [ + { id: "agree", label: "Камера + геометрия", value: 6_341 }, + { id: "camera", label: "Только камера", value: 13_246 }, + { id: "conflict", label: "Конфликт", value: 38 }, + ] as const; + const conflictEpisodes = [ + "track 115 · 69,799–71,398 с", + "track 470 · 215,324–218,708 с", + "track 679 · 282,569–283,281 с", + "track 1011 · 354,937–355,242 с", + "track 1276 · 402,606–402,995 с", + ]; return ( -
-
-
- ОБЪЕКТ И КОНФИГУРАЦИЯ ИССЛЕДОВАНИЯ -

Текущий профиль · camera + LiDAR

-

- Объект и точная конфигурация берутся из LAB-provenance выбранной записи. - Исходный сенсор остаётся read-only, вычисление выполняется на worker D. -

+ <> + + +
+
+
+ ВИЗУАЛЬНЫЙ РЕЗУЛЬТАТ +

Покрытие семантических наблюдений геометрией

+

+ Это покрытие одного воспроизводимого replay, а не accuracy и не допуск + планировщика. Конфликты сохранены для ручного покадрового разбора. +

+
+ 16 конфликтных эпизодов +
+ +
+
+ {statuses.map((status) => ( + + ))} +
+
+ {statuses.map((status) => ( +
+ + {status.label} + {status.value.toLocaleString("ru-RU")} + {((status.value / semanticTotal) * 100).toLocaleString("ru-RU", { + maximumFractionDigits: 2, + })}% +
+ ))} +
+
+ +
+
+ НЕЗАВИСИМАЯ ГЕОМЕТРИЯ + 21 321 компонент +

+ Незасемантизированные занятые компоненты сохранены отдельным слоем: + им не назначается выдуманный класс и они не считаются свободным местом. +

+
+
+ КОНФЛИКТЫ ДЛЯ РАЗБОРА +
+ {conflictEpisodes.map((episode) => {episode})} +
+
- Активная серия
-
- +
+
+
+ РЕЗУЛЬТАТ И ВЫВОД +

Архитектурная схема подтверждена, safety-gate не пройден

+
+ Replay завершён +
+
+
Поддержка геометрией6 34132,31% current observations
+
Camera-only13 246Семантика сохранена
+
Postprocess p952,517 мс8,467 с полный build
+
Geometry-only21 321Занято / класс неизвестен
+
+

+ Следующий gate — покадровый разбор конфликтов, camera-only пробелов и + geometry-only компонентов, затем source-paced shadow с deadline и staleness. +

+ + ); +} - +function formatLaboratoryMetric(value: unknown): string { + if (typeof value === "boolean") return value ? "Да" : "Нет"; + if (typeof value === "number") { + return value.toLocaleString("ru-RU", { maximumFractionDigits: 3 }); + } + return String(value); +} + +function laboratoryMetricLabel(key: string): string { + return key + .replace(/_p95_ms$/, " · p95 мс") + .replaceAll("_", " ") + .replace(/^./, (value) => value.toLocaleUpperCase("ru-RU")); +} + +function laboratorySessionTitle(session: ObservationSessionSummary): string { + const labPrefix = session.lab ? `${session.lab.labId} · ` : ""; + return labPrefix && session.label.startsWith(labPrefix) + ? session.label.slice(labPrefix.length) + : session.label; +} + +function PublishedLaboratoryResult({ + props, + session, + loading, + error, +}: { + props: WorkspaceRendererProps; + session: ObservationSessionSummary; + loading: boolean; + error: string | null; +}) { + const lab = session.lab; + const metrics = Object.entries(lab?.provenance ?? {}) + .filter(([key, value]) => ( + (typeof value === "number" || typeof value === "boolean") + && !key.includes("sha256") + && !key.includes("authority") + )) + .slice(0, 8); + const replayReady = props.recordedReplay?.sessionId === session.id; + + return ( + <> +
ВИЗУАЛЬНОЕ ДОКАЗАТЕЛЬСТВО - - {surface === "replay" && props.recordedReplay - ? "Запись выбранной лабораторной работы" - : "LAB E29 · результат L2.6"} - + Исходная запись выбранной лабораторной работы
- {surface === "replay" && props.recordedReplay ? ( + {replayReady ? ( ) : ( - props.navigation.openView("spatial-scene")} - /> +
+ {loading ?
)}
+ +
+
+
+ ЗАФИКСИРОВАННЫЕ МЕТРИКИ +

Результат из LAB-provenance

+
+ + {lab?.provenance.benchmark_passed === true ? "Benchmark passed" : "Требует разбора"} + +
+
+ {metrics.length ? metrics.map(([key, value]) => ( +
+ {laboratoryMetricLabel(key)} + {formatLaboratoryMetric(value)} + Immutable provenance +
+ )) : ( +
+ Метрики + Не опубликованы + Доступна исходная запись +
+ )} +
+
+ + ); +} + +function LabArchiveWorkspace(props: WorkspaceRendererProps) { + const [profileId, setProfileId] = useState("sensor-fusion"); + const [workId, setWorkId] = useState("e28-local-surface"); + const sessions = useObservationSessions({ + limit: 100, + replayEnabled: props.sessionArchive.blockedReason === null, + onReplayBegin: props.sessionArchive.onReplayBegin, + onReplayAccepted: props.sessionArchive.onReplayAccepted, + onReplaySettled: props.sessionArchive.onReplaySettled, + }); + const publishedWorks = useMemo( + () => sessions.items.filter((session) => session.lab !== null), + [sessions.items], + ); + const rigLabel = useMemo(() => { + if (props.deviceLabel) return props.deviceLabel; + const coordinateFrame = publishedWorks + .map((session) => session.lab?.provenance.coordinate_frame) + .find((value) => typeof value === "string" && value.trim()); + const sensorToken = typeof coordinateFrame === "string" + ? coordinateFrame.split("-")[0]?.trim() + : ""; + return sensorToken ? sensorToken.toLocaleUpperCase("ru-RU") : "Сенсорный риг"; + }, [props.deviceLabel, publishedWorks]); + const profiles: readonly LaboratoryOption[] = [ + { + id: "sensor-fusion", + label: `${rigLabel} · камера + LiDAR · worker D`, + }, + { + id: "published-perception", + label: `${rigLabel} · опубликованный perception pipeline`, + }, + ]; + const workOptions: readonly LaboratoryOption[] = + profileId === "sensor-fusion" + ? [ + { id: "e28-local-surface", label: "LAB E28 · локальная поверхность L2.6" }, + { id: "e29-camera-geometry", label: "LAB E29 · camera-first + geometry" }, + ] + : publishedWorks.map((session) => ({ + id: `session:${session.id}` as const, + label: `${session.lab?.labId ?? "LAB"} · ${laboratorySessionTitle(session)}`, + })); + const selectedSessionId = workId.startsWith("session:") + ? workId.slice("session:".length) + : null; + const selectedSession = selectedSessionId + ? publishedWorks.find((session) => session.id === selectedSessionId) ?? null + : null; + + useEffect(() => { + if (profileId !== "published-perception" || workId.startsWith("session:")) return; + const first = publishedWorks[0]; + if (!first) return; + setWorkId(`session:${first.id}`); + }, [profileId, publishedWorks, workId]); + + const selectProfile = (next: LaboratoryProfileId) => { + setProfileId(next); + if (next === "sensor-fusion") { + setWorkId("e28-local-surface"); + return; + } + const first = publishedWorks[0]; + if (!first) return; + const nextWork = `session:${first.id}` as const; + setWorkId(nextWork); + void sessions.replay(first.id); + }; + + const selectWork = (next: LaboratoryWorkId) => { + setWorkId(next); + if (!next.startsWith("session:")) return; + void sessions.replay(next.slice("session:".length)); + }; + + return ( +
+ profile.id === profileId)?.label ?? rigLabel} + description="Профиль фиксирует объект исследования, сенсорные модули и вычислительный контур. Исходные данные остаются read-only; профиль объединяет серию сопоставимых лабораторных работ." + label="Профиль" + value={profileId} + options={profiles} + onChange={selectProfile} + /> + + work.id === workId)?.label ?? "Работа не выбрана"} + description="Выберите один зафиксированный эксперимент. Ниже откроются его задача, полноразмерное визуальное доказательство и структурированный результат." + label="Работа" + value={workId} + options={workOptions} + disabled={workOptions.length === 0} + onChange={selectWork} + /> + +
+ {workId === "e28-local-surface" ? ( + <> + + props.navigation.openView("spatial-scene")} + /> + + ) : workId === "e29-camera-geometry" ? ( + + ) : selectedSession ? ( + + ) : ( +
+ + В этом профиле пока нет опубликованных работ +

Работы появятся после публикации проверенного LAB-provenance.

+
+ )} +
); } diff --git a/apps/control-station/test/observationSessions.test.mjs b/apps/control-station/test/observationSessions.test.mjs index dbb75fa..bf00869 100644 --- a/apps/control-station/test/observationSessions.test.mjs +++ b/apps/control-station/test/observationSessions.test.mjs @@ -172,6 +172,31 @@ test("opened archive is named in the scene header and trash hover has no pill", assert.doesNotMatch(acquisitionPipeline, /Индикатор постоянно зелёный/); }); +test("data recordings keep the compact session dropdown and laboratory results stay separate", async () => { + const appSource = await readFile(new URL("../src/App.tsx", import.meta.url), "utf8"); + const workspaceSource = await readFile( + new URL("../src/workspaces/Workspaces.tsx", import.meta.url), + "utf8", + ); + const productSource = await readFile( + new URL("../src/productModel.ts", import.meta.url), + "utf8", + ); + const recordingsSource = workspaceSource.slice( + workspaceSource.indexOf("function RecordingsWorkspace"), + workspaceSource.indexOf("type LaboratoryProfileId"), + ); + + assert.match(appSource, /activeDefinition\.kind === "recordings"[\s\S]*/); + assert.doesNotMatch(recordingsSource, /ObservationSessionArchive/); + assert.match(productSource, /label: "Лабораторные контуры"/); + assert.match(workspaceSource, /ПРОФИЛЬ ЛАБОРАТОРНОГО КОНТУРА/); + assert.match(workspaceSource, /ЛАБОРАТОРНАЯ РАБОТА/); + assert.match(workspaceSource, /e28-local-surface/); + assert.match(workspaceSource, /e29-camera-geometry/); +}); + test("session catalog exposes authoritative background preparation state", () => { const catalog = decodeObservationSessionCatalog({ items: [session({ diff --git a/docs/09_OBSERVATION_SESSIONS.md b/docs/09_OBSERVATION_SESSIONS.md index 073544d..b1f2115 100644 --- a/docs/09_OBSERVATION_SESSIONS.md +++ b/docs/09_OBSERVATION_SESSIONS.md @@ -24,10 +24,12 @@ fully usable with only their available modalities. 3. Stop acquisition normally, or allow the local service to recover an unexpected interruption on its next start. Session recording is automatic; the disk action is not required. -4. Open **Данные → Сессии и записи**. The inline archive shows up to 100 - indexed runs, their date, duration, state, modalities and background - preparation state. Opening an archive never replaces the live source owned - by **Наблюдение**. +4. Open **Данные → Сессии и записи**. The compact **Сохранённые сессии** + dropdown shows up to 100 indexed runs, their date, duration, state, + modalities and background preparation state. The page itself remains one + full-size spatial viewer; the catalog is not expanded into an inline list. + Opening an archive never replaces the live source owned by + **Наблюдение**. 5. Choose a replayable run. Opening a replay never performs conversion in the request. A ready recording opens immediately; otherwise the client receives HTTP 202, keeps the current scene mounted and polls the preparation status. @@ -130,8 +132,10 @@ on deletion. The visual result is resolved by the LAB session id, so `LAB E19`, `LAB E21` and the derived temporal comparison `LAB E22` can coexist over `RAVNOVES00` without “latest accepted result” replacing an earlier experiment. The operator -can open any row in **Данные → Сессии и записи** or the corresponding LAB row -in **Тестировочный контур** and use the same +can open any row in **Данные → Сессии и записи** or choose the corresponding +published LAB work through **Тестировочный контур → Лабораторные контуры**. +The test surface presents LAB works through `profile → work`, never as a copy +of the saved-session list. A recorded LAB work then uses the same **Объекты 2D / Сегментация / Кубы 3D** controls. First publication must also warm the result-specific Rerun overlay; later opens reuse that verified cache and do not rerun AI inference. diff --git a/docs/13_LIDAR_WORKER_PRODUCT_AND_ROADMAP.md b/docs/13_LIDAR_WORKER_PRODUCT_AND_ROADMAP.md index 9fa1f9f..af06a60 100644 --- a/docs/13_LIDAR_WORKER_PRODUCT_AND_ROADMAP.md +++ b/docs/13_LIDAR_WORKER_PRODUCT_AND_ROADMAP.md @@ -432,7 +432,8 @@ Dataset expansion is no longer the next gate. The implemented `missioncore.k1-local-surface/v1` derivative is reproducible through `experiments/perception/run_k1_local_surface.py` and is exposed read-only through `GET /api/v1/lidar/local-surfaces` plus the bound frame, -timeline and review endpoints. **Тестировочный контур → Лабораторные работы** +timeline and review endpoints. **Тестировочный контур → Лабораторные контуры +→ LAB E28** reuses the five RAVNOVES00 scene selectors and also exposes a clickable timeline over the complete recording. The selected source frame shows observed surface, observed diff --git a/docs/14_LIDAR_DATASET_GATEWAY.md b/docs/14_LIDAR_DATASET_GATEWAY.md index 97eca56..c0ee877 100644 --- a/docs/14_LIDAR_DATASET_GATEWAY.md +++ b/docs/14_LIDAR_DATASET_GATEWAY.md @@ -61,7 +61,7 @@ Implemented now: lower-cell coordinates, signed residual and derived inlier mask are content-bound and visible as a read-only 3D overlay; - a provider-neutral read-only local-surface laboratory result in - **Тестировочный контур → Лабораторные работы**, synchronized to the five existing + **Тестировочный контур → Лабораторные контуры → LAB E28**, synchronized to the five existing RAVNOVES00 scene selectors and a clickable complete-recording timeline; - a complete recorded-host-paced 1× worker replay through authenticated ingress, K1 normalization, point/pose binding and a bounded latest-wins @@ -114,8 +114,13 @@ The gateway and current local-surface work are not live device diagnostics. inputs. Opening one dataset exposes its source fragments, sparse annotated scans, visual overlays and useful analysis inside the expanded source card. Opening another source closes the previous one. -- **Тестировочный контур → Лабораторные работы** owns immutable experiment - configurations, accepted LAB instances and visual conclusions such as E29. +- **Тестировочный контур → Лабораторные контуры** owns immutable experiment + configurations and results. The first selector chooses a configuration + profile, the second chooses one LAB work inside it, and the output uses one + reusable `task → full visual evidence → structured result` template. + LAB E28 owns the L2.6 local-surface viewer and review queue; LAB E29 owns the + camera-first semantic/geometry result. Published E19–E26 recordings remain a + separate profile and are not duplicated into the saved-session catalog UI. - A future **Парк** diagnostics surface must require a connected device and a dedicated live diagnostic contract. It cannot reuse an offline replay report. - Internal replay-run identity, provenance and immutable artifacts remain a diff --git a/docs/adr/0022-operator-surface-ownership.md b/docs/adr/0022-operator-surface-ownership.md index e184091..d960918 100644 --- a/docs/adr/0022-operator-surface-ownership.md +++ b/docs/adr/0022-operator-surface-ownership.md @@ -34,7 +34,10 @@ to **Данные → Датасеты**. They do not recreate the previous Poly page. Saved session replay is mounted only in **Данные → Сессии и записи** or from a -LAB entry. Leaving either archive returns **Наблюдение → Пространственная +selected recorded LAB work. **Данные → Сессии и записи** keeps the original +compact dropdown in the panel header and one full-size spatial viewer; it does +not expand the catalog into the content column. Leaving either archive returns +**Наблюдение → Пространственная сцена** to the runtime-published live source. A selected archive cannot leak into the live observation surface. @@ -42,11 +45,21 @@ Dataset sources use a single-open accordion. Expanding one source mounts its viewer and analysis inside that source card, pushes later sources down, and closes the previously expanded source. -The laboratory archive reads immutable LAB instances from the existing -observation-session catalog. The E29 local-surface result remains a read-only -laboratory surface and is no longer presented as live LiDAR diagnostics. A -future device-diagnostics workspace must require an attached device and its own -diagnostic contract. +The laboratory surface is a two-level catalog: + +1. a **laboratory contour profile** fixes the tested rig, modules and worker; +2. a **laboratory work** selects one immutable result inside that profile. + +The selected work opens one reusable output structure: task and configuration, +full-size visual evidence, then structured metrics, errors, timing and +conclusion. LAB E28 owns the L2.6 local-surface viewer, complete-recording +timeline and review queue. LAB E29 owns the camera-first semantic/geometry +coverage result. Recorded E19–E26 LAB instances are exposed through a separate +published-perception profile backed by the existing observation-session +catalog. They are not rendered as a second saved-session archive. + +Neither E28 nor E29 is live device diagnostics. A future device-diagnostics +workspace must require an attached device and its own diagnostic contract. Contour health reads only live contracts: