Offers are now de-duplicated by normalized URL (tracking params stripped)
in addition to (quelle, external_id), so the same posting never lands
twice — even re-scraped under a new id. Deleting an offer (web or API)
auto-blacklists it, so it can never reappear.
lib/blacklist.js provides shared normalization + matching. Manual entries
can block a URL, a whole domain, a company, or a company+title posting
(gender-marker tolerant). New /blacklist page lists and manages entries.
REST API: GET/POST /joboffers/blacklist, DELETE /joboffers/blacklist/{id};
POST /joboffers returns 409 when blacklisted; DELETE /joboffers/{id}
auto-blacklists (opt out with ?blacklist=false). Swagger updated with the
new paths and schemas.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
102 lines
7.6 KiB
Plaintext
102 lines
7.6 KiB
Plaintext
<header class="bg-gradient-to-r from-blue-700 to-blue-900 dark:from-gray-800 dark:to-gray-900 shadow-lg">
|
|
<div class="container mx-auto px-4 py-4">
|
|
<div class="flex items-center justify-between">
|
|
<a href="/" class="flex items-center space-x-3">
|
|
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
|
</svg>
|
|
<h1 class="text-xl font-bold text-white">Bewerbungs-Tracker</h1>
|
|
</a>
|
|
|
|
<div class="flex items-center space-x-4">
|
|
<!-- Postfach (unlinked incoming e-mails) link -->
|
|
<a href="/postfach"
|
|
class="relative flex items-center gap-1.5 px-3 py-2 rounded-md bg-white/20 hover:bg-white/30 transition-colors text-white text-sm font-medium"
|
|
title="E-Mails im Postfach, die noch keiner Bewerbung zugeordnet sind">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
|
|
</svg>
|
|
<span class="hidden sm:inline">Postfach</span>
|
|
<span id="unassignedBadge" class="hidden absolute -top-1 -right-1 min-w-[18px] h-[18px] px-1 flex items-center justify-center rounded-full bg-red-500 text-white text-[10px] font-bold ring-2 ring-blue-800 dark:ring-gray-900">0</span>
|
|
</a>
|
|
|
|
<!-- Jobangebote (ingested via third-party REST API) link -->
|
|
<a href="/jobangebote"
|
|
id="jobangeboteLink"
|
|
class="relative flex items-center gap-1.5 px-3 py-2 rounded-md bg-white/20 hover:bg-white/30 transition-colors text-white text-sm font-medium"
|
|
title="Über die REST-API eingespielte Jobangebote">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
|
|
</svg>
|
|
<span class="hidden sm:inline">Jobangebote</span>
|
|
<span id="jobangeboteBadge" class="hidden absolute -top-1 -right-1 min-w-[18px] h-[18px] px-1 flex items-center justify-center rounded-full bg-green-500 text-white text-[10px] font-bold ring-2 ring-blue-800 dark:ring-gray-900">0</span>
|
|
</a>
|
|
|
|
<!-- Blacklist (blocked job offers) link -->
|
|
<a href="/blacklist"
|
|
class="flex items-center gap-1.5 px-3 py-2 rounded-md bg-white/20 hover:bg-white/30 transition-colors text-white text-sm font-medium"
|
|
title="Geblockte Jobangebote (Blacklist) verwalten">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"></path>
|
|
</svg>
|
|
<span class="hidden sm:inline">Blacklist</span>
|
|
</a>
|
|
|
|
<!-- Vorlagen (base documents) link -->
|
|
<a href="/vorlagen"
|
|
class="flex items-center gap-1.5 px-3 py-2 rounded-md bg-white/20 hover:bg-white/30 transition-colors text-white text-sm font-medium"
|
|
title="Basis-Unterlagen für die KI-Generierung verwalten">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
|
</svg>
|
|
<span class="hidden sm:inline">Vorlagen</span>
|
|
</a>
|
|
|
|
<!-- Dark mode toggle -->
|
|
<button
|
|
id="darkModeToggle"
|
|
class="p-2 rounded-full bg-white/20 hover:bg-white/30 transition-colors text-white"
|
|
aria-label="Dark Mode umschalten"
|
|
>
|
|
<svg id="sunIcon" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"></path>
|
|
</svg>
|
|
<svg id="moonIcon" class="w-5 h-5 hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path>
|
|
</svg>
|
|
</button>
|
|
|
|
<!-- Settings button -->
|
|
<% if (typeof hideSettings === 'undefined' || !hideSettings) { %>
|
|
<button
|
|
id="settingsBtn"
|
|
class="p-2 rounded-full bg-white/20 hover:bg-white/30 transition-colors text-white"
|
|
aria-label="Einstellungen"
|
|
>
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
|
</svg>
|
|
</button>
|
|
<% } %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
// Populate the Postfach badge with the count of unlinked incoming e-mails.
|
|
(function () {
|
|
fetch('/api/emails/unassigned-count').then(function (r) { return r.json(); }).then(function (d) {
|
|
var b = document.getElementById('unassignedBadge');
|
|
if (b && d && d.count > 0) { b.textContent = d.count; b.classList.remove('hidden'); }
|
|
}).catch(function () { /* ignore — badge stays hidden */ });
|
|
})();
|
|
// Jobangebote badge: count of open offers ingested via the REST API.
|
|
(function () {
|
|
fetch('/jobangebote/anzahl-offen').then(function (r) { return r.json(); }).then(function (d) {
|
|
var b = document.getElementById('jobangeboteBadge');
|
|
if (b && d && d.count > 0) { b.textContent = d.count; b.classList.remove('hidden'); }
|
|
}).catch(function () { /* ignore */ });
|
|
})();
|
|
</script>
|
|
</header>
|