feat(observatory-ui): review portable results and retry publication

Checkpoint the existing generic result-viewing and publication lifecycle UI. Focused architecture and Observatory tests: 62 passed; no new visual changes or rebuild in this checkpoint.
This commit is contained in:
DCCONSTRUCTIONS
2026-09-02 00:59:21 +03:00
parent 62d5520c7a
commit 5a78c997ac
10 changed files with 748 additions and 86 deletions
@@ -178,6 +178,7 @@ export function ObservatoryWorkspace({
>(null);
const overviewRef = useRef<HTMLElement | null>(null);
const replayCoordinatorRef = useRef(createObservationReplayCoordinator());
const refreshedPublishedJobRef = useRef<string | null>(null);
const items = controller.catalog?.items ?? EMPTY_OBSERVATORY_ITEMS;
const closeReplay = useCallback(() => {
@@ -244,14 +245,23 @@ export function ObservatoryWorkspace({
const presentedJob = recordedJobsController.activeJob
?? recordedJobsController.latestJob;
const presentedJobStatus = presentedJob
? recordedJobStatus[presentedJob.state]
? presentedJob.state === "succeeded"
? presentedJob.publication.state === "published"
? { label: "Результат опубликован", tone: "success" as const }
: presentedJob.publication.state === "failed"
? { label: "Ошибка публикации", tone: "danger" as const }
: presentedJob.publication.state === "not-required"
? { label: "Расчёт завершён", tone: "success" as const }
: recordedJobStatus.succeeded
: 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";
|| recordedJobsController.state === "submitting"
|| recordedJobsController.state === "retrying-publication";
const canSubmitRecordedJob = queueSubmissionAllowed
&& recordedJobsController.state === "ready"
&& recordedJobsController.activeJob === null;
@@ -267,6 +277,16 @@ export function ObservatoryWorkspace({
(evidence) => evidence.sessionId === replayEvidenceId,
) ?? null;
useEffect(() => {
const publishedJob = recordedJobsController.jobs.find(
(job) => job.publication.state === "published" && job.resultId !== null,
);
if (!publishedJob || refreshedPublishedJobRef.current === publishedJob.jobId) return;
refreshedPublishedJobRef.current = publishedJob.jobId;
void controller.refresh();
setupController.refresh();
}, [controller.refresh, recordedJobsController.jobs, setupController.refresh]);
useEffect(() => {
if (
replayEvidenceId === null
@@ -496,10 +516,14 @@ export function ObservatoryWorkspace({
</StatusBadge>
) : recordedJobsController.state === "submitting" ? (
<StatusBadge tone="neutral">Ставим в очередь</StatusBadge>
) : recordedJobsController.state === "retrying-publication" ? (
<StatusBadge tone="neutral">Повторяем публикацию</StatusBadge>
) : presentedJobStatus ? (
<StatusBadge
tone={presentedJobStatus.tone}
title={presentedJob?.terminalMessage ?? undefined}
title={presentedJob?.publication.error
?? presentedJob?.terminalMessage
?? undefined}
>
{presentedJobStatus.label}
</StatusBadge>
@@ -542,6 +566,16 @@ export function ObservatoryWorkspace({
Рассчитать
</Button>
) : null}
{presentedJob?.publication.state === "failed"
&& recordedJobsController.state !== "retrying-publication" ? (
<Button
size="compact"
variant="secondary"
onClick={() => void recordedJobsController.retryPublication()}
>
Повторить публикацию
</Button>
) : null}
</div>
</div>
</GlassSurface>
@@ -610,6 +644,20 @@ export function ObservatoryWorkspace({
<dt>Чтение</dt>
<dd>{selectedSession.source.replayable ? "Доступна" : "Не подготовлена"}</dd>
</div>
<div>
<dt>Оборудование</dt>
<dd>
{selectedSession.source.captureAttestation?.equipmentDisplayName
?? "Не аттестовано"}
</dd>
</div>
<div>
<dt>Профиль записи</dt>
<dd title={selectedSession.source.captureAttestation?.captureProfileSha256}>
{selectedSession.source.captureAttestation?.captureProfileId
?? "Не определён"}
</dd>
</div>
</dl>
<div className="observatory-session-summary__end">
<StatusBadge tone={statusTone(selectedSession.source.status)}>
@@ -738,12 +786,20 @@ export function ObservatoryWorkspace({
</div>
</GlassSurface>
) : replay.kind === "ready" ? (
<section className="observatory-replay" aria-label="Канонический визуальный разбор">
<section className="observatory-replay" aria-label="Визуальный разбор результата">
<header className="observatory-replay__header">
<div>
<span className="section-eyebrow">ВИЗУАЛЬНЫЙ РАЗБОР / ЗАПИСАННАЯ СЕССИЯ</span>
<span className="section-eyebrow">
{replay.review.kind === "canonical-recorded-rerun"
? "ВИЗУАЛЬНЫЙ РАЗБОР / ЗАПИСАННАЯ СЕССИЯ"
: "РЕЗУЛЬТАТ / ПРОВЕРЕННЫЙ ДОКУМЕНТ"}
</span>
<h3>{replayEvidence?.label ?? replay.binding.resultId}</h3>
<p>Записанный маршрут синхронизирован по общей временной шкале.</p>
<p>
{replay.review.kind === "canonical-recorded-rerun"
? "Записанный маршрут синхронизирован по общей временной шкале."
: "Показан проверенный документ результата и связанные с ним артефакты."}
</p>
</div>
<Button
size="compact"
@@ -754,10 +810,24 @@ export function ObservatoryWorkspace({
Закрыть разбор
</Button>
</header>
<CanonicalVegetationRerunReplay
resultId={replay.binding.resultId}
review={replay.review}
/>
{replay.review.kind === "canonical-recorded-rerun" ? (
<CanonicalVegetationRerunReplay
resultId={replay.binding.resultId}
review={replay.review.review}
/>
) : (
<GlassSurface className="observatory-replay-state" padding="lg">
<div>
<StatusBadge tone="success">Проверено</StatusBadge>
<h3>{replay.review.resultKind}</h3>
<p>Связанных артефактов: {replay.review.artifacts.length}</p>
<details>
<summary>Документ результата</summary>
<pre>{JSON.stringify(replay.review.resultDocument, null, 2)}</pre>
</details>
</div>
</GlassSurface>
)}
</section>
) : null}