Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/admin-trust-automation-919.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"nostream": minor
---

Add admin trust and automation: Redis-backed audit log API and UI, signed HTTP webhooks for probe/mirror/SSL events, OpenTelemetry probe and mirror metrics, and redacted diagnostics export.
215 changes: 215 additions & 0 deletions resources/admin/assets/admin-trust.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
(() => {
const config = window.__ADMIN_DASHBOARD__ || { pathPrefix: '' }
const adminBase = `${config.pathPrefix || ''}/admin`

const auditBody = document.getElementById('trust-audit-body')
if (!auditBody) {
return
}

const elements = {
auditSync: document.getElementById('trust-audit-sync'),
auditRefresh: document.getElementById('trust-audit-refresh'),
auditBody,
diagnosticsDownload: document.getElementById('trust-diagnostics-download'),
diagnosticsPreviewButton: document.getElementById('trust-diagnostics-preview-button'),
diagnosticsPreviewWrap: document.getElementById('trust-diagnostics-preview-wrap'),
diagnosticsPreviewClose: document.getElementById('trust-diagnostics-preview-close'),
diagnosticsPreviewPlaceholder: document.getElementById('trust-diagnostics-preview-placeholder'),
diagnosticsPreviewBody: document.getElementById('trust-diagnostics-preview-body'),
diagnosticsStatus: document.getElementById('trust-diagnostics-status'),
error: document.getElementById('trust-error'),
}

const escapeHtml = (value) =>
String(value)
.replaceAll('&', '&')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;')

const formatTime = (iso) => {
if (!iso) {
return '—'
}
const date = new Date(iso)
if (Number.isNaN(date.getTime())) {
return iso
}
return date.toISOString().replace('T', ' ').replace(/\.\d{3}Z$/, ' UTC')
}

const adminFetch = async (path, options = {}) => {
const response = await fetch(`${adminBase}${path}`, {
credentials: 'same-origin',
...options,
})
if (response.status === 401) {
throw new Error('Unauthorized')
}
return response
}

const showError = (message) => {
if (!elements.error) {
return
}
elements.error.textContent = message
elements.error.classList.remove('d-none')
}

const hideError = () => {
elements.error?.classList.add('d-none')
}

const handleUnauthorized = () => {
globalThis.dispatchEvent(new CustomEvent('admin-unauthorized'))
}

const handleFetchError = (error, fallbackMessage) => {
if (error instanceof Error && error.message === 'Unauthorized') {
handleUnauthorized()
return
}
showError(fallbackMessage)
}

const renderAuditEntries = (entries) => {
elements.auditBody.replaceChildren()
if (!Array.isArray(entries) || entries.length === 0) {
elements.auditBody.innerHTML = '<tr><td colspan="4" class="admin-muted small">No audit entries yet.</td></tr>'
return
}

entries.forEach((entry) => {
const row = document.createElement('tr')
const detail =
entry.detail && Object.keys(entry.detail).length > 0
? `<code class="small">${escapeHtml(JSON.stringify(entry.detail))}</code>`
: '—'
row.innerHTML = `
<td>${escapeHtml(formatTime(entry.at))}</td>
<td><code>${escapeHtml(entry.action ?? '')}</code></td>
<td>${escapeHtml(entry.remoteAddress ?? '—')}</td>
<td>${detail}</td>
`
elements.auditBody.appendChild(row)
})
}

const loadAuditLog = async () => {
hideError()
if (elements.auditSync) {
elements.auditSync.textContent = '> audit: loading…'
}

try {
const response = await adminFetch('/audit-log?limit=100')
if (!response.ok) {
showError('Failed to load audit log')
if (elements.auditSync) {
elements.auditSync.textContent = '> audit: load failed'
}
return
}

const body = await response.json()
renderAuditEntries(body.entries ?? [])
if (elements.auditSync) {
elements.auditSync.textContent = `> audit: ${body.entries?.length ?? 0} entries`
}
} catch (error) {
handleFetchError(error, 'Network error while loading audit log')
}
}

const downloadDiagnostics = async () => {
hideError()
if (elements.diagnosticsStatus) {
elements.diagnosticsStatus.textContent = 'Preparing export…'
}

try {
const response = await adminFetch('/diagnostics/export?download=1')
if (!response.ok) {
showError('Diagnostics export failed')
return
}

const blob = await response.blob()
const url = URL.createObjectURL(blob)
const anchor = document.createElement('a')
anchor.href = url
anchor.download = `nostream-diagnostics-${new Date().toISOString().replace(/[:.]/g, '-')}.json`
anchor.click()
URL.revokeObjectURL(url)
if (elements.diagnosticsStatus) {
elements.diagnosticsStatus.textContent = 'Download started (secrets redacted).'
}
} catch (error) {
handleFetchError(error, 'Network error during diagnostics export')
}
}

const closeDiagnosticsPreview = () => {
elements.diagnosticsPreviewWrap?.classList.add('d-none')
elements.diagnosticsPreviewPlaceholder?.classList.remove('d-none')
if (elements.diagnosticsPreviewBody) {
elements.diagnosticsPreviewBody.textContent = ''
}
if (elements.diagnosticsStatus) {
elements.diagnosticsStatus.innerHTML =
'<span class="prompt-char">&gt;</span> diagnostics: idle'
}
}

const showDiagnosticsPreview = (payload) => {
elements.diagnosticsPreviewPlaceholder?.classList.add('d-none')
elements.diagnosticsPreviewWrap?.classList.remove('d-none')
if (elements.diagnosticsPreviewBody) {
elements.diagnosticsPreviewBody.textContent = JSON.stringify(payload, null, 2)
}
if (elements.diagnosticsStatus) {
elements.diagnosticsStatus.textContent = `Generated ${payload.generatedAt ?? ''} · version ${payload.version ?? 'unknown'}`
}
}

const previewDiagnostics = async () => {
hideError()
if (elements.diagnosticsStatus) {
elements.diagnosticsStatus.textContent = 'Loading preview…'
}

try {
const response = await adminFetch('/diagnostics/export')
if (!response.ok) {
showError('Diagnostics preview failed')
return
}

const payload = await response.json()
showDiagnosticsPreview(payload)
} catch (error) {
handleFetchError(error, 'Network error during diagnostics preview')
}
}

elements.auditRefresh?.addEventListener('click', () => {
void loadAuditLog()
})
elements.diagnosticsDownload?.addEventListener('click', () => {
void downloadDiagnostics()
})
elements.diagnosticsPreviewButton?.addEventListener('click', () => {
void previewDiagnostics()
})
elements.diagnosticsPreviewClose?.addEventListener('click', () => {
closeDiagnosticsPreview()
})

window.__ADMIN_TRUST__ = {
load: loadAuditLog,
closeDiagnosticsPreview,
}
})()
73 changes: 72 additions & 1 deletion resources/admin/assets/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
const networkHealthRunAt = document.getElementById('network-health-run-at')
const networkHealthTimelineWrap = document.getElementById('network-health-timeline-wrap')
const networkHealthTimeline = document.getElementById('network-health-timeline')
const networkHealthRunButton = document.getElementById('network-health-run-button')

