Add log viewer
This commit is contained in:
@@ -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