feat(ui): add M4.8S replay with LiDAR overlay

This commit is contained in:
DCCONSTRUCTIONS
2026-08-25 16:44:52 +03:00
parent 3679e43fe3
commit 15be698097
17 changed files with 1448 additions and 31 deletions
@@ -44,6 +44,7 @@ import { M4ReplayThreatResultView } from "./M4ReplayThreatResult";
import { M47ReferenceGraphResultView } from "./M47ReferenceGraphResult";
import { M48ObjectCentricQualityResultView } from "./M48ObjectCentricQualityResult";
import { M48SmallStaticPassageRegressionResultView } from "./M48SmallStaticPassageRegressionResult";
import { M48SFixedClassDetectorResultView } from "./M48SFixedClassDetectorResult";
export { isAdvancedLaboratoryWorkId };
export type { AdvancedLaboratoryWorkId };
@@ -92,6 +93,9 @@ export function AdvancedLaboratoryResult({
if (workId === "m48-small-static-passage-regression" && results.m48SmallStatic) {
return <M48SmallStaticPassageRegressionResultView rigLabel={rigLabel} result={results.m48SmallStatic} />;
}
if (workId === "m48s-fixed-class-detector" && results.m48s) {
return <M48SFixedClassDetectorResultView rigLabel={rigLabel} result={results.m48s} />;
}
if (workId === "m47-reference-graph-shadow" && results.m47Graph) {
return <M47ReferenceGraphResultView rigLabel={rigLabel} result={results.m47Graph} />;
}
@@ -0,0 +1,103 @@
import {
LaboratoryEvidence,
LaboratoryResultSummary,
LaboratorySummary,
LaboratoryWorkTemplate,
} from "../../components/laboratory/LaboratoryPresentation";
import type { M48SFixedClassDetectorResult } from "../../core/laboratory/m48sFixedClassDetector";
import { M4ReplayThreatVisual } from "./M4ReplayThreatVisual";
function decimal(value: number, digits = 1): string {
return value.toLocaleString("ru-RU", { maximumFractionDigits: digits });
}
export function M48SFixedClassDetectorResultView({
rigLabel,
result,
}: {
rigLabel: string;
result: M48SFixedClassDetectorResult;
}) {
const selected = result.metrics.candidates.find((candidate) => candidate.selected);
const load = result.metrics.detectorLoad;
const integrated = result.metrics.integratedWorldState;
const status = integrated
? "Полный RF-DETR reference graph выдержал realtime shadow"
: "RF-DETR-L выдержал detector-only realtime shadow";
return (
<LaboratoryWorkTemplate
summary={(
<LaboratorySummary
title="M4.8S · fixed-class semantics риск-объектов"
description="Сравнение трёх готовых COCO-детекторов на точных кадрах RAVNOVES00, 30-минутная квалификация RF-DETR-L и полный source-paced прогон RF-DETR → geometry → temporal → motion → rolling map → threat на Worker 006. Статические препятствия остаются в геометрическом контуре; классы используются только там, где меняется ожидаемое поведение."
status={status}
statusTone="success"
facts={[
{ label: "Источник", value: `${rigLabel} RIGHT · raw KB4 · ${result.source.evidenceFrameCount} diagnostic frames` },
{ label: "Сравнение", value: "YOLOX-S · D-FINE-S · RF-DETR-L · единый threshold 0.50" },
{ label: "Worker", value: "Worker 006 · RTX 4090 · TensorRT 11 + isolated Triton" },
{ label: "Authority", value: "SHADOW ONLY · commands OFF · actuation OFF · production NO" },
]}
brief={{
question: "Можно ли заменить слабую class-семантику YOLOX готовой моделью, не потеряв realtime на предельном Worker с RTX 4090?",
approach: `YOLOX-S, D-FINE-S и RF-DETR-L сравнили на одинаковых ${result.source.evidenceFrameCount} raw-KB4 кадрах с порогом 0.50. RF-DETR-L отдельно квалифицировали ${decimal(load.durationSeconds / 60, 0)} минут, затем встроили в полный reference graph без дополнительного inference-прохода.`,
principalResult: integrated
? `Полный граф доставил ${integrated.deliveredWorldStates.toLocaleString("ru-RU")} world states при ${decimal(integrated.effectiveWorldStateFps, 3)} FPS и p95 ${decimal(integrated.worldStateCompletionAgeP95Ms, 3)} ms; ${integrated.supersededFrames} входных кадров штатно вытеснены latest-wins очередью.`
: `RF-DETR-L выбран из трёх кандидатов и обработал ${load.sourceFramesConsumed.toLocaleString("ru-RU")} кадров detector-only без замен и ошибок.`,
limitation: "Прогон доказывает runtime envelope, а не истинность классов, качество track identity, корректность risk policy или безопасность движения. Production authority и команды отключены.",
}}
method={{
completeness: result.method.completeness,
executionClass: result.method.executionClass,
pipelineId: result.method.pipelineId,
components: result.method.components.map((component) => ({
...component,
identitySha256: component.identitySha256,
})),
}}
/>
)}
evidence={(
<LaboratoryEvidence
eyebrow="M4.8S VISUAL EVIDENCE · FULL REFERENCE GRAPH REPLAY"
title="Полное видео: RF-DETR классы, LiDAR, 3D/PLAN и world-state на общем таймлайне"
kind="diagnostic-model"
resizable
>
<M4ReplayThreatVisual
resultId={result.resultId}
timelineEndpointRoot="/api/v1/laboratory/m48s/fixed-class-detector"
evidenceLabel="M4.8S RF-DETR GRAPH"
/>
</LaboratoryEvidence>
)}
result={(
<LaboratoryResultSummary
title={integrated
? "Что дал прогон: полный world-state graph проходит realtime envelope"
: "Что дал прогон: RF-DETR-L проходит detector-only realtime envelope"}
status={status}
statusTone="success"
metrics={integrated ? [
{ label: "Complete graph", value: `${decimal(integrated.effectiveWorldStateFps, 3)} FPS`, hint: "target ≥ 9.5 FPS · source-paced" },
{ label: "World-state age p95", value: `${decimal(integrated.worldStateCompletionAgeP95Ms, 3)} ms`, hint: `p99 ${decimal(integrated.worldStateCompletionAgeP99Ms, 3)} ms · target ≤ 175 ms` },
{ label: "Delivered / superseded", value: `${integrated.deliveredWorldStates.toLocaleString("ru-RU")} / ${integrated.supersededFrames}`, hint: `${integrated.failures} failures · queues ${Math.max(...Object.values(integrated.queueHighWatermarks))}/${integrated.queueCapacity}` },
{ label: "GPU / VRAM peak", value: `${decimal(integrated.gpuUtilizationMaximumPercent, 0)}% / ${decimal(integrated.gpuMemoryMaximumMib / 1024)} GiB`, hint: `GPU mean ${decimal(integrated.gpuUtilizationMeanPercent)}% · ${decimal(integrated.gpuPowerMaximumW)} W` },
] : [
{ label: "Detector capacity", value: `${decimal(selected?.capacityFps ?? 0)} FPS`, hint: "RF-DETR-L TensorRT/Triton" },
{ label: "Completion age p95", value: `${decimal(load.completionAgeP95Ms)} ms`, hint: "detector-only · target ≤ 175 ms" },
{ label: "Consumed / replaced", value: `${load.sourceFramesConsumed.toLocaleString("ru-RU")} / ${load.sourceFrameReplacements}`, hint: `${decimal(load.effectiveConsumedFps, 3)} source FPS · ${load.failures} failures` },
{ label: "GPU / VRAM peak", value: `${decimal(load.gpuUtilizationMaximumPercent, 0)}% / ${decimal(load.gpuMemoryMaximumMib / 1024)} GiB`, hint: `GPU mean ${decimal(load.gpuUtilizationMeanPercent)}% · queue ${load.queueMaximumDepth}/${load.queueCapacity}` },
]}
conclusion={{
proved: integrated
? `На Worker 006 полный граф обработал ${integrated.sourceFramesAdmitted.toLocaleString("ru-RU")} входных кадров, доставил ${integrated.deliveredWorldStates.toLocaleString("ru-RU")} состояний без ошибок, удержал все очереди в пределах ${integrated.queueCapacity} и p95 ${decimal(integrated.worldStateCompletionAgeP95Ms, 3)} ms. Advisory сформировал публикации по семействам: geometry-only (${integrated.advisoryFamilyCounts["generic-obstacle"].toLocaleString("ru-RU")}), люди (${integrated.advisoryFamilyCounts.person.toLocaleString("ru-RU")}), животные (${integrated.advisoryFamilyCounts.animal.toLocaleString("ru-RU")}) и транспорт (${integrated.advisoryFamilyCounts.vehicle.toLocaleString("ru-RU")}); это не количество уникальных физических объектов и не потребовало второго inference.`
: `RF-DETR-L ${decimal(load.durationSeconds / 60, 0)} минут устойчиво потреблял source-paced поток около 10 FPS: ${load.sourceFramesConsumed.toLocaleString("ru-RU")} кадров, 0 замен, 0 ошибок, completion-age p95 ${decimal(load.completionAgeP95Ms, 3)} ms.`,
notProved: "Не доказаны unbiased precision/recall классов, независимое качество track identity и risk policy, поведение planner или collision safety. Кадровые рамки не заменяют геометрическую occupancy-карту.",
decision: "Сохранить RF-DETR-L как risk-semantic shadow provider полного reference graph. Не классифицировать миллионы статических форм: неизвестное неподвижное препятствие остаётся geometry-owned и объезжается; классы сохраняются для людей, животных и транспорта. Production switch не разрешён.",
}}
/>
)}
/>
);
}
@@ -0,0 +1,168 @@
import { useEffect, useMemo, useState } from "react";
import { Icon, IconButton, StatusBadge } from "@nodedc/ui-react";
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
import {
RecordedEvidenceBoxOverlay,
type RecordedEvidenceBox,
type RecordedEvidenceBoxTone,
} from "../../components/laboratory/RecordedEvidenceBoxOverlay";
import {
fetchM48SFixedClassDetectorFrame,
type M48SDetectorFrame,
type M48SDetectorMode,
type M48SFixedClassDetectorResult,
} from "../../core/laboratory/m48sFixedClassDetector";
const MODES = [
{ value: "source", label: "SOURCE" },
{ value: "yolox", label: "YOLOX" },
{ value: "dfine", label: "D-FINE" },
{ value: "rf-detr", label: "RF-DETR" },
] as const;
const ANIMAL_LABELS = new Set([
"bird",
"cat",
"dog",
"horse",
"sheep",
"cow",
"elephant",
"bear",
"zebra",
"giraffe",
]);
const VULNERABLE_ROAD_USERS = new Set(["person", "bicycle", "motorcycle", "skateboard"]);
function message(error: unknown): string {
return error instanceof Error && error.message.trim()
? error.message
: "M4.8S visual evidence недоступно.";
}
function toneForLabel(label: string): RecordedEvidenceBoxTone {
if (ANIMAL_LABELS.has(label)) return "danger";
if (VULNERABLE_ROAD_USERS.has(label)) return "warning";
return "accent";
}
function DetectorScene({ frame, mode }: { frame: M48SDetectorFrame; mode: M48SDetectorMode }) {
const boxes = useMemo<readonly RecordedEvidenceBox[]>(() => {
if (mode === "source") return [];
return frame.detections[mode].map((detection) => ({
boxXyxy: detection.bboxXyxy,
label: `${detection.label} · ${detection.score.toFixed(2)}`,
tone: toneForLabel(detection.label),
}));
}, [frame, mode]);
return (
<div className="m48-atlas-visual__scene">
<img src={frame.cameraUrl} alt="" draggable={false} />
<RecordedEvidenceBoxOverlay
imageWidth={frame.imageWidth}
imageHeight={frame.imageHeight}
boxes={boxes}
ariaLabel={`M4.8S ${mode} fixed-class detections`}
/>
</div>
);
}
export function M48SFixedClassDetectorVisual({
result,
}: {
result: M48SFixedClassDetectorResult;
}) {
const preferredIndex = Math.max(
0,
result.frames.findIndex((frame) => frame.frameId === "000253"),
);
const [index, setIndex] = useState(preferredIndex);
const [frame, setFrame] = useState<M48SDetectorFrame | null>(null);
const [mode, setMode] = useState<M48SDetectorMode>("rf-detr");
const [expanded, setExpanded] = useState(false);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const selected = result.frames[index] ?? null;
useEffect(() => {
if (!selected) {
setFrame(null);
setLoading(false);
return;
}
const controller = new AbortController();
setLoading(true);
setError(null);
void fetchM48SFixedClassDetectorFrame(result.resultId, selected.frameId, {
signal: controller.signal,
})
.then((next) => !controller.signal.aborted && setFrame(next))
.catch((caught: unknown) => !controller.signal.aborted && setError(message(caught)))
.finally(() => !controller.signal.aborted && setLoading(false));
return () => controller.abort();
}, [result.resultId, selected]);
const count = frame && mode !== "source" ? frame.detections[mode].length : 0;
return (
<LaboratoryEvidenceViewer
label="M4.8S fixed-class detector comparison"
className="m48-atlas-visual"
mode={mode}
modes={MODES}
expanded={expanded}
onModeChange={setMode}
onExpandedChange={setExpanded}
actions={(
<>
<IconButton
label="Предыдущий кадр M4.8S"
disabled={!result.frames.length}
onClick={() => setIndex((current) => (
current - 1 + result.frames.length
) % result.frames.length)}
>
<Icon name="chevron-left" size={16} />
</IconButton>
<IconButton
label="Следующий кадр M4.8S"
disabled={!result.frames.length}
onClick={() => setIndex((current) => (current + 1) % result.frames.length)}
>
<Icon name="chevron-right" size={16} />
</IconButton>
</>
)}
overlay={selected ? (
<div className="m48-atlas-visual__case">
<StatusBadge tone="warning">SHADOW ONLY</StatusBadge>
<strong>RAVNOVES00 · frame {selected.frameId} · {mode.toUpperCase()}</strong>
<small>
{count} risk detections · threshold 0.50 · independent ground truth отсутствует
</small>
</div>
) : null}
>
{loading ? (
<div className="m48-atlas-visual__state" role="status">
<span className="busy-indicator" aria-hidden="true" />
Загружаем точный camera-кадр
</div>
) : error ? (
<div className="m48-atlas-visual__state" role="alert">
<Icon name="alert" size={18} />
{error}
</div>
) : frame ? (
<DetectorScene frame={frame} mode={mode} />
) : (
<div className="m48-atlas-visual__state" role="alert">
<Icon name="alert" size={18} />
Каталог кадров M4.8S пуст.
</div>
)}
</LaboratoryEvidenceViewer>
);
}
@@ -17,6 +17,7 @@ import {
} from "../../components/laboratory/LaboratoryMetricEvidenceScene";
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
import { RecordedEvidenceImageScene } from "../../components/laboratory/RecordedEvidenceImageScene";
import type { RecordedEvidencePointCloudOverlayData } from "../../components/laboratory/RecordedEvidencePointCloudOverlay";
import type {
RecordedEvidenceSemanticClass,
RecordedEvidenceSemanticOverlay,
@@ -58,10 +59,14 @@ function toneForProposal(proposal: M4ThreatCameraProposal): RecordedEvidenceBox[
}
function proposalLabel(proposal: M4ThreatCameraProposal): string {
const decision = proposal.threatDecision ?? "unknown";
if (proposal.rangeM === null) return decision;
const decision = proposal.threatDecision
?? (proposal.occupiedSupport ? "geometry-supported" : "camera-only");
const semantic = proposal.semanticHint ?? "object";
if (proposal.rangeM === null) {
return `${semantic} · ${proposal.objectness.toFixed(2)} · ${decision}`;
}
const range = `${proposal.rangeM.toLocaleString("ru-RU", { maximumFractionDigits: 2 })} м`;
return `${range} · ${decision}`;
return `${semantic} · ${proposal.objectness.toFixed(2)} · ${range} · ${decision}`;
}
function boxes(proposals: readonly M4ThreatCameraProposal[]): readonly RecordedEvidenceBox[] {
@@ -104,10 +109,14 @@ export function M4ReplayThreatVisual({
resultId,
semantic,
reviewAnchors = EMPTY_REVIEW_ANCHORS,
timelineEndpointRoot,
evidenceLabel = "M4.6",
}: {
resultId: string;
semantic?: M4ReplayThreatSemanticLayer;
reviewAnchors?: readonly M4ReplayThreatReviewAnchor[];
timelineEndpointRoot?: string;
evidenceLabel?: string;
}) {
const [mediaMode, setMediaMode] = useState<M4ThreatMediaMode | null>("video");
const [spatialMode, setSpatialMode] = useState<LaboratoryMetricSceneMode | null>(null);
@@ -116,6 +125,7 @@ export function M4ReplayThreatVisual({
const [showRollingMap, setShowRollingMap] = useState(true);
const [showMediaSemantic, setShowMediaSemantic] = useState(true);
const [showSpatialSemantic, setShowSpatialSemantic] = useState(true);
const [showMediaPoints, setShowMediaPoints] = useState(false);
const [splitPrimarySize, setSplitPrimarySize] = useState(50);
const [splitOrientation, setSplitOrientation] = useState<SplitPaneOrientation>(() => (
typeof window !== "undefined" && window.matchMedia("(max-width: 900px)").matches
@@ -125,7 +135,7 @@ export function M4ReplayThreatVisual({
const [expanded, setExpanded] = useState(false);
const [selectedReviewAnchorIndex, setSelectedReviewAnchorIndex] = useState(0);
const metricSceneRef = useRef<LaboratoryMetricEvidenceSceneHandle | null>(null);
const metadata = useM4ThreatTimelineMetadata(resultId);
const metadata = useM4ThreatTimelineMetadata(resultId, timelineEndpointRoot);
const playbackRange = useMemo(() => metadata.timeline ? ({
startSeconds: metadata.timeline.timelineStartSeconds,
endSeconds: metadata.timeline.timelineEndSeconds,
@@ -139,6 +149,7 @@ export function M4ReplayThreatVisual({
resultId,
timeline: metadata.timeline,
currentSeconds: playbackController.playback.currentSeconds,
endpointRoot: timelineEndpointRoot,
});
const [videoSource, setVideoSource] = useState<ObservationSourceDescriptor | null>(null);
const [videoLoading, setVideoLoading] = useState(false);
@@ -362,6 +373,16 @@ export function M4ReplayThreatVisual({
ariaLabel: `E47 semantic mask frame ${frame.sequence + 1}`,
}
: undefined;
const pointCloudOverlay: RecordedEvidencePointCloudOverlayData | undefined =
showMediaPoints && frame?.cameraProjection === "factory-kb4-exact"
? {
pointsXyd: frame.cameraProjectedPointsXyd,
sourcePointCount: frame.cameraProjectedSourceCount,
projectedPointCount: frame.cameraProjectedPointCount,
projection: "factory-kb4-exact",
ariaLabel: `${evidenceLabel} LiDAR projection: ${frame.cameraProjectedSampleCount} points`,
}
: undefined;
const handleMediaModeChange = (next: M4ThreatMediaSelection) => {
if (next === "none") return;
@@ -408,21 +429,35 @@ export function M4ReplayThreatVisual({
</div>
);
const mediaLayerControls = semantic ? (
const mediaLayerControls = semantic || metadata.timeline?.cameraPointDelivery ? (
<div
className="m4-replay-threat-visual__pane-layer-controls"
role="group"
aria-label="Слои камеры и видео"
>
<Button
size="compact"
shape="pill"
variant={showMediaSemantic ? "primary" : "secondary"}
aria-pressed={showMediaSemantic}
onClick={() => setShowMediaSemantic((visible) => !visible)}
>
SEMANTICS
</Button>
{semantic ? (
<Button
size="compact"
shape="pill"
variant={showMediaSemantic ? "primary" : "secondary"}
aria-pressed={showMediaSemantic}
onClick={() => setShowMediaSemantic((visible) => !visible)}
>
SEMANTICS
</Button>
) : null}
{metadata.timeline?.cameraPointDelivery ? (
<Button
size="compact"
shape="pill"
variant={showMediaPoints ? "primary" : "secondary"}
aria-pressed={showMediaPoints}
title="Exact LiDAR increment · factory KB4 camera projection"
onClick={() => setShowMediaPoints((visible) => !visible)}
>
POINTS
</Button>
) : null}
</div>
) : null;
@@ -570,6 +605,12 @@ export function M4ReplayThreatVisual({
{spatialFrame
? `${spatialFrame.pointCloudSampleCount}/${spatialFrame.pointCloudSourceCount} exact · ${localSurface.pointsBodyXyzM.length} local SLAM / ${localSurface.sourceFrameCount} frames`
: "квалифицированный spatial frame ещё не получен"}
{frame.worldStateAvailable
? " · world-state delivered"
: ` · world-state gap (${frame.terminalOutcome})`}
{pointCloudOverlay
? ` · camera points ${frame.cameraProjectedSampleCount}/${frame.cameraProjectedPointCount}`
: ""}
{semantic && spatialSemanticFrame
? ` · semantic L ${spatialSemanticFrame.counts.labeled} · A ${spatialSemanticFrame.counts.ambiguous} · U ${spatialSemanticFrame.counts.unprojected} · Ø ${spatialSemanticFrame.counts.absent}`
: semantic ? " · semantic buffer" : ""}
@@ -595,7 +636,7 @@ export function M4ReplayThreatVisual({
content = (
<div className="l3-visual-audit__state" role="status">
<span className="busy-indicator" aria-hidden="true" />
<span>Открываем recorded-realtime timeline M4.6</span>
<span>Открываем recorded-realtime timeline {evidenceLabel}</span>
</div>
);
} else {
@@ -628,7 +669,8 @@ export function M4ReplayThreatVisual({
imageHeight={timeline.imageHeight}
boxes={activeBoxes}
semanticOverlay={mediaMode === "video" ? semanticOverlay : undefined}
ariaLabel={`M4.6 recorded-realtime frame ${frame?.sequence ?? 0}: ${activeBoxes.length} proposals`}
pointCloudOverlay={mediaMode === "video" ? pointCloudOverlay : undefined}
ariaLabel={`${evidenceLabel} recorded-realtime frame ${frame?.sequence ?? 0}: ${activeBoxes.length} proposals`}
interactive={false}
segmentSequence={
timelineFrame.activeSequence === null
@@ -655,7 +697,8 @@ export function M4ReplayThreatVisual({
imageHeight={timeline.imageHeight}
boxes={activeBoxes}
semanticOverlay={semanticOverlay}
ariaLabel={`M4.6 exact camera frame ${frame.sequence}: ${activeBoxes.length} proposals`}
pointCloudOverlay={pointCloudOverlay}
ariaLabel={`${evidenceLabel} exact camera frame ${frame.sequence}: ${activeBoxes.length} proposals`}
/>
) : null}
</section>
@@ -689,7 +732,7 @@ export function M4ReplayThreatVisual({
corridor={timeline.corridor}
occupiedVoxelSizeM={timeline.occupiedVoxelSizeM}
mode={spatialMode}
label="M4.6 exact current increment, bounded local SLAM surface and rolling occupancy"
label={`${evidenceLabel} exact current increment, bounded local SLAM surface and rolling occupancy`}
showCurrentIncrement={showCurrentIncrement}
showLocalSurface={showLocalSurface}
showRollingMap={showRollingMap}
@@ -791,7 +834,7 @@ export function M4ReplayThreatVisual({
<LaboratoryEvidenceViewer
label={semantic
? "E47 semantic + SLAM diagnostic replay"
: "M4.6 dual-evidence recorded-realtime replay"}
: `${evidenceLabel} recorded-realtime replay`}
className="m4-replay-threat-evidence-viewer"
mode={mediaMode ?? "none"}
modes={[
@@ -77,6 +77,13 @@ const KNOWN_WORKS: Readonly<Record<Exclude<LaboratoryWorkId, `session:${string}`
experimentName: "M4.8 · small static passage regression",
variantName: "M4.8R1 · Worker 006 small-static assisted baseline",
},
"m48s-fixed-class-detector": {
profileId: "rig-ravnoves-perception-gate-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · RAVNOVES00 perception gate`,
experimentId: "m48s-fixed-class-risk-detector",
experimentName: "RAVNOVES00 fixed-class risk detector",
variantName: "M4.8S · RF-DETR-L TensorRT/Triton shadow",
},
"m47-reference-graph-shadow": {
profileId: "rig-dual-evidence-virtual-corridor-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · Camera + LiDAR dual evidence`,
@@ -21,6 +21,7 @@ function mergeResults(
m47Graph: next.m47Graph ?? current.m47Graph,
m48: next.m48 ?? current.m48,
m48SmallStatic: next.m48SmallStatic ?? current.m48SmallStatic,
m48s: next.m48s ?? current.m48s,
m4Threat: next.m4Threat ?? current.m4Threat,
l3: next.l3 ?? current.l3,
l31: next.l31 ?? current.l31,
@@ -41,7 +41,7 @@ export function cancelM4ThreatChunkRequestsOutsideWindow<T extends { abort(): vo
}
}
export function useM4ThreatTimelineMetadata(resultId: string) {
export function useM4ThreatTimelineMetadata(resultId: string, endpointRoot?: string) {
const [timeline, setTimeline] = useState<M4ThreatTimeline | null>(null);
const [error, setError] = useState<string | null>(null);
@@ -49,7 +49,7 @@ export function useM4ThreatTimelineMetadata(resultId: string) {
const controller = new AbortController();
setTimeline(null);
setError(null);
void fetchM4ThreatTimeline(resultId, { signal: controller.signal })
void fetchM4ThreatTimeline(resultId, { signal: controller.signal, endpointRoot })
.then((next) => {
if (!controller.signal.aborted) setTimeline(next);
})
@@ -59,7 +59,7 @@ export function useM4ThreatTimelineMetadata(resultId: string) {
}
});
return () => controller.abort();
}, [resultId]);
}, [endpointRoot, resultId]);
return { timeline, loading: !timeline && !error, error };
}
@@ -68,10 +68,12 @@ export function useM4ThreatTimelineFrame({
resultId,
timeline,
currentSeconds,
endpointRoot,
}: {
resultId: string;
timeline: M4ThreatTimeline | null;
currentSeconds: number;
endpointRoot?: string;
}) {
const [chunks, setChunks] = useState<ReadonlyMap<number, M4ThreatTimelineChunk>>(
() => new Map(),
@@ -124,6 +126,7 @@ export function useM4ThreatTimelineFrame({
inFlight.current.set(start, controller);
void fetchM4ThreatTimelineChunk(resultId, start, chunkSize, {
signal: controller.signal,
endpointRoot,
})
.then((chunk) => {
if (controller.signal.aborted) return;
@@ -151,7 +154,7 @@ export function useM4ThreatTimelineFrame({
if (inFlight.current.get(start) === controller) inFlight.current.delete(start);
});
}
}, [activeChunkStart, chunkSize, resultId, timeline]);
}, [activeChunkStart, chunkSize, endpointRoot, resultId, timeline]);
const activeFrame: M4ThreatTimelineFrame | null = useMemo(() => {
if (activeSequence === null || activeChunkStart === null) return null;