Add AI application assistant: Indeed import + Ollama document generation
- Browser extension (Chromium MV3) injecting a "send to tracker" button next to the Indeed job description; scrapes job info and posts it to a new /api/indeed-import endpoint (CORS-enabled), configurable tracker URL via popup. - New "Entwurf" status. Imports create a draft and trigger background AI generation of tailored Anschreiben + Lebenslauf (PDF attachments) via the Ollama Cloud API, grounded strictly in user-provided base documents. - Vorlagen page to manage base documents; attachments UI, generation status polling, regenerate and download routes on the application page. - Schema: ort/stellenbeschreibung/quelle_url/generierung_* columns, plus basis_dokumente and anhaenge tables (with migrations). - Config via .env (OLLAMA_API_KEY/OLLAMA_MODEL/OLLAMA_HOST); dependency-free .env loader. Dockerfile copies lib/, .dockerignore added. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
|
||||
<%
|
||||
const statusColor = {
|
||||
'Entwurf': 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200',
|
||||
'Gesendet': 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-200',
|
||||
'Eingangsbestätigung': 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
|
||||
'Vorstellungsgespräch': 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
|
||||
@@ -93,6 +94,113 @@
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- Bewerbungsunterlagen (KI-generiert) -->
|
||||
<% const genStatus = application.generierung_status; %>
|
||||
<% if (genStatus || (anhaenge && anhaenge.length) || (application.stellenbeschreibung && application.stellenbeschreibung.trim())) { %>
|
||||
<section id="unterlagen" class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-8"
|
||||
data-status="<%= genStatus || '' %>">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3 mb-4">
|
||||
<h2 class="text-lg font-semibold text-gray-800 dark:text-white">Bewerbungsunterlagen</h2>
|
||||
<form action="/bewerbung/<%= application.id %>/regenerate" method="POST"
|
||||
onsubmit="return confirm('Unterlagen neu generieren? Vorhandene generierte Dateien werden ersetzt.');">
|
||||
<button type="submit"
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>
|
||||
Neu generieren
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Generation status banner -->
|
||||
<div id="genBanner">
|
||||
<% if (genStatus === 'ausstehend') { %>
|
||||
<div class="flex items-center gap-3 rounded-lg bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-800 px-4 py-3 mb-4">
|
||||
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||
</svg>
|
||||
<span class="text-sm text-blue-800 dark:text-blue-200">Die KI erstellt gerade individuelle Bewerbungsunterlagen … Diese Seite aktualisiert sich automatisch.</span>
|
||||
</div>
|
||||
<% } else if (genStatus === 'fehler') { %>
|
||||
<div class="rounded-lg bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 px-4 py-3 mb-4">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<svg class="w-5 h-5 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
|
||||
</svg>
|
||||
<span class="text-sm font-semibold text-red-800 dark:text-red-200">Generierung fehlgeschlagen</span>
|
||||
</div>
|
||||
<p class="text-sm text-red-700 dark:text-red-300 ml-7"><%= application.generierung_fehler || 'Unbekannter Fehler.' %></p>
|
||||
</div>
|
||||
<% } else if (genStatus === 'fertig') { %>
|
||||
<div class="flex items-center gap-2 rounded-lg bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-800 px-4 py-3 mb-4">
|
||||
<svg class="w-5 h-5 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span class="text-sm text-green-800 dark:text-green-200">Unterlagen wurden erstellt. Bitte vor dem Versand prüfen und ggf. anpassen.</span>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<!-- Attachments -->
|
||||
<div id="anhaengeListe">
|
||||
<% if (anhaenge && anhaenge.length) { %>
|
||||
<ul class="space-y-2">
|
||||
<% anhaenge.forEach(a => { %>
|
||||
<li class="flex items-center justify-between gap-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700/40 px-4 py-3">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<svg class="w-6 h-6 text-red-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm font-medium text-gray-800 dark:text-gray-100 truncate"><%= a.name || a.dateiname %></p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 truncate"><%= a.dateiname %></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 shrink-0">
|
||||
<a href="/anhaenge/<%= a.id %>/download"
|
||||
class="inline-flex items-center gap-1 text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 hover:underline">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
|
||||
</svg>
|
||||
Download
|
||||
</a>
|
||||
<form action="/bewerbung/<%= application.id %>/anhaenge/<%= a.id %>/delete" method="POST"
|
||||
onsubmit="return confirm('Diesen Anhang löschen?');">
|
||||
<button type="submit" class="text-red-500 hover:text-red-700 dark:text-red-400" aria-label="Anhang löschen">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</li>
|
||||
<% }); %>
|
||||
</ul>
|
||||
<% } else if (genStatus !== 'ausstehend') { %>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Noch keine generierten Unterlagen vorhanden.</p>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<!-- Job description from the import -->
|
||||
<% if (application.stellenbeschreibung && application.stellenbeschreibung.trim()) { %>
|
||||
<details class="mt-6 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<summary class="cursor-pointer select-none px-4 py-3 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Importierte Stellenbeschreibung anzeigen
|
||||
</summary>
|
||||
<div class="px-4 pb-4">
|
||||
<% if (application.quelle_url) { %>
|
||||
<a href="<%= application.quelle_url %>" target="_blank" rel="noopener"
|
||||
class="inline-block mb-3 text-sm text-blue-600 dark:text-blue-400 hover:underline break-all"><%= application.quelle_url %></a>
|
||||
<% } %>
|
||||
<p class="whitespace-pre-wrap break-words text-sm leading-relaxed text-gray-700 dark:text-gray-300"><%= application.stellenbeschreibung %></p>
|
||||
</div>
|
||||
</details>
|
||||
<% } %>
|
||||
</section>
|
||||
<% } %>
|
||||
|
||||
<!-- Status timeline -->
|
||||
<section class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800 dark:text-white mb-1">Status-Verlauf</h2>
|
||||
@@ -214,6 +322,24 @@
|
||||
});
|
||||
const cy = document.getElementById('currentYear');
|
||||
if (cy) cy.textContent = new Date().getFullYear();
|
||||
|
||||
// While documents are being generated, poll for completion and
|
||||
// reload the page once the status changes.
|
||||
const unterlagen = document.getElementById('unterlagen');
|
||||
if (unterlagen && unterlagen.dataset.status === 'ausstehend') {
|
||||
const appId = <%= application.id %>;
|
||||
const poll = setInterval(async () => {
|
||||
try {
|
||||
const res = await fetch('/api/bewerbungen/' + appId + '/generierung', { cache: 'no-store' });
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
if (data.status && data.status !== 'ausstehend') {
|
||||
clearInterval(poll);
|
||||
window.location.reload();
|
||||
}
|
||||
} catch (e) { /* keep polling */ }
|
||||
}, 4000);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
+3
-1
@@ -144,9 +144,10 @@
|
||||
<% if (applications.length > 0) { %>
|
||||
<%
|
||||
// Status-Reihenfolge synchron zu public/js/main.js halten
|
||||
const statusOrder = ['Gesendet', 'Eingangsbestätigung', 'Vorstellungsgespräch',
|
||||
const statusOrder = ['Entwurf', 'Gesendet', 'Eingangsbestätigung', 'Vorstellungsgespräch',
|
||||
'Einstellung', 'Absage', 'Keine Rückmeldung'];
|
||||
const statusDot = {
|
||||
'Entwurf': 'bg-purple-500',
|
||||
'Gesendet': 'bg-indigo-500',
|
||||
'Eingangsbestätigung': 'bg-blue-500',
|
||||
'Vorstellungsgespräch': 'bg-yellow-500',
|
||||
@@ -204,6 +205,7 @@
|
||||
<%= app.status === 'Einstellung' ? 'bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100' :
|
||||
app.status === 'Absage' ? 'bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100' :
|
||||
app.status === 'Vorstellungsgespräch' ? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-800 dark:text-yellow-100' :
|
||||
app.status === 'Entwurf' ? 'bg-purple-100 text-purple-800 dark:bg-purple-800 dark:text-purple-100' :
|
||||
'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300' %>">
|
||||
<%= app.status || '-' %>
|
||||
</span>
|
||||
|
||||
@@ -9,8 +9,18 @@
|
||||
</a>
|
||||
|
||||
<div class="flex items-center space-x-4">
|
||||
<!-- 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
|
||||
<button
|
||||
id="darkModeToggle"
|
||||
class="p-2 rounded-full bg-white/20 hover:bg-white/30 transition-colors text-white"
|
||||
aria-label="Dark Mode umschalten"
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<%- include('partials/head') %>
|
||||
</head>
|
||||
<body class="min-h-screen transition-colors duration-300 bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100" id="body">
|
||||
<%- include('partials/header', { hideSettings: true }) %>
|
||||
|
||||
<main class="container mx-auto px-4 py-8 max-w-3xl">
|
||||
<!-- Back link -->
|
||||
<a href="/" class="inline-flex items-center gap-2 text-sm text-blue-600 dark:text-blue-400 hover:underline mb-6">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
|
||||
</svg>
|
||||
Zurück zur Übersicht
|
||||
</a>
|
||||
|
||||
<h1 class="text-2xl font-bold text-gray-800 dark:text-white mb-2">Basis-Unterlagen (Vorlagen)</h1>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mb-6">
|
||||
Diese Unterlagen dienen der KI als Faktengrundlage. Beim Import einer Stelle über die Browser-Erweiterung
|
||||
werden daraus automatisch zugeschnittene Bewerbungsunterlagen erstellt – es werden ausschließlich die
|
||||
hier hinterlegten Informationen verwendet, es wird nichts hinzuerfunden. Füge z. B. dein
|
||||
Standard-Anschreiben und deinen Lebenslauf als Text ein.
|
||||
</p>
|
||||
|
||||
<!-- API key warning -->
|
||||
<% if (!hasApiKey) { %>
|
||||
<div class="rounded-lg bg-amber-50 dark:bg-amber-900/30 border border-amber-200 dark:border-amber-800 px-4 py-3 mb-6">
|
||||
<div class="flex items-start gap-2">
|
||||
<svg class="w-5 h-5 text-amber-600 dark:text-amber-400 shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
|
||||
</svg>
|
||||
<div class="text-sm text-amber-800 dark:text-amber-200">
|
||||
<p class="font-semibold">Kein API-Schlüssel konfiguriert</p>
|
||||
<p>Setze die Umgebungsvariable <code class="px-1 rounded bg-amber-100 dark:bg-amber-800">OLLAMA_API_KEY</code>
|
||||
(z. B. in einer <code class="px-1 rounded bg-amber-100 dark:bg-amber-800">.env</code>-Datei),
|
||||
damit Bewerbungsunterlagen automatisch generiert werden können.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<!-- Existing base documents -->
|
||||
<section class="space-y-4 mb-8">
|
||||
<% if (basisDokumente.length === 0) { %>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 text-center text-gray-500 dark:text-gray-400">
|
||||
Noch keine Basis-Unterlagen hinterlegt. Füge unten dein erstes Dokument hinzu.
|
||||
</div>
|
||||
<% } %>
|
||||
<% basisDokumente.forEach(doc => { %>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
||||
<form action="/vorlagen/<%= doc.id %>" method="POST" class="space-y-3">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">Typ</label>
|
||||
<select name="typ" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white">
|
||||
<% basisTypOptions.forEach(opt => { %>
|
||||
<option value="<%= opt %>" <%= doc.typ === opt ? 'selected' : '' %>><%= opt %></option>
|
||||
<% }); %>
|
||||
</select>
|
||||
</div>
|
||||
<div class="sm:col-span-2">
|
||||
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">Bezeichnung</label>
|
||||
<input type="text" name="name" value="<%= doc.name || '' %>"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white"
|
||||
placeholder="z. B. Standard-Anschreiben">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">Inhalt (Text)</label>
|
||||
<textarea name="inhalt" rows="8"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white leading-relaxed font-mono text-sm"><%= doc.inhalt %></textarea>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button type="submit"
|
||||
class="px-3 py-1.5 text-sm bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors">
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<form action="/vorlagen/<%= doc.id %>/delete" method="POST" class="mt-1 text-right"
|
||||
onsubmit="return confirm('Dieses Basis-Dokument löschen?');">
|
||||
<button type="submit"
|
||||
class="text-xs text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300 hover:underline">
|
||||
Dokument löschen
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<% }); %>
|
||||
</section>
|
||||
|
||||
<!-- Add new base document -->
|
||||
<section class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800 dark:text-white mb-4">Basis-Dokument hinzufügen</h2>
|
||||
<form action="/vorlagen" method="POST" class="space-y-3">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">Typ</label>
|
||||
<select name="typ" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white">
|
||||
<% basisTypOptions.forEach(opt => { %>
|
||||
<option value="<%= opt %>"><%= opt %></option>
|
||||
<% }); %>
|
||||
</select>
|
||||
</div>
|
||||
<div class="sm:col-span-2">
|
||||
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">Bezeichnung</label>
|
||||
<input type="text" name="name"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white"
|
||||
placeholder="z. B. Standard-Anschreiben">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">Inhalt (Text)</label>
|
||||
<textarea name="inhalt" rows="10" required
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white leading-relaxed font-mono text-sm"
|
||||
placeholder="Füge hier den vollständigen Text deines Anschreibens oder Lebenslaufs ein…"></textarea>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<button type="submit" class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors">
|
||||
Hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<%- include('partials/footer') %>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const root = document.documentElement;
|
||||
const dm = localStorage.getItem('darkMode');
|
||||
if (dm === 'enabled' || (!dm && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
root.classList.add('dark');
|
||||
}
|
||||
const toggle = document.getElementById('darkModeToggle');
|
||||
const sun = document.getElementById('sunIcon');
|
||||
const moon = document.getElementById('moonIcon');
|
||||
function sync() {
|
||||
const d = root.classList.contains('dark');
|
||||
if (sun) sun.classList.toggle('hidden', d);
|
||||
if (moon) moon.classList.toggle('hidden', !d);
|
||||
}
|
||||
sync();
|
||||
if (toggle) toggle.addEventListener('click', () => {
|
||||
root.classList.toggle('dark');
|
||||
localStorage.setItem('darkMode', root.classList.contains('dark') ? 'enabled' : 'disabled');
|
||||
sync();
|
||||
});
|
||||
const cy = document.getElementById('currentYear');
|
||||
if (cy) cy.textContent = new Date().getFullYear();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user