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:
Pouzor
2026-06-27 03:13:49 +02:00
parent b3f7c38c67
commit 19b7d38ec0
10 changed files with 178 additions and 3 deletions
+22
View File
@@ -105,6 +105,28 @@ async def test_legacy_canvas_migrates_into_default_design(legacy_engine):
await engine.dispose()
async def test_legacy_nodes_gain_last_scan_column(legacy_engine):
"""A legacy nodes table (no last_scan) gains the column after init_db."""
db_path, engine = legacy_engine
await _build_legacy_schema(engine)
await database.init_db()
check = create_async_engine(f"sqlite+aiosqlite:///{db_path}")
try:
async with check.begin() as conn:
cols = (await conn.exec_driver_sql("PRAGMA table_info(nodes)")).fetchall()
assert "last_scan" in {c[1] for c in cols}
# Existing rows backfill to NULL (never scanned yet).
last_scan = (await conn.exec_driver_sql(
"SELECT last_scan FROM nodes WHERE id='n1'"
)).fetchone()
assert last_scan[0] is None
finally:
await check.dispose()
await engine.dispose()
async def test_migration_is_idempotent(legacy_engine):
"""Running init_db twice must not duplicate the design or drop any data."""
db_path, engine = legacy_engine