fix(rack): stamp the linked node's last-seen with an offset

SQLite has no timezone type: an aware `last_seen` reads back naive, and
Pydantic then put it on the wire as `2026-08-08T10:00:00`. To the browser
that is *local* time, so the Logical view panel printed a timestamp
shifted by the viewer's UTC offset.

ha-relevant: no
This commit is contained in:
Pouzor
2026-08-10 00:23:10 +02:00
committed by Pouzor - Rémy Jardient
parent e22152d2d3
commit c263f839a8
2 changed files with 41 additions and 1 deletions
+15 -1
View File
@@ -1,4 +1,4 @@
from datetime import datetime
from datetime import datetime, timezone
from typing import Any
from pydantic import BaseModel, field_validator, model_validator
@@ -283,6 +283,20 @@ class RackInventoryItem(BaseModel):
# True when this device is already mounted somewhere in this design.
racked: bool = False
@field_validator("node_last_seen")
@classmethod
def _utc(cls, v: datetime | None) -> datetime | None:
"""Stamp UTC on a naive timestamp before it goes on the wire.
SQLite has no timezone type: an aware datetime written to `last_seen`
reads back naive, and Pydantic then serializes it without an offset.
`new Date("2026-08-08T10:00:00")` in the browser is *local* time, so the
rack printed a last-seen shifted by the viewer's offset.
"""
if v is not None and v.tzinfo is None:
return v.replace(tzinfo=timezone.utc)
return v
class RackInventoryResponse(BaseModel):
items: list[RackInventoryItem]
+26
View File
@@ -1,5 +1,6 @@
import pytest
from httpx import AsyncClient
from sqlalchemy import text
from app.db.models import PendingDevice
@@ -512,6 +513,31 @@ class TestInventory:
assert entry["node_design_id"] == network
assert entry["node_design_name"] == "Network"
async def test_stamps_last_seen_with_an_offset(self, client: AsyncClient, headers, db_session):
"""SQLite reads `last_seen` back naive, and a naive ISO string is *local*
time to `new Date()` — the panel printed it shifted by the viewer's offset.
"""
design_id = await _design(client, headers)
node = (
await client.post(
"/api/v1/nodes",
json={"type": "server", "label": "seen", "ip": "192.168.1.77"},
headers=headers,
)
).json()
db_session.add(PendingDevice(id="pd-seen", ip="192.168.1.77", suggested_type="server"))
await db_session.execute(
text("UPDATE nodes SET last_seen = :ts WHERE id = :id"),
{"ts": "2026-08-08 10:00:00.000000", "id": node["id"]},
)
await db_session.commit()
items = (
await client.get(f"/api/v1/racks/inventory?design_id={design_id}", headers=headers)
).json()["items"]
entry = next(i for i in items if i["id"] == "pd-seen")
assert entry["node_last_seen"].endswith(("Z", "+00:00"))
async def test_leaves_the_node_fields_empty_without_a_match(
self, client: AsyncClient, headers
):