Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dda6c517fc | |||
| 887cd2dfcc | |||
| 227823c534 |
@@ -268,3 +268,9 @@ Roughly in priority order:
|
|||||||
`v0.16.71` fixes a Gateway Events / Audit log message that slipped past v0.16.66: the Hosted Site file-upload route ("Replace files") still wrote the old, un-prefixed `Files replaced for "<name>".` line instead of the `Hosted site "<name>" ...` convention every other Hosted Site action uses (created, enabled/disabled, deleted, and the field-level update summaries). It lived in its own route separate from the four `PATCH` handlers v0.16.66 touched, so it was missed at the time and only noticed once a user pointed out the log entry gave no way to tell which route type it belonged to. Now reads `Hosted site "<name>" files replaced.`, matching the rest.
|
`v0.16.71` fixes a Gateway Events / Audit log message that slipped past v0.16.66: the Hosted Site file-upload route ("Replace files") still wrote the old, un-prefixed `Files replaced for "<name>".` line instead of the `Hosted site "<name>" ...` convention every other Hosted Site action uses (created, enabled/disabled, deleted, and the field-level update summaries). It lived in its own route separate from the four `PATCH` handlers v0.16.66 touched, so it was missed at the time and only noticed once a user pointed out the log entry gave no way to tell which route type it belonged to. Now reads `Hosted site "<name>" files replaced.`, matching the rest.
|
||||||
|
|
||||||
`v0.16.72` fixes the favicon not appearing in some Chromium-family browsers (reported: DuckDuckGo's browser showed the default globe icon while Safari showed the real one correctly). The app previously declared a single `<link rel="icon">` pointing straight at the 1018x1001, 396 KB source PNG with no `sizes` attribute and no `/favicon.ico` fallback -- Safari is forgiving about oversized, unsized favicons, but some Chromium-based browsers silently skip one that large rather than downscale it, and several also probe `/favicon.ico` directly regardless of what the `<link>` tag says. Added properly sized `favicon.ico` (16/32px, multi-size), standalone `favicon-16.png`/`favicon-32.png` with `sizes` attributes, and a 180x180 `apple-touch-icon.png`, all generated from the existing approved icon artwork and served automatically by the existing static file handler -- no server route or icon design changes.
|
`v0.16.72` fixes the favicon not appearing in some Chromium-family browsers (reported: DuckDuckGo's browser showed the default globe icon while Safari showed the real one correctly). The app previously declared a single `<link rel="icon">` pointing straight at the 1018x1001, 396 KB source PNG with no `sizes` attribute and no `/favicon.ico` fallback -- Safari is forgiving about oversized, unsized favicons, but some Chromium-based browsers silently skip one that large rather than downscale it, and several also probe `/favicon.ico` directly regardless of what the `<link>` tag says. Added properly sized `favicon.ico` (16/32px, multi-size), standalone `favicon-16.png`/`favicon-32.png` with `sizes` attributes, and a 180x180 `apple-touch-icon.png`, all generated from the existing approved icon artwork and served automatically by the existing static file handler -- no server route or icon design changes.
|
||||||
|
|
||||||
|
`v0.16.73` makes the Dashboard's System panel top accent bar reflect resource state, instead of always showing green -- pointed out after a screenshot showed CPU pinned at 100% (its own stat correctly shown in red) while the panel's top bar stayed the hardcoded `var(--green)` it always had. `renderHeroPanel()` now tracks the worst tone across CPU/memory/swap/disk (the same "warning" at 75%+ / "critical" at 90%+ thresholds each stat's own value and fill bar already used) and applies a `tone-warning`/`tone-critical` class to the panel, which `.system-panel::before` now reads instead of a fixed color. Network, uptime, and throughput don't carry a tone and are excluded from the calculation, same as before. Applies to the Dashboard's System panel only -- the Administration > System tab's equivalent hero grid has no top accent bar to react.
|
||||||
|
|
||||||
|
`v0.16.74` fixes an inaccurate status label on Hosted Site, Proxy Host, and Streaming Host cards, and the Certificates table's Upstream column: a route with monitoring intentionally turned off via "Monitor this site/upstream" in Advanced options -- while the route itself stays enabled and running -- read "Monitoring paused", the same text used for a route that's fully disabled. "Paused" implies a temporary interruption; deliberately unchecking the monitor box is an ongoing, intentional setting. The backend already distinguished the two cases (`status: "disabled"` when the route itself is off vs. `status: "unmonitored"` when only health checks are off, in `checkProxy()`), the frontend just collapsed them into one string in four places. Disabled routes keep "Monitoring paused"; a running route with health checks off now reads "Monitoring disabled". No backend or status-logic changes, text only.
|
||||||
|
|
||||||
|
`v0.16.75` is a one-line smoke test to confirm the Gitea remote (`git.us2plus2.com/marvin/site-gateway`) actually receives pushes end-to-end after the GitHub-to-Gitea migration: the Dashboard heading text reads "Dashboard v2" instead of "Dashboard" (both the static HTML fallback and the JS that sets it on render), with no other functional change. Pushed to both `origin` (Gitea) and `github` remotes per the dual-push arrangement while GitHub CI still builds the published container image.
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "site-gateway",
|
"name": "site-gateway",
|
||||||
"version": "0.16.72",
|
"version": "0.16.75",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Site Gateway: simple self-hosted website publishing, reverse proxying, and automatic HTTPS.",
|
"description": "Site Gateway: simple self-hosted website publishing, reverse proxying, and automatic HTTPS.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
+4
-4
@@ -247,14 +247,14 @@ function upstreamStateClass(enabled, upstream) {
|
|||||||
}
|
}
|
||||||
function hostedCard(site) {
|
function hostedCard(site) {
|
||||||
const status = site.status === "running" ? "running" : site.status === "error" ? "error" : "disabled";
|
const status = site.status === "running" ? "running" : site.status === "error" ? "error" : "disabled";
|
||||||
const upstream = !site.enabled || site.upstream?.status === "unmonitored" ? "Monitoring paused" : !site.upstream || site.upstream.status === "pending" ? "Upstream check pending" : site.upstream.status === "healthy" ? `Upstream ${site.upstream.httpStatus} · ${site.upstream.responseMs} ms` : `Upstream unavailable · ${escapeHtml(site.upstream.error || "check failed")}`;
|
const upstream = !site.enabled ? "Monitoring paused" : site.upstream?.status === "unmonitored" ? "Monitoring disabled" : !site.upstream || site.upstream.status === "pending" ? "Upstream check pending" : site.upstream.status === "healthy" ? `Upstream ${site.upstream.httpStatus} · ${site.upstream.responseMs} ms` : `Upstream unavailable · ${escapeHtml(site.upstream.error || "check failed")}`;
|
||||||
const menu = canManage() ? `<div class="menu-wrap"><button class="icon-button menu-button" aria-label="Site options" aria-expanded="false">•••</button><div class="menu"><button data-action="settings">Domain & TLS</button><button data-action="icon">Change icon</button><button data-action="caddy-config">View Caddy config</button><button data-action="replace">Replace files</button><button data-action="delete" class="danger-text">Delete site</button></div></div>` : "";
|
const menu = canManage() ? `<div class="menu-wrap"><button class="icon-button menu-button" aria-label="Site options" aria-expanded="false">•••</button><div class="menu"><button data-action="settings">Domain & TLS</button><button data-action="icon">Change icon</button><button data-action="caddy-config">View Caddy config</button><button data-action="replace">Replace files</button><button data-action="delete" class="danger-text">Delete site</button></div></div>` : "";
|
||||||
const toggle = canManage() ? `<button class="toggle ${site.enabled ? "on" : ""}" data-action="toggle" aria-label="${site.enabled ? "Disable" : "Enable"} ${escapeHtml(site.name)}"><span></span></button>` : "";
|
const toggle = canManage() ? `<button class="toggle ${site.enabled ? "on" : ""}" data-action="toggle" aria-label="${site.enabled ? "Disable" : "Enable"} ${escapeHtml(site.name)}"><span></span></button>` : "";
|
||||||
return `<article class="site-card" data-id="${site.id}" data-kind="hosted"><div class="card-top"><div class="site-icon">${iconMarkup(site)}</div>${menu}</div><h2>${escapeHtml(site.name)}</h2><p class="address">${escapeHtml(site.domain || `Port ${site.port}`)}</p>${site.domain ? `<p class="gateway-address ${site.tls !== "http" ? "secure" : ""}">${escapeHtml(publicUrl(site))}</p>` : ""}<p class="upstream-copy ${upstreamStateClass(site.enabled, site.upstream)}">${upstream}</p><div class="card-footer"><span class="status-pill"><span class="status-dot ${status}"></span>${status === "error" ? "Needs attention" : status[0].toUpperCase() + status.slice(1)}</span><div class="card-actions">${toggle}<a class="launch" href="${publicUrl(site)}" target="_blank" rel="noopener" aria-label="Open ${escapeHtml(site.name)}">↗</a></div></div></article>`;
|
return `<article class="site-card" data-id="${site.id}" data-kind="hosted"><div class="card-top"><div class="site-icon">${iconMarkup(site)}</div>${menu}</div><h2>${escapeHtml(site.name)}</h2><p class="address">${escapeHtml(site.domain || `Port ${site.port}`)}</p>${site.domain ? `<p class="gateway-address ${site.tls !== "http" ? "secure" : ""}">${escapeHtml(publicUrl(site))}</p>` : ""}<p class="upstream-copy ${upstreamStateClass(site.enabled, site.upstream)}">${upstream}</p><div class="card-footer"><span class="status-pill"><span class="status-dot ${status}"></span>${status === "error" ? "Needs attention" : status[0].toUpperCase() + status.slice(1)}</span><div class="card-actions">${toggle}<a class="launch" href="${publicUrl(site)}" target="_blank" rel="noopener" aria-label="Open ${escapeHtml(site.name)}">↗</a></div></div></article>`;
|
||||||
}
|
}
|
||||||
function proxyCard(proxy) {
|
function proxyCard(proxy) {
|
||||||
const status = proxy.status === "running" ? "running" : proxy.status === "error" ? "error" : "disabled";
|
const status = proxy.status === "running" ? "running" : proxy.status === "error" ? "error" : "disabled";
|
||||||
const upstream = !proxy.enabled || proxy.upstream?.status === "unmonitored" ? "Monitoring paused" : !proxy.upstream || proxy.upstream.status === "pending" ? "Upstream check pending" : proxy.upstream.status === "healthy" ? `Upstream ${proxy.upstream.httpStatus} · ${proxy.upstream.responseMs} ms` : `Upstream unavailable · ${escapeHtml(proxy.upstream.error || "check failed")}`;
|
const upstream = !proxy.enabled ? "Monitoring paused" : proxy.upstream?.status === "unmonitored" ? "Monitoring disabled" : !proxy.upstream || proxy.upstream.status === "pending" ? "Upstream check pending" : proxy.upstream.status === "healthy" ? `Upstream ${proxy.upstream.httpStatus} · ${proxy.upstream.responseMs} ms` : `Upstream unavailable · ${escapeHtml(proxy.upstream.error || "check failed")}`;
|
||||||
const menu = canManage() ? `<div class="menu-wrap"><button class="icon-button menu-button" aria-label="Proxy options" aria-expanded="false">•••</button><div class="menu"><button data-action="settings">Edit proxy</button><button data-action="icon">Change icon</button><button data-action="caddy-config">View Caddy config</button><button data-action="delete" class="danger-text">Delete proxy</button></div></div>` : "";
|
const menu = canManage() ? `<div class="menu-wrap"><button class="icon-button menu-button" aria-label="Proxy options" aria-expanded="false">•••</button><div class="menu"><button data-action="settings">Edit proxy</button><button data-action="icon">Change icon</button><button data-action="caddy-config">View Caddy config</button><button data-action="delete" class="danger-text">Delete proxy</button></div></div>` : "";
|
||||||
const toggle = canManage() ? `<button class="toggle ${proxy.enabled ? "on" : ""}" data-action="toggle" aria-label="${proxy.enabled ? "Disable" : "Enable"} ${escapeHtml(proxy.name)}"><span></span></button>` : "";
|
const toggle = canManage() ? `<button class="toggle ${proxy.enabled ? "on" : ""}" data-action="toggle" aria-label="${proxy.enabled ? "Disable" : "Enable"} ${escapeHtml(proxy.name)}"><span></span></button>` : "";
|
||||||
const access = proxy.accessListId ? (state.accessLists.find(item => item.id === proxy.accessListId)?.name || "Access List") : "Public · no Access List";
|
const access = proxy.accessListId ? (state.accessLists.find(item => item.id === proxy.accessListId)?.name || "Access List") : "Public · no Access List";
|
||||||
@@ -277,7 +277,7 @@ function renderCertificates() {
|
|||||||
const tlsOk = item ? ["healthy", "warning", "critical", "not-configured"].includes(item.tls.status) : null;
|
const tlsOk = item ? ["healthy", "warning", "critical", "not-configured"].includes(item.tls.status) : null;
|
||||||
const dnsCell = item ? `<span class="status-dot ${dnsOk ? "running" : "error"}"></span>${dnsOk ? "Resolved" : "Failed"}` : `<span class="status-dot idle"></span>—`;
|
const dnsCell = item ? `<span class="status-dot ${dnsOk ? "running" : "error"}"></span>${dnsOk ? "Resolved" : "Failed"}` : `<span class="status-dot idle"></span>—`;
|
||||||
const tlsCell = item ? `<span class="status-dot ${tlsOk ? "running" : "error"}"></span>${escapeHtml(item.tls.status.replaceAll("-", " "))}` : `<span class="status-dot idle"></span>—`;
|
const tlsCell = item ? `<span class="status-dot ${tlsOk ? "running" : "error"}"></span>${escapeHtml(item.tls.status.replaceAll("-", " "))}` : `<span class="status-dot idle"></span>—`;
|
||||||
const upstreamCell = !item ? `<span class="status-dot idle"></span>—` : !item.upstream || item.upstream.status === "unmonitored" ? `<span class="status-dot idle"></span>Monitoring paused` : item.upstream.status === "pending" ? `<span class="status-dot idle"></span>Check pending` : item.upstream.status === "healthy" ? `<span class="status-dot running"></span>${item.upstream.httpStatus}` : `<span class="status-dot bad"></span>${escapeHtml(item.upstream.error || "Unavailable")}`;
|
const upstreamCell = !item ? `<span class="status-dot idle"></span>—` : !item.upstream ? `<span class="status-dot idle"></span>Monitoring paused` : item.upstream.status === "unmonitored" ? `<span class="status-dot idle"></span>Monitoring disabled` : item.upstream.status === "pending" ? `<span class="status-dot idle"></span>Check pending` : item.upstream.status === "healthy" ? `<span class="status-dot running"></span>${item.upstream.httpStatus}` : `<span class="status-dot bad"></span>${escapeHtml(item.upstream.error || "Unavailable")}`;
|
||||||
const statusLabel = cert.status === "mismatch" ? "Domain mismatch" : cert.status.charAt(0).toUpperCase() + cert.status.slice(1);
|
const statusLabel = cert.status === "mismatch" ? "Domain mismatch" : cert.status.charAt(0).toUpperCase() + cert.status.slice(1);
|
||||||
return `<tr class="cert-table-row" data-index="${index}" tabindex="0"><td><strong>${escapeHtml(cert.domain)}</strong><br><small class="muted">${escapeHtml(cert.kind)} · ${escapeHtml(cert.source)}</small></td><td><span class="status-dot ${cert.status === "healthy" ? "running" : cert.status === "pending" ? "idle" : "error"}"></span>${escapeHtml(statusLabel)}</td><td>${cert.expiresAt ? `${cert.daysRemaining} days` : "—"}</td><td>${escapeHtml(cert.issuer || "—")}</td><td>${dnsCell}</td><td>${tlsCell}</td><td>${upstreamCell}</td></tr>`;
|
return `<tr class="cert-table-row" data-index="${index}" tabindex="0"><td><strong>${escapeHtml(cert.domain)}</strong><br><small class="muted">${escapeHtml(cert.kind)} · ${escapeHtml(cert.source)}</small></td><td><span class="status-dot ${cert.status === "healthy" ? "running" : cert.status === "pending" ? "idle" : "error"}"></span>${escapeHtml(statusLabel)}</td><td>${cert.expiresAt ? `${cert.daysRemaining} days` : "—"}</td><td>${escapeHtml(cert.issuer || "—")}</td><td>${dnsCell}</td><td>${tlsCell}</td><td>${upstreamCell}</td></tr>`;
|
||||||
}).join("") : '<tr><td colspan="7" class="quiet-state">No HTTPS domains are configured.</td></tr>';
|
}).join("") : '<tr><td colspan="7" class="quiet-state">No HTTPS domains are configured.</td></tr>';
|
||||||
@@ -511,7 +511,7 @@ function render() {
|
|||||||
const adminUsersActive = activeAdminTab === "users", adminGroupsActive = activeAdminTab === "groups", adminApiActive = activeAdminTab === "api";
|
const adminUsersActive = activeAdminTab === "users", adminGroupsActive = activeAdminTab === "groups", adminApiActive = activeAdminTab === "api";
|
||||||
$("#open-create").classList.toggle("hidden", !(management || adminUsersActive || adminGroupsActive || adminApiActive || ["streaming","redirects","access"].includes(state.view)) || !canManage()); $("#check-health").classList.toggle("hidden", state.view !== "certificates" || !canAdmin()); $("#refresh-logs").classList.toggle("hidden", state.view !== "logs");
|
$("#open-create").classList.toggle("hidden", !(management || adminUsersActive || adminGroupsActive || adminApiActive || ["streaming","redirects","access"].includes(state.view)) || !canManage()); $("#check-health").classList.toggle("hidden", state.view !== "certificates" || !canAdmin()); $("#refresh-logs").classList.toggle("hidden", state.view !== "logs");
|
||||||
if (overview) {
|
if (overview) {
|
||||||
$("#page-title").textContent = "Dashboard";
|
$("#page-title").textContent = "Dashboard v2";
|
||||||
$("#page-subtitle").textContent = "Health, activity, and system status at a glance.";
|
$("#page-subtitle").textContent = "Health, activity, and system status at a glance.";
|
||||||
renderDashboard();
|
renderDashboard();
|
||||||
return;
|
return;
|
||||||
|
|||||||
+17
-4
@@ -22,7 +22,7 @@ function renderStreams() {
|
|||||||
empty.classList.toggle("hidden", !state.loaded || state.streams.length > 0);
|
empty.classList.toggle("hidden", !state.loaded || state.streams.length > 0);
|
||||||
list.innerHTML = state.streams.map(item => {
|
list.innerHTML = state.streams.map(item => {
|
||||||
const status = item.status === "running" ? "running" : item.status === "error" ? "error" : "disabled";
|
const status = item.status === "running" ? "running" : item.status === "error" ? "error" : "disabled";
|
||||||
const upstream = item.enabled === false || item.upstream?.status === "unmonitored" ? "Monitoring paused" : !item.upstream || item.upstream.status === "pending" ? "Target check pending" : item.upstream.status === "healthy" ? `Target reachable · ${item.upstream.responseMs} ms` : `Target unreachable · ${extendedEscape(item.upstream.error || "check failed")}`;
|
const upstream = item.enabled === false ? "Monitoring paused" : item.upstream?.status === "unmonitored" ? "Monitoring disabled" : !item.upstream || item.upstream.status === "pending" ? "Target check pending" : item.upstream.status === "healthy" ? `Target reachable · ${item.upstream.responseMs} ms` : `Target unreachable · ${extendedEscape(item.upstream.error || "check failed")}`;
|
||||||
const protocols = [item.tcp !== false ? "TCP" : null, item.udp ? "UDP" : null].filter(Boolean).map(value => `<span class="chip">${value}</span>`).join("");
|
const protocols = [item.tcp !== false ? "TCP" : null, item.udp ? "UDP" : null].filter(Boolean).map(value => `<span class="chip">${value}</span>`).join("");
|
||||||
const toggle = `<button class="toggle ${item.enabled === false ? "" : "on"}" data-stream-action="toggle" aria-label="${item.enabled === false ? "Enable" : "Disable"} ${extendedEscape(item.name)}"><span></span></button>`;
|
const toggle = `<button class="toggle ${item.enabled === false ? "" : "on"}" data-stream-action="toggle" aria-label="${item.enabled === false ? "Enable" : "Disable"} ${extendedEscape(item.name)}"><span></span></button>`;
|
||||||
return `<article class="site-card stream-card" data-stream-id="${item.id}" data-kind="stream"><div class="card-top"><div class="site-icon">${featureIcon(item,"SH")}</div><div class="menu-wrap"><button class="icon-button menu-button" aria-label="Streaming host options" aria-expanded="false">•••</button><div class="menu"><button data-stream-action="edit">Edit streaming host</button><button data-stream-action="icon">Change icon</button><button data-stream-action="delete" class="danger-text">Delete streaming host</button></div></div></div><h2>${extendedEscape(item.name)}</h2><p class="address">Port ${item.port}</p><p class="gateway-address">→ ${extendedEscape(item.target)}</p><p class="upstream-copy ${item.enabled === false || item.upstream?.status === "unmonitored" || !item.upstream || item.upstream.status === "pending" ? "idle" : item.upstream.status === "healthy" ? "" : "bad"}">${upstream}</p><div class="card-footer"><span class="status-pill"><span class="status-dot ${status}"></span>${status === "error" ? "Needs attention" : status[0].toUpperCase() + status.slice(1)}</span><div class="card-actions">${toggle}${protocols}</div></div></article>`;
|
return `<article class="site-card stream-card" data-stream-id="${item.id}" data-kind="stream"><div class="card-top"><div class="site-icon">${featureIcon(item,"SH")}</div><div class="menu-wrap"><button class="icon-button menu-button" aria-label="Streaming host options" aria-expanded="false">•••</button><div class="menu"><button data-stream-action="edit">Edit streaming host</button><button data-stream-action="icon">Change icon</button><button data-stream-action="delete" class="danger-text">Delete streaming host</button></div></div></div><h2>${extendedEscape(item.name)}</h2><p class="address">Port ${item.port}</p><p class="gateway-address">→ ${extendedEscape(item.target)}</p><p class="upstream-copy ${item.enabled === false || item.upstream?.status === "unmonitored" || !item.upstream || item.upstream.status === "pending" ? "idle" : item.upstream.status === "healthy" ? "" : "bad"}">${upstream}</p><div class="card-footer"><span class="status-pill"><span class="status-dot ${status}"></span>${status === "error" ? "Needs attention" : status[0].toUpperCase() + status.slice(1)}</span><div class="card-actions">${toggle}${protocols}</div></div></article>`;
|
||||||
@@ -622,30 +622,43 @@ function renderHeroPanel(prefix, health, { sixthSlot = "throughput" } = {}) {
|
|||||||
if (!document.querySelector(`#${prefix}-${keys[0]}-value`)) return;
|
if (!document.querySelector(`#${prefix}-${keys[0]}-value`)) return;
|
||||||
if (!health) { keys.forEach(key => setHeroStat(prefix, key, { value: "\u2014", detail: "Unavailable" })); return; }
|
if (!health) { keys.forEach(key => setHeroStat(prefix, key, { value: "\u2014", detail: "Unavailable" })); return; }
|
||||||
const tone = percent => percent >= 90 ? "critical" : percent >= 75 ? "warning" : "";
|
const tone = percent => percent >= 90 ? "critical" : percent >= 75 ? "warning" : "";
|
||||||
|
// Tracks the worst tone across the resource stats (not network/uptime/throughput, which don't
|
||||||
|
// carry one) so the panel's own top accent bar can reflect it too, instead of always showing
|
||||||
|
// green regardless of whether CPU/memory/swap/disk are actually in a warning/critical state.
|
||||||
|
const toneRank = { "": 0, warning: 1, critical: 2 };
|
||||||
|
let worstTone = "";
|
||||||
|
const trackTone = value => { if (toneRank[value] > toneRank[worstTone]) worstTone = value; };
|
||||||
if (health.cpu) {
|
if (health.cpu) {
|
||||||
const quotaLabel = health.cpu.quotaSource === "quota" ? `Of ${health.cpu.quotaCpus} allocated CPU${health.cpu.quotaCpus === 1 ? "" : "s"}` : health.cpu.quotaSource === "pinned" ? `Of ${health.cpu.quotaCpus} pinned core${health.cpu.quotaCpus === 1 ? "" : "s"}` : `Of host\u2019s ${health.cpu.quotaCpus} core${health.cpu.quotaCpus === 1 ? "" : "s"} \u2014 no limit set`;
|
const quotaLabel = health.cpu.quotaSource === "quota" ? `Of ${health.cpu.quotaCpus} allocated CPU${health.cpu.quotaCpus === 1 ? "" : "s"}` : health.cpu.quotaSource === "pinned" ? `Of ${health.cpu.quotaCpus} pinned core${health.cpu.quotaCpus === 1 ? "" : "s"}` : `Of host\u2019s ${health.cpu.quotaCpus} core${health.cpu.quotaCpus === 1 ? "" : "s"} \u2014 no limit set`;
|
||||||
|
trackTone(tone(health.cpu.percent));
|
||||||
setHeroStat(prefix, "cpu", { value: `${health.cpu.percent.toFixed(1)}%`, percent: health.cpu.percent, tone: tone(health.cpu.percent), detail: quotaLabel });
|
setHeroStat(prefix, "cpu", { value: `${health.cpu.percent.toFixed(1)}%`, percent: health.cpu.percent, tone: tone(health.cpu.percent), detail: quotaLabel });
|
||||||
}
|
}
|
||||||
else setHeroStat(prefix, "cpu", { value: "\u2014", detail: "cgroup CPU stats unavailable" });
|
else setHeroStat(prefix, "cpu", { value: "\u2014", detail: "cgroup CPU stats unavailable" });
|
||||||
if (health.memory) setHeroStat(prefix, "memory", { value: `${health.memory.percent.toFixed(1)}%`, percent: health.memory.percent, tone: tone(health.memory.percent), detail: `${formatBytes(health.memory.usedBytes)} / ${formatBytes(health.memory.limitBytes)}` });
|
if (health.memory) { trackTone(tone(health.memory.percent)); setHeroStat(prefix, "memory", { value: `${health.memory.percent.toFixed(1)}%`, percent: health.memory.percent, tone: tone(health.memory.percent), detail: `${formatBytes(health.memory.usedBytes)} / ${formatBytes(health.memory.limitBytes)}` }); }
|
||||||
else setHeroStat(prefix, "memory", { value: "\u2014", detail: "cgroup memory stats unavailable" });
|
else setHeroStat(prefix, "memory", { value: "\u2014", detail: "cgroup memory stats unavailable" });
|
||||||
// Swap only gets a real percentage when the container has an actual --memory-swap limit set
|
// Swap only gets a real percentage when the container has an actual --memory-swap limit set
|
||||||
// (memory.swap.max is a real number). Without one it's unbounded and shares the host's swap,
|
// (memory.swap.max is a real number). Without one it's unbounded and shares the host's swap,
|
||||||
// so a raw "0 B" would read like a hard cap that doesn't exist -- say so instead.
|
// so a raw "0 B" would read like a hard cap that doesn't exist -- say so instead.
|
||||||
if (health.swap && health.swap.configured === false) setHeroStat(prefix, "swap", { value: "Off", percent: 0, detail: "Swap is not configured for this container" });
|
if (health.swap && health.swap.configured === false) setHeroStat(prefix, "swap", { value: "Off", percent: 0, detail: "Swap is not configured for this container" });
|
||||||
else if (health.swap && health.swap.limitBytes) setHeroStat(prefix, "swap", { value: `${health.swap.percent.toFixed(1)}%`, percent: health.swap.percent, tone: tone(health.swap.percent), detail: `${formatBytes(health.swap.usedBytes)} / ${formatBytes(health.swap.limitBytes)}` });
|
else if (health.swap && health.swap.limitBytes) { trackTone(tone(health.swap.percent)); setHeroStat(prefix, "swap", { value: `${health.swap.percent.toFixed(1)}%`, percent: health.swap.percent, tone: tone(health.swap.percent), detail: `${formatBytes(health.swap.usedBytes)} / ${formatBytes(health.swap.limitBytes)}` }); }
|
||||||
else if (health.swap) setHeroStat(prefix, "swap", { value: formatBytes(health.swap.usedBytes), percent: 0, detail: "Unlimited \u2014 shares host swap" });
|
else if (health.swap) setHeroStat(prefix, "swap", { value: formatBytes(health.swap.usedBytes), percent: 0, detail: "Unlimited \u2014 shares host swap" });
|
||||||
else setHeroStat(prefix, "swap", { value: "\u2014", detail: "cgroup swap stats unavailable" });
|
else setHeroStat(prefix, "swap", { value: "\u2014", detail: "cgroup swap stats unavailable" });
|
||||||
if (health.disk) {
|
if (health.disk) {
|
||||||
const overAssigned = health.disk.assignedLimitBytes && health.disk.percent > 100;
|
const overAssigned = health.disk.assignedLimitBytes && health.disk.percent > 100;
|
||||||
const diskDetail = health.disk.assignedLimitBytes ? `${formatBytes(health.disk.usedBytes)} used of ${formatBytes(health.disk.assignedLimitBytes)} assigned \u00b7 ${formatBytes(health.disk.availableBytes)} free on host` : `${formatBytes(health.disk.usedBytes)} used \u00b7 ${formatBytes(health.disk.availableBytes)} free`;
|
const diskDetail = health.disk.assignedLimitBytes ? `${formatBytes(health.disk.usedBytes)} used of ${formatBytes(health.disk.assignedLimitBytes)} assigned \u00b7 ${formatBytes(health.disk.availableBytes)} free on host` : `${formatBytes(health.disk.usedBytes)} used \u00b7 ${formatBytes(health.disk.availableBytes)} free`;
|
||||||
setHeroStat(prefix, "disk", { value: `${health.disk.percent.toFixed(1)}%`, percent: Math.min(100, health.disk.percent), tone: overAssigned ? "critical" : tone(health.disk.percent), detail: diskDetail });
|
const diskTone = overAssigned ? "critical" : tone(health.disk.percent);
|
||||||
|
trackTone(diskTone);
|
||||||
|
setHeroStat(prefix, "disk", { value: `${health.disk.percent.toFixed(1)}%`, percent: Math.min(100, health.disk.percent), tone: diskTone, detail: diskDetail });
|
||||||
}
|
}
|
||||||
else setHeroStat(prefix, "disk", { value: "\u2014", detail: "Disk stats unavailable" });
|
else setHeroStat(prefix, "disk", { value: "\u2014", detail: "Disk stats unavailable" });
|
||||||
if (health.network) setHeroStat(prefix, "network", { value: formatRate(health.network.rxBytesPerSec + health.network.txBytesPerSec), percent: 0, detail: `\u2193 ${formatRate(health.network.rxBytesPerSec)} \u00b7 \u2191 ${formatRate(health.network.txBytesPerSec)}` });
|
if (health.network) setHeroStat(prefix, "network", { value: formatRate(health.network.rxBytesPerSec + health.network.txBytesPerSec), percent: 0, detail: `\u2193 ${formatRate(health.network.rxBytesPerSec)} \u00b7 \u2191 ${formatRate(health.network.txBytesPerSec)}` });
|
||||||
else setHeroStat(prefix, "network", { value: "\u2014", detail: "Sampling\u2026" });
|
else setHeroStat(prefix, "network", { value: "\u2014", detail: "Sampling\u2026" });
|
||||||
if (sixthSlot === "throughput") setHeroStat(prefix, "throughput", { value: String(health.throughput?.liveRequests ?? 0), percent: 0, detail: "requests in the last minute" });
|
if (sixthSlot === "throughput") setHeroStat(prefix, "throughput", { value: String(health.throughput?.liveRequests ?? 0), percent: 0, detail: "requests in the last minute" });
|
||||||
else if (sixthSlot === "uptime") setHeroStat(prefix, "uptime", Number.isFinite(health.uptimeSeconds) ? { value: formatDuration(health.uptimeSeconds), detail: "Since last restart" } : { value: "\u2014", detail: "Unavailable" });
|
else if (sixthSlot === "uptime") setHeroStat(prefix, "uptime", Number.isFinite(health.uptimeSeconds) ? { value: formatDuration(health.uptimeSeconds), detail: "Since last restart" } : { value: "\u2014", detail: "Unavailable" });
|
||||||
|
// Reflect the worst CPU/memory/swap/disk tone on the panel's own top accent bar -- previously
|
||||||
|
// hardcoded green regardless of what the stats inside it were actually showing.
|
||||||
|
const heroPanel = document.querySelector(`#${prefix}-grid`)?.closest(".system-panel");
|
||||||
|
if (heroPanel) { heroPanel.classList.remove("tone-warning", "tone-critical"); if (worstTone) heroPanel.classList.add(`tone-${worstTone}`); }
|
||||||
}
|
}
|
||||||
// --- System tab: environment/integration status, storage, scheduled jobs, sync, restart --------
|
// --- System tab: environment/integration status, storage, scheduled jobs, sync, restart --------
|
||||||
function renderSystemPanel() {
|
function renderSystemPanel() {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16.png">
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png">
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||||
<link rel="stylesheet" href="/styles.css?v=0.16.72">
|
<link rel="stylesheet" href="/styles.css?v=0.16.75">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<!-- ================================================================
|
<!-- ================================================================
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
<button class="admin-only" data-view="administration">Admin</button><button data-view="account">Account</button><button data-view="documentation">Docs</button>
|
<button class="admin-only" data-view="administration">Admin</button><button data-view="account">Account</button><button data-view="documentation">Docs</button>
|
||||||
</nav>
|
</nav>
|
||||||
<header>
|
<header>
|
||||||
<div><p class="eyebrow">Gateway control</p><h1 id="page-title">Dashboard</h1><p id="page-subtitle" class="muted">Health, activity, and system status at a glance.</p></div>
|
<div><p class="eyebrow">Gateway control</p><h1 id="page-title">Dashboard v2</h1><p id="page-subtitle" class="muted">Health, activity, and system status at a glance.</p></div>
|
||||||
<div class="header-actions"><button id="open-create" class="button primary">+ New hosted site</button><button id="check-health" class="button primary hidden">Run certificate check</button><button id="refresh-logs" class="button primary hidden">Refresh logs</button><button id="refresh-view" class="icon-button page-refresh" aria-label="Refresh this page" title="Refresh this page">↻</button></div>
|
<div class="header-actions"><button id="open-create" class="button primary">+ New hosted site</button><button id="check-health" class="button primary hidden">Run certificate check</button><button id="refresh-logs" class="button primary hidden">Refresh logs</button><button id="refresh-view" class="icon-button page-refresh" aria-label="Refresh this page" title="Refresh this page">↻</button></div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -454,6 +454,6 @@
|
|||||||
<div id="toast" class="toast" role="status"></div>
|
<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>
|
<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) -->
|
<!-- App scripts: core (app.js) then extended views/admin (features.js) -->
|
||||||
<script src="/app.js?v=0.16.72" defer></script><script src="/features.js?v=0.16.72" defer></script><script src="/select-enhance.js?v=0.16.72" defer></script>
|
<script src="/app.js?v=0.16.75" defer></script><script src="/features.js?v=0.16.75" defer></script><script src="/select-enhance.js?v=0.16.75" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -222,7 +222,9 @@ header{align-items:flex-end}
|
|||||||
.live-dot.checking{background:var(--warning);animation-duration:.9s}
|
.live-dot.checking{background:var(--warning);animation-duration:.9s}
|
||||||
@keyframes live-pulse{0%{box-shadow:0 0 0 0 rgba(var(--green-rgb),.5)}70%{box-shadow:0 0 0 6px rgba(var(--green-rgb),0)}100%{box-shadow:0 0 0 0 rgba(var(--green-rgb),0)}}
|
@keyframes live-pulse{0%{box-shadow:0 0 0 0 rgba(var(--green-rgb),.5)}70%{box-shadow:0 0 0 6px rgba(var(--green-rgb),0)}100%{box-shadow:0 0 0 0 rgba(var(--green-rgb),0)}}
|
||||||
.system-panel{position:relative;overflow:hidden}
|
.system-panel{position:relative;overflow:hidden}
|
||||||
.system-panel::before{content:"";position:absolute;inset:0 0 auto 0;height:3px;background:var(--green);opacity:.85}
|
.system-panel::before{content:"";position:absolute;inset:0 0 auto 0;height:3px;background:var(--green);opacity:.85;transition:background .2s}
|
||||||
|
.system-panel.tone-warning::before{background:var(--warning)}
|
||||||
|
.system-panel.tone-critical::before{background:var(--danger)}
|
||||||
.system-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;margin:0}
|
.system-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;margin:0}
|
||||||
.system-tile{min-width:0;padding:13px 14px;border:1px solid var(--line);border-radius:var(--radius-md);background:rgba(var(--bg-rgb),.28)}
|
.system-tile{min-width:0;padding:13px 14px;border:1px solid var(--line);border-radius:var(--radius-md);background:rgba(var(--bg-rgb),.28)}
|
||||||
.system-grid dt{color:var(--muted);font-size:.72rem}
|
.system-grid dt{color:var(--muted);font-size:.72rem}
|
||||||
|
|||||||
Reference in New Issue
Block a user