Add log viewer
This commit is contained in:
+5
-4
@@ -9,12 +9,12 @@ Tag="URBM Unraid Restic Backup Manager backup snapshots restore"
|
||||
<?php
|
||||
$pluginRoot = '/plugins/urbm';
|
||||
?>
|
||||
<link rel="stylesheet" href="<?= $pluginRoot ?>/assets/urbm.css?v=20260710r003">
|
||||
<link rel="stylesheet" href="<?= $pluginRoot ?>/assets/urbm.css?v=20260710r004">
|
||||
<div id="urbm-app" data-api="<?= $pluginRoot ?>/api.php">
|
||||
<header class="bu-header">
|
||||
<div class="bu-brand">
|
||||
<img class="bu-logo" src="<?= $pluginRoot ?>/images/urbm.png?v=20260710r003" alt="URBM-Logo">
|
||||
<div><h1>URBM <span class="bu-version">2026.07.10.r003</span></h1><p>Restic Backup Manager für Unraid</p></div>
|
||||
<img class="bu-logo" src="<?= $pluginRoot ?>/images/urbm.png?v=20260710r004" alt="URBM-Logo">
|
||||
<div><h1>URBM <span class="bu-version">2026.07.10.r004</span></h1><p>Restic Backup Manager für Unraid</p></div>
|
||||
</div>
|
||||
<div id="bu-health" class="bu-health" tabindex="0" data-tooltip="Zeigt, ob die URBM-Hintergrundkomponente erreichbar ist. Beispiel: 'Daemon online' bedeutet, dass Jobs gestartet werden können.">Verbindung wird hergestellt...</div>
|
||||
</header>
|
||||
@@ -25,6 +25,7 @@ $pluginRoot = '/plugins/urbm';
|
||||
<button data-view="snapshots" data-tooltip="Zeigt vorhandene Sicherungsstände und deren Dateien. Beispiel: Snapshot vom gestrigen Backup öffnen.">Snapshots</button>
|
||||
<button data-view="restore" data-tooltip="Stellt Dateien oder Ordner aus einem Snapshot wieder her. Standardmäßig in ein separates Staging-Verzeichnis.">Wiederherstellen</button>
|
||||
<button data-view="activity" data-tooltip="Zeigt wartende, laufende und abgeschlossene Backup-, Restore- und Wartungsaufgaben.">Aktivitäten</button>
|
||||
<button data-view="logs" data-tooltip="Zeigt das URBM-Daemon-Protokoll und gespeicherte Laufprotokolle zur Fehlersuche.">Protokolle</button>
|
||||
<button data-view="settings" data-tooltip="Globale Einstellungen sowie native Unraid- und Gotify-Benachrichtigungen konfigurieren.">Einstellungen</button>
|
||||
</nav>
|
||||
<main id="bu-content" aria-live="polite"></main>
|
||||
@@ -32,4 +33,4 @@ $pluginRoot = '/plugins/urbm';
|
||||
<div id="bu-tooltip" role="tooltip" aria-hidden="true"></div>
|
||||
</div>
|
||||
<script>window.URBM_CSRF = <?= json_encode($var['csrf_token'] ?? '') ?>;</script>
|
||||
<script src="<?= $pluginRoot ?>/assets/urbm-2026.07.10.r003.js"></script>
|
||||
<script src="<?= $pluginRoot ?>/assets/urbm-2026.07.10.r004.js"></script>
|
||||
|
||||
@@ -138,6 +138,7 @@
|
||||
.bu-live-log { background: #11161a; border: 1px solid var(--bu-border); border-radius: 6px; color: #e8edf1; margin: 2px 0 8px; padding: 9px 12px; }
|
||||
.bu-live-log summary { cursor: pointer; font-weight: 700; }
|
||||
.bu-live-log pre { color: #e8edf1; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; line-height: 1.5; margin: 10px 0 0; max-height: 320px; overflow: auto; white-space: pre-wrap; word-break: break-word; }
|
||||
.bu-log-pre { background: #11161a; border: 1px solid var(--bu-border); border-radius: 6px; color: #e8edf1; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; line-height: 1.5; margin: 10px 0 0; max-height: 520px; overflow: auto; padding: 12px; white-space: pre-wrap; word-break: break-word; }
|
||||
@keyframes bu-progress-scan { from { transform: translateX(-20%); } to { transform: translateX(250%); } }
|
||||
.bu-form-grid { display: grid; gap: 15px; grid-template-columns: repeat(2, minmax(220px, 1fr)); }
|
||||
.bu-managed-mount, .bu-external-mount { margin-top: 14px; }
|
||||
|
||||
+19
-3
@@ -8,7 +8,7 @@
|
||||
const tooltip = document.getElementById('bu-tooltip');
|
||||
const sourceRoots = ['/mnt/user','/mnt/disks','/mnt/remotes','/boot'].map(path=>({name:path,path}));
|
||||
const repositoryRoots = ['/mnt/user','/mnt/disks','/mnt/remotes'].map(path=>({name:path,path}));
|
||||
const state = { config: null, runs: [], repositoryStats: [], repositoryStatsLoading: false, view: 'dashboard', snapshots: [], selectedSnapshot: null, snapshotRepo: null, files: [], snapshotTree: null, snapshotTreeLoading: false, snapshotTreeError: '', snapshotTreeExpanded: {}, restoreIncludes: [], backupSelections: [], workloadSelections: [], workloads: [], workloadAvailable: true, workloadError: '', sourceTreeRoots: sourceRoots, sourceTreeChildren: {}, sourceTreeExpanded: {}, sourceTreeLoading: {}, sourceTreeErrors: {}, directoryTrees: {}, liveLogOpen: {} };
|
||||
const state = { config: null, runs: [], logs: null, logsLoading: false, logsError: '', repositoryStats: [], repositoryStatsLoading: false, view: 'dashboard', snapshots: [], selectedSnapshot: null, snapshotRepo: null, files: [], snapshotTree: null, snapshotTreeLoading: false, snapshotTreeError: '', snapshotTreeExpanded: {}, restoreIncludes: [], backupSelections: [], workloadSelections: [], workloads: [], workloadAvailable: true, workloadError: '', sourceTreeRoots: sourceRoots, sourceTreeChildren: {}, sourceTreeExpanded: {}, sourceTreeLoading: {}, sourceTreeErrors: {}, directoryTrees: {}, liveLogOpen: {} };
|
||||
|
||||
const fieldHelp = {
|
||||
name: 'Frei wählbarer Anzeigename. Beispiel: Appdata täglich oder VM Home Assistant.',
|
||||
@@ -72,6 +72,7 @@
|
||||
'submit-unraid-notify': 'Speichert native Unraid-Benachrichtigungen. Die Zustellung erfolgt über die in Unraid eingerichteten Notification Agents.',
|
||||
'submit-gotify-notify': 'Speichert den Gotify-Kanal und das App-Token verschlüsselt.',
|
||||
'refresh': 'Lädt Laufstatus und Historie erneut vom URBM-Daemon.',
|
||||
'refresh-logs': 'Lädt Daemon-Protokoll und gespeicherte Laufprotokolle erneut.',
|
||||
'clear-activity': 'Löscht alle abgeschlossenen Aktivitätseinträge und deren gespeicherte Laufprotokolle. Laufende und wartende Aufgaben bleiben erhalten.',
|
||||
'cancel-form': 'Verwirft ungespeicherte Eingaben und kehrt zur Übersicht zurück.',
|
||||
'toggle-source-tree': 'Klappt die direkten Unterordner dieses Ordners auf oder zu. Inhalte werden erst beim ersten Öffnen geladen.',
|
||||
@@ -359,7 +360,7 @@
|
||||
}
|
||||
|
||||
function render() {
|
||||
const views = {dashboard:renderDashboard, jobs:renderJobs, repositories:renderRepositories, snapshots:renderSnapshots, restore:renderRestore, activity:renderActivity, settings:renderSettings};
|
||||
const views = {dashboard:renderDashboard, jobs:renderJobs, repositories:renderRepositories, snapshots:renderSnapshots, restore:renderRestore, activity:renderActivity, logs:renderLogs, settings:renderSettings};
|
||||
content.innerHTML = views[state.view]();
|
||||
enhanceTooltips(content);
|
||||
content.querySelectorAll('.bu-file-select[data-partial="true"]').forEach(box=>{box.indeterminate=true;box.setAttribute('aria-checked','mixed');});
|
||||
@@ -373,6 +374,7 @@
|
||||
if(!state.directoryTrees.persistentLogDir) initDirectoryTree('persistentLogDir','Protokollverzeichnis auswählen','#bu-settings-form [name="persistentLogDir"]');
|
||||
else renderDirectoryTree('persistentLogDir');
|
||||
}
|
||||
if(state.view==='logs'&&!state.logs&&!state.logsLoading&&!state.logsError) loadLogs();
|
||||
}
|
||||
|
||||
function captureScrollState(root=document) {
|
||||
@@ -726,6 +728,13 @@
|
||||
}
|
||||
|
||||
function renderActivity() { return `<div class="bu-toolbar"><div><h2>Aktivitäten</h2><div class="bu-muted">Laufende Aufgaben werden automatisch alle zwei Sekunden aktualisiert.</div></div><div class="bu-actions">${button('Aktualisieren','refresh','primary')}${button('Abgeschlossene Aktivitäten löschen','clear-activity','danger')}</div></div><section class="bu-card">${runsTable(newestRuns(state.runs))}</section>`; }
|
||||
function renderLogs() {
|
||||
const daemon=state.logs?.daemon||{};
|
||||
const daemonLines=daemon.lines||[];
|
||||
const daemonHint=daemon.error?`<div class="bu-inline-error">${esc(daemon.error)}</div>`:`<div class="bu-muted">${esc(daemon.path||'/var/log/urbm.log')}${daemon.truncatedLines?` · ${daemon.truncatedLines} ältere Zeilen ausgeblendet`:''}${daemon.truncatedBytes?` · Anfang wegen Größe gekürzt`:''}</div>`;
|
||||
const runLogs=(state.logs?.runs||[]).map(run=>`<details class="bu-live-log" open><summary>${esc(taskLabels[run.taskType]||run.taskType)} · ${esc(runTarget(run))} · ${esc(statusLabels[run.status]||run.status)} · ${esc(new Date(run.createdAt).toLocaleString())}</summary><pre>${esc((run.liveLog||[]).join('\n'))}</pre></details>`).join('');
|
||||
return `<div class="bu-toolbar"><div><h2>Protokolle</h2><div class="bu-muted">Nur lesbare Diagnoseansicht. Secrets und Passwörter werden von URBM nicht geloggt.</div></div><div class="bu-actions">${button('Aktualisieren','refresh-logs','primary')}</div></div>${state.logsLoading?'<section class="bu-card"><div class="bu-empty">Protokolle werden geladen…</div></section>':state.logsError?`<section class="bu-card"><div class="bu-inline-error">${esc(state.logsError)}</div></section>`:`<section class="bu-card"><h3>Daemon-Protokoll</h3>${daemonHint}<pre class="bu-log-pre">${esc(daemonLines.join('\n'))}</pre></section><section class="bu-card" style="margin-top:16px"><h3>Laufprotokolle</h3>${runLogs||'<div class="bu-empty">Keine gespeicherten Laufprotokolle vorhanden.</div>'}</section>`}`;
|
||||
}
|
||||
function progressView(run) {
|
||||
if(run.status==='queued') return '<span class="bu-muted">Wartet in der Warteschlange</span>';
|
||||
if(!['backup','rsync','restore'].includes(run.taskType)&&!run.progressPercent) return '<span class="bu-muted">Kein Prozentwert verfügbar</span>';
|
||||
@@ -772,6 +781,12 @@
|
||||
notify('Konfiguration gespeichert');
|
||||
return state.config;
|
||||
}
|
||||
async function loadLogs() {
|
||||
state.logsLoading=true; state.logsError=''; render();
|
||||
try { state.logs=await api('/v1/logs'); }
|
||||
catch(error) { state.logsError=error.message; }
|
||||
finally { state.logsLoading=false; render(); }
|
||||
}
|
||||
function renderPreservingScroll() {
|
||||
const scrollSnapshot=captureScrollState(content);
|
||||
render();
|
||||
@@ -799,6 +814,7 @@
|
||||
if (name === 'pause-run') { await api(`/v1/runs/${a}/pause`,{method:'POST'}); notify('Vorgang pausiert'); return load(); }
|
||||
if (name === 'resume-run') { await api(`/v1/runs/${a}/resume`,{method:'POST'}); notify('Vorgang wird fortgesetzt'); return load(); }
|
||||
if (name === 'clear-activity') { if(confirm('Alle abgeschlossenen Aktivitäten und deren gespeicherte Laufprotokolle löschen?\n\nLaufende und wartende Aufgaben bleiben erhalten.')) { const result=await api('/v1/runs',{method:'DELETE'}); notify(result.cleared?`${result.cleared} abgeschlossene Aktivitäten gelöscht`:'Keine abgeschlossenen Aktivitäten vorhanden'); return load(); } return; }
|
||||
if (name === 'refresh-logs') { state.logs=null; return loadLogs(); }
|
||||
if (name === 'test-repo') { await api(`/v1/repositories/${a}/test`,{method:'POST'}); return notify('Repository-Verbindung erfolgreich getestet'); }
|
||||
if (name === 'init-repo') { if(confirm('Hier ein NEUES Restic-Repository erstellen?\n\nNur fortfahren, wenn dieses Ziel noch kein Restic-Repository enthält. Bei einem vorhandenen Repository das ursprüngliche Passwort unter Bearbeiten eintragen und Testen verwenden.')) { await api(`/v1/repositories/${a}/init`,{method:'POST'}); notify('Repository erfolgreich initialisiert'); } return; }
|
||||
if (name === 'unlock-repo') { if(confirm('Verwaiste Restic-Sperren dieses Repositorys entfernen?\n\nRestic entfernt dabei nur als veraltet erkannte Locks. Verwende diese Aktion nach einem Neustart oder abgebrochenen Restic-Prozess.')) { await api(`/v1/repositories/${a}/unlock`,{method:'POST'}); notify('Verwaiste Repository-Sperren wurden entfernt'); } return; }
|
||||
@@ -885,7 +901,7 @@
|
||||
notify(adopt?'Repository-Passwort wurde geprüft und auf diesem Server übernommen':'Repository-Passwort wurde sicher geändert'); render();
|
||||
}
|
||||
|
||||
document.querySelector('.bu-tabs').addEventListener('click', e => { const button=e.target.closest('[data-view]'); if(!button)return; state.view=button.dataset.view; document.querySelectorAll('.bu-tabs button').forEach(x=>x.classList.toggle('active',x===button)); render(); if(state.view==='dashboard')loadRepositoryStats(); });
|
||||
document.querySelector('.bu-tabs').addEventListener('click', e => { const button=e.target.closest('[data-view]'); if(!button)return; state.view=button.dataset.view; document.querySelectorAll('.bu-tabs button').forEach(x=>x.classList.toggle('active',x===button)); render(); if(state.view==='dashboard')loadRepositoryStats(); if(state.view==='logs'&&!state.logs&&!state.logsLoading)loadLogs(); });
|
||||
content.addEventListener('click', e => { const target=e.target.closest('[data-action]'); if(!target)return; e.preventDefault(); handle(target.dataset.action,target); });
|
||||
content.addEventListener('change', e => {
|
||||
if (e.target.name === 'type' && e.target.closest('#bu-repo-form')) {
|
||||
|
||||
Reference in New Issue
Block a user