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:
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import {
|
||||
_resetForTest,
|
||||
getInstances, getInstance, getDistinctStacks,
|
||||
createInstance, updateInstance, deleteInstance, importInstances,
|
||||
createInstance, updateInstance, deleteInstance, importInstances, getInstanceHistory,
|
||||
} from '../server/db.js'
|
||||
|
||||
beforeEach(() => _resetForTest());
|
||||
@@ -185,6 +185,43 @@ describe('importInstances', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── instance history ─────────────────────────────────────────────────────────
|
||||
|
||||
describe('instance history', () => {
|
||||
const base = { state: 'deployed', stack: 'production', atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 };
|
||||
|
||||
it('logs a created event when an instance is created', () => {
|
||||
createInstance({ ...base, name: 'a', vmid: 1 });
|
||||
const h = getInstanceHistory(1);
|
||||
expect(h).toHaveLength(1);
|
||||
expect(h[0].field).toBe('created');
|
||||
});
|
||||
|
||||
it('logs changed fields when an instance is updated', () => {
|
||||
createInstance({ ...base, name: 'a', vmid: 1 });
|
||||
updateInstance(1, { ...base, name: 'a', vmid: 1, state: 'degraded' });
|
||||
const h = getInstanceHistory(1);
|
||||
const stateEvt = h.find(e => e.field === 'state');
|
||||
expect(stateEvt).toBeDefined();
|
||||
expect(stateEvt.old_value).toBe('deployed');
|
||||
expect(stateEvt.new_value).toBe('degraded');
|
||||
});
|
||||
|
||||
it('logs no events when nothing changes on update', () => {
|
||||
createInstance({ ...base, name: 'a', vmid: 1 });
|
||||
updateInstance(1, { ...base, name: 'a', vmid: 1 });
|
||||
const h = getInstanceHistory(1).filter(e => e.field !== 'created');
|
||||
expect(h).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('records history under the new vmid when vmid changes', () => {
|
||||
createInstance({ ...base, name: 'a', vmid: 1 });
|
||||
updateInstance(1, { ...base, name: 'a', vmid: 2 });
|
||||
expect(getInstanceHistory(2).some(e => e.field === 'vmid')).toBe(true);
|
||||
expect(getInstanceHistory(1).filter(e => e.field !== 'created')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Test environment boot isolation ───────────────────────────────────────────
|
||||
|
||||
describe('test environment boot isolation', () => {
|
||||
|
||||
Reference in New Issue
Block a user