diff --git a/CHANGELOG.md b/CHANGELOG.md index b3f9409..555667b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ Alle betekenisvolle wijzigingen worden hier bijgehouden. Het project volgt voorlopig een pre-1.0 semantische versieaanpak. +## 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 diff --git a/README.md b/README.md index d8ca791..bd814b9 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ VacatureRadar is een persoonlijke vacature-assistent voor België. De applicatie volgt toegestane werkgeversbronnen, ATS-pagina’s, 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.15.** De volledige P0/P1-releasebasis is afgerond en live -gevalideerd: 324 tests zijn groen, Microsoft 365 verwerkt platformmails via +**Actuele release: 0.3.16.** 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 en de productie-installatie draait vanaf een immutable image in de private Gitea-containerregistry. Het controleerbare acceptatiebewijs staat in [Finale acceptance](docs/audit/FINAL_ACCEPTANCE.md). diff --git a/apps/jobs/services/activity.py b/apps/jobs/services/activity.py index 9800e22..2ce9638 100644 --- a/apps/jobs/services/activity.py +++ b/apps/jobs/services/activity.py @@ -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" diff --git a/apps/jobs/services/cockpit.py b/apps/jobs/services/cockpit.py index 0250391..45749e2 100644 --- a/apps/jobs/services/cockpit.py +++ b/apps/jobs/services/cockpit.py @@ -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(), } diff --git a/apps/jobs/services/demo_seed.py b/apps/jobs/services/demo_seed.py index 0c1a8e7..a6ab5ab 100644 --- a/apps/jobs/services/demo_seed.py +++ b/apps/jobs/services/demo_seed.py @@ -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() diff --git a/apps/sources/tasks.py b/apps/sources/tasks.py index b03916a..72226ef 100644 --- a/apps/sources/tasks.py +++ b/apps/sources/tasks.py @@ -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] ) diff --git a/config/settings.py b/config/settings.py index 10d596f..6158941 100644 --- a/config/settings.py +++ b/config/settings.py @@ -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.16").strip() or "0.3.16" LANGUAGE_CODE = "nl-be" USE_I18N = True USE_TZ = True diff --git a/deployment/production.env.example b/deployment/production.env.example index f5a4166..60589be 100644 --- a/deployment/production.env.example +++ b/deployment/production.env.example @@ -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.16 POSTGRES_DB=vacatureradar POSTGRES_USER=vacatureradar diff --git a/docs/ai/BACKLOG.yaml b/docs/ai/BACKLOG.yaml index ba4345f..162b6a6 100644 --- a/docs/ai/BACKLOG.yaml +++ b/docs/ai/BACKLOG.yaml @@ -1,6 +1,6 @@ schema_version: 1 project: VacatureRadar -updated_at: '2026-07-30' +updated_at: '2026-08-12' execution_policy: order: file-order, then first ready task whose dependencies are done no_product_questions: true @@ -2376,3 +2376,44 @@ 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. diff --git a/docs/ai/PROJECT_STATE.md b/docs/ai/PROJECT_STATE.md index 0c8c2ef..32b15f0 100644 --- a/docs/ai/PROJECT_STATE.md +++ b/docs/ai/PROJECT_STATE.md @@ -1,7 +1,7 @@ # Projectstatus -- Laatst bijgewerkt: 2026-07-30 -- Repositoryversie: 0.3.15 M365 OAuth en immutable registryrelease +- 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` @@ -21,6 +21,22 @@ De repository bevat een uitvoerbare Django-MVP met: ## 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. Volgende uitvoerbare taak: geen. + README-update op 2026-07-30: - de repository-ingang benoemt release 0.3.15, de groene 324-testgate, de live diff --git a/docs/quality/THREAT_MODEL.md b/docs/quality/THREAT_MODEL.md index 47a8feb..1b2cc36 100644 --- a/docs/quality/THREAT_MODEL.md +++ b/docs/quality/THREAT_MODEL.md @@ -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 diff --git a/docs/quality/TRACEABILITY_MATRIX.md b/docs/quality/TRACEABILITY_MATRIX.md index 0ab06a4..e381343 100644 --- a/docs/quality/TRACEABILITY_MATRIX.md +++ b/docs/quality/TRACEABILITY_MATRIX.md @@ -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. diff --git a/templates/base.html b/templates/base.html index 70fed06..79fe6fb 100644 --- a/templates/base.html +++ b/templates/base.html @@ -48,7 +48,7 @@
diff --git a/templates/dashboard/today.html b/templates/dashboard/today.html index 0a3fcec..fab7282 100644 --- a/templates/dashboard/today.html +++ b/templates/dashboard/today.html @@ -3,7 +3,7 @@ {% block content %}Intelligence cockpit · vandaag
{{ day_conclusion }}