From 3a57d809a4e84e3afefe912c0cf0e9dc47fd8b4a Mon Sep 17 00:00:00 2001 From: Pranjal Joshi Date: Sun, 31 May 2026 14:44:17 +0530 Subject: [PATCH] fix: compatibility with multi-design schema for scan/zigbee/liveview/CustomStyleModal - liveview.py: replace hardcoded CanvasState PK lookup (get(1)) with design_id-aware query; filter nodes/edges by design_id - scan.py: add design_id to bulk approve, single approve, and edge resolution Node/Edge constructors (fallback to first design) - zigbee.py: add design_id to coordinator auto-approval Node constructor - schemas/nodes.py: add design_id to NodeCreate and NodeResponse - schemas/edges.py: add design_id to EdgeCreate - CustomStyleModal.tsx: add 'electrical' to EDITABLE_EDGE_TYPES --- backend/app/api/routes/liveview.py | 15 ++++++++++---- backend/app/api/routes/scan.py | 20 ++++++++++++++++++- backend/app/api/routes/zigbee.py | 7 ++++++- backend/app/schemas/edges.py | 2 +- backend/app/schemas/nodes.py | 4 +++- .../components/modals/CustomStyleModal.tsx | 2 +- 6 files changed, 41 insertions(+), 9 deletions(-) diff --git a/backend/app/api/routes/liveview.py b/backend/app/api/routes/liveview.py index c6cd0f1..137b2b9 100644 --- a/backend/app/api/routes/liveview.py +++ b/backend/app/api/routes/liveview.py @@ -7,7 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.core.config import settings from app.db.database import get_db -from app.db.models import CanvasState, Edge, Node +from app.db.models import CanvasState, Design, Edge, Node from app.schemas.canvas import CanvasStateResponse from app.schemas.edges import EdgeResponse from app.schemas.nodes import NodeResponse @@ -18,6 +18,7 @@ router = APIRouter() @router.get("", response_model=CanvasStateResponse) async def liveview_canvas( key: str | None = Query(default=None), + design_id: str | None = Query(default=None, description="Design to show; uses first if omitted"), db: AsyncSession = Depends(get_db), ) -> CanvasStateResponse: """Read-only public canvas endpoint. @@ -30,9 +31,15 @@ async def liveview_canvas( if not key or not hmac.compare_digest(key, settings.liveview_key): raise HTTPException(status_code=403, detail="Invalid live view key") - nodes = (await db.execute(select(Node))).scalars().all() - edges = (await db.execute(select(Edge))).scalars().all() - state = await db.get(CanvasState, 1) + if design_id is None: + first = (await db.execute(select(Design).order_by(Design.created_at).limit(1))).scalar() + design_id = first.id if first else None + if design_id is None: + return CanvasStateResponse(nodes=[], edges=[], viewport={"x": 0, "y": 0, "zoom": 1}, custom_style=None) + + nodes = (await db.execute(select(Node).where(Node.design_id == design_id))).scalars().all() + edges = (await db.execute(select(Edge).where(Edge.design_id == design_id))).scalars().all() + state = await db.get(CanvasState, design_id) viewport: dict[str, Any] = state.viewport if state else {"x": 0, "y": 0, "zoom": 1} custom_style: dict[str, Any] | None = state.custom_style if state else None return CanvasStateResponse( diff --git a/backend/app/api/routes/scan.py b/backend/app/api/routes/scan.py index 33c03df..aaa4c8f 100644 --- a/backend/app/api/routes/scan.py +++ b/backend/app/api/routes/scan.py @@ -11,7 +11,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.api.deps import get_current_user from app.core.config import settings from app.db.database import AsyncSessionLocal, get_db -from app.db.models import Edge, Node, PendingDevice, PendingDeviceLink, ScanRun +from app.db.models import Design, Edge, Node, PendingDevice, PendingDeviceLink, ScanRun from app.schemas.nodes import NodeCreate from app.schemas.scan import PendingDeviceResponse, ScanRunResponse from app.services.scanner import request_cancel, run_scan @@ -118,6 +118,10 @@ async def bulk_approve_devices( db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user), ) -> dict[str, Any]: + # Determine target design (use first design as fallback) + first_design = (await db.execute(select(Design).order_by(Design.created_at).limit(1))).scalar() + default_design_id = first_design.id if first_design else None + result = await db.execute( select(PendingDevice).where( PendingDevice.id.in_(payload.device_ids), @@ -144,6 +148,7 @@ async def bulk_approve_devices( # Default to ping so the status checker actually polls the new node. # Without this the scheduler skips it (check_method NULL → no check). check_method="none" if is_zigbee else ("ping" if device.ip else None), + design_id=default_design_id, ) db.add(node) created_nodes.append(node) @@ -227,6 +232,12 @@ async def approve_device( db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user), ) -> dict[str, Any]: + # Determine target design + node_design_id = node_data.design_id + if node_design_id is None: + first = (await db.execute(select(Design).order_by(Design.created_at).limit(1))).scalar() + node_design_id = first.id if first else None + device = await db.get(PendingDevice, device_id) if not device: raise HTTPException(status_code=404, detail="Device not found") @@ -247,6 +258,7 @@ async def approve_device( ) if _is_zigbee else (node_data.properties or []), check_method="none" if _is_zigbee else (node_data.check_method or ("ping" if node_data.ip else None)), check_target=None if _is_zigbee else node_data.check_target, + design_id=node_design_id, ) db.add(node) await db.flush() @@ -328,12 +340,18 @@ async def _resolve_pending_links_for_ieee( if (src_id, tgt_id) in existing_pairs or (tgt_id, src_id) in existing_pairs: await db.delete(link) continue + # Use the source node's design_id for the edge + edge_design_id = self_node.design_id if self_node else None + if edge_design_id is None: + first = (await db.execute(select(Design).order_by(Design.created_at).limit(1))).scalar() + edge_design_id = first.id if first else None edge = Edge( source=src_id, target=tgt_id, type="iot", source_handle="bottom", target_handle="top-t", + design_id=edge_design_id, ) db.add(edge) await db.flush() diff --git a/backend/app/api/routes/zigbee.py b/backend/app/api/routes/zigbee.py index 1379d23..5a4ee8c 100644 --- a/backend/app/api/routes/zigbee.py +++ b/backend/app/api/routes/zigbee.py @@ -11,7 +11,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.api.deps import get_current_user from app.db.database import AsyncSessionLocal, get_db -from app.db.models import Node, PendingDevice, PendingDeviceLink, ScanRun +from app.db.models import Design, Node, PendingDevice, PendingDeviceLink, ScanRun from app.schemas.scan import ScanRunResponse from app.schemas.zigbee import ( ZigbeeCoordinatorOut, @@ -138,6 +138,10 @@ async def _persist_pending_import( Coordinator auto-approves to a canvas Node. Other devices upsert by IEEE. All zigbee-source links are wiped and re-inserted from the new map. """ + # Determine target design (use first design as fallback) + first_design = (await db.execute(select(Design).order_by(Design.created_at).limit(1))).scalar() + default_design_id = first_design.id if first_design else None + coordinator_out: ZigbeeCoordinatorOut | None = None coordinator_existed = False pending_created = 0 @@ -174,6 +178,7 @@ async def _persist_pending_import( ieee_address=ieee, services=[], properties=props, + design_id=default_design_id, ) db.add(node) await db.flush() diff --git a/backend/app/schemas/edges.py b/backend/app/schemas/edges.py index 8baa45b..05db453 100644 --- a/backend/app/schemas/edges.py +++ b/backend/app/schemas/edges.py @@ -26,7 +26,7 @@ class EdgeBase(BaseModel): class EdgeCreate(EdgeBase): - pass + design_id: str | None = None class EdgeUpdate(BaseModel): diff --git a/backend/app/schemas/nodes.py b/backend/app/schemas/nodes.py index a7f03e5..5683486 100644 --- a/backend/app/schemas/nodes.py +++ b/backend/app/schemas/nodes.py @@ -34,7 +34,7 @@ class NodeBase(BaseModel): class NodeCreate(NodeBase): - pass + design_id: str | None = None class NodeUpdate(BaseModel): @@ -68,6 +68,8 @@ class NodeUpdate(BaseModel): class NodeResponse(NodeBase): id: str + design_id: str | None = None + ieee_address: str | None = None last_seen: datetime | None = None response_time_ms: int | None = None created_at: datetime diff --git a/frontend/src/components/modals/CustomStyleModal.tsx b/frontend/src/components/modals/CustomStyleModal.tsx index a68ff89..b1f6918 100644 --- a/frontend/src/components/modals/CustomStyleModal.tsx +++ b/frontend/src/components/modals/CustomStyleModal.tsx @@ -26,7 +26,7 @@ const EDITABLE_NODE_TYPES: NodeType[] = [ 'generic', ] -const EDITABLE_EDGE_TYPES: EdgeType[] = ['ethernet', 'wifi', 'iot', 'vlan', 'virtual', 'cluster', 'fibre'] +const EDITABLE_EDGE_TYPES: EdgeType[] = ['ethernet', 'wifi', 'iot', 'vlan', 'virtual', 'cluster', 'fibre', 'electrical'] const NODE_ICONS: Record = { isp: Globe, router: Router, firewall: Flame, switch: Network, server: Server, proxmox: Layers,