From 5b5ba9b3bea9736179817ab665021067e9cd35dc Mon Sep 17 00:00:00 2001 From: Marvin Wade Date: Sun, 20 Sep 2026 15:42:37 -0400 Subject: [PATCH] Wrap Audit log table in dashboard-panel card matching Backup history --- ROADMAP.md | 2 ++ package.json | 2 +- src/public/features.js | 2 +- src/public/index.html | 4 ++-- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 4c17ecc..d56fbd7 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -258,3 +258,5 @@ Roughly in priority order: `v0.16.66` replaces the generic "X updated" activity message every edit route wrote to Gateway Events -- Hosted Sites, Proxy Hosts, Redirect Hosts, and Streaming Hosts each logged one identical line regardless of what was actually changed in the save, so a rename, a domain change, a TLS toggle, and a health-check edit were all indistinguishable in the log. Each of the four `PATCH` routes now snapshots the relevant fields before applying the request body, diffs them against the saved result, and writes one comma-joined line naming exactly what changed in that save -- e.g. `Proxy host "Plex" updated — renamed from "Plex Media", target changed to 192.168.1.20:32400, TLS set to automatic.` Multiple fields changed in a single save produce one combined line, not one line per field; saving a form with no actual changes (an Edit dialog opened and immediately saved) now writes nothing at all, instead of the previous generic entry firing unconditionally. Primary fields (name, domain/aliases, target, TLS, HSTS, Access List, health-check on/off, and per-type fields like redirect code/path-preservation or streaming port/protocol) are called out individually; the long tail of advanced settings (custom headers, custom Caddy config, load-balancing upstreams/policy, upstream TLS overrides, and the fine-grained health-check parameters) are bucketed into a single "advanced settings updated" line to keep the summary readable rather than enumerating every possible field. `v0.16.67` converts the Administration -> Users -> Audit log tab from a card-row list (`.event-row` divs with a checkmark/bang `.activity-mark`) to the same pinned-header table every other log-type page now uses -- Gateway Events, Access Logs, and Backup history (v0.16.63). It was the last page still on the older list pattern, noticed immediately after v0.16.66 added several new distinct audit entries and made the mismatch obvious side by side with Gateway Events. The table has four columns -- Time, User, Result, Action -- reusing the exact `performance-table`/`event-table` CSS and the standard green/red `status-dot` convention in place of the old custom `.activity-mark` dot. No backend or data changes; `/api/audit` already returned everything the columns needed. The now-orphaned `#audit-list` div styling (border, background, max-height, the bespoke activity-mark colors) is removed along with it, since the table's own wrapper classes already provide the equivalent scrolling/sticky-header container. + +`v0.16.68` is a quick follow-on to v0.16.67: the Audit log's new table had no surrounding card, so it sat flush against the tab content instead of appearing inside the same bordered, elevated "tile" every other tab-hosted table uses -- Backup history's `dashboard-panel backup-history-section` wrapper on the Backups tab being the closest match. The Audit log's table markup is now wrapped in an equivalent `dashboard-panel audit-log-section` with an "Audit log" eyebrow above the existing heading, matching Backup history's structure exactly (search/filter row and table unchanged). Purely a wrapper/markup change -- no new CSS, no data or behavior changes. diff --git a/package.json b/package.json index 3d78887..51f79a2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "site-gateway", - "version": "0.16.67", + "version": "0.16.68", "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 6dc0ec3..3ef9720 100644 --- a/src/public/features.js +++ b/src/public/features.js @@ -140,7 +140,7 @@ function renderHealthSettings() { // --- Misc admin panel decorators, called after every refresh ------------------- function decorateAccessAssignments() { document.querySelectorAll("#access-list [data-access-id]").forEach(card => { const item = state.accessLists.find(value => value.id === card.dataset.accessId); if (!item || card.querySelector(".access-assignment-preview")) return; const assigned = [...state.proxies, ...state.sites, ...state.redirects].filter(host => host.accessListId === item.id); const preview = document.createElement("p"); preview.className = "access-assignment-preview"; preview.textContent = assigned.length ? `Protects: ${assigned.map(host => host.name || host.domain).join(" · ")}` : "Not assigned to a host"; card.querySelector(".card-footer")?.before(preview); }); } -function renderAuditPanel() { 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="audit"]'); if (!tab) { tab = document.createElement("button"); tab.dataset.adminTab = "audit"; tab.textContent = "Audit log"; tabs.insertBefore(tab, tabs.children[1]); } let panel = document.querySelector('[data-admin-panel="audit"]'); if (!panel) { panel = document.createElement("section"); panel.dataset.adminPanel = "audit"; panel.className = "settings-panel hidden"; users.parentElement.insertBefore(panel, users.nextElementSibling); } if (panel.dataset.ready) return; panel.dataset.ready = "1"; panel.innerHTML = '

Configuration audit log

A history of Site Gateway configuration changes. Audit records cannot be edited or deleted.

