Files
thomasandClaude Opus 4.8 1dd4807ca6 Einstellungen: Vorauswahl für KI-Generierung (Dokumente + Anlagen)
Zwei neue, pro Benutzer gespeicherte Konfigurationswerte:
GEN_DOKUMENTE_DEFAULT (Standard: Anschreiben + Lebenslauf) und
GEN_ANLAGEN_DEFAULT (Standard: keine). Sie belegen die Häkchen auf der
Bewerbungsseite vor; eine bereits getroffene Dokumentenauswahl der
Bewerbung gewinnt weiterhin. Die REST-API nutzt die Vorauswahl, wenn
dokumente/anlagen im Body fehlen.

Die Einstellungsseite kennt dafür jetzt Checkbox-Gruppen, deren Optionen
auch erst zur Renderzeit feststehen dürfen (die eigenen Anlagen).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 22:31:19 +02:00

207 lines
14 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<%- include('partials/head') %>
</head>
<body class="min-h-screen flex flex-col transition-colors duration-300 bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100" id="body">
<%- include('partials/header') %>
<main class="flex-1 container mx-auto px-4 py-8 max-w-4xl">
<!-- Back link -->
<a href="/" class="inline-flex items-center gap-2 text-sm text-blue-600 dark:text-blue-400 hover:underline mb-6">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
</svg>
Zurück zur Übersicht
</a>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-8">
<div class="flex items-start gap-3">
<svg class="w-6 h-6 text-gray-500 dark:text-gray-400 shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
<div>
<h2 class="text-lg font-semibold text-gray-800 dark:text-white">Einstellungen</h2>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1 max-w-2xl">
Hier liegen alle Werte, die früher über die <code class="px-1 rounded bg-gray-100 dark:bg-gray-700">.env</code>-Datei
gesetzt wurden Ollama, E-Mail, Kalender und die REST-API. Gespeichert wird in der Datenbank;
Änderungen wirken <em>sofort</em>, ein Neustart ist nicht nötig. Lediglich die Abruf-/Sync-Intervalle
(E-Mail &amp; Kalender) greifen erst nach einem Neustart.
</p>
</div>
</div>
</div>
<form action="/einstellungen" method="POST" class="space-y-6">
<% felder.forEach(function(sektion){ %>
<section class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 class="text-base font-semibold text-gray-800 dark:text-white"><%= sektion.titel %></h3>
<% if (sektion.beschreibung) { %>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1 mb-5 max-w-2xl"><%= sektion.beschreibung %></p>
<% } else { %>
<div class="mb-5"></div>
<% } %>
<div class="grid gap-4 sm:grid-cols-2">
<% sektion.items.forEach(function(f){
var val = werte[f.key] != null ? werte[f.key] : '';
var inputType = f.secret ? 'password' : 'text';
var inputClass = 'w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 text-sm pr-10';
var gewaehlt = String(val).split(',').map(function(s){ return s.trim(); });
// Feste Optionen aus der Feld-Definition oder bei „optionsFrom“
// die zur Renderzeit geladenen Listen (z. B. die eigenen Anlagen).
var optionen = f.options || (f.optionsFrom ? (auswahllisten[f.optionsFrom] || []) : []);
%>
<% if (f.type === 'checkboxes') { %>
<div class="sm:col-span-2">
<span class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<%= f.label %>
<code class="ml-1 text-[11px] font-normal text-gray-400 dark:text-gray-500"><%= f.key %></code>
</span>
<!-- Leerer Wert, damit „nichts angehakt“ auch gespeichert wird. -->
<input type="hidden" name="<%= f.key %>" value="">
<% if (!optionen.length) { %>
<p class="text-sm text-gray-400 dark:text-gray-500"><%= f.leerHinweis || 'Keine Auswahl verfügbar.' %></p>
<% } else { %>
<div class="flex flex-wrap gap-x-6 gap-y-2">
<% optionen.forEach(function(o){ %>
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
<input type="checkbox" name="<%= f.key %>" value="<%= o.value %>"
<%= gewaehlt.includes(String(o.value)) ? 'checked' : '' %>
class="rounded border-gray-300 dark:border-gray-600 text-blue-600">
<%= o.label %>
</label>
<% }); %>
</div>
<% } %>
<% if (f.help) { %>
<p class="text-xs text-gray-400 dark:text-gray-500 mt-2"><%= f.help %></p>
<% } %>
</div>
<% return; } %>
<div class="<%= f.secret ? 'sm:col-span-2' : '' %>">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="cfg-<%= f.key %>">
<%= f.label %>
<code class="ml-1 text-[11px] font-normal text-gray-400 dark:text-gray-500"><%= f.key %></code>
</label>
<div class="relative">
<input id="cfg-<%= f.key %>" name="<%= f.key %>" type="<%= inputType %>" autocomplete="off"
spellcheck="false" class="<%= inputClass %>" value="<%= val %>">
<% if (f.secret) { %>
<button type="button" data-toggle="cfg-<%= f.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="w-4 h-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>
<% } %>
</div>
<% if (f.generate) { %>
<div class="flex items-center gap-2 mt-2">
<button type="button" data-generate="cfg-<%= f.key %>"
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors">
<svg class="w-3.5 h-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"></path>
</svg>
Neu generieren
</button>
<button type="button" data-copy="cfg-<%= f.key %>"
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors">
<svg class="w-3.5 h-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"></path>
</svg>
Kopieren
</button>
<span data-hint="cfg-<%= f.key %>" class="text-xs text-amber-600 dark:text-amber-400 hidden"></span>
</div>
<% } %>
<% if (f.help) { %>
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1"><%= f.help %></p>
<% } %>
</div>
<% }); %>
</div>
</section>
<% }); %>
<div class="flex items-center justify-end gap-3">
<a href="/einstellungen" class="px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors">
Verwerfen
</a>
<button type="submit"
class="inline-flex items-center gap-2 px-4 py-2 text-sm bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
Speichern
</button>
</div>
</form>
</main>
<%- include('partials/footer') %>
<script>
(function () {
const cy = document.getElementById('currentYear');
if (cy) cy.textContent = new Date().getFullYear();
// Toggle reveal for secret (password) fields.
document.querySelectorAll('[data-toggle]').forEach(function (btn) {
btn.addEventListener('click', function () {
const input = document.getElementById(btn.getAttribute('data-toggle'));
if (input) input.type = input.type === 'password' ? 'text' : 'password';
});
});
function hint(id, text, tone) {
const el = document.querySelector('[data-hint="' + id + '"]');
if (!el) return;
el.textContent = text;
el.classList.remove('hidden');
el.classList.toggle('text-amber-600', tone !== 'ok');
el.classList.toggle('dark:text-amber-400', tone !== 'ok');
el.classList.toggle('text-green-600', tone === 'ok');
el.classList.toggle('dark:text-green-400', tone === 'ok');
}
// Generate a fresh random token (32 bytes hex, from the browser CSPRNG).
// It only fills the field — nothing changes until the form is saved,
// so an accidental click can still be discarded.
document.querySelectorAll('[data-generate]').forEach(function (btn) {
btn.addEventListener('click', function () {
const id = btn.getAttribute('data-generate');
const input = document.getElementById(id);
if (!input) return;
const bytes = new Uint8Array(32);
crypto.getRandomValues(bytes);
input.value = Array.from(bytes).map(function (b) {
return b.toString(16).padStart(2, '0');
}).join('');
input.type = 'text'; // reveal, so it can be copied down
hint(id, 'Neuer Token erzeugt — zum Aktivieren speichern. Der alte wird damit ungültig.');
});
});
document.querySelectorAll('[data-copy]').forEach(function (btn) {
btn.addEventListener('click', async function () {
const id = btn.getAttribute('data-copy');
const input = document.getElementById(id);
if (!input || !input.value) return;
try {
await navigator.clipboard.writeText(input.value);
hint(id, 'In die Zwischenablage kopiert.', 'ok');
} catch (e) {
input.type = 'text';
input.select();
hint(id, 'Kopieren nicht möglich — Wert ist markiert, bitte manuell kopieren.');
}
});
});
})();
</script>
</body>
</html>