Features:
- Import-Jobs: Persistierung in DB, Jobs beim Laden wiederherstellen
- Ordner loeschen: Button in Browser-Ansicht mit Modal-Dialog
- Serien konvertieren: Alle Episoden einer Serie in Queue senden
- Serien aufraumen: Alte Codec-Versionen nach Konvertierung loeschen
- Server-Log: Live-Ansicht in Admin mit Auto-Scroll
- Toast-Benachrichtigungen statt Browser-Alerts
- Bessere Fehlerbehandlung und Feedback
API:
- POST /api/library/delete-folder
- POST /api/library/series/{id}/convert
- GET /api/library/series/{id}/convert-status
- POST /api/library/series/{id}/cleanup
- GET /api/logs
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
154 lines
5.2 KiB
HTML
154 lines
5.2 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{% block title %}VideoKonverter{% endblock %}</title>
|
|
<link rel="stylesheet" href="/static/css/style.css">
|
|
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
|
{% block head %}{% endblock %}
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<div class="header-left">
|
|
<h1>VideoKonverter</h1>
|
|
</div>
|
|
<nav>
|
|
<a href="/" class="nav-link {% if request.path == '/' %}active{% endif %}">Dashboard</a>
|
|
<a href="/library" class="nav-link {% if request.path.startswith('/library') %}active{% endif %}">Bibliothek</a>
|
|
<a href="/admin" class="nav-link {% if request.path == '/admin' %}active{% endif %}">Einstellungen</a>
|
|
<a href="/statistics" class="nav-link {% if request.path == '/statistics' %}active{% endif %}">Statistik</a>
|
|
</nav>
|
|
</header>
|
|
|
|
<main>
|
|
{% block content %}{% endblock %}
|
|
</main>
|
|
|
|
<div id="toast-container"></div>
|
|
|
|
<!-- Benachrichtigungs-Glocke -->
|
|
<div id="notification-bell" class="notification-bell" onclick="toggleNotificationPanel()">
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/>
|
|
<path d="M13.73 21a2 2 0 0 1-3.46 0"/>
|
|
</svg>
|
|
<span id="notification-badge" class="notification-badge" style="display:none">0</span>
|
|
</div>
|
|
|
|
<!-- Log-Panel -->
|
|
<div id="notification-panel" class="notification-panel" style="display:none">
|
|
<div class="notification-header">
|
|
<span>Server-Log</span>
|
|
<div>
|
|
<button class="btn-small btn-secondary" onclick="clearNotifications()">Alle loeschen</button>
|
|
<button class="btn-close" onclick="toggleNotificationPanel()">×</button>
|
|
</div>
|
|
</div>
|
|
<div id="notification-list" class="notification-list">
|
|
<div class="notification-empty">Keine Nachrichten</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// === Benachrichtigungs-System ===
|
|
const notifications = [];
|
|
let unreadErrors = 0;
|
|
let lastLogId = 0;
|
|
|
|
function toggleNotificationPanel() {
|
|
const panel = document.getElementById("notification-panel");
|
|
const isOpen = panel.style.display !== "none";
|
|
panel.style.display = isOpen ? "none" : "flex";
|
|
|
|
if (!isOpen) {
|
|
// Panel geoeffnet - Fehler als gelesen markieren
|
|
unreadErrors = 0;
|
|
updateBadge();
|
|
}
|
|
}
|
|
|
|
function updateBadge() {
|
|
const badge = document.getElementById("notification-badge");
|
|
const bell = document.getElementById("notification-bell");
|
|
|
|
if (unreadErrors > 0) {
|
|
badge.textContent = unreadErrors > 99 ? "99+" : unreadErrors;
|
|
badge.style.display = "";
|
|
bell.classList.add("has-error");
|
|
} else {
|
|
badge.style.display = "none";
|
|
bell.classList.remove("has-error");
|
|
}
|
|
}
|
|
|
|
function addNotification(msg, level = "info") {
|
|
const time = new Date().toLocaleTimeString("de-DE", {hour: "2-digit", minute: "2-digit", second: "2-digit"});
|
|
|
|
notifications.unshift({msg, level, time});
|
|
if (notifications.length > 100) notifications.pop();
|
|
|
|
if (level === "error" || level === "ERROR") {
|
|
unreadErrors++;
|
|
updateBadge();
|
|
}
|
|
|
|
renderNotifications();
|
|
}
|
|
|
|
function renderNotifications() {
|
|
const list = document.getElementById("notification-list");
|
|
if (!notifications.length) {
|
|
list.innerHTML = '<div class="notification-empty">Keine Nachrichten</div>';
|
|
return;
|
|
}
|
|
|
|
list.innerHTML = notifications.map(n => {
|
|
const cls = n.level.toLowerCase() === "error" ? "notification-item error" :
|
|
n.level.toLowerCase() === "warning" ? "notification-item warning" :
|
|
"notification-item";
|
|
return `<div class="${cls}">
|
|
<span class="notification-time">${n.time}</span>
|
|
<span class="notification-msg">${escapeHtmlSimple(n.msg)}</span>
|
|
</div>`;
|
|
}).join("");
|
|
}
|
|
|
|
function clearNotifications() {
|
|
notifications.length = 0;
|
|
unreadErrors = 0;
|
|
updateBadge();
|
|
renderNotifications();
|
|
}
|
|
|
|
function escapeHtmlSimple(str) {
|
|
return String(str)
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">");
|
|
}
|
|
|
|
// Log-Polling vom Server
|
|
async function pollLogs() {
|
|
try {
|
|
const r = await fetch(`/api/logs?since=${lastLogId}`);
|
|
const data = await r.json();
|
|
|
|
if (data.logs && data.logs.length) {
|
|
for (const log of data.logs) {
|
|
addNotification(log.message, log.level);
|
|
if (log.id > lastLogId) lastLogId = log.id;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Ignorieren falls Endpoint nicht existiert
|
|
}
|
|
}
|
|
|
|
// Polling starten
|
|
setInterval(pollLogs, 2000);
|
|
</script>
|
|
|
|
{% block scripts %}{% endblock %}
|
|
</body>
|
|
</html>
|