diff --git a/backend/src/consoleLog.js b/backend/src/consoleLog.js new file mode 100644 index 0000000..89235bd --- /dev/null +++ b/backend/src/consoleLog.js @@ -0,0 +1,57 @@ +const fs = require('fs'); + +// Persistenter Pfad (selbes Volume wie die DB), übersteht Container-Neustarts +const LOG_FILE = process.env.CONSOLE_LOG_FILE || '/data/app-console.log'; +const MAX_AGE_MS = 24 * 60 * 60 * 1000; + +const origLog = console.log.bind(console); +const origError = console.error.bind(console); +const origWarn = console.warn.bind(console); + +function stringifyArg(a) { + if (typeof a === 'string') return a; + if (a instanceof Error) return a.stack || a.message; + try { return JSON.stringify(a); } catch { return String(a); } +} + +function appendLine(level, args) { + try { + const msg = args.map(stringifyArg).join(' '); + fs.appendFileSync(LOG_FILE, `[${new Date().toISOString()}] [${level}] ${msg}\n`); + } catch { + // Darf niemals die eigentliche Konsolen-Ausgabe verhindern + } +} + +// Entfernt Zeilen, die älter als MAX_AGE_MS sind. Zeilen ohne erkennbaren +// Zeitstempel (z.B. Fortsetzungszeilen eines mehrzeiligen Stacktraces) werden +// bewusst behalten, um nichts mittendrin abzuschneiden. +function pruneOldLines() { + try { + if (!fs.existsSync(LOG_FILE)) return; + const content = fs.readFileSync(LOG_FILE, 'utf8'); + const cutoff = Date.now() - MAX_AGE_MS; + const lines = content.split('\n').filter(line => { + const m = line.match(/^\[([^\]]+)\]/); + if (!m) return true; + const t = new Date(m[1]).getTime(); + return Number.isNaN(t) || t >= cutoff; + }); + fs.writeFileSync(LOG_FILE, lines.join('\n')); + } catch (e) { + origError('Konsolen-Log-Aufräumen fehlgeschlagen:', e.message); + } +} + +function installConsoleCapture() { + console.log = (...args) => { origLog(...args); appendLine('LOG', args); }; + console.error = (...args) => { origError(...args); appendLine('ERROR', args); }; + console.warn = (...args) => { origWarn(...args); appendLine('WARN', args); }; + + pruneOldLines(); // einmal direkt beim Start + setInterval(pruneOldLines, 60 * 60 * 1000); // danach stündlich +} + +function getLogFilePath() { return LOG_FILE; } + +module.exports = { installConsoleCapture, getLogFilePath, pruneOldLines }; diff --git a/backend/src/index.js b/backend/src/index.js index 9721fee..177b79d 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -1,3 +1,5 @@ +require('./consoleLog').installConsoleCapture(); + const express = require('express'); const path = require('path'); const fs = require('fs'); diff --git a/backend/src/routes/admin.js b/backend/src/routes/admin.js index fe0f7c0..004a216 100644 --- a/backend/src/routes/admin.js +++ b/backend/src/routes/admin.js @@ -590,4 +590,14 @@ router.delete('/logs/:id', authenticate, requireAdmin, (req, res) => { res.json({ ok: true }); }); +// GET /console-log/download – Live-Konsolen-Mitschnitt (letzte 24h) als +// Datei herunterladen (Admin) +router.get('/console-log/download', authenticate, requireAdmin, (req, res) => { + const { getLogFilePath } = require('../consoleLog'); + const filePath = getLogFilePath(); + if (!fs.existsSync(filePath)) return res.status(404).json({ error: 'Noch keine Logs vorhanden' }); + const stamp = new Date().toISOString().replace(/[:T]/g, '-').slice(0, 19); + res.download(filePath, `dickendock-console-${stamp}.log`); +}); + module.exports = router; diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 74db80f..7d8892d 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -3238,6 +3238,21 @@ function PushLogs({ toast }) { } }; + const downloadConsoleLog = async () => { + try { + const tokenVal = localStorage.getItem('sk_token'); + const res = await fetch('/api/admin/console-log/download', { headers: { Authorization: `Bearer ${tokenVal}` } }); + if (!res.ok) { const d = await res.json().catch(()=>({})); throw new Error(d.error || 'Fehler'); } + const blob = await res.blob(); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `dickendock-console-${new Date().toISOString().slice(0,10)}.log`; + document.body.appendChild(a); a.click(); document.body.removeChild(a); + URL.revokeObjectURL(url); + } catch(e) { toast?.(e.message||'Fehler','error'); } + }; + const fmtDate = iso => { if (!iso) return '—'; const d = new Date(iso.replace(' ', 'T')); @@ -3263,11 +3278,18 @@ function PushLogs({ toast }) { return (
+
+ Live-Mitschnitt der Server-Konsole (wie docker logs -f dickendock), letzte 24 Stunden — danach + werden ältere Zeilen automatisch überschrieben. +
+
setFilter(e.target.value)}