fix(control-station): separate lab contours from recordings
This commit is contained in:
@@ -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}
|
||||
</div>
|
||||
) : 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" ? (
|
||||
<StatusBadge tone="neutral">Offline evaluation</StatusBadge>
|
||||
) : activeDefinition.kind === "lab-archive" ? (
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<LidarReplayCatalog | null>(null);
|
||||
const [detail, setDetail] = useState<LidarReplayDetail | null>(null);
|
||||
@@ -248,29 +250,31 @@ export function LidarQualityWorkspace({
|
||||
</GlassSurface>
|
||||
) : (
|
||||
<>
|
||||
<section className="lidar-device-context" aria-label="Выбранный LiDAR и запись">
|
||||
<div className="lidar-device-context__identity">
|
||||
<span className="section-eyebrow">ВЫБРАННЫЙ СЕНСОР</span>
|
||||
<strong>{deviceLabel ?? "Архивный источник"} · LiDAR</strong>
|
||||
<small>
|
||||
Запись {fieldReview?.sessionId ?? detail.pack.sessionId}
|
||||
</small>
|
||||
</div>
|
||||
<div className="lidar-device-context__verdict">
|
||||
<StatusBadge tone="warning">Диагностическая запись</StatusBadge>
|
||||
<p>
|
||||
Данные воспроизводятся и доступны для визуального разбора.
|
||||
Физическая точность сенсора этой записью не аттестована.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
size="compact"
|
||||
variant="secondary"
|
||||
onClick={onOpenObservation}
|
||||
>
|
||||
Открыть наблюдение
|
||||
</Button>
|
||||
</section>
|
||||
{!embedded ? (
|
||||
<section className="lidar-device-context" aria-label="Выбранный LiDAR и запись">
|
||||
<div className="lidar-device-context__identity">
|
||||
<span className="section-eyebrow">ВЫБРАННЫЙ СЕНСОР</span>
|
||||
<strong>{deviceLabel ?? "Архивный источник"} · LiDAR</strong>
|
||||
<small>
|
||||
Запись {fieldReview?.sessionId ?? detail.pack.sessionId}
|
||||
</small>
|
||||
</div>
|
||||
<div className="lidar-device-context__verdict">
|
||||
<StatusBadge tone="warning">Диагностическая запись</StatusBadge>
|
||||
<p>
|
||||
Данные воспроизводятся и доступны для визуального разбора.
|
||||
Физическая точность сенсора этой записью не аттестована.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
size="compact"
|
||||
variant="secondary"
|
||||
onClick={onOpenObservation}
|
||||
>
|
||||
Открыть наблюдение
|
||||
</Button>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<GlassSurface className="lidar-field-review" padding="lg">
|
||||
<header className="panel-heading lidar-field-review__heading">
|
||||
|
||||
@@ -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 (
|
||||
<div className="recordings-workspace">
|
||||
<ObservationSessionArchive {...props.sessionArchive} />
|
||||
{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>
|
||||
)}
|
||||
<SpatialWorkspace {...props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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<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) => {
|
||||
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 (
|
||||
<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 (
|
||||
<div className="lab-archive-workspace">
|
||||
<section className="lab-configuration">
|
||||
<div>
|
||||
<span className="section-eyebrow">ОБЪЕКТ И КОНФИГУРАЦИЯ ИССЛЕДОВАНИЯ</span>
|
||||
<h2>Текущий профиль · camera + LiDAR</h2>
|
||||
<p>
|
||||
Объект и точная конфигурация берутся из LAB-provenance выбранной записи.
|
||||
Исходный сенсор остаётся read-only, вычисление выполняется на worker D.
|
||||
</p>
|
||||
<>
|
||||
<LaboratoryTask
|
||||
title="LAB E29 · camera-first semantics + независимая геометрия"
|
||||
description="Проверяется рабочая продуктовая граница: камера сохраняет семантический класс и идентичность объекта, LiDAR подтверждает метрическую дальность и занятую геометрию по локальной поверхности L2.6. Отсутствие точек не объявляется свободным пространством."
|
||||
status="Диагностический replay"
|
||||
facts={[
|
||||
{ label: "Конфигурация", value: `${rigLabel} · камера + LiDAR · worker D` },
|
||||
{ label: "Источник", value: "RAVNOVES00 · 4 489 кадров" },
|
||||
{ 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>
|
||||
<StatusBadge tone="accent">Активная серия</StatusBadge>
|
||||
</section>
|
||||
|
||||
<section className="lab-primary-result">
|
||||
<button
|
||||
type="button"
|
||||
data-active={surface === "lidar-e29" ? "true" : undefined}
|
||||
onClick={() => setSurface("lidar-e29")}
|
||||
>
|
||||
<span>LAB E29</span>
|
||||
<strong>Локальная модель поверхности L2.6</strong>
|
||||
<small>Диагностический replay всей записи и кадры для разбора</small>
|
||||
</button>
|
||||
<section className="laboratory-result-summary">
|
||||
<header>
|
||||
<div>
|
||||
<span className="section-eyebrow">РЕЗУЛЬТАТ И ВЫВОД</span>
|
||||
<h2>Архитектурная схема подтверждена, safety-gate не пройден</h2>
|
||||
</div>
|
||||
<StatusBadge tone="success">Replay завершён</StatusBadge>
|
||||
</header>
|
||||
<div className="laboratory-result-metrics">
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
<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">
|
||||
<header>
|
||||
<span className="section-eyebrow">ВИЗУАЛЬНОЕ ДОКАЗАТЕЛЬСТВО</span>
|
||||
<strong>
|
||||
{surface === "replay" && props.recordedReplay
|
||||
? "Запись выбранной лабораторной работы"
|
||||
: "LAB E29 · результат L2.6"}
|
||||
</strong>
|
||||
<strong>Исходная запись выбранной лабораторной работы</strong>
|
||||
</header>
|
||||
{surface === "replay" && props.recordedReplay ? (
|
||||
{replayReady ? (
|
||||
<SpatialWorkspace {...props} />
|
||||
) : (
|
||||
<LidarQualityWorkspace
|
||||
deviceLabel={props.deviceLabel}
|
||||
onOpenObservation={() => props.navigation.openView("spatial-scene")}
|
||||
/>
|
||||
<div className="laboratory-result-pending" role="status">
|
||||
{loading ? <span className="busy-indicator" aria-hidden="true" /> : <Icon name="database" size={20} />}
|
||||
<strong>{loading ? "Подготавливаем лабораторную запись" : "Запись не открыта"}</strong>
|
||||
<p>
|
||||
{error ?? (loading
|
||||
? "Связанное визуальное доказательство откроется после проверки записи."
|
||||
: "Выберите работу ещё раз, чтобы открыть связанное визуальное доказательство.")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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]*<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", () => {
|
||||
const catalog = decodeObservationSessionCatalog({
|
||||
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
|
||||
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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user