feat: release regional radar and mailbox integrations
deploy / deploy (push) Canceled after 0s

This commit is contained in:
Jens
2026-07-22 05:12:07 +02:00
parent 598d3ec18a
commit 551d0f46c2
131 changed files with 6209 additions and 336 deletions
+4 -4
View File
@@ -1,18 +1,18 @@
{% extends "base.html" %}
{% block title %}Sollicitaties · VacatureRadar{% endblock %}
{% block content %}
<header class="page-header"><div><p class="eyebrow">Persoonlijke opvolging</p><h1>Sollicitatiepipeline</h1><p>Vacaturesnapshots blijven beschikbaar wanneer de oorspronkelijke bron offline gaat.</p></div></header>
<header class="page-header pipeline-page-header"><div><p class="eyebrow">Persoonlijke opvolging</p><h1>Sollicitatiepipeline</h1><p>Beheer je actieve trajecten, deadlines en bewaarde vacaturesnapshots.</p></div><div class="page-actions"><span class="view-indicator"><svg class="icon"><use href="#icon-pipeline"></use></svg>Kanban</span><a class="button button-primary" href="{% url 'jobs:list' %}"><svg class="icon"><use href="#icon-search"></use></svg>Vacature toevoegen</a></div></header>
<section class="pipeline" aria-label="Sollicitaties per status">
{% for column in pipeline_columns %}
<div class="pipeline-column">
<div class="pipeline-head"><h2>{{ column.label }}</h2><span class="pipeline-count">{{ column.applications|length }}</span></div>
<div class="pipeline-head"><h2><span class="column-dot" aria-hidden="true"></span>{{ column.label }}</h2><span class="pipeline-count">{{ column.applications|length }}</span></div>
<div class="pipeline-cards">
{% for application in column.applications %}
<article class="pipeline-card"><span class="pill pill-{{ application.status }}">{{ application.get_status_display }}</span><h3><a href="{% url 'jobs:detail' application.job.pk %}">{{ application.job.original_title }}</a></h3><p>{{ application.job.employer_name }}{% if application.job.raw_location %} · {{ application.job.raw_location }}{% endif %}</p><div class="job-meta">{% if application.applied_at %}<span>Verzonden {{ application.applied_at|date:"d/m/Y" }}</span>{% endif %}{% if application.follow_up_date %}<span>Opvolgen {{ application.follow_up_date|date:"d/m/Y" }}</span>{% endif %}</div><div class="job-actions"><a class="button button-ghost button-small" href="{% url 'jobs:application-edit' application.pk %}">Dossier</a></div></article>
<article class="pipeline-card"><span class="pill pill-{{ application.status }}">{{ application.get_status_display }}</span><h3><a href="{% url 'jobs:detail' application.job.pk %}">{{ application.job.original_title }}</a></h3><p>{{ application.job.employer_name }}{% if application.job.raw_location %} · {{ application.job.raw_location }}{% endif %}</p><div class="job-meta">{% if application.applied_at %}<span>Verzonden {{ application.applied_at|date:"d/m/Y" }}</span>{% endif %}{% if application.follow_up_date %}<span>Opvolgen {{ application.follow_up_date|date:"d/m/Y" }}</span>{% endif %}</div><div class="job-actions"><a class="button button-ghost button-small" href="{% url 'jobs:application-edit' application.pk %}">Dossier <svg class="icon"><use href="#icon-arrow"></use></svg></a></div></article>
{% empty %}<div class="pipeline-empty">Geen dossiers in deze fase.</div>{% endfor %}
</div>
</div>
{% endfor %}
</section>
{% if not applications %}<div class="empty-state"><div class="empty-icon"><svg class="icon"><use href="#icon-pipeline"></use></svg></div><h2>Nog geen sollicitatiedossiers</h2><p>Markeer een vacature als Gesolliciteerd om automatisch een persoonlijk dossier te maken.</p><a class="button button-primary" href="{% url 'jobs:list' %}">Vacatures bekijken</a></div>{% endif %}
{% if not applications %}<div class="empty-state compact-empty"><div class="empty-icon"><svg class="icon"><use href="#icon-pipeline"></use></svg></div><h2>Nog geen sollicitatiedossiers</h2><p>Markeer een vacature als Gesolliciteerd om automatisch een persoonlijk dossier te maken.</p><a class="button button-primary" href="{% url 'jobs:list' %}">Vacatures bekijken</a></div>{% endif %}
{% endblock %}
+8 -2
View File
@@ -7,6 +7,7 @@
<meta name="color-scheme" content="dark light">
<meta name="theme-color" content="#07101f">
<title>{% block title %}{{ app_name }}{% endblock %}</title>
<link rel="icon" href="{% static 'favicon.svg' %}" type="image/svg+xml">
<link rel="stylesheet" href="{% static 'css/app.css' %}?v={{ app_version }}">
<script src="{% static 'js/app.js' %}?v={{ app_version }}" defer></script>
</head>
@@ -60,14 +61,19 @@
<button class="icon-button mobile-only" type="button" data-nav-toggle aria-label="Menu openen" aria-expanded="false">
<svg class="icon"><use href="#icon-menu"></use></svg>
</button>
<div class="utility-title"><span class="signal-dot" aria-hidden="true"></span><span>Persoonlijke radar</span></div>
<form class="global-search" method="get" action="{% url 'jobs:list' %}" role="search">
<label class="sr-only" for="global-search">Zoek door vacatures</label>
<svg class="icon" aria-hidden="true"><use href="#icon-search"></use></svg>
<input id="global-search" type="search" name="q" value="{% if request.resolver_match.namespace == 'jobs' and request.resolver_match.url_name == 'list' %}{{ request.GET.q }}{% endif %}" placeholder="Zoek door vacatures...">
</form>
<div class="utility-title utility-radar"><span class="signal-dot" aria-hidden="true"></span><span>Radar actief</span></div>
</div>
<div class="utility-actions">
<a class="utility-link" href="{% url 'jobs:list' %}" aria-label="Vacatures zoeken"><svg class="icon"><use href="#icon-search"></use></svg><span class="desktop-label">Zoeken</span></a>
<button class="icon-button" type="button" data-theme-toggle aria-label="Thema wisselen" title="Thema wisselen">
<svg class="icon theme-icon-dark"><use href="#icon-sun"></use></svg>
<svg class="icon theme-icon-light"><use href="#icon-moon"></use></svg>
</button>
{% if navigation_profile %}<a class="button button-primary utility-cta" href="{% url 'profiles:edit' navigation_profile.pk %}"><svg class="icon"><use href="#icon-profile"></use></svg><span class="desktop-label">Zoekprofiel aanpassen</span></a>{% endif %}
<form action="{% url 'logout' %}" method="post">{% csrf_token %}<button class="utility-link" type="submit" aria-label="Afmelden"><svg class="icon"><use href="#icon-logout"></use></svg><span class="desktop-label">Afmelden</span></button></form>
</div>
</header>
+41 -21
View File
@@ -1,10 +1,20 @@
{% extends "base.html" %}
{% block title %}Vandaag · VacatureRadar{% endblock %}
{% block content %}
<header class="page-header">
<div><p class="eyebrow">Intelligence cockpit</p><h1>Goedemorgen, {{ user.first_name|default:user.username }}</h1><p>Je persoonlijke radar brengt alleen actuele signalen en concrete volgende stappen samen.</p></div>
<div class="page-actions"><a class="button button-primary" href="{% url 'jobs:list' %}"><svg class="icon"><use href="#icon-search"></use></svg>Vacatures verkennen</a></div>
<header class="page-header dashboard-header">
<div>
<p class="eyebrow">Intelligence cockpit</p>
<h1>Goedemorgen, {{ user.first_name|default:user.username }}</h1>
<p>Er {% if new_today == 1 %}is <strong>{{ new_today }} nieuw signaal</strong>{% else %}zijn <strong>{{ new_today }} nieuwe signalen</strong>{% endif %} die je aandacht verdienen.</p>
</div>
<div class="radar-status" aria-label="Radarstatus">
<span class="signal-dot" aria-hidden="true"></span>
<span>Persoonlijke radar actief</span>
<span aria-hidden="true">·</span>
<a href="{% url 'dashboard:system' %}">Systeem bekijken</a>
</div>
</header>
<section class="stats-grid" aria-label="Kerncijfers">
<article class="stat-card signal"><span class="kpi-label">Nieuwe signalen</span><strong>{{ new_today }}</strong><small>vandaag gevonden</small></article>
<article class="stat-card"><span class="kpi-label">Actieve vacatures</span><strong>{{ active_jobs }}</strong><small>actueel in de radar</small></article>
@@ -13,32 +23,42 @@
</section>
<div class="dashboard-grid">
<section>
<div class="section-heading"><div><span class="section-kicker">Gerangschikt voor jou</span><h2>Beste matches</h2><p>Matchscore en dataconfidence blijven bewust afzonderlijk zichtbaar.</p></div><a href="{% url 'jobs:list' %}">Alles bekijken →</a></div>
<div class="job-grid">
<section class="dashboard-matches" aria-labelledby="best-matches-heading">
<div class="section-heading">
<div><span class="section-kicker">Gerangschikt voor jou</span><h2 id="best-matches-heading">Beste nieuwe matches</h2><p>Matchscore en datakwaliteit blijven afzonderlijk zichtbaar.</p></div>
<a class="section-link" href="{% url 'jobs:list' %}">Alles bekijken <svg class="icon"><use href="#icon-arrow"></use></svg></a>
</div>
<div class="job-feed">
{% for score in score_cards %}
<article class="job-card {% if score.recommendation == 'strong' %}strong-match{% endif %}">
<div class="job-card-head">
<div><span class="pill pill-{{ score.recommendation }}">{{ score.get_recommendation_display }}</span><h3><a href="{% url 'jobs:detail' score.job.pk %}">{{ score.job.original_title }}</a></h3><p>{{ score.job.employer_name }}</p></div>
<div class="score-block"><strong>{{ score.score|floatformat:0 }}%</strong><small>confidence {{ score.confidence|floatformat:2 }}</small></div>
<article class="job-card feed-card {% if score.recommendation == 'strong' %}strong-match{% endif %}">
<div class="job-card-main">
<div class="job-card-head">
<div>
<div class="title-row"><span class="pill pill-{{ score.recommendation }}">{{ score.get_recommendation_display }}</span><span class="confidence-label">Datakwaliteit {{ score.confidence|floatformat:2 }}</span></div>
<h3><a href="{% url 'jobs:detail' score.job.pk %}">{{ score.job.original_title }}</a></h3>
<p>{{ score.job.employer_name }}{% if score.job.raw_location %} · {{ score.job.raw_location }}{% endif %}</p>
</div>
<div class="score-block"><strong>{{ score.score|floatformat:0 }}%</strong><small>match</small></div>
</div>
<div class="job-meta"><span><svg class="icon"><use href="#icon-location"></use></svg>{{ score.job.raw_location|default:"Locatie onbekend" }}</span><span><svg class="icon"><use href="#icon-briefcase"></use></svg>{{ score.job.get_workplace_type_display }}</span></div>
{% if score.positives %}<ul class="signal-list">{% for item in score.positives|slice:":2" %}<li>{{ item }}</li>{% endfor %}</ul>{% endif %}
{% if score.concerns %}<p class="attention"><strong>Aandachtspunt:</strong> {{ score.concerns.0 }}</p>{% endif %}
</div>
<div class="job-meta"><span><svg class="icon"><use href="#icon-location"></use></svg>{{ score.job.raw_location|default:"Locatie onbekend" }}</span><span><svg class="icon"><use href="#icon-briefcase"></use></svg>{{ score.job.get_workplace_type_display }}</span></div>
{% if score.positives %}<ul class="compact-list positive">{% for item in score.positives|slice:":2" %}<li>{{ item }}</li>{% endfor %}</ul>{% endif %}
{% if score.concerns %}<p class="attention"><strong>Aandachtspunt:</strong> {{ score.concerns.0 }}</p>{% endif %}
<div class="job-actions">
<form method="post" action="{% url 'jobs:feedback' score.job.pk %}">{% csrf_token %}<input type="hidden" name="action" value="interesting"><input type="hidden" name="next" value="{{ request.path }}"><button class="button button-primary" type="submit">Interessant</button></form>
<form method="post" action="{% url 'jobs:feedback' score.job.pk %}">{% csrf_token %}<input type="hidden" name="action" value="save"><input type="hidden" name="next" value="{{ request.path }}"><button class="button button-secondary" type="submit"><svg class="icon"><use href="#icon-bookmark"></use></svg>Bewaren</button></form>
<a class="button button-ghost" href="{% url 'jobs:detail' score.job.pk %}">Analyse <svg class="icon"><use href="#icon-arrow"></use></svg></a>
<div class="feed-card-actions">
<a class="button button-primary" href="{% url 'jobs:detail' score.job.pk %}">Bekijk analyse <svg class="icon"><use href="#icon-arrow"></use></svg></a>
<form method="post" action="{% url 'jobs:feedback' score.job.pk %}">{% csrf_token %}<input type="hidden" name="action" value="save"><input type="hidden" name="next" value="{{ request.path }}"><button class="icon-button save-button" type="submit" aria-label="{{ score.job.original_title }} bewaren"><svg class="icon"><use href="#icon-bookmark"></use></svg></button></form>
</div>
</article>
{% empty %}
<div class="empty-state"><div class="empty-icon"><svg class="icon"><use href="#icon-radar"></use></svg></div><h2>Nog geen beoordeelde vacatures</h2><p>Importeer vacaturedata of activeer een toegestane bron. De eerste betrouwbare resultaten verschijnen hier automatisch.</p><a class="button button-secondary" href="{% url 'sources:list' %}">Bronnen beheren</a></div>
<div class="empty-state compact-empty"><div class="empty-icon"><svg class="icon"><use href="#icon-radar"></use></svg></div><h2>Nog geen beoordeelde vacatures</h2><p>Importeer vacaturedata of activeer een toegestane bron. Betrouwbare resultaten verschijnen hier automatisch.</p><a class="button button-secondary" href="{% url 'sources:list' %}">Bronnen beheren</a></div>
{% endfor %}
</div>
</section>
<aside class="dashboard-rail" aria-label="Radarstatus">
<section class="panel radar-summary"><span class="section-kicker">Vandaag</span><h2>Radarstatus</h2><p class="muted">Alle cijfers komen rechtstreeks uit de huidige database. Geen gesimuleerde trends of workerdata.</p><div class="rail-list"><div class="rail-item"><span>Bronnen actief</span><strong>{{ source_counts.total|default:0 }}</strong></div><div class="rail-item"><span>Aandacht nodig</span><strong>{{ source_counts.unhealthy|default:0 }}</strong></div></div></section>
<section class="panel"><span class="section-kicker">Volgende stap</span><h2>Snelle acties</h2><div class="stack-actions"><a class="button button-secondary button-block" href="{% url 'sources:list' %}"><svg class="icon"><use href="#icon-upload"></use></svg>Handmatig importeren</a><a class="button button-ghost button-block" href="{% url 'profiles:list' %}"><svg class="icon"><use href="#icon-profile"></use></svg>Zoekprofiel controleren</a><a class="button button-ghost button-block" href="{% url 'jobs:applications' %}"><svg class="icon"><use href="#icon-pipeline"></use></svg>Pipeline openen</a></div></section>
<aside class="dashboard-rail" aria-label="Acties en radarstatus">
<section class="panel rail-panel radar-summary"><span class="section-kicker">Vandaag</span><h2>Radarstatus</h2><p class="muted">Alle cijfers komen rechtstreeks uit de huidige database.</p><div class="rail-list"><div class="rail-item"><span>Bronnen geregistreerd</span><strong>{{ source_counts.total|default:0 }}</strong></div><div class="rail-item"><span>Aandacht nodig</span><strong>{{ source_counts.unhealthy|default:0 }}</strong></div><div class="rail-item"><span>Actieve vacatures</span><strong>{{ active_jobs }}</strong></div></div></section>
<section class="panel rail-panel"><span class="section-kicker">Actie vereist</span><h2>Open opvolging</h2><div class="rail-application-list">{% for application in priority_applications %}<a class="rail-application" href="{% url 'jobs:application-edit' application.pk %}"><span><strong>{{ application.job.original_title }}</strong><small>{{ application.job.employer_name }}</small></span>{% if application.follow_up_date %}<time datetime="{{ application.follow_up_date|date:'Y-m-d' }}">{{ application.follow_up_date|date:"d/m" }}</time>{% else %}<svg class="icon"><use href="#icon-arrow"></use></svg>{% endif %}</a>{% empty %}<p class="muted small">Geen open dossiers die opvolging vragen.</p>{% endfor %}</div><a class="button button-ghost button-block" href="{% url 'jobs:applications' %}">Pipeline openen</a></section>
<section class="panel rail-panel"><span class="section-kicker">Volgende stap</span><h2>Snelle acties</h2><div class="stack-actions"><a class="button button-secondary button-block" href="{% url 'sources:list' %}"><svg class="icon"><use href="#icon-upload"></use></svg>Handmatig importeren</a><a class="button button-ghost button-block" href="{% url 'profiles:list' %}"><svg class="icon"><use href="#icon-profile"></use></svg>Zoekprofiel controleren</a></div></section>
</aside>
</div>
{% endblock %}
+5
View File
@@ -8,6 +8,11 @@
<div><p class="eyebrow">{{ job.get_status_display }}{% if job.date_posted %} · gepubliceerd {{ job.date_posted|date:"d/m/Y" }}{% endif %}</p><h1>{{ job.original_title }}</h1><p class="hero-subtitle">{{ job.employer_name }}{% if job.raw_location %} · {{ job.raw_location }}{% endif %}</p><div class="job-meta"><span><svg class="icon"><use href="#icon-briefcase"></use></svg>{{ job.get_workplace_type_display }}</span>{% for type in job.employment_types %}<span>{{ type }}</span>{% endfor %}{% if job.hours_text %}<span>{{ job.hours_text }}</span>{% endif %}{% if job.valid_through %}<span><svg class="icon"><use href="#icon-clock"></use></svg>sluit {{ job.valid_through|date:"d/m/Y" }}</span>{% endif %}</div></div>
{% if score %}<div class="hero-score"><strong>{{ score.score|floatformat:0 }}%</strong><span>{{ score.get_recommendation_display }}</span><small>confidence {{ score.confidence|floatformat:2 }}</small></div>{% endif %}
</header>
<section class="intelligence-strip" aria-label="Vacaturekerngegevens">
<div><span class="field-label">Matchkwaliteit</span>{% if score %}<strong>{{ score.score|floatformat:0 }}%</strong><small>{{ score.get_recommendation_display }}</small>{% else %}<strong>—</strong><small>Nog niet beoordeeld</small>{% endif %}</div>
<div><span class="field-label">Datakwaliteit</span><strong>{{ job.extraction_confidence|floatformat:2 }}</strong><small>Extractieconfidence</small></div>
<div><span class="field-label">Werkmodel</span><strong>{{ job.get_workplace_type_display }}</strong><small>{{ job.hours_text|default:"Uren onbekend" }}</small></div>
</section>
{% if score %}
<section class="panel"><div class="section-heading"><div><span class="section-kicker">Deterministische analyse</span><h2>Waarom deze score?</h2><p>De inhoudelijke match en betrouwbaarheid worden afzonderlijk beoordeeld.</p></div><span class="pill pill-{{ score.recommendation }}">{{ score.get_recommendation_display }}</span></div><div class="score-components">{% for name,value in score.components.items %}<div><span>{{ name|capfirst }}</span><div class="progress" aria-label="{{ name }}: {{ value|floatformat:1 }} procent"><i style="width: {{ value }}%"></i></div><strong>{{ value|floatformat:1 }}</strong></div>{% endfor %}</div><div class="two-columns"><div><h3>Waarom passend</h3><ul class="compact-list positive">{% for item in score.positives %}<li>{{ item }}</li>{% empty %}<li>Nog onvoldoende positieve signalen.</li>{% endfor %}</ul></div><div><h3>Aandachtspunten</h3><ul class="compact-list caution">{% for item in score.concerns %}<li>{{ item }}</li>{% empty %}<li>Geen specifieke aandachtspunten gedetecteerd.</li>{% endfor %}</ul></div></div>{% if score.hard_exclusions %}<div class="message warning"><strong>Harde uitsluitingen:</strong> {{ score.hard_exclusions|join:", " }}</div>{% endif %}</section>
{% else %}<section class="panel panel-subtle"><h2>Nog geen persoonlijke score</h2><p class="muted">Deze vacature heeft nog geen score voor je actieve zoekprofiel. De broninformatie en vacaturetekst blijven wel beschikbaar.</p></section>{% endif %}
+25 -12
View File
@@ -1,23 +1,36 @@
{% extends "base.html" %}
{% block title %}Vacatures · VacatureRadar{% endblock %}
{% block content %}
<header class="page-header"><div><p class="eyebrow">Nieuwe matches & archief</p><h1>Vacature-intelligentie</h1><p>Doorzoek actuele en gearchiveerde vacatures zonder de herkomst of status uit het oog te verliezen.</p></div></header>
<form method="get" class="filter-bar">
<label class="grow"><span class="field-label">Zoeken</span><input type="search" name="q" value="{{ request.GET.q }}" placeholder="Titel, werkgever, locatie of skill"></label>
<header class="page-header">
<div><p class="eyebrow">Nieuwe matches & archief</p><h1>Vacature-intelligentie</h1><p>Doorzoek actuele en gearchiveerde vacatures met score, herkomst en status in één scanbare feed.</p></div>
</header>
<form method="get" class="filter-console">
<label class="grow"><span class="field-label">Zoeken</span><span class="input-with-icon"><svg class="icon"><use href="#icon-search"></use></svg><input type="search" name="q" value="{{ request.GET.q }}" placeholder="Titel, werkgever, locatie of skill"></span></label>
<label><span class="field-label">Status</span><select name="status"><option value="">Alle statussen</option>{% for value,label in view.model.Status.choices %}<option value="{{ value }}" {% if request.GET.status|default:'active' == value %}selected{% endif %}>{{ label }}</option>{% endfor %}</select></label>
<label><span class="field-label">Werkmodel</span><select name="workplace"><option value="">Elk werkmodel</option>{% for value,label in view.model.Workplace.choices %}<option value="{{ value }}" {% if request.GET.workplace == value %}selected{% endif %}>{{ label }}</option>{% endfor %}</select></label>
<button class="button button-primary" type="submit"><svg class="icon"><use href="#icon-search"></use></svg>Filter</button>
<button class="button button-primary" type="submit"><svg class="icon"><use href="#icon-search"></use></svg>Filters toepassen</button>
</form>
<div class="section-heading"><div><span class="section-kicker">Resultaten</span><h2>{{ page_obj.paginator.count|default:jobs|length }} vacatures</h2></div></div>
<div class="job-grid">
<div class="results-toolbar"><div><span class="section-kicker">Resultaten</span><h2>{{ page_obj.paginator.count }} vacatures gevonden</h2></div><span class="view-indicator"><svg class="icon"><use href="#icon-pipeline"></use></svg>Lijstweergave</span></div>
<div class="results-feed">
{% for job in jobs %}
<article class="job-card">
<div class="job-card-head"><div><span class="pill pill-{{ job.status }}">{{ job.get_status_display }}</span><h2><a href="{% url 'jobs:detail' job.pk %}">{{ job.original_title }}</a></h2><p>{{ job.employer_name }}</p></div></div>
<div class="job-meta"><span><svg class="icon"><use href="#icon-location"></use></svg>{{ job.raw_location|default:"Locatie onbekend" }}</span><span><svg class="icon"><use href="#icon-briefcase"></use></svg>{{ job.get_workplace_type_display }}</span><span><svg class="icon"><use href="#icon-clock"></use></svg>gevonden {{ job.first_seen|date:"d/m/Y" }}</span></div>
{% if job.skills_required %}<p class="muted small">Vereiste skills: {{ job.skills_required|slice:":3"|join:", " }}</p>{% endif %}
<div class="job-actions"><a class="button button-secondary" href="{% url 'jobs:detail' job.pk %}">Bekijk analyse <svg class="icon"><use href="#icon-arrow"></use></svg></a></div>
<article class="result-card {% if job.match_recommendation == 'strong' %}strong-match{% endif %}">
<div class="result-accent" aria-hidden="true"></div>
<div class="result-body">
<div class="result-heading">
<div>
<div class="title-row"><span class="pill pill-{{ job.status }}">{{ job.get_status_display }}</span>{% if job.match_recommendation %}<span class="pill pill-{{ job.match_recommendation }}">{% if job.match_recommendation == 'strong' %}Sterke match{% elif job.match_recommendation == 'possible' %}Mogelijke match{% elif job.match_recommendation == 'weak' %}Lage match{% else %}Verborgen{% endif %}</span>{% endif %}</div>
<h2><a href="{% url 'jobs:detail' job.pk %}">{{ job.original_title }}</a></h2>
<p>{{ job.employer_name }}</p>
</div>
{% if job.match_score is not None %}<div class="score-block"><strong>{{ job.match_score|floatformat:0 }}%</strong><small>confidence {{ job.match_confidence|floatformat:2 }}</small></div>{% endif %}
</div>
<div class="job-meta"><span><svg class="icon"><use href="#icon-location"></use></svg>{{ job.raw_location|default:"Locatie onbekend" }}</span><span><svg class="icon"><use href="#icon-briefcase"></use></svg>{{ job.get_workplace_type_display }}</span><span><svg class="icon"><use href="#icon-clock"></use></svg>gevonden {{ job.first_seen|date:"d/m/Y" }}</span>{% if job.direct_employer %}<span>Directe werkgever</span>{% endif %}</div>
{% if job.skills_required %}<p class="result-summary"><strong>Herkenbare skills:</strong> {{ job.skills_required|slice:":4"|join:", " }}</p>{% elif job.description_text %}<p class="result-summary">{{ job.description_text|truncatewords:24 }}</p>{% endif %}
</div>
<div class="result-actions"><a class="button button-primary" href="{% url 'jobs:detail' job.pk %}">Bekijk analyse <svg class="icon"><use href="#icon-arrow"></use></svg></a></div>
</article>
{% empty %}<div class="empty-state"><div class="empty-icon"><svg class="icon"><use href="#icon-search"></use></svg></div><h2>Geen vacatures gevonden</h2><p>Pas de zoekterm of filters aan. Gearchiveerde resultaten verschijnen wanneer je de statusfilter leegmaakt.</p></div>{% endfor %}
{% empty %}<div class="empty-state compact-empty"><div class="empty-icon"><svg class="icon"><use href="#icon-search"></use></svg></div><h2>Geen vacatures gevonden</h2><p>Pas de zoekterm of filters aan. Gearchiveerde resultaten verschijnen wanneer je de statusfilter leegmaakt.</p></div>{% endfor %}
</div>
{% if is_paginated %}<nav class="pagination" aria-label="Paginering">{% if page_obj.has_previous %}<a class="button button-ghost" href="?page={{ page_obj.previous_page_number }}&q={{ request.GET.q }}&status={{ request.GET.status }}&workplace={{ request.GET.workplace }}">Vorige</a>{% endif %}<span>Pagina {{ page_obj.number }} van {{ page_obj.paginator.num_pages }}</span>{% if page_obj.has_next %}<a class="button button-ghost" href="?page={{ page_obj.next_page_number }}&q={{ request.GET.q }}&status={{ request.GET.status }}&workplace={{ request.GET.workplace }}">Volgende</a>{% endif %}</nav>{% endif %}
{% endblock %}
+17 -7
View File
@@ -1,11 +1,21 @@
{% extends "base.html" %}
{% block title %}{{ object.name }} bewerken · VacatureRadar{% endblock %}
{% block content %}
<header class="page-header"><div><p class="eyebrow">Zoekprofiel · versie {{ object.version }}</p><h1>{{ object.name }} bewerken</h1><p>Je voorkeuren sturen ranking en uitsluiting; bronbeleid en veiligheidsregels blijven automatisch bewaakt.</p></div><span class="pill {% if object.is_active %}pill-active{% endif %}">{% if object.is_active %}Actief profiel{% else %}Inactief profiel{% endif %}</span></header>
<form method="post" class="panel form-panel">{% csrf_token %}{% if form.non_field_errors %}<div class="message error">{{ form.non_field_errors }}</div>{% endif %}<div class="form-grid">
{% for field in form %}
{% if field.name == 'name' %}<div class="form-section"><h2>Profielbasis</h2><p>Naam, status en thuislocatie voor afstandsbepaling.</p></div>{% elif field.name == 'desired_titles_text' %}<div class="form-section"><h2>Functies en vaardigheden</h2><p>Eén titel of skill per regel; uitsluitingen hebben voorrang op voorkeuren.</p></div>{% elif field.name == 'allowed_employment_types' %}<div class="form-section"><h2>Werkvorm en regio</h2><p>Lijstvelden gebruiken JSON-notatie, bijvoorbeeld ["hybrid", "on_site"].</p></div>{% elif field.name == 'recommendation_threshold' %}<div class="form-section"><h2>Scoring en automatisering</h2><p>Topmatch moet minstens gelijk zijn aan de aanbevelingsdrempel.</p></div>{% endif %}
<label class="{% if field.name|slice:'-5:' == '_text' %}span-2{% endif %}"><span>{{ field.label }}</span>{{ field }}{% if field.help_text %}<small>{{ field.help_text }}</small>{% endif %}{% for error in field.errors %}<small class="field-error">{{ error }}</small>{% endfor %}</label>
{% endfor %}
</div><div class="form-actions"><a class="button button-ghost" href="{% url 'profiles:list' %}">Annuleren</a><button class="button button-primary" type="submit">Profiel opslaan</button></div></form>
<header class="page-header"><div><p class="eyebrow">Zoekprofiel · versie {{ object.version }}</p><h1>Configureer je radar</h1><p>Verfijn {{ object.name }} zodat alleen relevante, uitlegbare kansen bovenaan verschijnen.</p></div><span class="pill {% if object.is_active %}pill-active{% endif %}">{% if object.is_active %}Actief profiel{% else %}Inactief profiel{% endif %}</span></header>
<div class="editor-layout">
<form id="profile-form" method="post" class="panel form-panel profile-editor">{% csrf_token %}
{% if form.non_field_errors %}<div class="message error">{{ form.non_field_errors }}</div>{% endif %}
<div class="form-grid">
{% for field in form %}
{% if field.name == 'name' %}<div class="form-section"><span class="form-section-icon"><svg class="icon"><use href="#icon-profile"></use></svg></span><div><h2>Profielbasis</h2><p>Je postcode volstaat; technische locatievelden worden op de achtergrond afgeleid.</p></div></div>{% elif field.name == 'desired_titles' %}<div class="form-section"><span class="form-section-icon"><svg class="icon"><use href="#icon-briefcase"></use></svg></span><div><h2>Gewenste rollen & technologie</h2><p>Kies herkenbare opties. Uitsluitingen blijven altijd doorslaggevend.</p></div></div>{% elif field.name == 'allowed_employment_types' %}<div class="form-section"><span class="form-section-icon"><svg class="icon"><use href="#icon-location"></use></svg></span><div><h2>Werkvorm, regio & reistijd</h2><p>Selecteer contractvormen, werkmodellen en regio's zonder technische notatie.</p></div></div>{% elif field.name == 'recommendation_threshold' %}<div class="form-section"><span class="form-section-icon"><svg class="icon"><use href="#icon-radar"></use></svg></span><div><h2>Scoring & automatisering</h2><p>Een topmatch ligt altijd boven de gewone aanbevelingsdrempel.</p></div></div>{% endif %}
{% if field.field.widget.allow_multiple_selected %}<fieldset class="choice-field span-2"><legend>{{ field.label }}</legend>{{ field }}{% if field.help_text %}<small>{{ field.help_text }}</small>{% endif %}{% for error in field.errors %}<small class="field-error">{{ error }}</small>{% endfor %}</fieldset>{% else %}<label><span>{{ field.label }}</span>{{ field }}{% if field.help_text %}<small>{{ field.help_text }}</small>{% endif %}{% for error in field.errors %}<small class="field-error">{{ error }}</small>{% endfor %}</label>{% endif %}
{% endfor %}
</div>
<div class="form-actions"><a class="button button-ghost" href="{% url 'profiles:list' %}">Annuleren</a><button class="button button-primary" type="submit">Instellingen opslaan</button></div>
</form>
<aside class="editor-rail" aria-label="Profielsamenvatting">
<section class="panel radar-preview"><div class="title-row"><span class="section-kicker">Radarprofiel</span><span class="pill {% if object.is_active %}pill-active{% endif %}">{% if object.is_active %}Live{% else %}Pauze{% endif %}</span></div><h2>{{ object.name }}</h2><p class="muted">De huidige opgeslagen configuratie.</p><div class="preview-score"><strong>{{ object.top_match_threshold }}%</strong><span>Topmatch vanaf</span></div><div class="rail-list"><div class="rail-item"><span>Thuislocatie</span><strong>{% if object.home_postal_code %}{{ object.home_postal_code }}{% if object.home_municipality %} · {{ object.home_municipality }}{% endif %}{% else %}Niet ingesteld{% endif %}</strong></div><div class="rail-item"><span>Gewenste rollen</span><strong>{{ object.desired_titles|length }}</strong></div><div class="rail-item"><span>Gewenste skills</span><strong>{{ object.desired_skills|length }}</strong></div><div class="rail-item"><span>Maximumafstand</span><strong>{{ object.max_distance_km }} km</strong></div><div class="rail-item"><span>Gecontroleerd leren</span><strong>{% if object.learning_enabled %}Aan{% else %}Uit{% endif %}</strong></div></div></section>
<section class="panel rail-panel"><span class="section-kicker">Veilige grenzen</span><h2>Deterministisch eerst</h2><p class="muted">Harde uitsluitingen en bronbeleid veranderen niet door AI of feedback.</p><button class="button button-primary button-block" type="submit" form="profile-form">Instellingen opslaan</button></section>
</aside>
</div>
{% endblock %}
+1 -1
View File
@@ -2,5 +2,5 @@
{% block title %}Zoekprofielen · VacatureRadar{% endblock %}
{% block content %}
<header class="page-header"><div><p class="eyebrow">Persoonlijke regels</p><h1>Zoekprofiel</h1><p>Harde grenzen veranderen nooit automatisch; zachte voorkeuren kunnen alleen gecontroleerd leren.</p></div></header>
<div class="card-list">{% for profile in profiles %}<article class="panel profile-card {% if profile.is_active %}strong-match{% endif %}"><div><div class="title-row"><span class="pill {% if profile.is_active %}pill-active{% endif %}">{% if profile.is_active %}Actief{% else %}Inactief{% endif %}</span><h2>{{ profile.name }}</h2></div><p class="muted">{{ profile.desired_titles|join:", "|default:"Nog geen functietitels" }}</p><div class="job-meta"><span><svg class="icon"><use href="#icon-location"></use></svg>max. {{ profile.max_distance_km }} km</span><span><svg class="icon"><use href="#icon-clock"></use></svg>digest {{ profile.digest_time|time:"H:i" }}</span><span>versie {{ profile.version }}</span><span>leren {% if profile.learning_enabled %}aan{% else %}uit{% endif %}</span></div></div><div class="job-actions"><a class="button button-primary" href="{% url 'profiles:edit' profile.pk %}">Profiel bewerken</a>{% if not profile.is_active %}<form method="post" action="{% url 'profiles:activate' profile.pk %}">{% csrf_token %}<button class="button button-ghost" type="submit">Activeren</button></form>{% endif %}</div></article>{% empty %}<div class="empty-state"><div class="empty-icon"><svg class="icon"><use href="#icon-profile"></use></svg></div><h2>Geen profiel beschikbaar</h2><p>Voer de bootstrapopdracht uit om het standaardprofiel aan te maken.</p></div>{% endfor %}</div>
<div class="card-list">{% for profile in profiles %}<article class="panel profile-card {% if profile.is_active %}strong-match{% endif %}"><div><div class="title-row"><span class="pill {% if profile.is_active %}pill-active{% endif %}">{% if profile.is_active %}Actief{% else %}Inactief{% endif %}</span><h2>{{ profile.name }}</h2></div><p class="muted">{{ profile.desired_titles|join:", "|default:"Nog geen functietitels" }}</p><div class="job-meta"><span><svg class="icon"><use href="#icon-location"></use></svg>{% if profile.home_postal_code %}{{ profile.home_postal_code }}{% if profile.home_municipality %} · {{ profile.home_municipality }}{% endif %}{% else %}geen thuispostcode{% endif %}</span><span>max. {{ profile.max_distance_km }} km</span><span><svg class="icon"><use href="#icon-clock"></use></svg>digest {{ profile.digest_time|time:"H:i" }}</span><span>versie {{ profile.version }}</span><span>leren {% if profile.learning_enabled %}aan{% else %}uit{% endif %}</span></div></div><div class="job-actions"><a class="button button-primary" href="{% url 'profiles:edit' profile.pk %}">Profiel bewerken</a>{% if not profile.is_active %}<form method="post" action="{% url 'profiles:activate' profile.pk %}">{% csrf_token %}<button class="button button-ghost" type="submit">Activeren</button></form>{% endif %}</div></article>{% empty %}<div class="empty-state"><div class="empty-icon"><svg class="icon"><use href="#icon-profile"></use></svg></div><h2>Geen profiel beschikbaar</h2><p>Voer de bootstrapopdracht uit om het standaardprofiel aan te maken.</p></div>{% endfor %}</div>
{% endblock %}
+63 -9
View File
@@ -6,12 +6,68 @@
<section class="source-overview" aria-label="Bronoverzicht">
<article class="stat-card signal"><span class="kpi-label">Actieve bronnen</span><strong>{{ active_source_count }}</strong><small>van {{ source_total_count }} geregistreerd</small></article>
<article class="stat-card"><span class="kpi-label">Policyreview</span><strong>{{ review_pending_count }}</strong><small>bronnen te beoordelen</small></article>
<article class="stat-card"><span class="kpi-label">Werkgeverswaaklijst</span><strong>{{ watchlist_count }}</strong><small>interessante regionale werkgevers</small></article>
<article class="stat-card"><span class="kpi-label">Veilige modus</span><strong>AAN</strong><small>policy + SSRF-validatie</small></article>
</section>
<section class="panel">
<div class="section-heading"><div><span class="section-kicker">Gecontroleerde invoer</span><h2>Handmatige import</h2><p>Gebruik een publieke URL of plak vacaturetekst. URL-import passeert altijd het bronbeleid.</p></div></div>
<section class="panel" aria-labelledby="regional-scope-title">
<div class="section-heading"><div><span class="section-kicker">Regionale werkgeversradar</span><h2 id="regional-scope-title">2400 Mol · straal 40 km</h2><p>De werkgeverscatalogus is rond Mol gereviewd. Afstanden gebruiken lokale postcodecoördinaten en blijven afhankelijk van de concrete werklocatie in de vacature.</p></div><span class="pill {% if active_search_profile.home_latitude != None and active_search_profile.home_longitude != None %}pill-success{% else %}pill-review{% endif %}">{% if active_search_profile.home_latitude != None and active_search_profile.home_longitude != None %}Afstanden actief{% else %}Profiel controleren{% endif %}</span></div>
{% if active_search_profile %}<p class="muted">Actief profiel: postcode {{ active_search_profile.home_postal_code|default:"niet ingevuld" }} · {{ active_search_profile.home_municipality|default:"gemeente onbekend" }} · maximaal {{ active_search_profile.max_distance_km }} km.{% if active_search_profile.home_latitude == None or active_search_profile.home_longitude == None %} Coördinaten ontbreken nog; vacatureafstanden worden niet geschat.{% else %} Postcodecentrum gecontroleerd via de lokale GeoNames-dataset.{% endif %}</p>{% endif %}
</section>
{% if watchlist_sources %}<section class="panel" aria-labelledby="employer-watchlist-title">
<div class="section-heading"><div><span class="section-kicker">Ook volgen zonder vacature</span><h2 id="employer-watchlist-title">Interessante werkgeverswaaklijst</h2><p>Deze werkgevers liggen in de ruime regio en blijven zichtbaar, ook wanneer er nu geen geschikte vacature is. Ze worden pas automatisch gescand na een stabiele, veilige parserproef.</p></div><span class="pill pill-review">{{ watchlist_count }} gevolgd</span></div>
<div class="source-alert-grid">
{% for source in watchlist_sources %}<article class="source-alert-card"><div><div class="title-row"><strong>{{ source.name }}</strong><span class="pill pill-review">Waaklijst</span></div><p>{{ source.metadata.municipality_scope }} · {{ source.metadata.watchlist_reason }}</p><small>{{ source.metadata.current_observation }}</small></div><a class="button button-secondary" href="{{ source.metadata.public_job_page|default:source.base_url }}" target="_blank" rel="noopener noreferrer">Open carrièrepagina</a></article>{% endfor %}
</div>
</section>{% endif %}
<section class="panel" aria-labelledby="platform-alerts-title">
<div class="section-heading"><div><span class="section-kicker">Brede marktdekking</span><h2 id="platform-alerts-title">Platformmailboxen</h2><p>Koppel per jobboard een aparte alertmailbox. De scheduler controleert elke vijf minuten welke mailbox volgens haar eigen interval aan de beurt is.</p></div><span class="pill {% if platform_alert_ingress_ready %}pill-success{% else %}pill-review{% endif %}">{{ mailbox_connections|length }} gekoppeld</span></div>
{% if not mailbox_credential_key_ready %}<p class="message error">Credentialopslag is nog vergrendeld. Stel eerst <code>MAILBOX_CREDENTIAL_KEYS</code> in; app-wachtwoorden worden nooit onversleuteld opgeslagen.</p>{% endif %}
<div class="source-alert-grid">
{% for connection in mailbox_connections %}
<article class="source-alert-card">
<div><div class="title-row"><strong>{{ connection.get_platform_display }}-mailbox</strong><span class="pill {% if connection.enabled and not connection.last_error_category %}pill-success{% elif connection.last_error_category %}pill-failed{% else %}pill-review{% endif %}">{% if connection.enabled %}{% if connection.last_error_category %}Aandacht nodig{% else %}Actief{% endif %}{% else %}Gepauzeerd{% endif %}</span></div>
<p>{{ connection.get_provider_display }} · {{ connection.username }} · {{ connection.get_poll_interval_minutes_display|lower }}</p>
<small>{% if connection.last_success_at %}Laatste import {{ connection.last_success_at|date:"d/m/Y H:i" }}.{% else %}Nog geen geslaagde import.{% endif %} {% if connection.enabled %}Volgende controle {{ connection.next_poll_at|date:"d/m/Y H:i" }}.{% endif %}</small>
{% if connection.last_error_message %}<p class="message error">{{ connection.last_error_message }}</p>{% endif %}</div>
<div class="job-actions">{% if connection.enabled %}<form method="post" action="{% url 'sources:sync_platform_alerts' connection.pk %}">{% csrf_token %}<button class="button button-primary" type="submit">Nu synchroniseren</button></form>{% endif %}<a class="button button-secondary" href="{% url 'sources:mailbox_edit' connection.pk %}#mailbox-form">Wijzigen</a><form method="post" action="{% url 'sources:mailbox_toggle' connection.pk %}">{% csrf_token %}<button class="button button-ghost" type="submit">{% if connection.enabled %}Pauzeren{% else %}Activeren{% endif %}</button></form></div>
</article>
{% empty %}
<article class="source-alert-card"><div><strong>Nog geen mailbox gekoppeld</strong><p>Koppel hieronder per gewenst jobboard de mailbox die de bijbehorende vacaturealerts ontvangt.</p></div></article>
{% endfor %}
</div>
<details id="mailbox-form" class="import-disclosure mailbox-disclosure" {% if editing_mailbox or mailbox_form.errors %}open{% endif %}>
<summary><span class="source-icon"><svg class="icon"><use href="#icon-sources"></use></svg></span><span><span class="section-kicker">Versleutelde IMAP-koppeling</span><strong>{% if editing_mailbox %}{{ editing_mailbox.get_platform_display }}-mailbox wijzigen{% else %}Platformmailbox koppelen{% endif %}</strong><small>Gmail, Outlook / Microsoft 365 of aangepaste IMAP</small></span><svg class="icon disclosure-icon"><use href="#icon-chevron"></use></svg></summary>
<div class="import-disclosure-body"><form method="post" action="{% if editing_mailbox %}{% url 'sources:mailbox_edit' editing_mailbox.pk %}{% else %}{% url 'sources:mailbox_add' %}{% endif %}">{% csrf_token %}
<div class="form-grid mailbox-form-grid">
<label><span class="field-label">{{ mailbox_form.platform.label }}</span>{{ mailbox_form.platform }}{% for error in mailbox_form.platform.errors %}<small class="field-error">{{ error }}</small>{% endfor %}</label>
<label><span class="field-label">{{ mailbox_form.provider.label }}</span>{{ mailbox_form.provider }}{% for error in mailbox_form.provider.errors %}<small class="field-error">{{ error }}</small>{% endfor %}</label>
<label><span class="field-label">{{ mailbox_form.custom_host.label }}</span>{{ mailbox_form.custom_host }}<small class="muted">Alleen invullen bij een andere IMAP-provider.</small>{% for error in mailbox_form.custom_host.errors %}<small class="field-error">{{ error }}</small>{% endfor %}</label>
<label><span class="field-label">{{ mailbox_form.port.label }}</span>{{ mailbox_form.port }}{% for error in mailbox_form.port.errors %}<small class="field-error">{{ error }}</small>{% endfor %}</label>
<label><span class="field-label">{{ mailbox_form.username.label }}</span>{{ mailbox_form.username }}{% for error in mailbox_form.username.errors %}<small class="field-error">{{ error }}</small>{% endfor %}</label>
<label><span class="field-label">{{ mailbox_form.password.label }}</span>{{ mailbox_form.password }}<small class="muted">Gebruik een app-wachtwoord; dit veld wordt nooit teruggetoond.</small>{% for error in mailbox_form.password.errors %}<small class="field-error">{{ error }}</small>{% endfor %}</label>
<label><span class="field-label">{{ mailbox_form.mailbox.label }}</span>{{ mailbox_form.mailbox }}{% for error in mailbox_form.mailbox.errors %}<small class="field-error">{{ error }}</small>{% endfor %}</label>
<label><span class="field-label">{{ mailbox_form.poll_interval_minutes.label }}</span>{{ mailbox_form.poll_interval_minutes }}{% for error in mailbox_form.poll_interval_minutes.errors %}<small class="field-error">{{ error }}</small>{% endfor %}</label>
</div>
<label class="checkbox-row">{{ mailbox_form.enabled }}<span>{{ mailbox_form.enabled.label }}</span></label>
{% for error in mailbox_form.non_field_errors %}<div class="message error">{{ error }}</div>{% endfor %}
<div class="job-actions"><button class="button button-primary" type="submit" {% if not mailbox_credential_key_ready %}disabled{% endif %}>{% if editing_mailbox %}Wijzigingen opslaan{% else %}Mailbox koppelen en testen{% endif %}</button>{% if editing_mailbox %}<a class="button button-ghost" href="{% url 'sources:list' %}">Annuleren</a>{% endif %}</div>
</form></div>
</details>
<div class="source-alert-grid platform-help-grid">
{% for platform in platform_alerts %}<article class="source-alert-card"><div><strong>{{ platform.label }}-alert instellen</strong><p>{{ platform.help_text }} VacatureRadar verwerkt alleen links van {{ platform.label }} en logt niet in op het platform.</p></div><a class="button button-secondary" href="{{ platform.alert_url }}" target="_blank" rel="noopener noreferrer">Open {{ platform.label }}</a></article>{% endfor %}
</div>
</section>
<details class="panel import-disclosure" {% if manual_import_form.errors %}open{% endif %}>
<summary><span class="source-icon"><svg class="icon"><use href="#icon-upload"></use></svg></span><span><span class="section-kicker">Gecontroleerde invoer</span><strong>Handmatige import</strong><small>Publieke URL of vacaturetekst toevoegen</small></span><svg class="icon disclosure-icon"><use href="#icon-chevron"></use></svg></summary>
<div class="import-disclosure-body"><p class="muted">URL-import passeert altijd het bronbeleid en de SSRF-validatie.</p>
<form id="manual-import-form" method="post" action="{% url 'sources:manual_import' %}" data-paste-limit="{{ manual_import_paste_max_bytes }}">{% csrf_token %}
<div class="import-layout">
<label><span class="field-label">{{ manual_import_form.source_url.label }}</span>{{ manual_import_form.source_url }}{% for error in manual_import_form.source_url.errors %}<small class="field-error">{{ error }}</small>{% endfor %}<small class="muted">Laat leeg wanneer je tekst plakt.</small></label>
@@ -20,19 +76,17 @@
{% for error in manual_import_form.non_field_errors %}<div class="message error">{{ error }}</div>{% endfor %}
<div class="job-actions"><button class="button button-primary" type="submit"><svg class="icon"><use href="#icon-upload"></use></svg>Import uitvoeren</button><button id="copy-bookmarklet" class="button button-ghost" type="button">Bookmarklet kopiëren</button><a id="manual-bookmarklet" class="button button-link" href="#" data-manual-import-endpoint="{{ manual_import_bookmarklet_endpoint }}">Bookmarklet installeren</a></div>
</form>
<textarea id="manual-bookmarklet-code" rows="2" readonly class="sr-only" aria-label="Bookmarklet endpoint">{{ manual_import_bookmarklet_endpoint }}</textarea>
</section>
<textarea id="manual-bookmarklet-code" rows="2" readonly class="sr-only" aria-label="Bookmarklet endpoint">{{ manual_import_bookmarklet_endpoint }}</textarea></div>
</details>
{% if manual_import_result %}<section class="panel"><span class="pill pill-success">Importresultaat</span><h2>Importresultaat</h2><p><strong>Bron:</strong> {{ manual_import_result.source_name }} ({{ manual_import_result.source_id }})</p><dl class="definition-grid"><div><dt>Modus</dt><dd>Modus: {{ manual_import_result.mode }}</dd></div><div><dt>Herkenbare items</dt><dd>Herkenbare items: {{ manual_import_result.extracted_count }}</dd></div><div><dt>Nieuwe vacatures</dt><dd>Nieuwe vacatures: {{ manual_import_result.created_count }}</dd></div><div><dt>Duplicaten</dt><dd>Duplicaten: {{ manual_import_result.duplicate_count }}</dd></div></dl>{% if manual_import_result.warnings %}<h3>Waarschuwingen</h3><ul class="compact-list caution">{% for warning in manual_import_result.warnings %}<li>{{ warning }}</li>{% endfor %}</ul>{% endif %}{% if manual_import_result.jobs %}<h3>Geïmporteerde vacatures</h3><ul class="source-list">{% for job in manual_import_result.jobs %}<li>{% if job.id %}<a href="{% url 'jobs:detail' job.id %}">{{ job.title }}</a>{% else %}{{ job.title }}{% endif %}<span>{{ job.employer }}</span></li>{% endfor %}</ul>{% endif %}</section>{% endif %}
<form id="bulk-action-form" method="post" action="{% url 'sources:bulk' %}" class="filter-bar">{% csrf_token %}<label><span class="field-label">Bulkactie voor selectie</span><select id="action" name="action"><option value="approve_to_trial">Kandidaten naar proefrun</option><option value="allow">Toestaan</option><option value="pause">Pauzeren</option><option value="dismiss">Afwijzen</option></select></label><button class="button button-secondary" type="submit">Uitvoeren</button></form>
<div class="section-heading"><div><span class="section-kicker">Endpoints</span><h2>{{ source_total_count }} geregistreerde bronnen</h2><p>Status is gebaseerd op bronbeleid en de meest recente run, indien beschikbaar.</p></div></div>
<div class="section-heading source-list-heading"><div><span class="section-kicker">Scan-endpoints</span><h2>{{ source_total_count }} geregistreerde bronnen</h2><p>Status is gebaseerd op bronbeleid en de meest recente run.</p></div><form id="bulk-action-form" method="post" action="{% url 'sources:bulk' %}" class="inline-bulk-action">{% csrf_token %}<label><span class="sr-only">Bulkactie voor selectie</span><select id="action" name="action"><option value="approve_to_trial">Naar proefrun</option><option value="allow">Toestaan</option><option value="pause">Pauzeren</option><option value="dismiss">Afwijzen</option></select></label><button class="button button-secondary" type="submit">Uitvoeren</button></form></div>
<div class="card-list">
{% for source in sources %}{% with latest_run=source.runs.all|first %}
<article class="source-card">
<div class="source-card-head">
<div class="source-identity"><span class="source-icon"><svg class="icon"><use href="#icon-sources"></use></svg></span><div><div class="title-row">{% if source.policy != 'deny' %}<input type="checkbox" name="source_ids" form="bulk-action-form" value="{{ source.pk }}" aria-label="Selecteer {{ source.name }}">{% endif %}<h2>{{ source.name }}</h2></div><p>{{ source.get_source_type_display }} · {{ source.domain|default:"Geen domein" }}</p><div class="job-meta"><span>{{ source.get_policy_display }}</span>{% if latest_run %}<span>laatste run {{ latest_run.started_at|date:"d/m/Y H:i" }}</span><span>{{ latest_run.extracted_count }} gevonden</span>{% else %}<span>nog niet gestart</span>{% endif %}</div></div></div>
<div class="source-identity"><span class="source-icon"><svg class="icon"><use href="#icon-sources"></use></svg></span><div><div class="title-row">{% if source.policy != 'deny' %}<input type="checkbox" name="source_ids" form="bulk-action-form" value="{{ source.pk }}" aria-label="Selecteer {{ source.name }}">{% endif %}<h2>{{ source.name }}</h2></div><p>{{ source.get_source_type_display }} · {{ source.domain|default:"Geen domein" }}</p><div class="job-meta"><span>{{ source.get_policy_display }}</span>{% if latest_run %}<span>laatste run {{ latest_run.started_at|date:"d/m/Y H:i" }}</span><span>{{ source.job_count }} vacatures in radar</span>{% else %}<span>nog niet gestart</span>{% endif %}</div></div></div>
<div class="source-actions"><span class="pill {% if source.policy == 'deny' or source.status == 'quarantined' or latest_run and latest_run.status == 'failed' %}pill-failed{% elif latest_run and latest_run.status == 'partial' %}pill-partial{% elif latest_run and latest_run.status == 'success' %}pill-success{% else %}pill-review{% endif %}">{% if source.policy == 'deny' %}Geblokkeerd{% elif source.status == 'quarantined' %}Quarantaine{% elif latest_run %}{{ latest_run.get_status_display }}{% else %}Geen run{% endif %}</span><button class="icon-button" type="button" data-disclosure aria-controls="source-details-{{ forloop.counter }}" aria-expanded="false" aria-label="Details van {{ source.name }} tonen"><svg class="icon disclosure-icon"><use href="#icon-chevron"></use></svg></button></div>
</div>
{% if source.discovery_evidence %}<div class="source-details"><span class="field-label">Ontdekkingsevidence</span><ul class="compact-list">{% for evidence in source.discovery_evidence %}<li>{{ evidence.label|default:"Ontdekte bron" }}: {{ evidence.url }}</li>{% endfor %}</ul></div>{% endif %}
+11 -7
View File
@@ -1,11 +1,15 @@
{% extends "base.html" %}
{% block title %}Systeemstatus · VacatureRadar{% endblock %}
{% block content %}
<header class="page-header"><div><p class="eyebrow">Automatisering centrum</p><h1>Systeemstatus</h1><p>Readiness en echte database-aantallen. Secrets, fictieve uptime en niet-gemeten latency worden nooit getoond.</p></div><span class="pill {% if health.ok %}pill-success{% else %}pill-failed{% endif %}">{% if health.ok %}Operationeel{% else %}Aandacht nodig{% endif %}</span></header>
<section class="stats-grid"><article class="stat-card signal"><span class="kpi-label">Readiness</span><strong>{% if health.ok %}OK{% else %}FOUT{% endif %}</strong><small>database: {{ health.checks.database }}</small></article>{% for row in source_summary %}<article class="stat-card"><span class="kpi-label">Bronnen · {{ row.status }}</span><strong>{{ row.total }}</strong><small>geregistreerde bronnen</small></article>{% endfor %}{% for row in job_summary %}<article class="stat-card"><span class="kpi-label">Vacatures · {{ row.status }}</span><strong>{{ row.total }}</strong><small>vacatures in database</small></article>{% endfor %}</section>
<div class="section-heading"><div><span class="section-kicker">Operaties</span><h2>Veilige beheeropdrachten</h2><p>Voer deze uitsluitend uit vanaf een vertrouwde shell op de server.</p></div></div>
<section class="panel"><pre><code>make verify
./scripts/backup.sh
./scripts/restore.sh /pad/naar/backup.sql.gz
uv run python manage.py seed_sources</code></pre></section>
<header class="page-header"><div><p class="eyebrow">Automatisering centrum</p><h1>Radarbesturing</h1><p>Readiness, bronruns en veilige beheeracties op basis van actuele systeemdata.</p></div><span class="pill {% if health.ok %}pill-success{% else %}pill-failed{% endif %}">{% if health.ok %}Operationeel{% else %}Aandacht nodig{% endif %}</span></header>
<section class="system-hero">
<article class="system-readiness {% if health.ok %}is-healthy{% endif %}"><span class="source-icon"><svg class="icon"><use href="#icon-system"></use></svg></span><div><span class="field-label">Systeemreadiness</span><h2>{% if health.ok %}Radar is operationeel{% else %}Controle vereist{% endif %}</h2><p>Databasecontrole: {{ health.checks.database }}</p></div><span class="signal-dot" aria-hidden="true"></span></article>
<div class="system-counts">{% for row in source_summary %}<article><span class="field-label">Bronnen · {{ row.status }}</span><strong>{{ row.total }}</strong></article>{% endfor %}{% for row in job_summary %}<article><span class="field-label">Vacatures · {{ row.status }}</span><strong>{{ row.total }}</strong></article>{% endfor %}</div>
</section>
<div class="section-heading"><div><span class="section-kicker">Brontelemetrie</span><h2>Recente rungezondheid</h2><p>Meetwaarden uit maximaal de laatste 30 runs per bron.</p></div><a class="section-link" href="{% url 'sources:list' %}">Bronnen beheren <svg class="icon"><use href="#icon-arrow"></use></svg></a></div>
<div class="health-grid">{% for row in source_health %}<article class="health-card"><div class="health-card-head"><div><span class="field-label">{{ row.source_type }}</span><h3>{{ row.source_name }}</h3></div><span class="pill {% if row.success_ratio >= 0.8 %}pill-success{% elif row.monitored_runs == 0 %}pill-review{% else %}pill-failed{% endif %}">{% if row.monitored_runs == 0 %}Geen runs{% else %}Ratio {{ row.success_ratio|floatformat:2 }}{% endif %}</span></div><dl><div><dt>Runs</dt><dd>{{ row.monitored_runs }}</dd></div><div><dt>Gevonden</dt><dd>{{ row.extracted_count }}</dd></div><div><dt>Bijgewerkt</dt><dd>{{ row.updated_count }}</dd></div><div><dt>Latency</dt><dd>{% if row.avg_latency_ms is not None %}{{ row.avg_latency_ms|floatformat:0 }} ms{% else %}—{% endif %}</dd></div></dl>{% if row.last_health_reason %}<p class="attention">{{ row.last_health_reason }}</p>{% endif %}</article>{% empty %}<div class="empty-state compact-empty"><h2>Nog geen brontelemetrie</h2><p>Bronruns verschijnen hier zodra een toegestane bron is uitgevoerd.</p></div>{% endfor %}</div>
<div class="section-heading"><div><span class="section-kicker">Operaties</span><h2>Veilige beheeropdrachten</h2><p>Uitsluitend uitvoeren vanaf een vertrouwde shell op de server.</p></div></div>
<section class="panel command-panel"><div><span class="field-label">Verificatie</span><pre><code>make verify</code></pre></div><div><span class="field-label">Back-up</span><pre><code>./scripts/backup.sh</code></pre></div><div><span class="field-label">Herstel</span><pre><code>./scripts/restore.sh /pad/naar/backup.sql.gz</code></pre></div><div><span class="field-label">Bronnen initialiseren</span><pre><code>uv run python manage.py seed_sources</code></pre></div></section>
{% endblock %}