diff --git a/ROADMAP.md b/ROADMAP.md index 773148b..4a0de67 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -268,3 +268,5 @@ 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 "".` line instead of the `Hosted site "" ...` 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 "" 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 `` 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 `` 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. diff --git a/package.json b/package.json index a57aa82..92cd17a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "site-gateway", - "version": "0.16.72", + "version": "0.16.73", "private": true, "description": "Site Gateway: simple self-hosted website publishing, reverse proxying, and automatic HTTPS.", "type": "module", diff --git a/src/public/features.js b/src/public/features.js index 3ef9720..5ccd36d 100644 --- a/src/public/features.js +++ b/src/public/features.js @@ -622,30 +622,43 @@ function renderHeroPanel(prefix, health, { sixthSlot = "throughput" } = {}) { if (!document.querySelector(`#${prefix}-${keys[0]}-value`)) return; if (!health) { keys.forEach(key => setHeroStat(prefix, key, { value: "\u2014", detail: "Unavailable" })); return; } 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) { 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 }); } 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" }); // 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, // 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" }); - 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 setHeroStat(prefix, "swap", { value: "\u2014", detail: "cgroup swap stats unavailable" }); if (health.disk) { 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`; - 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" }); 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" }); 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" }); + // 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 -------- function renderSystemPanel() { diff --git a/src/public/index.html b/src/public/index.html index 228341c..34512fa 100644 --- a/src/public/index.html +++ b/src/public/index.html @@ -11,7 +11,7 @@ - + - + diff --git a/src/public/styles.css b/src/public/styles.css index 543fbd4..6b3754a 100644 --- a/src/public/styles.css +++ b/src/public/styles.css @@ -222,7 +222,9 @@ header{align-items:flex-end} .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)}} .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-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}