diff --git a/server.js b/server.js
index 02fe5a7..b81eb21 100644
--- a/server.js
+++ b/server.js
@@ -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) => {
diff --git a/views/partials/einrichtung.ejs b/views/partials/einrichtung.ejs
index 476e9a3..287eeae 100644
--- a/views/partials/einrichtung.ejs
+++ b/views/partials/einrichtung.ejs
@@ -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 || {};
%>
-
+
NextJobs einrichten
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.
@@ -66,56 +63,36 @@
-
-
-
- 1
- Willkommen
-
-
- 2
- Profil
-
-
- 3
- Vorlagen
-
-
- 4
- KI
-
-
- 5
- Abschluss
+
+
+ <% var stepLabels = ['Willkommen','Profil','Vorlagen','KI','E-Mail','API','Abschluss']; %>
+ <% for (var si = 1; si <= 7; si++) { %>
+
+ <%= si %>
+ <%= stepLabels[si-1] %>
+ <% } %>
-
@@ -212,8 +187,8 @@
Ollama-KI einrichten
Ü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.
@@ -222,21 +197,14 @@
-
Schlüssel holen unter
ollama.com/settings/keys.
- <% if (setupWizard.hasApiKey) { %>
- · bereits hinterlegt
- <% } %>
+ <% if (setupWizard.hasApiKey) { %>· bereits hinterlegt<% } %>
@@ -256,30 +224,159 @@
-
+
+
E-Mail & Kalender
+
+ 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.
+
+
+
+
+
E-Mail (SMTP / IMAP)
+
+
+
+
+
+
+
+
+
587 = STARTTLS, 465 = implicit TLS
+
+
+
+
+
+
+
+
+
Auch CalDAV-Login
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
greift nach Neustart
+
+
+
+
+
+
+
Bewerbungskalender (CalDAV)
+
+
+
+
+
Voll-URL der Sammlung, mit abschließendem /. Anmeldung über Benutzername/Passwort oben.
+
+
+
+
+
+
+
+
+
greift nach Neustart
+
+
+
+
+
+
+
+
REST-API (optional)
+
+ Ist ein Token gesetzt, ist /api/v1
+ für dich aktiv und erwartet den Wert im Header
+ X-API-Key.
+ Der Token identifiziert dein Konto — Anfragen sehen und ändern nur deine
+ eigenen Daten. Swagger unter /swagger.
+
+
+
+
+
+
+
+
+
+
+
+
+ Neu generieren
+
+
+
+ Kopieren
+
+
+ <% if (c.hasApiToken) { %>· bereits hinterlegt<% } %>
+
+
Leer lassen = vorhandenen Token nicht anfassen. Ein neuer Token macht den alten sofort ungültig.
+
+
+
+
+
Fast geschafft!
- Hier ist dein Stand. Fehlende Punkte kannst du jederzeit unter
+ Hier ist dein Stand. Fehlende Punkte lassen sich jederzeit unter
„Vorlagen“ oder „Einstellungen“ nachholen.
-
-
-
- Persönliche Angaben
-
-
-
- Standard-Anschreiben hinterlegt
-
-
-
- Lebenslauf hinterlegt
-
-
-
- Ollama-KI aktiv
-
+
+
Persönliche Angaben
+
Standard-Anschreiben hinterlegt
+
Lebenslauf hinterlegt
+
Ollama-KI aktiv
+
+
+
Weitere Verbindungen (optional)
+
+
E-Mail (SMTP/IMAP)
+
Bewerbungskalender (CalDAV)
+
REST-API-Token
@@ -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 = '';
+ dot.innerHTML = '';
} 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
+ ? ''
+ : '';
+ }
+ // 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
+ ? ''
+ : '';
+ }
+
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
- ? ''
- : '';
- 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();
})();