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
@@ -4,6 +4,7 @@ import * as api from '../api';
import type { Component, Port } from '../types';
import { COMPONENT_META, COMPONENT_TYPES } from '../types';
import MarkdownEditor from '../components/MarkdownEditor';
import AuditTrail from '../components/AuditTrail';
const STATUS_OPTIONS = ['active', 'maintenance', 'decommissioned'] as const;
@@ -141,21 +142,26 @@ export default function ComponentPage() {
}
}
if (portModal?.editPort) {
await api.updatePort(componentId, portModal.editPort.id, {
label: portForm.label || undefined,
port_type: portForm.port_type,
notes: portForm.notes || undefined,
connected_to_port_id: connectedToPortId,
});
} else {
await api.createPort(componentId, {
port_number: Number(portForm.port_number),
label: portForm.label || undefined,
port_type: portForm.port_type,
notes: portForm.notes || undefined,
connected_to_port_id: connectedToPortId ?? undefined,
});
try {
if (portModal?.editPort) {
await api.updatePort(componentId, portModal.editPort.id, {
label: portForm.label || undefined,
port_type: portForm.port_type,
notes: portForm.notes || undefined,
connected_to_port_id: connectedToPortId,
});
} else {
await api.createPort(componentId, {
port_number: Number(portForm.port_number),
label: portForm.label || undefined,
port_type: portForm.port_type,
notes: portForm.notes || undefined,
connected_to_port_id: connectedToPortId ?? undefined,
});
}
} catch (err: unknown) {
setPortError(err instanceof Error ? err.message : 'Failed to save port');
return;
}
setPortModal(null);
setSwLink(null);
@@ -353,6 +359,14 @@ export default function ComponentPage() {
}
</div>
)}
{port.last_edited_by && (
<div className="port-edited-by">
{port.last_edited_by}
{port.updated_at && (
<> · {new Date(port.updated_at).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}</>
)}
</div>
)}
</div>
<button className="port-edit-btn" onClick={() => openPortModal(port)} title="Edit port"></button>
<button className="port-delete" onClick={() => handleDeletePort(port.id)} title="Delete port"></button>
@@ -372,6 +386,14 @@ export default function ComponentPage() {
? `Edit ${component.type === 'patch_panel' ? 'PP' : 'SW'} Port ${portModal.editPort.port_number}`
: `Add ${component.type === 'patch_panel' ? 'PP Port' : 'SW Port'}`}
</span>
{portModal.editPort?.last_edited_by && (
<span className="modal-last-edited">
{portModal.editPort.last_edited_by}
{portModal.editPort.updated_at && (
<> · {new Date(portModal.editPort.updated_at).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}</>
)}
</span>
)}
<button className="modal-close" onClick={closePortModal}></button>
</div>
<form onSubmit={handleSavePort} style={{ padding: '20px', display: 'flex', flexDirection: 'column', gap: '16px' }}>
@@ -464,8 +486,16 @@ export default function ComponentPage() {
{/* Notes */}
<section className="content-section">
<h2 className="section-title">Documentation</h2>
<MarkdownEditor value={notes} onChange={setNotes} onSave={saveNotes} />
<MarkdownEditor
value={notes}
onChange={setNotes}
onSave={saveNotes}
lastEditedBy={component.notes_last_edited_by}
lastEditedAt={component.notes_updated_at}
/>
</section>
<AuditTrail entityId={component.id} />
</div>
);
}