@@ -0,0 +1,38 @@
|
||||
# Documentatiekaart
|
||||
|
||||
De documentatie is opgesplitst per beslisniveau zodat een mens of coding agent alleen de relevante context hoeft te laden.
|
||||
|
||||
## Startpunten
|
||||
|
||||
- `../CODEX_START_HERE.md` — autonome projectoverdracht;
|
||||
- `ai/PROJECT_STATE.md` — huidige uitvoeringsstatus;
|
||||
- `ai/BACKLOG.yaml` — machineleesbare resterende taken;
|
||||
- `product/PRODUCT_REQUIREMENTS.md` — wat het product moet doen;
|
||||
- `architecture/SYSTEM_ARCHITECTURE.md` — hoe het systeem is opgebouwd;
|
||||
- `design/UX_SPEC.md` — gebruikerservaring en schermgedrag;
|
||||
- `operations/UNRAID_DEPLOYMENT.md` — productie-installatie;
|
||||
- `quality/DEFINITION_OF_DONE.md` — afrondingsnorm.
|
||||
- `../VERIFICATION_REPORT.md` — bewezen baseline en expliciete verificatiebeperkingen.
|
||||
|
||||
## Documenthiërarchie
|
||||
|
||||
Bij conflict geldt, van hoog naar laag:
|
||||
|
||||
1. security- en juridische grenzen in `SECURITY.md` en het bronbeleid;
|
||||
2. expliciete ADR's in `architecture/adrs/`;
|
||||
3. product requirements en acceptatiecriteria;
|
||||
4. architectuur-, API- en UX-specificaties;
|
||||
5. taakacceptatiecriteria in `ai/BACKLOG.yaml`;
|
||||
6. oorspronkelijke referentiedocumentatie in `reference/`.
|
||||
|
||||
Een afgeleid document mag een oorspronkelijke formulering verfijnen, maar niet stilzwijgend een veiligheidsgrens versoepelen.
|
||||
|
||||
## Onderhoud
|
||||
|
||||
Gedragswijzigingen vereisen een update van minimaal:
|
||||
|
||||
- relevante specificatie;
|
||||
- tests;
|
||||
- `quality/TRACEABILITY_MATRIX.md`;
|
||||
- `ai/PROJECT_STATE.md`;
|
||||
- taakstatus of changelog.
|
||||
@@ -0,0 +1,155 @@
|
||||
# Autonoom uitvoeringsprotocol
|
||||
|
||||
Dit protocol is ontworpen voor Codex of een vergelijkbare code-agent die de uitgepakte repository als workspace heeft. Het doel is langdurig voortwerken zonder productvragen, terwijl security- en kwaliteitsgrenzen behouden blijven.
|
||||
|
||||
## Hoofdlus
|
||||
|
||||
```text
|
||||
bootstrap/verify
|
||||
→ selecteer eerste uitvoerbare ready taak
|
||||
→ lees alleen gekoppelde specs/code/tests
|
||||
→ schrijf of verfijn falende tests
|
||||
→ implementeer kleinste complete oplossing
|
||||
→ relevante tests + lint
|
||||
→ docs/traceability/backlog/project state
|
||||
→ volledige gate
|
||||
→ markeer done
|
||||
→ herhaal
|
||||
```
|
||||
|
||||
Commando om de volgende taak te kiezen:
|
||||
|
||||
```bash
|
||||
python scripts/backlog.py next
|
||||
```
|
||||
|
||||
De selectie is deterministisch: bestandsvolgorde in `BACKLOG.yaml`, alleen `status: ready`, alle dependencies `done`.
|
||||
|
||||
## Geen productonderbrekingen
|
||||
|
||||
Vraag niet om bevestiging voor keuzes die in ADRs, productdefaults of backlogacceptatie staan. Bij een echt ontbrekend detail:
|
||||
|
||||
1. kies de veiligste;
|
||||
2. kies de eenvoudigste;
|
||||
3. kies een omkeerbare implementatie;
|
||||
4. behoud bestaande contracts;
|
||||
5. documenteer de aanname in taakresultaat of nieuwe ADR.
|
||||
|
||||
Voorbeelden:
|
||||
|
||||
- onbekende bron → `review`, niet fetch;
|
||||
- ontbrekende locatie → `unknown`, niet gokken;
|
||||
- AI niet beschikbaar → deterministische fallback;
|
||||
- provider niet gekozen → interface + fixture + lokale importer, providerstap extern blokkeren;
|
||||
- rijke frontend niet noodzakelijk → Django template/progressive enhancement.
|
||||
|
||||
## Externe blokkadeprocedure
|
||||
|
||||
Een credential, domein, live bronvoorwaardenreview, registry of gelicenseerde bulkdataset mag codewerk niet stoppen. Voor het taakdeel dat extern geblokkeerd is:
|
||||
|
||||
- implementeer configuratieschema en validatie;
|
||||
- maak providerinterface;
|
||||
- voeg fictieve fixtures/mocks toe;
|
||||
- test succes, fout en disabled state;
|
||||
- schrijf exact productiechecklistje;
|
||||
- maak/gebruik een aparte `blocked-external` taak;
|
||||
- ga onmiddellijk door met de volgende uitvoerbare taak.
|
||||
|
||||
Markeer nooit een volledige feature als geblokkeerd wanneer alleen de live configuratie ontbreekt.
|
||||
|
||||
## Werk per taak
|
||||
|
||||
### 1. Reconnaissance
|
||||
|
||||
- `python scripts/backlog.py show <ID>`
|
||||
- lees requirement- en traceabilityregels;
|
||||
- zoek bestaande services, modellen en tests;
|
||||
- bevestig architectuurgrenzen;
|
||||
- noteer regressierisico's.
|
||||
|
||||
Geen brede herschrijving zonder aantoonbare noodzaak.
|
||||
|
||||
### 2. Test-first waar zinvol
|
||||
|
||||
Voor bugs/securitygrenzen eerst een falende regressietest. Voor nieuwe functionaliteit minstens tests voor:
|
||||
|
||||
- normale input;
|
||||
- lege/ontbrekende input;
|
||||
- malformed/aanvallende input;
|
||||
- replay/idempotentie;
|
||||
- autorisatie/user-scoping;
|
||||
- disabled/external dependency unavailable.
|
||||
|
||||
### 3. Implementatie
|
||||
|
||||
- pure logica in services;
|
||||
- tasks alleen orchestration;
|
||||
- adapters zonder writes;
|
||||
- transacties rond meerdere afhankelijke writes;
|
||||
- machineleesbare foutcategorieën;
|
||||
- geen live internet in tests;
|
||||
- geen secrets of persoonlijke voorbeelden.
|
||||
|
||||
### 4. Relevante verificatie
|
||||
|
||||
```bash
|
||||
uv run ruff format .
|
||||
uv run ruff check .
|
||||
uv run pytest <relevante tests>
|
||||
```
|
||||
|
||||
Herstel oorzaak, niet gate/drempel.
|
||||
|
||||
### 5. Overdracht in dezelfde wijziging
|
||||
|
||||
Werk bij:
|
||||
|
||||
- code en migraties;
|
||||
- tests/fixtures;
|
||||
- relevante product/architectuur/operationele docs;
|
||||
- `TRACEABILITY_MATRIX.md`;
|
||||
- `BACKLOG.yaml` resultaatstatus;
|
||||
- `PROJECT_STATE.md`.
|
||||
|
||||
### 6. Volledige gate
|
||||
|
||||
```bash
|
||||
./scripts/codex_verify.sh
|
||||
```
|
||||
|
||||
Pas na een geslaagde gate:
|
||||
|
||||
```bash
|
||||
python scripts/backlog.py set <ID> done --note "<concreet resultaat + tests>"
|
||||
./scripts/codex_verify.sh
|
||||
```
|
||||
|
||||
De tweede gate bewijst dat de ledgerwijziging zelf geldig is.
|
||||
|
||||
## Omgaan met onverwachte defecten
|
||||
|
||||
Een blocker in bestaande code die de geselecteerde taak verhindert, mag direct worden hersteld. Voeg een regressietest toe en vermeld het in het taakresultaat. Een los, niet-blokkerend defect krijgt een nieuwe taak met uniek ID alleen wanneer het niet veilig binnen de huidige scope past; wijzig de kernprioriteiten niet stilzwijgend.
|
||||
|
||||
## Stopcriteria
|
||||
|
||||
Stop de autonome lus alleen wanneer:
|
||||
|
||||
- geen uitvoerbare `ready` taak meer bestaat;
|
||||
- alle resterende taken `blocked-external` of `deferred` zijn;
|
||||
- de volledige kwaliteitsgate slaagt;
|
||||
- `PROJECT_STATE.md` exact de resterende blokkades en productiechecklist noemt.
|
||||
|
||||
Stop niet alleen omdat één provider, secret of live bron ontbreekt.
|
||||
|
||||
## Eindrapport
|
||||
|
||||
Rapporteer:
|
||||
|
||||
- afgeronde taak-ID's;
|
||||
- belangrijkste gedrag- en datamodelwijzigingen;
|
||||
- migraties/configuratieimpact;
|
||||
- tests en coverage;
|
||||
- externe blokkades met exacte invulstap;
|
||||
- eerste resterende taak of “geen uitvoerbare ready taak”.
|
||||
|
||||
Claim geen productieacceptatie zonder Docker/release-/restore-smoke wanneer die in de taakcriteria staat.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,10 @@
|
||||
# Optioneel voorbeeld voor een lokale Codex-configuratie.
|
||||
# Bewaar echte machine-/accountinstellingen buiten de repository.
|
||||
# De repository zelf geeft geen toestemming om buiten de workspace te schrijven
|
||||
# of veiligheids-/approvalgrenzen te omzeilen.
|
||||
|
||||
sandbox_mode = "workspace-write"
|
||||
approval_policy = "on-request"
|
||||
|
||||
# Voeg alleen vertrouwde documentatie-/packagehosts toe wanneer de runtime dit vereist.
|
||||
# Tests en normale implementatie moeten offline kunnen draaien.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Zelfreview voor een autonome taak
|
||||
|
||||
Voer deze review uit vóór `BACKLOG.yaml` naar `done` gaat.
|
||||
|
||||
## Correctheid
|
||||
|
||||
- Is ieder acceptatiecriterium aantoonbaar in code/test/documentatie terug te vinden?
|
||||
- Klopt gedrag bij nul items, dubbele uitvoering, gedeeltelijke fout en tijdzonegrens?
|
||||
- Is het datamodel consistent na een exception of retry?
|
||||
- Zijn bestaande canonieke data, provenance en gebruikersacties behouden?
|
||||
|
||||
## Security
|
||||
|
||||
- Kan externe input een URL, command, template, log of AI-instructie worden?
|
||||
- Worden policy en SSRF na redirects opnieuw toegepast?
|
||||
- Zijn HTML, filenames, exports en e-mailvelden gesanitized/begrensd?
|
||||
- Is iedere muterende view authenticated, CSRF-beschermd en user-scoped?
|
||||
- Kan een secret/PII in debugoutput, exception of fixture terechtkomen?
|
||||
|
||||
## Architectuur
|
||||
|
||||
- Staat beslislogica in een service en niet in view/task/template?
|
||||
- Doet een adapter uitsluitend extractie?
|
||||
- Is de dependency nodig en gelockt?
|
||||
- Is de verandering compatibel met accepted ADRs?
|
||||
- Is een migratie werkelijk nodig en veilig?
|
||||
|
||||
## UX
|
||||
|
||||
- Is feit versus inferentie versus onbekend helder?
|
||||
- Werkt de kern zonder JavaScript, met keyboard en op smal scherm?
|
||||
- Zijn fouten herstelbaar en leggen ze geen technische details/secrets bloot?
|
||||
- Vermijdt de flow extra dagelijkse handelingen?
|
||||
|
||||
## Tests
|
||||
|
||||
- Zou de test falen zonder de implementatie?
|
||||
- Zijn negatieve en replaypaden aanwezig?
|
||||
- Gebruikt geen standaardtest live internet of persoonlijke data?
|
||||
- Is een bug als regressietest vastgelegd?
|
||||
- Slaagt de volledige gate zonder thresholds te verlagen?
|
||||
|
||||
## Overdracht
|
||||
|
||||
- Zijn traceability, runbook, configvoorbeeld en threat model aangepast waar nodig?
|
||||
- Bevat het taakresultaat concrete uitkomst en verificatie, geen algemene claim?
|
||||
- Is `PROJECT_STATE.md` eerlijk over wat niet live getest kon worden?
|
||||
@@ -0,0 +1,20 @@
|
||||
# Beslissingslog
|
||||
|
||||
Accepted architectuurkeuzes staan formeel in `docs/architecture/adrs/`. Dit log bevat uitvoeringsdefaults die Codex niet opnieuw hoeft voor te leggen.
|
||||
|
||||
| Datum | Beslissing | Reden | Herzieningstrigger |
|
||||
|---|---|---|---|
|
||||
| 2026-07-20 | Werknaam en package blijven `VacatureRadar`/`vacatureradar`. | Consistente repository, UI en deployment. | Alleen expliciete rebranding. |
|
||||
| 2026-07-20 | UI-taal Nederlands (`nl-BE`), technische identifiers Engels. | Doelgebruiker en onderhoudbaarheid. | Meertalige productrequirement. |
|
||||
| 2026-07-20 | Single-user-first; user-scoped persoonlijke data. | Lage beheerlast zonder toekomstige lekke querysets. | Multi-tenant productbesluit. |
|
||||
| 2026-07-20 | België-first, tijdzone Europe/Brussels. | Relevantie voor regio, digest en datums. | Gebruiker verhuist of meertalige release. |
|
||||
| 2026-07-20 | Django templates + minimale vanilla JS. | Geen onnodige SPA/API/deploymentlaag. | Meetbare UX-beperking met ADR. |
|
||||
| 2026-07-20 | PostgreSQL productie, SQLite tests/dev, Redis geen waarheid. | Transacties, eenvoudige lokale start en herstelbaarheid. | Schaal/HA-requirement. |
|
||||
| 2026-07-20 | Onbekende bron is review; direct fetch alleen allow. | Fail-closed veiligheid en bronbeheer. | Nooit verzwakken zonder security-/bronreview. |
|
||||
| 2026-07-20 | Platforms via vacaturemail/manual import, niet direct crawlen. | Minimale handelingen zonder login-/botrisico. | Expliciete schriftelijke toestemming en nieuwe ADR. |
|
||||
| 2026-07-20 | Harde regels, dedupe en lifecycle zijn deterministisch. | Uitlegbaar, offline, prompt-injectionbestendig. | Geen voorzien trigger; AI blijft aanvullend. |
|
||||
| 2026-07-20 | Ollama standaard uit; geen cloud-AI nodig. | Privacy en set-and-forget zonder externe account. | Expliciete providerkeuze met privacyreview. |
|
||||
| 2026-07-20 | Geen automatische sollicitatie of externe submit. | Menselijke eindcontrole en reputatie/privacy. | Alleen nieuwe product-/securitybeslissing; huidige scope verbiedt het. |
|
||||
| 2026-07-20 | Geen ongeverifieerde bulk-geodata in repository. | Licentie, actualiteit en ZIP-grootte. | Gecontroleerde dataset met herkomst/licentie/checksum. |
|
||||
| 2026-07-20 | Externe blokkades worden geïsoleerd, niet als reden om te stoppen gebruikt. | Codex kan code, tests en docs autonoom afmaken. | Geen. |
|
||||
| 2026-07-20 | Minimum coverage 70% branch-aware; securitykritieke paden hoger. | Betrouwbare baseline zonder cosmetische testjacht. | Alleen omhoog na stabiele baseline. |
|
||||
@@ -0,0 +1,155 @@
|
||||
# Projectstatus
|
||||
|
||||
- Laatst bijgewerkt: 2026-07-21
|
||||
- Repositoryversie: 0.1.0 MVP-basis
|
||||
- Uitvoeringsmodus: autonome backlog
|
||||
- Eerste uitvoerbare taak: controleer met `python scripts/backlog.py next`
|
||||
|
||||
## Werkende basis
|
||||
|
||||
De repository bevat een uitvoerbare Django-MVP met:
|
||||
|
||||
- profielen, revisies en begrensde feedbackconfiguratie;
|
||||
- source registry, policy/denylist, veilige fetcher en source runs;
|
||||
- JSON-LD-, HTML-, RSS/Atom- en e-mailadapters;
|
||||
- normalisatie, sanitization, werkgeverresolutie, dedupe, aliassen, provenance en versies;
|
||||
- deterministische features, harde exclusions, scores en optionele uitgeschakelde Ollama-boundary;
|
||||
- dashboard, vacaturelijst/detail, profiel-, bron- en sollicitatiepagina's;
|
||||
- feedbackacties, applicationdossiers, lifecycle en dagelijkse digest-outbox;
|
||||
- Celeryworker/scheduler, healthchecks, Docker/Unraidconfiguratie en back-up-/herstelscripts;
|
||||
- offline unit-, integratie- en securitytests.
|
||||
|
||||
## Laatste geverifieerde baseline
|
||||
|
||||
De definitieve projectbasis en een schoon uit de ZIP opgebouwde checkout zijn beide geverifieerd met:
|
||||
|
||||
- 43 geslaagde tests;
|
||||
- 80,42% branch-aware codedekking over `apps` en `config`;
|
||||
- Ruff en Django system checks geslaagd;
|
||||
- geen ontbrekende migraties;
|
||||
- clean-room bootstrap, demo-import en HTTP-smoke voor liveness, readiness en login geslaagd.
|
||||
|
||||
## Laatste uitgevoerde backlogtaak
|
||||
|
||||
- 2026-07-21: `VR-117` voltooid.
|
||||
- Productiereleasecontrole toegevoegd met `scripts/release_verify.sh`, inclusief smoke, backup/restore-smoke en artifactbundel.
|
||||
- Changelog, traceability en Unraid-runbook samengekoppeld aan het releasepad.
|
||||
- Volgende taak: `VR-201` (blocked-external).
|
||||
|
||||
- 2026-07-21: `VR-116` voltooid.
|
||||
- Synthetische testset (`fixtures/benchmark/quality_benchmark.json`) en benchmarkspec (`docs/quality/BENCHMARKS.md`) toegevoegd.
|
||||
- `tests/benchmark/test_benchmark.py` bevat smokecontrole voor dataset load, helperlogica en rapportschema.
|
||||
- Geen UI/UITA-aanpassingen uitgevoerd (expliciet buiten scope, extern met Stitch).
|
||||
|
||||
- 2026-07-21: `VR-108` voltooid.
|
||||
- Resolutie naar oorspronkelijke werkgever geïmplementeerd met deterministische matcher voor recruiter/ATS-bronnen.
|
||||
- Titel, locatie, employer-naam, domein en canonical metadata leveren evidence-rijke beslissingen op; conflicten leiden naar review i.p.v. auto-merge.
|
||||
- Bij directe resolutie wordt `canonical_url` naar de directe bron gekopieerd terwijl alias/provenance behouden blijft.
|
||||
- Unittests en integratietest toegevoegd voor resolvergedrag en pipeline-resolutie.
|
||||
- 2026-07-21: `VR-109` voltooid.
|
||||
- AI-analyse geïntegreerd als optionele scorecomponent met caching op `content_hash + model + prompt + schema`.
|
||||
- Uitgebreide JSON-schema validatie, evidencecontrole en foutstatussen met deterministische fallback.
|
||||
- Golden evaluatieset met validatiescenario's (disabled, valid, malformed, timeout, prompt-injection) toegevoegd in `fixtures/ai/evaluation_set.json`.
|
||||
- Scoringsmetadata uitgebreid met AI-status, errorcategorie en weging voor uitlegbaarheid.
|
||||
- 2026-07-21: `VR-110` voltooid.
|
||||
- Feedbackleren standaard uit met opt-in, per-feature sampledrempel en metadata logging.
|
||||
- Verbergreasons worden geclassificeerd als niet-learn (titel/afstand/werkvorm) of leerrijp.
|
||||
- Positieve feedback groeit alleen na minimale herhaling per feature; negatieve feedback past gewichten terug indien leerrijp.
|
||||
- Offline evaluatiescript toegevoegd: `scripts/feedback_learning_report.py`.
|
||||
- UI-presentatie/reset weergave staat open voor externe afhandeling (Stitch).
|
||||
- Volgende taak: `VR-111`.
|
||||
|
||||
- 2026-07-21: `VR-111` voltooid.
|
||||
- Reminder-outbox toegevoegd voor sluitingsdatum, opvolgdatum en uitzonderlijk sterke nieuwe matches.
|
||||
- Topmatchherinnering is per profiel in- of uit te schakelen en vereist score/confidence/drempel.
|
||||
- Herinneringen hebben stabiele event keys, respecteren quiet hours en gebruiken profielweekdagen.
|
||||
- Integratietests toegevoegd voor idempotentie, stille ontvangers, follow-up werkdagen, DST en rustige uren.
|
||||
- Volgende taak: `VR-112`.
|
||||
|
||||
- 2026-07-21: `VR-112` voltooid.
|
||||
- Bronhealth en observability toegevoegd met rolling metrics, parserdriftdetectie en automatische quarantaine.
|
||||
- Raw cleanup bewaart gequarantaineerde documenten en kanaryherstel verloopt via trialpad.
|
||||
- Dagelijkse taken gepland voor source-health evaluatie, canaryherstel en geautomatiseerde raw cleanup.
|
||||
- Integratietests toegevoegd voor tijdelijke fouten, policyfouten, parserdrift, canaryherstel en raw cleanup.
|
||||
- Volgende taak: `VR-113`.
|
||||
|
||||
- 2026-07-21: `VR-113` voltooid.
|
||||
- Sollicitatiedossiers bevriezen nu een gesanitiseerde snapshot met bronlinks, status/contact/notities en timeline-events.
|
||||
- Export bevat dossier JSON, timeline CSV en printbare HTML zonder externe assets.
|
||||
- Dossierexport en dossierverwijdering zijn gebruiker-scoped en idempotent.
|
||||
- Integratietests toegevoegd voor snapshot, export, print, timeline en cross-user-afscherming.
|
||||
- Volgende taak: `VR-114`.
|
||||
|
||||
- 2026-07-21: `VR-114` voltooid.
|
||||
- Browser-e2e toegevoegd voor login, dashboard, lijst/detailfilter, profielbewerkingen,
|
||||
vier feedbackacties, sollicitatiedossierbewerking en handmatige tekstimport.
|
||||
- Accessibilitycontrole toegevoegd met Playwrightpad (desktop + 360px viewport) en fallback HTML-a11yprobe
|
||||
wanneer browser niet beschikbaar is.
|
||||
- Screenshots worden vastgelegd bij falen binnen e2e-run.
|
||||
- Volgende taak: `VR-115`.
|
||||
|
||||
- 2026-07-21: `VR-115` voltooid.
|
||||
- Productie-hardening toegevoegd met non-root/read-only containerinstellingen, CI-secret- en kwetsbaarheidsscans,
|
||||
production settings validatie, DNS-rebindcontrole in fetcher en rate limiting op login/manual import.
|
||||
- Nieuwe tests toegevoegd voor `settings`-validatie, rate limiteren, login-manual-import blokkades
|
||||
en DNS-rebinddetectie.
|
||||
- Geen UI-wijzigingen toegevoegd (manueel afhandelen bevestigd).
|
||||
- Volgende taak: `VR-116`.
|
||||
|
||||
- 2026-07-21: `VR-107` voltooid.
|
||||
- Handmatige URL- en tekstimport toegevoegd met CSRF-beveiligde POST-flow en GET-prefill.
|
||||
- URL-import hergebruikt bestaande policy/SSRF-checks en fetchpad; paste-import gebruikt een lokale synthetische HTML-payload zonder extern fetch.
|
||||
- Bronlijst bevat bookmarklet, resultaatpanel met created/duplicate/warnings en links naar vacaturedetail.
|
||||
- Unit- en integrationtests toegevoegd voor URL-flow, paste-flow, denylist en paste-limiet.
|
||||
|
||||
- 2026-07-21: `VR-105` voltooid.
|
||||
- Haversine-afstand berekent nu op basis van geocoderingscoördinaten met confidencegedrag voor exacte afstand.
|
||||
- Reistijdschatting toegevoegd als expliciete, conservatieve offline-schatting en gelabeld als inferentie.
|
||||
- Harde afstandsuitsluiting blijft strikt, met optionele harde reistijdbeperking via `hard_rules["max_commute_minutes"]`.
|
||||
- Bij geocodecache-import worden getroffen actieve vacatures opnieuw gescoord.
|
||||
- Score-evidence verrijkt met afstands- en reistijdmetadata.
|
||||
|
||||
- 2026-07-21: `VR-103` voltooid.
|
||||
- Geïmplementeerd: databaseleaseclaim (`SourceLease` + `SourceOriginState`) voor concurrency- en crash-veiligheid.
|
||||
- Per-origin intervalcoördinatie, backoff inclusief 429/Retry-After en jitter op successen.
|
||||
- Fetcher stuurt expliciete foutcategorieën voor 429, timeout en andere netwerkfouten.
|
||||
|
||||
- 2026-07-21: `VR-101` voltooid.
|
||||
- Inrichting van bronontdekking vanuit HTML/sitemap/feed/e-mail, inclusief JSON-LD en feedlinkdetectie.
|
||||
- Persistente kandidaatservice met provenance in `Source.metadata["discovery"]` en idempotent gedrag.
|
||||
- Bulk-safe kandidaatacties `approve_to_trial` en `dismiss` in bronlijst-UI.
|
||||
- Management command `discover_sources`.
|
||||
- Fixtures toegevoegd voor HTML, sitemap index/urlset, feed en e-mail.
|
||||
|
||||
Een toekomstige agent moet altijd de actuele waarheid opnieuw bepalen met:
|
||||
|
||||
```bash
|
||||
./scripts/codex_verify.sh
|
||||
```
|
||||
|
||||
## Nog te bouwen
|
||||
|
||||
Momenteel staan er geen `ready` taken meer over; de volgende te behandelen item is `VR-201` als `blocked-external` (bronreview).
|
||||
|
||||
## Externe blokkades
|
||||
|
||||
Deze blokkeren onafhankelijke code niet:
|
||||
|
||||
- concrete werkgevers-/bronnenkeuze en actuele menselijke bronreview;
|
||||
- live IMAP/SMTP-account en app-password;
|
||||
- Unraid-domein, DNS, TLS en reverse proxy;
|
||||
- containerregistry en credentials;
|
||||
- keuze/verkrijging van een gelicenseerde actuele Belgische postcode-/gemeentedataset.
|
||||
|
||||
## Bekende verificatiebeperking van de aangeleverde basis
|
||||
|
||||
De Pythonapplicatie en tests zijn lokaal geverifieerd. Een echte Dockerimagebuild en volledige PostgreSQL/Redis/Unraid-smoke vereisen nog steeds een omgeving met Docker; daarom is dat nog niet als uitgevoerd te claimen in deze workspace.
|
||||
|
||||
## Hervatten
|
||||
|
||||
```bash
|
||||
./scripts/codex_verify.sh
|
||||
python scripts/backlog.py next
|
||||
```
|
||||
|
||||
Werk daarna exact volgens `docs/ai/AUTONOMOUS_EXECUTION_PROTOCOL.md`.
|
||||
@@ -0,0 +1,24 @@
|
||||
# Startprompt voor Codex
|
||||
|
||||
Kopieer de tekst hieronder als eerste opdracht nadat de ZIP is uitgepakt en de repository als workspace is geopend.
|
||||
|
||||
```text
|
||||
Werk autonoom aan VacatureRadar.
|
||||
|
||||
1. Lees CODEX_START_HERE.md en AGENTS.md volledig.
|
||||
2. Voer ./scripts/codex_bootstrap.sh uit wanneer de omgeving nog niet is opgebouwd; voer anders ./scripts/codex_verify.sh uit.
|
||||
3. Lees docs/ai/PROJECT_STATE.md en valideer docs/ai/BACKLOG.yaml met python scripts/backlog.py validate.
|
||||
4. Selecteer steeds de eerste taak die python scripts/backlog.py next toont.
|
||||
5. Voer iedere taak volledig uit volgens de acceptance_criteria, gekoppelde requirements, AGENTS.md en docs/quality/DEFINITION_OF_DONE.md.
|
||||
6. Vraag geen productbeslissingen of bevestiging. Gebruik de vastgelegde veilige defaults. Bij ontbrekende externe credentials, accounts, live bronreview, domein of dataset: implementeer interface, fixtures, mocks, tests, foutafhandeling en documentatie; laat alleen het externe restpunt blocked-external en ga meteen verder.
|
||||
7. Houd adapters write-free, bedrijfslogica in services, netwerk fail-closed, harde regels deterministisch vóór AI, en voeg nooit automatisch solliciteren, platformloginbots, CAPTCHA-omzeiling of directe denylistscraping toe.
|
||||
8. Werk bij iedere taak code, migraties, tests, docs, traceability, BACKLOG.yaml en PROJECT_STATE.md samen bij.
|
||||
9. Markeer een taak pas done nadat ./scripts/codex_verify.sh slaagt; draai de gate daarna opnieuw na de ledgerwijziging.
|
||||
10. Herhaal zonder onderbreking tot geen uitvoerbare ready taak meer bestaat. Eindig met een feitelijk eindrapport en exacte blocked-external checklist.
|
||||
```
|
||||
|
||||
## Herstart na een onderbroken sessie
|
||||
|
||||
```text
|
||||
Lees AGENTS.md, docs/ai/PROJECT_STATE.md en de git diff. Draai python scripts/backlog.py validate en ./scripts/codex_verify.sh. Herstel eerst een onvoltooide wijziging of falende gate. Ga daarna verder met python scripts/backlog.py next volgens het autonome protocol. Vraag geen reeds vastgelegde productbeslissingen.
|
||||
```
|
||||
@@ -0,0 +1,173 @@
|
||||
# Interne contracten
|
||||
|
||||
Dit document beschrijft de stabiele grenzen tussen domeinmodules. Het is geen belofte van een externe REST-API. De eerste release gebruikt Django-views, formulieren en servicefuncties; daardoor blijft de onderhoudslast laag en kan later zonder herschrijven een JSON-laag boven dezelfde services worden geplaatst.
|
||||
|
||||
## Contractprincipes
|
||||
|
||||
1. **Adapters lezen, services beslissen.** Een bronadapter maakt alleen een `ExtractionResult`; hij schrijft niet naar de database en voert geen scoring uit.
|
||||
2. **Onbetrouwbare data blijft data.** HTML, e-mail, JSON-LD en modeluitvoer mogen nooit instructies of code worden.
|
||||
3. **Persistente writes zijn atomair en idempotent.** Herhaalde import van dezelfde broninhoud maakt geen tweede vacature aan.
|
||||
4. **Orchestrators bevatten geen productregels.** Celery-taken plannen en registreren; domeinservices normaliseren, dedupliceren en scoren.
|
||||
5. **AI is optioneel.** Geen core-contract mag een taalmodel vereisen om geldige output te leveren.
|
||||
|
||||
## Bronadapter
|
||||
|
||||
Implementatie: `apps/sources/adapters/base.py`.
|
||||
|
||||
```python
|
||||
class SourceAdapter(Protocol):
|
||||
parser_key: str
|
||||
parser_version: str
|
||||
|
||||
def extract(self, content: str, *, url: str) -> ExtractionResult: ...
|
||||
```
|
||||
|
||||
### Invoer
|
||||
|
||||
- `content`: reeds opgehaalde Unicode-inhoud; netwerktoegang vindt vóór de adapter plaats.
|
||||
- `url`: gevalideerde bron- of document-URL, uitsluitend voor context en relatieve URL-resolutie.
|
||||
|
||||
### Uitvoer
|
||||
|
||||
`ExtractionResult` bevat:
|
||||
|
||||
- `jobs`: nul of meer `ExtractedJob`-objecten;
|
||||
- `parser_key` en `parser_version`: reproduceerbare parseridentiteit;
|
||||
- `confidence`: globaal getal tussen `0.0` en `1.0`;
|
||||
- `warnings`: niet-fatale, korte, machineveilige waarschuwingen.
|
||||
|
||||
Een `ExtractedJob` gebruikt zo veel mogelijk brongetrouwe waarden. Normalisatie gebeurt pas in `normalize_extracted_job`. Een adapter moet:
|
||||
|
||||
- ontbrekende velden leeg laten in plaats van ze te verzinnen;
|
||||
- iedere URL absoluut maken;
|
||||
- beschrijving als bron-HTML én/of platte tekst leveren;
|
||||
- veldbewijs toevoegen wanneer een waarde heuristisch is gevonden;
|
||||
- nooit secrets, cookies, headers of volledige mailmetadata in `raw` opnemen;
|
||||
- geen exception gebruiken voor een geldig document met nul vacatures.
|
||||
|
||||
## Fetcher
|
||||
|
||||
Implementatie: `apps/sources/services/fetcher.py` en `url_security.py`.
|
||||
|
||||
```python
|
||||
fetch_url(
|
||||
url: str,
|
||||
*,
|
||||
source: Source | None = None,
|
||||
conditional_headers: dict[str, str] | None = None,
|
||||
) -> FetchedDocument
|
||||
```
|
||||
|
||||
Voorwaarden vóór ieder request en na iedere redirect:
|
||||
|
||||
- schema is `http` of `https`;
|
||||
- host is niet leeg, localhost, link-local, multicast, private of anderszins speciaal;
|
||||
- poort is 80/443, tenzij expliciet toegestaan;
|
||||
- domein is niet op de centrale platformdenylist;
|
||||
- bronstatus en bronbeleid laten toegang toe;
|
||||
- response blijft binnen tijd-, redirect- en bytegrenzen.
|
||||
|
||||
`FetchedDocument` bevat de aangevraagde en uiteindelijke URL, status, gefilterde headers, bytes, tekst en SHA-256. Callers mogen de responsebody niet loggen.
|
||||
|
||||
Machineleesbare foutcategorieën:
|
||||
|
||||
- `PolicyBlockedError`: bron/domein is niet toegestaan; niet blind retrien;
|
||||
- `ContentRejectedError`: type, grootte of response is onbruikbaar;
|
||||
- `FetchError`: tijdelijke netwerk- of HTTP-fout die begrensd mag worden herhaald;
|
||||
- `UnsafeUrlError`: URL faalt SSRF-validatie vóór netwerktoegang.
|
||||
|
||||
## Normalisatie
|
||||
|
||||
Implementatie: `apps/jobs/services/normalization.py`.
|
||||
|
||||
```python
|
||||
normalize_extracted_job(extracted: ExtractedJob) -> CanonicalJobDraft
|
||||
```
|
||||
|
||||
De functie:
|
||||
|
||||
- saneert HTML vóór opslag/rendering;
|
||||
- normaliseert titel, contracttypen, werkplek en taal;
|
||||
- canonicaliseert de URL en verwijdert gekende trackingparameters;
|
||||
- berekent een stabiele `canonical_key` en `content_hash`;
|
||||
- parseert datums conservatief en timezone-aware;
|
||||
- behoudt bronwaarden in `raw` en `evidence` voor uitlegbaarheid.
|
||||
|
||||
Een `CanonicalJobDraft` is een immutable overdrachtsobject. Het kent geen database-ID en bevat geen score.
|
||||
|
||||
## Persistente pipeline
|
||||
|
||||
Implementatie: `apps/jobs/services/pipeline.py`.
|
||||
|
||||
```python
|
||||
process_raw_document(document: RawDocument) -> dict[str, int | str | list[str]]
|
||||
```
|
||||
|
||||
Transactionele volgorde:
|
||||
|
||||
1. kies adapter via registry;
|
||||
2. extracteer jobs en parsermetadata;
|
||||
3. normaliseer iedere job;
|
||||
4. zoek een bestaand canoniek cluster;
|
||||
5. maak of werk werkgever, vacature en bronalias bij;
|
||||
6. leg veldherkomst en versie vast;
|
||||
7. score tegen ieder actief zoekprofiel.
|
||||
|
||||
De resultaatmapping bevat minimaal `extracted`, `created`, `updated`, `duplicates`, `parser` en `warnings`. De tellers moeten deterministisch blijven bij een replay van hetzelfde document.
|
||||
|
||||
## Deduplicatie
|
||||
|
||||
Implementatie: `apps/jobs/services/dedupe.py`.
|
||||
|
||||
```python
|
||||
find_existing_job(draft: CanonicalJobDraft) -> DedupeDecision
|
||||
```
|
||||
|
||||
`DedupeDecision` bevat:
|
||||
|
||||
- `job`: bestaand canoniek object of `None`;
|
||||
- `reason`: machineleesbare reden, bijvoorbeeld `exact_canonical_key`, `exact_external_id` of `fuzzy_*`;
|
||||
- `confidence`: getal tussen `0.0` en `1.0`.
|
||||
|
||||
Een fuzzy match mag alleen plaatsvinden wanneer werkgever, titel, locatie en periode voldoende bewijs leveren. Een rechtstreekse werkgeversbron krijgt canonieke voorkeur boven een platform- of recruiterlink; bronaliassen blijven behouden.
|
||||
|
||||
## Scoring
|
||||
|
||||
Implementatie: `apps/jobs/services/scoring.py`.
|
||||
|
||||
```python
|
||||
score_job(job: JobPosting, profile: SearchProfile) -> ScoreResult
|
||||
score_and_save(job: JobPosting, profile: SearchProfile) -> ScoreRun
|
||||
```
|
||||
|
||||
`ScoreResult` levert:
|
||||
|
||||
- `score`: begrensd op 0–100;
|
||||
- `hard_exclusions`: stabiele regelcodes;
|
||||
- `explanation`: korte positieve en negatieve factoren;
|
||||
- `feature_values`: auditbare, niet-persoonsgevoelige featurewaarden;
|
||||
- `model_version`: versie van regels/gewichten.
|
||||
|
||||
Harde uitsluitingen worden nooit door een hoge zachte score opgeheven. AI-output mag hoogstens aanvullende kenmerken leveren nadat schema- en confidencevalidatie zijn geslaagd.
|
||||
|
||||
## Feedback
|
||||
|
||||
Implementatie: `apps/jobs/services/feedback.py`.
|
||||
|
||||
Toegestane acties zijn `interesting`, `save`, `hide` en `applied`. Iedere mutatie is user-scoped en CSRF-beschermd. Feedback mag gewichten slechts binnen de in het profiel vastgelegde grenzen aanpassen. De gebruiker moet de wijziging kunnen resetten en een vacature handmatig terugvinden, ook wanneer ze is verborgen.
|
||||
|
||||
## Notificaties
|
||||
|
||||
Implementatie: `apps/notifications/services.py`.
|
||||
|
||||
Een digest wordt eerst als `DigestOutbox` aangemaakt en daarna verzonden. Uniciteit per profiel en digestdatum voorkomt dubbele mails. Verzending verandert alleen de outboxstatus; het opnieuw genereren van de selectie moet reproduceerbaar zijn uit opgeslagen `ScoreRun`-records.
|
||||
|
||||
## Nieuwe service toevoegen
|
||||
|
||||
Een nieuwe interne service is pas een stabiel contract wanneer:
|
||||
|
||||
- publieke types en foutcategorieën zijn gedocumenteerd;
|
||||
- unit- en integratietests succes, lege invoer en negatieve paden dekken;
|
||||
- netwerk- of secretvereisten injecteerbaar zijn;
|
||||
- er geen databasewrite in parser- of pure normalisatielogica zit;
|
||||
- traceability en backlog zijn bijgewerkt.
|
||||
@@ -0,0 +1,92 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: VacatureRadar HTTP-contract
|
||||
version: 0.1.0
|
||||
description: |
|
||||
Het geïmplementeerde publieke HTTP-contract van VacatureRadar 0.1.0.
|
||||
De hoofdinterface is server-rendered en vereist een Django-sessie. Alleen de
|
||||
health-endpoints zijn bedoeld voor anonieme infrastructuurchecks. Geplande JSON-API's
|
||||
worden pas aan dit document toegevoegd nadat code en contracttests bestaan.
|
||||
servers:
|
||||
- url: http://localhost:8080
|
||||
description: Lokale of via reverse proxy afgeschermde instantie
|
||||
paths:
|
||||
/health/live/:
|
||||
get:
|
||||
operationId: getLiveness
|
||||
summary: Controleer of het webproces HTTP-verkeer verwerkt
|
||||
security: []
|
||||
responses:
|
||||
"200":
|
||||
description: Proces is bereikbaar
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Liveness"
|
||||
examples:
|
||||
ok:
|
||||
value:
|
||||
ok: true
|
||||
service: vacatureradar
|
||||
/health/ready/:
|
||||
get:
|
||||
operationId: getReadiness
|
||||
summary: Controleer of de applicatie en database gebruiksklaar zijn
|
||||
security: []
|
||||
responses:
|
||||
"200":
|
||||
description: Alle verplichte readinesschecks slagen
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Readiness"
|
||||
examples:
|
||||
ready:
|
||||
value:
|
||||
ok: true
|
||||
checks:
|
||||
database: ok
|
||||
"503":
|
||||
description: Minstens één verplichte readinesscheck faalt
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Readiness"
|
||||
examples:
|
||||
databaseUnavailable:
|
||||
value:
|
||||
ok: false
|
||||
checks:
|
||||
database: error:OperationalError
|
||||
components:
|
||||
schemas:
|
||||
Liveness:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [ok, service]
|
||||
properties:
|
||||
ok:
|
||||
type: boolean
|
||||
const: true
|
||||
service:
|
||||
type: string
|
||||
const: vacatureradar
|
||||
Readiness:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [ok, checks]
|
||||
properties:
|
||||
ok:
|
||||
type: boolean
|
||||
checks:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
required: [database]
|
||||
securitySchemes:
|
||||
sessionCookie:
|
||||
type: apiKey
|
||||
in: cookie
|
||||
name: sessionid
|
||||
security:
|
||||
- sessionCookie: []
|
||||
@@ -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 |
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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,65–0,75 |
|
||||
| generieke HTMLselectors | 0,45–0,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-URL’s | 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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,64 @@
|
||||
# Toegankelijkheid
|
||||
|
||||
Doel: WCAG 2.2 AA voor de kernflows.
|
||||
|
||||
## Semantiek
|
||||
|
||||
- één `h1` per pagina, hiërarchische headings;
|
||||
- navigatie in `<nav>` met duidelijke accessible name;
|
||||
- lijsten/tabellen voor werkelijke lijst-/tabulaire data;
|
||||
- formulieren met expliciete `<label>`;
|
||||
- statusmetadata als definition list waar passend;
|
||||
- externe links herkenbaar in tekst/accessible label.
|
||||
|
||||
## Toetsenbord
|
||||
|
||||
Alle functies moeten zonder muis werken. Focusvolgorde volgt visuele volgorde. `:focus-visible` heeft een contrastvolle ring. Geen keyboard traps. Na server-POST landt focus logisch via heading/message; toekomstige modals herstellen focus.
|
||||
|
||||
## Kleur en contrast
|
||||
|
||||
- bodytekst minimaal 4,5:1;
|
||||
- grote tekst minimaal 3:1;
|
||||
- focus en componentgrenzen minimaal 3:1 waar vereist;
|
||||
- status nooit alleen via kleur;
|
||||
- light/dark afzonderlijk toetsen.
|
||||
|
||||
## Zoom en reflow
|
||||
|
||||
- bruikbaar op 320 CSS px breed;
|
||||
- 200% zoom zonder verlies van acties/tekst;
|
||||
- geen vaste hoogte voor tekstkaarten;
|
||||
- lange URL's en woorden breken;
|
||||
- horizontale scroll alleen voor intrinsiek brede data met alternatief.
|
||||
|
||||
## Forms en fouten
|
||||
|
||||
- foutsummary bovenaan bij meerdere fouten;
|
||||
- inline fout bij veld;
|
||||
- `aria-invalid` en `aria-describedby`;
|
||||
- voorbeeld en vereiste syntax als hint;
|
||||
- geen placeholder als enige label;
|
||||
- validatie niet alleen on blur.
|
||||
|
||||
## Dynamiek
|
||||
|
||||
- server-rendering heeft geen verborgen loading state;
|
||||
- async updates gebruiken live regions spaarzaam;
|
||||
- successmessages niet alleen tijdelijk visueel;
|
||||
- motion respecteert reduced motion;
|
||||
- tijdslimieten bestaan niet in gebruikersflows.
|
||||
|
||||
## Vacaturetekst
|
||||
|
||||
Gesanitized HTML behoudt headings, paragraphs, lists en links waar veilig. Verwijder scripts, forms, iframes, eventhandlers en gevaarlijke URL-schema's. Voeg waar nodig `rel="noopener noreferrer"` toe aan externe links.
|
||||
|
||||
## Testen
|
||||
|
||||
Per zichtbare taak:
|
||||
|
||||
1. keyboard-only;
|
||||
2. browser zoom/reflow;
|
||||
3. automatische HTML/accessibilityscan waar beschikbaar;
|
||||
4. screenreader smoke test voor dashboard, vacaturedetail en profiel;
|
||||
5. contrastcontrole;
|
||||
6. test met lange/lege/unknown content.
|
||||
@@ -0,0 +1,121 @@
|
||||
# Design system
|
||||
|
||||
## 1. Merkgevoel
|
||||
|
||||
Rustig, betrouwbaar, helder en licht technisch. De interface moet focus geven, niet gamificeren. Geen confetti, eindeloze badges of agressieve notificatiekleuren.
|
||||
|
||||
## 2. Visuele tokens
|
||||
|
||||
De implementatie staat in `static/css/app.css`. Gebruik semantische custom properties in plaats van losse kleuren in componenten.
|
||||
|
||||
Aanbevolen tokenrollen:
|
||||
|
||||
```css
|
||||
--surface-page
|
||||
--surface-card
|
||||
--surface-raised
|
||||
--text-primary
|
||||
--text-secondary
|
||||
--text-muted
|
||||
--border-subtle
|
||||
--accent
|
||||
--accent-strong
|
||||
--success
|
||||
--warning
|
||||
--danger
|
||||
--focus-ring
|
||||
--shadow-card
|
||||
--radius-sm
|
||||
--radius-md
|
||||
--radius-lg
|
||||
--space-1 ... --space-8
|
||||
```
|
||||
|
||||
Donkere modus volgt `prefers-color-scheme` en behoudt semantische contrastrollen.
|
||||
|
||||
## 3. Typografie
|
||||
|
||||
- systeemfontstack, geen externe fontrequest;
|
||||
- basis 16px;
|
||||
- body line-height ongeveer 1,55;
|
||||
- compacte metadata 0,8–0,9rem maar nooit onder bruikbare leesgrootte;
|
||||
- titels met duidelijke schaal, niet alleen gewicht;
|
||||
- cijfers voor scores mogen tabular nums gebruiken.
|
||||
|
||||
## 4. Spacing en layout
|
||||
|
||||
- 4/8px-gebaseerde schaal;
|
||||
- contentcontainer ongeveer 1180–1280px;
|
||||
- kaarten met consistente padding 20–24px desktop, 16px mobiel;
|
||||
- minimaal 44×44px touch targets voor primaire interactie;
|
||||
- grids breken onder 900px en 640px logisch af;
|
||||
- vaste/sticky elementen mogen content niet bedekken.
|
||||
|
||||
## 5. Componenten
|
||||
|
||||
### Button
|
||||
|
||||
Varianten:
|
||||
|
||||
- primary — één primaire actie per context;
|
||||
- secondary — normale navigatie/opslaan;
|
||||
- ghost — lage nadruk;
|
||||
- danger — destructief met bevestiging;
|
||||
- compact icon+label — vacaturefeedback.
|
||||
|
||||
States: hover, focus-visible, active, disabled, busy. Disabled blijft leesbaar en verklaarbaar.
|
||||
|
||||
### Badge/status pill
|
||||
|
||||
Bevat altijd tekst. Rollen: strong, possible, weak, hidden, active, review, deny, quarantined, expired. Gebruik niet alleen rood/groen.
|
||||
|
||||
### Card
|
||||
|
||||
Semantische header/body/footer. Hele kaart niet klikbaar wanneer er meerdere interne acties zijn; titel/link is de primaire link.
|
||||
|
||||
### Score
|
||||
|
||||
Score = getal/100 + recommendationtekst. Confidence wordt als “Datakwaliteit hoog/middel/laag” of exact percentage in detail getoond. Geen cirkeldiagram nodig voor één waarde.
|
||||
|
||||
### Form
|
||||
|
||||
- label boven veld;
|
||||
- hint vóór fout;
|
||||
- fout gekoppeld via `aria-describedby`;
|
||||
- required expliciet;
|
||||
- JSON-achtige lijstvelden uiteindelijk vervangen door chips/multi-select;
|
||||
- opslaan bevestigt, maar verliest scroll/focus niet onnodig.
|
||||
|
||||
### Alert
|
||||
|
||||
Info/success/warning/error met icoon, titel en tekst. Alerts zijn `role=status` of `role=alert` afhankelijk van urgentie.
|
||||
|
||||
### Table/list
|
||||
|
||||
Kolomkoppen semantisch; op mobiel cards of stacked definition list. Actiekolom niet te breed.
|
||||
|
||||
## 6. Motion
|
||||
|
||||
- alleen functionele transities 120–200ms;
|
||||
- respecteer `prefers-reduced-motion`;
|
||||
- geen automatische carrousels, pulserende scores of decoratieve parallax.
|
||||
|
||||
## 7. Iconen
|
||||
|
||||
Gebruik eenvoudige inline SVG's met `aria-hidden=true` naast zichtbare tekst. Geen externe iconfont. Een icon-only button vereist accessible label/tooltips.
|
||||
|
||||
## 8. Datavisualisatie
|
||||
|
||||
Gebruik balken voor scorecomponenten met numerieke tekst. Kleur ondersteunt, maar label/waarde draagt betekenis. Grafieken zijn niet nodig zolang data laag-volume is.
|
||||
|
||||
## 9. Designreviewchecklist
|
||||
|
||||
- primaire taak binnen één scherm duidelijk;
|
||||
- geen ruwe technische identifiers in dagelijkse UI;
|
||||
- empty/error/loading states aanwezig;
|
||||
- focusvolgorde logisch;
|
||||
- contrast en zoom 200% gecontroleerd;
|
||||
- mobiel 320px breed bruikbaar;
|
||||
- lange titels/werkgevers/URL's breken veilig;
|
||||
- Nederlands consistent;
|
||||
- scoreverklaring niet verstopt achter kleur.
|
||||
@@ -0,0 +1,165 @@
|
||||
# UX-specificatie
|
||||
|
||||
## 1. Ervaringsprincipe
|
||||
|
||||
VacatureRadar voelt niet als een zoekmachine maar als een rustige persoonlijke inbox. De gebruiker ziet eerst beslisbare informatie en pas daarna technische details. Bronbeheer en parserproblemen blijven beschikbaar, maar domineren de dagelijkse flow niet.
|
||||
|
||||
## 2. Informatiearchitectuur
|
||||
|
||||
Hoofdnavigatie:
|
||||
|
||||
1. **Vandaag** — beste matches en kernstatus;
|
||||
2. **Vacatures** — volledige doorzoekbare lijst;
|
||||
3. **Sollicitaties** — dossiers en opvolging;
|
||||
4. **Profiel** — voorkeuren en drempels;
|
||||
5. **Bronnen** — source health en retries;
|
||||
6. **Systeem** — health, versies en beheercontext.
|
||||
|
||||
Op mobiel wordt dit een compacte uitklapbare navigatie; primaire vacatureacties blijven binnen duimbereik.
|
||||
|
||||
## 3. Dashboard “Vandaag”
|
||||
|
||||
### Doel
|
||||
|
||||
Binnen 30 seconden weten of er iets relevants is.
|
||||
|
||||
### Volgorde
|
||||
|
||||
1. paginatitel + korte samenvatting;
|
||||
2. KPI's: nieuwe vandaag, actieve vacatures, open dossiers, ongezonde bronnen;
|
||||
3. kaarten, descending score;
|
||||
4. lege toestand met concrete uitleg;
|
||||
5. subtiele link naar alle vacatures/bronnen.
|
||||
|
||||
### Matchkaart
|
||||
|
||||
Boven de vouw:
|
||||
|
||||
- recommendationlabel;
|
||||
- score als `86/100` en confidence apart;
|
||||
- functietitel;
|
||||
- werkgever;
|
||||
- locatie + werkvorm;
|
||||
- maximaal twee kernpluspunten;
|
||||
- maximaal één belangrijkste concern;
|
||||
- acties: bekijken, interessant, bewaren, verbergen, gesolliciteerd.
|
||||
|
||||
Regels:
|
||||
|
||||
- geen score zonder label/uitleg;
|
||||
- onbekende data als “Niet vermeld” of “Nog niet berekend”;
|
||||
- hard uitgesloten jobs niet op Vandaag;
|
||||
- dubbele aliassen niet als aparte kaart.
|
||||
|
||||
## 4. Vacaturelijst
|
||||
|
||||
Filters:
|
||||
|
||||
- vrije tekst;
|
||||
- status;
|
||||
- recommendation/scoreband;
|
||||
- werkvorm;
|
||||
- regio;
|
||||
- werkgeverstype;
|
||||
- feedbackstatus.
|
||||
|
||||
Desktop gebruikt een compacte filterbalk; mobiel een disclosurepanel. Filters blijven in querystring zodat bookmarks en back navigation werken.
|
||||
|
||||
Tabel of kaarten kiezen op breedte. Geen horizontale scroll voor kernvelden.
|
||||
|
||||
## 5. Vacaturedetail
|
||||
|
||||
Secties:
|
||||
|
||||
1. titel, werkgever, status, canonical link;
|
||||
2. score-overzicht met componenten;
|
||||
3. “Waarom aanbevolen” en aandachtspunten;
|
||||
4. harde uitsluitingen indien via directe link geopend;
|
||||
5. vacaturetekst (gesanitized);
|
||||
6. voorwaarden/skills/locatie;
|
||||
7. bronnen en provenance;
|
||||
8. wijzigingshistorie;
|
||||
9. sollicitatiestatus/notities.
|
||||
|
||||
Bronbewijs wordt technisch maar scanbaar getoond: veld, methode, confidence, excerpt en bron.
|
||||
|
||||
## 6. Profielwizard/editor
|
||||
|
||||
De eerste onboarding wordt in maximaal vijf logische stappen opgezet:
|
||||
|
||||
1. rollen en ongewenste titels;
|
||||
2. skills en ongewenste verplichte skills;
|
||||
3. regio, afstand en werkvorm;
|
||||
4. contract en employerpreferences;
|
||||
5. digest en samenvatting.
|
||||
|
||||
De bestaande editor mag één pagina blijven voor MVP, maar groepeert velden visueel en geeft voorbeeldsyntax. JSON mag in de interne modelvorm bestaan; de eindgebruiker ziet uiteindelijk chips/checkboxes, geen ruwe JSON.
|
||||
|
||||
Opslaan toont:
|
||||
|
||||
- welke harde regels actief zijn;
|
||||
- scoregewichten in begrijpelijke labels;
|
||||
- profielversie;
|
||||
- mogelijkheid om learning uit/reset te zetten.
|
||||
|
||||
## 7. Bronnen
|
||||
|
||||
Bronkaart/-rij toont:
|
||||
|
||||
- naam en domein;
|
||||
- type;
|
||||
- policy en operationele status apart;
|
||||
- laatste succes/fout;
|
||||
- volgende run;
|
||||
- parser/confidence;
|
||||
- laatste tellingen;
|
||||
- retryknop indien policy niet deny.
|
||||
|
||||
Kleur is nooit de enige indicator. `deny`, `review`, `quarantined` hebben tekst en icoon/patroon.
|
||||
|
||||
## 8. Sollicitaties
|
||||
|
||||
Kanban is niet noodzakelijk. Een duidelijke lijst met status, werkgever, functie, applied date, follow-up date en eerstvolgende actie is beter voor laag volume.
|
||||
|
||||
Dossierdetail/editor bevat:
|
||||
|
||||
- immutable vacaturesnapshot;
|
||||
- huidige bronlink/status;
|
||||
- contact;
|
||||
- notities;
|
||||
- documentenmanifest;
|
||||
- tijdlijn van statuswijzigingen (backlog);
|
||||
- opvolgdatum.
|
||||
|
||||
## 9. Meldingen
|
||||
|
||||
Digestcopy:
|
||||
|
||||
```text
|
||||
2 sterke matches
|
||||
4 mogelijke matches
|
||||
11 automatisch uitgesloten
|
||||
1 opgeslagen vacature sluit binnenkort
|
||||
```
|
||||
|
||||
Elke vacature heeft titel, werkgever, locatie, score, twee redenen, één aandachtspunt en originele link. Geen volledige vacaturetekst in mail.
|
||||
|
||||
## 10. States
|
||||
|
||||
Iedere pagina specificeert:
|
||||
|
||||
- loading: alleen nodig bij async enhancement; serverrender toont normale response;
|
||||
- empty: leg uit waarom en geef één relevante actie;
|
||||
- error: concrete fout + veilige retry, geen stacktrace;
|
||||
- partial: bron/resultaten beschikbaar met warning;
|
||||
- stale: toon laatste succesvolle tijd en dat vernieuwing faalde;
|
||||
- unauthorized: login en geen informatielek.
|
||||
|
||||
## 11. Copyrichtlijnen
|
||||
|
||||
- Nederlands, direct en niet-oordelend;
|
||||
- “Mogelijke match” in plaats van “Je bent geschikt”;
|
||||
- “Niet teruggevonden” in plaats van “Je mist”;
|
||||
- “Rechtstreekse werkgeversbron” in plaats van absolute garantie;
|
||||
- percentages alleen wanneer betekenis duidelijk is;
|
||||
- technische bronfout in beheer, eenvoudige impacttekst op dashboard.
|
||||
@@ -0,0 +1,113 @@
|
||||
# Low-fidelity wireframes
|
||||
|
||||
## Dashboard — desktop
|
||||
|
||||
```text
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ VacatureRadar Vandaag Vacatures Sollicitaties Profiel Bronnen │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ Vandaag Laatste update 07:31│
|
||||
│ Alleen vacatures die aandacht verdienen. │
|
||||
│ │
|
||||
│ [ 3 nieuwe ] [ 128 actief ] [ 2 open dossiers ] [ 1 bron vraagt aandacht ] │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ STERKE MATCH 88/100 Infrastructure Engineer │ │
|
||||
│ │ Example IT · Hasselt · Hybride · Voltijds │ │
|
||||
│ │ + Rechtstreekse werkgeversbron │ │
|
||||
│ │ + Microsoft 365 en VMware teruggevonden │ │
|
||||
│ │ ! Salaris niet vermeld │ │
|
||||
│ │ [Bekijken] [Interessant] [Bewaren] [Verbergen] [Gesolliciteerd] │ │
|
||||
│ └──────────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ MOGELIJKE MATCH 72/100 ... │ │
|
||||
│ └──────────────────────────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Dashboard — mobiel
|
||||
|
||||
```text
|
||||
┌───────────────────────────┐
|
||||
│ VacatureRadar ☰ │
|
||||
├───────────────────────────┤
|
||||
│ Vandaag │
|
||||
│ 3 nieuwe · 2 sterke │
|
||||
│ │
|
||||
│ ┌───────────────────────┐ │
|
||||
│ │ STERKE MATCH │ │
|
||||
│ │ 88/100 │ │
|
||||
│ │ Infrastructure │ │
|
||||
│ │ Engineer │ │
|
||||
│ │ Example IT │ │
|
||||
│ │ Hasselt · Hybride │ │
|
||||
│ │ + M365 en VMware │ │
|
||||
│ │ ! Salaris onbekend │ │
|
||||
│ │ [Bekijken] │ │
|
||||
│ │ [Interessant] [Meer] │ │
|
||||
│ └───────────────────────┘ │
|
||||
└───────────────────────────┘
|
||||
```
|
||||
|
||||
## Vacaturedetail
|
||||
|
||||
```text
|
||||
Titel + werkgever + status [Open originele vacature]
|
||||
Locatie · contract · werkvorm · data
|
||||
|
||||
Score 88/100 Datakwaliteit 84%
|
||||
[Content ████████] [Skills ███████] [Locatie ████████] ...
|
||||
|
||||
Waarom dit past Aandachtspunten
|
||||
- titel sluit goed aan - salaris niet vermeld
|
||||
- gewenste skills gevonden - 3 jaar ervaring gevraagd
|
||||
- directe werkgever
|
||||
|
||||
[Interessant] [Bewaren] [Verbergen] [Gesolliciteerd]
|
||||
|
||||
Vacaturetekst
|
||||
...
|
||||
|
||||
Bronnen en bewijs
|
||||
- jobs.example.org · JSON-LD · 95%
|
||||
- vacaturemail · alias · 65%
|
||||
|
||||
Wijzigingshistorie | Sollicitatiedossier
|
||||
```
|
||||
|
||||
## Profiel
|
||||
|
||||
```text
|
||||
Zoekprofiel: Primair versie 4 · actief
|
||||
|
||||
Gewenste rollen Uitgesloten titels
|
||||
[Infrastructure Engineer] [Sales]
|
||||
[Systeembeheerder] [Recruiter]
|
||||
|
||||
Skills Uitgesloten verplichte skills
|
||||
[Microsoft 365] [VMware] [Apple-only]
|
||||
|
||||
Locatie
|
||||
Postcode [....] Gemeente [....] Max afstand [45 km]
|
||||
Voorkeursregio's [Limburg] [Vlaams-Brabant]
|
||||
Werkvorm [x] Hybride [x] Op locatie [ ] Remote
|
||||
|
||||
Contract [x] Voltijds [x] Vast
|
||||
|
||||
Digest [07:30] Leren van feedback [aan]
|
||||
|
||||
Harde regels (samenvatting) | Scoregewichten
|
||||
[Opslaan]
|
||||
```
|
||||
|
||||
## Bronstatus
|
||||
|
||||
```text
|
||||
Naam/domain | Type | Policy | Status | Laatste succes | Volgende run | Resultaat | Actie
|
||||
Example IT | ATS | allow | active | 06:42 | 18:42 | 12 jobs | Retry
|
||||
Platform X | mail | allow | active | 07:01 | 07:11 | 4 links | —
|
||||
Unknown | web | review | candidate| — | — | — | Review
|
||||
```
|
||||
|
||||
Een visueel voorbeeld staat in `dashboard-wireframe.svg`.
|
||||
@@ -0,0 +1,43 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="900" viewBox="0 0 1440 900" role="img" aria-labelledby="title desc">
|
||||
<title id="title">VacatureRadar dashboard wireframe</title>
|
||||
<desc id="desc">Licht dashboard met navigatie, statistieken en twee vacaturekaarten.</desc>
|
||||
<rect width="1440" height="900" fill="#f4f6f8"/>
|
||||
<rect x="0" y="0" width="1440" height="84" fill="#ffffff"/>
|
||||
<text x="72" y="52" font-family="system-ui, sans-serif" font-size="27" font-weight="700" fill="#17202a">VacatureRadar</text>
|
||||
<g font-family="system-ui, sans-serif" font-size="16" fill="#52606d">
|
||||
<text x="700" y="50">Vandaag</text><text x="790" y="50">Vacatures</text><text x="895" y="50">Sollicitaties</text><text x="1010" y="50">Profiel</text><text x="1085" y="50">Bronnen</text><text x="1175" y="50">Systeem</text>
|
||||
</g>
|
||||
<rect x="695" y="62" width="67" height="3" rx="1.5" fill="#2b6cb0"/>
|
||||
<text x="100" y="150" font-family="system-ui, sans-serif" font-size="38" font-weight="750" fill="#17202a">Vandaag</text>
|
||||
<text x="100" y="184" font-family="system-ui, sans-serif" font-size="18" fill="#66788a">Alleen vacatures die aandacht verdienen.</text>
|
||||
<text x="1160" y="160" font-family="system-ui, sans-serif" font-size="14" fill="#7b8794">Laatste update 07:31</text>
|
||||
<g font-family="system-ui, sans-serif">
|
||||
<g transform="translate(100 225)"><rect width="275" height="100" rx="14" fill="#fff" stroke="#dde3e9"/><text x="22" y="38" font-size="14" fill="#6b7785">NIEUW VANDAAG</text><text x="22" y="75" font-size="30" font-weight="700" fill="#17202a">3</text></g>
|
||||
<g transform="translate(395 225)"><rect width="275" height="100" rx="14" fill="#fff" stroke="#dde3e9"/><text x="22" y="38" font-size="14" fill="#6b7785">ACTIEVE VACATURES</text><text x="22" y="75" font-size="30" font-weight="700" fill="#17202a">128</text></g>
|
||||
<g transform="translate(690 225)"><rect width="275" height="100" rx="14" fill="#fff" stroke="#dde3e9"/><text x="22" y="38" font-size="14" fill="#6b7785">OPEN DOSSIERS</text><text x="22" y="75" font-size="30" font-weight="700" fill="#17202a">2</text></g>
|
||||
<g transform="translate(985 225)"><rect width="275" height="100" rx="14" fill="#fff" stroke="#dde3e9"/><text x="22" y="38" font-size="14" fill="#6b7785">BRONNEN MET AANDACHT</text><text x="22" y="75" font-size="30" font-weight="700" fill="#9c4221">1</text></g>
|
||||
</g>
|
||||
<g transform="translate(100 365)" font-family="system-ui, sans-serif">
|
||||
<rect width="1160" height="230" rx="18" fill="#ffffff" stroke="#d9e1e8"/>
|
||||
<rect x="24" y="24" width="128" height="30" rx="15" fill="#e8f5ec"/>
|
||||
<text x="40" y="44" font-size="13" font-weight="700" fill="#276749">STERKE MATCH</text>
|
||||
<text x="1020" y="50" font-size="28" font-weight="750" fill="#17202a">88/100</text>
|
||||
<text x="24" y="92" font-size="27" font-weight="750" fill="#17202a">Infrastructure Engineer</text>
|
||||
<text x="24" y="122" font-size="16" fill="#5e6c7a">Example IT · Hasselt · Hybride · Voltijds</text>
|
||||
<text x="24" y="158" font-size="15" fill="#2f855a">+ Rechtstreekse werkgeversbron</text>
|
||||
<text x="24" y="184" font-size="15" fill="#2f855a">+ Microsoft 365 en VMware teruggevonden</text>
|
||||
<text x="430" y="158" font-size="15" fill="#9c6b1b">! Salaris of barema niet vermeld</text>
|
||||
<rect x="800" y="170" width="120" height="38" rx="8" fill="#2b6cb0"/><text x="825" y="195" font-size="15" font-weight="650" fill="#fff">Bekijken</text>
|
||||
<rect x="934" y="170" width="100" height="38" rx="8" fill="#edf2f7"/><text x="950" y="195" font-size="15" fill="#2d3748">Bewaren</text>
|
||||
<rect x="1048" y="170" width="88" height="38" rx="8" fill="#edf2f7"/><text x="1064" y="195" font-size="15" fill="#2d3748">Verberg</text>
|
||||
</g>
|
||||
<g transform="translate(100 625)" font-family="system-ui, sans-serif">
|
||||
<rect width="1160" height="180" rx="18" fill="#ffffff" stroke="#d9e1e8"/>
|
||||
<rect x="24" y="24" width="145" height="30" rx="15" fill="#fff5df"/>
|
||||
<text x="40" y="44" font-size="13" font-weight="700" fill="#8a5b12">MOGELIJKE MATCH</text>
|
||||
<text x="1020" y="50" font-size="28" font-weight="750" fill="#17202a">72/100</text>
|
||||
<text x="24" y="92" font-size="25" font-weight="750" fill="#17202a">ICT Systeembeheerder</text>
|
||||
<text x="24" y="122" font-size="16" fill="#5e6c7a">Publieke organisatie · Genk · Op locatie</text>
|
||||
<text x="24" y="153" font-size="15" fill="#2f855a">+ Publieke sector en passende regio</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
@@ -0,0 +1,83 @@
|
||||
# Back-up en herstel
|
||||
|
||||
## Wat moet worden beschermd
|
||||
|
||||
Prioriteit:
|
||||
|
||||
1. PostgreSQL-database of lokale SQLite-database;
|
||||
2. productie-`.env` en reverse-proxyconfiguratie, versleuteld en apart;
|
||||
3. `media/` met gebruikersdocumenten/snapshots wanneer die functie actief is;
|
||||
4. `config-data/` met niet-geheime bron- en profielconfiguratie;
|
||||
5. immutable image/tag en de bijbehorende repositoryrelease.
|
||||
|
||||
Redis is een werkqueue/cache en hoeft normaal niet te worden hersteld. Ollama-modellen kunnen opnieuw worden gedownload; behoud alleen wanneer bandbreedte of modelbeschikbaarheid dat vereist.
|
||||
|
||||
## Minimumniveau
|
||||
|
||||
- database dagelijks;
|
||||
- configuratie na iedere wijziging;
|
||||
- minstens 7 dagelijkse en 4 wekelijkse herstelpunten;
|
||||
- back-up op een ander fysiek of logisch opslagdoel;
|
||||
- checksums op ieder archief;
|
||||
- minstens per kwartaal een volledige hersteltest;
|
||||
- gevoelige back-ups versleuteld at rest en tijdens transport.
|
||||
|
||||
## Repositoryscripts
|
||||
|
||||
```bash
|
||||
./scripts/backup.sh
|
||||
```
|
||||
|
||||
Het script gebruikt `pg_dump` wanneer `DATABASE_URL` PostgreSQL is en anders een kopie van `local/db.sqlite3`. Het maakt daarnaast een bestandsarchief en SHA-256-lijst. Voor productie moet de uitvoerdirectory naar een beschermd extern volume worden gesynchroniseerd; een back-up naast de live database is geen volwaardige back-up.
|
||||
|
||||
Herstel één databasebestand:
|
||||
|
||||
```bash
|
||||
./scripts/restore.sh /pad/naar/vacatureradar-YYYYMMDDTHHMMSSZ.sql.gz
|
||||
# of
|
||||
./scripts/restore.sh /pad/naar/vacatureradar-YYYYMMDDTHHMMSSZ.sqlite3
|
||||
```
|
||||
|
||||
## PostgreSQL-consistente back-up
|
||||
|
||||
Aanbevolen vanuit een beheercontainer of host met clienttools:
|
||||
|
||||
```bash
|
||||
pg_dump --format=custom --no-owner --no-acl "$DATABASE_URL" \
|
||||
> vacatureradar-$(date -u +%Y%m%dT%H%M%SZ).dump
|
||||
sha256sum vacatureradar-*.dump > SHA256SUMS
|
||||
```
|
||||
|
||||
De huidige scriptvariant gebruikt gecomprimeerde SQL voor brede compatibiliteit. Een custom-formatdump maakt selectiever herstel mogelijk; kies één formaat en test exact dat formaat.
|
||||
|
||||
## Veilige herstelprocedure
|
||||
|
||||
1. Meld onderhoud en blokkeer externe toegang.
|
||||
2. Stop scheduler en worker; stop daarna web zodat geen writes meer gebeuren.
|
||||
3. Maak een forensische kopie van de defecte huidige database/volumes.
|
||||
4. Verifieer checksum, datum, appversie en migratieniveau van de back-up.
|
||||
5. Herstel naar een lege database of aparte testinstantie.
|
||||
6. Gebruik de code/image die bij het back-upmoment hoort.
|
||||
7. Voer migraties alleen vooruit uit nadat de restore op dat oude niveau goed opent.
|
||||
8. Draai health, modelcounts, fixture-import en applicatiesmoke-test.
|
||||
9. Start web, daarna worker en precies één scheduler.
|
||||
10. Documenteer RPO/RTO, verloren wijzigingen en vervolgactie.
|
||||
|
||||
## Hersteltest
|
||||
|
||||
Een hersteltest is pas geslaagd wanneer:
|
||||
|
||||
- checksum is geverifieerd;
|
||||
- database start zonder reparaties;
|
||||
- migratiestatus verklaarbaar is;
|
||||
- aantallen gebruikers, profielen, jobs, bronaliassen, sollicitaties en outboxrecords plausibel zijn;
|
||||
- een bestaande vacaturedetailpagina opent;
|
||||
- een fixture idempotent importeert;
|
||||
- een score en digest kunnen worden aangemaakt;
|
||||
- geen live mailbox of bron onbedoeld wordt gepolld in de testomgeving.
|
||||
|
||||
Gebruik in een hersteltest `IMAP_ENABLED=0`, `CELERY_TASK_ALWAYS_EAGER=1`, consolemail en geen live source polling.
|
||||
|
||||
## Verwijdering en privacy
|
||||
|
||||
Back-ups verlengen feitelijk de bewaartermijn. Documenteer hoe verwijderde persoonsgegevens na de normale rotatie ook uit back-ups verdwijnen. Gebruik korte retentie voor ruwe vacaturemails en bewaar geen mailboxcredentials in databasedumps.
|
||||
@@ -0,0 +1,36 @@
|
||||
# Geodata import voor Belgische locaties
|
||||
|
||||
Deze module gebruikt lokaal beheerde postcode/gemeentedata om locatiecoördinaten te kunnen afleiden.
|
||||
|
||||
## CLI
|
||||
|
||||
```bash
|
||||
python manage.py import_geodata <pad-naar-geodata.csv> \
|
||||
--source-name local \
|
||||
--dataset-version 2026-01-01 \
|
||||
--license-name "Naam van licentie"
|
||||
--license-url "https://..."
|
||||
--replace
|
||||
```
|
||||
|
||||
- Gebruik `--validate-only` om validatie uit te voeren zonder databasewijzigingen.
|
||||
- Gebruik `--replace` wanneer een bron volledig opnieuw opgebouwd moet worden.
|
||||
|
||||
## Verplichte CSV-kolommen
|
||||
|
||||
- `postal_code` (exact 4 cijfers)
|
||||
- `municipality`
|
||||
- `region`
|
||||
- `latitude`
|
||||
- `longitude`
|
||||
|
||||
## Validatie
|
||||
|
||||
- Niet-numerieke coördinaten worden geweigerd.
|
||||
- Latitude en longitude moeten binnen geldige range vallen.
|
||||
- Dubbels op `postal_code + municipality` in de invoer worden afgekeurd.
|
||||
- Bestaande lookupregels van dezelfde `source_name`/`dataset-version` blokkeren import zonder `--replace`.
|
||||
|
||||
## Fallback
|
||||
|
||||
Zonder geïmporteerde geodata blijft afstandsafleiding op "onbekend".
|
||||
@@ -0,0 +1,40 @@
|
||||
# Gitea push-to-deploy (self-hosted, geen handmatige variabelenvulling)
|
||||
|
||||
Push naar `main` of `master` triggert automatisch:
|
||||
|
||||
- checkout op de server runner
|
||||
- `bash scripts/deploy_docker.sh`
|
||||
- deploy met `docker compose up -d --build`
|
||||
|
||||
Deze workflow gebruikt defaults en hoeft niet te wachten op repo-secrets voor host/ssh/credentials.
|
||||
|
||||
## Vereiste eenmalige setup op de server
|
||||
|
||||
1. Start en registreer een self-hosted Gitea-runner voor deze repo.
|
||||
2. Zorg dat die runner Docker/Compose kan draaien in de projectmap waar de runner draait.
|
||||
3. Laat éénmaal dit script draaien op de server:
|
||||
|
||||
```bash
|
||||
bash scripts/deploy_docker.sh
|
||||
```
|
||||
|
||||
Daarna is het gedrag volledig push-only:
|
||||
|
||||
- `git push` naar `main` of `master` → automatische deploy
|
||||
|
||||
## Defaults
|
||||
|
||||
- `APP_HOST`: hostname van de server (fallback: `hostname`)
|
||||
- `APP_SCHEME`: `http`
|
||||
- `DJANGO_DEBUG`: afgeleid uit `APP_SCHEME`
|
||||
- `0` als `APP_SCHEME=https`
|
||||
- `1` als `APP_SCHEME=http`
|
||||
- `DEPLOY_COMPOSE_FILE`: `docker-compose.yml`
|
||||
- `APP_ORIGINS`, `APP_HOSTS`, `APP_PORT`: optioneel en veilig overnemen als op runner gezet
|
||||
|
||||
## Endpoint na deploy
|
||||
|
||||
- `http://<runner-host>:1226/health/ready/`
|
||||
- `http://<runner-host>:1226/health/live/`
|
||||
|
||||
Voor productie: zet later op de runner de gewenste runtime-omgeving (bijv. reverse proxy + TLS) en herstart push.
|
||||
@@ -0,0 +1,101 @@
|
||||
# Lokale ontwikkeling
|
||||
|
||||
## Ondersteunde omgeving
|
||||
|
||||
- Linux, macOS of WSL2;
|
||||
- Python 3.12 of 3.13;
|
||||
- `uv` voor dependency- en lockfilebeheer;
|
||||
- optioneel Docker Compose voor PostgreSQL, Redis en de volledige stack.
|
||||
|
||||
Tests gebruiken standaard SQLite, eager Celery en een in-memory e-mailbackend. Ze hebben geen internet, Redis, mailbox of Ollama nodig.
|
||||
|
||||
## Eerste start
|
||||
|
||||
```bash
|
||||
./scripts/codex_bootstrap.sh
|
||||
uv run python manage.py runserver 0.0.0.0:8080
|
||||
```
|
||||
|
||||
Het script maakt alleen voor lokale ontwikkeling een `.env`, installeert de gelockte dependencies, migreert, laadt veilige demodata en draait alle kwaliteitsgates. De lokaal aangemaakte login is `admin / codex-local-only`. Verwijder of wijzig deze credentials vóór ieder gedeeld of bereikbaar gebruik.
|
||||
|
||||
## Handmatige start
|
||||
|
||||
```bash
|
||||
uv sync --all-groups
|
||||
mkdir -p local media logs backups
|
||||
cp .env.example .env
|
||||
# Laat DATABASE_URL leeg voor SQLite en vul een unieke DJANGO_SECRET_KEY in.
|
||||
uv run python manage.py migrate
|
||||
uv run python manage.py bootstrap_instance --with-demo
|
||||
uv run python manage.py runserver 0.0.0.0:8080
|
||||
```
|
||||
|
||||
## Veelgebruikte commando's
|
||||
|
||||
```bash
|
||||
make help
|
||||
make test
|
||||
make lint
|
||||
make format
|
||||
make verify
|
||||
uv run python manage.py shell
|
||||
uv run python manage.py seed_sources
|
||||
uv run python manage.py import_job_fixture fixtures/pages/sample_jsonld_job.html
|
||||
```
|
||||
|
||||
De volgende achtergrondprocessen zijn alleen nodig wanneer `CELERY_TASK_ALWAYS_EAGER=0`:
|
||||
|
||||
```bash
|
||||
uv run celery -A config worker -l INFO -Q high,default,low
|
||||
uv run celery -A config beat -l INFO
|
||||
```
|
||||
|
||||
## Demo opnieuw opbouwen
|
||||
|
||||
```bash
|
||||
./scripts/reset_demo.sh
|
||||
```
|
||||
|
||||
Dit verwijdert uitsluitend `local/db.sqlite3`. Gebruik het niet tegen een productievolume.
|
||||
|
||||
## Testen per laag
|
||||
|
||||
```bash
|
||||
uv run pytest tests/unit
|
||||
uv run pytest tests/integration
|
||||
uv run pytest tests/security
|
||||
uv run pytest tests/integration/test_pipeline.py -q
|
||||
uv run pytest --cov=apps --cov=config --cov-report=term-missing
|
||||
```
|
||||
|
||||
Voeg bij iedere parserwijziging een bronfixture toe. Tests mogen alleen een echte netwerkcall doen in een expliciet afgescheiden, handmatig gestarte smoke-test die niet in CI draait.
|
||||
|
||||
## Databasewijzigingen
|
||||
|
||||
```bash
|
||||
uv run python manage.py makemigrations
|
||||
uv run python manage.py migrate
|
||||
uv run python manage.py makemigrations --check --dry-run
|
||||
```
|
||||
|
||||
Controleer de gegenereerde migratie. Voor destructieve wijzigingen is een ADR, datamigratie, herstelpad en back-uptest verplicht.
|
||||
|
||||
## Static files
|
||||
|
||||
In development serveert Django static files. Productie gebruikt `collectstatic` in de container en WhiteNoise:
|
||||
|
||||
```bash
|
||||
uv run python manage.py collectstatic --noinput
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**`uv` ontbreekt:** installeer `uv` volgens de officiële instructies van de gekozen werkmachine en heropen de shell.
|
||||
|
||||
**Database lock op SQLite:** stop tweede web/workerprocessen of gebruik lokaal PostgreSQL voor parallel werk.
|
||||
|
||||
**Redis connection refused:** zet voor eenvoudige lokale runs `CELERY_TASK_ALWAYS_EAGER=1`, of start Redis/Compose.
|
||||
|
||||
**Geen digest zichtbaar:** met console-backend staat de mail in de terminal; controleer daarnaast `DigestOutbox` in Django admin.
|
||||
|
||||
**Bron wordt overgeslagen:** controleer `policy=allow`, een toegestane status, `next_run_at`, de domeinpolicy en de platformdenylist. Forceer nooit een fetch om een veiligheidsblokkade te omzeilen.
|
||||
@@ -0,0 +1,118 @@
|
||||
# Operationeel runbook
|
||||
|
||||
## Dagelijkse controle
|
||||
|
||||
Controleer bij voorkeur automatisch en minstens wekelijks handmatig:
|
||||
|
||||
- `/health/ready/` is 200;
|
||||
- web, worker, scheduler, PostgreSQL en Redis zijn actief;
|
||||
- vrije ruimte op database-, media-, log- en back-upvolume;
|
||||
- bronnen in `quarantined`, `paused` of met oplopende `failure_count`;
|
||||
- `DigestOutbox` met status `failed` of langdurig `pending`;
|
||||
- meest recente geslaagde back-up en laatste hersteltest;
|
||||
- onverwachte login- of policyfouten in logs.
|
||||
|
||||
## Incidentprioriteiten
|
||||
|
||||
| Niveau | Voorbeeld | Eerste actie |
|
||||
|---|---|---|
|
||||
| P1 | datalek, secret gepubliceerd, ongeautoriseerde netwerkfetch | stop web/worker/scheduler, isoleer netwerk, roteer secrets |
|
||||
| P2 | database niet beschikbaar, imports corrupt, herhaalde mail | stop schrijvende processen, behoud bewijs, maak snapshot |
|
||||
| P3 | één adapter kapot, digest vertraagd, bron in quarantaine | pauzeer bron/functie, herstel met fixture en test |
|
||||
| P4 | copy/layout/probleem zonder datarisico | registreer backlogtaak en plan normale release |
|
||||
|
||||
## Web niet ready
|
||||
|
||||
1. Controleer liveness. Faalt die ook, inspecteer webcontainer/proces en poortbinding.
|
||||
2. Bij `database=error:*`: controleer PostgreSQL-status, disk, credentials en netwerknaam.
|
||||
3. Test vanuit webcontainer of DNS `postgres` resolveert en poort 5432 bereikbaar is.
|
||||
4. Draai geen migraties herhaald blind; lees eerst de laatste stacktrace.
|
||||
5. Herstart alleen de falende laag. Een volledige stackrestart kan diagnostiek wissen.
|
||||
|
||||
## Worker verwerkt niets
|
||||
|
||||
1. Controleer Redis en `CELERY_BROKER_URL`.
|
||||
2. Controleer workerqueues `high,default,low`.
|
||||
3. Inspecteer actieve en gereserveerde taken:
|
||||
|
||||
```bash
|
||||
celery -A config inspect ping
|
||||
celery -A config inspect active
|
||||
celery -A config inspect reserved
|
||||
```
|
||||
|
||||
4. Kijk naar timeouts, policyblokkades en retryloops.
|
||||
5. Start niet meerdere schedulers; dat kan dubbele planning veroorzaken, ook al zijn imports idempotent.
|
||||
|
||||
## Bron faalt of gaat in quarantaine
|
||||
|
||||
1. Laat quarantaine staan; forceer geen fetch.
|
||||
2. Lees alleen categorie, status, URL-host en beperkte foutmelding in `SourceRun`.
|
||||
3. Controleer of het domein, redirectdoel, robots-/voorwaardenstatus of contenttype veranderde.
|
||||
4. Download geen pagina met een onbeveiligde shellopdracht vanaf de productiehost.
|
||||
5. Reproduceer met een gesaneerde fixture in een ontwikkelomgeving.
|
||||
6. Werk parser/policy en securitytests bij.
|
||||
7. Zet bron pas terug op `trial`, voer één begrensde run uit en promoveer daarna eventueel naar `active`.
|
||||
|
||||
## IMAP-import stopt
|
||||
|
||||
1. Zet `IMAP_ENABLED=0` om lockout of herhaalde fouten te beperken.
|
||||
2. Controleer host, poort, TLS, mailboxnaam en app-password.
|
||||
3. Verifieer dat de mailbox niet is hernoemd en het account niet is vergrendeld.
|
||||
4. Test eerst read-only met `IMAP_MARK_SEEN=0`.
|
||||
5. Een reeds verwerkte mail is idempotent via message identity; verwijder geen `EmailMessageRecord` om opnieuw te proberen zonder analyse.
|
||||
|
||||
## Digest ontbreekt of dubbel
|
||||
|
||||
- Controleer actief profiel, digesttijd/zone en `DIGEST_RECIPIENT`.
|
||||
- Controleer `DigestOutbox` op datum en status.
|
||||
- Bij SMTP-fout: corrigeer configuratie en retry dezelfde outbox gecontroleerd.
|
||||
- Maak geen tweede outboxrecord handmatig; uniciteit per profiel/datum is de duplicaatbarrière.
|
||||
- Een lege digest is toegestaan wanneer geen vacature boven de ingestelde drempel valt.
|
||||
|
||||
## Verdachte of kwaadaardige vacature-inhoud
|
||||
|
||||
1. Open de originele bron niet automatisch in een ingelogde browser.
|
||||
2. Zet bij twijfel het raw document op `quarantined` zodat retentie het bewijs niet wist.
|
||||
3. Controleer opgeslagen gesaniteerde tekst en veldherkomst.
|
||||
4. Voeg de payload als geanonimiseerde fixture toe wanneer een parser/sanitizerfout bestaat.
|
||||
5. Laat AI-output nooit een commando, URL-fetch of configuratiemutatie veroorzaken.
|
||||
|
||||
## Sollicitatiedossier exporteren en opschonen
|
||||
|
||||
1. Open de sollicitatielijst in een ingelogde gebruiker-sessie en ga naar het dossier.
|
||||
2. Controleer dat de timeline alleen mutaties voor dat dossier bevat (status, notities, contact, snapshot).
|
||||
3. Exporteer met het dossier-exportendpoint om een zip met `application.json`, `timeline.csv`, `application_print.html` te controleren.
|
||||
4. Controleer het zipbestand lokaal op:
|
||||
- correcte dossier-ID;
|
||||
- geen raw e-mailbody of bron-payload;
|
||||
- geen externe assets of scriptrequests in HTML.
|
||||
5. Verwijderen mag alleen op gebruikersniveau (`delete` endpoint); herhaalde delete-aanroepen moeten zonder fout uitkomen.
|
||||
|
||||
## Schijfruimte laag
|
||||
|
||||
1. Stop scheduler en worker wanneer de database- of PostgreSQL-volume kritiek vol is.
|
||||
2. Controleer oude back-ups, containerlogs, raw-documentretentie en Ollama-modellen.
|
||||
3. Verwijder nooit willekeurig PostgreSQL-bestanden.
|
||||
4. Laat `cleanup_raw_documents` alleen niet-gequarantaineerde verlopen documenten opruimen.
|
||||
5. Vergroot volume of herstel rotatie, start daarna database, web, worker en scheduler in die volgorde.
|
||||
|
||||
## Secret vermoedelijk gelekt
|
||||
|
||||
1. Stop externe toegang en disable betrokken account/token.
|
||||
2. Roteer Django secret, adminwachtwoord, database-, SMTP-, IMAP- en registrycredentials voor zover betrokken.
|
||||
3. Trek oude sessies in door sessies te verwijderen of het secret te wijzigen.
|
||||
4. Controleer logs en source-runs op misbruik, zonder gevoelige payload verder te kopiëren.
|
||||
5. Maak een incidentnotitie met exacte tijden, scope, rotaties en herstelvalidatie.
|
||||
|
||||
## Herstelvalidatie
|
||||
|
||||
Na ieder incident:
|
||||
|
||||
```bash
|
||||
python manage.py check
|
||||
python manage.py migrate --plan
|
||||
python manage.py shell -c "from apps.core.health import readiness; print(readiness())"
|
||||
```
|
||||
|
||||
Controleer daarna login, één fixture-import, scoreberekening, bronlijst, sollicitatielijst en mailbackend. Start live polling pas nadat deze controles slagen.
|
||||
@@ -0,0 +1,146 @@
|
||||
# Bron onboarden
|
||||
|
||||
Een nieuwe bron wordt nooit direct als onbeperkt actief beschouwd. Gebruik het traject **candidate → trial → active** en ga bij veiligheids- of kwaliteitsproblemen naar **quarantined**.
|
||||
|
||||
## Toegestane broncategorieën
|
||||
|
||||
Voorkeursvolgorde:
|
||||
|
||||
1. openbare carrièrepagina van de feitelijke werkgever;
|
||||
2. openbare, door de werkgever bedoelde ATS-vacaturepagina;
|
||||
3. expliciete RSS/Atom-feed of sitemap met vacatures;
|
||||
4. vacaturemail die de gebruiker zelf heeft geactiveerd;
|
||||
5. handmatige browserimport door de gebruiker.
|
||||
|
||||
Niet onboarden zonder expliciete, aantoonbare toestemming:
|
||||
|
||||
- ingelogde zoekresultaten of profielpagina's;
|
||||
- pagina's achter CAPTCHA, anti-bot challenge of toegangscontrole;
|
||||
- denylistplatformen via directe crawling;
|
||||
- verborgen/private endpoints die alleen door reverse engineering zijn gevonden;
|
||||
- bronnen waarvan robots/voorwaarden of technische signalen automatisering verbieden.
|
||||
|
||||
## Reviewchecklist
|
||||
|
||||
Leg per bron vast:
|
||||
|
||||
- naam, bronsoort, hoofddomein en exacte start-URL;
|
||||
- eigenaar/eindwerkgever en eventuele ATS-provider;
|
||||
- publieke toegankelijkheid zonder login;
|
||||
- datum en samenvatting van robots- en voorwaardenreview;
|
||||
- toegestane paden en eventuele uitgesloten paden;
|
||||
- parserstrategie: JSON-LD, RSS, gespecialiseerd ATS of generieke HTML;
|
||||
- verwachte frequentie en minimale interval;
|
||||
- maximaal documentvolume en paginatie;
|
||||
- welke velden daadwerkelijk aanwezig zijn;
|
||||
- retentiebehoefte en mogelijke persoonsgegevens;
|
||||
- contact-/user-agentinformatie indien passend;
|
||||
- rollback/quarantainecriterium.
|
||||
|
||||
`robots.txt` alleen is geen volledige juridische toestemming, en afwezigheid ervan is geen automatische toestemming. Het technische bronbeleid ondersteunt een conservatieve beslissing; de beheerder blijft verantwoordelijk voor de bronreview.
|
||||
|
||||
## Candidate aanmaken
|
||||
|
||||
Gebruik Django admin of `seed_sources` met een gecontroleerd YAML-record. Begin met:
|
||||
|
||||
```yaml
|
||||
name: Voorbeeld Werkgever
|
||||
source_type: employer
|
||||
base_url: https://careers.example.org/jobs
|
||||
status: candidate
|
||||
policy: review
|
||||
parser_key: auto
|
||||
strict_mode: true
|
||||
honor_robots: true
|
||||
crawl_interval_minutes: 720
|
||||
minimum_interval_seconds: 30
|
||||
max_concurrency: 1
|
||||
```
|
||||
|
||||
Zet `policy: allow` pas na review. Een onbekende bron blijft in strict mode zonder fetch.
|
||||
|
||||
## Fixture vóór live request
|
||||
|
||||
Bewaar een gesaneerd voorbeeld onder `fixtures/pages`, `fixtures/feeds` of `fixtures/emails`. Verwijder trackingtokens, persoonsgegevens en niet-noodzakelijke volledige teksten. Schrijf tests voor:
|
||||
|
||||
- normale vacature;
|
||||
- ontbrekende optionele velden;
|
||||
- nul vacatures;
|
||||
- gewijzigde markup of meerdere jobs;
|
||||
- kwaadaardige HTML en onveilige links;
|
||||
- idempotente replay;
|
||||
- parserconfidence en waarschuwingen.
|
||||
|
||||
## Trialrun
|
||||
|
||||
1. Zet bron op `trial` en `policy=allow`.
|
||||
2. Voer één handmatige run uit via de retryknop of Celerytask.
|
||||
3. Controleer status, final URL, bytes, parser, warnings en tellers.
|
||||
4. Open alleen gesaniteerde jobweergave; vergelijk steekproefsgewijs met de publieke bron.
|
||||
5. Controleer canonieke URL, werkgever, locatie, datum, verloopdatum, taal en duplicaten.
|
||||
6. Verifieer dat redirectdoelen, rate limit en conditional requests correct zijn.
|
||||
7. Laat minimaal twee geplande cycli goed verlopen vóór promotie naar `active`.
|
||||
|
||||
## Automatische quarantainecriteria
|
||||
|
||||
Een bron moet worden gepauzeerd of in quarantaine gezet bij:
|
||||
|
||||
- redirect naar denylist, login, private adresruimte of onverwacht domein;
|
||||
- herhaalde 401/403/429, CAPTCHA of anti-botpagina;
|
||||
- contenttype/grootte buiten beleid;
|
||||
- parseroutput met plotseling nul jobs terwijl de bron zichtbaar jobs bevat;
|
||||
- abnormale volumestijging of duplicaatstorm;
|
||||
- HTML-sanitization/securityfout;
|
||||
- voorwaardenwijziging of verlopen bronreview;
|
||||
- opeenvolgende fouten boven de vastgelegde drempel.
|
||||
|
||||
## Gespecialiseerde ATS-adapter
|
||||
|
||||
Voeg alleen een adapter toe wanneer meerdere bronnen hetzelfde stabiele publieke formaat gebruiken of de generieke adapter onvoldoende bewijs levert. De adapter:
|
||||
|
||||
- krijgt geen credentials;
|
||||
- gebruikt uitsluitend gedocumenteerde publieke jobdata of publieke pagina's;
|
||||
- implementeert het interne adaptercontract;
|
||||
- heeft providerfixtures en contracttests;
|
||||
- valt veilig terug zonder globale pipeline te breken;
|
||||
- documenteert paginatie, sluitingssignalen en rate limits.
|
||||
|
||||
## Bron verwijderen
|
||||
|
||||
Pauzeer eerst. Behoud canonieke vacatures en herkomst zolang productretentie dat vereist; verwijder niet blind clusters die ook andere aliassen hebben. Verwijder raw documents volgens retentie, trek de policy in en noteer de reden en datum.
|
||||
|
||||
## Reviewbeleid bij bronbeoordeling
|
||||
|
||||
- Een bron mag alleen automatisch gefetcht worden wanneer een actuele `SourcePolicyReview` bestaat met geldige reden, scope en vervaldatum.
|
||||
- Terms review blijft menselijk, niet automatisch door AI of heuristiek.
|
||||
- Bij ontbrekende of verlopen review of bij expliciet `pause`/`deny` besluit blokkeert de taakuitvoering.
|
||||
|
||||
## Handmatige import in de interface
|
||||
|
||||
Voor uitzonderlijke vacatures kan de beheerder handmatig importeren via de bronpagina:
|
||||
|
||||
- navigeer naar `Brongezondheid` en gebruik het "Handmatige import" formulier;
|
||||
- gebruik de bookmarklet om de huidige pagina-URL te vullen in `source_url`;
|
||||
- of plak direct relevante vacaturetekst in het tekstveld wanneer fetch niet is toegestaan.
|
||||
|
||||
De bookmarklet stuurt alleen `source_url` naar de import-URL, zonder secret of broninhoud.
|
||||
|
||||
Na import toont de bronlijst:
|
||||
|
||||
- de gekozen modus (`url` of `paste`);
|
||||
- bron-ID/bron-URL;
|
||||
- aantallen herkend, nieuw en duplicaat;
|
||||
- waarschuwingen;
|
||||
- links naar vacaturedetail voor direct vervolg.
|
||||
|
||||
## Providerdetails (VR-106)
|
||||
|
||||
Voor het onboarden van publieke ATS-bronnen moet het bronrecord minimaal een van de volgende providerspecificaties gebruiken:
|
||||
|
||||
- Greenhouse (`*.greenhouse.io` / `*.boards.greenhouse.io`) — max 15 requests/min, alleen publiek toegankelijke jobdata.
|
||||
- Lever (`jobs.lever.co`) — max 60 requests/min, alleen `jobs.lever.co` en officiële publieke endpoints.
|
||||
- Recruitee (`*.recruitee.com`) — max 60 requests/min, geen login/partner endpoint.
|
||||
- SmartRecruiters (`*.smartrecruiters.com`) — max 30 requests/min, alleen publieke vacaturepagina/feeds.
|
||||
- Workable (`apply.workable.com`) — max 30 requests/min, alleen publieke vacaturepagina/feeds.
|
||||
|
||||
Bij twijfel altijd op `review` blijven en eerst via een trialrun met gesloten evaluatiecriteria (gesloten job, lege joblijst, markeringswijziging) valideren.
|
||||
@@ -0,0 +1,163 @@
|
||||
# Unraid-deployment
|
||||
|
||||
Deze handleiding gebruikt `docker-compose.unraid.yml` als declaratieve bron van waarheid. Unraid-installaties verschillen in gebruikte Compose Manager, reverse proxy en shares; behoud de hieronder genoemde volumes, secrets en netwerkgrenzen ook wanneer de UI de services afzonderlijk aanmaakt.
|
||||
|
||||
## Vooraf
|
||||
|
||||
Maak deze directories:
|
||||
|
||||
```text
|
||||
/mnt/user/appdata/vacatureradar/config
|
||||
/mnt/user/appdata/vacatureradar/media
|
||||
/mnt/user/appdata/vacatureradar/logs
|
||||
/mnt/user/appdata/vacatureradar/postgres
|
||||
/mnt/user/appdata/vacatureradar/redis
|
||||
/mnt/user/appdata/vacatureradar/ollama # alleen bij lokale AI
|
||||
/mnt/user/backups/vacatureradar
|
||||
```
|
||||
|
||||
Gebruik bij voorkeur een cache-backed `appdata`-share en neem die afzonderlijk in je back-upregime op. De PostgreSQL-datadir mag niet gelijktijdig door file-syncsoftware worden gemuteerd.
|
||||
|
||||
## Imagekeuze
|
||||
|
||||
De Unraid-compose bevat bewust `ghcr.io/CHANGE_ME/vacatureradar:latest`. Codex kan code, Dockerfile en CI afwerken zonder registrycredentials, maar voor productie moet een van deze paden worden gekozen:
|
||||
|
||||
1. bouw lokaal op de Unraid-host en vervang `image:` door een vaste lokale tag;
|
||||
2. publiceer vanuit CI naar een private registry en pin op een immutable digest;
|
||||
3. gebruik `build:`, mits de gekozen Compose-plugin builds betrouwbaar ondersteunt.
|
||||
|
||||
Gebruik in productie geen zwevende `latest` zonder gecontroleerd rollbackpad.
|
||||
|
||||
## Configuratiebestand
|
||||
|
||||
Kopieer of genereer `.env` naar:
|
||||
|
||||
De deploy-helper `scripts/deploy_docker.sh` vult ontbrekende sleutels op basis van veilige defaults.
|
||||
Zet voor publieke productie vooraf de gewenste variabelen:
|
||||
|
||||
```text
|
||||
/mnt/user/appdata/vacatureradar/config/.env
|
||||
APP_HOST=jobs.example.be
|
||||
APP_SCHEME=https
|
||||
DJANGO_DEBUG=0
|
||||
SESSION_COOKIE_SECURE=1
|
||||
CSRF_COOKIE_SECURE=1
|
||||
SECURE_SSL_REDIRECT=1
|
||||
```
|
||||
|
||||
Gebruik daarna de helper (zie hieronder).
|
||||
|
||||
Beperk de bestandsrechten van `.env` tot de beheerder. Voeg geen secrets toe aan de ZIP, Git, screenshots of supportlogs.
|
||||
|
||||
## Eerste uitrol
|
||||
|
||||
1. Controleer alle `CHANGE_ME`-waarden.
|
||||
2. Gebruik de deploy-helper voor een snelle eerste start:
|
||||
|
||||
```bash
|
||||
cd /app/VacatureRadar
|
||||
APP_HOST=jobs.example.be APP_SCHEME=https DJANGO_DEBUG=0 \
|
||||
SESSION_COOKIE_SECURE=1 CSRF_COOKIE_SECURE=1 SECURE_SSL_REDIRECT=1 \
|
||||
bash scripts/deploy_docker.sh
|
||||
```
|
||||
|
||||
3. Start eerst PostgreSQL en Redis.
|
||||
4. Start web; de entrypoint voert migraties uit.
|
||||
5. Start worker en scheduler.
|
||||
6. Controleer `GET /health/live/` en `GET /health/ready/`.
|
||||
6. Voer eenmalig in de webcontainer uit:
|
||||
|
||||
```bash
|
||||
python manage.py bootstrap_instance
|
||||
python manage.py collectstatic --noinput
|
||||
```
|
||||
|
||||
7. Meld lokaal aan en wijzig het bootstrapwachtwoord.
|
||||
8. Activeer nog geen live bron of mailbox voordat bronbeleid, retentie en back-up zijn gecontroleerd.
|
||||
|
||||
## Releaseverificatie en upgradepad
|
||||
|
||||
VR-117 voegt `./scripts/release_verify.sh` toe als release-check met artifacts voor package, checksum, configdiff, sbom en upgrade/rollback.
|
||||
Het script bewaart:
|
||||
|
||||
- `release-artifacts/<ts>/artifacts/release-smoke.json`
|
||||
- `release-artifacts/<ts>/artifacts/smoke-restored-validate.json`
|
||||
- `release-artifacts/<ts>/artifacts/upgrade-rollback.md`
|
||||
- `release-artifacts/<ts>/artifacts/checksums.txt`
|
||||
- `release-artifacts/<ts>/artifacts/config-diff.txt`
|
||||
|
||||
De restorecontrole vergelijkt primaire en gerestoreerde modelcounts in beide smoke-rapporten.
|
||||
|
||||
## Reverse proxy en TLS
|
||||
|
||||
Expose de applicatie niet rechtstreeks op internet. Plaats Nginx Proxy Manager, Traefik, Caddy of een gelijkwaardig beheerd reverse-proxyprofiel voor poort 1226. Vereisten:
|
||||
|
||||
- geldig TLS-certificaat;
|
||||
- alleen HTTPS extern;
|
||||
- `X-Forwarded-Proto` correct doorgeven;
|
||||
- request-bodylimiet klein houden; er is geen algemene upload-API;
|
||||
- optioneel extra access control/VPN voor persoonlijke installatie;
|
||||
- geen publieke toegang tot PostgreSQL, Redis of Ollama.
|
||||
|
||||
Wanneer een proxy op hetzelfde Docker-netwerk draait, publiceer poort 1226 alleen intern. Wanneer Unraid routing een hostpoort vereist, beperk die via firewall/VLAN tot de proxy of het beheernetwerk.
|
||||
|
||||
## Mailbox
|
||||
|
||||
Gebruik een aparte mailbox of alias, niet de hoofdmailbox. Begin met:
|
||||
|
||||
```dotenv
|
||||
IMAP_ENABLED=0
|
||||
IMAP_MARK_SEEN=0
|
||||
```
|
||||
|
||||
Na een succesvolle fixture- en read-onlytest:
|
||||
|
||||
```dotenv
|
||||
IMAP_ENABLED=1
|
||||
IMAP_HOST=<host>
|
||||
IMAP_USER=<account>
|
||||
IMAP_PASSWORD=<app-password>
|
||||
IMAP_MAILBOX=INBOX
|
||||
IMAP_USE_SSL=1
|
||||
```
|
||||
|
||||
Het account heeft alleen mailboxrechten nodig. Gebruik waar beschikbaar een app-password en schakel interactieve login op die mailbox niet uit zolang herstel nodig is.
|
||||
|
||||
## Ollama
|
||||
|
||||
Ollama is optioneel. Start de composeprofile pas wanneer de deterministische kern goed werkt:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.unraid.yml --profile ai up -d
|
||||
```
|
||||
|
||||
Pin een modelnaam in `OLLAMA_MODEL`; zonder model blijft `OLLAMA_ENABLED=0`. Stel Ollama niet extern bloot. Modeloutput wordt als onbetrouwbare gestructureerde data behandeld en mag geen harde regel of sollicitatieactie uitvoeren.
|
||||
|
||||
## Upgraden
|
||||
|
||||
1. Maak en verifieer een database- en configuratieback-up.
|
||||
2. Lees `CHANGELOG.md` en de releaseartefacten.
|
||||
3. Als de release nog niet geverifieerd is, voer `./scripts/release_verify.sh` lokaal uit.
|
||||
4. Pull/bouw de nieuwe immutable image.
|
||||
5. Stop scheduler en worker, daarna web.
|
||||
6. Start web en laat migraties uitvoeren.
|
||||
7. Controleer health, login en release-smoke-rapport.
|
||||
8. Start worker en scheduler.
|
||||
9. Bewaar de vorige image/digest totdat één volledige schedulercyclus goed verliep.
|
||||
|
||||
## Rollback
|
||||
|
||||
Bij een codefout zonder incompatibele migratie: pin de vorige image en herstart web/worker/scheduler. Bij een incompatibele datamigratie: stop alle schrijvers, herstel de vooraf gemaakte databaseback-up en gebruik de overeenkomende vorige image. Voer nooit een restore uit terwijl de worker of scheduler schrijft.
|
||||
|
||||
## Productiechecklist
|
||||
|
||||
- [ ] alle `CHANGE_ME`-waarden verwijderd;
|
||||
- [ ] debug uit en secure cookies aan;
|
||||
- [ ] TLS en host/origin exact ingesteld;
|
||||
- [ ] PostgreSQL/Redis/Ollama niet publiek;
|
||||
- [ ] dagelijkse back-up plus periodieke hersteltest;
|
||||
- [ ] aparte IMAP-mailbox met minimale rechten;
|
||||
- [ ] bronpolicy per actieve bron op `allow` met reviewdatum;
|
||||
- [ ] adminwachtwoord gewijzigd;
|
||||
- [ ] healthmonitoring en vrije schijfruimtebewaking;
|
||||
- [ ] retentiejob en logrotatie gecontroleerd.
|
||||
@@ -0,0 +1,50 @@
|
||||
# Systeemacceptatiecriteria
|
||||
|
||||
## A. Gebruik met minimale handelingen
|
||||
|
||||
- **AC-001** Een nieuwe installatie kan met één bootstrapcommando lokaal starten.
|
||||
- **AC-002** Na onboarding kan de gebruiker zonder bronhandwerk het dashboard en de digest gebruiken.
|
||||
- **AC-003** De normale vacatureactie vereist maximaal één klik plus optionele reden/notitie.
|
||||
- **AC-004** Geen standaardflow vraagt een platformwachtwoord.
|
||||
|
||||
## B. Verzameling
|
||||
|
||||
- **AC-010** Dezelfde fixture tweemaal verwerken creëert één canonieke vacature en één bronalias per identieke bronidentiteit.
|
||||
- **AC-011** Een private/loopback/link-local URL, embedded credential of niet-standaardpoort wordt geblokkeerd.
|
||||
- **AC-012** Een redirect naar een denylist- of niet-publiek doel wordt geblokkeerd vóór de tweede request.
|
||||
- **AC-013** Een niet-toegestaan contenttype en te groot document worden afgewezen.
|
||||
- **AC-014** Een Message-ID tweemaal importeren creëert één e-mailrecord.
|
||||
- **AC-015** JSON-LD, generieke HTML, RSS en e-mailfixtures leveren geldige `ExtractionResult`-objecten.
|
||||
|
||||
## C. Data en matching
|
||||
|
||||
- **AC-020** `m/v/x`, whitespace, contracttypes en datums worden canoniek genormaliseerd.
|
||||
- **AC-021** Exacte ID/URL/key wint van fuzzy matching.
|
||||
- **AC-022** Een uitgesloten titel, regio, contractvorm, afstand of verplichte skill levert recommendation `hidden` met reden.
|
||||
- **AC-023** Een niet-uitgesloten vacature bewaart alle scorecomponenten en evidence.
|
||||
- **AC-024** Ontbrekende afstand levert een concern/confidence-effect maar geen verzonnen kilometerwaarde.
|
||||
- **AC-025** AI-uitval verandert de deterministische kern niet.
|
||||
|
||||
## D. Meldingen en lifecycle
|
||||
|
||||
- **AC-030** Een digestvenster maakt maximaal één outboxrecord per profiel/dag.
|
||||
- **AC-031** Een verborgen vacature verschijnt niet in de digest.
|
||||
- **AC-032** Een verzonden outbox wordt bij herhaling niet opnieuw verstuurd.
|
||||
- **AC-033** `valid_through` in het verleden markeert een actieve vacature verlopen.
|
||||
- **AC-034** Lang niet geziene vacatures gaan eerst naar onzeker en daarna verwijderd.
|
||||
|
||||
## E. Sollicitaties
|
||||
|
||||
- **AC-040** Feedback `applied` creëert één user/job-dossier met snapshot en opvolgdatum.
|
||||
- **AC-041** Een bestaande voorbereiding wordt bij `applied` bijgewerkt zonder duplicaatdossier.
|
||||
- **AC-042** Een gebruiker kan geen dossier van een andere gebruiker openen of wijzigen.
|
||||
- **AC-043** Er bestaat geen endpoint of task die een sollicitatie extern verstuurt.
|
||||
|
||||
## F. Beheer en kwaliteit
|
||||
|
||||
- **AC-050** `/health/live/` antwoordt zonder databaseafhankelijkheid; `/health/ready/` controleert databaseconnectiviteit.
|
||||
- **AC-051** `./scripts/codex_verify.sh` slaagt op een schone checkout na bootstrap.
|
||||
- **AC-052** De standaardtestset doet geen live internetrequests.
|
||||
- **AC-053** Codedekking blijft minimaal 70% en securitykritieke services hebben negatieve tests.
|
||||
- **AC-054** `BACKLOG.yaml` is valide, uniek en dependency-consistent.
|
||||
- **AC-055** Back-up/restorecommando's weigeren onveilige of ontbrekende configuratie met duidelijke fout.
|
||||
@@ -0,0 +1,164 @@
|
||||
# Product Requirements Document — VacatureRadar
|
||||
|
||||
## 1. Productvisie
|
||||
|
||||
VacatureRadar vermindert de tijd en mentale belasting van persoonlijk vacaturezoeken. Na een eenmalige configuratie verzamelt het systeem zelfstandig vacatures uit toegestane bronnen, verwijdert het duplicaten en duidelijke mismatches, verklaart het waarom een vacature relevant is en helpt het de opvolging structureren.
|
||||
|
||||
Het product is geen massale scraper en geen automatische sollicitatiebot. Het is een persoonlijke vacancy-intelligencehub met menselijke eindcontrole.
|
||||
|
||||
## 2. Doelgebruiker
|
||||
|
||||
Primaire gebruiker: één persoon in België die gericht werk zoekt en slechts enkele keren per week de beste resultaten wil bekijken. De implementatie blijft technisch user-scoped zodat later meerdere geïsoleerde gebruikers mogelijk zijn, maar multi-tenancy, billing en teams zijn geen MVP-doel.
|
||||
|
||||
## 3. Succesdefinitie
|
||||
|
||||
Het product is succesvol wanneer:
|
||||
|
||||
- de gebruiker na onboarding geen zoekopdrachten of broncontroles handmatig hoeft te herhalen;
|
||||
- nieuwe passende vacatures binnen 24 uur zichtbaar zijn voor dagelijks gecontroleerde bronnen;
|
||||
- dubbele vermeldingen als één canonieke vacature verschijnen;
|
||||
- iedere aanbeveling een begrijpelijke score, bewijs en aandachtspunten bevat;
|
||||
- harde uitsluitregels nooit door AI worden genegeerd;
|
||||
- bronfalen zichtbaar is zonder dat één fout de rest van de pipeline stopt;
|
||||
- normale bediening beperkt blijft tot bekijken, interessant/bewaren/verbergen/gesolliciteerd;
|
||||
- geen sollicitatie zonder bewuste gebruikersactie wordt verstuurd.
|
||||
|
||||
## 4. Scope
|
||||
|
||||
### 4.1 In scope
|
||||
|
||||
- zoekprofielen met titels, skills, regio, afstand, werkvorm en contract;
|
||||
- harde uitsluitingen en gewogen voorkeuren;
|
||||
- werkgeverspagina's, publieke ATS-pagina's, RSS/Atom, sitemaps en vacaturemails;
|
||||
- handmatige/browserimport als veilige aanvulling;
|
||||
- bronbeleid, rate limiting, health en quarantaine;
|
||||
- extractie, normalisatie, taalherkenning en provenance;
|
||||
- deduplicatie en canonieke bronselectie;
|
||||
- uitlegbare deterministische score;
|
||||
- optionele lokale AI voor begrensde classificatie/samenvatting;
|
||||
- dagelijkse digest en sluitings-/opvolgherinneringen;
|
||||
- sollicitatiedossiers, notities en statussen;
|
||||
- self-hosting op Unraid via containers;
|
||||
- back-up, herstel, tests en observability.
|
||||
|
||||
### 4.2 Buiten scope
|
||||
|
||||
- inloggen op of geautomatiseerd bedienen van vacatureplatformen;
|
||||
- CAPTCHA-, paywall- of blokkadeomzeiling;
|
||||
- massaal herpubliceren van vacaturedatabanken;
|
||||
- automatisch invullen/versturen van sollicitaties;
|
||||
- recruitment-CRM, multi-tenant SaaS of werkgeverstoegang;
|
||||
- garanties dat letterlijk iedere vacature wordt gevonden;
|
||||
- juridisch advies over een bron;
|
||||
- een taalmodel dat definitief bepaalt of iets wordt verwijderd.
|
||||
|
||||
## 5. Functionele requirements
|
||||
|
||||
De IDs zijn stabiel en worden gebruikt in tests en traceability.
|
||||
|
||||
### Profiel en onboarding
|
||||
|
||||
- **PR-001** — De gebruiker kan minstens één zoekprofiel aanmaken en één actief profiel kiezen.
|
||||
- **PR-002** — Het profiel ondersteunt gewenste/uitgesloten titels en skills, regio's, maximale afstand, contracttypes, werkvormen, drempels en digesttijd.
|
||||
- **PR-003** — Iedere betekenisvolle profielwijziging krijgt een versioned snapshot.
|
||||
- **PR-004** — Feedbackleren is opt-in/uitschakelbaar, begrensd en wijzigt geen harde regels.
|
||||
- **PR-005** — De onboarding levert bruikbare defaults zonder dat AI of externe API's nodig zijn.
|
||||
|
||||
### Bronnen en collectie
|
||||
|
||||
- **PR-010** — Iedere bron heeft type, domein, status, policy, parser, planning en healthmetadata.
|
||||
- **PR-011** — Alleen expliciet toegestane bronnen worden automatisch opgehaald.
|
||||
- **PR-012** — Denylistplatformen worden nooit rechtstreeks gecrawld; vacaturemails mogen wel worden verwerkt.
|
||||
- **PR-013** — Iedere URL en redirect wordt vóór toegang op schema, credentials, poort, DNS en IP-bereik gevalideerd.
|
||||
- **PR-014** — Fetches hanteren timeouts, contenttype- en groottelimieten en conditionele headers.
|
||||
- **PR-015** — Bronnen worden idempotent gepland met begrensde retries en backoff.
|
||||
- **PR-016** — Vacaturemails worden idempotent op Message-ID of contenthash verwerkt.
|
||||
- **PR-017** — Nieuwe kandidaatbronnen kunnen worden ontdekt, maar gaan pas na policychecks naar trial/active.
|
||||
- **PR-018** — Een bron met herhaald falen wordt gedegradeerd of in quarantaine geplaatst zonder andere bronnen te stoppen.
|
||||
|
||||
### Extractie en data
|
||||
|
||||
- **PR-020** — JSON-LD `JobPosting` is de voorkeursparser voor individuele vacaturepagina's.
|
||||
- **PR-021** — HTML, RSS/Atom en e-mail hebben deterministische fallbackadapters.
|
||||
- **PR-022** — Iedere parser levert een gemeenschappelijk extractiecontract en confidence.
|
||||
- **PR-023** — Externe HTML wordt gesanitized vóór opslag/rendering.
|
||||
- **PR-024** — Canonieke velden omvatten titel, werkgever, URL, locatie, tekst, contract, werkvorm, data, skills en bronbewijs.
|
||||
- **PR-025** — Wijzigingen aan een bestaande vacature bewaren een versie/snapshot.
|
||||
- **PR-026** — Ruwe documenten hebben een korte instelbare retentie en kunnen worden gequarantained.
|
||||
- **PR-027** — Veldherkomst is zichtbaar voor audit en debugging.
|
||||
|
||||
### Deduplicatie
|
||||
|
||||
- **PR-030** — Exacte external ID, canonieke URL en canonieke sleutel hebben voorrang.
|
||||
- **PR-031** — Fuzzy matching gebruikt titel, werkgever, locatie en inhoud met conservatieve drempel.
|
||||
- **PR-032** — De directe werkgeversbron krijgt waar mogelijk canonieke voorkeur boven recruiter/platformalias.
|
||||
- **PR-033** — Deduplicatie is herhaalbaar en creëert geen extra canonieke vacature bij dezelfde input.
|
||||
|
||||
### Matching en ranking
|
||||
|
||||
- **PR-040** — Harde regels worden vóór scoring toegepast en zijn uitlegbaar.
|
||||
- **PR-041** — Scorecomponenten omvatten inhoud, skills, locatie, voorwaarden, werkgever, senioriteit en voorkeuren.
|
||||
- **PR-042** — Onbekende data verlaagt confidence en wordt niet automatisch als negatief feit behandeld.
|
||||
- **PR-043** — Iedere score bewaart componenten, positives, concerns, uitsluitingen en evidence.
|
||||
- **PR-044** — Profielversie en model-/promptversie worden aan een score gekoppeld.
|
||||
- **PR-045** — AI is optioneel, temperatuurarm/gestructureerd en mag alleen aanvullende features/samenvatting leveren.
|
||||
- **PR-046** — Prompt injection in vacaturetekst mag geen tool- of beleidsactie veroorzaken.
|
||||
|
||||
### Gebruik en opvolging
|
||||
|
||||
- **PR-050** — Het dashboard toont een kleine lijst van de beste actuele niet-uitgesloten vacatures.
|
||||
- **PR-051** — De detailpagina toont score, redenen, bronaliassen, provenance en vacaturestatus.
|
||||
- **PR-052** — De gebruiker kan interessant, bewaren, verbergen en gesolliciteerd registreren.
|
||||
- **PR-053** — Gesolliciteerd maakt of actualiseert een sollicitatiedossier met snapshot en opvolgdatum.
|
||||
- **PR-054** — Een dagelijkse digest bevat sterke/mogelijke matches en verzendt idempotent via outbox.
|
||||
- **PR-055** — Verlopen, onzekere en verwijderde vacatures worden automatisch gemarkeerd.
|
||||
- **PR-056** — Geen enkele gebruikersactie verstuurt automatisch een sollicitatie naar een werkgever.
|
||||
|
||||
### Beheer
|
||||
|
||||
- **PR-060** — Liveness/readiness endpoints zijn beschikbaar.
|
||||
- **PR-061** — Bronruns bewaren aantallen, HTTP-status en foutcategorie.
|
||||
- **PR-062** — Back-up en restore zijn scriptbaar en gedocumenteerd.
|
||||
- **PR-063** — De app kan volledig lokaal zonder externe AI draaien.
|
||||
- **PR-064** — Configuratie komt uit environmentvariables; secrets staan niet in de repository.
|
||||
|
||||
## 6. Niet-functionele requirements
|
||||
|
||||
- **NFR-001 Beveiliging** — Fail-closed voor bronbeleid en URL-validatie.
|
||||
- **NFR-002 Privacy** — Dataminimalisatie, korte ruwe retentie en eenvoudige verwijderbaarheid.
|
||||
- **NFR-003 Betrouwbaarheid** — Imports en meldingen zijn idempotent; fouten blijven per bron/taak geïsoleerd.
|
||||
- **NFR-004 Uitlegbaarheid** — Iedere zichtbare beslissing heeft bewijs en een onderscheid tussen feit, inferentie en onbekend.
|
||||
- **NFR-005 Onderhoudbaarheid** — Modulaire monoliet, servicescheiding, versieerbare adapters en fixtures.
|
||||
- **NFR-006 Testbaarheid** — Geen live internet nodig voor de standaardtests; minimaal 70% branch-aware dekking.
|
||||
- **NFR-007 Performance** — Dashboard p95 onder 1 seconde bij 25.000 vacatures op aanbevolen hardware; bronwerk asynchroon.
|
||||
- **NFR-008 Toegankelijkheid** — Toetsenbordbruikbaar, semantische HTML, zichtbare focus, voldoende contrast en reduced-motionrespect.
|
||||
- **NFR-009 Portabiliteit** — Lokale Pythonmodus en containerdeployment op x86_64; geen cloudvendorlock-in.
|
||||
- **NFR-010 Observability** — Gestructureerde logs, healthstatus, bronmetrics en herstelbare foutcontext zonder secrets.
|
||||
|
||||
## 7. Productdefaults
|
||||
|
||||
- taal: `nl-BE`;
|
||||
- tijdzone: `Europe/Brussels`;
|
||||
- dagelijkse digest: 07:30 lokale tijd;
|
||||
- ruwe-documentretentie: 7 dagen;
|
||||
- standaard maximumafstand: 45 km;
|
||||
- aanbevelingsdrempel: 65;
|
||||
- topmatchdrempel: 90;
|
||||
- onbekende bronpolicy: `review`;
|
||||
- AI: uit;
|
||||
- browserautomatisering: uit;
|
||||
- automatisch solliciteren: niet beschikbaar.
|
||||
|
||||
## 8. Productmetrics
|
||||
|
||||
Te meten zonder tracking naar derden:
|
||||
|
||||
- nieuwe canonieke vacatures per bron/dag;
|
||||
- duplicaatratio;
|
||||
- parserconfidence en velddichtheid;
|
||||
- aandeel vacatures per recommendation;
|
||||
- hide/interessant/sollicitatiefeedback per scoreband;
|
||||
- tijd van publicatie/first-seen tot digest;
|
||||
- bronfoutpercentage en herstelduur;
|
||||
- gemiste vacatures in een handmatige benchmarksteekproef;
|
||||
- false-positive harde uitsluitingen via gebruikersundo.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Scope, grenzen en aannames
|
||||
|
||||
## Kernbelofte
|
||||
|
||||
VacatureRadar levert een zo compleet mogelijke, persoonlijke selectie binnen toegestane en technisch bereikbare bronnen. Het belooft niet letterlijk alle vacatures te vinden.
|
||||
|
||||
## Brondekking
|
||||
|
||||
Voorkeursvolgorde:
|
||||
|
||||
1. oorspronkelijke werkgeverspagina;
|
||||
2. publieke ATS-carrièrepagina;
|
||||
3. gestructureerde RSS/Atom/sitemap;
|
||||
4. vacaturemail met link;
|
||||
5. handmatige/browserimport;
|
||||
6. platformalias uitsluitend voor herkomst en ontdekking, niet voor direct crawlen wanneer denylist/policy dat blokkeert.
|
||||
|
||||
## Aannames
|
||||
|
||||
- één beheerder/eindgebruiker per installatie;
|
||||
- server heeft betrouwbare klok en tijdzoneconfiguratie;
|
||||
- DNS en outbound internet zijn beschikbaar voor goedgekeurde bronnen;
|
||||
- mailbox wordt specifiek voor vacaturealerts gebruikt;
|
||||
- de gebruiker beoordeelt live bronvoorwaarden en sollicitaties;
|
||||
- Unraid-opslag is persistent en wordt extern geback-upt;
|
||||
- lokale AI is optioneel en niet noodzakelijk voor kernfunctionaliteit.
|
||||
|
||||
## Niet-doelen
|
||||
|
||||
- stealthscraping, proxyrotatie of anti-botomzeiling;
|
||||
- het nabouwen van LinkedIn/Indeed zoekresultaten;
|
||||
- persoonlijkheids- of discriminatoire profiling;
|
||||
- autonome carrièrebeslissingen;
|
||||
- cv-ranking voor werkgevers;
|
||||
- geautomatiseerde massacommunicatie;
|
||||
- bewaren van complete mailboxen of onnodige persoonsgegevens;
|
||||
- generieke workflowengine of microserviceplatform.
|
||||
|
||||
## Productrisico
|
||||
|
||||
De grootste productrisico's zijn bronverandering, gemiste vacatures, foutpositieve matches en onderhoudslast. Daarom kiest het ontwerp voor adapters met fixtures, confidence, provenance, bronhealth, conservatieve dedupe en een dagelijkse menselijke review van alleen de beste resultaten.
|
||||
@@ -0,0 +1,112 @@
|
||||
# User stories en primaire flows
|
||||
|
||||
## Persona
|
||||
|
||||
De gebruiker wil geen dagelijkse zoekmachine bedienen. Hij wil enkele sterke vacatures zien, begrijpen waarom ze passen en snel opvolgen wat hij ermee deed.
|
||||
|
||||
## Onboarding
|
||||
|
||||
### US-001 — Zoekprofiel instellen
|
||||
|
||||
Als gebruiker wil ik mijn gewenste rollen, skills, regio en harde uitsluitingen één keer instellen zodat de tool daarna zelfstandig zoekt.
|
||||
|
||||
Acceptatie:
|
||||
|
||||
- defaults zijn direct bruikbaar;
|
||||
- titel/skillvelden ondersteunen meerdere regels;
|
||||
- locatie kan eerst zonder coördinaten worden opgeslagen;
|
||||
- ongeldige drempels of coördinaten geven veldgerichte feedback;
|
||||
- opslaan maakt een profielrevisie.
|
||||
|
||||
### US-002 — Veilige bronkanalen kiezen
|
||||
|
||||
Als gebruiker wil ik vacaturemails en publieke werkgeversbronnen kunnen activeren zonder platformlogins aan de tool te geven.
|
||||
|
||||
Acceptatie:
|
||||
|
||||
- mailboxconfiguratie staat alleen in secrets/environment;
|
||||
- denylistlinks uit mails worden als aliassen opgeslagen maar niet door de fetcher bezocht;
|
||||
- onbekende bron blijft in review.
|
||||
|
||||
## Dagelijkse lus
|
||||
|
||||
### US-010 — Beste matches bekijken
|
||||
|
||||
Als gebruiker wil ik bij het openen maximaal een overzichtelijke selectie zien zodat ik niet opnieuw honderden resultaten hoef te filteren.
|
||||
|
||||
Acceptatie:
|
||||
|
||||
- actieve, niet-hard-uitgesloten vacatures eerst;
|
||||
- één kaart per canonieke vacature;
|
||||
- score, werkgever, locatie, werkvorm en kernreden zichtbaar;
|
||||
- sterke en mogelijke matches duidelijk onderscheiden.
|
||||
|
||||
### US-011 — Begrijpen waarom iets past
|
||||
|
||||
Als gebruiker wil ik bewijs, pluspunten en aandachtspunten zien zodat ik de score kan vertrouwen en corrigeren.
|
||||
|
||||
Acceptatie:
|
||||
|
||||
- scorecomponenten en confidence zijn zichtbaar;
|
||||
- onbekende afstand/salaris wordt als onbekend gemeld;
|
||||
- herkomstlinks blijven bereikbaar;
|
||||
- gesanitized vacaturetekst bevat geen uitvoerbare scripts/forms.
|
||||
|
||||
### US-012 — Snelle feedback
|
||||
|
||||
Als gebruiker wil ik met één actie interessant, bewaren, verbergen of gesolliciteerd registreren.
|
||||
|
||||
Acceptatie:
|
||||
|
||||
- actie is CSRF-beschermd en user-scoped;
|
||||
- verbergen verwijdert de vacature uit toekomstige digests;
|
||||
- gesolliciteerd maakt een dossier en bewaart een snapshot;
|
||||
- geen actie verstuurt iets naar de werkgever.
|
||||
|
||||
## Meldingen
|
||||
|
||||
### US-020 — Dagelijkse digest
|
||||
|
||||
Als gebruiker wil ik één samenvatting ontvangen zodat ik de interface niet voortdurend hoef te controleren.
|
||||
|
||||
Acceptatie:
|
||||
|
||||
- één outboxrecord per profiel/dag;
|
||||
- herhaald uitvoeren verzendt niet dubbel;
|
||||
- sterke en mogelijke matches worden geteld;
|
||||
- zonder ontvanger wordt veilig overgeslagen;
|
||||
- verzendfouten blijven retrybaar en zichtbaar.
|
||||
|
||||
### US-021 — Deadline en opvolging
|
||||
|
||||
Als gebruiker wil ik herinnerd worden aan sluitings- en opvolgdata zodat kansen niet ongemerkt verlopen.
|
||||
|
||||
Acceptatie:
|
||||
|
||||
- reminders respecteren quiet hours;
|
||||
- verlopen vacature wordt niet als nieuwe match gemeld;
|
||||
- herinnering bevat bronlink en dossierstatus;
|
||||
- gebruiker kan herinnering uitschakelen.
|
||||
|
||||
## Beheer en herstel
|
||||
|
||||
### US-030 — Bronproblemen zien
|
||||
|
||||
Als gebruiker wil ik zien welke bronnen gezond, vertraagd of geblokkeerd zijn zonder logs te lezen.
|
||||
|
||||
Acceptatie:
|
||||
|
||||
- status, laatste succes/fout, volgende run en reden zichtbaar;
|
||||
- handmatige retry respecteert denybeleid;
|
||||
- herhaalde parsefout degradeert bron, niet hele app.
|
||||
|
||||
### US-031 — Herstellen na storing
|
||||
|
||||
Als beheerder wil ik back-up en restore kunnen uitvoeren zodat profiel, vacatures en sollicitatiedossiers niet verloren gaan.
|
||||
|
||||
Acceptatie:
|
||||
|
||||
- database- en mediaback-up zijn apart;
|
||||
- restore vereist expliciete bevestiging;
|
||||
- hersteltest is gedocumenteerd;
|
||||
- secrets worden niet in gewone databack-up opgenomen.
|
||||
@@ -0,0 +1,58 @@
|
||||
# Benchmarks en kwaliteitsrapportage (VR-116)
|
||||
|
||||
## Doel
|
||||
|
||||
VR-116 levert een reproduceerbaar benchmarkpakket voor:
|
||||
|
||||
- parservelddekking;
|
||||
- deduplicatie-kwaliteit (precision/recall, false merges/missed merges);
|
||||
- rankingstabiliteit (top-N churn, onbekende-data ratio);
|
||||
- performance (p50/p95, querycount en memory peak).
|
||||
|
||||
De CLI werkt zonder live netwerk met de synthetische fixtures uit `fixtures/benchmark/`.
|
||||
|
||||
## Datasets
|
||||
|
||||
`fixtures/benchmark/quality_benchmark.json`
|
||||
|
||||
- bevat parsercases met verwachte parserkey/version en verwachte minima;
|
||||
- bevat dedupe seeds en querycases met verwachte matchlabels;
|
||||
- bevat geen echte broninhoud of persoonlijke gegevens.
|
||||
|
||||
## CLI
|
||||
|
||||
```bash
|
||||
uv run python scripts/benchmark.py --quick
|
||||
uv run python scripts/benchmark.py --jobs 25000 --top-n 25
|
||||
uv run python scripts/benchmark.py --jobs 25000 --json --top-n 25
|
||||
uv run python scripts/benchmark.py --skip-performance --quick
|
||||
```
|
||||
|
||||
## Rapportformat
|
||||
|
||||
De JSON-rapportage bevat minimaal:
|
||||
|
||||
- `parser`: dekking en onbekend-aandeel;
|
||||
- `dedupe`: precision, recall, false merges, missed merges;
|
||||
- `ranking`: top-N churn en unknown-data ratio;
|
||||
- `performance`: p50, p95, querycount, peak memory, totale duur;
|
||||
- `nfr_007`: gegraveerde metingstatus/passing van p95 < 1000 ms.
|
||||
|
||||
## Drempels en interpretatie
|
||||
|
||||
- Parser: standaard doel `coverage_ratio >= 0.90`.
|
||||
- Dedupe: standaard doel `precision >= 0.90` en `recall >= 0.90`.
|
||||
- Ranking: standaard doel `churn <= 0.35`.
|
||||
- NFR-007: enkel als `nfr_007.measured == true` wordt de p95-check als afgewerkt beoordeeld.
|
||||
|
||||
## Uitvoeringsmodus
|
||||
|
||||
- `--quick`: voert parser/dedupe/ranking uit met 250 ingestaste jobs.
|
||||
- standaardmodus: voert ook een performancepass uit op 25.000 jobs (mits runtime toelaatbaar).
|
||||
- `--skip-performance`: nuttig voor CI/quickvalidatie zonder de lange performance-run.
|
||||
|
||||
## Afwijkingen
|
||||
|
||||
Wanneer p95 of dedupe niet voldoet, rapporteert het script een status `failed` met detaildata per case.
|
||||
Die output is bedoeld als regressiebasis voor foutanalyse en threshold-sturing.
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# Definition of Done
|
||||
|
||||
Een backlogtaak is alleen `done` wanneer alle toepasselijke punten hieronder zijn voldaan. “Code geschreven” of “werkt op mijn machine” is geen afronding.
|
||||
|
||||
## 1. Gedrag
|
||||
|
||||
- De taak voldoet aantoonbaar aan ieder acceptatiecriterium uit `docs/ai/BACKLOG.yaml`.
|
||||
- Succes-, lege-input-, fout- en herhaalpad zijn bewust ontworpen.
|
||||
- Imports, schedulertaken en notificaties zijn idempotent waar at-least-once uitvoering mogelijk is.
|
||||
- De wijziging respecteert de product-nongoals: geen platformloginbot, blokkadeomzeiling of automatische sollicitatie.
|
||||
- Ontbrekende externe services hebben een bruikbare fallback of duidelijke gedegradeerde toestand.
|
||||
|
||||
## 2. Architectuur
|
||||
|
||||
- Bedrijfslogica staat in een domeinservice; views/tasks blijven dun.
|
||||
- Adapters schrijven niet naar de database.
|
||||
- Modellen en querysets blijven user-scoped waar persoonsgegevens of gebruikersacties betrokken zijn.
|
||||
- Een nieuwe dependency heeft een concrete noodzaak, vaste versie, lockfilewijziging en operationele impactdocumentatie.
|
||||
- Een architectuurwijziging die een accepted ADR tegenspreekt krijgt eerst een nieuwe ADR.
|
||||
|
||||
## 3. Beveiliging en privacy
|
||||
|
||||
- Externe data wordt als onbetrouwbaar behandeld, gesanitized en niet als instructie uitgevoerd.
|
||||
- Netwerktoegang passeert policy- en SSRF-controle op iedere redirect.
|
||||
- Geen secret, echt persoonlijk gegeven, volledige mailboxpayload of gevoelige header staat in code, fixture, testoutput of log.
|
||||
- Autorisatie, CSRF en objectscoping zijn getest voor muterende webflows.
|
||||
- Retentie, verwijdering en back-upimpact zijn beoordeeld bij nieuwe opgeslagen data.
|
||||
- `docs/quality/THREAT_MODEL.md` is bijgewerkt voor nieuwe trust boundaries of mitigaties.
|
||||
|
||||
## 4. Datamodel en migraties
|
||||
|
||||
- `makemigrations --check --dry-run` is schoon.
|
||||
- Nieuwe migraties zijn handmatig beoordeeld en hebben een herstel-/rollbackstrategie.
|
||||
- Datamigraties zijn begrensd, herstartbaar of transactioneel waar passend.
|
||||
- Indexen en queryvormen zijn beoordeeld voor de verwachte schaal.
|
||||
- Historiek/provenance blijft behouden bij normalisatie- of dedupewijzigingen.
|
||||
|
||||
## 5. Tests
|
||||
|
||||
- Unit-tests dekken pure regels en randgevallen.
|
||||
- Integratietests dekken database-, pipeline- en idempotentiegedrag.
|
||||
- Securitytests dekken negatieve inputs voor securitykritieke wijzigingen.
|
||||
- Externe formaten hebben gesaneerde fixtures; standaardtests doen geen internetcall.
|
||||
- Bugs krijgen een regressietest die vóór de fix faalt.
|
||||
- Totale branch-aware dekking blijft minstens 70%; securitykritieke services streven naar 90%+ relevante paden.
|
||||
|
||||
## 6. UX en toegankelijkheid
|
||||
|
||||
- UI-copy is begrijpelijk Nederlands en maakt feit/inferentie/onbekend onderscheid.
|
||||
- Kernflow werkt met toetsenbord, zichtbare focus en zonder JavaScript.
|
||||
- Labels, foutmeldingen, headingstructuur en statusmeldingen zijn semantisch correct.
|
||||
- Layout blijft bruikbaar op 360 px breedte en gangbare desktopbreedte.
|
||||
- Reduced motion, contrast en niet-kleurgebonden statussignalen zijn behouden.
|
||||
|
||||
## 7. Documentatie en traceability
|
||||
|
||||
- Publieke/internal contracts, runbooks en configuratievoorbeelden zijn bijgewerkt.
|
||||
- Requirement- en acceptatie-ID's zijn gekoppeld in `TRACEABILITY_MATRIX.md`.
|
||||
- Backlogtaak bevat een korte resultaatnotitie en exacte verificatie.
|
||||
- `PROJECT_STATE.md` noemt de afgeronde taak, impact, gate-uitkomst en volgende taak.
|
||||
- Geen document claimt functionaliteit die alleen gepland is.
|
||||
|
||||
## 8. Verificatie
|
||||
|
||||
De volledige gate slaagt vanaf de repositoryroot:
|
||||
|
||||
```bash
|
||||
./scripts/codex_verify.sh
|
||||
```
|
||||
|
||||
Daarbij slagen minimaal:
|
||||
|
||||
- Ruff;
|
||||
- Django system check;
|
||||
- migratiecheck;
|
||||
- volledige testset met dekkingsdrempel;
|
||||
- backlogvalidatie;
|
||||
- documentlink-/configuratievalidatie voor zover in het script opgenomen.
|
||||
|
||||
## 9. Extern geblokkeerd
|
||||
|
||||
Een taak wordt niet `done` wanneer live verificatie noodzakelijk maar onmogelijk is. Het externe restpunt mag `blocked-external` worden nadat interface, mocks, tests, foutafhandeling, configuratie en een exact uitvoerchecklistje zijn afgemaakt. Onafhankelijke taken gaan daarna door.
|
||||
@@ -0,0 +1,60 @@
|
||||
# Releasechecklist
|
||||
|
||||
## Code en requirements
|
||||
|
||||
- [ ] alle voor de release geplande backlogtaken staan `done`;
|
||||
- [ ] geen `ready` taak is stilzwijgend overgeslagen;
|
||||
- [ ] `PROJECT_STATE.md`, changelog en traceability zijn actueel;
|
||||
- [ ] nieuwe/gewijzigde requirements hebben acceptatiecriteria en tests;
|
||||
- [ ] geen geplande functionaliteit wordt als geïmplementeerd beschreven.
|
||||
|
||||
## Quality gate
|
||||
|
||||
```bash
|
||||
./scripts/codex_verify.sh
|
||||
```
|
||||
|
||||
- [ ] Ruff schoon;
|
||||
- [ ] Django check schoon;
|
||||
- [ ] geen ontbrekende migraties;
|
||||
- [ ] alle tests geslaagd;
|
||||
- [ ] coverage boven drempel;
|
||||
- [ ] backlog en documentlinks geldig;
|
||||
- [ ] Compose YAML parseert;
|
||||
- [ ] containerbuild/smoke-test uitgevoerd op releaseomgeving.
|
||||
|
||||
## Security/privacy
|
||||
|
||||
- [ ] threat model en securitytests bijgewerkt;
|
||||
- [ ] dependency-/image-/secret-scan beoordeeld;
|
||||
- [ ] geen echte credentials of PII in repository, fixtures of artifact;
|
||||
- [ ] debug uit in productievoorbeeld;
|
||||
- [ ] source denylist en strict policy behouden;
|
||||
- [ ] retentie en back-upimpact gecontroleerd;
|
||||
- [ ] geen submit-, loginbot- of CAPTCHA-code toegevoegd.
|
||||
|
||||
## Database en upgrade
|
||||
|
||||
- [ ] migraties handmatig beoordeeld;
|
||||
- [ ] upgrade vanaf vorige ondersteunde release getest;
|
||||
- [ ] back-up vóór migratie gemaakt en checksum geverifieerd;
|
||||
- [ ] rollback/herstelpad getest of expliciet gedocumenteerd;
|
||||
- [ ] vorige immutable image beschikbaar.
|
||||
|
||||
## UX
|
||||
|
||||
- [ ] dashboard, lijst, detail, profiel, bronnen en sollicitaties gesmoked;
|
||||
- [ ] keyboard/focus en 360 px layout gecontroleerd;
|
||||
- [ ] foutmeldingen en lege toestanden begrijpelijk;
|
||||
- [ ] digest in tekst en HTML beoordeeld;
|
||||
- [ ] geen externe tracking of onverwachte third-party assets.
|
||||
|
||||
## Deployment
|
||||
|
||||
- [ ] image krijgt semantische tag en immutable digest;
|
||||
- [ ] release-notes noemen config/migratie/breaking changes;
|
||||
- [ ] healthchecks slagen;
|
||||
- [ ] precies één scheduler actief;
|
||||
- [ ] PostgreSQL, Redis en Ollama niet publiek bereikbaar;
|
||||
- [ ] monitoring en back-upjob actief;
|
||||
- [ ] live source/IMAP blijft uit tot credentials en bronreview gereed zijn.
|
||||
@@ -0,0 +1,145 @@
|
||||
# Teststrategie
|
||||
|
||||
## Doel
|
||||
|
||||
Tests bewijzen dat VacatureRadar veilig herhaalbaar verzamelt, betrouwbare canonieke data maakt, uitlegbaar scoort en zonder externe services blijft functioneren. De standaardtestset is snel, deterministisch en offline.
|
||||
|
||||
## Testpiramide
|
||||
|
||||
### Unit
|
||||
|
||||
Pure functies en begrensde services:
|
||||
|
||||
- URL-validatie en canonicalisatie;
|
||||
- source policy en denylist;
|
||||
- adapterextractie uit fixtures;
|
||||
- HTML-sanitization;
|
||||
- normalisatie van titel, contract, locatie en datums;
|
||||
- feature-extractie, distance fallback en scoring;
|
||||
- AI-schema/parsing/fallback;
|
||||
- profielgewichten en feedbackgrenzen.
|
||||
|
||||
Doel: veel kleine tests, geen echte netwerkcall en alleen database wanneer het objectgedrag zelf wordt getest.
|
||||
|
||||
### Integratie
|
||||
|
||||
Meerdere componenten met testdatabase:
|
||||
|
||||
- raw document → adapter → normalisatie → dedupe → provenance → score;
|
||||
- replay/idempotentie en vacatureversies;
|
||||
- IMAP/e-mailrecording via gesimuleerde berichten;
|
||||
- digest-outbox en mailbackend;
|
||||
- lifecycle en sollicitatiedossier;
|
||||
- Django-views, login, user-scoping en POST-acties;
|
||||
- Celerytasks in eager mode.
|
||||
|
||||
### Security
|
||||
|
||||
Negatieve tests voor trust boundaries:
|
||||
|
||||
- loopback, private, link-local, encoded/credentialed en onverwachte URL's;
|
||||
- redirecthercontrole;
|
||||
- denylistplatformen;
|
||||
- contenttype/grootte;
|
||||
- XSS/sanitization en onveilige links;
|
||||
- prompt-injectionachtige vacaturetekst;
|
||||
- cross-user objecttoegang;
|
||||
- secret-/payloadlekken in foutpaden waar praktisch testbaar.
|
||||
|
||||
### Browser/e2e
|
||||
|
||||
Gepland in backlogtaak `VR-114`. Gebruik een lokale testserver en seeded data, geen live bron. Minimale flows:
|
||||
|
||||
1. aanmelden;
|
||||
2. dashboard openen;
|
||||
3. profiel wijzigen en revisie zien;
|
||||
4. vacature zoeken/openen;
|
||||
5. interessant/verbergen/gesolliciteerd;
|
||||
6. sollicitatiedossier bijwerken;
|
||||
7. bronstatus bekijken;
|
||||
8. toetsenbord- en basisaccessibilityscan.
|
||||
|
||||
Browsertests zijn aanvullend; zij vervangen geen service- of autorisatietests.
|
||||
|
||||
### Release- en deploymenttests
|
||||
|
||||
- Dockerfile buildt zonder ontwikkelbestanden/secrets;
|
||||
- container start als niet-rootgebruiker;
|
||||
- migraties en `collectstatic` slagen;
|
||||
- liveness/readiness reageren correct;
|
||||
- Composeconfiguratie parseert;
|
||||
- PostgreSQL/Redisstack verwerkt één fixture en digest;
|
||||
- back-up en restore zijn op een tijdelijke omgeving getest.
|
||||
|
||||
Deze laag kan in CI of een releasejob draaien zodra een Dockerdaemon beschikbaar is.
|
||||
|
||||
## Fixtures
|
||||
|
||||
Directoryindeling:
|
||||
|
||||
```text
|
||||
fixtures/pages/ HTML/JSON-LD
|
||||
fixtures/feeds/ RSS/Atom/sitemaps
|
||||
fixtures/emails/ RFC822-berichten
|
||||
fixtures/golden/ verwachte gestructureerde output
|
||||
```
|
||||
|
||||
Regels:
|
||||
|
||||
- fictieve werkgevers/personen en `example.*`-domeinen;
|
||||
- alleen het minimum aan vacaturetekst;
|
||||
- geen cookies, tokens, echte Message-ID's of persoonlijke adressen;
|
||||
- bronvariant en doel van fixture in testnaam of korte comment;
|
||||
- grote regressiepayloads waar mogelijk reduceren tot minimale reproducer.
|
||||
|
||||
## Netwerkisolatie
|
||||
|
||||
Geen test mag standaard internet vereisen. Patch `httpx`, DNS-resolutie, IMAP, SMTP en Ollama aan de boundary. Een test die per ongeluk live netwerk gebruikt, is een testfout. Eventuele live smoke-tests krijgen een aparte marker, expliciete environmentflag en draaien nooit in de standaard-CI.
|
||||
|
||||
## Dekkingsbeleid
|
||||
|
||||
`pyproject.toml` vereist minimaal 70% branch-aware totaal. Dekking is een vangnet, geen doel op zich:
|
||||
|
||||
- fetcher, URL-security, policy, sanitization, authscoping en automatische acties: alle belangrijke negatieve paden;
|
||||
- parsers: iedere ondersteunde variant plus malformed/lege input;
|
||||
- scoring/dedupe: beslisdrempels en tie-breakers;
|
||||
- triviale adminregistratie en migratiecode mogen uit de meting blijven.
|
||||
|
||||
Verlaag de drempel niet voor een taak. Voeg tests toe of documenteer en verwijder aantoonbaar dode code.
|
||||
|
||||
## Testdata en tijd
|
||||
|
||||
- Gebruik timezone-aware vaste datums of `timezone.now()` relatief binnen de test.
|
||||
- Vermijd assertions die na middernacht lokale tijd flaken.
|
||||
- Randomized/propertytests gebruiken een vaste seed en melden die bij falen.
|
||||
- Volgorde-onafhankelijkheid: iedere test maakt eigen state via fixtures/factories.
|
||||
|
||||
## Performance
|
||||
|
||||
Voor `NFR-007` komt een reproduceerbaar benchmarkscript in `VR-116` met minstens:
|
||||
|
||||
- 25.000 canonieke vacatures;
|
||||
- meerdere aliassen en scoreruns;
|
||||
- dashboard/list/detailqueries;
|
||||
- importbatch en rescore;
|
||||
- p50/p95, querycount en geheugengebruik.
|
||||
|
||||
Een performanceclaim wordt alleen in documentatie opgenomen met hardwareprofiel, dataset en meetcommando.
|
||||
|
||||
## Uitvoercommando's
|
||||
|
||||
```bash
|
||||
uv run pytest tests/unit
|
||||
uv run pytest tests/integration
|
||||
uv run pytest tests/security
|
||||
uv run pytest --cov=apps --cov=config --cov-report=term-missing
|
||||
./scripts/codex_verify.sh
|
||||
```
|
||||
|
||||
## Bugworkflow
|
||||
|
||||
1. Reproduceer met kleinste betrouwbare fixture/test.
|
||||
2. Laat de test falen op de huidige code.
|
||||
3. Herstel bij de laag waar de regel hoort.
|
||||
4. Draai relevante tests en daarna de volledige gate.
|
||||
5. Werk threat model/runbook bij wanneer het een security- of operationeel defect was.
|
||||
@@ -0,0 +1,140 @@
|
||||
# Threat model
|
||||
|
||||
## Scope en assets
|
||||
|
||||
Te beschermen:
|
||||
|
||||
- gebruikersaccount, sessies en zoekvoorkeuren;
|
||||
- mailbox-, SMTP-, database- en registrycredentials;
|
||||
- sollicitatiedossiers, notities en eventuele documenten;
|
||||
- integriteit van vacatureclusters, scores en feedback;
|
||||
- beschikbaarheid van de Unraid-host en het thuisnetwerk;
|
||||
- bronbeleid en bewijs dat alleen toegestane bronnen worden benaderd.
|
||||
|
||||
Niet vertrouwd:
|
||||
|
||||
- iedere vacaturetekst, HTML, JSON-LD, feed en e-mail;
|
||||
- alle links en redirectdoelen;
|
||||
- externe DNS/HTTP/TLS-responses;
|
||||
- AI-modeloutput, ook lokaal;
|
||||
- browserinput en handmatige URL's;
|
||||
- logs of exports die buiten de applicatie worden geopend.
|
||||
|
||||
## Trust boundaries
|
||||
|
||||
```text
|
||||
Internet/mailbox
|
||||
-> URL/source policy + IMAP boundary
|
||||
-> raw document quarantine/retention
|
||||
-> parser + sanitizer
|
||||
-> normalized domain model
|
||||
-> deterministic rules/scoring
|
||||
-> authenticated UI / digest
|
||||
|
||||
User browser
|
||||
-> TLS/reverse proxy
|
||||
-> Django session + CSRF + object scoping
|
||||
-> database
|
||||
|
||||
Django/Celery
|
||||
-> Redis (queue, geen waarheid)
|
||||
-> PostgreSQL (bron van waarheid)
|
||||
-> optionele Ollama boundary
|
||||
```
|
||||
|
||||
## Belangrijkste dreigingen en mitigaties
|
||||
|
||||
| ID | Dreiging | Impact | Mitigaties | Verificatie |
|
||||
|---|---|---|---|---|
|
||||
| T-01 | SSRF via bron- of vacature-URL | toegang tot LAN, metadata of beheerinterfaces | schema/host/IP/poortcontrole, DNS-resolutie, redirecthercontrole, denylist, geen credentials in URL | `tests/security/test_url_security.py`, `test_policy.py`, fetchertests |
|
||||
| T-02 | DNS rebinding/tijd-van-controle | request bereikt ander IP dan beoordeeld | iedere fetch en redirect valideert opnieuw DNS-resolutie en vereist consistente IP-set-overlap | `VR-115`, `tests/unit/test_fetcher.py`, aanvullende fetcher negative tests |
|
||||
| T-03 | XSS/HTML-injectie | sessiediefstal of ongewenste navigatie | Bleach allowlist, platte tekst, template autoescaping, CSP/securityheaders | `tests/security/test_sanitize.py`, viewtests |
|
||||
| T-04 | Prompt injection in vacature | AI verandert beleid of voert tools uit | vacaturetekst in data-envelope, schema-output, evidence-validatie, cachefouten met foutcategorie, geen toolrechten, deterministische harde regels, AI optioneel | AI-tests, ADR-0003, `VR-109` evaluaties |
|
||||
| T-05 | Malafide/oversized document | geheugen/CPU/diskuitputting | contenttype-, byte-, timeout- en redirectlimieten; korte retentie | fetchertests, source-runmetrics |
|
||||
| T-06 | Crawlstorm of ban | externe overlast, IP-blokkade | expliciet allowbeleid, per-origin minimuminterval, concurrencylimiet, leasegebaseerde taakclaim met TTL + crash recovery, conditional requests, backoff, quarantaine | tasktests; `VR-103`, `VR-112` |
|
||||
| T-07 | Direct platformscrapen | voorwaarden-/blokkaderisico | centrale denylist, e-mailingress, geen loginbot/CAPTCHA | policytests, ADR-0002/0005 |
|
||||
| T-08 | E-mailparserlink naar phishing/private host | ongewenste fetch of gebruiker misleid | linkextractie slaat unsubscribe over, follow-up opnieuw door policy/SSRF, bronlabel zichtbaar | e-mail- en policytests |
|
||||
| T-09 | MIME/HTML-bom of malformed mail | resource-uitputting/parsercrash | IMAPbatchlimiet, bodylimiet bij vervolgtaak, robuuste parser, idempotente recordidentiteit | `VR-115`, e-mailfixtures |
|
||||
| T-10 | Cross-user objecttoegang | lek van sollicitaties/feedback | LoginRequired, user-scoped querysets/get_or_create, autorisatietests | integratie-viewtests |
|
||||
| T-11 | CSRF op feedback/profiel | ongewenste status- of profielmutatie | Django CSRF-middleware, POST-only acties, SameSitecookies | viewtests en frameworkcheck |
|
||||
| T-12 | Credentiallek in Git/logs | account- of infrastructuurcompromis | env-only secrets, gitignore, beperkte logs, voorbeeldwaarden, secretrotatierunbook | CI-secret scan gepland `VR-115`; review |
|
||||
| T-13 | Onveilige reverse proxy | sessie over HTTP, host-header-/originproblemen | TLS, exact ALLOWED_HOSTS/CSRF origins, secure cookies, proxychecklist | deployment smoke-test |
|
||||
| T-14 | Redis als waarheid behandeld | dataverlies/dubbele verwerking | PostgreSQL-state, idempotente taken/outbox, acks-late | integratietests |
|
||||
| T-15 | Dubbele Celery-uitvoering | dubbele vacatures/mails | unieke keys, transacties, message identity, outbox-uniciteit | pipeline/digesttests |
|
||||
| T-16 | Dedupe voegt verschillende jobs samen | gemiste kans/verkeerde data | exact vóór fuzzy, conservatieve drempel, bronaliassen/provenance, herstelbare clusterbeslissing gepland | dedupetests; `VR-116` evaluaties |
|
||||
| T-17 | AI hallucineert ontbrekend feit | foutieve ranking | onbekend blijft onbekend, evidence/confidence, geen hard exclusion door AI | scoring/AI-tests |
|
||||
| T-18 | Feedback poisoning/self-reinforcement | steeds smallere resultaten | begrensde gewichten, opt-in standaard uit, per-feature minimum samples, metadata-logging en resetpad | profieltests; `VR-110` |
|
||||
| T-19 | Automatisch solliciteren/massamail | reputatie/privacy/juridisch risico | geen submitendpoint/task, expliciete ADR en non-goal | codezoekreview, AC-043 |
|
||||
| T-20 | Kwaadaardig bestand in sollicitatiedossier | host/browsercompromis | uploads nog niet actief; vóór implementatie allowlist, sniffing, size limit, niet-uitvoerbare opslag vereist | `VR-113`, nieuwe threat review |
|
||||
| T-21 | Back-up bevat secrets/PII | langdurig datalek | encryptie, gescheiden opslag, korte retentie, herstelprocedure | runbook en hersteltest |
|
||||
| T-22 | Supply-chainpackage/image | code execution | exacte versies/lockfile, beperkte dependencies, pinned baseimages/digests en scans gepland | CI, `VR-115` |
|
||||
| T-23 | Onbevoegde admin op thuisnetwerk | volledige datatoegang | uniek wachtwoord, TLS/VPN, geen defaultcredentials, sessiebeveiliging | Unraidchecklist |
|
||||
| T-24 | Source terms wijzigen | ongewenste voortgezette crawling | reviewdatum, source health en policy expiry gepland | `VR-102`, `VR-112` |
|
||||
| T-31 | Sollicitatiedossier-export bevat niet-gewenste payload | data-lek of onbedoeld dossieroverdragen | export bevat alleen het gekozen dossier, ZIP-inhoud is beperkt tot snapshot/tijdlijn/print-HTML, bestandsnaam is geslugified zonder padseparators | `tests/integration/test_applications.py` |
|
||||
|
||||
## Misbruikscenario's
|
||||
|
||||
### Vacature zegt “negeer eerdere instructies”
|
||||
|
||||
De tekst wordt gesanitized en als veldwaarde doorgegeven. Een eventuele AI-call krijgt een vast schema en geen tools. De output kan geen bron activeren, URL openen, harde regel verwijderen, mail sturen of sollicitatie indienen.
|
||||
|
||||
### Publieke pagina redirect naar `192.168.1.1`
|
||||
|
||||
De redirectlocatie wordt opnieuw gevalideerd. Het request naar het private adres wordt niet uitgevoerd; de bronrun krijgt een policy/securityfout en de bron kan in quarantaine gaan.
|
||||
|
||||
### E-mailalert bevat een platformlink en uitschrijflink
|
||||
|
||||
De adapter herkent vacaturekandidaten, negeert bekende unsubscribepatronen en slaat de platformlink hoogstens als alias op. Automatisch volgen vereist opnieuw een toegestane source policy; denylistplatformen worden geweigerd.
|
||||
|
||||
### Aanvaller laat scheduler dezelfde job herhalen
|
||||
|
||||
Canonieke sleutels, databaseconstraints, transacties, idempotente message identity en outboxuniciteit beperken duplicaten. Taken zijn ontworpen voor at-least-once in plaats van exact-once aannames.
|
||||
|
||||
## Security gates bij nieuwe functionaliteit
|
||||
|
||||
Een wijziging aan netwerk, mail, AI, rendering, auth, exports of uploads vereist:
|
||||
|
||||
1. nieuwe/gewijzigde threat in deze tabel;
|
||||
2. negatieve test die de aanval representeert;
|
||||
3. begrensde input/output en foutcategorie;
|
||||
4. logreview op secrets/PII;
|
||||
5. runbook- en configuratie-update;
|
||||
6. volledige `codex_verify`-gate.
|
||||
|
||||
## Resterende risico's
|
||||
|
||||
- Een applicatielaag kan DNS rebinding niet volledig uitsluiten zonder transportniveau-IP-pinning; dit staat in `VR-115`.
|
||||
- Brute-force of herhaalde foutieve mutatiepogingen blijven mogelijk, maar zijn beperkt door in-memory rate limiting en blokkades op login/manual-import.
|
||||
- Bronvoorwaarden vereisen menselijke/externe review per domein; automatisering kan dat niet juridisch beslissen.
|
||||
- Fuzzy dedupe en featureheuristieken kunnen inhoudelijk verkeerd zijn; provenance, feedback en benchmarkevaluaties beperken maar elimineren dit niet.
|
||||
- Een persoonlijke Unraid-host blijft afhankelijk van correcte netwerksegmentatie, patching en back-ups door de beheerder.
|
||||
|
||||
- T-25: Verloren of verlopen policyreview
|
||||
- Impact: bron kan op onbedoelde wijze doorgaan met crawlen
|
||||
- Mitigatie: `SourcePolicyReview` met decision/expiry + policy gate in `assess_url`
|
||||
- Verificatie: security/integration tests rond verlopen review in fail-closed gedrag
|
||||
|
||||
- T-26: Handmatige URL/tekstimport
|
||||
- Impact: ongewild ophalen van geblokkeerde of niet-ingesteste informatie, XSS via ingevulde tekst of overschrijding van limieten
|
||||
- Mitigatie: CSRF-only import endpoint, allowlist-domeinvrije policy checks, sanitize op tekstveld, byte-limiet en max-size/timeout gedrag
|
||||
- Verificatie: integration/security tests rond rejectie van geblokkeerde URL's en overschrijding van paste-limiet
|
||||
|
||||
- T-27: Reminder-flood of dubbele reminder-mails
|
||||
- Impact: gebruiker ontvangt herhaalde of ongewenste herinneringen
|
||||
- Mitigatie: stabiele `dedupe_key`, idempotente outbox met statusmachine, `reminder_type`-types en task-driven verzending
|
||||
- Verificatie: `tests/integration/test_reminders.py`
|
||||
|
||||
- T-28: Onterecht verzenden zonder ontvanger/uitgeschakelde reminderinstelling
|
||||
- Impact: onnodige fout- of testmails, verwarring over afmeldgedrag
|
||||
- Mitigatie: master- en topmatchschakelaar per profiel, `SKIPPED`-status bij lege ontvanger en foutlogica met retries op taakniveau
|
||||
- Verificatie: `tests/integration/test_reminders.py`
|
||||
|
||||
- T-29: Bronhealth zet bronnen te vroeg in quarantaine bij incidentele netwerkruis
|
||||
- Impact: tijdelijke uitval of incidentele bronfouten blokkeren een gezonde bron
|
||||
- Mitigatie: duidelijke foutcategoriedrempels, gescheiden policy/security- en tijdelijke-foutlogica, en canaryherstel via trialpad
|
||||
- Verificatie: `tests/integration/test_source_health.py`
|
||||
|
||||
- T-30: Geautomatiseerde canaryherstel blijft onbegrensd draaien
|
||||
- Impact: herhaalde herstarts van dezelfde bron veroorzaken extra belasting
|
||||
- Mitigatie: cooldown plus metadataflag voor eenmalige canary, en trialstatus met beperkte runtrigger
|
||||
- Verificatie: `tests/integration/test_source_health.py`
|
||||
@@ -0,0 +1,91 @@
|
||||
# Traceabilitymatrix
|
||||
|
||||
Legenda: **I** = geïmplementeerd en getest in de huidige MVP; **P** = gedeeltelijk/volgende backlogtaak; **E** = alleen externe productieconfiguratie of bronreview ontbreekt. Bestandsnamen verwijzen naar de primaire implementatie; aanvullende tests kunnen in dezelfde testdirectory staan.
|
||||
|
||||
## Functionele requirements
|
||||
|
||||
| ID | Status | Primaire implementatie | Bewijs/tests | Vervolgtaak |
|
||||
|---|---|---|---|---|
|
||||
| PR-001 | I | `profiles/models.py`, forms/views | `test_profiles.py`, `test_views.py` | — |
|
||||
| PR-002 | I | `SearchProfile`, defaults/form, geocodeparser | `tests/unit/test_geocoding.py`, `tests/integration/test_import_geodata.py`, profiel- en viewtests | VR-105 verfijnt afstand |
|
||||
| PR-003 | I | `ProfileRevision`, profile services | `test_profiles.py` | — |
|
||||
| PR-004 | I | begrensde feedbackservice/velden | `tests/unit/test_feedback_learning.py`, `test_profiles.py` | VR-110 uitgevoerd |
|
||||
| PR-005 | I | `bootstrap_instance`, defaults | task/viewtests | — |
|
||||
| PR-010 | I | `sources/models.py` | `test_source_models.py` | VR-112 breidt health uit |
|
||||
| PR-011 | I | policy + fetch task | policy/fetch/tasktests | — |
|
||||
| PR-012 | I | denylist, e-mailadapter | policy/e-mailtests | — |
|
||||
| PR-013 | I | `url_security.py`, redirectfetcher | security URL/fetchtests | VR-115 transporthardening |
|
||||
| PR-014 | I | `fetcher.py` | `test_fetcher.py` | — |
|
||||
| PR-015 | I | `SourceLease`, `SourceOriginState`, `apps/sources/services/scheduling.py`, `apps/sources/tasks.py` | `test_scheduling.py`, `test_fetcher.py`, `test_tasks_and_more_views.py` | `VR-103` afgerond |
|
||||
| PR-016 | I | `email_import.py`, recordmodel | e-mailintegratietests | — |
|
||||
| PR-017 | I | candidate/status + basisdiscovery | `apps/sources/services/discovery.py`, `tests/unit/test_discovery_rss.py`, `tests/integration/test_discovery_command.py`, `tests/integration/test_tasks_and_more_views.py` | VR-101/102 |
|
||||
| PR-018 | I | `apps/sources/services/health.py`, `apps/sources/tasks.py` | `tests/integration/test_source_health.py` | VR-112 |
|
||||
| PR-020 | I | `JsonLdJobPostingAdapter` | JSON-LD-tests/fixtures | — |
|
||||
| PR-021 | I | HTML/RSS/e-mailadapters + apps/sources/adapters/ats.py + tests/unit/test_ats_adapters.py | adaptertests + tests/unit/test_ats_adapters.py | VR-106 afgerond |
|
||||
| PR-022 | I | `adapters/base.py` | contract via alle adaptertests | — |
|
||||
| PR-023 | I | `sanitize.py` | security sanitizationtests | — |
|
||||
| PR-024 | I | `ExtractedJob`, `CanonicalJobDraft`, models | pipeline/normalisatietests | — |
|
||||
| PR-025 | I | `JobVersion` in pipeline | dedupe/lifecycle-integratietest | — |
|
||||
| PR-026 | I | `RawDocument`, `apps/sources/tasks.py`, `config/settings.py` | `tests/integration/test_source_health.py` | VR-112/115 operationaliseert |
|
||||
| PR-027 | I | `FieldProvenance` | pipeline-integratietests | — |
|
||||
| PR-030 | I | `dedupe.py` | dedupetests | — |
|
||||
| PR-031 | I | conservatieve fuzzy dedupe | `tests/unit/test_dedupe_lifecycle_feedback.py`, `scripts/benchmark.py`, `tests/benchmark/test_benchmark.py` | VR-116 benchmark |
|
||||
| PR-032 | I | direct-sourcepreferentie/alias | pipeline- en dedupetests | VR-108 resolver |
|
||||
| PR-033 | I | canonical constraints/transacties | replaytests | — |
|
||||
| PR-040 | I | features/scoring | scoringtests | — |
|
||||
| PR-041 | I | scorecomponenten | `tests/unit/test_features_distance.py`, `tests/unit/test_scoring.py` | VR-105 afgerond |
|
||||
| PR-042 | I | unknown/confidencegedrag | `tests/unit/test_features_distance.py`, `tests/unit/test_scoring.py` | VR-105 |
|
||||
| PR-043 | I | `ScoreRun` | pipeline/scoringtests | — |
|
||||
| PR-044 | I | profiel/modelversievelden | scoring/profiletests | VR-109 promptversie |
|
||||
| PR-045 | I | optionele Ollamaservice/fallback | `test_ai.py` | VR-109 |
|
||||
| PR-046 | I | geen tools + guardrails | AI/securitytests | VR-109/115 evaluaties |
|
||||
| PR-050 | I | dashboard view/template | viewtests | VR-114 e2e/a11y |
|
||||
| PR-051 | I | detailview/template/provenance | viewtests | VR-114 |
|
||||
| PR-052 | I | feedbackendpoint/UI | feedback/viewtests | — |
|
||||
| PR-053 | I | applicationmodel/serviceflow | `tests/integration/test_dedupe_lifecycle_feedback.py`, `tests/integration/test_applications.py` | VR-113 uitgebreid dossier |
|
||||
| PR-054 | I | `notifications/models.py`, outbox/services/tasks | `tests/integration/test_email_import_notifications.py`, `tests/integration/test_reminders.py` | VR-111 |
|
||||
| PR-055 | I | lifecycle service/task | lifecycletests | — |
|
||||
| PR-056 | I | expliciete non-goal; geen submitcontract | AC-043 code review, ADR-0006 | — |
|
||||
| PR-060 | I | health views/service | view/healthtests | — |
|
||||
| PR-061 | I | `apps/sources/services/health.py`, `apps/sources/tasks.py` | `tests/integration/test_source_health.py` | VR-112 dashboard/metrics |
|
||||
| PR-062 | I | `scripts/release_verify.sh`, `scripts/release_smoke.py`, `docker-compose*.yml` | `release-verificatie-smoke`, `configdiff` en upgrade/rollbackartefacten | VR-117 afgerond |
|
||||
| PR-063 | I | AI standaard uit/fallback | volledige testset zonder Ollama | — |
|
||||
| PR-064 | I | settings/env-example/gitignore | configreview en CI | VR-115 secretscan |
|
||||
|
||||
## Niet-functionele requirements
|
||||
|
||||
| ID | Status | Bewijs | Vervolgtaak |
|
||||
|---|---|---|---|
|
||||
| NFR-001 | I | policy-, URL- en fetchersecuritytests + settings/rate-limit tests | `tests/unit/test_fetcher.py`, `tests/unit/test_rate_limit.py`, `tests/unit/test_settings_security.py`, `tests/integration/test_views.py`, `tests/integration/test_sources_manual_import.py` |
|
||||
| NFR-002 | P | raw-retentie, minimale modellen, privacyrunbooks | VR-113/115 |
|
||||
| NFR-003 | I/P | replay-, mail- en outboxtests; per-bronfouten | VR-103/112 |
|
||||
| NFR-004 | I | ScoreRun, provenance, UI-copy | VR-109 uitgebreid evidence |
|
||||
| NFR-005 | I | domeinapps/services/adapters/ADRs | continue review |
|
||||
| NFR-006 | I | offline pytest + coveragegate | VR-114 verbreedt e2e |
|
||||
| NFR-007 | P | `scripts/benchmark.py`, `tests/benchmark/test_benchmark.py` | VR-116 |
|
||||
| NFR-008 | I | semantische templates/designspec | `tests/e2e/test_vr114_browser_and_accessibility.py` |
|
||||
| NFR-009 | I | lokale SQLite en Docker/Unraidconfig, release-smoke en restore | VR-117 afgerond |
|
||||
| NFR-010 | I | `apps/sources/services/health.py`, `apps/sources/tasks.py`, `apps/core/views.py`, `config/settings.py` | `tests/integration/test_source_health.py` | VR-112 observability |
|
||||
|
||||
## Systeemacceptatie
|
||||
|
||||
| Criteria | Status | Primair bewijs |
|
||||
|---|---|---|
|
||||
| AC-001–004 | I | bootstrap, auth/UI, e-mailingress zonder platformlogin |
|
||||
| AC-010–015 | I | integration/security/adaptertests |
|
||||
| AC-020–025 | I | normalisatie-, dedupe-, scoring- en AI-fallbacktests |
|
||||
| AC-030–034 | I | notificatie- en lifecycletests |
|
||||
| AC-040–043 | I | application/feedback/user-scopingtests en ADR-0006 |
|
||||
| AC-050–054 | I | health/viewtests, verify-script, offline fixtures, coverage en ledger-validator |
|
||||
| AC-055 | I | `scripts/release_verify.sh` met release-smoke en restore-compare | VR-117 afgerond |
|
||||
|
||||
## Onderhoudsregel
|
||||
|
||||
Iedere backlogtaak die een requirement wijzigt, werkt in dezelfde commit deze matrix, relevante acceptatiecriteria en tests bij. Een status wordt alleen naar **I** gezet wanneer de code én het genoemde bewijs bestaan.
|
||||
|
||||
## Aanvullende traceability-opmerking
|
||||
- VR-102 is gerealiseerd met auditbare policyreviews (`SourcePolicyReview`), robotscache met TTL/size/SSRF-controles en fail-closed policy-gating op verlopen of conflicterende reviews.
|
||||
- VR-107 is gerealiseerd met `apps/sources/views.py`, `apps/sources/services/manual_import.py`, `templates/sources/list.html`, `static/js/manual_import.js` en bijhorende unit/integration-tests.
|
||||
- VR-115 is afgerond met productie-hardening voor `config/settings.py`, DNS-rebindmitigatie in `apps/sources/services/fetcher.py`, rate limiting op login en handmatige import, CI-secret- en kwetsbaarheidsscans, non-root/read-only containerhardening en bijbehorende security/unit/integration-tests.
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user