diff --git a/app/features/scanner/components/EventsSummary.tsx b/app/features/scanner/components/EventsSummary.tsx index 695161f7c..f5babfff1 100644 --- a/app/features/scanner/components/EventsSummary.tsx +++ b/app/features/scanner/components/EventsSummary.tsx @@ -1,7 +1,8 @@ /** - * One light line summarizing a scan's raw detections as per-type counts, - * with a toggle for the full event card feed — the matches are the main - * view, the events stay one click away. + * One light line summarizing raw detections as per-type counts, with a + * toggle for their event card feed. Callers pass only events not covered + * by a match card — a fallback for detections not yet grouped, hidden + * entirely once everything lives in a match. */ import { DEATH_EVENT_TYPE } from "../core/detectors/death/index"; diff --git a/app/features/scanner/components/LivePage.tsx b/app/features/scanner/components/LivePage.tsx index 077245aeb..0ca736126 100644 --- a/app/features/scanner/components/LivePage.tsx +++ b/app/features/scanner/components/LivePage.tsx @@ -207,6 +207,10 @@ export function LivePage({ } }, [deviceId, refreshFeed, send]); + const builtMatches = buildScannerMatches(feed); + const groupedEvents = new Set(builtMatches.flatMap((b) => b.sources)); + const ungroupedFeed = feed.filter((e) => !groupedEvents.has(e)); + const stop = useCallback(() => { stopRef.current?.(); stopRef.current = null; @@ -314,55 +318,53 @@ export function LivePage({ {feed.length === 0 ? (

No detections yet.

) : null} - {[...buildScannerMatches(feed)] - .reverse() - .map((built, reverseIndex) => { - const id = built.sources[0]!.id!; - const ingestable = isIngestableMatch(built.match); - return ( - void send(matchContaining(id), { manual: true }) - : undefined - } - > - {withoutRepeatEvents(built.sources).map((e) => ( - loadEventFrame(e.id!) - : undefined - } - /> - ))} - - ); - })} - {feed.length > 0 ? ( + {[...builtMatches].reverse().map((built, reverseIndex) => { + const id = built.sources[0]!.id!; + const ingestable = isIngestableMatch(built.match); + return ( + void send(matchContaining(id), { manual: true }) + : undefined + } + > + {withoutRepeatEvents(built.sources).map((e) => ( + loadEventFrame(e.id!) + : undefined + } + /> + ))} + + ); + })} + {ungroupedFeed.length > 0 ? ( setEventsOpen(!eventsOpen)} /> ) : null} {eventsOpen - ? feed.map((e) => ( + ? ungroupedFeed.map((e) => ( m.event)); const vodMatchByEvent = new Map(matches.map((m) => [m.event, m] as const)); + const groupedEvents = new Set(builtMatches.flatMap((b) => b.sources)); + const ungroupedMatches = matches.filter((m) => !groupedEvents.has(m.event)); // "Upload as results" sends the whole scan in one go, so its outcome maps // onto every ingestable card; a partial failure (some chunks sent, some @@ -607,20 +609,20 @@ export function VodPage({ ); })} - {matches.length > 0 ? ( + {ungroupedMatches.length > 0 ? ( m.event)} + events={ungroupedMatches.map((m) => m.event)} open={eventsOpen} onToggle={() => setEventsOpen(!eventsOpen)} /> ) : null} {/* newest detection on top; storage keeps ascending video-time order */} {eventsOpen - ? [...matches] + ? [...ungroupedMatches] .reverse() .map((m, i) => ( { - return db().then( - (database) => - new Promise((resolve, reject) => { - const transaction = database.transaction( - [EVENTS_STORE, FRAMES_STORE], - "readwrite", - ); - transaction.objectStore(EVENTS_STORE).delete(id); - transaction.objectStore(FRAMES_STORE).delete(id); - transaction.oncomplete = () => resolve(); - transaction.onerror = () => reject(transaction.error); - }), - ); -} - export function listEvents(): Promise { return tx( EVENTS_STORE,