Compare commits

..

1 Commits

Author SHA1 Message Date
marvin 9c20253fa4 Replace inline error-breakdown expand with a themed popup (v0.11.114) 2026-09-16 15:21:54 -04:00
4 changed files with 10 additions and 6 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "site-gateway",
"version": "0.11.113",
"version": "0.11.114",
"private": true,
"description": "Site Gateway: simple self-hosted website publishing, reverse proxying, and automatic HTTPS.",
"type": "module",
+6 -2
View File
@@ -338,8 +338,8 @@ function renderPerformance() {
$("#performance-sparkline").innerHTML = points.length ? `${gridLines}<path d="${areaPath}" fill="var(--green)" opacity="0.12" stroke="none" /><path d="${smoothLine}" fill="none" stroke="var(--green)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />` : "";
if (!points.length) $("#performance-sparkline-labels").innerHTML = '<span class="axis-label" style="left:0;width:100%;top:45%;text-align:center">No request data for this window yet.</span>';
const routes = data.routes || [];
const countCell = (count, errors, breakdown) => { if (!errors) return `${count.toLocaleString()}`; if (!breakdown?.length) return `${count.toLocaleString()} <span class="count-divider">·</span> <span class="http-status bad">${errors.toLocaleString()}</span>`; const rows = breakdown.map(item => `<div class="error-breakdown-row"><span>${escapeHtml(item.status)}</span><span>${item.count.toLocaleString()}</span></div>`).join(""); return `${count.toLocaleString()} <span class="count-divider">·</span> <details class="error-breakdown"><summary class="http-status bad">${errors.toLocaleString()}</summary><div class="error-breakdown-body">${rows}</div></details>`; };
$("#performance-rows").innerHTML = routes.length ? routes.map(route => `<tr class="${selected && route.host === selected ? "row-highlight" : ""}"><td title="${escapeHtml(route.host)}">${escapeHtml(route.host)}</td><td>${countCell(route.hourRequests, route.hourErrors)}</td><td>${countCell(route.dayRequests, route.dayErrors, route.errorBreakdown)}</td><td>${route.dayAvgMs == null ? "—" : `${route.dayAvgMs} ms`}</td></tr>`).join("") : '<tr><td colspan="4" class="quiet-state">No requests have been logged yet.</td></tr>';
const countCell = (count, errors, breakdown, host) => { if (!errors) return `${count.toLocaleString()}`; if (!breakdown?.length) return `${count.toLocaleString()} <span class="count-divider">·</span> <span class="http-status bad">${errors.toLocaleString()}</span>`; return `${count.toLocaleString()} <span class="count-divider">·</span> <button type="button" class="http-status bad count-link-button" data-error-host="${escapeHtml(host)}" data-error-total="${errors}" data-error-breakdown="${escapeHtml(JSON.stringify(breakdown))}">${errors.toLocaleString()}</button>`; };
$("#performance-rows").innerHTML = routes.length ? routes.map(route => `<tr class="${selected && route.host === selected ? "row-highlight" : ""}"><td title="${escapeHtml(route.host)}">${escapeHtml(route.host)}</td><td>${countCell(route.hourRequests, route.hourErrors)}</td><td>${countCell(route.dayRequests, route.dayErrors, route.errorBreakdown, route.host)}</td><td>${route.dayAvgMs == null ? "—" : `${route.dayAvgMs} ms`}</td></tr>`).join("") : '<tr><td colspan="4" class="quiet-state">No requests have been logged yet.</td></tr>';
if (selected) $(`#performance-rows tr.row-highlight`)?.scrollIntoView({ block: "nearest" });
}
@@ -513,6 +513,10 @@ $("#refresh-logs").addEventListener("click", () => loadFeatureView().catch(error
$("#log-host").addEventListener("change", () => loadFeatureView().catch(error => toast(error.message)));
$("#performance-host").addEventListener("change", () => loadFeatureView().catch(error => toast(error.message)));
$("#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 => `<div class="error-breakdown-row"><span>${escapeHtml(item.status)}</span><span>${item.count.toLocaleString()}</span></div>`).join(""); dialog.innerHTML = `<form method="dialog" class="dialog-card compact"><div class="dialog-heading"><div><p class="eyebrow">Performance · Last 24h</p><h2>${escapeHtml(host)}</h2></div><button value="cancel" formnovalidate class="icon-button" aria-label="Close">×</button></div><p class="muted">${total.toLocaleString()} error response${total === 1 ? "" : "s"} in the last 24 hours, by status code.</p><div class="error-breakdown-list">${rows}</div><div class="dialog-actions"><button value="cancel" class="button secondary">Close</button></div></form>`; 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); });
$("#log-status").addEventListener("change", renderLogs);
$("#event-severity").addEventListener("change", renderLogs);
$("#event-category").addEventListener("change", renderLogs);
+2 -2
View File
@@ -7,7 +7,7 @@
<title>Site Gateway</title>
<meta name="description" content="Host sites, proxy services, and manage HTTPS from one simple dashboard.">
<link rel="icon" type="image/png" href="/site-gateway-icon-approved.png">
<link rel="stylesheet" href="/styles.css?v=0.11.113">
<link rel="stylesheet" href="/styles.css?v=0.11.114">
</head>
<!-- ================================================================
@@ -430,6 +430,6 @@
<div id="toast" class="toast" role="status"></div>
<div id="update-banner" class="update-banner hidden" role="status"><span>A new version of Site Gateway is available.</span><div class="update-banner-actions"><button id="update-banner-refresh" class="button primary">Refresh</button><button id="update-banner-dismiss" class="text-button">Dismiss</button></div></div>
<!-- App scripts: core (app.js) then extended views/admin (features.js) -->
<script src="/app.js?v=0.11.113" defer></script><script src="/features.js?v=0.11.113" defer></script>
<script src="/app.js?v=0.11.114" defer></script><script src="/features.js?v=0.11.114" defer></script>
</body>
</html>
+1 -1
View File
@@ -277,7 +277,7 @@ select{appearance:none!important;-webkit-appearance:none!important;background-re
.performance-table th:nth-child(3),.performance-table td:nth-child(3){width:23%;text-align:center}
.performance-table th:nth-child(4),.performance-table td:nth-child(4){width:23%;text-align:center}
.performance-table .count-divider{color:var(--muted);margin:0 2px}
.performance-table td:has(.error-breakdown[open]){white-space:normal;overflow:visible}.performance-table .error-breakdown{display:inline-block;margin:0;border:0;border-radius:0;background:transparent;text-align:left;vertical-align:middle}.performance-table .error-breakdown summary{display:inline;padding:0;color:inherit;font-weight:inherit;cursor:pointer;list-style:none}.performance-table .error-breakdown summary::-webkit-details-marker{display:none}.performance-table .error-breakdown summary::marker{content:""}.performance-table .error-breakdown[open] summary{text-decoration:underline dotted;text-underline-offset:3px}.performance-table .error-breakdown-body{margin-top:6px;padding:8px 10px;border:1px solid var(--line);border-radius:8px;background:rgba(8,16,29,.28);font-size:.76rem;white-space:normal}.performance-table .error-breakdown-row{display:flex;justify-content:space-between;gap:14px;padding:2px 0}
.performance-table .count-link-button{border:0;background:none;padding:0;font:inherit;color:var(--danger);cursor:pointer;text-decoration:underline dotted;text-underline-offset:3px}.performance-table .count-link-button:hover{text-decoration-style:solid}.error-breakdown-list{margin-top:16px;border:1px solid var(--line);border-radius:12px;background:rgba(8,16,29,.28);padding:0 16px}.error-breakdown-row{display:flex;justify-content:space-between;gap:14px;padding:10px 0;border-top:1px solid var(--line);font-size:.85rem}.error-breakdown-row:first-child{border-top:0}
@media(max-width:760px){.performance-table th:nth-child(2),.performance-table td:nth-child(2){display:none}.performance-table th:nth-child(1),.performance-table td:nth-child(1){width:40%}.performance-table th:nth-child(3),.performance-table td:nth-child(3){width:30%}.performance-table th:nth-child(4),.performance-table td:nth-child(4){width:30%}}
.mfa-qr{display:flex;justify-content:center;padding:16px;background:#fff;border-radius:12px;margin:16px 0}
.mfa-qr svg{width:200px;height:200px}