feat(observatory): add laboratory setup preflight

This commit is contained in:
DCCONSTRUCTIONS
2026-08-30 22:21:58 +03:00
parent be83283ee0
commit 8d5aeb0533
15 changed files with 2491 additions and 9 deletions
@@ -0,0 +1,139 @@
import {
ActivityIndicator,
Button,
GlassSurface,
Icon,
StatusBadge,
} from "@nodedc/ui-react";
import type { ObservatoryEvidence } from "../../core/observatory/catalog";
import type { ObservatoryLaboratorySetup } from "../../core/observatory/laboratorySetups";
import type { ObservatoryRecordedRunBinding } from "../../core/observatory/recordedRun";
import type { SetupPreflightState } from "../../core/observatory/useObservatoryLaboratorySetups";
const relationLabel: Readonly<Record<string, string>> = {
"primary-visual": "Основной визуальный разбор",
"compute-successor": "Связанное вычислительное доказательство",
"canonical-projection": "Готовый записанный разбор",
};
function workerLabel(value: string): string {
const match = /^worker-(\d+)$/.exec(value);
return match ? `Worker ${match[1]}` : value;
}
export function ObservatorySetupDetail({
setup,
evidence,
preflight,
onCheck,
onOpenExisting,
}: {
setup: ObservatoryLaboratorySetup;
evidence: readonly ObservatoryEvidence[];
preflight: SetupPreflightState;
onCheck: () => void;
onOpenExisting: (binding: ObservatoryRecordedRunBinding) => void;
}) {
const exactExisting = evidence.find(
(candidate) => candidate.recordedRun
&& setup.preflight.existingResultIds.includes(candidate.recordedRun.resultId),
)?.recordedRun ?? null;
const displayedReason = preflight.kind === "ready"
? preflight.value.checks.find((check) => check.checkId === "executor")?.message
?? setup.executor.reason
: preflight.kind === "error"
? preflight.message
: setup.preflight.reason;
return (
<GlassSurface className="observatory-setup-detail" padding="md">
<div className="observatory-setup-detail__identity">
<span className="section-eyebrow">СЕТАП ЛАБОРАТОРИИ</span>
<h3>{setup.displayName}</h3>
<p>{setup.description}</p>
</div>
<div className="observatory-setup-detail__badges">
<StatusBadge tone={setup.compatibility.compatible ? "success" : "warning"}>
{setup.compatibility.compatible ? "Совместим" : "Другая сессия"}
</StatusBadge>
<StatusBadge tone={setup.origin === "existing-result" ? "accent" : "neutral"}>
{setup.origin === "existing-result" ? "Готовый результат" : "Архивный сетап"}
</StatusBadge>
</div>
<dl className="observatory-setup-detail__facts">
<div>
<dt>Идентичность</dt>
<dd>
{setup.runDefinition
? `Версия ${setup.runDefinition.version} · конфигурация зафиксирована`
: "Готовый записанный результат без повторно запускаемой конфигурации"}
</dd>
</div>
<div>
<dt>Исполнитель</dt>
<dd>
{setup.preflight.outcome === "existing"
? "Не требуется для просмотра"
: `${workerLabel(setup.executor.contourId)} · повторный запуск не подключён`}
</dd>
</div>
<div>
<dt>Сохранено</dt>
<dd>{setup.preservedResults.length} неизменяемых результатов</dd>
</div>
</dl>
<ol className="observatory-setup-results" aria-label="Сохранённые результаты сетапа">
{setup.preservedResults.map((result) => (
<li key={result.resultId}>
<Icon name={result.access === "evidence-only" ? "database" : "clipboard"} size={15} />
<span>
<strong>{relationLabel[result.relation] ?? "Сохранённый результат"}</strong>
<small>
{result.access === "observatory"
? "Доступен в Обсерватории"
: result.access === "legacy-lab"
? "Сохранён в legacy LAB"
: "Сохранено как связанное доказательство"}
</small>
</span>
<StatusBadge tone={result.observatoryProjectionAvailable ? "success" : "neutral"}>
{result.access === "observatory"
? result.observatoryProjectionAvailable ? "Готов" : "Сохранён"
: result.access === "legacy-lab" ? "Legacy LAB" : "Evidence"}
</StatusBadge>
</li>
))}
</ol>
<div className="observatory-setup-detail__footer">
<div className="observatory-setup-detail__reason" role={preflight.kind === "error" ? "alert" : "status"}>
<Icon name={preflight.kind === "error" ? "alert" : "activity"} size={16} />
<span>{displayedReason}</span>
</div>
<div className="observatory-setup-detail__actions">
{exactExisting ? (
<Button
size="compact"
variant="secondary"
icon={<Icon name="eye" size={15} />}
onClick={() => onOpenExisting(exactExisting)}
>
Открыть готовый результат
</Button>
) : (
<Button
size="compact"
variant="secondary"
disabled={preflight.kind === "checking"}
onClick={onCheck}
>
{preflight.kind === "checking"
? <ActivityIndicator size="compact" label="Проверяем совместимость" />
: "Проверить совместимость"}
</Button>
)}
</div>
</div>
</GlassSurface>
);
}
@@ -31,7 +31,9 @@ import {
renameObservatoryLabProjection,
} from "../../core/observatory/catalogMutations";
import { useObservatoryCatalog } from "../../core/observatory/useObservatoryCatalog";
import { useObservatoryLaboratorySetups } from "../../core/observatory/useObservatoryLaboratorySetups";
import type { WorkspaceDefinition } from "../../productModel";
import { ObservatorySetupDetail } from "./ObservatorySetupDetail";
const MAX_PRESENTED_EVIDENCE = 6;
const EMPTY_OBSERVATORY_ITEMS = [] as const;
@@ -133,6 +135,7 @@ export function ObservatoryWorkspace({
}) {
const controller = useObservatoryCatalog();
const [selectedSessionId, setSelectedSessionId] = useState("");
const setupController = useObservatoryLaboratorySetups(selectedSessionId);
const [replay, setReplay] = useState<ObservatoryReplayState>({ kind: "closed" });
const [renameTarget, setRenameTarget] = useState<ObservatoryEvidence | null>(null);
const [renameValue, setRenameValue] = useState("");
@@ -170,6 +173,15 @@ export function ObservatoryWorkspace({
label: source.label,
description: `${formatTimestamp(source.startedAtUtc)} · ${formatDuration(source.durationSeconds)} · ${evidence.length} результатов`,
})), [items]);
const setupOptions = useMemo(() => (
setupController.catalog?.setups.map((setup) => ({
value: setup.setupId,
label: setup.displayName,
description: setup.compatibility.compatible
? setup.origin === "existing-result" ? "Готовый результат" : "Совместимый архивный сетап"
: "Несовместим с выбранной сессией",
})) ?? []
), [setupController.catalog]);
const initialLoading = !controller.catalog
&& ["idle", "loading"].includes(controller.state);
const unavailable = !controller.catalog && controller.state === "error";
@@ -375,12 +387,32 @@ export function ObservatoryWorkspace({
menuWidth={460}
onChange={selectSession}
/>
<Select
label="Выбрать сетап лаборатории"
value={setupController.selectedSetupId}
options={setupOptions}
disabled={!selectedSessionId || setupOptions.length === 0}
searchable
searchPlaceholder="Поиск по сетапам"
emptyLabel={setupController.state === "error" ? "Каталог сетапов недоступен" : "Сетап не найден"}
minMenuWidth={360}
menuWidth={500}
onChange={setupController.selectSetup}
/>
<Button
size="compact"
variant="secondary"
disabled={controller.state === "loading" || controller.state === "refreshing"}
disabled={
controller.state === "loading"
|| controller.state === "refreshing"
|| setupController.state === "loading"
|| setupController.state === "refreshing"
}
icon={<Icon name="refresh" />}
onClick={controller.refresh}
onClick={() => {
void controller.refresh();
setupController.refresh();
}}
>
Обновить
</Button>
@@ -395,6 +427,14 @@ export function ObservatoryWorkspace({
</GlassSurface>
) : null}
{setupController.error && setupController.catalog ? (
<GlassSurface className="observatory-notice" padding="md" tone="soft" role="alert">
<StatusBadge tone="warning">Показан последний каталог сетапов</StatusBadge>
<span className="observatory-notice__copy">{setupController.error}</span>
<Button size="compact" variant="ghost" onClick={setupController.refresh}>Повторить</Button>
</GlassSurface>
) : null}
{controller.catalog
&& (controller.catalog.window.sourceLimitReached
|| controller.catalog.window.laboratoryLimitReached) ? (
@@ -460,6 +500,22 @@ export function ObservatoryWorkspace({
</div>
</GlassSurface>
{setupController.selectedSetup ? (
<ObservatorySetupDetail
setup={setupController.selectedSetup}
evidence={selectedSession.evidence}
preflight={setupController.preflight}
onCheck={() => { void setupController.check(); }}
onOpenExisting={openReplay}
/>
) : setupController.error ? (
<GlassSurface className="observatory-notice" padding="md" tone="soft" role="alert">
<StatusBadge tone="warning">Сетапы недоступны</StatusBadge>
<span className="observatory-notice__copy">{setupController.error}</span>
<Button size="compact" variant="ghost" onClick={setupController.refresh}>Повторить</Button>
</GlassSurface>
) : null}
<section className="observatory-evidence">
<header>
<div>