Compare commits

..

3 Commits

10 changed files with 33 additions and 9 deletions
+6
View File
@@ -264,3 +264,9 @@ Roughly in priority order:
`v0.16.69` closes a gap flagged after reviewing the ZimaOS compose file: `compose.yaml`, `compose.release.yaml`, and `compose.zimaos.yaml` all documented `DATA_DIR_LIMIT_GB` and the `mem_limit`/`cpus`/`cpuset` resource-panel options in README/install.html but never actually carried them as commented-optional entries the way the marketing site's install guide showed -- someone copying a real compose file instead of the docs page got none of that guidance. All three now include the same commented-out block (env-var style in the two static compose files, `${VAR}`-substituted and wired through `.env.example` in `compose.release.yaml`). This also folds in the marketing site's `install.html`, which had drifted out of sync with an already-updated draft and was missing the same options on the user's machine -- resynced so the published guide matches what ships in the repo.
`v0.16.70` fixes the Access Logs table (Administration > Logs, "Access requests"), the last page still on its own pre-unification CSS: `.log-table` pinned the Status and Duration columns with `position:sticky` and drew a divider `box-shadow` on each -- a horizontal-scroll affordance none of the other log-type pages use -- and left the Request column at a fixed width instead of stretching to fill the panel, so wide viewports showed an empty gap past Duration. Both are removed: the two trailing columns are back in normal table flow, and Request now takes `width:auto` to absorb the remaining space, matching how Gateway Events, Backup history, and the Audit log already size their last column. Purely a CSS change -- no markup, data, or behavior changes.
`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.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.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "site-gateway",
"version": "0.16.70",
"version": "0.16.73",
"private": true,
"description": "Site Gateway: simple self-hosted website publishing, reverse proxying, and automatic HTTPS.",
"type": "module",
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

+16 -3
View File
@@ -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() {
+6 -3
View File
@@ -7,8 +7,11 @@
<meta name="color-scheme" content="dark">
<title>Site Gateway</title>
<meta name="description" content="Host sites, proxy services, and manage HTTPS from one simple dashboard.">
<link rel="icon" type="image/png" href="/site-gateway-icon-approved.png">
<link rel="stylesheet" href="/styles.css?v=0.16.70">
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<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="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="stylesheet" href="/styles.css?v=0.16.73">
</head>
<!-- ================================================================
@@ -451,6 +454,6 @@
<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>
<!-- App scripts: core (app.js) then extended views/admin (features.js) -->
<script src="/app.js?v=0.16.70" defer></script><script src="/features.js?v=0.16.70" defer></script><script src="/select-enhance.js?v=0.16.70" defer></script>
<script src="/app.js?v=0.16.73" defer></script><script src="/features.js?v=0.16.73" defer></script><script src="/select-enhance.js?v=0.16.73" defer></script>
</body>
</html>
+3 -1
View File
@@ -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}
+1 -1
View File
@@ -2192,7 +2192,7 @@ app.post("/api/sites/:id/files", upload.single("files"), async (req, res, next)
if (!req.file) return res.status(400).json({ error: "Choose a ZIP file or index.html." });
await installUpload(site, req.file);
await syncCaddy();
recordActivity(`Files replaced for “${site.name}.`);
recordActivity(`Hosted site “${site.name}” files replaced.`);
res.json(publicSite(site));
} catch (error) { next(error); }
});