Add log viewer

This commit is contained in:
Mikei386
2026-07-10 12:51:35 +02:00
parent f22c2e462d
commit bf06b10519
10 changed files with 85 additions and 11 deletions
-1
View File
@@ -1 +0,0 @@
e08f4f2ee2a9589531051a519eceed485dde2d0b456e4e9bf160dd9796be2a8e urbm-2026.07.10.r003-x86_64-1.txz
+1
View File
@@ -0,0 +1 @@
70477362baaefac7a0f820a737a7af16efd107828f60a00ed5d9e02a9d2bb316 urbm-2026.07.10.r004-x86_64-1.txz
+6 -2
View File
@@ -2,13 +2,17 @@
<!DOCTYPE PLUGIN [ <!DOCTYPE PLUGIN [
<!ENTITY name "urbm"> <!ENTITY name "urbm">
<!ENTITY author "Michael Roll"> <!ENTITY author "Michael Roll">
<!ENTITY version "2026.07.10.r003"> <!ENTITY version "2026.07.10.r004">
<!ENTITY pluginURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm.plg"> <!ENTITY pluginURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm.plg">
<!ENTITY packageURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm-&version;-x86_64-1.txz"> <!ENTITY packageURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm-&version;-x86_64-1.txz">
<!ENTITY packageSHA256 "e08f4f2ee2a9589531051a519eceed485dde2d0b456e4e9bf160dd9796be2a8e"> <!ENTITY packageSHA256 "70477362baaefac7a0f820a737a7af16efd107828f60a00ed5d9e02a9d2bb316">
]> ]>
<PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="&pluginURL;" min="7.0.0" support="https://git.casaderoll.de/michael/URBM/issues" icon="urbm.png"> <PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="&pluginURL;" min="7.0.0" support="https://git.casaderoll.de/michael/URBM/issues" icon="urbm.png">
<CHANGES> <CHANGES>
### 2026.07.10.r004
- Add a dedicated Protokolle tab showing the daemon log tail and recent saved run logs.
- Add a read-only `/v1/logs` API endpoint limited to `/var/log/urbm.log` and stored URBM run logs.
### 2026.07.10.r003 ### 2026.07.10.r003
- Stream Restic snapshot file listings instead of buffering the full `restic ls --json` output in memory. - Stream Restic snapshot file listings instead of buffering the full `restic ls --json` output in memory.
- Extend snapshot file-listing timeouts across the daemon and PHP bridge to reduce generic 500 errors on large snapshots. - Extend snapshot file-listing timeouts across the daemon and PHP bridge to reduce generic 500 errors on large snapshots.
+2
View File
@@ -37,6 +37,7 @@ func New(socket string, svc *service.Service, log *slog.Logger) *Server {
mux.HandleFunc("DELETE /v1/secrets/{id}", s.deleteSecret) mux.HandleFunc("DELETE /v1/secrets/{id}", s.deleteSecret)
mux.HandleFunc("GET /v1/runs", s.runs) mux.HandleFunc("GET /v1/runs", s.runs)
mux.HandleFunc("DELETE /v1/runs", s.clearRuns) mux.HandleFunc("DELETE /v1/runs", s.clearRuns)
mux.HandleFunc("GET /v1/logs", s.logs)
mux.HandleFunc("GET /v1/filesystem/directories", s.directories) mux.HandleFunc("GET /v1/filesystem/directories", s.directories)
mux.HandleFunc("GET /v1/workloads/{kind}", s.workloads) mux.HandleFunc("GET /v1/workloads/{kind}", s.workloads)
mux.HandleFunc("POST /v1/jobs/{id}/run", s.runJob) mux.HandleFunc("POST /v1/jobs/{id}/run", s.runJob)
@@ -129,6 +130,7 @@ func (s *Server) runs(w http.ResponseWriter, _ *http.Request) { writeJSON(w, 200
func (s *Server) clearRuns(w http.ResponseWriter, _ *http.Request) { func (s *Server) clearRuns(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, 200, map[string]int{"cleared": s.service.ClearRunHistory()}) writeJSON(w, 200, map[string]int{"cleared": s.service.ClearRunHistory()})
} }
func (s *Server) logs(w http.ResponseWriter, _ *http.Request) { writeJSON(w, 200, s.service.Logs()) }
func (s *Server) directories(w http.ResponseWriter, r *http.Request) { func (s *Server) directories(w http.ResponseWriter, r *http.Request) {
items, err := s.service.BrowseDirectories(r.URL.Query().Get("path")) items, err := s.service.BrowseDirectories(r.URL.Query().Get("path"))
if err != nil { if err != nil {
+46
View File
@@ -1,6 +1,7 @@
package service package service
import ( import (
"bytes"
"context" "context"
"crypto/rand" "crypto/rand"
"encoding/hex" "encoding/hex"
@@ -65,6 +66,51 @@ func (s *Service) RunQueue(ctx context.Context) { s.queue.Run(ctx) }
func (s *Service) Stop() { s.queue.Stop() } func (s *Service) Stop() { s.queue.Stop() }
func (s *Service) Config() model.Config { s.mu.RLock(); defer s.mu.RUnlock(); return s.config } func (s *Service) Config() model.Config { s.mu.RLock(); defer s.mu.RUnlock(); return s.config }
func (s *Service) Runs() []model.Run { return s.queue.Snapshot() } func (s *Service) Runs() []model.Run { return s.queue.Snapshot() }
func (s *Service) Logs() map[string]any {
return map[string]any{
"daemon": tailFile("/var/log/urbm.log", 512*1024, 300),
"runs": runsWithLogs(s.queue.Snapshot(), 25),
}
}
func runsWithLogs(runs []model.Run, limit int) []model.Run {
result := make([]model.Run, 0, limit)
for index := len(runs) - 1; index >= 0 && len(result) < limit; index-- {
if len(runs[index].LiveLog) == 0 {
continue
}
result = append(result, runs[index])
}
return result
}
func tailFile(path string, maxBytes int64, maxLines int) map[string]any {
data, err := os.ReadFile(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return map[string]any{"path": path, "lines": []string{}, "error": "Logdatei ist noch nicht vorhanden"}
}
return map[string]any{"path": path, "lines": []string{}, "error": err.Error()}
}
truncatedBytes := 0
if int64(len(data)) > maxBytes {
truncatedBytes = len(data) - int(maxBytes)
data = data[len(data)-int(maxBytes):]
if index := bytes.IndexByte(data, '\n'); index >= 0 && index+1 < len(data) {
data = data[index+1:]
}
}
rawLines := strings.Split(strings.TrimRight(string(data), "\n"), "\n")
if len(rawLines) == 1 && rawLines[0] == "" {
rawLines = []string{}
}
truncatedLines := 0
if len(rawLines) > maxLines {
truncatedLines = len(rawLines) - maxLines
rawLines = rawLines[len(rawLines)-maxLines:]
}
return map[string]any{"path": path, "lines": rawLines, "truncatedBytes": truncatedBytes, "truncatedLines": truncatedLines}
}
func (s *Service) BrowseDirectories(path string) ([]platform.DirectoryEntry, error) { func (s *Service) BrowseDirectories(path string) ([]platform.DirectoryEntry, error) {
if path == "" || path == "/" { if path == "" || path == "/" {
+5 -1
View File
@@ -2,13 +2,17 @@
<!DOCTYPE PLUGIN [ <!DOCTYPE PLUGIN [
<!ENTITY name "urbm"> <!ENTITY name "urbm">
<!ENTITY author "Michael Roll"> <!ENTITY author "Michael Roll">
<!ENTITY version "2026.07.10.r003"> <!ENTITY version "2026.07.10.r004">
<!ENTITY pluginURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm.plg"> <!ENTITY pluginURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm.plg">
<!ENTITY packageURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm-&version;-x86_64-1.txz"> <!ENTITY packageURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm-&version;-x86_64-1.txz">
<!ENTITY packageSHA256 "REPLACE_DURING_RELEASE"> <!ENTITY packageSHA256 "REPLACE_DURING_RELEASE">
]> ]>
<PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="&pluginURL;" min="7.0.0" support="https://git.casaderoll.de/michael/URBM/issues" icon="urbm.png"> <PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="&pluginURL;" min="7.0.0" support="https://git.casaderoll.de/michael/URBM/issues" icon="urbm.png">
<CHANGES> <CHANGES>
### 2026.07.10.r004
- Add a dedicated Protokolle tab showing the daemon log tail and recent saved run logs.
- Add a read-only `/v1/logs` API endpoint limited to `/var/log/urbm.log` and stored URBM run logs.
### 2026.07.10.r003 ### 2026.07.10.r003
- Stream Restic snapshot file listings instead of buffering the full `restic ls --json` output in memory. - Stream Restic snapshot file listings instead of buffering the full `restic ls --json` output in memory.
- Extend snapshot file-listing timeouts across the daemon and PHP bridge to reduce generic 500 errors on large snapshots. - Extend snapshot file-listing timeouts across the daemon and PHP bridge to reduce generic 500 errors on large snapshots.
+5 -4
View File
@@ -9,12 +9,12 @@ Tag="URBM Unraid Restic Backup Manager backup snapshots restore"
<?php <?php
$pluginRoot = '/plugins/urbm'; $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"> <div id="urbm-app" data-api="<?= $pluginRoot ?>/api.php">
<header class="bu-header"> <header class="bu-header">
<div class="bu-brand"> <div class="bu-brand">
<img class="bu-logo" src="<?= $pluginRoot ?>/images/urbm.png?v=20260710r003" alt="URBM-Logo"> <img class="bu-logo" src="<?= $pluginRoot ?>/images/urbm.png?v=20260710r004" 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> <div><h1>URBM <span class="bu-version">2026.07.10.r004</span></h1><p>Restic Backup Manager für Unraid</p></div>
</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> <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> </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="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="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="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> <button data-view="settings" data-tooltip="Globale Einstellungen sowie native Unraid- und Gotify-Benachrichtigungen konfigurieren.">Einstellungen</button>
</nav> </nav>
<main id="bu-content" aria-live="polite"></main> <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 id="bu-tooltip" role="tooltip" aria-hidden="true"></div>
</div> </div>
<script>window.URBM_CSRF = <?= json_encode($var['csrf_token'] ?? '') ?>;</script> <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>
+1
View File
@@ -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 { 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 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-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%); } } @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-form-grid { display: grid; gap: 15px; grid-template-columns: repeat(2, minmax(220px, 1fr)); }
.bu-managed-mount, .bu-external-mount { margin-top: 14px; } .bu-managed-mount, .bu-external-mount { margin-top: 14px; }
+19 -3
View File
@@ -8,7 +8,7 @@
const tooltip = document.getElementById('bu-tooltip'); const tooltip = document.getElementById('bu-tooltip');
const sourceRoots = ['/mnt/user','/mnt/disks','/mnt/remotes','/boot'].map(path=>({name:path,path})); 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 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 = { const fieldHelp = {
name: 'Frei wählbarer Anzeigename. Beispiel: Appdata täglich oder VM Home Assistant.', 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-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.', 'submit-gotify-notify': 'Speichert den Gotify-Kanal und das App-Token verschlüsselt.',
'refresh': 'Lädt Laufstatus und Historie erneut vom URBM-Daemon.', '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.', '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.', '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.', '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() { 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](); content.innerHTML = views[state.view]();
enhanceTooltips(content); enhanceTooltips(content);
content.querySelectorAll('.bu-file-select[data-partial="true"]').forEach(box=>{box.indeterminate=true;box.setAttribute('aria-checked','mixed');}); 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"]'); if(!state.directoryTrees.persistentLogDir) initDirectoryTree('persistentLogDir','Protokollverzeichnis auswählen','#bu-settings-form [name="persistentLogDir"]');
else renderDirectoryTree('persistentLogDir'); else renderDirectoryTree('persistentLogDir');
} }
if(state.view==='logs'&&!state.logs&&!state.logsLoading&&!state.logsError) loadLogs();
} }
function captureScrollState(root=document) { 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 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) { function progressView(run) {
if(run.status==='queued') return '<span class="bu-muted">Wartet in der Warteschlange</span>'; 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>'; 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'); notify('Konfiguration gespeichert');
return state.config; 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() { function renderPreservingScroll() {
const scrollSnapshot=captureScrollState(content); const scrollSnapshot=captureScrollState(content);
render(); render();
@@ -799,6 +814,7 @@
if (name === 'pause-run') { await api(`/v1/runs/${a}/pause`,{method:'POST'}); notify('Vorgang pausiert'); return load(); } 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 === '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 === '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 === '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 === '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; } 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(); 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('click', e => { const target=e.target.closest('[data-action]'); if(!target)return; e.preventDefault(); handle(target.dataset.action,target); });
content.addEventListener('change', e => { content.addEventListener('change', e => {
if (e.target.name === 'type' && e.target.closest('#bu-repo-form')) { if (e.target.name === 'type' && e.target.closest('#bu-repo-form')) {