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:
committed by
Pouzor - Rémy Jardient
parent
e22152d2d3
commit
c263f839a8
@@ -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]
|
||||
|
||||
@@ -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
|
||||
):
|
||||
|
||||
Reference in New Issue
Block a user