Fix missed removals from async ownership state rendering

Add/Remove button state loads asynchronously after page render, so a
fixed delay could scan before all buttons updated and skip real
candidates (confirmed via Strum's VCV Modules brand page). Add a
button-state stabilization wait and multi-pass per-page scanning to
catch late-rendering items.
This commit is contained in:
Olaf
2026-08-24 11:43:08 +02:00
parent 9c67af04c1
commit a006b9ca13
+49 -2
View File
@@ -46,6 +46,30 @@ function pageUrl(page, mode) {
return base.toString(); 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) { async function scanAndRemoveOnPage(page, dryRun) {
return page.evaluate(({ dryRunArg }) => { return page.evaluate(({ dryRunArg }) => {
function isVisible(el) { function isVisible(el) {
@@ -114,6 +138,27 @@ async function scanAndRemoveOnPage(page, dryRun) {
}, { dryRunArg: 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) { function reportPage(pageNumber, info, dryRun) {
const modeText = dryRun ? 'dry-run' : 'apply'; const modeText = dryRun ? 'dry-run' : 'apply';
const unchanged = info.removed === 0 ? 'yes' : 'no'; 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.'); 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 const lastPage = opts.maxPages
? Math.min(opts.startPage + opts.maxPages - 1, first.maxPage) ? Math.min(opts.startPage + opts.maxPages - 1, first.maxPage)
: first.maxPage; : first.maxPage;
@@ -235,7 +281,8 @@ async function main() {
const url = pageUrl(p, opts.mode); const url = pageUrl(p, opts.mode);
console.log(`Navigating page ${p}/${lastPage}: ${url}`); console.log(`Navigating page ${p}/${lastPage}: ${url}`);
await navigateWithRetry(page, url, opts.navTimeoutMs, `Page ${p} navigation`); 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); addStats(info);
reportPage(p, info, opts.dryRun); reportPage(p, info, opts.dryRun);
} }