Compare commits

...

2 Commits

4 changed files with 7 additions and 7 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "site-gateway",
"version": "0.11.95",
"version": "0.11.96",
"private": true,
"description": "Site Gateway: simple self-hosted website publishing, reverse proxying, and automatic HTTPS.",
"type": "module",
+3 -3
View File
@@ -219,9 +219,9 @@ function normalizeRetentionLayout() { const form = document.querySelector('[data
normalizeRetentionLayout();
function cleanRetentionLabels() { const form = document.querySelector('[data-admin-panel="retention"] .retention-form'); if (!form) return; const descriptions = { 'Access logs':'High-volume request records.', 'Gateway activity':'Operational and configuration events.', 'Audit logs':'Administrative accountability records.', 'Certificate events':'Certificate issuance and health changes.', 'Security events':'Authentication and security-related events.' }; [...form.querySelectorAll('label:not(.check-control)')].forEach(field => { const text = field.firstChild; const name = text?.textContent?.trim().replace(/ \(days\)$/, ''); if (!text || !descriptions[name]) return; if (!text.textContent.includes('(days)')) text.textContent = `${name} (days)`; let help = field.querySelector('small'); if (!help) { help = document.createElement('small'); field.append(help); } help.textContent = descriptions[name]; }); }
setTimeout(() => { cleanRetentionLabels(); normalizeRetentionLayout(); }, 0); setInterval(() => { cleanRetentionLabels(); normalizeRetentionLayout(); }, 300);
function renderRetentionRunStatus() { const panel = document.querySelector('[data-admin-panel="retention"]'); const form = panel?.querySelector('.retention-form'); if (!panel || !form) return; const value = state.settings?.logsRetention?.lastRunAt ? state.settings.logsRetention : null; let status = panel.querySelector('.retention-run-status'); if (!status) { status = document.createElement('div'); status.className = 'retention-run-status muted'; const actions = form.querySelector('.dialog-actions'); if (actions) actions.before(status); else form.append(status); } status.textContent = value ? `Last run: ${value.lastRunMode || 'manual'} · ${new Date(value.lastRunAt).toLocaleString()} · Snapshot: ${value.lastRunSnapshot || 'available'}` : 'No pruning run yet.'; }
async function renderRetentionPreview() { const panel = document.querySelector('[data-admin-panel="retention"]'); if (!panel) return; let preview = panel.querySelector('.retention-preview'); if (!preview) { preview = document.createElement('div'); preview.className = 'retention-preview muted'; const form = panel.querySelector('.retention-form'); const status = panel.querySelector('.retention-run-status'); (status || form)?.before(preview); } try { const data = await api('/api/logs/prune/preview'); const counts = data.counts || {}; const total = Object.values(counts).reduce((sum, value) => sum + Number(value || 0), 0); preview.textContent = data.enabled ? `Eligible to prune: ${total} records · Access ${counts.access || 0} · Activity ${counts.activity || 0} · Certificates ${counts.certificate || 0} · Security ${counts.security || 0} · Audit ${counts.audit || 0}` : 'Pruning is disabled. Enable automatic pruning to preview eligible records.'; } catch { preview.textContent = 'Prune preview unavailable.'; } }
async function renderRetentionHistory() { const panel = document.querySelector('[data-admin-panel="retention"]'); if (!panel) return; let history = panel.querySelector('.retention-history'); if (!history) { history = document.createElement('div'); history.className = 'retention-history'; (panel.querySelector('.retention-run-status') || panel.querySelector('.retention-form'))?.after(history); } try { const rows = (await api('/api/audit?action=pruning')).filter(item => /pruning/i.test(item.action)).slice(0, 50); history.innerHTML = `<div class="retention-history-heading"><strong>Prune history</strong><span>${rows.length} runs</span></div>` + (rows.length ? `<div class="retention-history-list">${rows.map(item => `<div class="retention-history-row"><span class="status-dot ${item.status === 'error' ? 'disabled' : 'running'}"></span><span><strong>${extendedEscape(item.action)}</strong><small>${extendedEscape(item.actor || 'System')} · ${extendedEscape(item.status === 'error' ? 'Failed' : 'Success')} · ${extendedEscape(formatTime(item.created_at))}</small></span></div>`).join('')}</div>` : '<p class="muted">No pruning runs recorded yet.</p>'); } catch { history.innerHTML = '<p class="muted">Prune history unavailable.</p>'; } }
function renderRetentionRunStatus() { if (!state.user) return; const panel = document.querySelector('[data-admin-panel="retention"]'); const form = panel?.querySelector('.retention-form'); if (!panel || !form) return; const value = state.settings?.logsRetention?.lastRunAt ? state.settings.logsRetention : null; let status = panel.querySelector('.retention-run-status'); if (!status) { status = document.createElement('div'); status.className = 'retention-run-status muted'; const actions = form.querySelector('.dialog-actions'); if (actions) actions.before(status); else form.append(status); } status.textContent = value ? `Last run: ${value.lastRunMode || 'manual'} · ${new Date(value.lastRunAt).toLocaleString()} · Snapshot: ${value.lastRunSnapshot || 'available'}` : 'No pruning run yet.'; }
async function renderRetentionPreview() { if (!state.user) return; const panel = document.querySelector('[data-admin-panel="retention"]'); if (!panel) return; let preview = panel.querySelector('.retention-preview'); if (!preview) { preview = document.createElement('div'); preview.className = 'retention-preview muted'; const form = panel.querySelector('.retention-form'); const status = panel.querySelector('.retention-run-status'); (status || form)?.before(preview); } try { const data = await api('/api/logs/prune/preview'); const counts = data.counts || {}; const total = Object.values(counts).reduce((sum, value) => sum + Number(value || 0), 0); preview.textContent = data.enabled ? `Eligible to prune: ${total} records · Access ${counts.access || 0} · Activity ${counts.activity || 0} · Certificates ${counts.certificate || 0} · Security ${counts.security || 0} · Audit ${counts.audit || 0}` : 'Pruning is disabled. Enable automatic pruning to preview eligible records.'; } catch { preview.textContent = 'Prune preview unavailable.'; } }
async function renderRetentionHistory() { if (!state.user) return; const panel = document.querySelector('[data-admin-panel="retention"]'); if (!panel) return; let history = panel.querySelector('.retention-history'); if (!history) { history = document.createElement('div'); history.className = 'retention-history'; (panel.querySelector('.retention-run-status') || panel.querySelector('.retention-form'))?.after(history); } try { const rows = (await api('/api/audit?action=pruning')).filter(item => /pruning/i.test(item.action)).slice(0, 50); history.innerHTML = `<div class="retention-history-heading"><strong>Prune history</strong><span>${rows.length} runs</span></div>` + (rows.length ? `<div class="retention-history-list">${rows.map(item => `<div class="retention-history-row"><span class="status-dot ${item.status === 'error' ? 'disabled' : 'running'}"></span><span><strong>${extendedEscape(item.action)}</strong><small>${extendedEscape(item.actor || 'System')} · ${extendedEscape(item.status === 'error' ? 'Failed' : 'Success')} · ${extendedEscape(formatTime(item.created_at))}</small></span></div>`).join('')}</div>` : '<p class="muted">No pruning runs recorded yet.</p>'); } catch { history.innerHTML = '<p class="muted">Prune history unavailable.</p>'; } }
function ensureRetentionLoadMore() { const history = document.querySelector('.retention-history'); if (!history || history.querySelector('[data-retention-load-more]')) return; const button = document.createElement('button'); button.className = 'text-button retention-load-more'; button.dataset.retentionLoadMore = 'true'; button.textContent = 'Load more'; history.append(button); }
document.addEventListener('click', async event => { const button = event.target.closest('[data-retention-load-more]'); if (!button) return; try { const rows = (await api('/api/audit?action=pruning')).filter(item => /pruning/i.test(item.action)).slice(50); const list = button.parentElement.querySelector('.retention-history-list'); rows.forEach(item => { const row = document.createElement('div'); row.className = 'retention-history-row'; row.innerHTML = `<span class="status-dot ${item.status === 'error' ? 'disabled' : 'running'}"></span><span><strong>${extendedEscape(item.action)}</strong><small>${extendedEscape(item.actor || 'System')} · ${extendedEscape(item.status === 'error' ? 'Failed' : 'Success')} · ${extendedEscape(formatTime(item.created_at))}</small></span>`; list?.append(row); }); button.remove(); } catch { button.textContent = 'History unavailable'; } });
function normalizeRetentionActions() { const form = document.querySelector('[data-admin-panel="retention"] .retention-form'); const actions = form?.querySelector('.dialog-actions'); const section = form?.querySelector('.form-section'); if (form && actions && section && actions.previousElementSibling !== section) section.after(actions); }
+2 -2
View File
@@ -7,7 +7,7 @@
<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.11.95">
<link rel="stylesheet" href="/styles.css?v=0.11.96">
</head>
<body>
<div id="login" class="login-shell hidden">
@@ -370,6 +370,6 @@
<input id="replace-files" type="file" accept=".zip,.html,text/html,application/zip" hidden>
<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>
<script src="/app.js?v=0.11.89" defer></script><script src="/features.js?v=0.11.95" defer></script>
<script src="/app.js?v=0.11.89" defer></script><script src="/features.js?v=0.11.97" defer></script>
</body>
</html>
+1 -1
View File
@@ -223,7 +223,7 @@ select{appearance:none!important;-webkit-appearance:none!important;background-re
#audit-list .activity-mark{width:8px;height:8px;min-width:8px;border-radius:50%;padding:0;font-size:0;background:var(--green);box-shadow:0 0 0 4px rgba(98,230,167,.1)}
#audit-list .activity-mark.bad{background:var(--danger);box-shadow:0 0 0 4px rgba(255,113,133,.09)}
.diagnostic-section-heading{margin:20px 0 10px;padding:18px 20px;border:1px solid var(--line);border-bottom:0;border-radius:15px 15px 0 0;background:var(--panel)}
.diagnostic-section-heading .eyebrow{margin-bottom:5px}.diagnostic-section-heading h2{margin:0;font-size:1.15rem}.diagnostic-section-heading p:last-child{margin:4px 0 0;font-size:.76rem}
.diagnostic-section-heading .eyebrow{margin-bottom:5px}.diagnostic-section-heading h2{margin:0;font-size:1.15rem}.diagnostic-section-heading p:last-child{margin:4px 0 0}
.diagnostic-section-heading{margin-bottom:0}
.diagnostic-section-heading + .log-table-wrap{border-top:0;border-radius:0 0 15px 15px}
#certificate-list.diagnostic-list,#readiness-list.diagnostic-list,#gateway-log-list.diagnostic-list,#audit-list.diagnostic-list{border-radius:0 0 15px 15px}