From 175dface50150fcb464d8b6887c10a9c1d9ceb16 Mon Sep 17 00:00:00 2001 From: Steppenstreuner Date: Sun, 25 Oct 2026 20:09:06 +0100 Subject: [PATCH] feat(ui): add a system page with health checks, version and logs --- static/app.js | 60 ++++++++++++++++++++++++++++++++++++++++++++++- static/i18n.js | 28 ++++++++++++++++++++++ static/index.html | 25 ++++++++++++++++++++ static/style.css | 7 ++++++ 4 files changed, 119 insertions(+), 1 deletion(-) diff --git a/static/app.js b/static/app.js index 3dbe386..ead96b9 100644 --- a/static/app.js +++ b/static/app.js @@ -93,7 +93,7 @@ async function refreshMissingBadge() { // ---- navigation ---- // the view lives in the hash so reload, back button and links keep it -const VIEWS = ["search", "missing", "imported", "import", "activity", "settings"]; +const VIEWS = ["search", "missing", "imported", "import", "activity", "settings", "system"]; function showView() { const hash = location.hash.slice(1); @@ -102,6 +102,7 @@ function showView() { $$("main > section").forEach((s) => (s.hidden = s.id !== "view-" + name)); if (name === "missing" || name === "imported") loadRequests(name); if (name === "activity") loadHistory(); + if (name === "system") loadSystem(); if (name === "settings") loadLibraries().catch((err) => toast(err.message, true)); } @@ -117,6 +118,7 @@ window.addEventListener("hashchange", showView); // ---- libraries ---- async function loadLibraries() { libraries = await api("/api/libraries"); + refreshHealth(); const tbody = $("#lib-table tbody"); tbody.innerHTML = libraries .map( @@ -699,6 +701,59 @@ function renderHistory() { .join(""); } +// ---- system ---- +let healthIssues = []; +let logRows = []; + +// also called after library changes, so a fixed root path clears the badge +async function refreshHealth() { + try { + healthIssues = await api("/api/system/health"); + } catch { + return; + } + const badge = $("#health-badge"); + badge.textContent = healthIssues.length; + badge.hidden = !healthIssues.length; + badge.classList.toggle("error", healthIssues.some((i) => i.level === "error")); + badge.classList.toggle("warn", !healthIssues.some((i) => i.level === "error")); + renderHealth(); +} + +async function loadSystem() { + refreshHealth(); + try { + logRows = await api("/api/system/logs"); + } catch (err) { + toast(err.message, true); + } + renderLogs(); +} + +function renderHealth() { + $("#health-list").innerHTML = healthIssues.length + ? healthIssues.map((i) => `
  • ${esc(t("health." + i.code, i))}
  • `).join("") + : `
  • ${t("system.healthy")}
  • `; +} + +function renderLogs() { + const tbody = $("#log-table tbody"); + if (!logRows.length) { + tbody.innerHTML = `${t("common.nothingHere")}`; + return; + } + const locale = lang === "de" ? "de-DE" : "en-GB"; + const LEVEL_STATE = { WARNING: "warn", ERROR: "failed", CRITICAL: "failed" }; + tbody.innerHTML = logRows + .map((r) => ` + ${esc(new Date(r.at).toLocaleString(locale))} + ${esc(r.level)} + ${esc(r.logger)} + ${esc(r.message)} + `) + .join(""); +} + // ---- request detail dialog ---- let detailRequest = null; @@ -1905,6 +1960,8 @@ function onLangChange() { renderRequests("missing"); renderRequests("imported"); renderHistory(); + renderHealth(); + renderLogs(); if (libraries.length) loadLibraries(); const editId = $("#lib-form").dataset.editId; editId ? startLibraryEdit(parseInt(editId)) : endLibraryEdit(); @@ -1920,6 +1977,7 @@ async function showVersion() { // the version alone rarely moves, so the stamp is what tells deploys apart $("#app-version").textContent = `v${v.version} · ${v.build || v.built_at}`; $("#app-version").title = `${v.built_at} UTC${v.build ? " · " + v.build : ""}`; + $("#system-version").textContent = `v${v.version}${v.build ? " · " + v.build : ""} · ${v.built_at} UTC`; } catch {} } diff --git a/static/i18n.js b/static/i18n.js index 14bb8dc..98eaf4a 100644 --- a/static/i18n.js +++ b/static/i18n.js @@ -47,6 +47,20 @@ const STRINGS = { "nav.import": "Import", "nav.settings": "Libraries", "nav.activity": "Aktivität", + "nav.system": "System", + "system.health": "Zustand", + "system.healthy": "Alles in Ordnung.", + "system.version": "Version", + "system.logs": "Letzte Log-Einträge", + "system.level": "Stufe", + "system.source": "Quelle", + "system.message": "Meldung", + "health.download.missing": "Der Download-Ordner {path} existiert nicht.", + "health.download.readonly": "wordarr kann im Download-Ordner {path} nicht schreiben, Importe schlagen fehl.", + "health.library.missing": "Library „{name}“: Der Ordner {path} existiert nicht.", + "health.library.readonly": "Library „{name}“: wordarr kann in {path} nicht schreiben.", + "health.hardcover.token": "Kein Hardcover-Token gesetzt (WORDARR_HARDCOVER_TOKEN), die Ebook-Suche fällt auf Google Books und Open Library zurück.", + "health.abs.incomplete": "Audiobookshelf ist nur halb eingerichtet: WORDARR_ABS_URL und WORDARR_ABS_TOKEN müssen beide gesetzt sein.", "nav.langToggle": "Switch to English", "history.time": "Zeit", "history.event": "Ereignis", @@ -295,6 +309,20 @@ const STRINGS = { "nav.import": "Import", "nav.settings": "Libraries", "nav.activity": "Activity", + "nav.system": "System", + "system.health": "Health", + "system.healthy": "All good.", + "system.version": "Version", + "system.logs": "Recent log entries", + "system.level": "Level", + "system.source": "Source", + "system.message": "Message", + "health.download.missing": "The download folder {path} does not exist.", + "health.download.readonly": "wordarr cannot write to the download folder {path}, imports will fail.", + "health.library.missing": "Library “{name}”: the folder {path} does not exist.", + "health.library.readonly": "Library “{name}”: wordarr cannot write to {path}.", + "health.hardcover.token": "No Hardcover token set (WORDARR_HARDCOVER_TOKEN), ebook search falls back to Google Books and Open Library.", + "health.abs.incomplete": "Audiobookshelf is only half set up: WORDARR_ABS_URL and WORDARR_ABS_TOKEN must both be set.", "nav.langToggle": "Auf Deutsch umschalten", "history.time": "Time", "history.event": "Event", diff --git a/static/index.html b/static/index.html index b92f1ca..0209978 100644 --- a/static/index.html +++ b/static/index.html @@ -22,6 +22,10 @@ + @@ -299,6 +303,27 @@ + +