feat(api): phase 3 step 2 — topology read endpoints (list/get/status/catalog)
GET /api/v1/topologies — paginated list with status filter. Extends
repo.list_topologies() to accept limit/offset and adds count_topologies()
for the total envelope field.
GET /api/v1/topologies/{id} — hydrated TopologyDetail; 404 if missing.
GET /api/v1/topologies/{id}/status-events — audit trail, limit-capped.
Catalog helpers for the phase-4 canvas UI:
* GET /topologies/services — full service catalog.
* GET /topologies/next-subnet?base=172.20 — wraps SubnetAllocator against
reserved_subnets across non-torn-down topologies.
* GET /topologies/{id}/lans/{lan_id}/next-ip — IPAllocator pre-seeded
with existing decky IPs in that LAN.
All read routes are viewer-or-admin. Sub-routers are included in an
order that keeps literal catalog paths (/services, /next-subnet) from
being shadowed by the /{topology_id} trie branch.
This commit is contained in:
@@ -949,11 +949,18 @@ class SQLModelRepository(BaseRepository):
|
||||
return self._deserialize_json_fields(d, ("config_snapshot",))
|
||||
|
||||
async def list_topologies(
|
||||
self, status: Optional[str] = None
|
||||
self,
|
||||
status: Optional[str] = None,
|
||||
limit: Optional[int] = None,
|
||||
offset: Optional[int] = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
statement = select(Topology).order_by(desc(Topology.created_at))
|
||||
if status:
|
||||
statement = statement.where(Topology.status == status)
|
||||
if offset is not None:
|
||||
statement = statement.offset(offset)
|
||||
if limit is not None:
|
||||
statement = statement.limit(limit)
|
||||
async with self._session() as session:
|
||||
result = await session.execute(statement)
|
||||
return [
|
||||
@@ -963,6 +970,15 @@ class SQLModelRepository(BaseRepository):
|
||||
for r in result.scalars().all()
|
||||
]
|
||||
|
||||
async def count_topologies(self, status: Optional[str] = None) -> int:
|
||||
from sqlalchemy import func
|
||||
statement = select(func.count(Topology.id))
|
||||
if status:
|
||||
statement = statement.where(Topology.status == status)
|
||||
async with self._session() as session:
|
||||
result = await session.execute(statement)
|
||||
return int(result.scalar_one() or 0)
|
||||
|
||||
async def update_topology_status(
|
||||
self,
|
||||
topology_id: str,
|
||||
|
||||
Reference in New Issue
Block a user