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:
@@ -1709,8 +1709,9 @@ initializeDatabase().then(async () => {
|
||||
|
||||
// Einrichtungsassistent: eingeblendet, wenn der Nutzer noch keine
|
||||
// Bewerbung angelegt hat und den Assistenten nicht dauerhaft ausgeblendet
|
||||
// hat. Der Wizard selbst führt dann durch die fehlenden Einstellungen
|
||||
// (Persönliche Angaben, Basis-Unterlagen, Ollama-KI).
|
||||
// hat. Der Wizard führt durch alle Bereiche, die sonst unter /einstellungen
|
||||
// liegen: Persönliche Angaben, Basis-Unterlagen, Ollama-KI, E-Mail,
|
||||
// Bewerbungskalender (CalDAV) und REST-API.
|
||||
const keinDashboard = !totalCount || totalCount.count === 0;
|
||||
const verstecktRow = keinDashboard
|
||||
? await dbGet("SELECT value FROM app_state WHERE user_id = ? AND key = 'state:einrichtung_versteckt'", [U])
|
||||
@@ -1720,6 +1721,10 @@ initializeDatabase().then(async () => {
|
||||
const basisRows = await dbAll('SELECT id, typ, inhalt FROM basis_dokumente WHERE user_id = ? ORDER BY id ASC', [U]);
|
||||
const anschreiben = basisRows.find((d) => d.typ === 'Anschreiben');
|
||||
const lebenslauf = basisRows.find((d) => d.typ === 'Lebenslauf');
|
||||
// Aktuelle Konfiguration für die Vorbelegung — Secrets (Passwörter, Token,
|
||||
// API-Schlüssel) werden bewusst nicht ans Frontend gegeben, sondern nur
|
||||
// als „vorhanden"-Flag.
|
||||
const cfg = config.getAll();
|
||||
setupWizard = {
|
||||
aktiv: true,
|
||||
persoenlich: await loadSettings(),
|
||||
@@ -1728,6 +1733,14 @@ initializeDatabase().then(async () => {
|
||||
hasApiKey: Boolean(config.get('OLLAMA_API_KEY')),
|
||||
ollamaModel: config.get('OLLAMA_MODEL'),
|
||||
ollamaHost: config.get('OLLAMA_HOST'),
|
||||
cfg: {
|
||||
MAIL_HOST: cfg.MAIL_HOST, MAIL_SMTP_PORT: cfg.MAIL_SMTP_PORT, MAIL_IMAP_PORT: cfg.MAIL_IMAP_PORT,
|
||||
MAIL_USER: cfg.MAIL_USER, MAIL_FROM_NAME: cfg.MAIL_FROM_NAME, MAIL_FROM: cfg.MAIL_FROM,
|
||||
MAIL_IMAP_MAILBOX: cfg.MAIL_IMAP_MAILBOX, MAIL_POLL_MS: cfg.MAIL_POLL_MS,
|
||||
hasMailPassword: Boolean(cfg.MAIL_PASSWORD),
|
||||
CALDAV_URL: cfg.CALDAV_URL, CALDAV_ALARM_MIN: cfg.CALDAV_ALARM_MIN, CALDAV_POLL_MS: cfg.CALDAV_POLL_MS,
|
||||
hasApiToken: Boolean(cfg.API_TOKEN),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2063,6 +2076,49 @@ initializeDatabase().then(async () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Generisches Speichern für die weiteren Wizard-Schritte (E-Mail, CalDAV,
|
||||
// REST-API). Nimmt nur Schlüssel aus config.DEFAULTS entgegen und schreibt
|
||||
// sie gezielt via setForUser — ungenannte Keys bleiben unangetastet. Hosts
|
||||
// werden gegen SSRF geprüft, der API-Token auf Kollision mit anderem Benutzer.
|
||||
app.post('/api/einrichtung/config', async (req, res) => {
|
||||
try {
|
||||
const u = uid();
|
||||
const erlaubt = new Set(Object.keys(config.DEFAULTS));
|
||||
const werte = {};
|
||||
Object.keys(req.body || {}).forEach(function (k) {
|
||||
if (erlaubt.has(k)) werte[k] = String(req.body[k] == null ? '' : req.body[k]).trim();
|
||||
});
|
||||
// SSRF-Guard für externe Hosts, die der Server anruft.
|
||||
if (werte.OLLAMA_HOST) {
|
||||
try { await assertSafeUrl(werte.OLLAMA_HOST); }
|
||||
catch (vErr) { return res.status(400).json({ error: 'Ollama-Host abgelehnt: ' + (vErr.message || 'ungültig') }); }
|
||||
}
|
||||
if (werte.CALDAV_URL) {
|
||||
try { await assertSafeUrl(werte.CALDAV_URL); }
|
||||
catch (vErr) { return res.status(400).json({ error: 'Kalender-URL abgelehnt: ' + (vErr.message || 'ungültig') }); }
|
||||
}
|
||||
if (werte.MAIL_HOST) {
|
||||
try { await assertSafeHost(werte.MAIL_HOST); }
|
||||
catch (vErr) { return res.status(400).json({ error: 'Mail-Host abgelehnt: ' + (vErr.message || 'ungültig') }); }
|
||||
}
|
||||
// API-Token identifiziert den Benutzer gegenüber /api/v1 — Kollision würde
|
||||
// beide Nutzer aussperren, daher schon hier ablehnen.
|
||||
if (werte.API_TOKEN) {
|
||||
const fremd = await dbGet(
|
||||
`SELECT u.username FROM app_state a JOIN users u ON u.id = a.user_id
|
||||
WHERE a.key = ? AND a.value = ? AND a.user_id != ?`,
|
||||
[config.PREFIX + 'API_TOKEN', werte.API_TOKEN, u]
|
||||
);
|
||||
if (fremd) return res.status(409).json({ error: 'API-Token bereits von anderem Benutzer belegt. Bitte „Neu generieren".' });
|
||||
}
|
||||
for (const k of Object.keys(werte)) await config.setForUser(u, k, werte[k]);
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Einrichtung/config:', error);
|
||||
res.status(500).json({ error: 'Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
// Wizard dauerhaft ausblenden (app_state-Flag), damit er nicht bei jedem
|
||||
// Besuch der leeren Startseite wieder erscheint.
|
||||
app.post('/api/einrichtung/verstecken', async (req, res) => {
|
||||
|
||||
+291
-163
@@ -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 & 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 & 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 & 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 & 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.
|
||||
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 + '"]');
|
||||
// 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');
|
||||
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 ? '' : '';
|
||||
});
|
||||
}
|
||||
// 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() {
|
||||
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();
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user