Einrichtung: Wizard um E-Mail, Kalender und REST-API erweitert

Der Assistent deckt nun alle Bereiche aus /einstellungen ab. Neue Schritte
fuer E-Mail (SMTP/IMAP) & CalDAV sowie REST-API; Indikator auf 7 Schritte.
Secrets (Passwort, Token) werden nie vorausgefuellt und beim Speeren leerer
Felder nicht ueberschrieben. Neuer Endpunkt /api/einrichtung/config mit
SSRF-Guard (Mail-Host, CalDAV-URL, Ollama-Host) und API-Token-Kollisionspruefung.
Abschluss-Checkliste trennt Pflicht- von optionalen Verbindungen.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-14 12:55:20 +02:00
co-authored by Claude
parent 1bc0188fcc
commit 2566dcc98d
2 changed files with 355 additions and 171 deletions
+297 -169
View File
@@ -1,11 +1,14 @@
<%
// Einrichtungsassistent für Nutzer, die noch keine Bewerbung angelegt haben.
// Server liefert `setupWizard` mit: aktiv, persoenlich (row), anschreiben/lebenslauf
// ({id,inhalt}), hasApiKey, ollamaModel, ollamaHost. Das Partial rendert nur den
// Wizard; das umgebende index.ejs blendet gleichzeitig das Dashboard aus.
// Server liefert `setupWizard` mit: aktiv, persoenlich (row),
// anschreiben/lebenslauf ({id,inhalt}), hasApiKey, ollamaModel, ollamaHost,
// cfg (nicht-geheime Mail-/CalDAV-Werte + hasMailPassword + hasApiToken).
// Das Partial rendert nur den Wizard; das umgebende index.ejs blendet
// gleichzeitig das Dashboard aus (Markup bleibt im DOM für main.js).
var p = setupWizard.persoenlich || {};
var anschreibenInhalt = setupWizard.anschreiben ? setupWizard.anschreiben.inhalt : '';
var lebenslaufInhalt = setupWizard.lebenslauf ? setupWizard.lebenslauf.inhalt : '';
var c = setupWizard.cfg || {};
%>
<style>
/* Fortschrittsanzeige: Verbindungslinie zwischen den Schritt-Punkten. */
@@ -13,50 +16,44 @@
.einr-steps::before {
content: "";
position: absolute;
top: 50%;
left: 0;
right: 0;
top: 14px; /* Mitte der 28px-Punkte */
left: 6%;
right: 6%;
height: 2px;
background-color: rgba(255, 255, 255, 0.25);
transform: translateY(-1px);
}
.einr-step-dot {
position: relative;
z-index: 1;
transition: background-color 0.2s ease, transform 0.2s ease;
}
.einr-step.is-active .einr-step-dot {
transform: scale(1.12);
}
.einr-step.is-active .einr-step-dot { transform: scale(1.12); }
/* Sanftes Einblenden der Schrittinhalte. */
.einr-panel { animation: einrFade 0.28s ease; }
@keyframes einrFade {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
.einr-panel { animation: none; }
}
@media (prefers-reduced-motion: reduce) { .einr-panel { animation: none; } }
</style>
<section id="einrichtung" class="mb-10">
<div class="mx-auto max-w-3xl overflow-hidden rounded-2xl border border-gray-200/80 bg-white shadow-xl shadow-gray-900/5 dark:border-white/10 dark:bg-gray-800">
<!-- Kopfbereich mit Verlaufsanzeige -->
<div class="relative bg-gradient-to-br from-sky-500 via-blue-600 to-indigo-600 px-6 py-7 text-white sm:px-8">
<div class="relative bg-gradient-to-br from-sky-500 via-blue-600 to-indigo-600 px-5 py-7 text-white sm:px-8">
<div class="flex items-start justify-between gap-4">
<div class="flex items-start gap-3">
<span class="grid h-11 w-11 shrink-0 place-items-center rounded-xl bg-white/15 ring-1 ring-white/25 backdrop-blur">
<svg class="h-6 w-6" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 010 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.293-.24.438-.613.43-.991a7.723 7.723 0 010-.255c.008-.379-.137-.75-.43-.992l-1.004-.827a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.28z"></path>
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" d="M11.42 15.17L17.25 21A2.652 2.652 0 0021 17.25l-5.877-5.877M11.42 15.17l2.496 3.06c.318.392.74.665 1.215.82a3.51 3.51 0 003.99-1.51l1.75-2.93a1.5 1.5 0 00-2.57-1.55l-1.75 2.93a.75.75 0 01-1.05.23l-.4-.27m-3.24-2.29l-2.83-3.5m0 0A3.291 3.291 0 001.87 7.28l.8 1.2m4.4 3.69l-3.6-5.4m1.55 6.6L4 7.6"/>
</svg>
</span>
<div>
<h1 class="text-xl font-semibold leading-tight sm:text-2xl">NextJobs einrichten</h1>
<p class="mt-1 max-w-md text-sm text-white/80">
In wenigen Schritten bist du startklar für deine erste Bewerbung.
Du kannst jeden Schritt überspringen und später nachholen.
Jeder Schritt lässt sich überspringen und später nachholen.
</p>
</div>
</div>
@@ -66,56 +63,36 @@
</button>
</div>
<!-- Schrittanzeige -->
<ol id="einrSteps" class="einr-steps mt-7 flex items-center justify-between gap-1">
<li class="einr-step flex flex-1 flex-col items-center gap-1.5 text-center" data-step="1">
<span class="einr-step-dot grid h-8 w-8 place-items-center rounded-full bg-white/20 text-xs font-semibold ring-1 ring-white/30">1</span>
<span class="text-[11px] font-medium text-white/80">Willkommen</span>
</li>
<li class="einr-step flex flex-1 flex-col items-center gap-1.5 text-center" data-step="2">
<span class="einr-step-dot grid h-8 w-8 place-items-center rounded-full bg-white/20 text-xs font-semibold ring-1 ring-white/30">2</span>
<span class="text-[11px] font-medium text-white/80">Profil</span>
</li>
<li class="einr-step flex flex-1 flex-col items-center gap-1.5 text-center" data-step="3">
<span class="einr-step-dot grid h-8 w-8 place-items-center rounded-full bg-white/20 text-xs font-semibold ring-1 ring-white/30">3</span>
<span class="text-[11px] font-medium text-white/80">Vorlagen</span>
</li>
<li class="einr-step flex flex-1 flex-col items-center gap-1.5 text-center" data-step="4">
<span class="einr-step-dot grid h-8 w-8 place-items-center rounded-full bg-white/20 text-xs font-semibold ring-1 ring-white/30">4</span>
<span class="text-[11px] font-medium text-white/80">KI</span>
</li>
<li class="einr-step flex flex-1 flex-col items-center gap-1.5 text-center" data-step="5">
<span class="einr-step-dot grid h-8 w-8 place-items-center rounded-full bg-white/20 text-xs font-semibold ring-1 ring-white/30">5</span>
<span class="text-[11px] font-medium text-white/80">Abschluss</span>
<!-- Schrittanzeige (7 Schritte; Labels nur auf >= sm) -->
<ol id="einrSteps" class="einr-steps mt-7 flex items-start justify-between gap-1">
<% var stepLabels = ['Willkommen','Profil','Vorlagen','KI','E-Mail','API','Abschluss']; %>
<% for (var si = 1; si <= 7; si++) { %>
<li class="einr-step flex flex-1 flex-col items-center gap-1.5 text-center" data-step="<%= si %>">
<span class="einr-step-dot grid h-7 w-7 place-items-center rounded-full bg-white/20 text-xs font-semibold ring-1 ring-white/30"><%= si %></span>
<span class="hidden text-[11px] font-medium text-white/80 sm:block"><%= stepLabels[si-1] %></span>
</li>
<% } %>
</ol>
</div>
<!-- Formular / Schrittinhalte -->
<form id="einrForm" class="px-6 py-6 sm:px-8" autocomplete="off" novalidate>
<form id="einrForm" class="px-5 py-6 sm:px-8" autocomplete="off" novalidate>
<!-- Schritt 1: Willkommen -->
<div class="einr-panel" data-panel="1">
<h2 class="text-lg font-semibold text-gray-800 dark:text-white">Herzlich willkommen!</h2>
<p class="mt-2 text-sm leading-relaxed text-gray-600 dark:text-gray-300">
NextJobs begleitet dich vom Stellenimport bis zur fertigen Bewerbung — mit
automatisch zugeschnittenen Anschreiben und Lebenslauf. Damit das klappt,
hinterlegen wir zuerst deine Kontaktdaten, deine Basis-Unterlagen und den
KI-Zugang. Das dauert etwa drei Minuten.
automatisch zugeschnittenen Anschreiben und Lebenslauf, E-Mail-Posteingang
und Bewerbungskalender. Damit das klappt, hinterlegen wir zuerst die nötigen
Daten. Das dauert etwa fünf Minuten.
</p>
<ul class="mt-5 space-y-2.5 text-sm text-gray-600 dark:text-gray-300">
<li class="flex items-start gap-2.5">
<svg class="mt-0.5 h-5 w-5 shrink-0 text-blue-500" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
<span><strong class="font-medium text-gray-800 dark:text-white">Persönliche Angaben</strong> — fließen in jede Bewerbung ein.</span>
</li>
<li class="flex items-start gap-2.5">
<svg class="mt-0.5 h-5 w-5 shrink-0 text-blue-500" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
<span><strong class="font-medium text-gray-800 dark:text-white">Basis-Unterlagen</strong> — dein Standard-Anschreiben & Lebenslauf als Text.</span>
</li>
<li class="flex items-start gap-2.5">
<svg class="mt-0.5 h-5 w-5 shrink-0 text-blue-500" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
<span><strong class="font-medium text-gray-800 dark:text-white">Ollama-KI</strong> — erzeugt daraus später die passenden Unterlagen.</span>
</li>
<li class="flex items-start gap-2.5"><svg class="mt-0.5 h-5 w-5 shrink-0 text-blue-500" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg><span><strong class="font-medium text-gray-800 dark:text-white">Persönliche Angaben</strong> — fließen in jede Bewerbung ein.</span></li>
<li class="flex items-start gap-2.5"><svg class="mt-0.5 h-5 w-5 shrink-0 text-blue-500" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg><span><strong class="font-medium text-gray-800 dark:text-white">Basis-Unterlagen</strong> — dein Standard-Anschreiben &amp; Lebenslauf als Text.</span></li>
<li class="flex items-start gap-2.5"><svg class="mt-0.5 h-5 w-5 shrink-0 text-blue-500" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg><span><strong class="font-medium text-gray-800 dark:text-white">Ollama-KI</strong> — erzeugt daraus die passenden Unterlagen.</span></li>
<li class="flex items-start gap-2.5"><svg class="mt-0.5 h-5 w-5 shrink-0 text-blue-500" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg><span><strong class="font-medium text-gray-800 dark:text-white">E-Mail &amp; Kalender</strong> — Posteingang versenden und Termine synchronisieren.</span></li>
<li class="flex items-start gap-2.5"><svg class="mt-0.5 h-5 w-5 shrink-0 text-blue-500" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg><span><strong class="font-medium text-gray-800 dark:text-white">REST-API</strong> — optional, für Drittanbietersoftware.</span></li>
</ul>
</div>
@@ -184,8 +161,7 @@
<h2 class="text-lg font-semibold text-gray-800 dark:text-white">Basis-Unterlagen</h2>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
Füge dein Standard-Anschreiben und deinen Lebenslauf als Text ein. Die KI
nutzt sie als Faktengrundlage und erfindet nichts hinzu. Du kannst auch
schon vorhandene Texte einfach hereinkopieren.
nutzt sie als Faktengrundlage und erfindet nichts hinzu.
</p>
<div class="mt-5 space-y-5">
<div>
@@ -201,8 +177,7 @@
placeholder="Persönliche Daten, Berufserfahrung, Ausbildung, Kenntnisse …"><%= lebenslaufInhalt %></textarea>
</div>
<p class="text-xs text-gray-400 dark:text-gray-500">
Tipp: Ausführliche Vorlagen lassen sich später unter „Vorlagen“ verfeinern —
inkl. Design, Foto und weiteren Doktypen.
Tipp: Ausführliche Vorlagen lassen sich später unter „Vorlagen“ verfeinern — inkl. Design und Foto.
</p>
</div>
</div>
@@ -212,8 +187,8 @@
<h2 class="text-lg font-semibold text-gray-800 dark:text-white">Ollama-KI einrichten</h2>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
Über die Ollama Cloud erzeugt NextJobs automatisch zugeschnittene
Bewerbungsunterlagen. Ohne API-Schlüssel bleibt dieser Schritt deaktiviert —
du kannst ihn auch später unter „Einstellungen“ nachholen.
Bewerbungsunterlagen. Ohne API-Schlüssel bleibt die KI deaktiviert —
später unter „Einstellungen“ nachholbar.
</p>
<div class="mt-5 space-y-4">
<div>
@@ -222,21 +197,14 @@
<input type="password" id="einr-apikey" name="OLLAMA_API_KEY"
class="w-full rounded-md border border-gray-300 bg-white px-3 py-2 pr-10 text-sm text-gray-800 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="ol-…" autocomplete="off">
<button type="button" id="einr-toggle-key"
class="absolute inset-y-0 right-0 flex items-center px-3 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
title="Wert anzeigen/verbergen" tabindex="-1">
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
</svg>
<button type="button" class="einr-reveal absolute inset-y-0 right-0 flex items-center px-3 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200" data-target="einr-apikey" title="Wert anzeigen/verbergen" tabindex="-1">
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
</button>
</div>
<p class="mt-1 text-xs text-gray-400 dark:text-gray-500">
Schlüssel holen unter
<a href="https://ollama.com/settings/keys" target="_blank" rel="noopener" class="text-blue-600 hover:underline dark:text-sky-400">ollama.com/settings/keys</a>.
<% if (setupWizard.hasApiKey) { %>
<span class="ml-1 text-green-600 dark:text-green-400">· bereits hinterlegt</span>
<% } %>
<% if (setupWizard.hasApiKey) { %><span class="ml-1 text-green-600 dark:text-green-400">· bereits hinterlegt</span><% } %>
</p>
</div>
<div class="grid gap-4 sm:grid-cols-2">
@@ -256,30 +224,159 @@
</div>
</div>
<!-- Schritt 5: Abschluss -->
<!-- Schritt 5: E-Mail & Kalender -->
<div class="einr-panel hidden" data-panel="5">
<h2 class="text-lg font-semibold text-gray-800 dark:text-white">E-Mail &amp; Kalender</h2>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
E-Mail-Empfang (IMAP) und -Versand (SMTP) laufen über dein eigenes Postfach.
Der Bewerbungskalender nutzt CalDAV und teilt sich Login &amp; Passwort mit
der E-Mail. Beides ist optional — ohne Angabe bleiben Postfach und
Kalender-Sync deaktiviert.
</p>
<!-- E-Mail -->
<div class="mt-5">
<h3 class="text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">E-Mail (SMTP / IMAP)</h3>
<div class="mt-3 grid gap-4 sm:grid-cols-2">
<div class="sm:col-span-2">
<label for="einr-mail-host" class="block text-xs font-medium text-gray-500 dark:text-gray-400">SMTP/IMAP Host</label>
<input type="text" id="einr-mail-host" class="mt-1 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-800 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="mail.example.com" value="<%= c.MAIL_HOST || '' %>">
</div>
<div>
<label for="einr-mail-smtp-port" class="block text-xs font-medium text-gray-500 dark:text-gray-400">SMTP-Port</label>
<input type="text" id="einr-mail-smtp-port" class="mt-1 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-800 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
value="<%= c.MAIL_SMTP_PORT || '587' %>">
<p class="mt-1 text-[11px] text-gray-400 dark:text-gray-500">587 = STARTTLS, 465 = implicit TLS</p>
</div>
<div>
<label for="einr-mail-imap-port" class="block text-xs font-medium text-gray-500 dark:text-gray-400">IMAP-Port</label>
<input type="text" id="einr-mail-imap-port" class="mt-1 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-800 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
value="<%= c.MAIL_IMAP_PORT || '993' %>">
</div>
<div>
<label for="einr-mail-user" class="block text-xs font-medium text-gray-500 dark:text-gray-400">Benutzername (Login)</label>
<input type="text" id="einr-mail-user" class="mt-1 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-800 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="name@example.com" value="<%= c.MAIL_USER || '' %>">
<p class="mt-1 text-[11px] text-gray-400 dark:text-gray-500">Auch CalDAV-Login</p>
</div>
<div>
<label for="einr-mail-password" class="block text-xs font-medium text-gray-500 dark:text-gray-400">Passwort</label>
<div class="relative mt-1">
<input type="password" id="einr-mail-password"
class="w-full rounded-md border border-gray-300 bg-white px-3 py-2 pr-10 text-sm text-gray-800 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="<%= c.hasMailPassword ? 'vorhanden — leer lassen, um nichts zu ändern' : 'Passwort' %>" autocomplete="off">
<button type="button" class="einr-reveal absolute inset-y-0 right-0 flex items-center px-3 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200" data-target="einr-mail-password" title="Wert anzeigen/verbergen" tabindex="-1">
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
</button>
</div>
</div>
<div>
<label for="einr-mail-fromname" class="block text-xs font-medium text-gray-500 dark:text-gray-400">Absendername</label>
<input type="text" id="einr-mail-fromname" class="mt-1 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-800 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Max Mustermann" value="<%= c.MAIL_FROM_NAME || '' %>">
</div>
<div>
<label for="einr-mail-from" class="block text-xs font-medium text-gray-500 dark:text-gray-400">Absenderadresse</label>
<input type="text" id="einr-mail-from" class="mt-1 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-800 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="leer = Benutzername" value="<%= c.MAIL_FROM || '' %>">
</div>
<div>
<label for="einr-mail-mailbox" class="block text-xs font-medium text-gray-500 dark:text-gray-400">IMAP-Postfach</label>
<input type="text" id="einr-mail-mailbox" class="mt-1 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-800 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
value="<%= c.MAIL_IMAP_MAILBOX || 'INBOX' %>">
</div>
<div>
<label for="einr-mail-poll" class="block text-xs font-medium text-gray-500 dark:text-gray-400">Abrufintervall (ms)</label>
<input type="text" id="einr-mail-poll" class="mt-1 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-800 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
value="<%= c.MAIL_POLL_MS || '180000' %>">
<p class="mt-1 text-[11px] text-gray-400 dark:text-gray-500">greift nach Neustart</p>
</div>
</div>
</div>
<!-- CalDAV -->
<div class="mt-7">
<h3 class="text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Bewerbungskalender (CalDAV)</h3>
<div class="mt-3 grid gap-4 sm:grid-cols-2">
<div class="sm:col-span-2">
<label for="einr-caldav-url" class="block text-xs font-medium text-gray-500 dark:text-gray-400">Kalender-URL</label>
<input type="text" id="einr-caldav-url" class="mt-1 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-800 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="https://mail.example.com/SOGo/dav/name@…/Calendar/XXXX/" value="<%= c.CALDAV_URL || '' %>">
<p class="mt-1 text-[11px] text-gray-400 dark:text-gray-500">Voll-URL der Sammlung, mit abschließendem /. Anmeldung über Benutzername/Passwort oben.</p>
</div>
<div>
<label for="einr-caldav-alarm" class="block text-xs font-medium text-gray-500 dark:text-gray-400">Erinnerung (Min. vor Termin)</label>
<input type="text" id="einr-caldav-alarm" class="mt-1 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-800 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
value="<%= c.CALDAV_ALARM_MIN || '60' %>">
</div>
<div>
<label for="einr-caldav-poll" class="block text-xs font-medium text-gray-500 dark:text-gray-400">Sync-Intervall (ms)</label>
<input type="text" id="einr-caldav-poll" class="mt-1 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-800 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
value="<%= c.CALDAV_POLL_MS || '300000' %>">
<p class="mt-1 text-[11px] text-gray-400 dark:text-gray-500">greift nach Neustart</p>
</div>
</div>
</div>
</div>
<!-- Schritt 6: REST-API -->
<div class="einr-panel hidden" data-panel="6">
<h2 class="text-lg font-semibold text-gray-800 dark:text-white">REST-API (optional)</h2>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
Ist ein Token gesetzt, ist <code class="rounded bg-gray-100 px-1 dark:bg-gray-700">/api/v1</code>
für dich aktiv und erwartet den Wert im Header
<code class="rounded bg-gray-100 px-1 dark:bg-gray-700">X-API-Key</code>.
Der Token identifiziert dein Konto — Anfragen sehen und ändern nur deine
eigenen Daten. Swagger unter <a href="/swagger" class="text-blue-600 hover:underline dark:text-sky-400">/swagger</a>.
</p>
<div class="mt-5">
<label for="einr-api-token" class="block text-sm font-medium text-gray-700 dark:text-gray-300">API-Token</label>
<div class="relative mt-1">
<input type="password" id="einr-api-token"
class="w-full rounded-md border border-gray-300 bg-white px-3 py-2 pr-10 text-sm text-gray-800 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="<%= c.hasApiToken ? 'vorhanden — leer lassen, um nichts zu ändern' : 'Token erzeugen oder eintragen' %>" autocomplete="off">
<button type="button" class="einr-reveal absolute inset-y-0 right-0 flex items-center px-3 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200" data-target="einr-api-token" title="Wert anzeigen/verbergen" tabindex="-1">
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
</button>
</div>
<div class="mt-2 flex flex-wrap items-center gap-2">
<button type="button" id="einr-token-gen"
class="inline-flex items-center gap-1.5 rounded-md border border-gray-300 px-3 py-1.5 text-xs text-gray-700 transition hover:bg-gray-100 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-700">
<svg class="h-3.5 w-3.5" 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"/></svg>
Neu generieren
</button>
<button type="button" id="einr-token-copy"
class="inline-flex items-center gap-1.5 rounded-md border border-gray-300 px-3 py-1.5 text-xs text-gray-700 transition hover:bg-gray-100 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-700">
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>
Kopieren
</button>
<span id="einr-token-hint" class="hidden text-xs text-amber-600 dark:text-amber-400"></span>
<% if (c.hasApiToken) { %><span class="text-xs text-green-600 dark:text-green-400">· bereits hinterlegt</span><% } %>
</div>
<p class="mt-2 text-[11px] text-gray-400 dark:text-gray-500">Leer lassen = vorhandenen Token nicht anfassen. Ein neuer Token macht den alten sofort ungültig.</p>
</div>
</div>
<!-- Schritt 7: Abschluss -->
<div class="einr-panel hidden" data-panel="7">
<h2 class="text-lg font-semibold text-gray-800 dark:text-white">Fast geschafft!</h2>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
Hier ist dein Stand. Fehlende Punkte kannst du jederzeit unter
Hier ist dein Stand. Fehlende Punkte lassen sich jederzeit unter
„Vorlagen“ oder „Einstellungen“ nachholen.
</p>
<ul id="einrStatus" class="mt-5 divide-y divide-gray-100 dark:divide-gray-700/60 overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
<li class="flex items-center gap-3 px-4 py-3" data-check="profil">
<span class="einr-ico grid h-6 w-6 place-items-center rounded-full"></span>
<span class="text-sm text-gray-700 dark:text-gray-200">Persönliche Angaben</span>
</li>
<li class="flex items-center gap-3 px-4 py-3" data-check="anschreiben">
<span class="einr-ico grid h-6 w-6 place-items-center rounded-full"></span>
<span class="text-sm text-gray-700 dark:text-gray-200">Standard-Anschreiben hinterlegt</span>
</li>
<li class="flex items-center gap-3 px-4 py-3" data-check="lebenslauf">
<span class="einr-ico grid h-6 w-6 place-items-center rounded-full"></span>
<span class="text-sm text-gray-700 dark:text-gray-200">Lebenslauf hinterlegt</span>
</li>
<li class="flex items-center gap-3 px-4 py-3" data-check="ki">
<span class="einr-ico grid h-6 w-6 place-items-center rounded-full"></span>
<span class="text-sm text-gray-700 dark:text-gray-200">Ollama-KI aktiv</span>
</li>
<ul class="mt-5 divide-y divide-gray-100 dark:divide-gray-700/60 overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
<li class="flex items-center gap-3 px-4 py-3" data-check="profil"><span class="einr-ico grid h-6 w-6 place-items-center rounded-full"></span><span class="text-sm text-gray-700 dark:text-gray-200">Persönliche Angaben</span></li>
<li class="flex items-center gap-3 px-4 py-3" data-check="anschreiben"><span class="einr-ico grid h-6 w-6 place-items-center rounded-full"></span><span class="text-sm text-gray-700 dark:text-gray-200">Standard-Anschreiben hinterlegt</span></li>
<li class="flex items-center gap-3 px-4 py-3" data-check="lebenslauf"><span class="einr-ico grid h-6 w-6 place-items-center rounded-full"></span><span class="text-sm text-gray-700 dark:text-gray-200">Lebenslauf hinterlegt</span></li>
<li class="flex items-center gap-3 px-4 py-3" data-check="ki"><span class="einr-ico grid h-6 w-6 place-items-center rounded-full"></span><span class="text-sm text-gray-700 dark:text-gray-200">Ollama-KI aktiv</span></li>
</ul>
<p class="mt-5 text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">Weitere Verbindungen (optional)</p>
<ul class="mt-2 divide-y divide-gray-100 dark:divide-gray-700/60 overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
<li class="flex items-center gap-3 px-4 py-3" data-opt="mail"><span class="einr-ico grid h-6 w-6 place-items-center rounded-full"></span><span class="text-sm text-gray-700 dark:text-gray-200">E-Mail (SMTP/IMAP)</span></li>
<li class="flex items-center gap-3 px-4 py-3" data-opt="kalender"><span class="einr-ico grid h-6 w-6 place-items-center rounded-full"></span><span class="text-sm text-gray-700 dark:text-gray-200">Bewerbungskalender (CalDAV)</span></li>
<li class="flex items-center gap-3 px-4 py-3" data-opt="api"><span class="einr-ico grid h-6 w-6 place-items-center rounded-full"></span><span class="text-sm text-gray-700 dark:text-gray-200">REST-API-Token</span></li>
</ul>
<div class="mt-5 rounded-lg bg-blue-50 p-4 text-sm text-blue-800 dark:bg-blue-500/10 dark:text-blue-200">
@@ -330,14 +427,21 @@
var form = document.getElementById('einrForm');
if (!form) return;
var MAX = 7;
var step = 1;
var MAX = 5;
var panels = {};
Array.prototype.slice.call(form.querySelectorAll('.einr-panel')).forEach(function (el) {
panels[el.getAttribute('data-panel')] = el;
});
var stepEls = Array.prototype.slice.call(document.querySelectorAll('#einrSteps .einr-step'));
// Vorhandene Secrets kennen wir nur als Flag — der Wert steht nie im Formular.
var VORHANDEN = {
apiKey: <%= setupWizard.hasApiKey ? 'true' : 'false' %>,
mailPassword: <%= (c.hasMailPassword) ? 'true' : 'false' %>,
apiToken: <%= (c.hasApiToken) ? 'true' : 'false' %>
};
var btnZurueck = document.getElementById('einrZurueck');
var btnUeberspringen = document.getElementById('einrUeberspringen');
var btnWeiter = document.getElementById('einrWeiter');
@@ -348,23 +452,19 @@
var btnAusblenden = document.getElementById('einrAusblenden');
var fehler = document.getElementById('einrFehler');
var labels = { 1: 'Los geht\'s', 2: 'Weiter', 3: 'Weiter', 4: 'Weiter', 5: 'Abschließen' };
function showFehler(msg) {
fehler.textContent = msg || '';
fehler.classList.toggle('hidden', !msg);
}
var labels = { 1: 'Los geht\'s', 2: 'Weiter', 3: 'Weiter', 4: 'Weiter', 5: 'Weiter', 6: 'Weiter', 7: 'Abschließen' };
function v(sel) { var el = form.querySelector(sel); return el ? el.value : ''; }
function showFehler(msg) { fehler.textContent = msg || ''; fehler.classList.toggle('hidden', !msg); }
function setBusy(busy) {
btnWeiter.disabled = busy;
spinner.classList.toggle('hidden', !busy);
iconWeiter.classList.toggle('hidden', busy);
}
function check(r) { if (!r.ok || !r.json.success) throw new Error(r.json.error || 'Speichern fehlgeschlagen'); }
function render() {
Object.keys(panels).forEach(function (n) {
panels[n].classList.toggle('hidden', String(n) !== String(step));
});
Object.keys(panels).forEach(function (n) { panels[n].classList.toggle('hidden', String(n) !== String(step)); });
stepEls.forEach(function (el) {
var n = Number(el.getAttribute('data-step'));
var dot = el.querySelector('.einr-step-dot');
@@ -372,7 +472,7 @@
if (n < step) {
dot.style.backgroundColor = 'rgba(255,255,255,0.92)';
dot.style.color = '#1d4ed8';
dot.innerHTML = '<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>';
dot.innerHTML = '<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>';
} else if (n === step) {
dot.style.backgroundColor = '#ffffff';
dot.style.color = '#1d4ed8';
@@ -383,7 +483,6 @@
dot.textContent = String(n);
}
});
btnZurueck.classList.toggle('hidden', step === 1);
btnUeberspringen.classList.toggle('hidden', step === 1 || step === MAX);
btnDirekt.classList.toggle('hidden', step !== 1 && step !== MAX);
@@ -392,35 +491,43 @@
function gehZu(n) {
step = Math.max(1, Math.min(MAX, n));
if (step === MAX) aktualiereStatus();
if (step === MAX) { aktualiereStatus(); }
render();
// Oberkante sichtbar halten, damit der nächste Schritt nicht unter dem
// sticky Header verschwindet.
var sec = document.getElementById('einrichtung');
if (sec) sec.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
// Abschluss-Checkliste live aus den Formularwerten berechnen.
// ico für Pflicht-Checkliste: grün = erledigt, orange = offen.
function setzeCheck(name, ok) {
var li = form.querySelector('[data-check="' + name + '"]');
if (!li) return;
var ico = li.querySelector('.einr-ico');
ico.style.backgroundColor = ok ? 'rgba(16,185,129,0.12)' : 'rgba(245,158,11,0.12)';
ico.style.color = ok ? '#059669' : '#d97706';
ico.innerHTML = ok
? '<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>'
: '<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4m0 4h.01"/></svg>';
}
// ico für optionale Verbindungen: grün = eingerichtet, neutral = optional/offen.
function setzeOpt(name, ok) {
var li = form.querySelector('[data-opt="' + name + '"]');
if (!li) return;
var ico = li.querySelector('.einr-ico');
ico.style.backgroundColor = ok ? 'rgba(16,185,129,0.12)' : 'rgba(148,163,184,0.15)';
ico.style.color = ok ? '#059669' : '#94a3b8';
ico.innerHTML = ok
? '<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>'
: '<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14"/></svg>';
}
function aktualiereStatus() {
var vals = {
profil: !!(form.querySelector('#einr-name') || {}).value,
anschreiben: !!(form.querySelector('#einr-anschreiben') || {}).value,
lebenslauf: !!(form.querySelector('#einr-lebenslauf') || {}).value,
ki: !!(form.querySelector('#einr-apikey') || {}).value
};
if (!vals.ki) vals.ki = <%= setupWizard.hasApiKey ? 'true' : 'false' %>;
Object.keys(vals).forEach(function (k) {
var li = form.querySelector('[data-check="' + k + '"]');
if (!li) return;
var ico = li.querySelector('.einr-ico');
var ok = !!vals[k];
ico.style.backgroundColor = ok ? 'rgba(16,185,129,0.12)' : 'rgba(245,158,11,0.12)';
ico.style.color = ok ? '#059669' : '#d97706';
ico.innerHTML = ok
? '<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>'
: '<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4m0 4h.01"/></svg>';
li.style.color = ok ? '' : '';
});
setzeCheck('profil', !!v('#einr-name').trim());
setzeCheck('anschreiben', !!v('#einr-anschreiben').trim());
setzeCheck('lebenslauf', !!v('#einr-lebenslauf').trim());
setzeCheck('ki', !!v('#einr-apikey').trim() || VORHANDEN.apiKey);
setzeOpt('mail', !!v('#einr-mail-host').trim());
setzeOpt('kalender', !!v('#einr-caldav-url').trim());
setzeOpt('api', !!v('#einr-api-token').trim() || VORHANDEN.apiToken);
}
function post(url, data) {
@@ -433,40 +540,44 @@
// Speichert den Inhalt des aktuellen Schritts vor dem Weiterspringen.
function speichern(stepNr) {
if (stepNr === 2) {
if (!form.querySelector('#einr-name').value.trim()) {
if (!v('#einr-name').trim()) {
showFehler('Bitte gib mindestens deinen Namen ein — das ist das einzige Pflichtfeld.');
return Promise.reject();
}
return post('/api/einrichtung/persoenlich', {
name: form.querySelector('#einr-name').value,
adresse: form.querySelector('#einr-adresse').value,
kundennummer: form.querySelector('#einr-kundennummer').value,
email: form.querySelector('#einr-email').value,
telefon: form.querySelector('#einr-telefon').value,
ort: form.querySelector('#einr-ort').value,
webseite: form.querySelector('#einr-webseite').value,
geburtsdatum: form.querySelector('#einr-geburtsdatum').value
}).then(function (r) {
if (!r.ok || !r.json.success) throw new Error(r.json.error || 'Speichern fehlgeschlagen');
});
name: v('#einr-name'), adresse: v('#einr-adresse'), kundennummer: v('#einr-kundennummer'),
email: v('#einr-email'), telefon: v('#einr-telefon'), ort: v('#einr-ort'),
webseite: v('#einr-webseite'), geburtsdatum: v('#einr-geburtsdatum')
}).then(check);
}
if (stepNr === 3) {
var a = form.querySelector('#einr-anschreiben').value;
var l = form.querySelector('#einr-lebenslauf').value;
return post('/api/einrichtung/basis', { typ: 'Anschreiben', inhalt: a })
.then(function () { return post('/api/einrichtung/basis', { typ: 'Lebenslauf', inhalt: l }); })
.then(function (r) {
if (!r.ok || !r.json.success) throw new Error(r.json.error || 'Speichern fehlgeschlagen');
});
return post('/api/einrichtung/basis', { typ: 'Anschreiben', inhalt: v('#einr-anschreiben') })
.then(function () { return post('/api/einrichtung/basis', { typ: 'Lebenslauf', inhalt: v('#einr-lebenslauf') }); })
.then(check);
}
if (stepNr === 4) {
return post('/api/einrichtung/ollama', {
OLLAMA_API_KEY: form.querySelector('#einr-apikey').value,
OLLAMA_MODEL: form.querySelector('#einr-model').value,
OLLAMA_HOST: form.querySelector('#einr-host').value
}).then(function (r) {
if (!r.ok || !r.json.success) throw new Error(r.json.error || 'Speichern fehlgeschlagen');
});
OLLAMA_API_KEY: v('#einr-apikey'), OLLAMA_MODEL: v('#einr-model'), OLLAMA_HOST: v('#einr-host')
}).then(check);
}
if (stepNr === 5) {
var data = {
MAIL_HOST: v('#einr-mail-host'), MAIL_SMTP_PORT: v('#einr-mail-smtp-port'),
MAIL_IMAP_PORT: v('#einr-mail-imap-port'), MAIL_USER: v('#einr-mail-user'),
MAIL_FROM_NAME: v('#einr-mail-fromname'), MAIL_FROM: v('#einr-mail-from'),
MAIL_IMAP_MAILBOX: v('#einr-mail-mailbox'), MAIL_POLL_MS: v('#einr-mail-poll'),
CALDAV_URL: v('#einr-caldav-url'), CALDAV_ALARM_MIN: v('#einr-caldav-alarm'),
CALDAV_POLL_MS: v('#einr-caldav-poll')
};
var pw = v('#einr-mail-password');
if (pw) data.MAIL_PASSWORD = pw; // leer = vorhandenes Passwort nicht überschreiben
return post('/api/einrichtung/config', data).then(check);
}
if (stepNr === 6) {
var token = v('#einr-api-token');
var data = {};
if (token) data.API_TOKEN = token; // leer = vorhandenen Token nicht anfassen
return post('/api/einrichtung/config', data).then(check);
}
return Promise.resolve();
}
@@ -474,12 +585,16 @@
btnWeiter.addEventListener('click', function () {
showFehler('');
if (step === MAX) {
// Assistent abschließen: dauerhaft ausblenden und zur Übersicht.
post('/api/einrichtung/verstecken', {}).then(function () { window.location.href = '/'; });
return;
}
setBusy(true);
speichern(step).then(function () {
// Nach dem Speichern ggf. „vorhanden"-Flags aktualisieren, damit die
// Abschluss-Checkliste korrekt steht (z. B. neu erzeugter API-Token).
if (step === 4 && v('#einr-apikey')) VORHANDEN.apiKey = VORHANDEN.apiKey || !!v('#einr-apikey').trim();
if (step === 5 && v('#einr-mail-password')) VORHANDEN.mailPassword = VORHANDEN.mailPassword || !!v('#einr-mail-password').trim();
if (step === 6 && v('#einr-api-token')) VORHANDEN.apiToken = VORHANDEN.apiToken || !!v('#einr-api-token').trim();
setBusy(false);
gehZu(step + 1);
}).catch(function (e) {
@@ -489,29 +604,42 @@
});
btnZurueck.addEventListener('click', function () { showFehler(''); gehZu(step - 1); });
btnUeberspringen.addEventListener('click', function () { showFehler(''); gehZu(step + 1); });
btnDirekt.addEventListener('click', function () { if (typeof window.openAddApplicationModal === 'function') window.openAddApplicationModal(); });
btnAusblenden.addEventListener('click', function () { post('/api/einrichtung/verstecken', {}).then(function () { window.location.href = '/'; }); });
btnUeberspringen.addEventListener('click', function () {
showFehler('');
// Überspringen speichert bewusst nicht — der Schritt gilt als offen.
gehZu(step + 1);
});
btnDirekt.addEventListener('click', function () {
if (typeof window.openAddApplicationModal === 'function') window.openAddApplicationModal();
});
btnAusblenden.addEventListener('click', function () {
post('/api/einrichtung/verstecken', {}).then(function () { window.location.href = '/'; });
});
// API-Schlüssel anzeigen/verbergen.
var toggle = document.getElementById('einr-toggle-key');
if (toggle) {
toggle.addEventListener('click', function () {
var inp = document.getElementById('einr-apikey');
// Reveal-Toggle für alle Passwort-/Secret-Felder (ein shared Handler).
Array.prototype.slice.call(form.querySelectorAll('.einr-reveal')).forEach(function (btn) {
btn.addEventListener('click', function () {
var inp = document.getElementById(btn.getAttribute('data-target'));
if (inp) inp.type = inp.type === 'password' ? 'text' : 'password';
});
});
// API-Token neu generieren (32 Byte hex, clientseitig CSPRNG).
var gen = document.getElementById('einr-token-gen');
var copy = document.getElementById('einr-token-copy');
var hinweis = document.getElementById('einr-token-hint');
function tokenHint(text, ok) {
hinweis.textContent = text;
hinweis.classList.remove('hidden');
hinweis.classList.toggle('text-amber-600', !ok); hinweis.classList.toggle('dark:text-amber-400', !ok);
hinweis.classList.toggle('text-green-600', ok); hinweis.classList.toggle('dark:text-green-400', ok);
}
if (gen) gen.addEventListener('click', function () {
var inp = document.getElementById('einr-api-token');
if (!inp) return;
var bytes = new Uint8Array(32); crypto.getRandomValues(bytes);
inp.value = Array.from(bytes).map(function (b) { return b.toString(16).padStart(2, '0'); }).join('');
inp.type = 'text';
tokenHint('Neuer Token erzeugt — zum Aktivieren „Weiter". Der alte wird damit ungültig.', false);
});
if (copy) copy.addEventListener('click', async function () {
var inp = document.getElementById('einr-api-token');
if (!inp || !inp.value) return;
try { await navigator.clipboard.writeText(inp.value); tokenHint('In die Zwischenablage kopiert.', true); }
catch (e) { inp.type = 'text'; inp.select(); tokenHint('Kopieren nicht möglich — Wert ist markiert.', false); }
});
render();
})();