diff --git a/dist/urbm-2026.07.10.r004-x86_64-1.txz.sha256 b/dist/urbm-2026.07.10.r004-x86_64-1.txz.sha256 deleted file mode 100644 index d1f9f2d..0000000 --- a/dist/urbm-2026.07.10.r004-x86_64-1.txz.sha256 +++ /dev/null @@ -1 +0,0 @@ -70477362baaefac7a0f820a737a7af16efd107828f60a00ed5d9e02a9d2bb316 urbm-2026.07.10.r004-x86_64-1.txz diff --git a/dist/urbm-2026.07.10.r004-x86_64-1.txz b/dist/urbm-2026.07.10.r005-x86_64-1.txz similarity index 56% rename from dist/urbm-2026.07.10.r004-x86_64-1.txz rename to dist/urbm-2026.07.10.r005-x86_64-1.txz index d3a6cb4..8aa22ef 100644 Binary files a/dist/urbm-2026.07.10.r004-x86_64-1.txz and b/dist/urbm-2026.07.10.r005-x86_64-1.txz differ diff --git a/dist/urbm-2026.07.10.r005-x86_64-1.txz.sha256 b/dist/urbm-2026.07.10.r005-x86_64-1.txz.sha256 new file mode 100644 index 0000000..c492a9c --- /dev/null +++ b/dist/urbm-2026.07.10.r005-x86_64-1.txz.sha256 @@ -0,0 +1 @@ +9c7e39a06a10ae9557796457e9ed754e9e0dcf65f12be39561276d9fce6cbc0b urbm-2026.07.10.r005-x86_64-1.txz diff --git a/dist/urbm.plg b/dist/urbm.plg index 1613c6a..5b266c7 100644 --- a/dist/urbm.plg +++ b/dist/urbm.plg @@ -2,13 +2,17 @@ - + - + ]> +### 2026.07.10.r005 +- Log snapshot file-listing failures from the daemon and PHP bridge to `/var/log/urbm.log` so they appear in the Protokolle tab. +- Increase the daemon HTTP write timeout for large snapshot trees and return concrete backend errors instead of generic 500 responses where possible. + ### 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. diff --git a/internal/api/server.go b/internal/api/server.go index 1f4209d..d3d53f8 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -26,9 +26,11 @@ type Server struct { } var Version = "dev" +var apiErrorLog *slog.Logger func New(socket string, svc *service.Service, log *slog.Logger) *Server { s := &Server{service: svc, log: log, socket: socket} + apiErrorLog = log mux := http.NewServeMux() mux.HandleFunc("GET /v1/health", s.health) mux.HandleFunc("GET /v1/config", s.getConfig) @@ -55,7 +57,7 @@ func New(socket string, svc *service.Service, log *slog.Logger) *Server { mux.HandleFunc("GET /v1/repositories/{id}/snapshots", s.snapshots) mux.HandleFunc("GET /v1/repositories/{id}/snapshots/{snapshot}/files", s.snapshotFiles) mux.HandleFunc("POST /v1/restores", s.restore) - s.http = &http.Server{Handler: requestLog(log, mux), ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 30 * time.Second, WriteTimeout: 5 * time.Minute, IdleTimeout: 30 * time.Second} + s.http = &http.Server{Handler: requestLog(log, mux), ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 30 * time.Second, WriteTimeout: 25 * time.Minute, IdleTimeout: 30 * time.Second} return s } @@ -321,6 +323,9 @@ func writeError(w http.ResponseWriter, err error) { if strings.Contains(message, "already queued") { status = 409 } + if apiErrorLog != nil { + apiErrorLog.Warn("api error", "status", status, "error", message) + } writeJSON(w, status, map[string]string{"error": message}) } diff --git a/internal/service/service.go b/internal/service/service.go index 5c26846..ff7d832 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -385,16 +385,24 @@ func (s *Service) RepositoryStats(ctx context.Context) []model.RepositoryStats { } func (s *Service) SnapshotFiles(ctx context.Context, repoID, snapshot, path string) ([]map[string]any, error) { + started := time.Now() repo, ok := s.repository(repoID) if !ok { return nil, errors.New("validation: unknown repository") } mounted, err := s.mounts.Prepare(ctx, repo) if err != nil { + s.log.Warn("snapshot file listing failed", "repoID", repoID, "snapshot", snapshot, "path", path, "durationMs", time.Since(started).Milliseconds(), "error", err) return nil, err } defer s.mounts.Cleanup(context.Background(), mounted) - return s.restic.List(ctx, mounted.Repository, snapshot, path) + items, err := s.restic.List(ctx, mounted.Repository, snapshot, path) + if err != nil { + s.log.Warn("snapshot file listing failed", "repoID", repoID, "snapshot", snapshot, "path", path, "durationMs", time.Since(started).Milliseconds(), "error", err) + return nil, err + } + s.log.Info("snapshot file listing loaded", "repoID", repoID, "snapshot", snapshot, "path", path, "items", len(items), "durationMs", time.Since(started).Milliseconds()) + return items, nil } func (s *Service) TestRepository(ctx context.Context, repoID string, initialize bool) error { diff --git a/plugin/urbm.plg b/plugin/urbm.plg index ddbb988..ed48489 100644 --- a/plugin/urbm.plg +++ b/plugin/urbm.plg @@ -2,13 +2,17 @@ - + ]> +### 2026.07.10.r005 +- Log snapshot file-listing failures from the daemon and PHP bridge to `/var/log/urbm.log` so they appear in the Protokolle tab. +- Increase the daemon HTTP write timeout for large snapshot trees and return concrete backend errors instead of generic 500 responses where possible. + ### 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. diff --git a/webgui/URBM.page b/webgui/URBM.page index dcfdd88..3bad3f1 100644 --- a/webgui/URBM.page +++ b/webgui/URBM.page @@ -14,7 +14,7 @@ $pluginRoot = '/plugins/urbm';
-

