Compare commits

...

1 Commits

Author SHA1 Message Date
marvin 02271d9072 Performance tab: sparkline axis, per-route card, fix table overflow/truncation 2026-09-13 20:03:26 -04:00
4 changed files with 33 additions and 8 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "site-gateway",
"version": "0.11.60",
"version": "0.11.61",
"private": true,
"description": "Site Gateway: simple self-hosted website publishing, reverse proxying, and automatic HTTPS.",
"type": "module",
+16 -4
View File
@@ -245,11 +245,23 @@ function renderPerformance() {
$("#performance-trend-title").textContent = `Requests · last 6 hours${selected ? ` · ${selected}` : ""}`;
const points = data.trend || [];
const max = Math.max(1, ...points.map(point => point.count));
const stepX = points.length > 1 ? 600 / (points.length - 1) : 600;
const path = points.map((point, index) => `${index === 0 ? "M" : "L"}${(index * stepX).toFixed(1)},${(120 - (point.count / max) * 110 - 4).toFixed(1)}`).join(" ");
$("#performance-sparkline").innerHTML = points.length ? `<polyline points="${points.map((point, index) => `${(index * stepX).toFixed(1)},${(120 - (point.count / max) * 110 - 4).toFixed(1)}`).join(" ")}" fill="none" stroke="var(--green)" stroke-width="2" /><path d="${path} L${(600).toFixed(1)},120 L0,120 Z" fill="var(--green)" opacity="0.12" stroke="none" />` : "";
const left = 34, right = 8, top = 10, bottom = 20, width = 600, height = 140;
const plotWidth = width - left - right, plotHeight = height - top - bottom;
const xAt = index => left + (points.length > 1 ? (index / (points.length - 1)) * plotWidth : plotWidth);
const yAt = count => top + plotHeight - (count / max) * plotHeight;
const gridLines = [0, 0.5, 1].map(fraction => {
const y = (top + plotHeight * (1 - fraction)).toFixed(1);
const value = Math.round(max * fraction);
return `<line x1="${left}" y1="${y}" x2="${width - right}" y2="${y}" stroke="var(--line)" stroke-width="1" /><text x="${left - 8}" y="${y}" text-anchor="end" dominant-baseline="middle" fill="var(--muted)" font-size="10">${value}</text>`;
}).join("");
const firstPoint = points[0], lastPoint = points[points.length - 1];
const timeLabels = points.length ? `<text x="${left}" y="${height - 4}" fill="var(--muted)" font-size="10">${escapeHtml(formatTime(firstPoint.at))}</text><text x="${width - right}" y="${height - 4}" text-anchor="end" fill="var(--muted)" font-size="10">${escapeHtml(formatTime(lastPoint.at))}</text>` : "";
const linePoints = points.map((point, index) => `${xAt(index).toFixed(1)},${yAt(point.count).toFixed(1)}`).join(" ");
const areaPath = points.length ? `M${xAt(0).toFixed(1)},${(top + plotHeight).toFixed(1)} L${linePoints.replaceAll(" ", " L")} L${xAt(points.length - 1).toFixed(1)},${(top + plotHeight).toFixed(1)} Z` : "";
$("#performance-sparkline").setAttribute("viewBox", `0 0 ${width} ${height}`);
$("#performance-sparkline").innerHTML = points.length ? `${gridLines}<path d="${areaPath}" fill="var(--green)" opacity="0.12" stroke="none" /><polyline points="${linePoints}" fill="none" stroke="var(--green)" stroke-width="2" />${timeLabels}` : '<text x="10" y="70" fill="var(--muted)" font-size="12">No request data for this window yet.</text>';
const routes = data.routes || [];
$("#performance-rows").innerHTML = routes.length ? routes.map(route => `<tr class="${selected && route.host === selected ? "row-highlight" : ""}"><td>${escapeHtml(route.host)}</td><td>${route.hourRequests}</td><td>${route.dayRequests}</td><td>${route.dayErrors ? `<span class="http-status bad">${route.dayErrors}</span>` : "0"}</td><td>${route.dayAvgMs == null ? "—" : `${route.dayAvgMs} ms`}</td></tr>`).join("") : '<tr><td colspan="5" class="quiet-state">No requests have been logged yet.</td></tr>';
$("#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>${route.hourRequests}</td><td>${route.dayRequests}</td><td>${route.dayErrors ? `<span class="http-status bad">${route.dayErrors}</span>` : "0"}</td><td>${route.dayAvgMs == null ? "—" : `${route.dayAvgMs} ms`}</td></tr>`).join("") : '<tr><td colspan="5" class="quiet-state">No requests have been logged yet.</td></tr>';
if (selected) $(`#performance-rows tr.row-highlight`)?.scrollIntoView({ block: "nearest" });
}
+6 -3
View File
@@ -145,10 +145,13 @@
<p id="performance-summary" class="muted feature-note">No requests recorded yet. <span id="performance-last-checked">Not checked yet.</span></p>
<section class="dashboard-panel">
<div class="panel-heading"><div><p class="eyebrow">Trend</p><h2 id="performance-trend-title">Requests · last 6 hours</h2></div></div>
<svg id="performance-sparkline" class="performance-sparkline" viewBox="0 0 600 120" preserveAspectRatio="none" aria-label="Request volume trend"></svg>
<svg id="performance-sparkline" class="performance-sparkline" viewBox="0 0 600 140" aria-label="Request volume trend"></svg>
</section>
<section class="dashboard-panel">
<div class="panel-heading"><div><p class="eyebrow">Per-route</p><h2>Throughput by domain</h2></div></div>
<p class="muted feature-note">Requests, error rate, and average response time for each configured domain.</p>
<div class="table-wrap performance-table-wrap"><table class="performance-table"><thead><tr><th>Domain</th><th>Last hour</th><th>Last 24h</th><th>Errors (24h)</th><th>Avg. response</th></tr></thead><tbody id="performance-rows"></tbody></table></div>
</section>
<div class="diagnostic-section-heading"><p class="eyebrow">Per-route</p><h2>Throughput by domain</h2><p class="muted">Requests, error rate, and average response time for each configured domain.</p></div>
<div class="table-wrap diagnostic-list"><table class="log-table performance-table"><thead><tr><th>Domain</th><th>Last hour</th><th>Last 24h</th><th>Errors (24h)</th><th>Avg. response</th></tr></thead><tbody id="performance-rows"></tbody></table></div>
</section>
<section id="users-view" class="feature-view hidden">
<div class="admin-tabs"><button class="tab-active" data-admin-tab="users">Users</button><button data-admin-tab="defaults">Gateway defaults</button><button data-admin-tab="backups">Backup & restore</button><button data-admin-tab="security">Security & updates</button><button data-admin-tab="danger" class="danger-tab">Danger Zone</button></div>
+10
View File
@@ -259,3 +259,13 @@ select{appearance:none!important;-webkit-appearance:none!important;background-re
#dashboard-view>.dashboard-columns #dashboard-jobs{margin:0;align-self:start}
#dashboard-view>.dashboard-columns #dashboard-jobs{align-self:stretch;height:auto}
#access-list.loading { visibility: hidden; }
.table-wrap.performance-table-wrap{overflow-x:hidden;overflow-y:auto;max-height:min(52vh,620px)}
.performance-table{width:100%;border-collapse:collapse;font-size:.82rem;table-layout:fixed}
.performance-table th,.performance-table td{padding:13px 15px;text-align:left;border-top:1px solid var(--line);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.performance-table thead th{border-top:0;color:var(--muted);font-size:.7rem;text-transform:uppercase;letter-spacing:.06em;white-space:normal}
.performance-table th:nth-child(1),.performance-table td:nth-child(1){width:32%}
.performance-table th:nth-child(2),.performance-table td:nth-child(2){width:14%}
.performance-table th:nth-child(3),.performance-table td:nth-child(3){width:14%}
.performance-table th:nth-child(4),.performance-table td:nth-child(4){width:19%;text-align:center}
.performance-table th:nth-child(5),.performance-table td:nth-child(5){width:21%}
@media(max-width:760px){.performance-table th:nth-child(2),.performance-table td:nth-child(2),.performance-table th:nth-child(3),.performance-table td:nth-child(3){display:none}.performance-table th:nth-child(1),.performance-table td:nth-child(1){width:46%}.performance-table th:nth-child(4),.performance-table td:nth-child(4){width:27%}.performance-table th:nth-child(5),.performance-table td:nth-child(5){width:27%}}