diff --git a/backend/app/schemas/racks.py b/backend/app/schemas/racks.py index 2c03244..34f3bff 100644 --- a/backend/app/schemas/racks.py +++ b/backend/app/schemas/racks.py @@ -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] diff --git a/backend/tests/test_racks.py b/backend/tests/test_racks.py index 055a569..1e52421 100644 --- a/backend/tests/test_racks.py +++ b/backend/tests/test_racks.py @@ -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 ):