feat: surface inventory timestamps on nodes
Add creation_date, last_scan and last_modify to the node inventory and the right-hand detail panel (last_seen already shown): - creation_date -> existing created_at column - last_modify -> existing updated_at column (bumped on any node change) - last_scan -> new column, stamped when a scan observes a node by IP/MAC - last_seen -> unchanged Backend: new nodes.last_scan column + idempotent migration, NodeResponse field, scanner stamps matching canvas nodes per scanned host. Frontend: NodeData fields + DetailPanel rows with UTC-safe timestamp formatting. ha-relevant: maybe
This commit is contained in:
@@ -14,6 +14,13 @@ interface DetailPanelProps {
|
||||
onEdit: (id: string) => void
|
||||
}
|
||||
|
||||
// Backend timestamps may arrive without a timezone suffix (naive UTC). Append
|
||||
// 'Z' when absent so the browser parses them as UTC rather than local time.
|
||||
function formatTimestamp(value: string): string {
|
||||
const iso = /[Zz]|[+-]\d{2}:?\d{2}$/.test(value) ? value : value + 'Z'
|
||||
return new Date(iso).toLocaleString()
|
||||
}
|
||||
|
||||
type SvcForm = { port: string; protocol: 'tcp' | 'udp'; service_name: string; path: string }
|
||||
const EMPTY_FORM: SvcForm = { port: '', protocol: 'tcp', service_name: '', path: '' }
|
||||
|
||||
@@ -252,7 +259,10 @@ export function DetailPanel({ onEdit }: DetailPanelProps) {
|
||||
{data.mac && <DetailRow label="MAC" value={data.mac} mono />}
|
||||
{data.os && <DetailRow label="OS" value={data.os} />}
|
||||
{data.check_method && <DetailRow label="Check" value={data.check_method} mono />}
|
||||
{data.last_seen && <DetailRow label="Last Seen" value={new Date(/[Zz]|[+-]\d{2}:?\d{2}$/.test(data.last_seen) ? data.last_seen : data.last_seen + 'Z').toLocaleString()} />}
|
||||
{data.last_seen && <DetailRow label="Last Seen" value={formatTimestamp(data.last_seen)} />}
|
||||
{data.last_scan && <DetailRow label="Last Scan" value={formatTimestamp(data.last_scan)} />}
|
||||
{data.created_at && <DetailRow label="Created" value={formatTimestamp(data.created_at)} />}
|
||||
{data.updated_at && <DetailRow label="Last Modified" value={formatTimestamp(data.updated_at)} />}
|
||||
</div>
|
||||
|
||||
{/* Size section — manual width/height entry for pixel-exact sizing */}
|
||||
|
||||
@@ -212,6 +212,37 @@ describe('DetailPanel', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Inventory timestamps', () => {
|
||||
it('renders Created, Last Scan and Last Modified rows when present', () => {
|
||||
setupStore({
|
||||
created_at: '2026-01-02T10:00:00Z',
|
||||
last_scan: '2026-06-01T08:30:00Z',
|
||||
updated_at: '2026-06-20T12:00:00Z',
|
||||
last_seen: '2026-06-25T09:15:00Z',
|
||||
})
|
||||
render(<DetailPanel onEdit={vi.fn()} />)
|
||||
expect(screen.getByText('Created')).toBeInTheDocument()
|
||||
expect(screen.getByText('Last Scan')).toBeInTheDocument()
|
||||
expect(screen.getByText('Last Modified')).toBeInTheDocument()
|
||||
expect(screen.getByText('Last Seen')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('omits rows whose timestamps are absent', () => {
|
||||
setupStore({ created_at: '2026-01-02T10:00:00Z' })
|
||||
render(<DetailPanel onEdit={vi.fn()} />)
|
||||
expect(screen.getByText('Created')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Last Scan')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('Last Modified')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('parses naive (suffix-less) UTC timestamps without throwing', () => {
|
||||
setupStore({ created_at: '2026-01-02 10:00:00' })
|
||||
render(<DetailPanel onEdit={vi.fn()} />)
|
||||
// Renders a locale string for the date (year is locale-independent).
|
||||
expect(screen.getByText(/2026/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Panel actions', () => {
|
||||
it('calls setSelectedNode(null) when close button is clicked', () => {
|
||||
const setSelectedNode = vi.fn()
|
||||
|
||||
@@ -102,6 +102,9 @@ export interface NodeData extends Record<string, unknown> {
|
||||
check_target?: string
|
||||
services: ServiceInfo[]
|
||||
last_seen?: string
|
||||
last_scan?: string
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
response_time_ms?: number
|
||||
notes?: string
|
||||
cpu_count?: number
|
||||
|
||||
Reference in New Issue
Block a user