Harden public demo for 0.3.16
release / verify-publish (push) Failing after 6s
deploy / deploy (push) Canceled after 0s

This commit is contained in:
Jens
2026-08-12 11:42:00 +02:00
parent 2419e6bf01
commit a6c8a79e31
17 changed files with 228 additions and 24 deletions
+9
View File
@@ -2,6 +2,15 @@
Alle betekenisvolle wijzigingen worden hier bijgehouden. Het project volgt voorlopig een pre-1.0 semantische versieaanpak. 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 ## 0.3.14 - 2026-07-29
### Gewijzigd ### Gewijzigd
+2 -2
View File
@@ -8,8 +8,8 @@
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. 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.15.** De volledige P0/P1-releasebasis is afgerond en live **Actuele release: 0.3.16.** De volledige P0/P1-releasebasis en de publieke
gevalideerd: 324 tests zijn groen, Microsoft 365 verwerkt platformmails via 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 app-only OAuth/XOAUTH2 en de productie-installatie draait vanaf een immutable
image in de private Gitea-containerregistry. Het controleerbare acceptatiebewijs image in de private Gitea-containerregistry. Het controleerbare acceptatiebewijs
staat in [Finale acceptance](docs/audit/FINAL_ACCEPTANCE.md). staat in [Finale acceptance](docs/audit/FINAL_ACCEPTANCE.md).
+3 -2
View File
@@ -8,7 +8,7 @@ from django.urls import reverse
from apps.notifications.models import DigestOutbox, ReminderOutbox from apps.notifications.models import DigestOutbox, ReminderOutbox
from apps.profiles.models import ProfileRevision 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 from ..models import ApplicationTimelineEvent, Feedback, JobVersion
@@ -37,7 +37,8 @@ ACTIVITY_FILTERS = (
def _source_events() -> list[ActivityEvent]: def _source_events() -> list[ActivityEvent]:
events = [] 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 = ( detail = (
f"{run.extracted_count} geëxtraheerd · {run.created_count} nieuw · " f"{run.extracted_count} geëxtraheerd · {run.created_count} nieuw · "
f"{run.duplicate_count} duplicaat" 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]]: def _recent_dashboard_activity(user: User) -> list[dict[str, Any]]:
activity: 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( activity.append(
{ {
"occurred_at": run.started_at, "occurred_at": run.started_at,
@@ -154,7 +155,9 @@ def build_dashboard_cockpit(user: User, profile: SearchProfile | None) -> dict[s
"source_counts": source_counts, "source_counts": source_counts,
"recent_activity": _recent_dashboard_activity(user), "recent_activity": _recent_dashboard_activity(user),
"day_conclusion": day_conclusion, "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: def seed_demo_environment(user) -> None:
"""Maak (idempotent) een fictieve demo-omgeving voor ``user``.""" """Maak of herstel een fictieve, tijdsbestendige demo-omgeving voor ``user``."""
if demo_environment_seeded(user):
return
from apps.jobs.models import Application, Employer, JobPosting, ScoreRun 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( profile, _ = SearchProfile.objects.get_or_create(
user=user, user=user,
@@ -185,7 +184,7 @@ def seed_demo_environment(user) -> None:
}, },
) )
slug = _digest("demo-job", str(index), title, employer_name) 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, canonical_key=slug,
defaults={ defaults={
"employer": employer, "employer": employer,
@@ -221,6 +220,11 @@ def seed_demo_environment(user) -> None:
) )
created_jobs.append(job) created_jobs.append(job)
ScoreRun.objects.filter(
job=job,
profile=profile,
model_version="demo-seed",
).delete()
ScoreRun.objects.create( ScoreRun.objects.create(
job=job, job=job,
profile=profile, profile=profile,
@@ -244,7 +248,7 @@ def seed_demo_environment(user) -> None:
# Een kleine sollicitatiepijplijn voor de demo. # Een kleine sollicitatiepijplijn voor de demo.
if created_jobs: if created_jobs:
Application.objects.get_or_create( Application.objects.update_or_create(
user=user, user=user,
job=created_jobs[0], job=created_jobs[0],
defaults={ defaults={
@@ -257,7 +261,7 @@ def seed_demo_environment(user) -> None:
}, },
) )
if len(created_jobs) > 3: if len(created_jobs) > 3:
Application.objects.get_or_create( Application.objects.update_or_create(
user=user, user=user,
job=created_jobs[3], job=created_jobs[3],
defaults={ defaults={
@@ -267,3 +271,42 @@ def seed_demo_environment(user) -> None:
"notes": "Sollicitatie verstuurd. Demo-data.", "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() now = timezone.now()
due = Source.objects.filter( due = Source.objects.filter(
status__in=[Source.Status.ACTIVE, Source.Status.TRIAL], 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]) ids = list(due.values_list("id", flat=True).distinct()[:200])
for source_id in ids: for source_id in ids:
fetch_source.delay(source_id) fetch_source.delay(source_id)
@@ -200,6 +201,7 @@ def schedule_mailbox_polls() -> dict[str, int]:
now = timezone.now() now = timezone.now()
due_ids = list( due_ids = list(
MailboxConnection.objects.filter(enabled=True, next_poll_at__lte=now) 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)) .filter(Q(lease_expires_at__isnull=True) | Q(lease_expires_at__lte=now))
.values_list("id", flat=True)[:100] .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") TIME_ZONE = os.getenv("DJANGO_TIME_ZONE", "Europe/Brussels")
VACATURERADAR_OWNER_NAME = os.getenv("VACATURERADAR_OWNER_NAME", "Jens").strip()[:40] or "Jens" 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" LANGUAGE_CODE = "nl-be"
USE_I18N = True USE_I18N = True
USE_TZ = 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_CSRF_TRUSTED_ORIGINS=https://vacatureradar.example.be
DJANGO_TIME_ZONE=Europe/Brussels DJANGO_TIME_ZONE=Europe/Brussels
VACATURERADAR_OWNER_NAME=Jens VACATURERADAR_OWNER_NAME=Jens
VACATURERADAR_VERSION=0.3.14 VACATURERADAR_VERSION=0.3.16
POSTGRES_DB=vacatureradar POSTGRES_DB=vacatureradar
POSTGRES_USER=vacatureradar POSTGRES_USER=vacatureradar
+42 -1
View File
@@ -1,6 +1,6 @@
schema_version: 1 schema_version: 1
project: VacatureRadar project: VacatureRadar
updated_at: '2026-07-30' updated_at: '2026-08-12'
execution_policy: execution_policy:
order: file-order, then first ready task whose dependencies are done order: file-order, then first ready task whose dependencies are done
no_product_questions: true 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%; 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 twee volledige gates met 318 tests groen; productie healthy op image sha256:1ee84241, transactionele importsmoke
teruggerold en rollbackimage 0.3.13 beschikbaar.' 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.
+18 -2
View File
@@ -1,7 +1,7 @@
# Projectstatus # Projectstatus
- Laatst bijgewerkt: 2026-07-30 - Laatst bijgewerkt: 2026-08-12
- Repositoryversie: 0.3.15 M365 OAuth en immutable registryrelease - Repositoryversie: 0.3.16 publieke demo-hardening
- Uitvoeringsmodus: autonome backlog - Uitvoeringsmodus: autonome backlog
- Eerste uitvoerbare taak: controleer met `python scripts/backlog.py next` - 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 ## 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: README-update op 2026-07-30:
- de repository-ingang benoemt release 0.3.15, de groene 324-testgate, de live - de repository-ingang benoemt release 0.3.15, de groene 324-testgate, de live
+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-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-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-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 ## Misbruikscenario's
+1
View File
@@ -85,6 +85,7 @@ Iedere backlogtaak die een requirement wijzigt, werkt in dezelfde commit deze ma
## Aanvullende traceability-opmerking ## 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-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-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-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-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.
+1 -1
View File
@@ -48,7 +48,7 @@
<div class="sidebar-footer"> <div class="sidebar-footer">
<div class="sidebar-radar-card"> <div class="sidebar-radar-card">
<span class="status-beacon {% if navigation_profile %}is-live{% endif %}" aria-hidden="true"></span> <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> </div>
{% include "components/developer_credit.html" %} {% include "components/developer_credit.html" %}
</div> </div>
+1 -1
View File
@@ -3,7 +3,7 @@
{% block content %} {% block content %}
<header class="cockpit-heading dashboard-heading"> <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><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> </header>
<section class="stats-grid" aria-label="Radar kerncijfers"> <section class="stats-grid" aria-label="Radar kerncijfers">
+1 -1
View File
@@ -56,7 +56,7 @@
</div> </div>
<div class="input-wrap"> <div class="input-wrap">
<input class="auth-input" id="id_password" name="password" type="password" <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 %}> {% if form.errors %}aria-invalid="true"{% endif %}>
<button class="pw-toggle" type="button" data-pw-toggle aria-label="Wachtwoord tonen"> <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> <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. # Het gevulde dashboard rendert zonder fouten.
dashboard = client.get(reverse("dashboard:today")) dashboard = client.get(reverse("dashboard:today"))
assert dashboard.status_code == 200 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): 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")) client.post(reverse("demo-login"))
first = JobPosting.objects.count() 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")) client.post(reverse("demo-login"))
assert JobPosting.objects.count() == first 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): 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 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): def test_fetch_source_is_blocked_by_conflicting_review(db):
source = Source.objects.create( source = Source.objects.create(
name="Blocked source", name="Blocked source",