feat(13-03): add link service, schemas, router, and wire subscribers into lifespan

- LinkResponse/UnknownClientResponse Pydantic schemas with from_attributes
- Link service with get_links, get_device_links, get_site_links, get_unknown_clients
- Unknown clients query uses DISTINCT ON for latest registration per MAC
- 4 REST endpoints: tenant links, device links, site links, unknown clients
- Interface and link discovery subscribers wired into FastAPI lifespan start/stop
- Links router registered at /api prefix

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jason Staack
2026-03-19 06:12:06 -05:00
parent 3209a7d9be
commit 0434d31030
4 changed files with 357 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
"""Pydantic schemas for Link and Unknown Client endpoints."""
import uuid
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, ConfigDict
class LinkResponse(BaseModel):
"""Single wireless link between an AP and CPE device."""
id: uuid.UUID
ap_device_id: uuid.UUID
cpe_device_id: uuid.UUID
ap_hostname: str | None = None
cpe_hostname: str | None = None
interface: str | None = None
client_mac: str
signal_strength: int | None = None
tx_ccq: int | None = None
tx_rate: str | None = None
rx_rate: str | None = None
state: str
missed_polls: int
discovered_at: datetime
last_seen: datetime
model_config = ConfigDict(from_attributes=True)
class LinkListResponse(BaseModel):
"""List of wireless links with total count."""
items: list[LinkResponse]
total: int
class UnknownClientResponse(BaseModel):
"""A wireless client whose MAC does not resolve to any known device interface."""
mac_address: str
interface: str | None = None
signal_strength: int | None = None
tx_rate: str | None = None
rx_rate: str | None = None
last_seen: datetime
model_config = ConfigDict(from_attributes=True)
class UnknownClientListResponse(BaseModel):
"""List of unknown clients with total count."""
items: list[UnknownClientResponse]
total: int