5 Commits
Author SHA1 Message Date
Jens 3cfb2260cc Fix immutable release bootstrap for 0.3.17
release / verify-publish (push) Successful in 6m13s
deploy / deploy (push) Canceled after 0s
2026-08-12 13:35:02 +02:00
Jens b5045cf487 Document 0.3.16 deployment evidence
deploy / deploy (push) Canceled after 0s
2026-08-12 12:04:50 +02:00
Jens a6c8a79e31 Harden public demo for 0.3.16
release / verify-publish (push) Failing after 6s
deploy / deploy (push) Canceled after 0s
2026-08-12 11:42:00 +02:00
Jens 2419e6bf01 Update README for 0.3.15 release
deploy / deploy (push) Canceled after 0s
2026-08-12 11:00:50 +02:00
Jens 3ab9127548 Record live mail and registry acceptance
deploy / deploy (push) Canceled after 0s
2026-07-30 02:53:38 +02:00
20 changed files with 376 additions and 49 deletions
+3 -1
View File
@@ -10,8 +10,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
- name: Install pinned uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
version: "0.11.12"
python-version: "3.13"
- name: Install verification browser
run: |
+17
View File
@@ -2,6 +2,23 @@
Alle betekenisvolle wijzigingen worden hier bijgehouden. Het project volgt voorlopig een pre-1.0 semantische versieaanpak.
## 0.3.17 - 2026-08-12
### Hersteld
- De Gitea-releasebootstrap gebruikt een immutable `setup-uv`-commit en een exacte
uv-versie; downloads lopen via de Astral-mirror en zijn niet langer afhankelijk
van de gelimiteerde GitHub Releases-API op het gedeelde runner-IP.
## 0.3.16 - 2026-08-12
### Hersteld
- De publieke demo herstelt bij iedere nieuwe demosessie zes actuele fictieve vacatures, hun laatste scores en twee toekomstige opvolgmomenten zonder duplicaten of onbeperkte scorehistoriek.
- Negen fictieve `example.test`-platformmailboxen maken de mailboxflow zichtbaar zonder echte accounts, credentials of externe polls.
- Mailboxingress wordt uitsluitend door de mailboxscheduler verwerkt; e-mailbronnen komen niet meer in URL-fetchplanning, dashboardscanstatus of de algemene bronrunactiviteit terecht.
- Statuscopy onderscheidt een actief zoekprofiel van operationele brongezondheid en het lege loginwachtwoordveld lijkt niet langer vooraf ingevuld.
## 0.3.14 - 2026-07-29
### Gewijzigd
+25 -4
View File
@@ -8,6 +8,13 @@
VacatureRadar is een persoonlijke vacature-assistent voor België. De applicatie volgt toegestane werkgeversbronnen, ATS-paginas, feeds en vacaturemails, verwijdert dubbels en irrelevante resultaten en rangschikt de overblijvende kansen tegen jouw zoekprofiel. Jij behoudt altijd de controle: VacatureRadar helpt zoeken en opvolgen, maar solliciteert nooit automatisch.
**Actuele release: 0.3.17.** De volledige P0/P1-releasebasis en de publieke
demo-hardening zijn afgerond: 327 tests zijn groen, Microsoft 365 verwerkt platformmails via
app-only OAuth/XOAUTH2. Release 0.3.17 herstelt bovendien de reproduceerbare
private-registrypublicatie zonder afhankelijkheid van de GitHub Releases-API.
Het controleerbare acceptatiebewijs staat in
[Finale acceptance](docs/audit/FINAL_ACCEPTANCE.md).
![Dagoverzicht van VacatureRadar in licht thema](docs/screenshots/readme/dashboard-light.png)
## Voor wie is dit?
@@ -37,12 +44,15 @@ VacatureRadar is bedoeld voor één persoon of een eigen, afgeschermde installat
| Vacatureverkenner | Zoeken en filteren met een uitlegbare detailanalyse naast de resultaten |
| Skillsradar | Actuele marktvraag naast de skills uit je eigen profiel |
| Sollicitatiepipeline | Dossiers, status, notities, deadlines en een auditbare tijdlijn |
| Bronnenbeheer | Veilige bronconfiguratie, gezondheid, handmatige import en mailboxkoppelingen |
| Bronnenbeheer | Veilige bronconfiguratie, gezondheid, handmatige import en afzonderlijke mailboxkoppelingen voor negen jobplatformen |
| Automatisering | Eerlijke 24-uursmetingen; niet-gemeten informatie blijft expliciet onbekend |
## Bekijk de volledige productflow
De beelden komen uit de lokale demo-omgeving. De getoonde cijfers zijn omgevingsdata, geen marketingclaims. Klik op een beeld voor de volledige resolutie.
De beelden komen uit een gesaneerde lokale demo-omgeving. De getoonde aantallen
zijn een momentopname van die demo en bewijzen geen productie-status; daarvoor
geldt uitsluitend de gedateerde [finale acceptance](docs/audit/FINAL_ACCEPTANCE.md).
Klik op een beeld voor de volledige resolutie.
### 1. Vind de relevante kansen
@@ -78,7 +88,7 @@ Een analyse maakt score, datakwaliteit, beslissignalen, aandachtspunten en broni
<table>
<tr>
<td width="50%"><strong>Sollicitatiepipeline</strong><br><sub>Bewaar status, deadlines, notities en tijdlijn per dossier; de uiteindelijke sollicitatie blijft altijd handmatig.</sub></td>
<td width="50%"><strong>Bronnenbeheer</strong><br><sub>Zie policyreview, laatste scans, volgende runs en brongezondheid zonder technische logs te moeten doorzoeken.</sub></td>
<td width="50%"><strong>Bronnenbeheer</strong><br><sub>Zie policyreview, laatste scans, volgende runs, brongezondheid en de status van iedere platformmailbox zonder technische logs te moeten doorzoeken.</sub></td>
</tr>
<tr>
<td><a href="docs/screenshots/readme/applications-dark.png"><img src="docs/screenshots/readme/applications-dark.png" alt="Sollicitatiepipeline met persoonlijke dossiers"></a></td>
@@ -141,7 +151,7 @@ Na de eerste login:
1. Open **Zoekprofiel** en stel regio, radius, rollen, skills en werkvoorkeuren in.
2. Open **Bronnen** en activeer alleen bronnen waarvan beleid en voorwaarden beoordeeld zijn.
3. Configureer optioneel een eigen vacaturemailbox; credentials worden niet in de repository opgeslagen.
3. Koppel optioneel afzonderlijke vacaturemailboxen voor VDAB, Indeed, LinkedIn, ictjob.be, Jobat, StepStone, Careerjet, Randstad en Robert Half. Credentials worden niet in de repository opgeslagen.
4. Controleer **Automatisering** voordat je op onbemande verwerking vertrouwt.
5. Verifieer in productie altijd HTTPS, back-ups, herstel en healthchecks.
@@ -158,6 +168,12 @@ De publieke URL wordt vervolgens gebruikt voor `ALLOWED_HOSTS`, CSRF, secure coo
Publiceer poort `1226` niet rechtstreeks op internet. Gebruik een beheerde reverse proxy met TLS en volg [Unraid-deployment](docs/operations/UNRAID_DEPLOYMENT.md) en de concrete [Nginx Proxy Manager-handleiding](docs/operations/NGINX_PROXY_MANAGER.md). Een volledig productievoorbeeld staat in [`deployment/production.env.example`](deployment/production.env.example).
Voor Microsoft 365 gebruikt VacatureRadar uitsluitend app-only OAuth/XOAUTH2
voor IMAP en SMTP; Basic Auth en Microsoft 365-app-wachtwoorden worden niet
ondersteund. Beperk de Entra-serviceprincipal tot de bedoelde mailbox en geef
alleen `IMAP.AccessAsApp` en `SMTP.SendAsApp`. De exacte configuratie- en
rotatiestappen staan in het [operationele runbook](docs/operations/RUNBOOK.md).
## Eerlijke grenzen
VacatureRadar:
@@ -193,6 +209,11 @@ Die controleert formatting, lint, Django-configuratie, migraties, de volledige t
## Beheer en deployment
Productiereleases worden door Gitea Actions gebouwd, gescand en als private
containerimage gepubliceerd. Pin Unraid op `versie@sha256:digest`, bewaar de
vorige digest voor rollback en gebruik nooit alleen een veranderlijke tag als
productiereferentie.
- [Lokaal ontwikkelen](docs/operations/LOCAL_DEVELOPMENT.md)
- [Docker en Unraid uitrollen](docs/operations/UNRAID_DEPLOYMENT.md)
- [Nginx Proxy Manager configureren](docs/operations/NGINX_PROXY_MANAGER.md)
+3 -2
View File
@@ -8,7 +8,7 @@ from django.urls import reverse
from apps.notifications.models import DigestOutbox, ReminderOutbox
from apps.profiles.models import ProfileRevision
from apps.sources.models import SourceRun
from apps.sources.models import Source, SourceRun
from ..models import ApplicationTimelineEvent, Feedback, JobVersion
@@ -37,7 +37,8 @@ ACTIVITY_FILTERS = (
def _source_events() -> list[ActivityEvent]:
events = []
for run in SourceRun.objects.select_related("source").order_by("-started_at")[:100]:
runs = SourceRun.objects.exclude(source__source_type=Source.Type.EMAIL)
for run in runs.select_related("source").order_by("-started_at")[:100]:
detail = (
f"{run.extracted_count} geëxtraheerd · {run.created_count} nieuw · "
f"{run.duplicate_count} duplicaat"
+5 -2
View File
@@ -57,7 +57,8 @@ def annotate_latest_profile_score(
def _recent_dashboard_activity(user: User) -> list[dict[str, Any]]:
activity: list[dict[str, Any]] = []
for run in SourceRun.objects.select_related("source").order_by("-started_at")[:4]:
source_runs = SourceRun.objects.exclude(source__source_type=Source.Type.EMAIL)
for run in source_runs.select_related("source").order_by("-started_at")[:4]:
activity.append(
{
"occurred_at": run.started_at,
@@ -154,7 +155,9 @@ def build_dashboard_cockpit(user: User, profile: SearchProfile | None) -> dict[s
"source_counts": source_counts,
"recent_activity": _recent_dashboard_activity(user),
"day_conclusion": day_conclusion,
"latest_scan": SourceRun.objects.order_by("-started_at").first(),
"latest_scan": SourceRun.objects.exclude(source__source_type=Source.Type.EMAIL)
.order_by("-started_at")
.first(),
}
+50 -7
View File
@@ -122,11 +122,10 @@ def demo_environment_seeded(user) -> bool:
def seed_demo_environment(user) -> None:
"""Maak (idempotent) een fictieve demo-omgeving voor ``user``."""
if demo_environment_seeded(user):
return
"""Maak of herstel een fictieve, tijdsbestendige demo-omgeving voor ``user``."""
from apps.jobs.models import Application, Employer, JobPosting, ScoreRun
from apps.sources.models import MailboxConnection, Source, SourceRun
from apps.sources.platforms import PLATFORM_ALERTS
profile, _ = SearchProfile.objects.get_or_create(
user=user,
@@ -185,7 +184,7 @@ def seed_demo_environment(user) -> None:
},
)
slug = _digest("demo-job", str(index), title, employer_name)
job, job_created = JobPosting.objects.get_or_create(
job, _ = JobPosting.objects.update_or_create(
canonical_key=slug,
defaults={
"employer": employer,
@@ -221,6 +220,11 @@ def seed_demo_environment(user) -> None:
)
created_jobs.append(job)
ScoreRun.objects.filter(
job=job,
profile=profile,
model_version="demo-seed",
).delete()
ScoreRun.objects.create(
job=job,
profile=profile,
@@ -244,7 +248,7 @@ def seed_demo_environment(user) -> None:
# Een kleine sollicitatiepijplijn voor de demo.
if created_jobs:
Application.objects.get_or_create(
Application.objects.update_or_create(
user=user,
job=created_jobs[0],
defaults={
@@ -257,7 +261,7 @@ def seed_demo_environment(user) -> None:
},
)
if len(created_jobs) > 3:
Application.objects.get_or_create(
Application.objects.update_or_create(
user=user,
job=created_jobs[3],
defaults={
@@ -267,3 +271,42 @@ def seed_demo_environment(user) -> None:
"notes": "Sollicitatie verstuurd. Demo-data.",
},
)
# Platformmailboxen zijn zichtbare ingressconfiguratie, geen crawlbare URL-bronnen.
# De demo gebruikt uitsluitend fictieve adressen en wordt door de scheduler overgeslagen.
for platform in PLATFORM_ALERTS:
MailboxConnection.objects.update_or_create(
user=user,
platform=platform,
defaults={
"provider": MailboxConnection.Provider.OUTLOOK,
"username": "alerts@example.test",
"mailbox": f"VacatureRadar/{platform}",
"enabled": True,
"poll_interval_minutes": MailboxConnection.PollInterval.THIRTY_MINUTES,
"next_poll_at": now + timedelta(minutes=30),
"last_polled_at": now - timedelta(minutes=5),
"last_success_at": now - timedelta(minutes=5),
"last_error_category": "",
"last_error_message": "",
"lease_token": "",
"lease_expires_at": None,
},
)
source, _ = Source.objects.update_or_create(
source_type=Source.Type.EMAIL,
domain=f"{platform}.mailbox.local",
defaults={
"name": f"{PLATFORM_ALERTS[platform].label} vacaturemailbox",
"base_url": "",
"status": Source.Status.ACTIVE,
"policy": Source.Policy.ALLOW,
"policy_reason": "Mailboxingress; niet via URL-fetch gepland.",
"parser_key": "email-alert",
"failure_count": 0,
"last_failure_at": None,
"next_run_at": None,
"metadata": {"demo": True, "mailbox_ingress": True},
},
)
SourceRun.objects.filter(source=source).delete()
+3 -1
View File
@@ -188,7 +188,8 @@ def schedule_due_sources() -> dict[str, int]:
now = timezone.now()
due = Source.objects.filter(
status__in=[Source.Status.ACTIVE, Source.Status.TRIAL],
).filter(Q(next_run_at__isnull=True) | Q(next_run_at__lte=now))
).exclude(source_type=Source.Type.EMAIL)
due = due.filter(Q(next_run_at__isnull=True) | Q(next_run_at__lte=now))
ids = list(due.values_list("id", flat=True).distinct()[:200])
for source_id in ids:
fetch_source.delay(source_id)
@@ -200,6 +201,7 @@ def schedule_mailbox_polls() -> dict[str, int]:
now = timezone.now()
due_ids = list(
MailboxConnection.objects.filter(enabled=True, next_poll_at__lte=now)
.exclude(user__username=settings.DEMO_USERNAME)
.filter(Q(lease_expires_at__isnull=True) | Q(lease_expires_at__lte=now))
.values_list("id", flat=True)[:100]
)
+1 -1
View File
@@ -139,7 +139,7 @@ CSRF_TRUSTED_ORIGINS = merge_unique(
)
TIME_ZONE = os.getenv("DJANGO_TIME_ZONE", "Europe/Brussels")
VACATURERADAR_OWNER_NAME = os.getenv("VACATURERADAR_OWNER_NAME", "Jens").strip()[:40] or "Jens"
VACATURERADAR_VERSION = os.getenv("VACATURERADAR_VERSION", "0.3.14").strip() or "0.3.14"
VACATURERADAR_VERSION = os.getenv("VACATURERADAR_VERSION", "0.3.17").strip() or "0.3.17"
LANGUAGE_CODE = "nl-be"
USE_I18N = True
USE_TZ = True
+1 -1
View File
@@ -9,7 +9,7 @@ DJANGO_ALLOWED_HOSTS=vacatureradar.example.be,127.0.0.1,localhost
DJANGO_CSRF_TRUSTED_ORIGINS=https://vacatureradar.example.be
DJANGO_TIME_ZONE=Europe/Brussels
VACATURERADAR_OWNER_NAME=Jens
VACATURERADAR_VERSION=0.3.14
VACATURERADAR_VERSION=0.3.17
POSTGRES_DB=vacatureradar
POSTGRES_USER=vacatureradar
+80 -11
View File
@@ -1,6 +1,6 @@
schema_version: 1
project: VacatureRadar
updated_at: '2026-07-29'
updated_at: '2026-08-12'
execution_policy:
order: file-order, then first ready task whose dependencies are done
no_product_questions: true
@@ -1341,7 +1341,7 @@ tasks:
gebruikt strikt de gepinde certifi-bundel zonder bypass.
- id: VR-202
title: Live IMAP- en SMTP-credentials configureren
status: blocked-external
status: done
priority: P0
requirement_ids:
- PR-016
@@ -1361,17 +1361,19 @@ tasks:
- Dezelfde live testmail importeert per mailbox idempotent en alle geconfigureerde intervallen worden door de
scheduler gerespecteerd.
- SMTP verstuurt één testdigest en outboxstatus klopt.
- OAuth-clientcredentials en de encryptiesleutel staan alleen in de productie-secretstore/.env; eventuele
Gmail/custom-app-wachtwoorden zijn als ciphertext opgeslagen en sleutelrotatie is live getest.
- OAuth-clientcredentials en de encryptiesleutel staan alleen in de productie-secretstore/.env; eventuele Gmail/custom-app-wachtwoorden
zijn als ciphertext opgeslagen en sleutelrotatie is live getest.
verification:
- Volg Unraid mailboxchecklist
- Inspecteer logs op afwezigheid van secret/payload
primary_paths:
- /mnt/user/appdata/vacatureradar/config/.env
- /mnt/user/appdata/VacatureRadar/.env
- Bronnen -> Platformmailboxen
external_blocker: De dedicated productie-alias, Entra-appregistratie, admin consent en Exchange-mailboxscope zijn
op 2026-07-30 live ingericht; resterend zijn de productie-uitrol en echte platformalertmails voor de
import-/digestproef.
result:
completed_at: '2026-07-30'
note: M365 app-only OAuth/XOAUTH2 live ingericht met dedicated alias/map en Exchange-regel; negen platformmailboxen
importeerden dezelfde live testmail idempotent, schedulerstatus was correct, testdigest stond sent en logs
bleven payload-/secretvrij.
- id: VR-203
title: Productiedomein, TLS en reverse proxy activeren
status: done
@@ -1403,7 +1405,7 @@ tasks:
containerintern bereikbaar.
- id: VR-204
title: Immutable containerimage publiceren
status: blocked-external
status: done
priority: P1
requirement_ids:
- NFR-009
@@ -1421,9 +1423,12 @@ tasks:
- Pull image op schone host
- Start smoke stack en rollback naar vorige digest
primary_paths:
- .github/workflows/release.yml
- .gitea/workflows/release.yml
- docker-compose.unraid.yml
external_blocker: Vereist registrykeuze, account en credentials.
result:
completed_at: '2026-07-30'
note: Gitea private registry publiceerde v0.3.15 als immutable digest met commitlabel, SPDX-SBOM, checksums
en release notes; Unraid pull, digest-pin, healthy smoke en rollback naar 0.3.14 plus herstel zijn live geslaagd.
- id: VR-205
title: Gelicenseerde Belgische geodatabron laden
status: done
@@ -2371,3 +2376,67 @@ tasks:
note: 'Release 0.3.14: geïsoleerde PostgreSQL-benchmark reduceert importduur 44,74% en queries minstens 63,73%;
twee volledige gates met 318 tests groen; productie healthy op image sha256:1ee84241, transactionele importsmoke
teruggerold en rollbackimage 0.3.13 beschikbaar.'
- id: VR-229
title: Herstel de publieke demo na de visuele 0.3.15-audit
status: done
priority: P1
requirement_ids:
- NFR-008
- NFR-009
depends_on:
- VR-228
summary: Maak de publieke demo opnieuw representatief en tijdsbestendig, scheid mailboxingress van URL-fetching
en laat operationele statussen exact zeggen wat ze meten.
acceptance_criteria:
- Iedere nieuwe demosessie herstelt zes fictieve actuele vacatures, scores en toekomstige opvolgdatums zonder
duplicaten of groeiende ScoreRun-historiek.
- De demo toont negen fictieve, actieve platformmailboxkoppelingen zonder secrets of echte persoonsgegevens.
- E-mailbronnen worden nooit door de generieke URL-fetchscheduler gepland en oude mailboxpolicyfouten vervuilen
dashboard en activiteit niet meer.
- Navigatie en dashboard noemen het zoekprofiel expliciet actief zonder brongezondheid te suggereren; het lege
loginwachtwoordveld lijkt niet vooraf ingevuld.
- De negen kernroutes blijven in dark/light en op mobiel, desktop en ultrawide zonder overflow, consolefouten
of headingregressies bruikbaar; de volledige kwaliteitsgate slaagt.
verification:
- python -m pytest tests/integration/test_auth_login.py tests/integration/test_tasks_and_more_views.py
- Browseraudit op 390x844, 1280x600, 1440x900 en 3440x1440
- ./scripts/codex_verify.sh
primary_paths:
- apps/jobs/services/demo_seed.py
- apps/jobs/services/activity.py
- apps/jobs/services/cockpit.py
- apps/sources/tasks.py
- templates/base.html
- templates/dashboard/today.html
- templates/registration/login.html
- tests/integration/test_auth_login.py
- tests/integration/test_tasks_and_more_views.py
result:
completed_at: '2026-08-12'
note: Publieke demo wordt bij iedere sessie idempotent hersteld met 6 actuele fictieve vacatures, tijdsbestendige
opvolging en 9 fictieve mailboxkoppelingen; EMAIL-bronnen en demo-mailboxen zijn uit de verkeerde schedulers
en activityfeeds uitgesloten, statuscopy en loginplaceholder zijn gecorrigeerd. Gerichte 51 tests en volledige
gate met 327 tests groen, 84,04% dekking.
- id: VR-230
title: Herstel de immutable Gitea-releasepublicatie
status: ready
priority: P0
requirement_ids:
- NFR-009
depends_on:
- VR-229
summary: Verwijder de externe GitHub-API-rate-limit uit de releasebootstrap en publiceer de actuele productiebuild
opnieuw als aantoonbaar immutable private-registryimage.
acceptance_criteria:
- De uv-bootstrap is op een immutable actioncommit en een exacte uv-versie gepind.
- De releaseworkflow gebruikt voor uv-downloads geen GitHub Releases-API die op een gedeeld runner-IP kan limiteren.
- Gitea Actions publiceert een private registryimage met digest, SBOM, checksums en release notes.
- Unraid is op de gepubliceerde digest gepind en live health/readiness slagen.
verification:
- uv run pytest tests/unit/test_deployment_configuration.py
- ./scripts/codex_verify.sh
- Gitea Actions releasebewijs en live Unraid health/readiness
primary_paths:
- .gitea/workflows/release.yml
- tests/unit/test_deployment_configuration.py
- docs/audit/FINAL_ACCEPTANCE.md
+69 -11
View File
@@ -1,7 +1,7 @@
# Projectstatus
- Laatst bijgewerkt: 2026-07-29
- Repositoryversie: 0.3.14 importperformance
- Laatst bijgewerkt: 2026-08-12
- Repositoryversie: 0.3.16 publieke demo-hardening
- Uitvoeringsmodus: autonome backlog
- Eerste uitvoerbare taak: controleer met `python scripts/backlog.py next`
@@ -19,6 +19,49 @@ De repository bevat een uitvoerbare Django-MVP met:
- Celeryworker/scheduler, healthchecks, Docker/Unraidconfiguratie en back-up-/herstelscripts;
- offline unit-, integratie- en securitytests.
## Laatste documentatie-update
Visuele demo-hardening `VR-229` op 2026-08-12:
- iedere publieke demosessie herstelt idempotent zes actuele fictieve vacatures,
de laatste scores, twee toekomstige opvolgmomenten en negen geïsoleerde
`example.test`-platformmailboxkoppelingen;
- mailboxbronnen worden niet meer door de URL-fetchscheduler gepland, de
publieke demo wordt uitgesloten van echte mailboxpolling en historische
mailboxpolicyfouten vervuilen dashboard en activiteit niet langer;
- navigatie en dashboard onderscheiden nu expliciet een actief zoekprofiel van
bron- of systeemgezondheid; het lege loginwachtwoordveld heeft geen misleidende
mask-placeholder meer;
- visuele audit: negen kernroutes groen in dark/light op 390×844, 1280×600,
1440×900 en 3440×1440 zonder overflow, heading- of consolefouten;
- gerichte regressies: 51 tests groen; volledige gate: 327 tests, 0 failures,
0 skips en 84,04% branch-aware dekking;
- commit `a6c8a79` en tag `v0.3.16` zijn naar Gitea gepusht; vóór uitrol is onder
`/mnt/user/appdata/VacatureRadar/local/backups/release-0.3.16-20260812T094608Z`
een checksum-geldige database- en configuratieback-up gemaakt;
- Unraid draait healthy zonder restarts op lokaal gebouwde image
`sha256:c98001c063e8358dc59dfc175c574187b36021dd5a81f44e3ba982a28c9f9341`;
een live sessiesmoke bevestigde dashboard 200, zes demomatches, negen
platformmailboxen, actieve ingress, nul oude policyfouten en geen
wachtwoord-maskplaceholder;
- de Gitea-releaseworkflow faalde tweemaal in stap `astral-sh/setup-uv@v6` en
publiceerde daarom nog geen 0.3.16-registrymanifest. De bewezen immutable
registryrelease blijft 0.3.15; runnerdiagnose en herpublicatie vereisen opnieuw
bereikbare Unraid-beheertoegang. Volgende uitvoerbare backlogtaak: geen.
README-update op 2026-07-30:
- de repository-ingang benoemt release 0.3.15, de groene 324-testgate, de live
M365 app-only OAuth/XOAUTH2-flow en de immutable private Gitea-registryrelease;
- eerste configuratie noemt alle negen ondersteunde jobplatformen en verwijst
expliciet naar het M365 least-privilege- en rotatiepad in het runbook;
- de bestaande negen screenshots zijn visueel herbeoordeeld. De layout is nog
actueel; de bronafbeelding toont bewust een gesaneerde demo zonder actieve
ingress en is daarom als demo-momentopname verduidelijkt, niet vervangen door
een productiebeeld met accountgegevens;
- eindverificatie: volledige gate groen met 324 tests, 0 failures, 0 skips en
84,07% branch-aware dekking.
## Laatste productiehardening
Productieoplevering 0.3.14 op 2026-07-29:
@@ -30,9 +73,28 @@ Productieoplevering 0.3.14 op 2026-07-29:
- de pre-releasecustomdump, media en composeconfig staan checksum-geldig onder `/mnt/user/appdata/VacatureRadar/backups/release-0.3.14-20260729T164500Z`; rollbackimage `vacatureradar:rollback-0.3.13` wijst naar `sha256:34aef8853f77a3f311b119bd67d9f1160a97fd4a21e62c9776222be289bf3b88`;
- productie is healthy zonder restarts, versie 0.3.14 is publiek zichtbaar, alle gastkernroutes antwoorden 200 en een echte importsmoke creëerde één job binnen een transactie waarna rollback expliciet bevestigd werd.
Volgende uitvoerbare taak: geen na afronding van VR-228. Alleen de optionele
mailbox-/identityconfiguratie (`VR-202`) en publicatie naar een externe private
registry (`VR-204`) blijven extern geblokkeerd.
Productieoplevering 0.3.15 op 2026-07-30:
- `VR-202` is live afgerond met een dedicated alias, geïsoleerde Exchange-map en
inboxregel, app-only OAuth/XOAUTH2 voor IMAP en SMTP en negen platformconnecties;
- dezelfde echte testmail werd per connectie eenmaal geïmporteerd en bij herhaling
als duplicate herkend; alle ingestelde intervallen zetten `next_poll_at` correct
en de scheduler plande geen connectie te vroeg;
- de productie-Fernet-sleutel werd live naar een nieuwe primaire MultiFernet-sleutel
geroteerd; een tijdelijke versleutelde custom credential bleef ontsleutelbaar en
is na de proef verwijderd;
- één echte digest werd verzonden en de outbox eindigde op `sent`; productie-logs
bevatten geen OAuth-secret, bearer-token of mailpayload;
- `VR-204` publiceerde `v0.3.15` naar de private Gitea-registry en Unraid is gepind
op `sha256:5a6dc253db7a60c42c0c806ae084ddc6ee7c98db5db71a857423b894a3662ba5`;
- commitlabel, digest, SPDX-SBOM, checksums en release notes zijn checksum-geldig
opgeslagen als Gitea Actions-artifact `release-evidence-v0.3.15`;
- pull, interne en publieke readiness, rollback naar `0.3.14` en herstel naar de
immutable `0.3.15`-digest zijn live geslaagd;
- lokale en Gitea-releasegate zijn groen met 324 tests, 0 failures, 0 skips en
84,05% branch-aware dekking.
Volgende uitvoerbare taak: geen; alle P0- en P1-releasepunten zijn afgerond.
## Laatste risicohardening
@@ -42,8 +104,7 @@ Live mailboxvoorbereiding op 2026-07-30:
- de alias is niet primair gemaakt en bestaande adressen zijn ongewijzigd gebleven;
- de Entra-appregistratie, admin consent en Exchange-mailboxscope zijn live ingericht
met app-only OAuth; tijdelijke bootstraprechten en de bootstrap-app zijn na afloop verwijderd;
- `VR-202` resteert alleen nog op de productie-uitrol en echte platformalert-/digestproeven;
Basic Auth wordt niet geactiveerd;
- `VR-202` is volledig afgerond; Basic Auth is niet geactiveerd;
- de volledige lokale gate is groen met 324 tests, 0 failures, 0 skips en 84,05%
branch-aware dekking.
@@ -576,16 +637,13 @@ Een toekomstige agent moet altijd de actuele waarheid opnieuw bepalen met:
## Nog te bouwen
Momenteel staan er geen `ready` taken meer over; het volgende te behandelen item is `VR-202` als `blocked-external` voor live mailbox-/SMTP-credentials en echte jobalertmails van de gekozen platformen.
Momenteel staan er geen `ready` taken of P0/P1-releaseblokkers meer over.
## Externe blokkades
Deze blokkeren onafhankelijke code niet:
- verdere persoonlijke werkgevers-/bronnenkeuze en actuele menselijke review buiten de actieve VITO-/Deliverect-startercatalogus;
- live IMAP/SMTP-account en app-password;
- containerregistry en credentials;
- actieve/voor deze repository geregistreerde Gitea Actions-runner;
## Bekende verificatiebeperking van de aangeleverde basis
+11 -3
View File
@@ -1,6 +1,6 @@
# Finale acceptance 0.3.14
# Finale acceptance 0.3.15
Status: productie geaccepteerd op 2026-07-29.
Status: productie geaccepteerd op 2026-07-30.
## Bewijs
@@ -14,7 +14,15 @@ Status: productie geaccepteerd op 2026-07-29.
- PostgreSQL custom dump plus media/configbewijs is naar een geïsoleerde database hersteld; migrations, exacte recordtellingen en een aparte applicatiestart slaagden.
- Een productie-importsmoke extraheerde en creëerde één geldige job in een expliciete transactie; rollback werd bevestigd en liet geen smokedata achter.
- De checksum-geldige pre-releaseback-up staat onder `backups/release-0.3.14-20260729T164500Z`; rollbacktag `vacatureradar:rollback-0.3.13` bewaart image `sha256:34aef8853f77a3f311b119bd67d9f1160a97fd4a21e62c9776222be289bf3b88`.
- M365 IMAP en SMTP werken app-only via OAuth/XOAUTH2; negen geïsoleerde
platformconnecties importeerden één live testmail idempotent en één testdigest
eindigde met outboxstatus `sent`.
- Gitea Actions publiceerde `v0.3.15` met immutable digest
`sha256:5a6dc253db7a60c42c0c806ae084ddc6ee7c98db5db71a857423b894a3662ba5`.
Het artifact bevat een geverifieerde SPDX-SBOM, checksums en release notes.
- Unraid is op die digest gepind; pull, health/readiness, rollback naar `0.3.14`
en herstel naar `0.3.15` zijn live geslaagd.
## Uitkomst
Er bestaan geen bekende P0- of P1-releaseblokkers. Alleen optionele Entra- en mailboxcredentials blijven extern; zij raken de kernrelease niet.
Er bestaan geen bekende P0- of P1-releaseblokkers.
+1 -1
View File
@@ -82,7 +82,7 @@ Django/Celery
| T-40 | Verouderde lokale CA-store of ongeldige serverketen lokt een TLS-bypass uit | onderschepte vacature- of mailboxdata en credentialdiefstal | expliciet gepinde `certifi`-CA-bundel voor HTTP, robots en IMAP; hostnaamcontrole en `CERT_REQUIRED`; nooit `verify=False`; dependency- en negatieve TLS-tests bij update | `tests/unit/test_tls.py`, `tests/security/test_robots.py`, `apps/sources/services/tls.py`, `VR-201` |
| T-41 | Gemodificeerde `next`-parameter na vacaturefeedback | open redirect naar phishingdomein na een geldige gebruikersactie | terugkeer-URL moet dezelfde host en het actuele HTTPS-beleid respecteren; anders veilige detailfallback | `tests/integration/test_tasks_and_more_views.py`, `VR-206` |
| T-42 | Gespoofte proxyheaders of ontbrekende publieke hostconfiguratie | host-headerfouten, onjuiste HTTPS-detectie of omzeilde IP-rate-limiting | `PUBLIC_BASE_URL`, exacte host/CSRF-validatie, proxy moet headers overschrijven, forwarded client-IP alleen vanaf `TRUSTED_PROXY_CIDRS` | `tests/unit/test_public_configuration.py`, `tests/unit/test_network.py`, `VR-225` |
| T-43 | Gedeeld demoaccount wijzigt persistente data | bezoekers beïnvloeden elkaars ervaring of echte data | server-side read-only middleware, korte sessie, geen bruikbaar wachtwoord, zichtbare UI-status en productiecheck | `tests/integration/test_auth_login.py`, `tests/unit/test_settings_security.py`, `VR-225` |
| T-43 | Gedeeld demoaccount wijzigt persistente data of activeert echte mailboxpolling | bezoekers beïnvloeden elkaars ervaring, echte data of externe accounts | server-side read-only middleware, korte sessie, geen bruikbaar wachtwoord, uitsluitend fictieve `example.test`-mailboxen; de loginseed herstelt begrensd en idempotent en de scheduler sluit de demo-user expliciet uit | `tests/integration/test_auth_login.py`, `tests/integration/test_tasks_and_more_views.py`, `tests/unit/test_settings_security.py`, `VR-225/229` |
## Misbruikscenario's
+7
View File
@@ -85,6 +85,7 @@ Iedere backlogtaak die een requirement wijzigt, werkt in dezelfde commit deze ma
## Aanvullende traceability-opmerking
- VR-228 levert release 0.3.14 met een fail-closed disposable PostgreSQL-benchmark, run-lokale importcaches, gebundelde veldprovenance en ongewijzigde score-snapshotcopy. De officiële drie iteraties bewijzen 3.264 queries per 250 items, parser 12/12, dedupeprecision/recall 1,0 en nul rankingchurn; bewijs staat in `docs/audit/IMPORT_PERFORMANCE_*.md` en het machineleesbare artifact.
- VR-229 houdt de publieke demo representatief door fictieve vacatures, scores, mailboxkoppelingen en opvolgdatums bij iedere demosessie idempotent te herstellen. Mailboxingress is uitgesloten van URL-fetchplanning en operationele UI-copy onderscheidt een actief profiel van bron- en systeemgezondheid.
- VR-227 professionaliseert release 0.3.13 integraal: gecentreerd ultrawideframe, mobiele gastbanner en verticale pipeline, begrensde vacaturepaginering, verplichte zes-viewport-Playwrightgate zonder skips, echte runtimeheartbeat, volledige generieke parserbenchmark en SBOM-generatie. Bewijs staat in `docs/audit/`, de E2E-/auth-/runtime-/adaptertests en `USER_INPUT_REQUIRED.md`.
- 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.
@@ -96,6 +97,12 @@ Iedere backlogtaak die een requirement wijzigt, werkt in dezelfde commit deze ma
- VR-122 operationaliseert de veilige platformingress als dagelijkse, configureerbare en begrensde mailboxpoll. VDAB-alerts krijgen expliciete provenance, replay blijft idempotent en de bronpagina onderscheidt de werkende jobalertroute van de contractueel afgeschermde officiële Vacature API. Bewijs staat in de e-mailadapter-/importtests, `apps/sources/services/imap_import.py` en ADR-0005.
- VR-123 vervangt die globale poll door gebruikersgescopeerde VDAB- en Indeed-mailboxkoppelingen met versleutelde app-wachtwoorden, afzonderlijke intervallen, per-mailboxleases en UI-beheer. Bewijs staat in `tests/unit/test_mailbox_credentials.py`, `tests/integration/test_mailbox_connections.py`, de migratie en ADR-0009.
- VR-124 breidt dezelfde mailboxingang uit met LinkedIn, ictjob.be, Jobat, StepStone, EuroBrussels en BrusselsJobs. De platformgebonden parser accepteert uitsluitend gelabelde HTTPS-links op het gekozen jobboarddomein; fixtures, adapter-/integratietests en de centrale denylist bewijzen de afscherming.
- VR-202 bewijst de productie-ingress met M365 app-only OAuth/XOAUTH2, een
dedicated alias en map, negen platformconnecties, een idempotente live
testmail, correcte schedulerstatus en een verzonden digest-outbox.
- VR-204 bewijst de immutable releaseketen via de private Gitea-registry,
digest-pin, commitlabel, SPDX-SBOM, checksums, release notes en een live
Unraid pull-/rollback-/herstelsmoke.
- VR-125 vervangt de twee Brusselse nicheboards door Careerjet, Randstad en Robert Half, retireert buitenregionale werkgevers zonder provenanceverlies en voegt host- en padgebonden adapters toe voor ACA Group, Xplore Group, UHasselt, PXL en ZOL in de kern Hasselt-Genk.
- VR-126 herankert profiel en broncatalogus op postcode 2400 Mol en 40 km. De host-, pad- en locatiefilters voor SCK CEN, Cipal Schaubroeck, VanRoey, NTX en Thomas More worden gedekt door regionale adaptertests, source-viewtests, live bronruns en de bijgewerkte threat T-39.
- VR-201 activeert Gemeente Mol en zeven bijkomende Kempen-werkgevers na menselijke voorwaardenreview, fixtures, fail-closed locatie-/routefilters en twee livecycli. Twaalf interessante werkgevers zonder stabiele actieve ingang staan afzonderlijk en niet-scanbaar op de UI-waaklijst. Bewijs staat in de livecatalogus, seeddata, `KempenEmployerAdapter`, bronruns en adapter-/seed-/viewtests.
+1 -1
View File
@@ -48,7 +48,7 @@
<div class="sidebar-footer">
<div class="sidebar-radar-card">
<span class="status-beacon {% if navigation_profile %}is-live{% endif %}" aria-hidden="true"></span>
<span><strong>{% if navigation_profile %}Radar actief{% else %}Profiel nodig{% endif %}</strong><small>{% if navigation_profile %}{{ navigation_profile.name }}{% else %}Configureer je zoekprofiel{% endif %}</small></span>
<span><strong>{% if navigation_profile %}Profiel actief{% else %}Profiel nodig{% endif %}</strong><small>{% if navigation_profile %}{{ navigation_profile.name }}{% else %}Configureer je zoekprofiel{% endif %}</small></span>
</div>
{% include "components/developer_credit.html" %}
</div>
+1 -1
View File
@@ -3,7 +3,7 @@
{% block content %}
<header class="cockpit-heading dashboard-heading">
<div><p class="eyebrow">Intelligence cockpit · vandaag</p><h1>Goedemorgen, {{ user.first_name|default:user.username }}.</h1><p>{{ day_conclusion }}</p></div>
<div class="radar-status" aria-label="Radarstatus"><span class="status-beacon {% if active_profile %}is-live{% endif %}" aria-hidden="true"></span><span>{% if active_profile %}{{ active_profile.name }} actief{% else %}Geen actief profiel{% endif %}</span>{% if latest_scan %}<span aria-hidden="true">·</span><span>scan {{ latest_scan.started_at|timesince }} geleden</span>{% endif %}</div>
<div class="radar-status" aria-label="Profiel- en scanstatus"><span class="status-beacon {% if active_profile %}is-live{% endif %}" aria-hidden="true"></span><span>{% if active_profile %}Profiel actief · {{ active_profile.name }}{% else %}Geen actief profiel{% endif %}</span>{% if latest_scan %}<span aria-hidden="true">·</span><span>laatste URL-scan {{ latest_scan.started_at|timesince }} geleden</span>{% endif %}</div>
</header>
<section class="stats-grid" aria-label="Radar kerncijfers">
+1 -1
View File
@@ -56,7 +56,7 @@
</div>
<div class="input-wrap">
<input class="auth-input" id="id_password" name="password" type="password"
autocomplete="current-password" placeholder="••••••••" required
autocomplete="current-password" required
{% if form.errors %}aria-invalid="true"{% endif %}>
<button class="pw-toggle" type="button" data-pw-toggle aria-label="Wachtwoord tonen">
<svg class="icon" viewBox="0 0 24 24" fill="none" aria-hidden="true" data-eye><path d="M2.5 12s3.5-6 9.5-6 9.5 6 9.5 6-3.5 6-9.5 6-9.5-6-9.5-6Z" stroke="currentColor" stroke-width="1.8"/><circle cx="12" cy="12" r="2.7" stroke="currentColor" stroke-width="1.8"/></svg>
+40 -1
View File
@@ -135,15 +135,54 @@ def test_demo_login_logs_in_and_seeds_environment(client):
# Het gevulde dashboard rendert zonder fouten.
dashboard = client.get(reverse("dashboard:today"))
assert dashboard.status_code == 200
dashboard_html = dashboard.content.decode("utf-8")
assert "Profiel actief" in dashboard_html
assert "Radar actief" not in dashboard_html
assert "Nog geen persoonlijke matches" not in dashboard_html
sources = client.get(reverse("sources:list"))
assert sources.status_code == 200
assert "minstens één import actief" in sources.content.decode("utf-8")
def test_demo_login_is_idempotent(client):
from apps.jobs.models import JobPosting
from datetime import timedelta
from django.utils import timezone
from apps.jobs.models import Application, JobPosting
from apps.profiles.models import SearchProfile
from apps.sources.models import MailboxConnection, Source
client.post(reverse("demo-login"))
first = JobPosting.objects.count()
demo_user = get_user_model().objects.get(username=settings.DEMO_USERNAME)
profile = SearchProfile.objects.get(user=demo_user, is_active=True)
job = profile.scores.order_by("-score").first().job
job.status = JobPosting.Status.EXPIRED
job.last_seen = timezone.now() - timedelta(days=30)
job.save(update_fields=["status", "last_seen"])
application = Application.objects.filter(user=demo_user).first()
application.follow_up_date = timezone.localdate() - timedelta(days=14)
application.save(update_fields=["follow_up_date"])
client.logout()
client.post(reverse("demo-login"))
assert JobPosting.objects.count() == first
job.refresh_from_db()
application.refresh_from_db()
assert job.status == JobPosting.Status.ACTIVE
assert job.last_seen.date() == timezone.localdate()
assert application.follow_up_date > timezone.localdate()
assert profile.scores.count() == 6
assert MailboxConnection.objects.filter(user=demo_user, enabled=True).count() == 9
assert Source.objects.filter(source_type=Source.Type.EMAIL, failure_count=0).count() >= 9
def test_login_password_field_has_no_mask_placeholder(client):
html = client.get(reverse("login")).content.decode("utf-8")
assert 'id="id_password"' in html
assert 'placeholder="••••••••"' not in html
def test_demo_account_is_read_only_and_session_is_bounded(client):
@@ -104,6 +104,55 @@ def test_fetch_source_resets_fail_state_on_304(db, source, monkeypatch):
assert source.failure_count == 0
def test_due_source_scheduler_never_schedules_mailbox_sources(db, monkeypatch):
from apps.sources.tasks import schedule_due_sources
email_source = Source.objects.create(
name="Mailboxbron",
source_type=Source.Type.EMAIL,
base_url="",
domain="example.mailbox.local",
status=Source.Status.ACTIVE,
policy=Source.Policy.ALLOW,
)
scheduled = []
monkeypatch.setattr(
"apps.sources.tasks.fetch_source.delay", lambda source_id: scheduled.append(source_id)
)
result = schedule_due_sources()
assert email_source.pk not in scheduled
assert result["scheduled"] == len(scheduled)
def test_mailbox_scheduler_excludes_public_demo_user(db, monkeypatch, settings):
from django.contrib.auth import get_user_model
from apps.sources.models import MailboxConnection
from apps.sources.tasks import schedule_mailbox_polls
demo = get_user_model().objects.create_user(username=settings.DEMO_USERNAME)
connection = MailboxConnection.objects.create(
user=demo,
platform="vdab",
provider="outlook",
username="demo@example.test",
mailbox="Demo/VDAB",
enabled=True,
next_poll_at=timezone.now() - timedelta(minutes=1),
)
scheduled = []
monkeypatch.setattr(
"apps.sources.tasks.poll_mailbox.delay",
lambda connection_id: scheduled.append(connection_id),
)
schedule_mailbox_polls()
assert connection.pk not in scheduled
def test_fetch_source_is_blocked_by_conflicting_review(db):
source = Source.objects.create(
name="Blocked source",
@@ -128,3 +128,11 @@ def test_deploy_uses_selected_environment_file_for_compose() -> None:
assert 'export VACATURERADAR_ENV_FILE="$env_file"' in deploy_script
assert 'docker compose --env-file "$env_file" -f "$compose_file" up -d --build' in deploy_script
assert "waarde uit ${env_file}" in deploy_script
def test_release_uv_install_is_immutable_and_does_not_require_github_api() -> None:
workflow = (ROOT / ".gitea" / "workflows" / "release.yml").read_text(encoding="utf-8")
assert "astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0" in workflow
assert 'version: "0.11.12"' in workflow
assert "astral-sh/setup-uv@v6" not in workflow