feat(observatory): add durable recorded compute queue

This commit is contained in:
DCCONSTRUCTIONS
2026-08-31 01:04:25 +03:00
parent e72609120f
commit d67e86176e
23 changed files with 6300 additions and 369 deletions
@@ -1,139 +0,0 @@
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>
);
}
@@ -32,8 +32,9 @@ import {
} from "../../core/observatory/catalogMutations";
import { useObservatoryCatalog } from "../../core/observatory/useObservatoryCatalog";
import { useObservatoryLaboratorySetups } from "../../core/observatory/useObservatoryLaboratorySetups";
import { useObservatoryRecordedJobs } from "../../core/observatory/useObservatoryRecordedJobs";
import type { ObservatoryRecordedJobState } from "../../core/observatory/recordedJobs";
import type { WorkspaceDefinition } from "../../productModel";
import { ObservatorySetupDetail } from "./ObservatorySetupDetail";
const MAX_PRESENTED_EVIDENCE = 6;
const EMPTY_OBSERVATORY_ITEMS = [] as const;
@@ -87,6 +88,27 @@ const modalityLabel: Record<string, string> = {
telemetry: "Телеметрия",
};
const recordedJobStatus: Record<
ObservatoryRecordedJobState,
{
readonly label: string;
readonly tone: "success" | "accent" | "warning" | "danger" | "neutral";
}
> = {
accepted: { label: "Принят", tone: "neutral" },
queued: { label: "Ждёт Worker", tone: "neutral" },
claimed: { label: "Назначен Worker", tone: "accent" },
running: { label: "Выполняется", tone: "accent" },
paused: { label: "Пауза: live-поток", tone: "warning" },
"preemption-pending": {
label: "Ждём подтверждения остановки",
tone: "warning",
},
succeeded: { label: "Готово", tone: "success" },
failed: { label: "Ошибка расчёта", tone: "danger" },
"reconciliation-required": { label: "Нужна сверка", tone: "warning" },
};
function statusTone(
status: ObservationSessionStatus,
): "success" | "accent" | "warning" | "danger" | "neutral" {
@@ -136,6 +158,10 @@ export function ObservatoryWorkspace({
const controller = useObservatoryCatalog();
const [selectedSessionId, setSelectedSessionId] = useState("");
const setupController = useObservatoryLaboratorySetups(selectedSessionId);
const recordedJobsController = useObservatoryRecordedJobs(
selectedSessionId,
setupController.selectedSetupId,
);
const [replay, setReplay] = useState<ObservatoryReplayState>({ kind: "closed" });
const [renameTarget, setRenameTarget] = useState<ObservatoryEvidence | null>(null);
const [renameValue, setRenameValue] = useState("");
@@ -188,6 +214,29 @@ export function ObservatoryWorkspace({
: "Несовместим с выбранной сессией",
})) ?? []
), [setupController.catalog]);
const runPreflight = setupController.preflight.kind === "ready"
? setupController.preflight.value
: null;
const queueSubmissionAllowed = Boolean(
setupController.selectedSetup?.runDefinition
&& setupController.selectedSetup.compatibility.compatible
&& runPreflight?.outcome === "queueable"
&& runPreflight.submissionAllowed,
);
const presentedJob = recordedJobsController.activeJob
?? recordedJobsController.latestJob;
const presentedJobStatus = presentedJob
? recordedJobStatus[presentedJob.state]
: null;
const queueStatusError = setupController.preflight.kind === "error"
? setupController.preflight.message
: recordedJobsController.error;
const queueStateBusy = recordedJobsController.state === "loading"
|| recordedJobsController.state === "refreshing"
|| recordedJobsController.state === "submitting";
const canSubmitRecordedJob = queueSubmissionAllowed
&& recordedJobsController.state === "ready"
&& recordedJobsController.activeJob === null;
const initialLoading = !controller.catalog
&& ["idle", "loading"].includes(controller.state);
const unavailable = !controller.catalog && controller.state === "error";
@@ -374,11 +423,10 @@ export function ObservatoryWorkspace({
</StatusBadge>
</section>
<GlassSurface className="observatory-catalog-bar" padding="md">
<GlassSurface className="observatory-catalog-bar" padding="sm">
<div className="observatory-catalog-bar__copy">
<span className="section-eyebrow">ИСТОЧНИК ДОКАЗАТЕЛЬСТВ</span>
<h3>Сохранённая сессия</h3>
<p>Выбор меняет только читаемую карточку и не готовит визуальный разбор в фоне.</p>
</div>
<div className="observatory-catalog-bar__controls">
<Select
@@ -418,10 +466,40 @@ export function ObservatoryWorkspace({
onClick={() => {
void controller.refresh();
setupController.refresh();
recordedJobsController.refresh();
}}
>
Обновить
</Button>
<div className="observatory-catalog-bar__run" aria-live="polite">
{queueStatusError ? (
<StatusBadge tone="danger" title={queueStatusError}>
Очередь недоступна
</StatusBadge>
) : recordedJobsController.state === "submitting" ? (
<StatusBadge tone="neutral">Ставим в очередь</StatusBadge>
) : presentedJobStatus ? (
<StatusBadge
tone={presentedJobStatus.tone}
title={presentedJob?.terminalMessage ?? undefined}
>
{presentedJobStatus.label}
</StatusBadge>
) : setupController.preflight.kind === "checking" ? (
<StatusBadge tone="neutral">Проверяем сетап</StatusBadge>
) : queueStateBusy ? (
<StatusBadge tone="neutral">Читаем очередь</StatusBadge>
) : null}
{canSubmitRecordedJob ? (
<Button
size="compact"
variant="primary"
onClick={() => { void recordedJobsController.submit(); }}
>
Рассчитать
</Button>
) : null}
</div>
</div>
</GlassSurface>
@@ -476,7 +554,7 @@ export function ObservatoryWorkspace({
</GlassSurface>
) : selectedSession ? (
<section className="observatory-session-stack" aria-label="Выбранная сессия и связанные результаты">
<GlassSurface className="observatory-session-summary" padding="md">
<GlassSurface className="observatory-session-summary" padding="sm">
<div className="observatory-session-summary__identity">
<span className="section-eyebrow">ИСХОДНАЯ СЕССИЯ</span>
<h3>{selectedSession.source.label}</h3>
@@ -506,22 +584,6 @@ 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>