NetworkView: API routing fix, logout button, audit trail, port/notes editor tracking

- Fix frontend API base path (VITE_API_BASE env var, GraphPage hardcoded /api)
- Add logout button to NetworkView sidebar (clears portal SSO)
- Add AuditTrail component: inline change history on all entity pages
- DB migration: add updated_at, last_edited_by to ports table
- DB migration: add notes_last_edited_by, notes_updated_at to all entity tables
- Backend: track actor on port create/update; notes editor on entity PUT
- Frontend: extend types, MarkdownEditor shows last editor, port modal/list show last editor
- Fix port CREATE TABLE definition to include new columns upfront
- Add try/catch in handleSavePort to surface API errors in modal
This commit is contained in:
ske087
2026-05-10 23:10:02 +03:00
parent 8d9df56b0b
commit 0aefadbfd8
27 changed files with 470 additions and 355 deletions
+17 -1
View File
@@ -68,7 +68,9 @@ db.exec(`
port_type TEXT DEFAULT 'RJ45',
connected_to_port_id TEXT REFERENCES ports(id) ON DELETE SET NULL,
notes TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_edited_by TEXT
);
`);
@@ -76,6 +78,20 @@ db.exec(`
try { db.exec('ALTER TABLE components ADD COLUMN port_count INTEGER DEFAULT NULL'); } catch (_) {}
try { db.exec('ALTER TABLE components ADD COLUMN sfp_count INTEGER DEFAULT NULL'); } catch (_) {}
// Migrate: port editor tracking
try { db.exec('ALTER TABLE ports ADD COLUMN updated_at DATETIME DEFAULT CURRENT_TIMESTAMP'); } catch (_) {}
try { db.exec('ALTER TABLE ports ADD COLUMN last_edited_by TEXT'); } catch (_) {}
// Migrate: notes editor tracking per entity
try { db.exec('ALTER TABLE sites ADD COLUMN notes_last_edited_by TEXT'); } catch (_) {}
try { db.exec('ALTER TABLE sites ADD COLUMN notes_updated_at DATETIME'); } catch (_) {}
try { db.exec('ALTER TABLE rooms ADD COLUMN notes_last_edited_by TEXT'); } catch (_) {}
try { db.exec('ALTER TABLE rooms ADD COLUMN notes_updated_at DATETIME'); } catch (_) {}
try { db.exec('ALTER TABLE racks ADD COLUMN notes_last_edited_by TEXT'); } catch (_) {}
try { db.exec('ALTER TABLE racks ADD COLUMN notes_updated_at DATETIME'); } catch (_) {}
try { db.exec('ALTER TABLE components ADD COLUMN notes_last_edited_by TEXT'); } catch (_) {}
try { db.exec('ALTER TABLE components ADD COLUMN notes_updated_at DATETIME'); } catch (_) {}
// ── Users & Audit tables ─────────────────────────────────────────────────────
db.exec(`
CREATE TABLE IF NOT EXISTS users (
+1 -1
View File
@@ -67,6 +67,6 @@ app.get('*', (_req, res) => {
});
});
app.listen(PORT, () => {
app.listen(PORT, '127.0.0.1', () => {
console.log(`NetworkView backend running on http://localhost:${PORT}`);
});
+10 -4
View File
@@ -126,12 +126,16 @@ router.put('/:id', (req, res) => {
}
}
const notesChanging = notes != null;
const actor = req.actor?.username || null;
db.prepare(`
UPDATE components SET
name = ?, type = ?, position = ?, height_units = ?,
manufacturer = ?, model = ?, serial_number = ?, asset_tag = ?,
ip_address = ?, mac_address = ?, status = ?, notes = ?,
port_count = ?, sfp_count = ?,
notes_last_edited_by = CASE WHEN ? = 1 THEN ? ELSE notes_last_edited_by END,
notes_updated_at = CASE WHEN ? = 1 THEN CURRENT_TIMESTAMP ELSE notes_updated_at END,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`).run(
@@ -149,6 +153,7 @@ router.put('/:id', (req, res) => {
notes ?? component.notes,
port_count !== undefined ? port_count : component.port_count,
sfp_count !== undefined ? sfp_count : component.sfp_count,
notesChanging ? 1 : 0, actor, notesChanging ? 1 : 0,
req.params.id
);
@@ -198,9 +203,9 @@ router.post('/:id/ports', (req, res) => {
const id = uuidv4();
db.prepare(`
INSERT INTO ports (id, component_id, port_number, label, port_type, connected_to_port_id, notes)
VALUES (?, ?, ?, ?, ?, ?, ?)
`).run(id, req.params.id, port_number, label || null, port_type || 'RJ45', connected_to_port_id || null, notes || null);
INSERT INTO ports (id, component_id, port_number, label, port_type, connected_to_port_id, notes, last_edited_by, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
`).run(id, req.params.id, port_number, label || null, port_type || 'RJ45', connected_to_port_id || null, notes || null, req.actor?.username || null);
// Mirror link on the other side
if (connected_to_port_id) {
@@ -225,12 +230,13 @@ router.put('/:componentId/ports/:portId', (req, res) => {
}
db.prepare(`
UPDATE ports SET label = ?, port_type = ?, connected_to_port_id = ?, notes = ? WHERE id = ?
UPDATE ports SET label = ?, port_type = ?, connected_to_port_id = ?, notes = ?, last_edited_by = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?
`).run(
label ?? port.label,
port_type ?? port.port_type,
newLinkedId,
notes ?? port.notes,
req.actor?.username || null,
req.params.portId
);
+5
View File
@@ -76,8 +76,12 @@ router.put('/:id', (req, res) => {
if (total_units != null && total_units !== rack.total_units) changes.total_units = { from: rack.total_units, to: total_units };
if (manufacturer != null && manufacturer !== rack.manufacturer) changes.manufacturer = { from: rack.manufacturer, to: manufacturer };
if (model != null && model !== rack.model) changes.model = { from: rack.model, to: model };
const notesChanging = notes != null;
const actor = req.actor?.username || null;
db.prepare(`
UPDATE racks SET name = ?, total_units = ?, manufacturer = ?, model = ?, notes = ?,
notes_last_edited_by = CASE WHEN ? = 1 THEN ? ELSE notes_last_edited_by END,
notes_updated_at = CASE WHEN ? = 1 THEN CURRENT_TIMESTAMP ELSE notes_updated_at END,
updated_at = CURRENT_TIMESTAMP WHERE id = ?
`).run(
name ?? rack.name,
@@ -85,6 +89,7 @@ router.put('/:id', (req, res) => {
manufacturer ?? rack.manufacturer,
model ?? rack.model,
notes ?? rack.notes,
notesChanging ? 1 : 0, actor, notesChanging ? 1 : 0,
req.params.id
);
+8 -2
View File
@@ -63,9 +63,15 @@ router.put('/:id', (req, res) => {
const { name, notes } = req.body;
const changes = {};
if (name != null && name !== room.name) changes.name = { from: room.name, to: name };
const notesChanging = notes != null;
const actor = req.actor?.username || null;
db.prepare(`
UPDATE rooms SET name = ?, notes = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?
`).run(name ?? room.name, notes ?? room.notes, req.params.id);
UPDATE rooms SET name = ?, notes = ?,
notes_last_edited_by = CASE WHEN ? = 1 THEN ? ELSE notes_last_edited_by END,
notes_updated_at = CASE WHEN ? = 1 THEN CURRENT_TIMESTAMP ELSE notes_updated_at END,
updated_at = CURRENT_TIMESTAMP WHERE id = ?
`).run(name ?? room.name, notes ?? room.notes,
notesChanging ? 1 : 0, actor, notesChanging ? 1 : 0, req.params.id);
logAudit(req, { action: 'update', entityType: 'room', entityId: room.id, entityName: name ?? room.name, changes });
res.json(db.prepare('SELECT * FROM rooms WHERE id = ?').get(req.params.id));
+8 -2
View File
@@ -56,10 +56,16 @@ router.put('/:id', (req, res) => {
const changes = {};
if (name != null && name !== site.name) changes.name = { from: site.name, to: name };
if (location != null && location !== site.location) changes.location = { from: site.location, to: location };
const notesChanging = notes != null;
const actor = req.actor?.username || null;
db.prepare(`
UPDATE sites SET name = ?, location = ?, notes = ?, updated_at = CURRENT_TIMESTAMP
UPDATE sites SET name = ?, location = ?, notes = ?,
notes_last_edited_by = CASE WHEN ? = 1 THEN ? ELSE notes_last_edited_by END,
notes_updated_at = CASE WHEN ? = 1 THEN CURRENT_TIMESTAMP ELSE notes_updated_at END,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`).run(name ?? site.name, location ?? site.location, notes ?? site.notes, req.params.id);
`).run(name ?? site.name, location ?? site.location, notes ?? site.notes,
notesChanging ? 1 : 0, actor, notesChanging ? 1 : 0, req.params.id);
logAudit(req, { action: 'update', entityType: 'site', entityId: site.id, entityName: name ?? site.name, changes });
res.json(db.prepare('SELECT * FROM sites WHERE id = ?').get(req.params.id));