- 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
211 lines
7.4 KiB
HTML
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,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
}
|
|
|
|
// 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>
|