diff --git a/package.json b/package.json
index 1897f07..bd74dfe 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "site-gateway",
- "version": "0.11.115",
+ "version": "0.11.116",
"private": true,
"description": "Site Gateway: simple self-hosted website publishing, reverse proxying, and automatic HTTPS.",
"type": "module",
diff --git a/src/public/app.js b/src/public/app.js
index cbe1246..21c47d6 100644
--- a/src/public/app.js
+++ b/src/public/app.js
@@ -8,7 +8,7 @@
// --- Shared DOM shortcut and app state ----------------------------------------
const $ = selector => document.querySelector(selector);
-const state = { sites: [], proxies: [], redirects: [], streams: [], accessLists: [], groups: [], backups: [], settings: null, dashboard: null, certificates: null, readiness: null, logs: null, users: [], user: null, config: null, view: "overview", loaded: false, pendingDelete: null, pendingReplace: null, editing: null, iconTarget: null, passwordTarget: null, healthTimer: null, updateCheckTimer: null, loadedVersion: null, updateAvailable: false };
+const state = { sites: [], proxies: [], redirects: [], streams: [], accessLists: [], groups: [], backups: [], settings: null, dashboard: null, certificates: null, readiness: null, logs: null, users: [], user: null, config: null, view: "overview", loaded: false, pendingDelete: null, pendingReplace: null, editing: null, iconTarget: null, passwordTarget: null, healthTimer: null, updateCheckTimer: null, loadedVersion: null, updateAvailable: false, performanceErrorBreakdowns: {} };
// One-time DOM patches: move the Access List field into the create/settings
// forms (features.js owns the Access List data, this file owns these forms).
@@ -338,7 +338,8 @@ function renderPerformance() {
$("#performance-sparkline").innerHTML = points.length ? `${gridLines}` : "";
if (!points.length) $("#performance-sparkline-labels").innerHTML = 'No request data for this window yet.';
const routes = data.routes || [];
- const countCell = (count, errors, breakdown, host) => { if (!errors) return `${count.toLocaleString()}`; if (!breakdown?.length) return `${count.toLocaleString()} · ${errors.toLocaleString()}`; return `${count.toLocaleString()} · `; };
+ state.performanceErrorBreakdowns = {};
+ const countCell = (count, errors, breakdown, host) => { if (!errors) return `${count.toLocaleString()}`; if (!breakdown?.length) return `${count.toLocaleString()} · ${errors.toLocaleString()}`; state.performanceErrorBreakdowns[host] = { total: errors, breakdown }; return `${count.toLocaleString()} · `; };
$("#performance-rows").innerHTML = routes.length ? routes.map(route => `
| ${escapeHtml(route.host)} | ${countCell(route.hourRequests, route.hourErrors)} | ${countCell(route.dayRequests, route.dayErrors, route.errorBreakdown, route.host)} | ${route.dayAvgMs == null ? "—" : `${route.dayAvgMs} ms`} |
`).join("") : '| No requests have been logged yet. |
';
if (selected) $(`#performance-rows tr.row-highlight`)?.scrollIntoView({ block: "nearest" });
}
@@ -515,8 +516,8 @@ $("#performance-host").addEventListener("change", () => loadFeatureView().catch(
$("#performance-range").addEventListener("change", () => loadFeatureView().catch(error => toast(error.message)));
// --- Performance: themed error-breakdown popup, replacing the old hover tooltip ------------
-function showErrorBreakdown(host, total, breakdown) { let dialog = document.querySelector("#error-breakdown-dialog"); if (!dialog) { dialog = document.createElement("dialog"); dialog.id = "error-breakdown-dialog"; document.body.append(dialog); } const rows = breakdown.map(item => `${escapeHtml(item.status)}${item.count.toLocaleString()}
`).join(""); dialog.innerHTML = ``; dialog.showModal(); }
-$("#performance-rows").addEventListener("click", event => { const button = event.target.closest("[data-error-breakdown]"); if (!button) return; let breakdown = []; try { breakdown = JSON.parse(button.dataset.errorBreakdown || "[]"); } catch { breakdown = []; } showErrorBreakdown(button.dataset.errorHost || "", Number(button.dataset.errorTotal) || 0, breakdown); });
+function showErrorBreakdown(host, total, breakdown) { let dialog = document.querySelector("#error-breakdown-dialog"); if (!dialog) { dialog = document.createElement("dialog"); dialog.id = "error-breakdown-dialog"; document.body.append(dialog); } const rows = breakdown.map(item => `${escapeHtml(item.status)}${item.count.toLocaleString()}
`).join(""); dialog.innerHTML = ``; dialog.showModal(); }
+$("#performance-rows").addEventListener("click", event => { const button = event.target.closest("[data-error-host]"); if (!button) return; const entry = state.performanceErrorBreakdowns[button.dataset.errorHost]; if (!entry) return; showErrorBreakdown(button.dataset.errorHost, entry.total, entry.breakdown); });
$("#log-status").addEventListener("change", renderLogs);
$("#event-severity").addEventListener("change", renderLogs);
$("#event-category").addEventListener("change", renderLogs);
diff --git a/src/public/index.html b/src/public/index.html
index 84b95f1..d8e205b 100644
--- a/src/public/index.html
+++ b/src/public/index.html
@@ -430,6 +430,6 @@
A new version of Site Gateway is available.
-
+