TimeUserResultAction
Open this tab to load audit records.
'; const load = async () => { const records = await api(`/api/audit?action=${encodeURIComponent(document.querySelector("#audit-action").value)}&status=${encodeURIComponent(document.querySelector("#audit-status").value)}`); document.querySelector("#audit-list").innerHTML = records.length ? records.map(item => { const failed = item.status === "error"; return `${extendedEscape(formatTime(item.created_at))}${extendedEscape(item.actor || "System")}${failed ? "Failed" : "Success"}${extendedEscape(item.action)}`; }).join("") : 'No matching audit records.'; }; let timer; tab.addEventListener("click", async () => { document.querySelectorAll("[data-admin-tab]").forEach(item => item.classList.toggle("tab-active", item === tab)); document.querySelectorAll("[data-admin-panel]").forEach(item => item.classList.toggle("hidden", item !== panel)); await load(); }); panel.querySelector("#audit-action").addEventListener("input", () => { clearTimeout(timer); timer = setTimeout(load, 300); }); panel.querySelector("#audit-status").addEventListener("change", load); } +function renderAuditPanel() { 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="audit"]'); if (!tab) { tab = document.createElement("button"); tab.dataset.adminTab = "audit"; tab.textContent = "Audit log"; tabs.insertBefore(tab, tabs.children[1]); } let panel = document.querySelector('[data-admin-panel="audit"]'); if (!panel) { panel = document.createElement("section"); panel.dataset.adminPanel = "audit"; panel.className = "settings-panel hidden"; users.parentElement.insertBefore(panel, users.nextElementSibling); } if (panel.dataset.ready) return; panel.dataset.ready = "1"; panel.innerHTML = '

Audit log

Configuration audit log

A history of Site Gateway configuration changes. Audit records cannot be edited or deleted.

TimeUserResultAction
Open this tab to load audit records.
'; const load = async () => { const records = await api(`/api/audit?action=${encodeURIComponent(document.querySelector("#audit-action").value)}&status=${encodeURIComponent(document.querySelector("#audit-status").value)}`); document.querySelector("#audit-list").innerHTML = records.length ? records.map(item => { const failed = item.status === "error"; return `${extendedEscape(formatTime(item.created_at))}${extendedEscape(item.actor || "System")}${failed ? "Failed" : "Success"}${extendedEscape(item.action)}`; }).join("") : 'No matching audit records.'; }; let timer; tab.addEventListener("click", async () => { document.querySelectorAll("[data-admin-tab]").forEach(item => item.classList.toggle("tab-active", item === tab)); document.querySelectorAll("[data-admin-panel]").forEach(item => item.classList.toggle("hidden", item !== panel)); await load(); }); panel.querySelector("#audit-action").addEventListener("input", () => { clearTimeout(timer); timer = setTimeout(load, 300); }); panel.querySelector("#audit-status").addEventListener("change", load); } function hideRestrictedControls() { if (state.user?.role !== "viewer") return; document.querySelectorAll("#access-list .menu-wrap, #redirect-list .menu-wrap, #stream-list .menu-wrap, #access-list [data-access-action=toggle], #redirect-list [data-redirect-action=toggle], #stream-list [data-stream-action=toggle], .create-trigger, #open-create, #create-backup, #import-backup").forEach(element => { element.classList.add("hidden"); element.setAttribute("aria-hidden", "true"); }); } function renderRetentionPanel() { 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="retention"]'); if (!tab) { tab = document.createElement("button"); tab.dataset.adminTab = "retention"; tab.textContent = "Logs & retention"; tabs.append(tab); } let panel = document.querySelector('[data-admin-panel="retention"]'); if (!panel) { panel = document.createElement("section"); panel.dataset.adminPanel = "retention"; panel.className = "settings-panel hidden"; users.parentElement.append(panel); } const policy = state.settings?.logsRetention || { accessDays:30, activityDays:90, auditDays:365, certificateDays:365, securityDays:365, pruningEnabled:false }; panel.innerHTML = `

Logs & retention

Choose how long Site Gateway keeps operational and administrative records. Pruning is disabled until you enable it.

`; panel.querySelector("form").addEventListener("submit", async event => { event.preventDefault(); const form = new FormData(event.target); try { const updated = await api("/api/settings", { method:"PATCH", headers:{"Content-Type":"application/json"}, body:JSON.stringify({ logsRetention:{ accessDays:Number(form.get("accessDays")), activityDays:Number(form.get("activityDays")), auditDays:Number(form.get("auditDays")), certificateDays:Number(form.get("certificateDays")), securityDays:Number(form.get("securityDays")), pruningEnabled:form.has("pruningEnabled") } }) }); state.settings = updated; toast("Log retention policy saved."); } catch (error) { toast(error.message); } }); } // The single entry point app.js calls after every refresh() to re-render every diff --git a/src/public/index.html b/src/public/index.html index 358e7ff..eed300d 100644 --- a/src/public/index.html +++ b/src/public/index.html @@ -8,7 +8,7 @@ Site Gateway - + - +