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:
39
server/db.js
39
server/db.js
@@ -43,6 +43,16 @@ function createSchema() {
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_instances_state ON instances(state);
|
||||
CREATE INDEX IF NOT EXISTS idx_instances_stack ON instances(stack);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS instance_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
vmid INTEGER NOT NULL,
|
||||
field TEXT NOT NULL,
|
||||
old_value TEXT,
|
||||
new_value TEXT,
|
||||
changed_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_history_vmid ON instance_history(vmid);
|
||||
`);
|
||||
}
|
||||
|
||||
@@ -99,8 +109,14 @@ export function getDistinctStacks() {
|
||||
|
||||
// ── Mutations ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const HISTORY_FIELDS = [
|
||||
'name', 'state', 'stack', 'vmid', 'tailscale_ip',
|
||||
'atlas', 'argus', 'semaphore', 'patchmon', 'tailscale', 'andromeda',
|
||||
'hardware_acceleration',
|
||||
];
|
||||
|
||||
export function createInstance(data) {
|
||||
return db.prepare(`
|
||||
db.prepare(`
|
||||
INSERT INTO instances
|
||||
(name, state, stack, vmid, atlas, argus, semaphore, patchmon,
|
||||
tailscale, andromeda, tailscale_ip, hardware_acceleration)
|
||||
@@ -108,10 +124,14 @@ export function createInstance(data) {
|
||||
(@name, @state, @stack, @vmid, @atlas, @argus, @semaphore, @patchmon,
|
||||
@tailscale, @andromeda, @tailscale_ip, @hardware_acceleration)
|
||||
`).run(data);
|
||||
db.prepare(
|
||||
`INSERT INTO instance_history (vmid, field, old_value, new_value) VALUES (?, 'created', NULL, NULL)`
|
||||
).run(data.vmid);
|
||||
}
|
||||
|
||||
export function updateInstance(vmid, data) {
|
||||
return db.prepare(`
|
||||
const old = getInstance(vmid);
|
||||
db.prepare(`
|
||||
UPDATE instances SET
|
||||
name=@name, state=@state, stack=@stack, vmid=@newVmid,
|
||||
atlas=@atlas, argus=@argus, semaphore=@semaphore, patchmon=@patchmon,
|
||||
@@ -119,6 +139,15 @@ export function updateInstance(vmid, data) {
|
||||
hardware_acceleration=@hardware_acceleration, updated_at=datetime('now')
|
||||
WHERE vmid=@vmid
|
||||
`).run({ ...data, newVmid: data.vmid, vmid });
|
||||
const newVmid = data.vmid;
|
||||
const insertEvt = db.prepare(
|
||||
`INSERT INTO instance_history (vmid, field, old_value, new_value) VALUES (?, ?, ?, ?)`
|
||||
);
|
||||
for (const field of HISTORY_FIELDS) {
|
||||
const oldVal = String(old[field] ?? '');
|
||||
const newVal = String(field === 'vmid' ? newVmid : (data[field] ?? ''));
|
||||
if (oldVal !== newVal) insertEvt.run(newVmid, field, oldVal, newVal);
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteInstance(vmid) {
|
||||
@@ -140,6 +169,12 @@ export function importInstances(rows) {
|
||||
db.exec('COMMIT');
|
||||
}
|
||||
|
||||
export function getInstanceHistory(vmid) {
|
||||
return db.prepare(
|
||||
'SELECT * FROM instance_history WHERE vmid = ? ORDER BY changed_at DESC'
|
||||
).all(vmid);
|
||||
}
|
||||
|
||||
// ── Test helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
export function _resetForTest() {
|
||||
|
||||
Reference in New Issue
Block a user