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,