Initial deploy setup
deploy / deploy (push) Canceled after 0s

This commit is contained in:
Jens
2026-07-21 14:00:00 +02:00
commit b8091e59bd
285 changed files with 27854 additions and 0 deletions
+82
View File
@@ -0,0 +1,82 @@
# Dataflows en invarianten
## Flow A — Publieke vacaturepagina
1. Scheduler selecteert alleen `allow` + `active/trial` + due.
2. Fetcher controleert policy, URL, DNS/IP, poort en iedere redirect.
3. Response wordt begrensd op type/grootte en als `RawDocument` met korte retentie opgeslagen.
4. Registry kiest RSS wanneer XML/feed, anders JSON-LD en daarna generieke HTML.
5. `ExtractedJob` wordt gesanitized en genormaliseerd.
6. Dedupe zoekt external ID, canonieke URL, canonieke key en conservatieve fuzzy kandidaat.
7. Pipeline schrijft werkgever, vacature, alias, provenance en versie.
8. Alle actieve profielen krijgen een nieuwe `ScoreRun`.
9. SourceRun registreert metrics en plant volgende run.
Invarianten:
- geen fetch zonder policy + URL-validatie;
- één canonieke vacature per canonical key;
- originele bronURL blijft als alias bewaard;
- HTML in de UI is gesanitized;
- verwerking van dezelfde broninhoud is idempotent.
## Flow B — Vacaturemail
1. IMAP-task haalt maximaal een begrensd aantal berichten op.
2. `message_identity` gebruikt RFC Message-ID of SHA-256 fallback.
3. Bestaand record stopt dubbele verwerking.
4. Raw mail wordt tijdelijk opgeslagen.
5. E-mailadapter extraheert alleen HTTP(S)-links en slaat unsubscribe/privacylinks over.
6. Iedere link wordt als vacaturedraft verwerkt; denylistlink wordt niet automatisch gefetcht.
7. Record bewaart linklijst, processedstatus en begrensde foutmelding.
Invarianten:
- mailboxpassword staat alleen in environment/secrets;
- geen attachmentverwerking in MVP;
- e-mailtekst is data, geen AI-/agentinstructie;
- duplicate Message-ID maakt geen extra vacaturesnapshot.
## Flow C — Feedback en sollicitatie
1. Authenticated user POST met CSRF.
2. Actie wordt tegen enum gevalideerd.
3. `Feedback` is append-only.
4. `applied` maakt atomair één `Application` per user/job.
5. Dossier krijgt vacaturesnapshot en standaard opvolgdatum.
6. Externe verzending bestaat niet.
Invarianten:
- user-scoped querysets voor dossierwijziging;
- één dossier per user/job;
- undo/verbergen is traceerbaar;
- feedbackleren mag uitsluitend begrensde soft weights wijzigen.
## Flow D — Dagelijkse digest
1. Scheduler evalueert actieve profielen in hun tijdzone.
2. Alleen binnen het 30-minutenvak rond digesttijd wordt een outbox gemaakt.
3. Dedupe key is profiel + lokale datum.
4. Selectie neemt laatste sterke/mogelijke score per actieve vacature, verborgen jobs uitgesloten.
5. Template rendert tekst en HTML.
6. Bij succes status `sent`; bij fout `failed` met begrensde foutcontext.
Invarianten:
- maximaal één outbox per profiel/dag;
- een reeds verzonden outbox wordt niet opnieuw verzonden;
- geen recipient betekent `skipped`, geen crash;
- digest bevat nooit raw HTML van vacaturebron.
## Dataretentie
| Data | Standaard | Reden |
|---|---:|---|
| Raw publieke documenten | 7 dagen | parserdebug en provenance |
| Raw vacaturemail | 7 dagen | idempotentie/debug; daarna verwijderen |
| Canonieke vacature | totdat verwijderd/retentiebeleid | gebruikerswaarde en historie |
| JobVersion | blijvend tenzij gebruiker wist | wijzigingsaudit |
| ScoreRun | configureerbaar, initieel blijvend | uitlegbaarheid/modelvergelijking |
| Feedback/Application | totdat gebruiker wist | persoonlijke opvolging |
| Logs | operationeel begrensd/geroteerd | foutdiagnose zonder inhoud/secrets |
+133
View File
@@ -0,0 +1,133 @@
# Datamodel
## Entity-relatiemodel
```mermaid
erDiagram
USER ||--o{ SEARCH_PROFILE : owns
SEARCH_PROFILE ||--o{ PROFILE_REVISION : versions
SEARCH_PROFILE ||--o{ SCORE_RUN : evaluates
SEARCH_PROFILE ||--o{ DIGEST_OUTBOX : receives
SOURCE ||--o{ SOURCE_RUN : executes
SOURCE ||--o{ RAW_DOCUMENT : retrieves
SOURCE_RUN ||--o{ RAW_DOCUMENT : produces
RAW_DOCUMENT ||--o{ JOB_SOURCE_ALIAS : evidences
RAW_DOCUMENT ||--o| EMAIL_MESSAGE_RECORD : wraps
EMPLOYER ||--o{ JOB_POSTING : publishes
JOB_POSTING ||--o{ JOB_SOURCE_ALIAS : aliases
JOB_POSTING ||--o{ FIELD_PROVENANCE : fields
JOB_SOURCE_ALIAS ||--o{ FIELD_PROVENANCE : proves
JOB_POSTING ||--o{ JOB_VERSION : versions
JOB_POSTING ||--o{ SCORE_RUN : scored
JOB_POSTING ||--o{ FEEDBACK : receives
JOB_POSTING ||--o{ APPLICATION : tracked
USER ||--o{ FEEDBACK : gives
USER ||--o{ APPLICATION : owns
```
## Entiteiten
### SearchProfile
Bevat harde/soft voorkeuren, locatie, drempels, digesttijd en version. De JSON-velden blijven schematisch gedocumenteerd in `docs/api/INTERNAL_CONTRACTS.md` en gevalideerd in service/formcode.
Belangrijke invarianten:
- unieke naam per user;
- recommendation threshold ≤ top threshold;
- gewichten numeriek, niet-negatief en met positieve som;
- coördinaten binnen bereik;
- precies één actief profiel per user via `activate()`-serviceflow.
### Source
Bronregister met `source_type`, `status` en onafhankelijk `policy`.
Status betekent operationele toestand; policy betekent toestemming:
- `candidate` — ontdekt, niet actief;
- `trial` — beperkte proefcontrole;
- `active` — normaal gepland;
- `quarantined` — automatisch/manual geïsoleerd;
- `paused/disabled` — niet plannen;
- `allow/review/deny` — netwerktoegangsbesluit.
Een `deny`-bron mag niet `active` zijn.
### RawDocument
Tijdelijke immutable-ish fetchsnapshot. Bevat alleen begrensde tekstbody, headers op allowlist, hash, parserresultaat en retentie. Quarantaine verhindert cleanup tot inspectie.
### Employer
Genormaliseerde werkgever/recruiteridentiteit. Uniek op normalized name + domain. `is_direct_employer` en `is_recruiter` zijn confidencegedreven kenmerken, geen juridisch oordeel.
### JobPosting
Canonieke vacature met UUID, canonical key, content hash, status en genormaliseerde velden. De canonical key wordt uit external ID/URL of fallbackidentiteit afgeleid. De exacte key is uniek; fuzzy matching is alleen een kandidaatbeslissing.
Statusflow:
```text
new -> active -> uncertain -> removed
\-> expired
active/uncertain -> duplicate/quarantined (beheerflow)
```
### JobSourceAlias
Verbindt alle bronvermeldingen met één canonieke vacature. Bewaart externe ID, bronURL, parser, confidence en beperkte payload. De alias is het anker voor veldprovenance.
### FieldProvenance
Per veld: extractiemethode, confidence, evidenceexcerpt en parserversie. Evidence is begrensd en mag geen geheime mailinhoud bevatten.
### JobVersion
Snapshot per unieke contenthash. Bij inhoudswijziging wordt de vorige toestand bewaard; de huidige toestand krijgt eveneens een versie.
### ScoreRun
Append-only score voor één job + profielversie. Bevat componentbijdragen, confidence, recommendation, redenen en evidence. Meerdere runs maken algoritme- en profielvergelijking mogelijk.
### Feedback
Append-only useractie. Geen laatste-statekolom; views kunnen de laatste actie afleiden. Hierdoor blijft correctiehistorie behouden.
### Application
Eén dossier per user/job. `snapshot` bewaart wat de gebruiker zag toen hij solliciteerde. `document_manifest` bevat alleen metadata/paden, geen willekeurige externe URL-executie.
### DigestOutbox
Idempotente meldingseenheid met unieke dedupe key. Statusovergangen: pending → sent/failed; skipped bij ontbrekende recipient/configuratie.
## Indexstrategie
Bestaand:
- job title + region;
- job status + valid_through;
- alias source + external ID;
- alias canonical URL;
- source + raw contenthash;
- profile + score descending + created_at;
- bron next_run_at en healthdatums.
Toekomstig bij meetbare noodzaak:
- trigram/index voor fuzzy title/employer (PostgreSQL);
- partial index op actieve vacatures;
- latest score materialisatie;
- JSON GIN uitsluitend voor werkelijk bevraagde keys.
## Migratiebeleid
- schemawijzigingen via Django migrations;
- destructieve wijzigingen in twee fasen;
- grote backfills chunked en restartable;
- productieback-up vóór migratie;
- migratiecheck in CI;
- rollback of forward-fix expliciet in taskplan.
+137
View File
@@ -0,0 +1,137 @@
# Matching- en scoringengine
## 1. Beslisvolgorde
```text
normalisatie
-> harde uitsluitingen
-> deterministische features
-> optionele AI-features (alleen aanvulling)
-> gewogen score
-> confidence
-> recommendation
-> append-only ScoreRun
```
Harde uitsluitingen hebben absolute voorrang. Een score van 99 kan een uitgesloten verplichte skill of regio niet overrulen.
## 2. Harde regels
Ondersteund of gepland:
- uitgesloten titelterm;
- uitgesloten/geen toegestane contractvorm;
- uitgesloten regio/gemeente;
- afstand boven maximum, behalve remote;
- uitgesloten verplichte skill;
- expliciete rijbewijs-/reis-/taalvereisten wanneer betrouwbaar geëxtraheerd;
- verlopen of niet-actieve vacature buiten de ranking.
Iedere uitsluiting bewaart een concrete reden. `unknown_is_insufficient` kan later per regel bepalen of ontbrekende data naar review gaat; veilige standaard is onbekend niet automatisch uitsluiten.
## 3. Componenten
Standaardgewichten, totaal 100:
| Component | Gewicht | Kernsignalen |
|---|---:|---|
| Content | 25 | titelovereenkomst, taken, weinig ongewenste support |
| Skills | 20 | gewenste skills in expliciete velden/tekst |
| Locatie | 15 | afstand, remote/hybrid, voorkeursregio |
| Voorwaarden | 10 | toegestane contract-/werkvorm |
| Werkgever | 10 | directe bron versus recruiter |
| Senioriteit | 10 | gevraagde ervaring versus profiel |
| Voorkeuren | 10 | publieke sector en andere soft signals |
Bij aangepaste gewichten worden componentbijdragen genormaliseerd op de totale som.
- AI-boost is opt-in via profielinstelling en telt alleen mee als profielgewicht > 0.
- De AI-component is softwarematig begrensd (maximale bijdrage via `ai`-gewicht) om dominantie te vermijden.
## 4. Confidence
Confidence is niet hetzelfde als score. Een vacature kan inhoudelijk sterk lijken maar lage confidence hebben door ontbrekende werkgever, locatie of beschrijving.
Baselineformule in MVP:
```text
confidence = 0.65 * extraction_confidence + 0.35 * field_completeness
```
Uitbreidingen mogen rekening houden met:
- provenancekwaliteit per veld;
- overeenstemming tussen aliassen;
- geocodingconfidence;
- parserspecifieke drift;
- AI-/deterministische featureconsistentie.
## 5. Recommendations
- `strong` — score ≥ topmatchthreshold en confidence ≥ 0,65;
- `possible` — score ≥ recommendationthreshold;
- `weak` — lager maar niet hard uitgesloten;
- `hidden` — minstens één harde uitsluiting.
Drempels zijn profielconfiguratie. De UI toont score en confidence apart.
## 6. Evidence en explainability
Een score bewaart:
- exacte componentbijdragen;
- positives;
- concerns;
- hard exclusions;
- afstand/evidence;
- profielversie;
- optionele model- en promptversie.
- AI-status (`ok`, `disabled`, `timeout`, `invalid`, `error`) en foutcategorie;
- gewichten (gevraagde vs. effectief toegepast, zodat cap zichtbaar is).
Copyregels:
- zeg "niet teruggevonden" in plaats van "ontbreekt" wanneer brondata onvolledig is;
- label inference als inference;
- toon maximaal enkele kernredenen bovenaan en volledige details uitklapbaar;
- geef geen kanspercentage op aanwerving zonder gevalideerd model.
## 7. Feedbackleren
Feedback kan alleen begrensde soft weights aanpassen. Learning staat standaard uit tot `learning_enabled` actief is.
Regels:
- learning kan uit;
- delta per feedbackactie klein en gelimiteerd;
- weight binnen 0..40;
- iedere wijziging maakt een profielrevisie;
- harde regels en bronpolicy worden nooit geleerd;
- een enkele actie leidt niet tot grote verschuiving (minimale sample-criteria per feature);
- gebruiker kan resetten en verschil bekijken.
- hidden feedback met reden en non-relevant categorieën wordt apart gelogd.
Feedbackregels worden als metadata op de feedback opgeslagen (`feedback.metadata["learning"]` met status, reden, feature, delta, sample count).
Offline evaluatie is beschikbaar via `scripts/feedback_learning_report.py` met feedbackcounts, churn en non-learning categorieën.
## 8. AI-rol
Toegestaan:
- support-/consultancy-/travelratio schatten;
- senioritylabel;
- korte Nederlandse samenvatting;
- evidencefragmenten selecteren;
- ambiguïteitswarnings.
Niet toegestaan:
- harde uitsluiting toevoegen zonder deterministisch verifieerbaar signaal;
- vacature verwijderen;
- URL bezoeken of tools aanroepen;
- source policy wijzigen;
- e-mail of sollicitatie verzenden;
- profiel zelfstandig herschrijven.
AI-input staat tussen duidelijke datamarkers, output volgt JSON-schema, temperatuur is 0 en alle waarden worden gevalideerd/geclamped. Uitval is een normale fallback, geen pipelinefout.
- AI-status en evidence worden altijd opgeslagen in `ScoreRun.evidence` en vallen nooit terug op hard-exclusions of sourcepolicywijzigingen.
+108
View File
@@ -0,0 +1,108 @@
# Securityarchitectuur
## 1. Principes
- fail closed voor bronpolicy en URL-validatie;
- least privilege voor containers en credentials;
- externe inhoud is data;
- menselijke bevestiging voor impactvolle externe acties;
- korte retentie en beperkte evidence;
- defense in depth: sanitization + CSP + outputescaping;
- idempotentie en audit boven verborgen magie.
## 2. Netwerkpad
Alle automatische publieke HTTP(S)-requests lopen via `apps.sources.services.fetcher.fetch_url`.
Verplichte stappen:
1. `assess_url`: hostname + denylist + source policy/status;
2. `validate_public_url`: schema, credentials, hostname, poort, DNS, alle IP's global;
3. request zonder automatische redirects;
4. ieder redirectdoel opnieuw door policy en URL-validatie;
5. response status/contenttype/grootte controleren;
6. alleen allowlisted headers bewaren.
Geen adapter of view mag dit pad omzeilen.
## 3. SSRF
Geblokkeerd:
- localhost en `.localhost`;
- IPv4/IPv6 loopback, private, link-local, multicast, unspecified en niet-global;
- embedded credentials;
- non-HTTP(S);
- niet-standaardpoorten tenzij expliciet geconfigureerd;
- redirect naar geblokkeerd doel.
Resterend risico: klassieke DNS rebinding tussen resolutie en socketconnectie. Productiehardening moet connectie-IP pinning of een outbound proxy/egress firewall toevoegen. Tot die tijd hoort de container geen toegang te hebben tot gevoelige interne control planes.
## 4. Webbeveiliging
- Django authentication en user-scoped querysets;
- CSRF-middleware op mutaties;
- secure/HttpOnly/SameSite cookies in productie;
- HSTS en SSL redirect achter correct geconfigureerde proxy;
- `X-Frame-Options: DENY` en CSP `frame-ancestors 'none'`;
- content type nosniff, strict referrer, beperkte permissions policy;
- geen externe scripts/fonts in kerninterface;
- vacature-HTML door Bleachallowlist vóór opslag/rendering.
## 5. E-mail
- dedicated mailbox;
- attachments worden genegeerd;
- message identity voorkomt dubbele verwerking;
- links worden niet automatisch gevolgd tenzij later door bronpolicy goedgekeurd;
- mailboxcredentials alleen environment/secret;
- raw mail korte retentie;
- log geen volledige onderwerp/body wanneer daarin PII kan staan.
## 6. AI
- opt-in en lokaal als defaultarchitectuur;
- vacaturetekst expliciet onbetrouwbare data;
- geen tools/function calling;
- gestructureerd schema en temperatuur 0;
- maximaal inputvolume;
- outputvalidatie en deterministische fallback;
- model-/promptversie opslaan;
- AI-container hoeft geen internettoegang.
## 7. Secrets
Minimaal:
- Django secret key;
- databasepassword;
- adminpassword bootstrap;
- IMAP/SMTPcredentials;
- eventueel reverse-proxy/OIDC secrets.
Regels:
- `.env` is gitignored en mode 600;
- geen secrets in Composebestand, fixtures, screenshots of docs;
- productie gebruikt Unraid secrets/configpad met beperkte rechten;
- rotatie na lek of restore naar onvertrouwde host;
- back-ups van config/secrets apart versleutelen.
## 8. Containerhardening (productietaak)
Aanbevolen:
- non-root runtimeuser;
- read-only root filesystem waar haalbaar;
- `no-new-privileges`;
- capabilities drop all;
- tmpfs voor `/tmp` en Celery beatschedule;
- resource limits;
- database/Redis niet publiceren;
- apart egressnetwerk/proxy;
- immutable image digest;
- dependency/image scanning.
## 9. Audit
Bronruns, vacatureversies, score runs, feedback en outbox vormen de functionele audittrail. Securityauditlogs moeten aanvullend login-/policy-/quarantainewijzigingen opnemen zonder vacaturetekst of secrets.
+160
View File
@@ -0,0 +1,160 @@
# Bron- en adapterarchitectuur
## 1. Adaptercontract
Iedere adapter implementeert conceptueel:
```python
class SourceAdapter(Protocol):
parser_key: str
parser_version: str
def extract(self, content: str, *, url: str) -> ExtractionResult: ...
```
`ExtractionResult` bevat:
- `jobs: list[ExtractedJob]`;
- stabiele parser key/version;
- confidence 0..1;
- machine-/mensleesbare warnings.
Adapters:
- doen geen netwerkrequests;
- schrijven niet naar ORM;
- voeren geen scoring of bronpolicy uit;
- werken deterministisch op aangeleverde fixtures;
- bewaren alleen beperkte raw payload voor debugging;
- leveren evidence per belangrijk veld.
## 2. Parservolgorde
Voor HTML:
1. JSON-LD `JobPosting`;
2. gespecialiseerde adapter op expliciete source parser key;
3. generieke HTML-fallback;
4. geen vacature wanneer minimumvelden ontbreken.
Voor XML/feed:
1. RSS/Atom;
2. sitemapdiscovery (backlog);
3. geen generieke HTMLparser.
Voor e-mail:
1. MIME decode zonder attachments;
2. linkextractie met unsubscribe/privacyfilter;
3. platformalias bewaren;
4. oorspronkelijke werkgever proberen te resolveren in een aparte begrensde taak.
## 3. Minimumvelden
Een jobdraft is verwerkbaar wanneer minstens aanwezig:
- titel;
- HTTP(S)-URL of stabiele externe identiteit.
Employer, locatie en beschrijving mogen tijdelijk ontbreken, maar verlagen confidence. Een generieke pagina zonder herkenbare titel wordt niet als job opgeslagen.
## 4. Confidence
Richtwaarden:
| Methode | Baseline |
|---|---:|
| JSON-LD geldig individueel JobPosting | 0,95 |
| gespecialiseerde publieke ATS-adapter | 0,90 |
| RSS/Atom | 0,82 |
| vacaturemailanchor | 0,650,75 |
| generieke HTMLselectors | 0,450,75 |
Fieldconfidence en extractionconfidence zijn gescheiden. Een perfecte titel maakt een ontbrekende werkgever niet betrouwbaar.
## 5. Nieuwe adapter toevoegen
1. Maak `apps/sources/adapters/<naam>.py`.
2. Geef vaste parser key en semverachtige version.
3. Parse alleen de aangeleverde content.
4. Maak geanonimiseerde fixture onder `fixtures/`.
5. Test happy path, ontbrekende velden, gewijzigd markup en malafide HTML.
6. Registreer uitsluitend voor een expliciete source/parserdetectie.
7. Documenteer bronbeleid, rate limit en canonical URL-gedrag.
8. Voeg traceability toe.
## 6. ATS-strategie
Gespecialiseerde adapters mogen publieke HTML of publieke, documenteerbare endpoints gebruiken wanneer:
- geen login/token nodig is;
- bronpolicy `allow` is;
- gebruiksvoorwaarden/robots zijn beoordeeld;
- het endpoint rechtstreeks vacatures van de werkgever publiceert;
- rate limiting en identifiers stabiel zijn;
- fallback naar HTML mogelijk is.
Een endpoint mag technisch JSON zijn zonder een commerciële platform-API-integratie te vormen. De policybeslissing blijft per bron vereist.
## 7. Bronpromotie
```text
candidate
-> policy/robots/terms review
-> trial (kleine frequentie, parserconfidence meten)
-> active (voldoende succesvolle runs)
-> quarantined (policy, security of herhaalde parsefout)
-> trial/active na expliciet herstel
```
Automatische promotie naar `active` vereist minimaal:
- meerdere succesvolle runs;
- geen private/deny redirect;
- voldoende extractieconfidence;
- geen onverwachte volumepiek;
- policy nog geldig.
Juridische voorwaarden mogen niet door een taalmodel als definitief toegestaan worden verklaard.
## 8. Rate limiting
Per eTLD+1/domein:
- concurrency standaard 1;
- minimum interval standaard 30 seconden;
- respecteer `Retry-After`;
- conditionele GET met ETag/Last-Modified;
- exponentiële backoff bij fout;
- geen retry op policyblokkade;
- adaptieve lagere frequentie bij stabiele, zelden wijzigende bron.
## 9. Parserdrift
Bronhealth detecteert:
- leeg resultaat waar eerder vacatures waren;
- sterke daling in velddichtheid/confidence;
- nieuwe foutstatus/contenttype;
- wijziging in jobvolume;
- ontbrekende title/URL;
- veel nieuwe canonical keys door URLtrackingwijziging.
Automatisch herstel mag veilige selectorvarianten proberen, maar nieuwe logica moet eerst op opgeslagen fixture/snapshot draaien. Geen live agressieve exploratie.
## Publieke ATS-providers (VR-106)
| Provider | Hostherkenning | Parserbenadering | Opmerking | Rate limit (advies) |
|---|---|---|---|---|
| Greenhouse | `*.greenhouse.io`, `*.boards.greenhouse.io` | gespecialiseerd endpoint/JSON + detailstructuren | gebruik alleen publieke boardpagina's/feeds, geen private/persoonlijke endpoints | 15 requests/min met conditional request indien beschikbaar |
| Lever | `jobs.lever.co` | gespecialiseerd JSON/HTML listing+detail | alleen publieke postings, geen recruiter login flow | 60 requests/min per domein |
| Recruitee | `*.recruitee.com` | gespecialiseerd listing+detail parser | parse alleen door werkgever geïntendeerde publieke vacature-URLs | 60 requests/min per domein |
| SmartRecruiters | `*.smartrecruiters.com` | gespecialiseerd listing+detail parser | geen interne API-auth nodig; stop bij CAPTCHA/anti-bot | 30 requests/min per domein |
| Workable | `apply.workable.com` | gespecialiseerd listing+detail parser | alleen publieke vacaturepagina/JSON; geen private endpoints | 30 requests/min per domein |
Voor elke provider-adapter:
- `supports()` kijkt eerst op host/marker om andere pagina's niet te matchen;
- listingpayloads produceren kandidaten zonder fetch van detail;
- detailpayloads vullen een enkele vacaturestructuur;
- elke veldafklaring krijgt veld-evidence met method-labels en confidence.
+159
View File
@@ -0,0 +1,159 @@
# Systeemarchitectuur
## 1. Architectuurstijl
VacatureRadar gebruikt een modulaire Django-monoliet. De domeinen delen één relationele database maar hebben duidelijke model-, service-, task- en UI-grenzen. Dit houdt deployment en beheer eenvoudig op Unraid, terwijl de kernlogica testbaar en later splitsbaar blijft.
## 2. Context
```mermaid
flowchart LR
U[Gebruiker] -->|browser| W[VacatureRadar web]
M[Vacaturemailbox] -->|IMAP| C[Collectielaag]
E[Publieke werkgevers- en ATS-pagina's] -->|HTTP(S)| C
C --> P[Extractie en normalisatie]
P --> D[(PostgreSQL)]
D --> S[Scoring en dedupe]
S --> W
S --> N[Digest/outbox]
N -->|SMTP| U
O[Optionele Ollama] <--> S
```
## 3. Containerarchitectuur
```mermaid
flowchart TB
RP[Reverse proxy / LAN] --> WEB[web: Gunicorn + Django]
WEB --> PG[(PostgreSQL)]
WEB --> REDIS[(Redis)]
WORKER[Celery worker] --> PG
WORKER --> REDIS
WORKER --> NET[Goedgekeurde publieke bronnen]
BEAT[Celery beat] --> REDIS
OLLAMA[Ollama optioneel] <--> WORKER
MAIL[IMAP/SMTP optioneel] <--> WORKER
```
Containerrollen:
- **web** — authenticatie, dashboard, detailpagina's, profiel- en beheerflows;
- **worker** — bronfetches, parsing, scoring, lifecycle, mailbox en verzending;
- **scheduler** — periodieke planning, geen bedrijfslogica;
- **postgres** — canonieke data, historie en outbox;
- **redis** — broker/resultbackend, geen bron van waarheid;
- **ollama** — optioneel lokaal model, niet nodig voor kern.
## 4. Domeinmodules
### `apps/core`
- liveness/readiness;
- dashboardaggregatie;
- systeemstatus;
- securityheaders en navigatiecontext.
### `apps/profiles`
- user-scoped zoekprofielen;
- harde regels, soft preferences en scoregewichten;
- profielversies en gecontroleerde feedbackdelta's.
### `apps/sources`
- bronregister en policy/status;
- veilige fetcher;
- adapters en source discovery;
- raw document, source run en e-mailrecord;
- Celery-orkestratie.
### `apps/jobs`
- canonieke vacature- en werkgevermodellen;
- normalisatie, features, dedupe en provenance;
- scoreberekening en scorehistorie;
- feedback, lifecycle en sollicitatiedossier.
### `apps/notifications`
- dagelijkse payloadselectie;
- idempotente outbox;
- rendering en SMTP-verzending.
## 5. Laagregels
```text
views/templates -> application services -> domain models / adapter interfaces
Celery tasks -> application services -> domain models / adapter interfaces
adapters -> pure extraction DTOs (geen ORM writes)
fetcher -> policy + URL security (geen parserlogica)
AI service -> gestructureerde optionele features (geen beslissingsbevoegdheid)
```
Verboden afhankelijkheden:
- template/view die externe websites fetcht;
- adapter die direct `JobPosting` schrijft;
- AI die `Feedback`, `Application`, `Source.policy` of hard rules wijzigt;
- task met verborgen businessregels die niet in een service testbaar zijn;
- model save-hook die netwerkverkeer uitvoert.
## 6. Verwerkingsflow
```mermaid
sequenceDiagram
participant B as Beat
participant T as Source task
participant F as Safe fetcher
participant A as Adapter registry
participant P as Pipeline
participant DB as PostgreSQL
participant S as Scoring
B->>T: schedule due source
T->>DB: create SourceRun
T->>F: fetch approved URL
F->>F: policy + DNS/IP + redirect checks
F-->>T: bounded document / 304 / error
T->>DB: save RawDocument
T->>A: extract(document)
A-->>P: ExtractionResult
P->>P: normalize + sanitize + features
P->>DB: find exact/fuzzy existing job
P->>DB: upsert job, alias, evidence, version
P->>S: score for active profiles
S->>DB: append ScoreRun
T->>DB: finish SourceRun + schedule next
```
## 7. Consistentie en transacties
- `process_raw_document` en `persist_draft` zijn transactioneel;
- unieke sleutels beschermen races op canonieke vacature, e-mail en outbox;
- scorehistorie is append-only;
- raw document en source run vormen debugcontext;
- een mislukte individuele e-maillink wordt in het e-mailrecord vastgelegd zonder andere links te verliezen;
- externe calls gebeuren buiten lange database-locks waar mogelijk.
## 8. Schaalpad
De eerste schaalgrens ligt niet bij HTTP-requestvolume maar bij het aantal bronjobs. Tot ongeveer tienduizenden vacatures en honderden bronnen volstaat de monoliet. Optimalisaties vóór opsplitsing:
1. indexes en querysetprofiling;
2. chunked scoring;
3. task queues per prioriteit;
4. per-domain rate limit en distributed lock;
5. materialized/latest scoreselectie;
6. raw-bodycompressie/objectstorage.
Pas na aantoonbare bottleneck kan collection worker als aparte service worden afgesplitst. De adapter- en DTO-grenzen maken dat mogelijk zonder nu operationele complexiteit toe te voegen.
## 9. Deploymenttrust boundaries
- browser ↔ web: authenticated session + CSRF;
- web/worker ↔ database/Redis: intern containernetwerk;
- worker ↔ internet: alleen goedgekeurde URL's via safe fetcher;
- worker ↔ IMAP/SMTP/Ollama: expliciete environmentconfiguratie;
- reverse proxy ↔ internet: TLS, host filtering en toegangsbeleid buiten de app.
Zie `SECURITY_ARCHITECTURE.md` en `docs/quality/THREAT_MODEL.md`.
@@ -0,0 +1,32 @@
# ADR-0001 — Modulaire Django-monoliet
- Status: accepted
- Datum: 2026-07-20
## Context
De applicatie is persoonlijk, self-hosted en heeft meerdere samenhangende domeinen maar beperkte operationele schaal. Afzonderlijke frontend/backend/microservices zouden deployment, auth, transacties, upgrades en debugging op Unraid verzwaren.
## Besluit
Gebruik één Django-codebase met domeinapps, server-rendered templates, PostgreSQL en Celery/Redis voor asynchroon werk. Bedrijfslogica blijft in services en adapters zodat latere extractie mogelijk blijft.
## Gevolgen
Positief:
- één image en releaseflow;
- eenvoudige auth/CSRF;
- transactionele pipeline;
- lage beheerlast;
- goede testbaarheid.
Negatief:
- frontendinteractie is minder SPA-achtig;
- domeingrenzen moeten door conventie/tests bewaakt worden;
- lange collectiontaken mogen nooit in webrequests draaien.
## Herzieningstrigger
Alleen herzien bij meetbare bottleneck, onafhankelijk releasevereiste of team-/tenantgrens die niet met modules/queues kan worden opgelost.
@@ -0,0 +1,19 @@
# ADR-0002 — Strikt bronbeleid en geen directe platformbots
- Status: accepted
- Datum: 2026-07-20
## Context
Grote vacatureplatformen hebben toegangsvoorwaarden, botdetectie en databank-/privacyrisico's. De gebruiker wil minimale handelingen, maar niet ten koste van blokkadeomzeiling of onbeheerbare scraping.
## Besluit
Onbekende bronnen starten als `review`. Alleen `allow`-bronnen worden automatisch gefetcht. LinkedIn, Indeed, StepStone, Jobat en VDAB staan standaard op de directe fetchdenylist. Platformvacatures komen binnen via door de gebruiker ingestelde e-mailalerts of handmatige import; de tool probeert de oorspronkelijke werkgeversbron te vinden.
## Gevolgen
- minder brede directe platformdekking;
- veel lagere juridische/technische onderhoudslast;
- bronaudit en policyreview worden productfunctionaliteit;
- denylistlinks mogen als alias bestaan maar worden niet automatisch bezocht.
@@ -0,0 +1,19 @@
# ADR-0003 — Deterministische kern vóór optionele AI
- Status: accepted
- Datum: 2026-07-20
## Context
Vacatureteksten zijn onbetrouwbare externe data. Taalmodellen kunnen hallucineren, gevoelig zijn voor prompt injection en niet altijd beschikbaar zijn.
## Besluit
Normalisatie, harde uitsluitingen, dedupe, lifecycle en baselinescoring zijn deterministisch. AI is standaard uit en mag alleen gestructureerde aanvullende features, evidence en samenvatting leveren. AI-uitval verandert de kernflow niet.
## Gevolgen
- voorspelbaar en offline bruikbaar;
- uitlegbare fallback;
- minder semantische nuance zonder model;
- model-/promptversie en evaluaties vereist vóór rankingimpact.
@@ -0,0 +1,19 @@
# ADR-0004 — Single-user-first, user-scoped data
- Status: accepted
- Datum: 2026-07-20
## Context
Het primaire doel is één persoonlijke Unraid-installatie. Volledige multi-tenant SaaS-architectuur zou onnodige complexiteit toevoegen, maar globale user-onafhankelijke querysets kunnen later datalekken veroorzaken.
## Besluit
Optimaliseer UX en beheer voor één gebruiker, maar koppel profielen, feedback, applications en digests altijd aan een Django-user en scope wijzigingsviews op die user. Canonieke vacatures/bronnen zijn installatiebreed gedeeld.
## Gevolgen
- eenvoudige installatie;
- veilige basis voor een tweede lokale user;
- geen tenantbilling, uitnodigingen of isolatie op databaseniveau;
- beheerfuncties blijven admin-only in productie.
@@ -0,0 +1,19 @@
# ADR-0005 — Vacaturemails als platformingress
- Status: accepted
- Datum: 2026-07-20
## Context
Vacatureplatformen bieden nuttige zoekalgoritmes en alerts, terwijl directe scraping ongewenst is. E-mail is door de gebruiker geactiveerde, begrensde input.
## Besluit
Gebruik een dedicated IMAP-mailbox en idempotente MIME/linkextractie. Bewaar platformlinks als bronalias. Volg een link alleen wanneer bronpolicy dat later toestaat; probeer bij voorkeur een oorspronkelijke werkgeverspagina te vinden.
## Gevolgen
- gebruiker stelt alerts eenmalig in;
- parser moet variërende e-mailsjablonen verdragen;
- dedicated mailbox en retentie nodig;
- geen platformcredentials in VacatureRadar.
@@ -0,0 +1,18 @@
# ADR-0006 — Geen automatische sollicitaties
- Status: accepted
- Datum: 2026-07-20
## Context
Automatisch solliciteren kan onjuiste verklaringen, privacyverlies, reputatieschade en ongewenste massacommunicatie veroorzaken. Vacatureformulieren verschillen sterk en bevatten vaak gevoelige vragen.
## Besluit
VacatureRadar archiveert, scoort, maakt een dossier en kan conceptinformatie voorbereiden, maar bevat geen externe submitactie. Alleen de gebruiker verstuurt een sollicitatie via het oorspronkelijke kanaal.
## Gevolgen
- menselijke controle blijft gegarandeerd;
- minder end-to-endautomatisering;
- een eventuele toekomstige wijziging vereist een nieuwe expliciete ADR, security/privacyreview en per-submitconfirmatie.
@@ -0,0 +1,19 @@
# ADR-0007 — PostgreSQL als waarheid, Redis/Celery voor werk
- Status: accepted
- Datum: 2026-07-20
## Context
Broncontroles, mailboxpolling en scoring mogen webrequests niet blokkeren. Taken moeten retrybaar zijn, terwijl Redisdata verloren mag gaan zonder canonieke data te verliezen.
## Besluit
PostgreSQL is de enige productiebron van waarheid. Redis is uitsluitend broker/resultbackend. Celery worker en beat voeren periodiek/asynchroon werk uit. SQLite en eager tasks blijven beschikbaar voor lokale tests.
## Gevolgen
- duidelijke herstelstrategie;
- extra twee containers;
- taken moeten idempotent zijn omdat at-least-once uitvoering mogelijk is;
- schedulerstate mag worden herbouwd uit database/sourcevelden.
@@ -0,0 +1,18 @@
# ADR-0008 — Server-rendered UI zonder aparte SPA
- Status: accepted
- Datum: 2026-07-20
## Context
De interface bestaat vooral uit lijsten, detailpagina's, formulieren en enkele POST-acties. Een React-buildketen verdubbelt contracts, auth, dependencies en deployment zonder duidelijke MVP-winst.
## Besluit
Gebruik Django templates, semantische HTML, eigen CSS en minimale vanilla JavaScript. Progressive enhancement mag; kernflows werken zonder clientframework.
## Gevolgen
- snelle, toegankelijke en eenvoudige deployment;
- minder rijke realtimeinteractie;
- API blijft intern tenzij een concrete integratiebehoefte ontstaat.