From 74f487486f2f1c2c27f181545fe0bd633bcc7eb2 Mon Sep 17 00:00:00 2001 From: Sjoerd de Vries Date: Sat, 5 Sep 2026 18:06:07 +0200 Subject: [PATCH] Fix final-review findings for Playwright fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - server.js: catch browser-launch failures inside the injected fetchAndExtractPriceViaBrowser wrapper so they route through checkProduct's normal error classification (recordCheckFailure) instead of escaping as a thrown error that bypasses the DB write. - server.js: add an in-flight guard around runCheck so overlapping callers (cron + check-now) share one run instead of each launching its own Chromium. - Dockerfile: copy only package.json/package-lock.json/node_modules before installing Chromium so the expensive install layer is keyed to dependency changes, not every commit; copy the rest of the app afterward. - Dockerfile: clean up apt package lists after the Playwright install step, matching the build stage's existing cleanup. - README.md: note that exercising the fallback locally (not just running the test suite) needs `npx playwright install chromium`. - README.md: correct the fallback description — domcontentloaded navigation reads the page before most client-side JS finishes, so the fallback mainly helps with HTTP-client blocking, not JS-rendered prices; reworded to drop the inaccurate claim. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_013gwc3aCiyQxmCWQke8RvEz --- Dockerfile | 5 ++++- README.md | 11 ++++++++--- server.js | 21 ++++++++++++++++++--- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8b33cc1..eac71df 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,8 +8,11 @@ COPY . . FROM node:22-slim WORKDIR /app +COPY --from=build /app/package.json /app/package-lock.json ./ +COPY --from=build /app/node_modules ./node_modules +RUN npx --yes playwright install --with-deps chromium \ + && rm -rf /var/lib/apt/lists/* COPY --from=build /app . -RUN npx --yes playwright install --with-deps chromium ENV NODE_ENV=production # De SQLite-database hoort op het persistente volume te staan; zonder deze # default zou een vergeten DB_PATH stilletjes naar de container-fs schrijven diff --git a/README.md b/README.md index 09158d5..b265c21 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,11 @@ app bij het opstarten met een melding. De UI staat achter basic auth (`BASIC_AUTH_USER` / `BASIC_AUTH_PASS`) op `http://localhost:3000`. +Wil je de headless-browser-fallback ook echt lokaal uitproberen (de +testsuite gebruikt fakes en heeft hier geen echte browser voor nodig), run +dan eerst `npx playwright install chromium` om de Chromium-binary te +downloaden. + ## Environment variables | Variabele | Omschrijving | @@ -43,6 +48,6 @@ controleren"-knop in de UI kan dit ook handmatig getriggerd worden. Als het ophalen van de prijs op de gewone manier mislukt (netwerkfout, of geen prijs gevonden in de HTML), probeert de app het één keer opnieuw met een echte headless-browser (Playwright/Chromium) — nuttig voor pagina's -die de prijs pas met JavaScript renderen, of die eenvoudige HTTP-clients -blokkeren. Geen garantie tegen elke vorm van bot-detectie, maar dekt een -reëel deel van de gevallen die de gewone aanpak mist. +die eenvoudige HTTP-clients blokkeren. Geen garantie tegen elke vorm van +bot-detectie, maar dekt een reëel deel van de gevallen die de gewone +aanpak mist. diff --git a/server.js b/server.js index c75d544..17376bd 100644 --- a/server.js +++ b/server.js @@ -37,7 +37,18 @@ function summarize(results) { return parts.length > 0 ? parts.join(' ') : 'geen producten'; } -async function runCheck() { +let inFlightCheck = null; +function runCheck() { + if (inFlightCheck) { + return inFlightCheck; + } + inFlightCheck = runCheckOnce().finally(() => { + inFlightCheck = null; + }); + return inFlightCheck; +} + +async function runCheckOnce() { const products = listProducts(db); let browserPromise = null; function getBrowser() { @@ -50,8 +61,12 @@ async function runCheck() { const deps = { fetchAndExtractPrice, fetchAndExtractPriceViaBrowser: async (url) => { - const browser = await getBrowser(); - return fetchAndExtractPriceViaBrowser(browser, url); + try { + const browser = await getBrowser(); + return await fetchAndExtractPriceViaBrowser(browser, url); + } catch (err) { + return { price: null, method: null, error: err }; + } }, sendMail: (payload) => sendPriceChangeEmail(transport, { from: process.env.SMTP_FROM,