From 71c85e98944767145e4972e8193b4edf420bf936 Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Sun, 19 Jul 2026 01:07:21 +0300 Subject: [PATCH] feat(archive): complete recorded session lifecycle --- apps/control-station/src/App.tsx | 25 +++- .../components/ObservationSessionSelect.tsx | 91 ++++++++++---- .../src/components/ObservationSources.tsx | 8 +- .../src/components/RecordedFmp4Player.tsx | 59 ++++------ .../observation/recordedSessionAdmission.ts | 8 +- .../src/core/observation/sessionArchive.ts | 40 +++++++ .../core/observation/useObservationLayout.ts | 30 +++++ .../observation/useObservationSessions.ts | 90 +++++++------- .../src/styles/observation-sessions.css | 37 ++++++ .../src/workspaces/Workspaces.tsx | 30 +++-- .../test/observationSessions.test.mjs | 66 +++++++++++ .../test/recordedSessionAdmission.test.mjs | 6 +- .../test/workspaceLayout.test.mjs | 38 +++++- src/k1link/sessions/media.py | 32 ++++- src/k1link/sessions/preparation.py | 104 ++++++++++++++-- src/k1link/sessions/recording.py | 24 ++++ src/k1link/sessions/store.py | 68 ++++++++++- src/k1link/web/session_api.py | 73 ++++++++++++ tests/test_session_api.py | 111 ++++++++++++++++++ tests/test_session_preparation.py | 35 ++++++ tests/test_session_recording.py | 19 +++ tests/test_session_store.py | 72 ++++++++++++ 22 files changed, 922 insertions(+), 144 deletions(-) diff --git a/apps/control-station/src/App.tsx b/apps/control-station/src/App.tsx index 1684e44..a7a9be3 100644 --- a/apps/control-station/src/App.tsx +++ b/apps/control-station/src/App.tsx @@ -40,7 +40,10 @@ import { } from "./core/runtime/latestAsyncCommitter"; import { useObservationLayout } from "./core/observation/useObservationLayout"; import { recordedObservationSources } from "./core/observation/recordedObservationSources"; -import type { ObservationSessionReplayLaunch } from "./core/observation/sessionArchive"; +import type { + ObservationSessionReplayLaunch, + ObservationSessionSummary, +} from "./core/observation/sessionArchive"; import { useRecordedSessionAdmission } from "./core/observation/useRecordedSessionAdmission"; import { useWorkspaceLayoutProfile } from "./core/observation/useWorkspaceLayoutProfile"; import { @@ -136,6 +139,7 @@ export default function App() { const [activeRoot, setActiveRoot] = useState(null); const [sourceUrl, setSourceUrl] = useState(""); const [recordedReplay, setRecordedReplay] = useState(null); + const [recordedReplayLabel, setRecordedReplayLabel] = useState(null); const [replayTransitioning, setReplayTransitioning] = useState(false); const [sourceDraft, setSourceDraft] = useState(""); const [sourceWindowOpen, setSourceWindowOpen] = useState(false); @@ -447,6 +451,7 @@ export default function App() { // accepting the already-ready archive. setReplayTransitioning(true); setRecordedReplay(null); + setRecordedReplayLabel(null); setSourceUrl(""); setSourceDraft(""); await new Promise((resolve) => { @@ -454,11 +459,15 @@ export default function App() { }); }, []); - const acceptRecordedReplay = useCallback((launch: ObservationSessionReplayLaunch) => { + const acceptRecordedReplay = useCallback(( + session: ObservationSessionSummary, + launch: ObservationSessionReplayLaunch, + ) => { if (sourceSwitchBlockedRef.current) { throw new Error(SPATIAL_SOURCE_SWITCH_BLOCKED_REASON); } setRecordedReplay(launch); + setRecordedReplayLabel(session.label); setSourceUrl(launch.sourceUrl); setSourceDraft(launch.sourceUrl); setReplayTransitioning(false); @@ -475,6 +484,7 @@ export default function App() { // switch, so it intentionally does not consult the acquisition guard. setReplayTransitioning(false); setRecordedReplay(null); + setRecordedReplayLabel(null); setSourceUrl(""); setSourceDraft(""); }, []); @@ -634,7 +644,11 @@ export default function App() { acceptRecordedReplay(launch)} + onReplayAccepted={acceptRecordedReplay} onReplaySettled={(_session, outcome) => settleRecordedReplaySwitch(outcome)} /> {layoutSaveNotice || workspaceLayoutProfile.error ? ( @@ -739,6 +752,7 @@ export default function App() { setSourceDraft(""); setSourceUrl(""); setRecordedReplay(null); + setRecordedReplayLabel(null); }} > Сбросить адрес @@ -752,6 +766,7 @@ export default function App() { if (sourceSwitchBlockedRef.current) return; setSourceUrl(sourceDraft.trim()); setRecordedReplay(null); + setRecordedReplayLabel(null); }} > Применить адрес diff --git a/apps/control-station/src/components/ObservationSessionSelect.tsx b/apps/control-station/src/components/ObservationSessionSelect.tsx index 440e1e4..9381606 100644 --- a/apps/control-station/src/components/ObservationSessionSelect.tsx +++ b/apps/control-station/src/components/ObservationSessionSelect.tsx @@ -1,4 +1,5 @@ -import { Dropdown, Icon } from "@nodedc/ui-react"; +import { useState } from "react"; +import { ConfirmationModal, Dropdown, Icon } from "@nodedc/ui-react"; import { type ObservationSessionReplayLaunch, @@ -93,7 +94,7 @@ function sessionDescription(session: ObservationSessionSummary): string { } export function ObservationSessionSelect({ - limit = 3, + limit = 100, disabled = false, blockedReason = null, onReplayBegin, @@ -116,6 +117,7 @@ export function ObservationSessionSelect({ outcome: ObservationReplayOutcome, ) => void | Promise; }) { + const [deleteTarget, setDeleteTarget] = useState(null); const sessions = useObservationSessions({ limit, replayEnabled: blockedReason === null, @@ -130,7 +132,7 @@ export function ObservationSessionSelect({ : "Сохранённые сессии"; const presentedTriggerCopy = blockedReason ?? triggerCopy; - return ( + return <> {sessions.items.map((session) => { const pending = sessions.replayingSessionId === session.id; + const deleting = sessions.deletingSessionId === session.id; const failed = sessions.failedSessionId === session.id; const visualState = observationSessionVisualState(session, { pending, failed }); return ( -