homelab-brain/fuenfvoacht/src/templates/logs.html
root 92645521b2 fix(fuenfvoacht): UNIQUE constraint bug + Einplanen für zukünftige Tage
- DB-Migration: UNIQUE(date) → UNIQUE(date, post_time) — alte DBs werden
  automatisch beim Start migriert (database.py init_db)
- api_save: gibt article_id zurück für nachgelagerte Operationen
- confirmPlan(): speichert auf selectedDate, verschiebt dann ggf. per
  reschedule auf Zieldatum — fixes "Kein Artikel für diesen Tag vorhanden"
- Alle Source-Dateien (app.py, database.py, templates, ...) hinzugefügt
- arakava-news: cursor-memory-system Artikel + SVG-Diagramm hinzugefügt

Made-with: Cursor
2026-02-28 22:46:55 +07:00

211 lines
7.4 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Logs — FünfVorAcht</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body { background: #0f172a; color: #e2e8f0; }
.card { background: #1e293b; border: 1px solid #334155; border-radius: 12px; }
.log-container {
background: #0d1117;
border: 1px solid #30363d;
border-radius: 8px;
font-family: 'Courier New', monospace;
font-size: 12px;
height: 520px;
overflow-y: auto;
padding: 12px;
}
.log-line { padding: 2px 0; border-bottom: 1px solid #1a2030; white-space: pre-wrap; word-break: break-all; }
.log-line:last-child { border-bottom: none; }
.log-ERROR { color: #f87171; }
.log-CRITICAL { color: #fb923c; font-weight: bold; }
.log-WARNING { color: #fbbf24; }
.log-INFO { color: #94a3b8; }
.log-DEBUG { color: #475569; }
.tab-btn { padding: 6px 16px; border-radius: 6px; font-size: 13px; cursor: pointer; transition: all .15s; }
.tab-active { background: #3b82f6; color: white; }
.tab-inactive { background: #1e293b; color: #94a3b8; border: 1px solid #334155; }
.badge-error { background: #7f1d1d; color: #fca5a5; }
.badge-warn { background: #713f12; color: #fde68a; }
</style>
</head>
<body class="min-h-screen">
<!-- Nav -->
<nav class="bg-slate-900 border-b border-slate-700 px-6 py-4 flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="text-2xl">🕗</span>
<span class="text-xl font-bold text-white">FünfVorAcht</span>
</div>
<div class="flex gap-4 text-sm">
<a href="/" class="text-slate-400 hover:text-white">Übersicht</a>
<a href="/history" class="text-slate-400 hover:text-white">History</a>
<a href="/prompts" class="text-slate-400 hover:text-white">Prompts</a>
<a href="/settings" class="text-slate-400 hover:text-white">Einstellungen</a>
<a href="/logs" class="text-blue-400 font-semibold">Logs</a>
</div>
</nav>
<div class="max-w-5xl mx-auto px-6 py-8">
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold text-white">📋 System-Logs</h1>
<div class="flex items-center gap-3">
<span id="error-badge" class="hidden text-xs px-2 py-1 rounded-full badge-error font-semibold"></span>
<button onclick="loadLogs()" class="text-sm bg-slate-700 hover:bg-slate-600 text-white px-4 py-2 rounded-lg">
🔄 Aktualisieren
</button>
<label class="flex items-center gap-2 text-sm text-slate-400 cursor-pointer">
<input type="checkbox" id="auto-refresh" onchange="toggleAutoRefresh()" class="rounded">
Live (10s)
</label>
</div>
</div>
<!-- Tab-Auswahl -->
<div class="flex gap-2 mb-4">
<button id="tab-app" onclick="switchTab('app.log')" class="tab-btn tab-active">
🌐 Web App
</button>
<button id="tab-bot" onclick="switchTab('bot.log')" class="tab-btn tab-inactive">
🤖 Bot
</button>
<button id="tab-error" onclick="switchTab('error.log')" class="tab-btn tab-inactive">
⚠️ Fehler
</button>
</div>
<!-- Filter-Leiste -->
<div class="flex gap-3 mb-4 items-center">
<input id="filter-input" type="text" placeholder="Filter (z.B. ERROR, /api, generate…)"
oninput="filterLogs()"
class="flex-1 bg-slate-800 border border-slate-600 rounded-lg px-3 py-2 text-sm text-white placeholder-slate-500 focus:outline-none focus:border-blue-500">
<select id="level-filter" onchange="filterLogs()"
class="bg-slate-800 border border-slate-600 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-blue-500">
<option value="">Alle Level</option>
<option value="ERROR">ERROR</option>
<option value="WARNING">WARNING</option>
<option value="INFO">INFO</option>
</select>
<select id="lines-select" onchange="loadLogs()"
class="bg-slate-800 border border-slate-600 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-blue-500">
<option value="100">100 Zeilen</option>
<option value="200" selected>200 Zeilen</option>
<option value="500">500 Zeilen</option>
</select>
</div>
<!-- Log-Container -->
<div class="log-container" id="log-output">
<span class="text-slate-500">Lade Logs…</span>
</div>
<!-- Statistiken -->
<div class="mt-4 flex gap-6 text-xs text-slate-500" id="log-stats"></div>
</div>
<script>
let currentFile = 'app.log';
let allEntries = [];
let autoTimer = null;
async function loadLogs() {
const lines = document.getElementById('lines-select').value;
const box = document.getElementById('log-output');
try {
const r = await fetch(`/api/logs/${currentFile}?lines=${lines}`);
const d = await r.json();
allEntries = d.entries || [];
renderLogs();
updateStats();
} catch (e) {
box.innerHTML = `<span class="text-red-400">Fehler beim Laden: ${e.message}</span>`;
}
}
function renderLogs() {
const box = document.getElementById('log-output');
const text = document.getElementById('filter-input').value.toLowerCase();
const level = document.getElementById('level-filter').value;
const bottom = box.scrollTop + box.clientHeight >= box.scrollHeight - 20;
const filtered = allEntries.filter(e => {
if (level && e.level !== level) return false;
if (text && !e.text.toLowerCase().includes(text)) return false;
return true;
});
if (filtered.length === 0) {
box.innerHTML = '<span class="text-slate-500">Keine Einträge für diesen Filter.</span>';
return;
}
box.innerHTML = filtered.map(e =>
`<div class="log-line log-${e.level}">${escHtml(e.text)}</div>`
).join('');
if (bottom) box.scrollTop = box.scrollHeight;
}
function filterLogs() { renderLogs(); updateStats(); }
function updateStats() {
const errors = allEntries.filter(e => e.level === 'ERROR').length;
const warnings = allEntries.filter(e => e.level === 'WARNING').length;
const total = allEntries.length;
document.getElementById('log-stats').innerHTML =
`<span>${total} Einträge</span>` +
(errors ? `<span class="text-red-400">● ${errors} Fehler</span>` : '') +
(warnings ? `<span class="text-yellow-400">● ${warnings} Warnungen</span>` : '');
const badge = document.getElementById('error-badge');
if (errors > 0) {
badge.textContent = `${errors} Fehler`;
badge.classList.remove('hidden');
} else {
badge.classList.add('hidden');
}
}
function switchTab(file) {
currentFile = file;
document.querySelectorAll('.tab-btn').forEach(b => {
b.classList.replace('tab-active', 'tab-inactive');
});
const map = {'app.log': 'tab-app', 'bot.log': 'tab-bot', 'error.log': 'tab-error'};
document.getElementById(map[file]).classList.replace('tab-inactive', 'tab-active');
document.getElementById('filter-input').value = '';
document.getElementById('level-filter').value = '';
loadLogs();
}
function toggleAutoRefresh() {
if (document.getElementById('auto-refresh').checked) {
autoTimer = setInterval(loadLogs, 10000);
} else {
clearInterval(autoTimer);
autoTimer = null;
}
}
function escHtml(t) {
return t.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
// Frontend-Fehler abfangen und serverseitig loggen
window.onerror = function(msg, url, line) {
fetch('/api/log-error', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({message: msg, url, line})
});
};
loadLogs();
</script>
</body>
</html>