This commit is contained in:
Mateusz Gruszczyński committed 2026-11-07 08:16:11 +01:00
1 parent c038ad44b3
commit cd3c28d390
33 files changed
+807 -221

No files matched your search

+69 -5
View File
@@ -44,7 +44,7 @@ async function loadLanguages() {
$("language").replaceChildren(automatic, ...packs.map(pack => { const option = el("option", pack.meta.native_name); option.value = pack.id; return option; }));
$("language").value = languagePreference === "auto" ? "auto" : language;
}
let latestStatus = null, certificateInventory = new Map();
let latestStatus = null, certificateInventory = new Map(), appHealthUpdatedAt = null;
let selectedCacheMemoryPreset = null, selectedCacheColdPreset = null, selectedTransformPreset = null;
function translate() {
clearTimeout(toastTimer); hideToast();
@@ -179,6 +179,68 @@ function uptime(value) {
}
function rateBytes(value) { value = Math.max(0, Number(value) || 0); if (value === 0) return "0.0 KiB"; value /= 1024; for (const unit of ["KiB", "MiB", "GiB", "TiB"]) { if (value < 1024 || unit === "TiB") { if (unit === "KiB" && value < 0.1) return "<0.1 KiB"; const digits = value < 10 ? 1 : 0; return `${Number(value.toFixed(digits))} ${unit}`; } value /= 1024; } }
function el(tag, text, cls) { const e = document.createElement(tag); if (text !== undefined) e.textContent = text; if (cls) e.className = cls; return e; }
function appHealthChecks(status) {
if (!connected || !status) return [{ level: "error", label: t("appHealthApi"), detail: t("appHealthApiError") }];
const checks = [{ level: "ok", label: t("appHealthApi"), detail: t("appHealthApiOk") }];
if (status.listeners?.restart_required) {
checks.push({ level: "warning", label: t("appHealthListeners"), detail: t("appHealthListenersRestart") });
} else {
checks.push({ level: "ok", label: t("appHealthListeners"), detail: t("appHealthListenersOk") });
}
const pools = Array.isArray(status.backend_pools) ? status.backend_pools : [];
const members = pools.flatMap(pool => (pool.members || []).map(member => ({ pool: pool.id, ...member })));
const down = members.filter(member => member.status === "down");
const checking = members.filter(member => member.status === "checking");
const unavailablePools = pools.filter(pool => pool.ready === false && (pool.members || []).some(member => ["up", "down", "checking"].includes(member.status))).map(pool => pool.id).filter(Boolean);
const affected = [...down, ...checking].slice(0, 5).map(member => `${member.pool}/${member.id}`).join(", ");
if (unavailablePools.length) {
let detail = t("appHealthBackendsError").replace("{pools}", unavailablePools.slice(0, 5).join(", ") + (unavailablePools.length > 5 ? " …" : ""));
if (affected) detail += ` ${t("appHealthBackendsAffected").replace("{members}", affected + (down.length + checking.length > 5 ? " …" : ""))}`;
checks.push({ level: "error", label: t("appHealthBackends"), detail });
} else if (down.length || checking.length) {
checks.push({ level: "warning", label: t("appHealthBackends"), detail: t("appHealthBackendsWarning").replace("{down}", String(down.length)).replace("{checking}", String(checking.length)).replace("{members}", affected + (down.length + checking.length > 5 ? " …" : "")) });
} else {
checks.push({ level: "ok", label: t("appHealthBackends"), detail: pools.length ? t("appHealthBackendsOk") : t("appHealthBackendsNone") });
}
const sink = status.logging?.sinks || {}, errorFile = status.logging?.error_file || {};
const loggingDropped = (Number(sink.queue_dropped) || 0) + (Number(errorFile.queue_dropped) || 0);
const loggingErrors = (Number(sink.errors) || 0) + (Number(errorFile.errors) || 0);
if (loggingDropped || loggingErrors) {
checks.push({ level: "warning", label: t("appHealthLogging"), detail: t("appHealthLoggingWarning").replace("{dropped}", String(loggingDropped)).replace("{errors}", String(loggingErrors)) });
} else {
checks.push({ level: "ok", label: t("appHealthLogging"), detail: t("appHealthLoggingOk") });
}
return checks;
}
function appHealthLevel(checks) {
if (!checks.length) return "checking";
if (checks.some(check => check.level === "error")) return "error";
if (checks.some(check => check.level === "warning")) return "warning";
return "ok";
}
function setAppHealthClass(node, level) {
if (!node) return;
node.classList.remove("is-checking", "is-ok", "is-warning", "is-error");
node.classList.add(`is-${level}`);
}
function renderAppHealth(status = latestStatus) {
const checks = appHealthChecks(status), level = appHealthLevel(checks);
const labelKeys = { checking: "appHealthSidebarChecking", ok: "appHealthSidebarOk", warning: "appHealthSidebarWarning", error: "appHealthSidebarError" };
const summaryKeys = { checking: "appHealthChecking", ok: "appHealthOk", warning: "appHealthWarning", error: "appHealthError" };
const button = $("app-health-open"), label = $("app-health-label"), summaryRow = $("app-health-summary-row"), summary = $("app-health-summary");
setAppHealthClass(button, level); setAppHealthClass(summaryRow, level);
if (label) { label.dataset.i18n = labelKeys[level]; label.textContent = t(labelKeys[level]); }
if (summary) { summary.dataset.i18n = summaryKeys[level]; summary.textContent = t(summaryKeys[level]); }
const updated = $("app-health-updated");
if (updated) updated.textContent = appHealthUpdatedAt ? t("appHealthUpdated").replace("{time}", appHealthUpdatedAt.toLocaleTimeString(locale())) : t("appHealthNoStatus");
const list = $("app-health-checks");
if (list) list.replaceChildren(...checks.map(check => {
const row = el("div", undefined, `app-health-check is-${check.level}`), dot = el("i"), copy = el("div");
dot.setAttribute("aria-hidden", "true"); copy.append(el("strong", check.label), el("p", check.detail, "fine")); row.append(dot, copy); return row;
}));
}
function enhanceEnabledControl(input, hintKey = "entityEnableHint") {
const label = input?.closest("label"); if (!label) return () => { };
label.classList.add("vh-enable-control", "vh-wide");
@@ -257,7 +319,7 @@ function renderStatusNow(s, systemSample = false) {
logging: s.logging ? { ...(latestStatus?.logging || {}), ...s.logging } : latestStatus?.logging
};
s = latestStatus; window.rustProxySettings?.render(); if (firstStatus) renderSettings();
connected = true; $("connection-label").textContent = t("connected"); $("signal").classList.remove("status-error"); const m = s.metrics;
connected = true; $("connection-label").textContent = t("connected"); $("signal").classList.remove("status-error"); $("app-health-slot").hidden = false; appHealthUpdatedAt = new Date(); renderAppHealth(s); const m = s.metrics;
$("metric-ws").textContent = m.websocket_active || 0;
$("metric-ws-h1").textContent = m.websocket_h1_active || 0;
$("metric-ws-h2").textContent = m.websocket_h2_active || 0;
@@ -299,7 +361,7 @@ function renderStatusNow(s, systemSample = false) {
function renderFooter(s) {
const apiLatency = lastApiLatencyMs == null ? "API -" : `API ${lastApiLatencyMs.toFixed(lastApiLatencyMs < 10 ? 1 : 0)} ms`;
const footer = $("footer-status");
footer.replaceChildren(document.createTextNode(`${apiLatency} / ${t("logsDropped")}: ${s.logs_dropped}`));
footer.replaceChildren(document.createTextNode(apiLatency));
const update = s.software_update;
if (update?.update_available && update.latest_version) {
footer.append(document.createTextNode(" / "));
@@ -421,12 +483,12 @@ function lock(notify = true) {
window.rustProxyInlineLayout?.cancel?.();
window.rustProxyRefresh?.stop();
const destination = pendingAuthDestination || authReturnDestination(), navigating = typeof location !== "undefined" && (location.protocol === "http:" || location.protocol === "https:");
pendingAuthDestination = destination; if (notify) adminAuthChannel?.postMessage("locked"); $("api-docs").hidden = true; $("logs-menu").hidden = true; resetAdminPreferences(); window.rustProxyCertificates?.reset(); window.rustProxySettings?.reset(); window.rustProxyTrafficPanel?.reset(); window.rustProxyCachePanel?.reset(); window.rustProxyTransformPanel?.reset(); window.rustProxyAcme?.reset(); window.rustProxyIntegrations?.reset(); window.rustProxyLogs?.reset(); window.rustProxyFileLogs?.reset(); window.rustProxyDrafts?.reset(); $("logs-open").hidden = true; $("file-logs-open").hidden = true; window.rustProxyAclProfiles?.reset(); window.rustProxyVhosts?.reset(); window.rustProxyWebServer?.reset(); window.rustProxyDocker?.reset(); window.rustProxyRedirects?.reset(); window.rustProxyNotFound?.reset(); window.rustProxyStreams?.reset(); window.rustProxyStreamDashboard?.reset(); window.rustProxyBackendDashboard?.reset(); window.rustProxyExtraOverview?.reset(); window.rustProxyWideOverview?.reset(); window.rustProxyWebOverview?.reset(); authEpoch++; stopLive(); csrf = ""; authenticated = false; sessionInfo = null; envelope = null; configStorage = null; certificateInventory = new Map(); previous = null; samples = []; protocolSamples = { h1: [], h2: [], h3: [] }; websocketProtocolSamples = { h1: [], h2: [], h3: [] }; transformSamples = []; systemSamples = []; connectionOpenSamples = []; connectionExchangeSamples = []; networkInterfaceSamples.clear(); lastLogs = null; latestStatus = null; clearSettingsDirty(); connected = false; busy = false; $("workspace").hidden = true; $("login").hidden = navigating; $("logout").hidden = true; $("password").value = ""; $("password-confirm").value = ""; $("setup-token").value = ""; $("password-form").reset(); $("config-editor").value = ""; $("logs").replaceChildren(); $("audit-rows").replaceChildren(); authMessage("");
pendingAuthDestination = destination; if (notify) adminAuthChannel?.postMessage("locked"); $("api-docs").hidden = true; $("logs-menu").hidden = true; $("app-health-slot").hidden = true; resetAdminPreferences(); window.rustProxyCertificates?.reset(); window.rustProxySettings?.reset(); window.rustProxyTrafficPanel?.reset(); window.rustProxyCachePanel?.reset(); window.rustProxyTransformPanel?.reset(); window.rustProxyAcme?.reset(); window.rustProxyIntegrations?.reset(); window.rustProxyLogs?.reset(); window.rustProxyFileLogs?.reset(); window.rustProxyDrafts?.reset(); $("logs-open").hidden = true; $("file-logs-open").hidden = true; window.rustProxyAclProfiles?.reset(); window.rustProxyVhosts?.reset(); window.rustProxyWebServer?.reset(); window.rustProxyDocker?.reset(); window.rustProxyRedirects?.reset(); window.rustProxyNotFound?.reset(); window.rustProxyStreams?.reset(); window.rustProxyStreamDashboard?.reset(); window.rustProxyBackendDashboard?.reset(); window.rustProxyExtraOverview?.reset(); window.rustProxyWideOverview?.reset(); window.rustProxyWebOverview?.reset(); authEpoch++; stopLive(); csrf = ""; authenticated = false; sessionInfo = null; envelope = null; configStorage = null; certificateInventory = new Map(); previous = null; samples = []; protocolSamples = { h1: [], h2: [], h3: [] }; websocketProtocolSamples = { h1: [], h2: [], h3: [] }; transformSamples = []; systemSamples = []; connectionOpenSamples = []; connectionExchangeSamples = []; networkInterfaceSamples.clear(); lastLogs = null; latestStatus = null; clearSettingsDirty(); connected = false; busy = false; $("workspace").hidden = true; $("login").hidden = navigating; $("logout").hidden = true; $("password").value = ""; $("password-confirm").value = ""; $("setup-token").value = ""; $("password-form").reset(); $("config-editor").value = ""; $("logs").replaceChildren(); $("audit-rows").replaceChildren(); authMessage("");
for (const id of ["certificate-list", "host-policy-list"]) { $(id)?.replaceChildren(); }
$("workspace").querySelectorAll("form").forEach(form => form.reset());
$("workspace").querySelectorAll("input:not([type=checkbox]):not([type=radio]),textarea").forEach(input => { input.value = ""; });
document.querySelectorAll("dialog").forEach(dialog => { dialog.close(); dialog.querySelectorAll("input:not([type=checkbox]):not([type=radio]),textarea").forEach(input => input.value = ""); });
renderSession(); syncAuthForm();
appHealthUpdatedAt = null; renderAppHealth(null); renderSession(); syncAuthForm();
if (navigating) { document.documentElement.classList.add("app-loading"); location.replace(destination); }
}
@@ -506,6 +568,8 @@ function stopTunnelRefresh() { window.rustProxyRefresh?.sync(); }
function startTunnelRefresh() { window.rustProxyRefresh?.sync(); }
$("login-form").addEventListener("submit", async event => { event.preventDefault(); const form = event.currentTarget; authMessage(""); try { await withFormSubmitState(form, event.submitter, async () => { const username = $("username").value.trim(); const password = $("password").value; if (setupRequired) { if (password !== $("password-confirm").value) throw fieldError("password-confirm", t("passwordMismatch")); await api("/api/auth/setup", { method: "POST", body: JSON.stringify({ token: $("setup-token").value.trim(), username, password }) }); setupRequired = false; syncAuthForm(); } const info = await api("/api/auth/login", { method: "POST", body: JSON.stringify({ username, password }) }); await activate(info); clearTimeout(toastTimer); hideToast(); }); } catch (e) { if (e.code === "auth.already_initialized") { setupRequired = false; syncAuthForm(); } $("password").value = ""; $("password-confirm").value = ""; authMessage(e.message); } });
$("logout").addEventListener("click", async () => { try { await api("/api/auth/logout", { method: "POST" }); lock(); toast(t("locked")); } catch (e) { if (e.status === 401) lock(); toast(e.message, true); } });
$("app-health-open")?.addEventListener("click", () => { renderAppHealth(connected ? latestStatus : null); const dialog = $("app-health-dialog"); if (!dialog.open) dialog.showModal(); });
$("app-health-close")?.addEventListener("click", () => $("app-health-dialog").close());
$("about-open").addEventListener("click", () => { const dialog = $("about-dialog"); if (!dialog.open) dialog.showModal(); });
$("about-close").addEventListener("click", () => $("about-dialog").close());
const mobileMenuToggle = $("mobile-menu-toggle"), dashboardSidebar = document.querySelector(".sidebar");