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:
@@ -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}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user