From 9c1426d276227ea12c48b5e138ce5f4745e45875 Mon Sep 17 00:00:00 2001 From: marvin Date: Fri, 18 Sep 2026 15:39:58 -0400 Subject: [PATCH] Add System tab; fix settings-revert bug, encryption-status messaging, JS caching, native dropdown theming, and backup panel presentation (v0.16.0) --- README.md | 2 +- ROADMAP.md | 17 +++++ package.json | 2 +- src/public/features.js | 81 +++++++++++++++++++++-- src/public/index.html | 14 ++-- src/public/select-enhance.js | 121 +++++++++++++++++++++++++++++++++++ src/public/styles.css | 17 +++++ src/server.js | 64 +++++++++++++++++- 8 files changed, 303 insertions(+), 15 deletions(-) create mode 100644 src/public/select-enhance.js diff --git a/README.md b/README.md index 1190741..72197b6 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Docker Architectures Caddy - Version + Version

Why Site Gateway · diff --git a/ROADMAP.md b/ROADMAP.md index 9df37ff..c96f1da 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -34,6 +34,21 @@ - The Top Paths popout now states it's showing the top 10, matching the existing server-side cap. - The Runtime/System dashboard panel's top accent bar changed from a stray `--blue` token to `--green`, matching the default accent already used by every other dashboard tile. +`v0.15.2` fixed regressions introduced by `v0.15.1` and one deeper architectural bug: + +- The dashboard attention tile's inline "Resync now" button (added in `v0.15.1`) silently did nothing — a script-generation guard meant to avoid double-adding its click handler matched on markup text that had already been introduced by the same change, so the handler was never actually attached. Fixed and verified by checking for the handler's functional code rather than just a string match. +- The Needs Attention dashboard chip and the attention-tile detail rows used different colors (amber vs. red) for the same condition; aligned to red. +- Configuration drift kept re-reporting immediately after a successful resync. The `v0.15.1` fix (order-independent JSON comparison) was necessary but not sufficient — the deeper issue was comparing a live running config against a freshly re-adapted Caddyfile, which will almost never match because Caddy fills in runtime defaults (automation policy, TLS management state) that never appear in a bare adapted config. Rewrote drift detection to compare two live-config snapshots against a captured baseline instead, recapturing that baseline after every successful sync. +- Removed the redundant "Resync now" callout from the Gateway Defaults page, superseded by the dashboard's inline button. + +`v0.16.0` adds the System tab and closes out a round of fixes found during live use of `v0.15.x`: + +- **New System tab** (Administration, first tab) — a read-only operations/diagnostics page: environment and integration status (Docker socket, `BACKUP_PASSWORD`), security status (default-credential and `ACME_EMAIL` detection), persistent gateway sync status with a Resync control, a scheduled-jobs table, per-folder storage usage, version/runtime info, and Reload/Restart controls. Restart is only enabled when the Docker socket is mounted and the container's own restart policy (checked via the Docker Engine API) is `always`, `unless-stopped`, or `on-failure`. The only interactive elements on the page are the Docker container-picker toggle (moved here from Gateway Defaults, which no longer carries integration/environment content) and the action buttons — everything else is status. +- Fixed a real correctness bug: `PATCH /api/settings` called `syncCaddy()` unconditionally before saving anything, for every settings change — including backups, certificate-health, and log-retention changes that have nothing to do with the Caddy config. An unrelated Caddy resync failure could silently discard and revert a just-saved change before it was ever persisted. `syncCaddy()` now only runs when a `defaultSite` change is part of the request; everything else saves unconditionally. +- The "Encrypt scheduled backups" toggle's helper text now positively confirms when `BACKUP_PASSWORD` is configured, instead of showing the same generic instructional copy regardless of whether it's set. +- `app.js`/`features.js`/`select-enhance.js` are now served with `Cache-Control: no-cache`, so browsers always revalidate instead of potentially serving a stale cached copy despite the version query string. +- Native `' + message + ''; @@ -446,7 +446,7 @@ async function renderBackupHistory() { // saved value, so the integration can never be switched on without its prerequisite. function renderDockerPanel() { if (state.user?.role !== "administrator") return; - const panel = document.querySelector('[data-admin-panel="defaults"]'); if (!panel) return; + const panel = document.querySelector('[data-admin-panel="system"] .system-integrations'); if (!panel) return; const socketMounted = state.config?.docker?.socketMounted === true; const enabled = socketMounted && (state.settings?.dockerIntegration?.enabled === true || state.config?.docker?.enabled === true); let section = panel.querySelector(".docker-integration-section"); @@ -524,11 +524,84 @@ document.addEventListener("click", async event => { }); +// --- System tab: environment/integration status, storage, scheduled jobs, sync, restart -------- +function renderSystemPanel() { + if (state.user?.role !== "administrator") return; + const tabs = document.querySelector(".admin-tabs"), users = document.querySelector('[data-admin-panel="users"]'); + if (!tabs || !users) return; + let tab = tabs.querySelector('[data-admin-tab="system"]'); + if (!tab) { tab = document.createElement("button"); tab.dataset.adminTab = "system"; tab.textContent = "System"; tabs.insertBefore(tab, tabs.firstChild); } + let panel = document.querySelector('[data-admin-panel="system"]'); + if (!panel) { panel = document.createElement("section"); panel.dataset.adminPanel = "system"; panel.className = "settings-panel hidden"; users.parentElement.insertBefore(panel, users); } + if (!panel.dataset.ready) { + panel.dataset.ready = "1"; + panel.innerHTML = [ + '

System

What\u2019s configured, what\u2019s running, and what this deployment can do. Nothing here is customizable except the Docker toggle below and the action buttons \u2014 everything else is status.

', + '

Environment

Integrations

', + '

Environment

Security status

', + '

Gateway

Sync

', + '

Operations

Scheduled jobs

', + '

Storage

Disk usage

', + '

Build

Version

', + '

Gateway

Reload & restart

Reloading re-applies the current configuration to Caddy with no downtime. Restarting stops and restarts the whole application \u2014 only available when a restart policy is set on the container.

', + ].join(""); + panel.querySelector("#system-resync").addEventListener("click", async event => { + const button = event.currentTarget; button.disabled = true; const original = button.textContent; button.textContent = "Resyncing\u2026"; + try { await api("/api/gateway/resync", { method: "POST" }); toast("Gateway configuration re-synced."); await refresh(); } + catch (error) { toast(error.message, "error"); } + finally { button.disabled = false; button.textContent = original; } + }); + panel.querySelector("#system-reload").addEventListener("click", async event => { + const button = event.currentTarget; button.disabled = true; const original = button.textContent; button.textContent = "Reloading\u2026"; + try { await api("/api/system/reload", { method: "POST" }); toast("Gateway configuration reloaded."); await refresh(); } + catch (error) { toast(error.message, "error"); } + finally { button.disabled = false; button.textContent = original; } + }); + panel.querySelector("#system-restart").addEventListener("click", async event => { + if (!await themedConfirm("Restart Site Gateway?", "The application will stop and restart. This takes a few seconds and briefly interrupts hosted sites and the dashboard.", "Restart")) return; + const button = event.currentTarget; button.disabled = true; button.textContent = "Restarting\u2026"; + try { await api("/api/system/restart", { method: "POST" }); toast("Restarting \u2014 this dashboard will be unavailable briefly."); } + catch (error) { toast(error.message, "error"); button.disabled = false; button.textContent = "Restart application"; } + }); + } + renderSystemStatus(panel); +} +async function renderSystemStatus(panel) { + panel = panel || document.querySelector('[data-admin-panel="system"]'); + if (!panel || panel.classList.contains("hidden")) return; + const security = document.querySelector("#system-security"), storage = document.querySelector("#system-storage"), + version = document.querySelector("#system-version"), jobs = document.querySelector("#system-jobs"), + syncStatus = document.querySelector("#system-sync-status"), restartButton = document.querySelector("#system-restart"), + restartStatus = document.querySelector("#system-restart-status"); + if (jobs) jobs.innerHTML = (state.dashboard?.jobs || []).map(job => `
${extendedEscape(job.name)}${job.enabled ? `Active \u00b7 ${extendedEscape(job.schedule)}` : "Disabled"}
`).join("") || '

No scheduled jobs reported.

'; + if (syncStatus) { const drift = (state.dashboard?.attention || []).some(item => item.kind === "drift"); syncStatus.textContent = drift ? "Configuration drift detected \u2014 the running gateway no longer matches the last known-good configuration." : `Gateway configuration is in sync. Last reload: ${state.dashboard?.gateway?.lastReload ? formatTime(state.dashboard.gateway.lastReload) : "unknown"}.`; syncStatus.className = drift ? "muted status-warning" : "muted"; } + if (version) version.innerHTML = `Site Gateway v${extendedEscape(state.config?.version || "unknown")}
Data directory: ${extendedEscape(state.config?.storage?.databasePath ? state.config.storage.databasePath.replace(/\/database\/.*/, "") : "/data")} · Admin port: ${extendedEscape(String(state.config?.adminPort ?? ""))} · Site ports: ${extendedEscape(String(state.config?.minPort ?? ""))}\u2013${extendedEscape(String(state.config?.maxPort ?? ""))}`; + try { + const [sec, store, policy] = await Promise.all([ + api("/api/system/security"), + api("/api/system/storage"), + api("/api/system/restart-policy"), + ]); + if (security) security.innerHTML = [ + { ok: !sec.adminPasswordIsDefault, label: "ADMIN_PASSWORD", detail: sec.adminPasswordIsDefault ? "Still using the built-in default \u2014 set this before exposing the dashboard." : "Configured." }, + { ok: !sec.sessionSecretIsDefault, label: "SESSION_SECRET", detail: sec.sessionSecretIsDefault ? "Not set \u2014 sessions are keyed off the admin credentials instead of an independent secret." : "Configured." }, + { ok: sec.acmeEmailConfigured, label: "ACME_EMAIL", detail: sec.acmeEmailConfigured ? "Configured." : "Not set \u2014 certificate issuance will proceed without a registration contact." }, + ].map(row => `
${row.label}${row.detail}
`).join(""); + if (storage) { + const rows = Object.entries(store.breakdown || {}).map(([key, bytes]) => `
${key[0].toUpperCase()}${key.slice(1)}${formatBytes(bytes)}
`).join(""); + const capacity = store.capacity ? `
Disk${formatBytes(store.capacity.availableBytes)} free of ${formatBytes(store.capacity.totalBytes)}
` : ""; + storage.innerHTML = rows + capacity || '

Storage usage unavailable.

'; + } + if (restartButton) { restartButton.disabled = !policy.restartAvailable; if (restartStatus) restartStatus.textContent = policy.reason || (policy.policyName ? `Restart policy: ${policy.policyName}.` : ""); } + } catch { /* Status widgets keep their last-known values if a refresh call fails. */ } +} + // --- Wire the new panels into the shared refresh entry point ---------------------------------- const baseRenderExtendedViews = window.renderExtendedViews; window.renderExtendedViews = function () { baseRenderExtendedViews(); renderApiTokensPanel(); + renderSystemPanel(); renderDockerPanel(); decorateContainerPickers(); renderBackupHistory(); diff --git a/src/public/index.html b/src/public/index.html index 51abc88..5fb145e 100644 --- a/src/public/index.html +++ b/src/public/index.html @@ -8,7 +8,7 @@ Site Gateway - + + - + @@ -313,7 +315,7 @@ - +