Einstellungen-Seite: .env-Werte in DB (app_state), keine .env mehr
Neue /einstellungen-Seite (Zahnrad im Header) mit allen bisherigen .env-Werten (Ollama, E-Mail, CalDAV, REST-API), gespeichert in SQLite (app_state, cfg:-Prefix). Libs lesen per lib/config.js zur Laufzeit statt beim Start -> Aenderungen wirken sofort, kein Neustart. Bestehende .env wird beim ersten Start einmalig migriert. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+163
@@ -0,0 +1,163 @@
|
||||
// Centralized configuration store.
|
||||
//
|
||||
// Replaces the .env file: all settings live in the SQLite app_state table
|
||||
// (rows prefixed "cfg:") and are editable via the /einstellungen page. The
|
||||
// libs read values through config.get() / config.ollama() at *call* time, so
|
||||
// an edit in the UI takes effect immediately — no restart, no .env file.
|
||||
//
|
||||
// On first start of an install that previously used .env, init() migrates any
|
||||
// still-present env value into the DB once, so existing config is not lost.
|
||||
// After that the database is the single source of truth; process.env is only a
|
||||
// fallback for keys that were never saved (and for the one-time migration).
|
||||
|
||||
const DEFAULTS = {
|
||||
// Ollama Cloud (KI text generation + chat).
|
||||
OLLAMA_API_KEY: '',
|
||||
OLLAMA_MODEL: 'gpt-oss:120b',
|
||||
OLLAMA_HOST: 'https://ollama.com',
|
||||
OLLAMA_TIMEOUT_MS: '300000',
|
||||
// E-Mail (SMTP submission + IMAP receive).
|
||||
MAIL_HOST: '',
|
||||
MAIL_SMTP_PORT: '587',
|
||||
MAIL_IMAP_PORT: '993',
|
||||
MAIL_USER: '',
|
||||
MAIL_PASSWORD: '',
|
||||
MAIL_FROM_NAME: '',
|
||||
MAIL_FROM: '',
|
||||
MAIL_IMAP_MAILBOX: 'INBOX',
|
||||
MAIL_POLL_MS: '180000',
|
||||
// Bewerbungskalender (CalDAV, z. B. SOGo).
|
||||
CALDAV_URL: '',
|
||||
CALDAV_ALARM_MIN: '60',
|
||||
CALDAV_POLL_MS: '300000',
|
||||
// REST-API für Drittanbietersoftware (/api/v1, Header X-API-Key).
|
||||
API_TOKEN: '',
|
||||
};
|
||||
|
||||
// UI metadata for the /einstellungen page: grouped sections with input types.
|
||||
// `secret` fields render as password inputs with a reveal toggle.
|
||||
const FIELDS = [
|
||||
{
|
||||
titel: 'Ollama Cloud (KI-Generierung + Chat)',
|
||||
beschreibung: 'Steuerung der KI, die Bewerbungsunterlagen erzeugt und den KI-Chat beantwortet. Ohne API-Schlüssel sind diese Funktionen deaktiviert.',
|
||||
items: [
|
||||
{ key: 'OLLAMA_API_KEY', label: 'API-Schlüssel', secret: true, help: 'Schlüssel von https://ollama.com/settings/keys' },
|
||||
{ key: 'OLLAMA_MODEL', label: 'Modell', help: 'Standard: gpt-oss:120b' },
|
||||
{ key: 'OLLAMA_HOST', label: 'Host', help: 'Standard: https://ollama.com — lokaler Ollama: http://localhost:11434' },
|
||||
{ key: 'OLLAMA_TIMEOUT_MS', label: 'Timeout (ms)', help: 'Standard: 300000 (5 Min.)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
titel: 'E-Mail (SMTP-Versand + IMAP-Empfang)',
|
||||
beschreibung: 'Versand läuft über den eigenen Mailserver (DKIM/SPF/DMARC-Alignment). Ohne Host/Benutzer/Passwort ist der E-Mail-Teil deaktiviert.',
|
||||
items: [
|
||||
{ key: 'MAIL_HOST', label: 'SMTP/IMAP Host' },
|
||||
{ key: 'MAIL_SMTP_PORT', label: 'SMTP-Port', help: '587 = STARTTLS, 465 = implicit TLS' },
|
||||
{ key: 'MAIL_IMAP_PORT', label: 'IMAP-Port', help: 'Standard: 993 (implicit TLS)' },
|
||||
{ key: 'MAIL_USER', label: 'Benutzername (Login)', help: 'Auch CalDAV-Login' },
|
||||
{ key: 'MAIL_PASSWORD', label: 'Passwort', secret: true, help: 'Auch CalDAV-Passwort' },
|
||||
{ key: 'MAIL_FROM_NAME', label: 'Absendername' },
|
||||
{ key: 'MAIL_FROM', label: 'Absenderadresse', help: 'Leer = Benutzername' },
|
||||
{ key: 'MAIL_IMAP_MAILBOX', label: 'IMAP-Postfach', help: 'Standard: INBOX' },
|
||||
{ key: 'MAIL_POLL_MS', label: 'Abrufintervall (ms)', help: 'Standard: 180000 (3 Min.) — greift nach Neustart' },
|
||||
],
|
||||
},
|
||||
{
|
||||
titel: 'Bewerbungskalender (CalDAV)',
|
||||
beschreibung: 'Voll-URL der Kalender-Sammlung (mit abschließendem /). Authentifizierung läuft über MAIL_USER/MAIL_PASSWORD. Ohne URL sind die Kalender-Funktionen deaktiviert.',
|
||||
items: [
|
||||
{ key: 'CALDAV_URL', label: 'Kalender-URL', help: 'z. B. https://mail.example.com/SOGo/dav/name@…/Calendar/XXXX/' },
|
||||
{ key: 'CALDAV_ALARM_MIN', label: 'Erinnerung (Minuten vor Termin)', help: 'Standard: 60' },
|
||||
{ key: 'CALDAV_POLL_MS', label: 'Sync-Intervall (ms)', help: 'Standard: 300000 (5 Min.) — greift nach Neustart' },
|
||||
],
|
||||
},
|
||||
{
|
||||
titel: 'REST-API für Drittanbietersoftware',
|
||||
beschreibung: 'Ist ein Token gesetzt, ist /api/v1 aktiv und erwartet den Wert im Header „X-API-Key“. Ohne Token antwortet die API (bis auf /health) mit 503. Swagger unter /swagger.',
|
||||
items: [
|
||||
{ key: 'API_TOKEN', label: 'API-Token (X-API-Key)', secret: true, help: 'Leer = API deaktiviert' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const PREFIX = 'cfg:';
|
||||
const cache = Object.create(null); // key -> string (only keys present in the DB)
|
||||
let dbAllFn = null;
|
||||
let dbRunFn = null;
|
||||
|
||||
function envOrDefault(key) {
|
||||
const e = process.env[key];
|
||||
return e && e.length ? e : DEFAULTS[key];
|
||||
}
|
||||
|
||||
// Synchronous read. Falls back to process.env (pre-migration / never saved)
|
||||
// then to the built-in default. After init() the DB value is cached and wins.
|
||||
function get(key) {
|
||||
const v = cache[key];
|
||||
if (v !== undefined) return v;
|
||||
return envOrDefault(key);
|
||||
}
|
||||
|
||||
// All keys with their effective values, keyed by name — used by the settings UI.
|
||||
function getAll() {
|
||||
const out = {};
|
||||
for (const key of Object.keys(DEFAULTS)) out[key] = get(key);
|
||||
return out;
|
||||
}
|
||||
|
||||
// Ollama bundle (shared by lib/documents.js + lib/chat.js).
|
||||
function ollama() {
|
||||
return {
|
||||
host: (get('OLLAMA_HOST') || 'https://ollama.com').replace(/\/+$/, ''),
|
||||
model: get('OLLAMA_MODEL') || 'gpt-oss:120b',
|
||||
timeoutMs: Number(get('OLLAMA_TIMEOUT_MS')) || 300000,
|
||||
apiKey: get('OLLAMA_API_KEY') || '',
|
||||
};
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (!dbAllFn) return;
|
||||
const rows = await dbAllFn('SELECT key, value FROM app_state WHERE key LIKE ?', [PREFIX + '%']);
|
||||
for (const r of rows) cache[r.key.slice(PREFIX.length)] = r.value;
|
||||
}
|
||||
|
||||
// Wire up DB helpers, load the cached rows, then one-time-migrate any env
|
||||
// value that isn't yet in the DB (so an existing .env install keeps its config).
|
||||
async function init({ dbAll, dbRun }) {
|
||||
dbAllFn = dbAll;
|
||||
dbRunFn = dbRun;
|
||||
await load();
|
||||
const toMigrate = [];
|
||||
for (const key of Object.keys(DEFAULTS)) {
|
||||
if (cache[key] === undefined) {
|
||||
const e = process.env[key];
|
||||
if (e && e.length) toMigrate.push([key, e]);
|
||||
}
|
||||
}
|
||||
if (toMigrate.length) {
|
||||
for (const [k, v] of toMigrate) {
|
||||
await dbRunFn(
|
||||
'INSERT INTO app_state (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value',
|
||||
[PREFIX + k, v]
|
||||
);
|
||||
cache[k] = v;
|
||||
}
|
||||
console.log(`Konfiguration aus .env in die Datenbank migriert (${toMigrate.length} Werte) — .env wird nicht mehr benötigt.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Persist every key (writes all rows, including empty strings, so a cleared
|
||||
// field is stored as empty and no longer falls back to env/default).
|
||||
async function saveAll(values) {
|
||||
if (!dbRunFn) throw new Error('Konfigurationsspeicher nicht initialisiert.');
|
||||
for (const key of Object.keys(DEFAULTS)) {
|
||||
const v = values && values[key] != null ? String(values[key]) : '';
|
||||
await dbRunFn(
|
||||
'INSERT INTO app_state (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value',
|
||||
[PREFIX + key, v]
|
||||
);
|
||||
cache[key] = v;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { DEFAULTS, FIELDS, get, getAll, ollama, init, load, saveAll };
|
||||
Reference in New Issue
Block a user