feat: audit log / history timeline on instance detail page
Adds an instance_history table that records every field change: - createInstance logs a 'created' event - updateInstance diffs old vs new and logs one row per changed field (name, state, stack, vmid, tailscale_ip, all service flags) - History is stored under the new vmid when vmid changes New endpoint: GET /api/instances/:vmid/history The 'timestamps' section on the detail page is replaced with a grid timeline showing timestamp | field | old → new for each event. State changes are colour-coded (deployed=green, testing=amber, degraded=red). Boolean service flags display as on/off. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
5
js/db.js
5
js/db.js
@@ -55,3 +55,8 @@ async function updateInstance(vmid, data) {
|
||||
async function deleteInstance(vmid) {
|
||||
await api(`/instances/${vmid}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
async function getInstanceHistory(vmid) {
|
||||
const res = await fetch(`${BASE}/instances/${vmid}/history`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
39
js/ui.js
39
js/ui.js
@@ -99,8 +99,21 @@ async function filterInstances() {
|
||||
|
||||
// ── Detail Page ───────────────────────────────────────────────────────────────
|
||||
|
||||
const BOOL_FIELDS = ['atlas','argus','semaphore','patchmon','tailscale','andromeda','hardware_acceleration'];
|
||||
|
||||
function stateClass(field, val) {
|
||||
if (field !== 'state') return '';
|
||||
return { deployed: 'tl-deployed', testing: 'tl-testing', degraded: 'tl-degraded' }[val] ?? '';
|
||||
}
|
||||
|
||||
function fmtHistVal(field, val) {
|
||||
if (val == null || val === '') return '—';
|
||||
if (BOOL_FIELDS.includes(field)) return val === '1' ? 'on' : 'off';
|
||||
return esc(val);
|
||||
}
|
||||
|
||||
async function renderDetailPage(vmid) {
|
||||
const inst = await getInstance(vmid);
|
||||
const [inst, history] = await Promise.all([getInstance(vmid), getInstanceHistory(vmid)]);
|
||||
if (!inst) { navigate('dashboard'); return; }
|
||||
currentVmid = vmid;
|
||||
|
||||
@@ -133,10 +146,26 @@ async function renderDetailPage(vmid) {
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
document.getElementById('detail-timestamps').innerHTML = `
|
||||
<div class="kv-row"><span class="kv-key">created</span><span class="kv-val">${fmtDateFull(inst.created_at)}</span></div>
|
||||
<div class="kv-row"><span class="kv-key">updated</span><span class="kv-val">${fmtDateFull(inst.updated_at)}</span></div>
|
||||
`;
|
||||
document.getElementById('detail-timestamps').innerHTML = history.length
|
||||
? history.map(e => {
|
||||
if (e.field === 'created') return `
|
||||
<div class="tl-item tl-created">
|
||||
<span class="tl-time">${fmtDateFull(e.changed_at)}</span>
|
||||
<span class="tl-field">created</span>
|
||||
<span class="tl-change">—</span>
|
||||
</div>`;
|
||||
return `
|
||||
<div class="tl-item">
|
||||
<span class="tl-time">${fmtDateFull(e.changed_at)}</span>
|
||||
<span class="tl-field">${esc(e.field)}</span>
|
||||
<span class="tl-change">
|
||||
<span class="tl-old">${fmtHistVal(e.field, e.old_value)}</span>
|
||||
<span class="tl-arrow">→</span>
|
||||
<span class="tl-new ${stateClass(e.field, e.new_value)}">${fmtHistVal(e.field, e.new_value)}</span>
|
||||
</span>
|
||||
</div>`;
|
||||
}).join('')
|
||||
: '<div class="tl-empty">no history yet</div>';
|
||||
|
||||
document.getElementById('detail-edit-btn').onclick = () => openEditModal(inst.vmid);
|
||||
document.getElementById('detail-delete-btn').onclick = () => confirmDeleteDialog(inst);
|
||||
|
||||
Reference in New Issue
Block a user