diff --git a/scripts/clean-non-hardware-clone.js b/scripts/clean-non-hardware-clone.js index 4650e7c..ff0c3bd 100644 --- a/scripts/clean-non-hardware-clone.js +++ b/scripts/clean-non-hardware-clone.js @@ -46,6 +46,30 @@ function pageUrl(page, mode) { return base.toString(); } +async function waitForStableActionButtons(page, { stableForMs = 800, maxWaitMs = 8000 } = {}) { + function signature() { + const buttons = Array.from(document.querySelectorAll('button.library-add, button.library-remove')); + return buttons + .map((b) => `${b.className.includes('library-add') ? 'add' : 'remove'}:${b.className.includes('hidden') ? 'h' : 'v'}`) + .join('|'); + } + + const start = Date.now(); + let lastSig = await page.evaluate(signature); + let stableSince = Date.now(); + + while (Date.now() - start < maxWaitMs) { + await page.waitForTimeout(200); + const sig = await page.evaluate(signature); + if (sig === lastSig) { + if (Date.now() - stableSince >= stableForMs) return; + } else { + lastSig = sig; + stableSince = Date.now(); + } + } +} + async function scanAndRemoveOnPage(page, dryRun) { return page.evaluate(({ dryRunArg }) => { function isVisible(el) { @@ -114,6 +138,27 @@ async function scanAndRemoveOnPage(page, dryRun) { }, { dryRunArg: dryRun }); } +async function scanAndRemoveWithRetries(page, dryRun) { + let combined = await scanAndRemoveOnPage(page, dryRun); + + // Extra passes catch modules whose ownership state renders after the initial stabilization wait. + for (let i = 0; i < 2; i += 1) { + await waitForStableActionButtons(page, { stableForMs: 600, maxWaitMs: 4000 }); + const again = await scanAndRemoveOnPage(page, dryRun); + if (again.visibleRemoveButtons === 0 && again.removed === 0) break; + + combined = { + url: again.url, + maxPage: again.maxPage, + visibleRemoveButtons: combined.visibleRemoveButtons + again.visibleRemoveButtons, + removed: combined.removed + again.removed, + keptHardwareClone: combined.keptHardwareClone + again.keptHardwareClone + }; + } + + return combined; +} + function reportPage(pageNumber, info, dryRun) { const modeText = dryRun ? 'dry-run' : 'apply'; const unchanged = info.removed === 0 ? 'yes' : 'no'; @@ -205,7 +250,8 @@ async function main() { throw new Error('Not logged in on VCV Library after login detection. Aborting to avoid false no-op run.'); } - const first = await scanAndRemoveOnPage(page, opts.dryRun); + await waitForStableActionButtons(page); + const first = await scanAndRemoveWithRetries(page, opts.dryRun); const lastPage = opts.maxPages ? Math.min(opts.startPage + opts.maxPages - 1, first.maxPage) : first.maxPage; @@ -235,7 +281,8 @@ async function main() { const url = pageUrl(p, opts.mode); console.log(`Navigating page ${p}/${lastPage}: ${url}`); await navigateWithRetry(page, url, opts.navTimeoutMs, `Page ${p} navigation`); - const info = await scanAndRemoveOnPage(page, opts.dryRun); + await waitForStableActionButtons(page); + const info = await scanAndRemoveWithRetries(page, opts.dryRun); addStats(info); reportPage(p, info, opts.dryRun); }