let settingsLoaded = false
let settingsLoading = false
Expand Down Expand Up @@ -220,11 +221,22 @@
streamStatus.appendChild(document.createTextNode(label))
}

const updateSystemAlert = (snapshot) => {
let activeDashboardView = 'metrics-view'

const hideSystemAlert = () => {
if (!systemAlert) {
return
}

systemAlert.className = 'system-alert d-none'
systemAlert.textContent = ''
}

const updateSystemAlert = (snapshot) => {
if (!systemAlert || activeDashboardView !== 'metrics-view') {
return
Comment thread
Ferryx349 marked this conversation as resolved.
}

if (lastSnapshot?.timestamp && Date.now() - lastSnapshot.timestamp > staleThresholdMs) {
systemAlert.textContent = '[WARN] Metrics stale — waiting for backend update.'
systemAlert.className = 'system-alert degraded'
Expand Down Expand Up @@ -359,6 +371,7 @@
}

const showDashboard = () => {
activeDashboardView = 'metrics-view'
loginPanel.classList.add('d-none')
dashboardPanel.classList.remove('d-none')
dashboardActions.classList.remove('d-none')
Expand Down Expand Up @@ -725,6 +738,42 @@
})
}

const runNetworkHealthProbe = async () => {
if (!networkHealthRunButton) {
return
}

networkHealthRunButton.disabled = true
setNetworkHealthSyncLine('manual probe running…')

try {
const response = await fetch(`${adminBase}/network-health/run`, {
method: 'POST',
credentials: 'include',
})

if (response.status === 401) {
showLogin()
return
}

if (!response.ok) {
setNetworkHealthSyncLine('manual probe failed')
return
}

networkHealthHistoryPinned = false
selectedNetworkHealthRunAt = null
await refreshNetworkHealth()
await refreshNetworkHealthHistory()
setNetworkHealthSyncLine('manual probe completed')
} catch {
setNetworkHealthSyncLine('network error during manual probe')
} finally {
networkHealthRunButton.disabled = false
}
}

const refreshNetworkHealth = async () => {
try {
const response = await fetch(`${adminBase}/network-health`, {
Expand Down Expand Up @@ -1476,6 +1525,11 @@
}

const showView = (viewId) => {
activeDashboardView = viewId
if (viewId !== 'metrics-view') {
hideSystemAlert()
}

dashboardViews.forEach((view) => {
view.classList.toggle('d-none', view.id !== viewId)
})
Expand Down Expand Up @@ -1509,6 +1563,14 @@
return
}

if (viewId === 'trust-view') {
stopNetworkHealthPolling()
stopMetricsStream()
window.__ADMIN_OPS__?.stop?.()
window.__ADMIN_TRUST__?.load?.()
return
}

if (viewId === 'metrics-view') {
stopNetworkHealthPolling()
window.__ADMIN_OPS__?.stop?.()
Expand Down Expand Up @@ -1797,13 +1859,22 @@
void loadSettingsData(true)
})

networkHealthRunButton?.addEventListener('click', () => {
void runNetworkHealthProbe()
})

if (liveClock) {
liveClock.textContent = formatClock()
setInterval(() => {
liveClock.textContent = formatClock()
}, 1000)
}

document.addEventListener('admin-unauthorized', () => {
window.__ADMIN_TRUST__?.closeDiagnosticsPreview?.()
showLogin()
})

applyTheme(getTheme())
checkSession()
})()
Loading
Loading