URBM 2026.07.10.r004

Restic Backup Manager für Unraid

+

URBM 2026.07.10.r005

Restic Backup Manager für Unraid

Verbindung wird hergestellt...
@@ -33,4 +33,4 @@ $pluginRoot = '/plugins/urbm'; - + diff --git a/webgui/api.php b/webgui/api.php index 8ab1b72..6969aba 100644 --- a/webgui/api.php +++ b/webgui/api.php @@ -3,21 +3,37 @@ declare(strict_types=1); header('Content-Type: application/json'); +function urbm_bridge_log(string $level, string $message, array $context = []): void +{ + $entry = array_merge([ + 'time' => gmdate('c'), + 'level' => $level, + 'component' => 'webgui', + 'message' => $message, + ], $context); + @file_put_contents('/var/log/urbm.log', json_encode($entry, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . PHP_EOL, FILE_APPEND | LOCK_EX); +} + +function urbm_json_error(int $status, string $message): void +{ + http_response_code($status); + echo json_encode(['error' => $message], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + exit; +} + $socket = '/run/urbm/urbm.sock'; $method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; $path = $_GET['path'] ?? '/v1/health'; $isSnapshotFileRequest = preg_match('#/snapshots/[^/]+/files(?:\?|$)#', $path) === 1; if (!preg_match('#^/v1/[A-Za-z0-9_./?=&%:-]*$#', $path) || str_contains($path, '..')) { - http_response_code(400); - echo json_encode(['error' => 'invalid API path']); - exit; + urbm_bridge_log('warn', 'invalid API path', ['path' => $path]); + urbm_json_error(400, 'invalid API path'); } if (!file_exists($socket)) { - http_response_code(503); - echo json_encode(['error' => 'Der URBM-Daemon läuft nicht']); - exit; + urbm_bridge_log('warn', 'daemon socket missing', ['path' => $path, 'socket' => $socket]); + urbm_json_error(503, 'Der URBM-Daemon läuft nicht'); } $curl = curl_init(); @@ -42,10 +58,30 @@ $error = curl_error($curl); curl_close($curl); if ($response === false) { - http_response_code(502); - echo json_encode(['error' => 'Verbindung zum URBM-Daemon fehlgeschlagen: ' . $error]); - exit; + $message = 'Verbindung zum URBM-Daemon fehlgeschlagen: ' . $error; + urbm_bridge_log('warn', 'daemon request failed', ['path' => $path, 'status' => 502, 'error' => $message]); + urbm_json_error(502, $message); } -http_response_code($status > 0 ? $status : 502); +if ($status <= 0) { + $message = 'URBM-Daemon lieferte keine HTTP-Antwort'; + urbm_bridge_log('warn', 'daemon response missing', ['path' => $path, 'status' => 502, 'error' => $message]); + urbm_json_error(502, $message); +} + +if ($status >= 400) { + $decoded = json_decode((string)$response, true); + if (is_array($decoded) && isset($decoded['error']) && is_string($decoded['error'])) { + $message = $decoded['error']; + } else { + $message = trim((string)$response); + } + if ($message === '') { + $message = 'URBM-Daemon lieferte HTTP ' . $status . ' ohne Fehlertext'; + } + urbm_bridge_log('warn', 'api request failed', ['path' => $path, 'status' => $status, 'error' => $message]); + urbm_json_error($status, $message); +} + +http_response_code($status); echo $response; diff --git a/webgui/assets/urbm.js b/webgui/assets/urbm.js index 804c235..14d1657 100644 --- a/webgui/assets/urbm.js +++ b/webgui/assets/urbm.js @@ -822,7 +822,7 @@ if (name === 'maint') { await api(`/v1/repositories/${b}/${a}`,{method:'POST'}); notify(`${a==='check'?'Prüfung':'Bereinigung'} wurde eingereiht`); return load(); } if (name === 'repo-snapshots') { state.view='snapshots'; document.querySelector('[data-view="snapshots"]').click(); render(); setTimeout(()=>{document.getElementById('bu-snapshot-repo').value=a; handle('load-snapshots');},0); return; } if (name === 'load-snapshots') { const repo=document.getElementById('bu-snapshot-repo').value; state.snapshots=newestSnapshots(await api(`/v1/repositories/${repo}/snapshots`)); state.snapshotRepo=repo; return render(); } - if (name === 'browse') { state.selectedSnapshot=a; state.restoreIncludes=[]; state.files=[]; state.snapshotTree=null; state.snapshotTreeError=''; state.snapshotTreeExpanded={'/':true}; state.snapshotTreeLoading=true; render(); try { state.files=await api(`/v1/repositories/${state.snapshotRepo}/snapshots/${a}/files`); state.snapshotTree=buildSnapshotTree(state.files); } catch(error) { state.snapshotTreeError=`Snapshot-Struktur konnte nicht geladen werden: ${error.message}`; } finally { state.snapshotTreeLoading=false; return render(); } } + if (name === 'browse') { state.selectedSnapshot=a; state.restoreIncludes=[]; state.files=[]; state.snapshotTree=null; state.snapshotTreeError=''; state.snapshotTreeExpanded={'/':true}; state.snapshotTreeLoading=true; render(); try { state.files=await api(`/v1/repositories/${state.snapshotRepo}/snapshots/${a}/files`); state.snapshotTree=buildSnapshotTree(state.files); } catch(error) { state.snapshotTreeError=`Snapshot-Struktur konnte nicht geladen werden: ${error.message}. Details stehen im Tab Protokolle.`; } finally { state.snapshotTreeLoading=false; return render(); } } if (name === 'toggle-source-tree') { return toggleSourceTree(decodeURIComponent(a)); } if (name === 'retry-source-tree') { const path=decodeURIComponent(a); delete state.sourceTreeChildren[path]; state.sourceTreeErrors[path]=''; return toggleSourceTree(path,true); } if (name === 'toggle-dir-tree') { return toggleDirectoryTree(a, decodeURIComponent(b)); }