diff --git a/README.md b/README.md
index ae6172e..54daaea 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@
-
+
Why Site Gateway ยท @@ -94,6 +94,7 @@ Automatic HTTPS requires valid public DNS and inbound access to port 80 or 443. | `ADMIN_PORT` | `8080` | Dashboard port inside the container | | `SITE_PORT_MIN` / `SITE_PORT_MAX` | `9000` / `9099` | Direct-LAN port range Hosted Sites can bind to | | `DATA_DIR` | `/data` | Persistent state location | +| `DATA_DIR_LIMIT_GB` | empty | Optional display-only allowance for the System tab's Disk stat (e.g. a smaller dedicated share); usage/free space still come from the real volume | | `BACKUP_PASSWORD` | empty | Encryption password used only when encrypted scheduled backups are enabled | | `PUID` / `PGID` | `1000` / `1000` | User/group the container writes files as (Unraid: `99`/`100`) | | `ACME_EMAIL` | empty | Optional certificate account email | diff --git a/ROADMAP.md b/ROADMAP.md index 448dce6..388e96a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -186,3 +186,5 @@ Roughly in priority order: `v0.16.29` adds a hero panel to the top of the Administration > System tab -- a single, visually distinct "one-stop shop" for this container's live CPU, memory, swap, disk, and network numbers, plus request throughput, all in one place instead of scattered across the plain status tiles below it. CPU, memory, and swap all read directly from this container's own cgroup v2 files (`cpu.stat`'s `usage_usec`, `memory.current`/`memory.max`, `memory.swap.current`/`memory.swap.max`) rather than host-level figures, on the same reasoning already settled for this feature: Site Gateway is rarely the only thing running on the host, so a host-wide number would be misleading in a dashboard scoped to one container. CPU percent is computed from two samples of the cumulative `usage_usec` counter taken a poll apart, normalized against `cpu.max`'s quota when one is set (or the host's core count when it isn't); memory and swap read straight off their `.current`/`.max` pairs, with swap showing "Off" rather than a stale percentage when the container has none configured. Disk reuses the same `statfs`-on-the-data-volume approach the System tab's storage breakdown already used. Network throughput is new: since `/sys/class/net/*/statistics/{rx_bytes,tx_bytes}` are cumulative counters too, a background sampler reads every non-loopback interface every 5 seconds and keeps a rolling rate in memory, so the hero panel always shows a real, smoothed rate rather than a lifetime total or a jittery two-reads-per-request estimate. Request throughput reuses the exact number already shown on the main Dashboard ("requests in the last minute"), so the two stay in sync without duplicating the underlying query. Each stat degrades independently and visibly rather than silently: a metric with no readable source (cgroup v1 hosts, a sandboxed `/sys/class/net`, and so on) shows a dash and a one-line explanation instead of a wrong number or a blank space, and CPU/memory/swap/disk fills turn amber past 75% and red past 90%, matching the color language already used elsewhere in the app for degraded/warning states. `v0.16.30` fixes two numbers on the System tab's new hero panel (added in v0.16.29) that were technically correct but meant the wrong thing. CPU percent was always computed against either a real Docker `--cpus` quota or, absent one, the *host's total core count* -- so pinning the container to 2 specific cores (`--cpuset-cpus`, Unraid's CPU pinning field) didn't change the denominator at all, since pinning caps which cores can run without capping how much of them can be used, and `cpu.max` stays `max` either way. CPU now checks `cpuset.cpus.effective` (the actual pinned core list, correctly counting ranges like `0-1,4`) whenever there's no real quota, and the hero panel's detail line now says which denominator applies -- "Of N allocated CPUs" for a real `--cpus` quota, "Of N pinned cores" for cpuset pinning with no quota, or "Of host's N cores -- no limit set" when neither is configured -- instead of always claiming "Of this container's CPU quota" even when there wasn't one. Swap had a similar honesty problem: without an explicit `--memory-swap` limit, `memory.swap.max` reads `max` (unbounded, shared with the host's swap) rather than "0," but the panel showed a bare "0 B" that read like a real, enforced cap. It now only shows a percentage when a real swap limit exists; otherwise it shows the actual bytes in use with "Unlimited -- shares host swap" instead of implying a limit that was never set. + +`v0.16.31` adds the two remaining items from the System tab hero panel's fix list. First, the panel now keeps itself current while you're actually looking at it: a lightweight timer polls `/api/system/health` directly every 7 seconds whenever the System tab is the visible admin panel, separate from the app's full `refresh()` (which also refetches sites, proxies, certificates, and everything else) so it stays cheap on a fast interval, and it's a no-op the moment you navigate away rather than continuing to poll in the background. Previously the hero panel only updated on initial page load or whenever *anything else* in the app happened to trigger a `refresh()` -- sitting on the tab watching it did nothing. Second, a new `DATA_DIR_LIMIT_GB` environment variable lets an operator tell the Disk stat what's actually assigned to this deployment -- a dedicated share or zvol smaller than the whole host volume, for instance -- instead of always showing usage against the full underlying filesystem size. This is necessarily display-only, since Docker has no real per-container disk-space quota the way it does for CPU (`cpu.max`) or memory (`memory.max`); actual usage and free space still come straight from `statfs` on the real volume, only the percentage's denominator and the "used of X assigned" label change. Set past 100%, the stat turns red rather than silently capping, since exceeding an assigned allowance is a real, meaningful warning rather than a display bug. diff --git a/package.json b/package.json index f51e0ab..b7d2d3c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "site-gateway", - "version": "0.16.30", + "version": "0.16.31", "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 03dfffa..ad664a4 100644 --- a/src/public/features.js +++ b/src/public/features.js @@ -600,7 +600,11 @@ function renderSystemHealthHero(health) { else if (health.swap && health.swap.limitBytes) setHeroStat("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("swap", { value: formatBytes(health.swap.usedBytes), percent: 0, detail: "Unlimited \u2014 shares host swap" }); else setHeroStat("swap", { value: "\u2014", detail: "cgroup swap stats unavailable" }); - if (health.disk) setHeroStat("disk", { value: `${health.disk.percent.toFixed(1)}%`, percent: health.disk.percent, tone: tone(health.disk.percent), detail: `${formatBytes(health.disk.usedBytes)} used \u00b7 ${formatBytes(health.disk.availableBytes)} free` }); + 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` : `${formatBytes(health.disk.usedBytes)} used \u00b7 ${formatBytes(health.disk.availableBytes)} free`; + setHeroStat("disk", { value: `${health.disk.percent.toFixed(1)}%`, percent: Math.min(100, health.disk.percent), tone: overAssigned ? "critical" : tone(health.disk.percent), detail: diskDetail }); + } else setHeroStat("disk", { value: "\u2014", detail: "Disk stats unavailable" }); if (health.network) setHeroStat("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("network", { value: "\u2014", detail: "Sampling\u2026" }); @@ -664,6 +668,16 @@ function renderSystemPanel() { } catch (error) { toast(error.message, "error"); button.disabled = false; button.textContent = "Restart application"; } }); + // Keep the hero panel's live numbers current while the System tab is actually visible -- + // a lightweight direct poll of /api/system/health, not a full refresh() (which also + // refetches sites/proxies/certificates/etc.), so it stays cheap even on a fast interval. + // Stops itself from doing any work (skips the fetch) once the tab isn't in view, mirroring + // the guard the Dashboard's own health timer already uses for the same reason. + if (!state.systemHealthTimer) state.systemHealthTimer = setInterval(() => { + const systemPanel = document.querySelector('[data-admin-panel="system"]'); + if (state.view !== "administration" || !systemPanel || systemPanel.classList.contains("hidden")) return; + api("/api/system/health").then(renderSystemHealthHero).catch(() => {}); + }, 7000); } renderSystemStatus(panel); } diff --git a/src/public/index.html b/src/public/index.html index 1530bda..4d69cc0 100644 --- a/src/public/index.html +++ b/src/public/index.html @@ -8,7 +8,7 @@