1bfe4fd794
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013gwc3aCiyQxmCWQke8RvEz
116 lines
4.5 KiB
Markdown
116 lines
4.5 KiB
Markdown
# 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 routes
|
|
- `db.js` — SQLite setup + queries (`better-sqlite3`)
|
|
- `scraper.js` — haalt een pagina op (`fetch`) en extraheert de prijs (`cheerio`)
|
|
- `mailer.js` — verstuurt email via `nodemailer` (SMTP)
|
|
- `scheduler.js` — `node-cron` job, dagelijks, doorloopt alle producten
|
|
- `views/` — 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:
|
|
|
|
1. JSON-LD (`<script type="application/ld+json">`) met `@type: Product` → `offers.price`
|
|
2. Meta tags: `meta[property="product:price:amount"]` of `og:price:amount`
|
|
3. 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`, schema `0 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`, en `last_price` niet
|
|
null): mail versturen met onderwerp `Prijs gewijzigd: {naam}`, body
|
|
met oude prijs, nieuwe prijs, en link naar het product.
|
|
- **Belangrijk:** `last_price` (en `last_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 var
|
|
`DB_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 de
|
|
`deploying-to-coolify` skill.
|
|
|
|
## 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.
|