fix(control-station): separate lab contours from recordings
This commit is contained in:
@@ -26,6 +26,7 @@ import {
|
|||||||
} from "@nodedc/ui-react";
|
} from "@nodedc/ui-react";
|
||||||
|
|
||||||
import { LandingStage } from "./components/LandingStage";
|
import { LandingStage } from "./components/LandingStage";
|
||||||
|
import { ObservationSessionSelect } from "./components/ObservationSessionSelect";
|
||||||
import { useDevicePluginHost } from "./core/device-plugins/DevicePluginHost";
|
import { useDevicePluginHost } from "./core/device-plugins/DevicePluginHost";
|
||||||
import { useMissionRuntime } from "./core/runtime/MissionRuntimeContext";
|
import { useMissionRuntime } from "./core/runtime/MissionRuntimeContext";
|
||||||
import type { ViewerSettings } from "./core/runtime/contracts";
|
import type { ViewerSettings } from "./core/runtime/contracts";
|
||||||
@@ -752,7 +753,14 @@ export default function App() {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : activeDefinition.kind === "recordings" ? (
|
) : activeDefinition.kind === "recordings" ? (
|
||||||
<StatusBadge tone="accent">Архив записей</StatusBadge>
|
<ObservationSessionSelect
|
||||||
|
disabled={runtime.pendingAction !== null || sourceSwitchBlocked}
|
||||||
|
blockedReason={sourceSwitchBlockedReason}
|
||||||
|
onReplayBegin={beginRecordedReplaySwitch}
|
||||||
|
onReplayAccepted={acceptRecordedReplay}
|
||||||
|
onReplaySettled={(_session, outcome) =>
|
||||||
|
settleRecordedReplaySwitch(outcome)}
|
||||||
|
/>
|
||||||
) : activeDefinition.kind === "datasets" ? (
|
) : activeDefinition.kind === "datasets" ? (
|
||||||
<StatusBadge tone="neutral">Offline evaluation</StatusBadge>
|
<StatusBadge tone="neutral">Offline evaluation</StatusBadge>
|
||||||
) : activeDefinition.kind === "lab-archive" ? (
|
) : activeDefinition.kind === "lab-archive" ? (
|
||||||
|
|||||||
@@ -153,10 +153,10 @@ export const workspaces: WorkspaceDefinition[] = [
|
|||||||
{
|
{
|
||||||
id: "lab-archive",
|
id: "lab-archive",
|
||||||
root: "polygon",
|
root: "polygon",
|
||||||
label: "Лабораторные работы",
|
label: "Лабораторные контуры",
|
||||||
title: "Архив лабораторных работ",
|
title: "Лабораторные контуры",
|
||||||
eyebrow: "ТЕСТОВЫЙ КОНТУР / ЛАБОРАТОРИЯ",
|
eyebrow: "ТЕСТОВЫЙ КОНТУР / ЛАБОРАТОРИЯ",
|
||||||
description: "Конфигурации, ход эксперимента и визуальные результаты принятых лабораторных работ.",
|
description: "Профили конфигураций, лабораторные работы и их воспроизводимые результаты.",
|
||||||
icon: "clipboard",
|
icon: "clipboard",
|
||||||
kind: "lab-archive",
|
kind: "lab-archive",
|
||||||
groups: [],
|
groups: [],
|
||||||
|
|||||||
@@ -2385,18 +2385,17 @@
|
|||||||
|
|
||||||
.recordings-workspace,
|
.recordings-workspace,
|
||||||
.lab-archive-workspace,
|
.lab-archive-workspace,
|
||||||
.recordings-workspace__viewer,
|
|
||||||
.lab-result-surface {
|
.lab-result-surface {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.85rem;
|
gap: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recordings-workspace__viewer,
|
.recordings-workspace {
|
||||||
.lab-result-surface {
|
width: 100%;
|
||||||
min-height: 30rem;
|
height: 100%;
|
||||||
|
min-height: 34rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recordings-workspace__viewer > header,
|
|
||||||
.lab-result-surface > header {
|
.lab-result-surface > header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -2404,102 +2403,346 @@
|
|||||||
padding: 0 0.2rem;
|
padding: 0 0.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recordings-workspace__viewer > header strong,
|
|
||||||
.lab-result-surface > header strong {
|
.lab-result-surface > header strong {
|
||||||
color: var(--nodedc-text-secondary);
|
color: var(--nodedc-text-secondary);
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recordings-workspace__empty {
|
.lab-archive-workspace {
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.laboratory-selector {
|
||||||
display: grid;
|
display: grid;
|
||||||
min-height: 27rem;
|
grid-template-columns: minmax(0, 1fr) minmax(18rem, 0.38fr);
|
||||||
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;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
gap: 1.4rem;
|
||||||
gap: 1rem;
|
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
background: rgb(255 255 255 / 0.025);
|
background: rgb(255 255 255 / 0.025);
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lab-configuration h2,
|
.laboratory-selector h2,
|
||||||
.lab-configuration p {
|
.laboratory-selector p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lab-configuration h2 {
|
.laboratory-selector h2 {
|
||||||
margin-top: 0.25rem;
|
margin-top: 0.25rem;
|
||||||
color: var(--nodedc-text-primary);
|
color: var(--nodedc-text-primary);
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lab-configuration p {
|
.laboratory-selector p {
|
||||||
max-width: 55rem;
|
max-width: 60rem;
|
||||||
margin-top: 0.35rem;
|
margin-top: 0.35rem;
|
||||||
color: var(--nodedc-text-muted);
|
color: var(--nodedc-text-muted);
|
||||||
font-size: 0.66rem;
|
font-size: 0.64rem;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lab-primary-result {
|
.laboratory-selector label {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
gap: 0.35rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lab-primary-result button {
|
.laboratory-selector label > span {
|
||||||
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 {
|
|
||||||
color: var(--nodedc-text-muted);
|
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;
|
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 {
|
.dataset-entry {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(20rem, 1.2fr) minmax(22rem, 0.8fr);
|
grid-template-columns: minmax(20rem, 1.2fr) minmax(22rem, 0.8fr);
|
||||||
|
|||||||
@@ -61,9 +61,11 @@ function errorMessage(error: unknown): string {
|
|||||||
export function LidarQualityWorkspace({
|
export function LidarQualityWorkspace({
|
||||||
deviceLabel,
|
deviceLabel,
|
||||||
onOpenObservation,
|
onOpenObservation,
|
||||||
|
embedded = false,
|
||||||
}: {
|
}: {
|
||||||
deviceLabel: string | null;
|
deviceLabel: string | null;
|
||||||
onOpenObservation: () => void;
|
onOpenObservation: () => void;
|
||||||
|
embedded?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const [catalog, setCatalog] = useState<LidarReplayCatalog | null>(null);
|
const [catalog, setCatalog] = useState<LidarReplayCatalog | null>(null);
|
||||||
const [detail, setDetail] = useState<LidarReplayDetail | null>(null);
|
const [detail, setDetail] = useState<LidarReplayDetail | null>(null);
|
||||||
@@ -248,29 +250,31 @@ export function LidarQualityWorkspace({
|
|||||||
</GlassSurface>
|
</GlassSurface>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<section className="lidar-device-context" aria-label="Выбранный LiDAR и запись">
|
{!embedded ? (
|
||||||
<div className="lidar-device-context__identity">
|
<section className="lidar-device-context" aria-label="Выбранный LiDAR и запись">
|
||||||
<span className="section-eyebrow">ВЫБРАННЫЙ СЕНСОР</span>
|
<div className="lidar-device-context__identity">
|
||||||
<strong>{deviceLabel ?? "Архивный источник"} · LiDAR</strong>
|
<span className="section-eyebrow">ВЫБРАННЫЙ СЕНСОР</span>
|
||||||
<small>
|
<strong>{deviceLabel ?? "Архивный источник"} · LiDAR</strong>
|
||||||
Запись {fieldReview?.sessionId ?? detail.pack.sessionId}
|
<small>
|
||||||
</small>
|
Запись {fieldReview?.sessionId ?? detail.pack.sessionId}
|
||||||
</div>
|
</small>
|
||||||
<div className="lidar-device-context__verdict">
|
</div>
|
||||||
<StatusBadge tone="warning">Диагностическая запись</StatusBadge>
|
<div className="lidar-device-context__verdict">
|
||||||
<p>
|
<StatusBadge tone="warning">Диагностическая запись</StatusBadge>
|
||||||
Данные воспроизводятся и доступны для визуального разбора.
|
<p>
|
||||||
Физическая точность сенсора этой записью не аттестована.
|
Данные воспроизводятся и доступны для визуального разбора.
|
||||||
</p>
|
Физическая точность сенсора этой записью не аттестована.
|
||||||
</div>
|
</p>
|
||||||
<Button
|
</div>
|
||||||
size="compact"
|
<Button
|
||||||
variant="secondary"
|
size="compact"
|
||||||
onClick={onOpenObservation}
|
variant="secondary"
|
||||||
>
|
onClick={onOpenObservation}
|
||||||
Открыть наблюдение
|
>
|
||||||
</Button>
|
Открыть наблюдение
|
||||||
</section>
|
</Button>
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<GlassSurface className="lidar-field-review" padding="lg">
|
<GlassSurface className="lidar-field-review" padding="lg">
|
||||||
<header className="panel-heading lidar-field-review__heading">
|
<header className="panel-heading lidar-field-review__heading">
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
} from "@nodedc/ui-react";
|
} from "@nodedc/ui-react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ObservationSessionArchive,
|
|
||||||
type ObservationSessionReplayCallbacks,
|
type ObservationSessionReplayCallbacks,
|
||||||
} from "../components/ObservationSessionSelect";
|
} from "../components/ObservationSessionSelect";
|
||||||
import {
|
import {
|
||||||
@@ -24,7 +23,11 @@ import {
|
|||||||
} from "../components/ObservationSources";
|
} from "../components/ObservationSources";
|
||||||
import { ObservationTimeline } from "../components/ObservationTimeline";
|
import { ObservationTimeline } from "../components/ObservationTimeline";
|
||||||
import { FloatingObservationWindow } from "../components/FloatingObservationWindow";
|
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 { RecordedSessionAdmissionController } from "../core/observation/useRecordedSessionAdmission";
|
||||||
import type {
|
import type {
|
||||||
RecordedAdmissionPhase,
|
RecordedAdmissionPhase,
|
||||||
@@ -1176,84 +1179,447 @@ function CatalogWorkspace({ definition }: WorkspaceRendererProps) {
|
|||||||
function RecordingsWorkspace(props: WorkspaceRendererProps) {
|
function RecordingsWorkspace(props: WorkspaceRendererProps) {
|
||||||
return (
|
return (
|
||||||
<div className="recordings-workspace">
|
<div className="recordings-workspace">
|
||||||
<ObservationSessionArchive {...props.sessionArchive} />
|
<SpatialWorkspace {...props} />
|
||||||
{props.recordedReplay ? (
|
|
||||||
<section className="recordings-workspace__viewer">
|
|
||||||
<header>
|
|
||||||
<span className="section-eyebrow">ВОСПРОИЗВЕДЕНИЕ</span>
|
|
||||||
<strong>Выбранная пространственная запись</strong>
|
|
||||||
</header>
|
|
||||||
<SpatialWorkspace {...props} />
|
|
||||||
</section>
|
|
||||||
) : (
|
|
||||||
<section className="recordings-workspace__empty">
|
|
||||||
<Icon name="database" size={20} />
|
|
||||||
<strong>Выберите сохранённую сессию</strong>
|
|
||||||
<p>Viewer появится здесь; эфир в разделе «Наблюдение» останется неизменным.</p>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function LabArchiveWorkspace(props: WorkspaceRendererProps) {
|
type LaboratoryProfileId = "sensor-fusion" | "published-perception";
|
||||||
const [surface, setSurface] = useState<"lidar-e29" | "replay">(
|
type LaboratoryWorkId = "e28-local-surface" | "e29-camera-geometry" | `session:${string}`;
|
||||||
props.recordedReplay ? "replay" : "lidar-e29",
|
|
||||||
|
interface LaboratoryOption<T extends string> {
|
||||||
|
id: T;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function LaboratorySelector<T extends string>({
|
||||||
|
eyebrow,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
options,
|
||||||
|
disabled = false,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
eyebrow: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
label: string;
|
||||||
|
value: T;
|
||||||
|
options: readonly LaboratoryOption<T>[];
|
||||||
|
disabled?: boolean;
|
||||||
|
onChange: (value: T) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<section className="laboratory-selector">
|
||||||
|
<div>
|
||||||
|
<span className="section-eyebrow">{eyebrow}</span>
|
||||||
|
<h2>{title}</h2>
|
||||||
|
<p>{description}</p>
|
||||||
|
</div>
|
||||||
|
<label>
|
||||||
|
<span>{label}</span>
|
||||||
|
<select
|
||||||
|
value={value}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={(event) => onChange(event.currentTarget.value as T)}
|
||||||
|
>
|
||||||
|
{options.map((option) => (
|
||||||
|
<option key={option.id} value={option.id}>{option.label}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</section>
|
||||||
);
|
);
|
||||||
const replayActions: WorkspaceRendererProps["sessionArchive"] = {
|
}
|
||||||
...props.sessionArchive,
|
|
||||||
onReplayAccepted: async (session, launch) => {
|
function LaboratoryTask({
|
||||||
await props.sessionArchive.onReplayAccepted?.(session, launch);
|
title,
|
||||||
setSurface("replay");
|
description,
|
||||||
},
|
status,
|
||||||
};
|
facts,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
status: string;
|
||||||
|
facts: readonly { label: string; value: string }[];
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<section className="laboratory-task">
|
||||||
|
<header>
|
||||||
|
<div>
|
||||||
|
<span className="section-eyebrow">ОПИСАНИЕ ЛАБОРАТОРНОЙ РАБОТЫ</span>
|
||||||
|
<h2>{title}</h2>
|
||||||
|
<p>{description}</p>
|
||||||
|
</div>
|
||||||
|
<StatusBadge tone="warning">{status}</StatusBadge>
|
||||||
|
</header>
|
||||||
|
<dl>
|
||||||
|
{facts.map((fact) => (
|
||||||
|
<div key={fact.label}>
|
||||||
|
<dt>{fact.label}</dt>
|
||||||
|
<dd>{fact.value}</dd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<div className="lab-archive-workspace">
|
<>
|
||||||
<section className="lab-configuration">
|
<LaboratoryTask
|
||||||
<div>
|
title="LAB E29 · camera-first semantics + независимая геометрия"
|
||||||
<span className="section-eyebrow">ОБЪЕКТ И КОНФИГУРАЦИЯ ИССЛЕДОВАНИЯ</span>
|
description="Проверяется рабочая продуктовая граница: камера сохраняет семантический класс и идентичность объекта, LiDAR подтверждает метрическую дальность и занятую геометрию по локальной поверхности L2.6. Отсутствие точек не объявляется свободным пространством."
|
||||||
<h2>Текущий профиль · camera + LiDAR</h2>
|
status="Диагностический replay"
|
||||||
<p>
|
facts={[
|
||||||
Объект и точная конфигурация берутся из LAB-provenance выбранной записи.
|
{ label: "Конфигурация", value: `${rigLabel} · камера + LiDAR · worker D` },
|
||||||
Исходный сенсор остаётся read-only, вычисление выполняется на worker D.
|
{ label: "Источник", value: "RAVNOVES00 · 4 489 кадров" },
|
||||||
</p>
|
{ label: "Объектов", value: "20 513 наблюдений" },
|
||||||
|
{ label: "Полномочия", value: "Только shadow · команды запрещены" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<section className="laboratory-visual-result" aria-label="Визуальный результат LAB E29">
|
||||||
|
<header>
|
||||||
|
<div>
|
||||||
|
<span className="section-eyebrow">ВИЗУАЛЬНЫЙ РЕЗУЛЬТАТ</span>
|
||||||
|
<h2>Покрытие семантических наблюдений геометрией</h2>
|
||||||
|
<p>
|
||||||
|
Это покрытие одного воспроизводимого replay, а не accuracy и не допуск
|
||||||
|
планировщика. Конфликты сохранены для ручного покадрового разбора.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<StatusBadge tone="warning">16 конфликтных эпизодов</StatusBadge>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="laboratory-e29-chart" role="img" aria-label="Распределение статусов геометрии">
|
||||||
|
<div className="laboratory-e29-chart__bar">
|
||||||
|
{statuses.map((status) => (
|
||||||
|
<i
|
||||||
|
key={status.id}
|
||||||
|
data-status={status.id}
|
||||||
|
style={{ width: `${(status.value / semanticTotal) * 100}%` }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="laboratory-e29-chart__legend">
|
||||||
|
{statuses.map((status) => (
|
||||||
|
<div key={status.id}>
|
||||||
|
<i data-status={status.id} />
|
||||||
|
<span>{status.label}</span>
|
||||||
|
<strong>{status.value.toLocaleString("ru-RU")}</strong>
|
||||||
|
<small>{((status.value / semanticTotal) * 100).toLocaleString("ru-RU", {
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
})}%</small>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="laboratory-e29-evidence">
|
||||||
|
<article>
|
||||||
|
<span className="section-eyebrow">НЕЗАВИСИМАЯ ГЕОМЕТРИЯ</span>
|
||||||
|
<strong>21 321 компонент</strong>
|
||||||
|
<p>
|
||||||
|
Незасемантизированные занятые компоненты сохранены отдельным слоем:
|
||||||
|
им не назначается выдуманный класс и они не считаются свободным местом.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
<article>
|
||||||
|
<span className="section-eyebrow">КОНФЛИКТЫ ДЛЯ РАЗБОРА</span>
|
||||||
|
<div>
|
||||||
|
{conflictEpisodes.map((episode) => <span key={episode}>{episode}</span>)}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
</div>
|
</div>
|
||||||
<StatusBadge tone="accent">Активная серия</StatusBadge>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="lab-primary-result">
|
<section className="laboratory-result-summary">
|
||||||
<button
|
<header>
|
||||||
type="button"
|
<div>
|
||||||
data-active={surface === "lidar-e29" ? "true" : undefined}
|
<span className="section-eyebrow">РЕЗУЛЬТАТ И ВЫВОД</span>
|
||||||
onClick={() => setSurface("lidar-e29")}
|
<h2>Архитектурная схема подтверждена, safety-gate не пройден</h2>
|
||||||
>
|
</div>
|
||||||
<span>LAB E29</span>
|
<StatusBadge tone="success">Replay завершён</StatusBadge>
|
||||||
<strong>Локальная модель поверхности L2.6</strong>
|
</header>
|
||||||
<small>Диагностический replay всей записи и кадры для разбора</small>
|
<div className="laboratory-result-metrics">
|
||||||
</button>
|
<div><span>Поддержка геометрией</span><strong>6 341</strong><small>32,31% current observations</small></div>
|
||||||
|
<div><span>Camera-only</span><strong>13 246</strong><small>Семантика сохранена</small></div>
|
||||||
|
<div><span>Postprocess p95</span><strong>2,517 мс</strong><small>8,467 с полный build</small></div>
|
||||||
|
<div><span>Geometry-only</span><strong>21 321</strong><small>Занято / класс неизвестен</small></div>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Следующий gate — покадровый разбор конфликтов, camera-only пробелов и
|
||||||
|
geometry-only компонентов, затем source-paced shadow с deadline и staleness.
|
||||||
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
<ObservationSessionArchive {...replayActions} labsOnly />
|
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 (
|
||||||
|
<>
|
||||||
|
<LaboratoryTask
|
||||||
|
title={`${lab?.labId ?? "LAB"} · ${laboratorySessionTitle(session)}`}
|
||||||
|
description="Опубликованная лабораторная работа открывается по своей неизменяемой записи. Viewer показывает исходные синхронные каналы; за ним приведены только метрики, зафиксированные в LAB-provenance этого результата."
|
||||||
|
status="Зафиксированный результат"
|
||||||
|
facts={[
|
||||||
|
{ label: "Тип результата", value: lab?.resultKind ?? "—" },
|
||||||
|
{ label: "Источник", value: lab?.sourceSessionId ?? session.id },
|
||||||
|
{ label: "Конфигурация", value: lab?.configSha256?.slice(0, 16) ?? "Не зафиксирована" },
|
||||||
|
{ label: "Полномочия", value: "Диагностика · без навигации и команд" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
<section className="lab-result-surface">
|
<section className="lab-result-surface">
|
||||||
<header>
|
<header>
|
||||||
<span className="section-eyebrow">ВИЗУАЛЬНОЕ ДОКАЗАТЕЛЬСТВО</span>
|
<span className="section-eyebrow">ВИЗУАЛЬНОЕ ДОКАЗАТЕЛЬСТВО</span>
|
||||||
<strong>
|
<strong>Исходная запись выбранной лабораторной работы</strong>
|
||||||
{surface === "replay" && props.recordedReplay
|
|
||||||
? "Запись выбранной лабораторной работы"
|
|
||||||
: "LAB E29 · результат L2.6"}
|
|
||||||
</strong>
|
|
||||||
</header>
|
</header>
|
||||||
{surface === "replay" && props.recordedReplay ? (
|
{replayReady ? (
|
||||||
<SpatialWorkspace {...props} />
|
<SpatialWorkspace {...props} />
|
||||||
) : (
|
) : (
|
||||||
<LidarQualityWorkspace
|
<div className="laboratory-result-pending" role="status">
|
||||||
deviceLabel={props.deviceLabel}
|
{loading ? <span className="busy-indicator" aria-hidden="true" /> : <Icon name="database" size={20} />}
|
||||||
onOpenObservation={() => props.navigation.openView("spatial-scene")}
|
<strong>{loading ? "Подготавливаем лабораторную запись" : "Запись не открыта"}</strong>
|
||||||
/>
|
<p>
|
||||||
|
{error ?? (loading
|
||||||
|
? "Связанное визуальное доказательство откроется после проверки записи."
|
||||||
|
: "Выберите работу ещё раз, чтобы открыть связанное визуальное доказательство.")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section className="laboratory-result-summary">
|
||||||
|
<header>
|
||||||
|
<div>
|
||||||
|
<span className="section-eyebrow">ЗАФИКСИРОВАННЫЕ МЕТРИКИ</span>
|
||||||
|
<h2>Результат из LAB-provenance</h2>
|
||||||
|
</div>
|
||||||
|
<StatusBadge tone={
|
||||||
|
lab?.provenance.benchmark_passed === true ? "success" : "warning"
|
||||||
|
}>
|
||||||
|
{lab?.provenance.benchmark_passed === true ? "Benchmark passed" : "Требует разбора"}
|
||||||
|
</StatusBadge>
|
||||||
|
</header>
|
||||||
|
<div className="laboratory-result-metrics">
|
||||||
|
{metrics.length ? metrics.map(([key, value]) => (
|
||||||
|
<div key={key}>
|
||||||
|
<span>{laboratoryMetricLabel(key)}</span>
|
||||||
|
<strong>{formatLaboratoryMetric(value)}</strong>
|
||||||
|
<small>Immutable provenance</small>
|
||||||
|
</div>
|
||||||
|
)) : (
|
||||||
|
<div>
|
||||||
|
<span>Метрики</span>
|
||||||
|
<strong>Не опубликованы</strong>
|
||||||
|
<small>Доступна исходная запись</small>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LabArchiveWorkspace(props: WorkspaceRendererProps) {
|
||||||
|
const [profileId, setProfileId] = useState<LaboratoryProfileId>("sensor-fusion");
|
||||||
|
const [workId, setWorkId] = useState<LaboratoryWorkId>("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<LaboratoryProfileId>[] = [
|
||||||
|
{
|
||||||
|
id: "sensor-fusion",
|
||||||
|
label: `${rigLabel} · камера + LiDAR · worker D`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "published-perception",
|
||||||
|
label: `${rigLabel} · опубликованный perception pipeline`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const workOptions: readonly LaboratoryOption<LaboratoryWorkId>[] =
|
||||||
|
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 (
|
||||||
|
<div className="lab-archive-workspace">
|
||||||
|
<LaboratorySelector
|
||||||
|
eyebrow="ПРОФИЛЬ ЛАБОРАТОРНОГО КОНТУРА"
|
||||||
|
title={profiles.find((profile) => profile.id === profileId)?.label ?? rigLabel}
|
||||||
|
description="Профиль фиксирует объект исследования, сенсорные модули и вычислительный контур. Исходные данные остаются read-only; профиль объединяет серию сопоставимых лабораторных работ."
|
||||||
|
label="Профиль"
|
||||||
|
value={profileId}
|
||||||
|
options={profiles}
|
||||||
|
onChange={selectProfile}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LaboratorySelector
|
||||||
|
eyebrow="ЛАБОРАТОРНАЯ РАБОТА"
|
||||||
|
title={workOptions.find((work) => work.id === workId)?.label ?? "Работа не выбрана"}
|
||||||
|
description="Выберите один зафиксированный эксперимент. Ниже откроются его задача, полноразмерное визуальное доказательство и структурированный результат."
|
||||||
|
label="Работа"
|
||||||
|
value={workId}
|
||||||
|
options={workOptions}
|
||||||
|
disabled={workOptions.length === 0}
|
||||||
|
onChange={selectWork}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="laboratory-work-output">
|
||||||
|
{workId === "e28-local-surface" ? (
|
||||||
|
<>
|
||||||
|
<LaboratoryTask
|
||||||
|
title="LAB E28 · локальная модель поверхности L2.6"
|
||||||
|
description="Полная запись RAVNOVES00 воспроизводится через bounded shadow-контур. Модель оценивает локальную поверхность, наблюдаемые препятствия, временные скачки и ошибки предсказания без изменения исходного сенсора и без командного канала."
|
||||||
|
status="Диагностический replay"
|
||||||
|
facts={[
|
||||||
|
{ label: "Конфигурация", value: `${rigLabel} · LiDAR + pose · worker D` },
|
||||||
|
{ label: "Покрытие", value: "3 928 / 3 928 кадров" },
|
||||||
|
{ label: "Режим", value: "Recorded-source-paced shadow" },
|
||||||
|
{ label: "Полномочия", value: "Навигация и команды запрещены" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<LidarQualityWorkspace
|
||||||
|
embedded
|
||||||
|
deviceLabel={props.deviceLabel}
|
||||||
|
onOpenObservation={() => props.navigation.openView("spatial-scene")}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : workId === "e29-camera-geometry" ? (
|
||||||
|
<E29LaboratoryResult rigLabel={rigLabel} />
|
||||||
|
) : selectedSession ? (
|
||||||
|
<PublishedLaboratoryResult
|
||||||
|
props={props}
|
||||||
|
session={selectedSession}
|
||||||
|
loading={sessions.replayingSessionId === selectedSession.id}
|
||||||
|
error={sessions.failedSessionId === selectedSession.id ? sessions.error : null}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="laboratory-result-pending">
|
||||||
|
<Icon name="database" size={20} />
|
||||||
|
<strong>В этом профиле пока нет опубликованных работ</strong>
|
||||||
|
<p>Работы появятся после публикации проверенного LAB-provenance.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -172,6 +172,31 @@ test("opened archive is named in the scene header and trash hover has no pill",
|
|||||||
assert.doesNotMatch(acquisitionPipeline, /Индикатор постоянно зелёный/);
|
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]*<ObservationSessionSelect/);
|
||||||
|
assert.match(recordingsSource, /<SpatialWorkspace \{\.\.\.props\} \/>/);
|
||||||
|
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", () => {
|
test("session catalog exposes authoritative background preparation state", () => {
|
||||||
const catalog = decodeObservationSessionCatalog({
|
const catalog = decodeObservationSessionCatalog({
|
||||||
items: [session({
|
items: [session({
|
||||||
|
|||||||
@@ -24,10 +24,12 @@ fully usable with only their available modalities.
|
|||||||
3. Stop acquisition normally, or allow the local service to recover an
|
3. Stop acquisition normally, or allow the local service to recover an
|
||||||
unexpected interruption on its next start. Session recording is automatic;
|
unexpected interruption on its next start. Session recording is automatic;
|
||||||
the disk action is not required.
|
the disk action is not required.
|
||||||
4. Open **Данные → Сессии и записи**. The inline archive shows up to 100
|
4. Open **Данные → Сессии и записи**. The compact **Сохранённые сессии**
|
||||||
indexed runs, their date, duration, state, modalities and background
|
dropdown shows up to 100 indexed runs, their date, duration, state,
|
||||||
preparation state. Opening an archive never replaces the live source owned
|
modalities and background preparation state. The page itself remains one
|
||||||
by **Наблюдение**.
|
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
|
5. Choose a replayable run. Opening a replay never performs conversion in the
|
||||||
request. A ready recording opens immediately; otherwise the client receives
|
request. A ready recording opens immediately; otherwise the client receives
|
||||||
HTTP 202, keeps the current scene mounted and polls the preparation status.
|
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`
|
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`
|
and the derived temporal comparison `LAB E22` can coexist over `RAVNOVES00`
|
||||||
without “latest accepted result” replacing an earlier experiment. The operator
|
without “latest accepted result” replacing an earlier experiment. The operator
|
||||||
can open any row in **Данные → Сессии и записи** or the corresponding LAB row
|
can open any row in **Данные → Сессии и записи** or choose the corresponding
|
||||||
in **Тестировочный контур** and use the same
|
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
|
**Объекты 2D / Сегментация / Кубы 3D** controls. First publication must also
|
||||||
warm the result-specific Rerun overlay; later opens reuse that verified cache
|
warm the result-specific Rerun overlay; later opens reuse that verified cache
|
||||||
and do not rerun AI inference.
|
and do not rerun AI inference.
|
||||||
|
|||||||
@@ -432,7 +432,8 @@ Dataset expansion is no longer the next gate.
|
|||||||
The implemented `missioncore.k1-local-surface/v1` derivative is reproducible
|
The implemented `missioncore.k1-local-surface/v1` derivative is reproducible
|
||||||
through `experiments/perception/run_k1_local_surface.py` and is exposed
|
through `experiments/perception/run_k1_local_surface.py` and is exposed
|
||||||
read-only through `GET /api/v1/lidar/local-surfaces` plus the bound frame,
|
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
|
reuses the five
|
||||||
RAVNOVES00 scene selectors and also exposes a clickable timeline over the
|
RAVNOVES00 scene selectors and also exposes a clickable timeline over the
|
||||||
complete recording. The selected source frame shows observed surface, observed
|
complete recording. The selected source frame shows observed surface, observed
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ Implemented now:
|
|||||||
lower-cell coordinates, signed residual and derived inlier mask are
|
lower-cell coordinates, signed residual and derived inlier mask are
|
||||||
content-bound and visible as a read-only 3D overlay;
|
content-bound and visible as a read-only 3D overlay;
|
||||||
- a provider-neutral read-only local-surface laboratory result in
|
- 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;
|
RAVNOVES00 scene selectors and a clickable complete-recording timeline;
|
||||||
- a complete recorded-host-paced 1× worker replay through authenticated
|
- a complete recorded-host-paced 1× worker replay through authenticated
|
||||||
ingress, K1 normalization, point/pose binding and a bounded latest-wins
|
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
|
inputs. Opening one dataset exposes its source fragments, sparse annotated
|
||||||
scans, visual overlays and useful analysis inside the expanded source card.
|
scans, visual overlays and useful analysis inside the expanded source card.
|
||||||
Opening another source closes the previous one.
|
Opening another source closes the previous one.
|
||||||
- **Тестировочный контур → Лабораторные работы** owns immutable experiment
|
- **Тестировочный контур → Лабораторные контуры** owns immutable experiment
|
||||||
configurations, accepted LAB instances and visual conclusions such as E29.
|
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
|
- A future **Парк** diagnostics surface must require a connected device and a
|
||||||
dedicated live diagnostic contract. It cannot reuse an offline replay report.
|
dedicated live diagnostic contract. It cannot reuse an offline replay report.
|
||||||
- Internal replay-run identity, provenance and immutable artifacts remain a
|
- Internal replay-run identity, provenance and immutable artifacts remain a
|
||||||
|
|||||||
@@ -34,7 +34,10 @@ to **Данные → Датасеты**. They do not recreate the previous Poly
|
|||||||
page.
|
page.
|
||||||
|
|
||||||
Saved session replay is mounted only in **Данные → Сессии и записи** or from a
|
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
|
сцена** to the runtime-published live source. A selected archive cannot leak
|
||||||
into the live observation surface.
|
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
|
viewer and analysis inside that source card, pushes later sources down, and
|
||||||
closes the previously expanded source.
|
closes the previously expanded source.
|
||||||
|
|
||||||
The laboratory archive reads immutable LAB instances from the existing
|
The laboratory surface is a two-level catalog:
|
||||||
observation-session catalog. The E29 local-surface result remains a read-only
|
|
||||||
laboratory surface and is no longer presented as live LiDAR diagnostics. A
|
1. a **laboratory contour profile** fixes the tested rig, modules and worker;
|
||||||
future device-diagnostics workspace must require an attached device and its own
|
2. a **laboratory work** selects one immutable result inside that profile.
|
||||||
diagnostic contract.
|
|
||||||
|
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:
|
Contour health reads only live contracts:
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user