feat(observatory): seal blocked run preparations

This commit is contained in:
DCCONSTRUCTIONS
2026-08-30 23:14:05 +03:00
parent 8d5aeb0533
commit 2a5763d3fb
12 changed files with 1935 additions and 25 deletions
+1 -1
View File
@@ -171,7 +171,7 @@ export const workspaces: WorkspaceDefinition[] = [
id: "observatory",
root: "polygon",
label: "Обсерватория",
title: "Обсерватория восприятия",
title: "Проверка компьютерного зрения",
eyebrow: "ТЕСТОВЫЙ КОНТУР / ОБСЕРВАТОРИЯ",
description: "Сессии и квалификация компьютерного зрения без доступа к управлению.",
icon: "eye",
@@ -1,5 +1,7 @@
.observatory-workspace {
display: grid;
grid-auto-rows: max-content;
align-content: start;
gap: 1rem;
min-width: 0;
min-height: 100%;
@@ -37,6 +39,14 @@
margin: 0.3rem 0 0;
}
.observatory-catalog-bar h3,
.observatory-session-summary h3,
.observatory-evidence h3 {
font-size: var(--nodedc-font-size-lg);
letter-spacing: -0.02em;
line-height: 1.15;
}
.observatory-lead p,
.observatory-catalog-bar p,
.observatory-state p,
@@ -44,7 +54,14 @@
max-width: 52rem;
margin: 0.35rem 0 0;
color: var(--nodedc-text-muted);
line-height: 1.5;
font-size: var(--nodedc-font-size-sm);
line-height: 1.35;
}
.observatory-catalog-bar,
.observatory-session-summary {
align-self: start;
min-height: 0;
}
.observatory-catalog-bar__copy {
@@ -77,6 +94,8 @@
.observatory-session-stack {
display: grid;
grid-auto-rows: max-content;
align-content: start;
gap: 0.9rem;
container-name: observatory-session;
container-type: inline-size;
@@ -130,6 +149,8 @@
margin: 0;
overflow-wrap: anywhere;
color: var(--nodedc-text-secondary);
font-size: var(--nodedc-font-size-sm);
line-height: 1.35;
}
.observatory-session-summary__end {
@@ -168,7 +189,8 @@
max-width: 68rem;
margin: 0.3rem 0 0;
color: var(--nodedc-text-muted);
line-height: 1.45;
font-size: var(--nodedc-font-size-sm);
line-height: 1.35;
}
.observatory-setup-detail__badges,
@@ -212,6 +234,8 @@
margin: 0;
overflow-wrap: anywhere;
color: var(--nodedc-text-secondary);
font-size: var(--nodedc-font-size-sm);
line-height: 1.35;
}
.observatory-setup-results {
@@ -250,6 +274,8 @@
.observatory-setup-results small {
color: var(--nodedc-text-muted);
font-size: var(--nodedc-font-size-sm);
line-height: 1.35;
}
.observatory-setup-detail__footer {
@@ -145,6 +145,7 @@ export function ObservatoryWorkspace({
const [mutationReconciliation, setMutationReconciliation] = useState<
ObservatoryMutationReconciliation | null
>(null);
const overviewRef = useRef<HTMLElement | null>(null);
const replayCoordinatorRef = useRef(createObservationReplayCoordinator());
const items = controller.catalog?.items ?? EMPTY_OBSERVATORY_ITEMS;
@@ -153,6 +154,11 @@ export function ObservatoryWorkspace({
setReplay((current) => current.kind === "closed" ? current : { kind: "closed" });
}, []);
const returnToOverview = useCallback(() => {
closeReplay();
overviewRef.current?.scrollIntoView({ block: "start" });
}, [closeReplay]);
useEffect(() => {
if (items.some((item) => item.source.id === selectedSessionId)) return;
closeReplay();
@@ -352,7 +358,7 @@ export function ObservatoryWorkspace({
data-observatory-authority="observation-only"
data-observatory-viewer={replay.kind === "ready" ? "attached" : "detached"}
>
<section className="observatory-lead">
<section ref={overviewRef} className="observatory-lead">
<div>
<span className="section-eyebrow">{definition.eyebrow}</span>
<h2>{definition.title}</h2>
@@ -368,7 +374,7 @@ export function ObservatoryWorkspace({
</StatusBadge>
</section>
<GlassSurface className="observatory-catalog-bar" padding="lg">
<GlassSurface className="observatory-catalog-bar" padding="md">
<div className="observatory-catalog-bar__copy">
<span className="section-eyebrow">ИСТОЧНИК ДОКАЗАТЕЛЬСТВ</span>
<h3>Сохранённая сессия</h3>
@@ -606,7 +612,7 @@ export function ObservatoryWorkspace({
<h3>Проверяем точную связь результата с исходной сессией</h3>
<p>Визуализатор и данные маршрута ещё не запущены.</p>
</div>
<Button size="compact" variant="ghost" onClick={closeReplay}>Отменить</Button>
<Button size="compact" variant="ghost" onClick={returnToOverview}>Отменить</Button>
</GlassSurface>
) : replay.kind === "error" ? (
<GlassSurface className="observatory-replay-state" padding="lg" role="alert">
@@ -623,7 +629,7 @@ export function ObservatoryWorkspace({
>
Повторить
</Button>
<Button size="compact" variant="ghost" onClick={closeReplay}>Закрыть</Button>
<Button size="compact" variant="ghost" onClick={returnToOverview}>Закрыть</Button>
</div>
</GlassSurface>
) : replay.kind === "ready" ? (
@@ -638,7 +644,7 @@ export function ObservatoryWorkspace({
size="compact"
variant="secondary"
icon={<Icon name="close" size={14} />}
onClick={closeReplay}
onClick={returnToOverview}
>
Закрыть разбор
</Button>
@@ -35,7 +35,7 @@ test("Observatory is the third independent Polygon workspace", () => {
id: "observatory",
root: "polygon",
label: "Обсерватория",
title: "Обсерватория восприятия",
title: "Проверка компьютерного зрения",
eyebrow: "ТЕСТОВЫЙ КОНТУР / ОБСЕРВАТОРИЯ",
description: "Сессии и квалификация компьютерного зрения без доступа к управлению.",
icon: "eye",
@@ -95,6 +95,10 @@ test("Observatory mounts the one shared canonical replay only after explicit adm
assert.match(workspace, /role="alert"/);
assert.match(workspace, /Повторить/);
assert.match(workspace, /Закрыть разбор/);
assert.match(
workspace,
/const returnToOverview[\s\S]*closeReplay\(\);[\s\S]*scrollIntoView\(\{ block: "start" \}\)/,
);
assert.match(workspace, /<h3>\{replayEvidence\?\.label \?\? replay\.binding\.resultId\}<\/h3>/);
assert.doesNotMatch(workspace, /RAVNOVES004TREE · полный маршрут восприятия/);
assert.match(workspace, /const selectSession[\s\S]*closeReplay\(\);[\s\S]*setSelectedSessionId/);
@@ -130,6 +134,14 @@ test("Observatory mounts the one shared canonical replay only after explicit adm
styles,
/\.observatory-session-stack \{[\s\S]*container-name: observatory-session;[\s\S]*container-type: inline-size;/,
);
assert.match(
styles,
/\.observatory-workspace \{[\s\S]*grid-auto-rows: max-content;[\s\S]*align-content: start;/,
);
assert.match(
styles,
/\.observatory-session-summary__facts dd \{[\s\S]*font-size: var\(--nodedc-font-size-sm\);/,
);
assert.match(
styles,
/@container observatory-session \(max-width: 56rem\) \{[\s\S]*\.observatory-session-summary \{[\s\S]*grid-template-columns: minmax\(0, 1fr\);/,