feat(scan): card-ready gating replaces the fixed settle delay
Adds src/lib/cardReadyGate.ts: after a tile click, poll the detail fingerprint until it has both changed from the previous artifact and stabilized across consecutive samples, instead of waiting a hardcoded 280ms and hoping. - Faster on quick machines (proceeds as soon as the card is stable), correct on slow ones (waits up to the budget). - Robust to particle/hover-glow animation: requiring two consecutive equal samples ignores single-frame noise, and if the card never fully stabilizes it still proceeds once the content has changed rather than looping on an animated frame. - ESC/stop abort is honored between polls via checkAbort. autoScanLoop now uses waitForCardReady for both the initial read and the one retry; CLICK_SETTLE_MS removed. 6 new unit tests; 94 total green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+29
-16
@@ -3,6 +3,7 @@ import type { ParsedArtifactCandidate } from "./artifactOcrParser";
|
||||
import { sessionSignature } from "./artifactStore";
|
||||
import { buildGridModel, buildInventoryPagePlan, type GridTarget } from "./automationPlanner";
|
||||
import { classifyAutoScanCapture, shouldAbortAfterConsecutiveMisses } from "./autoScanController";
|
||||
import { waitForCardReady } from "./cardReadyGate";
|
||||
import { captureRejectionReason, captureSourceRejectionReason, shouldPersistParsedArtifact } from "./scannerCaptureQuality";
|
||||
import type { AutoScanStats, ScanSummary } from "./scannerSession";
|
||||
import { clampSkipRows, emptyAutoScanStats, resolveScanTargetCount } from "./scannerSession";
|
||||
@@ -59,7 +60,11 @@ export type AutoScanLoopResult = {
|
||||
targetCount: number;
|
||||
};
|
||||
|
||||
const CLICK_SETTLE_MS = 280;
|
||||
// Card-ready gating replaces a fixed settle delay: poll the detail fingerprint
|
||||
// until it has changed and stabilized (or the budget is spent). See cardReadyGate.
|
||||
const CARD_READY_MAX_MS = 900;
|
||||
const CARD_READY_POLL_MS = 90;
|
||||
const CARD_READY_STABLE_SAMPLES = 2;
|
||||
const MISS_ABORT_THRESHOLD = 3;
|
||||
const UNREADABLE_ABORT_THRESHOLD = 5;
|
||||
|
||||
@@ -162,6 +167,19 @@ export async function runAutoScanLoop(
|
||||
if (initialParsed) lastDetailSignature = sessionSignature(initialParsed);
|
||||
let lastDetailViewFingerprint = detailFingerprint(currentCapture);
|
||||
|
||||
async function awaitCardReady() {
|
||||
return waitForCardReady(
|
||||
{
|
||||
sampleFingerprint: async () => detailFingerprint(await captureFastSelectedSource(0, true)),
|
||||
wait,
|
||||
now: () => Date.now(),
|
||||
checkAbort: checkGuard,
|
||||
},
|
||||
lastDetailViewFingerprint,
|
||||
{ minStableSamples: CARD_READY_STABLE_SAMPLES, maxWaitMs: CARD_READY_MAX_MS, pollIntervalMs: CARD_READY_POLL_MS },
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
while (!blockedReason && !shouldStop() && stats.clicked < maxTargets) {
|
||||
page++;
|
||||
@@ -224,16 +242,13 @@ export async function runAutoScanLoop(
|
||||
break;
|
||||
}
|
||||
|
||||
let waitStop = await waitDuringScan(CLICK_SETTLE_MS);
|
||||
if (waitStop) {
|
||||
blockedReason = waitStop;
|
||||
let ready = await awaitCardReady();
|
||||
if (ready.abortReason) {
|
||||
blockedReason = ready.abortReason;
|
||||
aborted = true;
|
||||
break;
|
||||
}
|
||||
|
||||
let previewCapture = await captureFastSelectedSource(0, true);
|
||||
let previewFingerprint = detailFingerprint(previewCapture);
|
||||
let changedDetail = Boolean(previewFingerprint && previewFingerprint !== lastDetailViewFingerprint);
|
||||
let changedDetail = ready.changed;
|
||||
|
||||
if (!changedDetail) {
|
||||
appendAutomationLog(`retry r${target.row} c${target.col}: Detailansicht unveraendert`);
|
||||
@@ -244,15 +259,13 @@ export async function runAutoScanLoop(
|
||||
aborted = true;
|
||||
break;
|
||||
}
|
||||
waitStop = await waitDuringScan(CLICK_SETTLE_MS);
|
||||
if (waitStop) {
|
||||
blockedReason = waitStop;
|
||||
ready = await awaitCardReady();
|
||||
if (ready.abortReason) {
|
||||
blockedReason = ready.abortReason;
|
||||
aborted = true;
|
||||
break;
|
||||
}
|
||||
previewCapture = await captureFastSelectedSource(0, true);
|
||||
previewFingerprint = detailFingerprint(previewCapture);
|
||||
changedDetail = Boolean(previewFingerprint && previewFingerprint !== lastDetailViewFingerprint);
|
||||
changedDetail = ready.changed;
|
||||
}
|
||||
|
||||
if (!changedDetail) {
|
||||
@@ -482,6 +495,6 @@ export function fingerprintDataUrl(dataUrl: string) {
|
||||
return `${dataUrl.length.toString(16)}:${(hash >>> 0).toString(16)}`;
|
||||
}
|
||||
|
||||
function wait(ms: number) {
|
||||
return new Promise((resolve) => window.setTimeout(resolve, ms));
|
||||
function wait(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => window.setTimeout(() => resolve(), ms));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user