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:
2026-07-13 20:57:31 +02:00
co-authored by Claude
parent 41887dd56c
commit 97b48f9841
12 changed files with 410 additions and 72 deletions
+44 -9
View File
@@ -40,6 +40,7 @@ const { buildOpenApiSpec } = require('./lib/openapi');
const blacklist = require('./lib/blacklist');
const caldav = require('./lib/caldav');
const { LABEL_OPTIONS, parseLabels, serializeLabels } = require('./lib/labels');
const config = require('./lib/config');
const app = express();
const PORT = process.env.PORT || 3000;
@@ -1065,9 +1066,14 @@ function initializeDatabase() {
}
// Initialize and start server
initializeDatabase().then(() => {
initializeDatabase().then(async () => {
console.log('Database initialized successfully');
// Load configuration from the DB (migrates any still-present .env values
// once). Must run before the boot checks below (mailer/caldav configured?) and
// before any route that reads config — values live in the DB now, not in .env.
await config.init({ dbAll, dbRun });
// Routes
app.get('/', async (req, res) => {
try {
@@ -1976,7 +1982,7 @@ initializeDatabase().then(() => {
hasSignatur: Boolean(currentSignaturFile()),
hasFoto: Boolean(currentFotoFile()),
basisTypOptions: BASIS_TYP_OPTIONS,
hasApiKey: Boolean(process.env.OLLAMA_API_KEY),
hasApiKey: Boolean(config.get('OLLAMA_API_KEY')),
hideSettings: true,
});
} catch (error) {
@@ -2653,6 +2659,34 @@ initializeDatabase().then(() => {
}
});
// ----- Einstellungen (früher .env — jetzt in der Datenbank) -----
// Alle Konfigurationswerte (Ollama, E-Mail, CalDAV, REST-API) liegen in der
// app_state-Tabelle und sind hier editierbar. Speichern wirkt sofort, ein
// Neustart ist nicht nötig (die Libs lesen per config.get() zur Laufzeit).
app.get('/einstellungen', (req, res) => {
res.render('settings', {
felder: config.FIELDS,
werte: config.getAll(),
hideSettings: true,
});
});
app.post('/einstellungen', async (req, res) => {
try {
const werte = {};
for (const sektion of config.FIELDS) {
for (const f of sektion.items) {
werte[f.key] = req.body[f.key] != null ? String(req.body[f.key]) : '';
}
}
await config.saveAll(werte);
res.redirect('/einstellungen');
} catch (error) {
console.error('Error saving settings:', error);
res.status(500).send('Serverfehler');
}
});
// ----- Conversational KI-Chat (Ollama, streaming) -----
// Gated behind OLLAMA_API_KEY. Threads + messages persist in SQLite; the
// assistant answer is streamed back via Server-Sent Events.
@@ -2804,7 +2838,7 @@ initializeDatabase().then(() => {
// Chat page: list threads + render the active thread (or a fresh empty one).
app.get('/chat', async (req, res) => {
if (!chat.isConfigured()) return res.status(503).send('KI-Chat deaktiviert OLLAMA_API_KEY fehlt.');
if (!chat.isConfigured()) return res.status(503).send('KI-Chat deaktiviert kein Ollama-API-Schlüssel konfiguriert (unter „Einstellungen“ eintragen).');
try {
const threads = await dbAll(
'SELECT id, titel, updated_at FROM chat_threads ORDER BY updated_at DESC'
@@ -2965,8 +2999,9 @@ initializeDatabase().then(() => {
// ----- Third-party REST API (/api/v1) + OpenAPI/Swagger -----
// API key for third-party software. When unset, the API responds 503 on
// every endpoint except /health — it never silently exposes data.
const apiToken = process.env.API_TOKEN || '';
// every endpoint except /health — it never silently exposes data. Read from
// the DB via config so an edit on /einstellungen is picked up live.
const apiToken = () => config.get('API_TOKEN') || '';
app.use('/api/v1', createExternalApi({
dbGet,
dbAll,
@@ -3036,7 +3071,7 @@ initializeDatabase().then(() => {
// Start server
app.listen(PORT, () => {
console.log(`Server läuft auf http://localhost:${PORT}`);
if (apiToken) console.log('REST-API (/api/v1) aktiv Swagger unter /swagger');
if (apiToken()) console.log('REST-API (/api/v1) aktiv Swagger unter /swagger');
else console.log('REST-API deaktiviert API_TOKEN fehlt (Swagger unter /swagger weiterhin verfügbar)');
});
@@ -3045,7 +3080,7 @@ initializeDatabase().then(() => {
mailer.verify()
.then(() => console.log(`E-Mail aktiv: Versand über ${mailer.config().host} als ${mailer.fromAddress()}`))
.catch((e) => console.warn('E-Mail SMTP-Verbindung nicht verifizierbar:', e.message));
const pollMs = Math.max(60000, Number(process.env.MAIL_POLL_MS) || 180000);
const pollMs = Math.max(60000, Number(config.get('MAIL_POLL_MS')) || 180000);
setTimeout(() => { pollInbox().catch(() => {}); }, 8000); // initial fetch after boot
setInterval(() => { pollInbox().catch(() => {}); }, pollMs); // periodic fetch
} else {
@@ -3055,7 +3090,7 @@ initializeDatabase().then(() => {
// Calendar: reconcile our appointments with the SOGo CalDAV calendar.
if (caldav.isConfigured()) {
console.log(`Kalender aktiv: CalDAV ${caldav.collectionUrl()}`);
const calPoll = Math.max(60000, Number(process.env.CALDAV_POLL_MS) || 300000);
const calPoll = Math.max(60000, Number(config.get('CALDAV_POLL_MS')) || 300000);
setTimeout(() => { refreshCaldav().catch(() => {}); }, 10000); // initial sync after boot
setInterval(() => { refreshCaldav().catch(() => {}); }, calPoll); // periodic reconcile
} else {