2 Commits
4 changed files with 108 additions and 11 deletions
@@ -123,9 +123,12 @@ export function FloatingObservationWindow({
return () => observer.disconnect(); return () => observer.disconnect();
}, [boundsRef]); }, [boundsRef]);
const initialRect = bounds ? initialObservationWindowRect(index, count, bounds) : null; // Camera media admission must not wait for the viewport observer. A source
// can arrive in the same commit as a workspace transition; mounting it with
// the canonical fallback geometry keeps the browser transport alive until
// the real bounds are measured immediately afterward.
const initialRect = initialObservationWindowRect(index, count, bounds ?? undefined);
const windowRect = rect ?? initialRect; const windowRect = rect ?? initialRect;
if (!windowRect) return null;
return ( return (
<WorkspaceWindow <WorkspaceWindow
@@ -6788,6 +6788,38 @@ test("read-only reconnect keeps its card and loader across a transient projectio
)), )),
]); ]);
const runningVerificationState = structuredClone(initialState);
runningVerificationState.connection_attempt = {
schema_version: "missioncore.xgrids-k1-connection-attempt/v1",
attempt_id: "verify-saved-k1-running",
connection_mode: "bridge",
status: "running",
phase: "network_applied",
control_state: "unknown",
stage: "host-route-and-control-endpoint",
public_error_code: null,
side_effect_status: "network-applied",
safe_next_action: "wait-for-current-attempt",
automatic_retry: false,
accepted_at: OBSERVED_AT,
completed_at: null,
timeline: [],
};
currentState = runningVerificationState;
tree = harness.render(props());
const runningMarkup = renderToStaticMarkup(tree);
const runningReconnect = buttonMarkupWithText(
runningMarkup,
"Переподключение…",
);
assert.equal(runningReconnect.length, 1);
assert.match(runningReconnect[0], /disabled/);
assert.match(runningReconnect[0], /aria-busy="true"/);
assert.match(runningReconnect[0], /nodedc-activity-indicator/);
assert.match(runningMarkup, /Сохранённое подключение/);
assert.doesNotMatch(runningMarkup, /Команда настройки завершена/);
assert.doesNotMatch(runningMarkup, /Подтверждение управления/);
const transientState = structuredClone(initialState); const transientState = structuredClone(initialState);
transientState.connection_policy.allowed_actions = transientState.connection_policy.allowed_actions =
transientState.connection_policy.allowed_actions.filter( transientState.connection_policy.allowed_actions.filter(
@@ -7461,7 +7493,11 @@ test("applied-network recovery spends the old intent and admits only explicit se
assert.match(source, /BLE-команда не повторяется/); assert.match(source, /BLE-команда не повторяется/);
assert.match( assert.match(
source, source,
/status=\{\s*searchActive[\s\S]*?: presentedPhysicalRecoveryRequired[\s\S]*?: networkRecoveryRequired[\s\S]*?>\s*\{searchActive \? \([\s\S]*?Поиск Bluetooth · \{scanSecondsRemaining \?\? 6\} с[\s\S]*?: networkRecoveryRequired && !connectionAttemptSettling \? \(/, /status=\{\s*searchActive[\s\S]*?: presentedPhysicalRecoveryRequired[\s\S]*?: presentedConnectionRecoveryRequired[\s\S]*?: networkRecoveryRequired/,
);
assert.match(
source,
/networkRecoveryRequired\s*&& !connectionAttemptSettling\s*&& !connectionRecoveryVerificationPending \? \(/,
); );
assert.match( assert.match(
@@ -29,6 +29,7 @@ let cameraTransportCallbackIsCurrent;
let cameraBrowserTransportIdentity; let cameraBrowserTransportIdentity;
let liveRerunRecoveryAuthorityIdentity; let liveRerunRecoveryAuthorityIdentity;
let initialObservationWindowRect; let initialObservationWindowRect;
let FloatingObservationWindow;
let ObservationTimeline; let ObservationTimeline;
let shouldCaptureWorkspacePointer; let shouldCaptureWorkspacePointer;
let normalizeTimelineRange; let normalizeTimelineRange;
@@ -86,9 +87,11 @@ before(async () => {
} = await server.ssrLoadModule( } = await server.ssrLoadModule(
"/src/core/observation/liveCameraRecovery.ts", "/src/core/observation/liveCameraRecovery.ts",
)); ));
({ initialObservationWindowRect, shouldCaptureWorkspacePointer } = await server.ssrLoadModule( ({
"/src/components/FloatingObservationWindow.tsx", FloatingObservationWindow,
)); initialObservationWindowRect,
shouldCaptureWorkspacePointer,
} = await server.ssrLoadModule("/src/components/FloatingObservationWindow.tsx"));
({ liveRerunRecoveryAuthorityIdentity } = await server.ssrLoadModule( ({ liveRerunRecoveryAuthorityIdentity } = await server.ssrLoadModule(
"/src/core/observation/liveReceiverWatchdog.ts", "/src/core/observation/liveReceiverWatchdog.ts",
)); ));
@@ -133,6 +136,59 @@ test("observation camera windows tile from the bottom-right above the live timel
assert.ok(right.y + right.height <= bounds.height - 64); assert.ok(right.y + right.height <= bounds.height - 64);
}); });
test("live camera window keeps its player mounted before viewport measurement", () => {
const source = {
id: "k1:sensor.camera.right",
sourceId: "sensor.camera.right",
label: "Правая камера",
description: "Live camera",
modality: "video",
availability: "streaming",
transport: "websocket",
endpointLabel: "Локальный видеопоток",
previewUrl: null,
delivery: {
id: "camera-preview-4",
kind: "mse-fmp4-websocket",
url: "/camera-preview/4",
mediaType: 'video/mp4; codecs="avc1.641028"',
},
activation: {
groupId: "k1:device-session:camera.preview.decoder",
maxActive: 1,
selected: true,
controllable: false,
},
binding: {
deviceId: "k1-a",
deviceSessionId: "device-session",
acquisitionId: "acquisition-quick",
},
capabilities: {
defaultVisible: true,
overlay: true,
resizable: true,
timelineMode: "live-only",
},
};
const noop = () => undefined;
const markup = renderToStaticMarkup(createElement(FloatingObservationWindow, {
source,
index: 0,
count: 1,
boundsRef: { current: null },
maximized: false,
active: true,
onRectChange: noop,
onMaximizedChange: noop,
onActivate: noop,
onClose: noop,
}));
assert.match(markup, /<video/);
assert.match(markup, /Правая камера/);
});
test("observation camera windows stack without overlap when the viewport is narrow", () => { test("observation camera windows stack without overlap when the viewport is narrow", () => {
const bounds = { width: 420, height: 700 }; const bounds = { width: 420, height: 700 };
const lower = initialObservationWindowRect(0, 2, bounds); const lower = initialObservationWindowRect(0, 2, bounds);
@@ -3265,16 +3265,16 @@ export function K1ProvisioningPipeline({
: savedBridgeNetworkSetupRequired : savedBridgeNetworkSetupRequired
? "Нужна настройка сети" ? "Нужна настройка сети"
: "Нужно выбрать действие" : "Нужно выбрать действие"
: networkRecoveryRequired
? appliedControlSettlementPending
? "Подтверждение управления"
: "Управление не подтверждено · выбор заблокирован"
: presentedConnectionRecoveryRequired : presentedConnectionRecoveryRequired
? connectionReconnectPending ? connectionReconnectPending
? "Переподключение…" ? "Переподключение…"
: connectionRecoveryTarget : connectionRecoveryTarget
? "Можно переподключиться" ? "Можно переподключиться"
: "Подключить заново" : "Подключить заново"
: networkRecoveryRequired
? appliedControlSettlementPending
? "Подтверждение управления"
: "Управление не подтверждено · выбор заблокирован"
: connectionEstablished : connectionEstablished
? "Подключение установлено" ? "Подключение установлено"
: explicitProvisioningDraftStale : explicitProvisioningDraftStale
@@ -3319,7 +3319,9 @@ export function K1ProvisioningPipeline({
Поиск Bluetooth · {scanSecondsRemaining ?? 6} с Поиск Bluetooth · {scanSecondsRemaining ?? 6} с
</strong> </strong>
</div> </div>
) : networkRecoveryRequired && !connectionAttemptSettling ? ( ) : networkRecoveryRequired
&& !connectionAttemptSettling
&& !connectionRecoveryVerificationPending ? (
<div className="connection-summary" role="status"> <div className="connection-summary" role="status">
<span> <span>
{appliedControlSettlementPending {appliedControlSettlementPending