diff --git a/package.json b/package.json
index 896b581..ee03a39 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/public/app.js b/src/public/app.js
index 46ed233..b3e6c24 100644
--- a/src/public/app.js
+++ b/src/public/app.js
@@ -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 ? `` : "";
+ 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 `${value}`;
+ }).join("");
+ const firstPoint = points[0], lastPoint = points[points.length - 1];
+ const timeLabels = points.length ? `${escapeHtml(formatTime(firstPoint.at))}${escapeHtml(formatTime(lastPoint.at))}` : "";
+ 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}${timeLabels}` : 'No request data for this window yet.';
const routes = data.routes || [];
- $("#performance-rows").innerHTML = routes.length ? routes.map(route => `
| ${escapeHtml(route.host)} | ${route.hourRequests} | ${route.dayRequests} | ${route.dayErrors ? `${route.dayErrors}` : "0"} | ${route.dayAvgMs == null ? "—" : `${route.dayAvgMs} ms`} |
`).join("") : '| No requests have been logged yet. |
';
+ $("#performance-rows").innerHTML = routes.length ? routes.map(route => `| ${escapeHtml(route.host)} | ${route.hourRequests} | ${route.dayRequests} | ${route.dayErrors ? `${route.dayErrors}` : "0"} | ${route.dayAvgMs == null ? "—" : `${route.dayAvgMs} ms`} |
`).join("") : '| No requests have been logged yet. |
';
if (selected) $(`#performance-rows tr.row-highlight`)?.scrollIntoView({ block: "nearest" });
}
diff --git a/src/public/index.html b/src/public/index.html
index c2e4eef..376e4a5 100644
--- a/src/public/index.html
+++ b/src/public/index.html
@@ -145,10 +145,13 @@
No requests recorded yet. Not checked yet.
+
+ Per-route
Throughput by domain
+ Requests, error rate, and average response time for each configured domain.
+
- Per-route
Throughput by domain
Requests, error rate, and average response time for each configured domain.
-
diff --git a/src/public/styles.css b/src/public/styles.css
index 5d99133..8f2b304 100644
--- a/src/public/styles.css
+++ b/src/public/styles.css
@@ -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%}}