From 607b5ec2b5350ecc28cbae289bfdaac3934f3316 Mon Sep 17 00:00:00 2001 From: type-two Date: Wed, 8 Jul 2026 11:33:18 +1000 Subject: [PATCH] fix(harvest): robust Download-row finder + two-step hover + Copy Log button + 300-line log MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported symptom: harvest opens the More menu then moves on without ever hovering Download. Cause: the size flyout's text can glue onto the Download row (textContent "Download1K Original size2K Upscaled..."), so the old /^download\b/ match — which needs a word boundary right after "download" — returned null and the whole size step was skipped. Fix: findDownloadRow() matches "download" anywhere and takes the SHORTEST visible candidate (the row itself, not a wrapper that swallowed the flyout). Stage-3 hover now moves in two steps (approach from the left, then settle on centre) to fire a real mouseenter that unfurls the submenu, and re-queries. Added per-stage logging (Download row text+coords, chosen size option) so a copied log shows exactly where any future miss happens. Popup gains a Copy button (whole activity log to clipboard) and log retention is raised 100 -> 300 so a full harvest pass is capturable. Co-Authored-By: Claude Fable 5 --- content.js | 42 +++++++++++++++++++++++++++++------------- popup.html | 2 +- popup.js | 19 ++++++++++++++++++- 3 files changed, 48 insertions(+), 15 deletions(-) diff --git a/content.js b/content.js index 96b6f81..08124eb 100644 --- a/content.js +++ b/content.js @@ -573,6 +573,19 @@ function findMenuItem(re) { return leaf.length ? (leaf[0].closest('[role="menuitem"], li, button') || leaf[0]) : null; } +// The "Download" menu ROW. NOTE: its size flyout may live inside/next to it so a naive +// textContent reads "Download1K Original size2K Upscaled…" — then /^download\b/ fails +// (no word boundary after 'download'), which silently skipped the whole step. Match +// "download" anywhere and take the SHORTEST visible candidate (the row, not a wrapper). +function findDownloadRow() { + const cands = [...document.querySelectorAll('[role="menuitem"], [role="menu"] button, button, [role="button"], div, span, li')] + .filter(visible) + .filter(e => /download/i.test(e.textContent || '') && (e.textContent || '').trim().length < 60); + if (!cands.length) return null; + cands.sort((a, b) => (a.textContent || '').length - (b.textContent || '').length); + return cands[0].closest('[role="menuitem"], button, [role="button"], li') || cands[0]; +} + // The FREE "original size" download option. Images: "1K Original size". Videos: // "720p Original Size". Both contain "original size" — that's the safe key. // SAFETY: never return an "Upscaled" / "…credits" row. The 4K video upscale costs @@ -636,25 +649,28 @@ async function executeHarvest(task) { // Stage 2/4: click ⋮ ("More"), wait for the menu with Download in it await bg({ type: 'clickAt', ...center(dots) }); - const dl = await waitFor(() => findMenuItem(/^\s*download\b/i), 3000, 300); - if (!dl) { missed++; missedThisPass.add(href); log('info', `harvest: ⋮ clicked but no Download item — ${gridCensus()}`); await bg({ type: 'pressEscape' }); await sleep(400); continue; } + const dl = await waitFor(findDownloadRow, 3000, 300); + if (!dl) { missed++; missedThisPass.add(href); log('info', `harvest: ⋮ clicked but no Download row found — ${gridCensus()}`); await bg({ type: 'pressEscape' }); await sleep(400); continue; } + const dc = center(dl); + log('info', `harvest: Download row "${(dl.textContent || '').trim().slice(0, 18)}" @ ${Math.round(dc.x)},${Math.round(dc.y)} — hovering for the size flyout…`); - // Stage 3/4: HOVER Download — the size flyout unfurls beside it (side depends on - // where the tile sits; irrelevant, coords are re-queried fresh). Jiggle the - // pointer a few px and retry — one mouseMoved sometimes isn't enough for the - // hover intent to register. We pick "original size" (1K image / 720p video) and - // NEVER the paid upscales — findSizeOption enforces that. + // Stage 3/4: HOVER Download so the size flyout unfurls beside it. A single CDP jump + // can miss the hover-intent, so move in TWO steps (approach from the left, then + // settle on centre) to fire a real mouseenter, and re-query. We pick "original + // size" (1K image / 720p video) and NEVER the paid upscales (findSizeOption). let oneK = null; - for (let j = 0; j < 3 && !oneK; j++) { + for (let j = 0; j < 4 && !oneK; j++) { const c = center(dl); - await bg({ type: 'hoverAt', x: c.x - 4 + j * 4, y: c.y }); - oneK = await waitFor(findSizeOption, 1600, 250); + await bg({ type: 'hoverAt', x: c.x - 28, y: c.y }); await sleep(130); + await bg({ type: 'hoverAt', x: c.x, y: c.y }); + oneK = await waitFor(findSizeOption, 1400, 200); } - if (!oneK) { // last resort: some builds expand on click + if (!oneK) { // last resort: some builds expand the submenu on click await bg({ type: 'clickAt', ...center(dl) }); oneK = await waitFor(findSizeOption, 1600, 250); } - if (!oneK) { missed++; missedThisPass.add(href); log('info', 'harvest: no "original size" option after hover + click-expand.'); await bg({ type: 'pressEscape' }); await sleep(400); continue; } + if (!oneK) { missed++; missedThisPass.add(href); log('info', `harvest: Download hovered but size flyout never appeared (no "original size"). ${gridCensus()}`); await bg({ type: 'pressEscape' }); await sleep(400); continue; } + log('info', `harvest: size option "${(oneK.textContent || '').trim().slice(0, 22)}" — downloading.`); // Stage 4/4: walk the pointer INTO the flyout (keeps it open), then click 1K, // then VERIFY a download event actually fired before counting it. @@ -737,7 +753,7 @@ async function handleRateLimit(task) { function log(level, text) { console.log(`[Flow][${level}] ${text}`); chrome.storage.local.get({ logs: [] }, (r) => { - chrome.storage.local.set({ logs: [...r.logs, { level, text, timestamp: Date.now() }].slice(-100) }); + chrome.storage.local.set({ logs: [...r.logs, { level, text, timestamp: Date.now() }].slice(-300) }); }); } diff --git a/popup.html b/popup.html index 50dd613..12c66a8 100644 --- a/popup.html +++ b/popup.html @@ -46,7 +46,7 @@
Activity Log - +  
[System] Extension loaded. Click Start to begin.
diff --git a/popup.js b/popup.js index d83aeb4..723751a 100644 --- a/popup.js +++ b/popup.js @@ -129,6 +129,23 @@ clearLogsBtn.addEventListener('click', () => { }); }); +// Copy Logs — grab the whole activity log to the clipboard for pasting into chat. +const copyLogsBtn = document.getElementById('copy-logs'); +copyLogsBtn.addEventListener('click', () => { + chrome.storage.local.get({ logs: [] }, (res) => { + const text = (res.logs || []).map(l => + `[${new Date(l.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })}] ${l.text}` + ).join('\n'); + const done = () => { copyLogsBtn.textContent = 'Copied!'; setTimeout(() => { copyLogsBtn.textContent = 'Copy'; }, 1200); }; + navigator.clipboard.writeText(text).then(done).catch(() => { + const ta = document.createElement('textarea'); ta.value = text; + document.body.appendChild(ta); ta.select(); + try { document.execCommand('copy'); } catch (e) {} + ta.remove(); done(); + }); + }); +}); + // Toggle Run Loop toggleBtn.addEventListener('click', () => { chrome.storage.local.get('isRunning', (res) => { @@ -152,7 +169,7 @@ function addSystemLog(text) { text: text, timestamp: Date.now() }; - const updated = [...res.logs, newLog].slice(-100); // limit to 100 logs + const updated = [...res.logs, newLog].slice(-300); // keep enough for a harvest debug chrome.storage.local.set({ logs: updated }, () => { appendLogToUI(newLog); });