Files
Sjoerd de Vries 74f487486f Fix final-review findings for Playwright fallback
- 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gwc3aCiyQxmCWQke8RvEz
2026-09-05 18:06:14 +02:00

109 lines
3.5 KiB
JavaScript

// Leest een .env-bestand in voor lokale ontwikkeling; in productie komen de
// env vars van Coolify. quiet houdt de opstartlogging schoon.
require('dotenv').config({ quiet: true });
const path = require('node:path');
const {
openDb, listProducts, addProduct, deleteProduct,
recordCheckSuccess, recordCheckFailure,
} = require('./src/db');
const { fetchAndExtractPrice } = require('./src/scraper');
const { launchBrowser, fetchAndExtractPriceViaBrowser } = require('./src/browserScraper');
const { createTransport, sendPriceChangeEmail } = require('./src/mailer');
const { checkAllProducts } = require('./src/checkProducts');
const { start: startScheduler } = require('./src/scheduler');
const { createApp } = require('./src/app');
const REQUIRED_ENV = ['BASIC_AUTH_USER', 'BASIC_AUTH_PASS', 'SMTP_FROM', 'NOTIFY_EMAIL'];
const missing = REQUIRED_ENV.filter((name) => !process.env[name]);
if (missing.length > 0) {
console.error(`Ontbrekende verplichte environment variables: ${missing.join(', ')}`);
console.error('Zet ze (bijvoorbeeld in .env, zie .env.example) en start opnieuw.');
process.exit(1);
}
const PORT = process.env.PORT || 3000;
const DB_PATH = process.env.DB_PATH || path.join(__dirname, 'data', 'korting.db');
const db = openDb(DB_PATH);
const transport = createTransport(process.env);
function summarize(results) {
const counts = {};
for (const result of results) {
counts[result.status] = (counts[result.status] || 0) + 1;
}
const parts = Object.entries(counts).map(([status, count]) => `${status}=${count}`);
return parts.length > 0 ? parts.join(' ') : 'geen producten';
}
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() {
if (!browserPromise) {
browserPromise = launchBrowser();
}
return browserPromise;
}
try {
const deps = {
fetchAndExtractPrice,
fetchAndExtractPriceViaBrowser: async (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,
to: process.env.NOTIFY_EMAIL,
...payload,
}),
recordCheckFailure: (id, status) => recordCheckFailure(db, id, status),
recordCheckSuccess: (id, data) => recordCheckSuccess(db, id, data),
now: () => new Date().toISOString(),
};
const results = await checkAllProducts(products, deps);
console.log(`[check] ${products.length} product(en) gecontroleerd: ${summarize(results)}`);
return results;
} finally {
if (browserPromise) {
const browser = await browserPromise.catch(() => null);
if (browser) {
await browser.close().catch((err) => {
console.error(`[check] kon de browser niet netjes sluiten: ${err && err.message}`);
});
}
}
}
}
startScheduler(runCheck);
const app = createApp({
listProducts: () => listProducts(db),
addProduct: (data) => addProduct(db, data),
deleteProduct: (id) => deleteProduct(db, id),
checkAllProducts: runCheck,
authUser: process.env.BASIC_AUTH_USER,
authPass: process.env.BASIC_AUTH_PASS,
});
app.listen(PORT, () => {
console.log(`Korting draait op poort ${PORT}`);
});