Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013gwc3aCiyQxmCWQke8RvEz
4.5 KiB
Korting — prijstracker design
Datum: 2026-09-05 Status: Approved
Doel
Een webdienst die dagelijks een lijst productpagina's bezoekt, de huidige
prijs extraheert, en een email stuurt wanneer de prijs is veranderd
sinds de vorige check. Gehost op Coolify, bereikbaar op
korting.sjoerd.app, repo in Gitea (git.pietpiraat.online).
Architectuur
Eén Node.js/Express monoliet in één Docker-container:
server.js— Express app, basic-auth middleware, web-UI routesdb.js— SQLite setup + queries (better-sqlite3)scraper.js— haalt een pagina op (fetch) en extraheert de prijs (cheerio)mailer.js— verstuurt email vianodemailer(SMTP)scheduler.js—node-cronjob, dagelijks, doorloopt alle productenviews/— server-rendered HTML (geen frontend-framework)
Geen aparte services: scheduler draait in-process in dezelfde container als de webserver. SQLite-bestand staat op een persistent Coolify volume zodat data een redeploy overleeft.
Data model
Tabel products:
| kolom | type | omschrijving |
|---|---|---|
| id | integer PK | |
| name | text | leesbare naam |
| url | text | productpagina-URL |
| last_price | real, nullable | laatst bekende prijs |
| last_checked_at | datetime, nullable | tijdstip laatste (succesvolle) check |
| last_check_status | text, nullable | bijv. ok / not_found / fetch_error |
| created_at | datetime |
Geen aparte prijshistorie-tabel (YAGNI) — kan later toegevoegd worden als er behoefte is aan een grafiek.
Prijs-extractie (scraper.js)
Volgorde van pogingen per pagina:
- JSON-LD (
<script type="application/ld+json">) met@type: Product→offers.price - Meta tags:
meta[property="product:price:amount"]ofog:price:amount - Fallback regex: eerste
€\s?\d+[.,]\d{2}patroon in de zichtbare pagina-tekst
Als niets oplevert: check telt als mislukt (last_check_status = 'not_found'), zichtbaar in de UI naast het product. Geen mail, geen
wijziging aan last_price.
Scheduling & email
node-cron, schema0 8 * * *, tijdzone Europe/Amsterdam.- Extra "nu controleren" knop in de UI om de hele check-run handmatig te triggeren (zelfde codepad als de cron-job).
- Elk product wordt onafhankelijk verwerkt (try/catch per product) — één mislukte check blokkeert de andere producten niet.
- Eerste keer dat een product wordt toegevoegd (
last_price IS NULL): alleen baseline opslaan, geen mail. - Bij prijsverschil (nieuwe prijs ≠
last_price, enlast_priceniet null): mail versturen met onderwerpPrijs gewijzigd: {naam}, body met oude prijs, nieuwe prijs, en link naar het product. - Belangrijk:
last_price(enlast_checked_at) wordt in de database alleen bijgewerkt als de mail succesvol is verstuurd (of als er geen mail nodig was, bijv. baseline of geen wijziging). Zo blijft het systeem de volgende dag opnieuw proberen te mailen als er een tijdelijk SMTP-probleem was, in plaats van de wijziging stilzwijgend te verliezen. - Mailer config via env vars:
SMTP_HOST(mail.sjoerd.app),SMTP_PORT,SMTP_USER,SMTP_PASS,SMTP_FROM,NOTIFY_EMAIL(ontvanger).
Beveiliging
- Basic auth over alle routes (UI + endpoints) via env vars
BASIC_AUTH_USER/BASIC_AUTH_PASS.
Deployment
- Dockerfile:
node:22-slim, production deps only,CMD ["node", "server.js"]. - Persistent volume gemount op
/data; SQLite-pad via env varDB_PATH(default/data/korting.db). - Repo aangemaakt in Gitea (
git.pietpiraat.online). - Coolify-applicatie gekoppeld aan die repo, domain
korting.sjoerd.app, env vars zoals hierboven, auto-deploy on push. Uitgevoerd via dedeploying-to-coolifyskill.
Error handling (samengevat)
| situatie | gedrag |
|---|---|
| netwerkfout bij ophalen productpagina | gelogd, last_check_status = 'fetch_error', geen crash van de run |
| geen prijs gevonden op pagina | gelogd, last_check_status = 'not_found', geen mail, last_price blijft ongewijzigd |
| SMTP-fout bij versturen mail | gelogd, last_price blijft ongewijzigd zodat de volgende run opnieuw probeert te mailen |
Testing
- Unit tests voor
scraper.js(Node's ingebouwde test runner) tegen HTML-fixtures: JSON-LD-pad, meta-tag-pad, regex-fallback-pad, en het "niets gevonden"-pad. - Handmatige verificatie van de volledige mail-flow via de "nu controleren"-knop na deploy.
Out of scope (mogelijke latere uitbreiding)
- Headless browser (Playwright) als fallback voor pagina's die de prijs pas via JavaScript renderen.
- Prijshistorie/grafiek per product.