Files
Infosec/public/logs.html
T

110 lines
4.1 KiB
HTML
Raw Normal View History

2026-09-13 20:09:20 +01:00
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Logs · Infosec</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<script src="/js/app.js"></script>
<script>
(async () => {
const shell = await buildShell('logs'); if (!shell) return;
const { content } = shell;
let meta = { actions: [], pages: [] };
content.innerHTML = `
<div class="page-head">
<h1>Logs</h1>
<p>Every action on the View Data and Data Management pages is recorded here.</p>
</div>
<div class="card">
<div class="card-head">
<h2>Audit trail</h2>
<div class="toolbar" style="margin:0">
<button class="btn" id="exportBtn">${ICON.download} Export CSV</button>
<button class="btn btn-primary" id="emailBtn">${ICON.mail} Email logs</button>
</div>
</div>
<div class="card-body">
<div class="toolbar">
<input type="search" id="q" placeholder="Search actor or detail…" style="max-width:240px">
<select id="action" style="max-width:200px"><option value="">All actions</option></select>
<select id="page" style="max-width:200px"><option value="">All pages</option></select>
<button class="btn" id="applyBtn">Filter</button>
<span class="spacer"></span>
<span class="hint" id="count"></span>
</div>
<div id="table"></div>
</div>
</div>`;
const $ = (id) => document.getElementById(id);
function params() {
const p = new URLSearchParams();
if ($('q').value.trim()) p.set('q', $('q').value.trim());
if ($('action').value) p.set('action', $('action').value);
if ($('page').value) p.set('page', $('page').value);
return p;
}
async function load() {
const data = await api('/api/logs?' + params().toString());
meta = data;
// populate selects once
if ($('action').options.length <= 1) {
$('action').innerHTML = '<option value="">All actions</option>' +
data.actions.map((a) => `<option value="${esc(a)}">${esc(a)}</option>`).join('');
$('page').innerHTML = '<option value="">All pages</option>' +
data.pages.map((p) => `<option value="${esc(p)}">${esc(p)}</option>`).join('');
}
$('count').textContent = `${data.logs.length} of ${data.total} entries`;
$('table').innerHTML = data.logs.length ? `
<table><thead><tr><th>Time</th><th>Actor</th><th>Action</th><th>Page</th><th>Detail</th><th>IP</th></tr></thead>
<tbody>${data.logs.map((l) => `<tr>
<td class="mono">${esc(fmtDateTime(l.ts))}</td>
<td>${esc(l.actor)}</td>
<td><span class="badge badge-admin">${esc(l.action)}</span></td>
<td>${esc(l.page || '—')}</td>
<td>${esc(l.detail || '')}</td>
<td class="mono">${esc(l.ip || '')}</td>
</tr>`).join('')}</tbody></table>` :
`<div class="empty">${ICON.logs}<div>No log entries match your filter.</div></div>`;
}
$('applyBtn').onclick = load;
$('q').addEventListener('keydown', (e) => { if (e.key === 'Enter') load(); });
$('action').onchange = load;
$('page').onchange = load;
$('exportBtn').onclick = () => { location.href = '/api/logs/export?' + params().toString(); };
$('emailBtn').onclick = () => {
modal('Email audit logs', `
<p>Send the currently filtered logs as a CSV attachment.</p>
<div class="field">
<label>Recipient (leave blank to send to all admins)</label>
<input type="email" id="to" placeholder="security@yourdomain.com">
</div>`, {
buttons: [{ label: 'Cancel' }, { label: 'Send', className: 'btn-primary', onClick: async (back) => {
const body = Object.fromEntries(params());
const to = back.querySelector('#to').value.trim();
if (to) body.to = to;
try {
const r = await api('/api/logs/email', { method: 'POST', body });
back.remove();
toast(r.delivered ? `Sent ${r.count} entries` : `Queued ${r.count} entries (SMTP not configured)`, 'ok');
} catch (e) { toast(e.message, 'err'); }
return false;
} }],
});
};
load();
})();
</script>
</body>
</html>