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
This commit is contained in:
@@ -7,7 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
|
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.db.database import get_db
|
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.canvas import CanvasStateResponse
|
||||||
from app.schemas.edges import EdgeResponse
|
from app.schemas.edges import EdgeResponse
|
||||||
from app.schemas.nodes import NodeResponse
|
from app.schemas.nodes import NodeResponse
|
||||||
@@ -18,6 +18,7 @@ router = APIRouter()
|
|||||||
@router.get("", response_model=CanvasStateResponse)
|
@router.get("", response_model=CanvasStateResponse)
|
||||||
async def liveview_canvas(
|
async def liveview_canvas(
|
||||||
key: str | None = Query(default=None),
|
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),
|
db: AsyncSession = Depends(get_db),
|
||||||
) -> CanvasStateResponse:
|
) -> CanvasStateResponse:
|
||||||
"""Read-only public canvas endpoint.
|
"""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):
|
if not key or not hmac.compare_digest(key, settings.liveview_key):
|
||||||
raise HTTPException(status_code=403, detail="Invalid live view key")
|
raise HTTPException(status_code=403, detail="Invalid live view key")
|
||||||
|
|
||||||
nodes = (await db.execute(select(Node))).scalars().all()
|
if design_id is None:
|
||||||
edges = (await db.execute(select(Edge))).scalars().all()
|
first = (await db.execute(select(Design).order_by(Design.created_at).limit(1))).scalar()
|
||||||
state = await db.get(CanvasState, 1)
|
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}
|
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
|
custom_style: dict[str, Any] | None = state.custom_style if state else None
|
||||||
return CanvasStateResponse(
|
return CanvasStateResponse(
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
from app.api.deps import get_current_user
|
from app.api.deps import get_current_user
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.db.database import AsyncSessionLocal, get_db
|
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.nodes import NodeCreate
|
||||||
from app.schemas.scan import PendingDeviceResponse, ScanRunResponse
|
from app.schemas.scan import PendingDeviceResponse, ScanRunResponse
|
||||||
from app.services.scanner import request_cancel, run_scan
|
from app.services.scanner import request_cancel, run_scan
|
||||||
@@ -118,6 +118,10 @@ async def bulk_approve_devices(
|
|||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
_: str = Depends(get_current_user),
|
_: str = Depends(get_current_user),
|
||||||
) -> dict[str, Any]:
|
) -> 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(
|
result = await db.execute(
|
||||||
select(PendingDevice).where(
|
select(PendingDevice).where(
|
||||||
PendingDevice.id.in_(payload.device_ids),
|
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.
|
# Default to ping so the status checker actually polls the new node.
|
||||||
# Without this the scheduler skips it (check_method NULL → no check).
|
# Without this the scheduler skips it (check_method NULL → no check).
|
||||||
check_method="none" if is_zigbee else ("ping" if device.ip else None),
|
check_method="none" if is_zigbee else ("ping" if device.ip else None),
|
||||||
|
design_id=default_design_id,
|
||||||
)
|
)
|
||||||
db.add(node)
|
db.add(node)
|
||||||
created_nodes.append(node)
|
created_nodes.append(node)
|
||||||
@@ -227,6 +232,12 @@ async def approve_device(
|
|||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
_: str = Depends(get_current_user),
|
_: str = Depends(get_current_user),
|
||||||
) -> dict[str, Any]:
|
) -> 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)
|
device = await db.get(PendingDevice, device_id)
|
||||||
if not device:
|
if not device:
|
||||||
raise HTTPException(status_code=404, detail="Device not found")
|
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 []),
|
) 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_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,
|
check_target=None if _is_zigbee else node_data.check_target,
|
||||||
|
design_id=node_design_id,
|
||||||
)
|
)
|
||||||
db.add(node)
|
db.add(node)
|
||||||
await db.flush()
|
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:
|
if (src_id, tgt_id) in existing_pairs or (tgt_id, src_id) in existing_pairs:
|
||||||
await db.delete(link)
|
await db.delete(link)
|
||||||
continue
|
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(
|
edge = Edge(
|
||||||
source=src_id,
|
source=src_id,
|
||||||
target=tgt_id,
|
target=tgt_id,
|
||||||
type="iot",
|
type="iot",
|
||||||
source_handle="bottom",
|
source_handle="bottom",
|
||||||
target_handle="top-t",
|
target_handle="top-t",
|
||||||
|
design_id=edge_design_id,
|
||||||
)
|
)
|
||||||
db.add(edge)
|
db.add(edge)
|
||||||
await db.flush()
|
await db.flush()
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
|
|
||||||
from app.api.deps import get_current_user
|
from app.api.deps import get_current_user
|
||||||
from app.db.database import AsyncSessionLocal, get_db
|
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.scan import ScanRunResponse
|
||||||
from app.schemas.zigbee import (
|
from app.schemas.zigbee import (
|
||||||
ZigbeeCoordinatorOut,
|
ZigbeeCoordinatorOut,
|
||||||
@@ -138,6 +138,10 @@ async def _persist_pending_import(
|
|||||||
Coordinator auto-approves to a canvas Node. Other devices upsert by IEEE.
|
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.
|
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_out: ZigbeeCoordinatorOut | None = None
|
||||||
coordinator_existed = False
|
coordinator_existed = False
|
||||||
pending_created = 0
|
pending_created = 0
|
||||||
@@ -174,6 +178,7 @@ async def _persist_pending_import(
|
|||||||
ieee_address=ieee,
|
ieee_address=ieee,
|
||||||
services=[],
|
services=[],
|
||||||
properties=props,
|
properties=props,
|
||||||
|
design_id=default_design_id,
|
||||||
)
|
)
|
||||||
db.add(node)
|
db.add(node)
|
||||||
await db.flush()
|
await db.flush()
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class EdgeBase(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class EdgeCreate(EdgeBase):
|
class EdgeCreate(EdgeBase):
|
||||||
pass
|
design_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class EdgeUpdate(BaseModel):
|
class EdgeUpdate(BaseModel):
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class NodeBase(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class NodeCreate(NodeBase):
|
class NodeCreate(NodeBase):
|
||||||
pass
|
design_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class NodeUpdate(BaseModel):
|
class NodeUpdate(BaseModel):
|
||||||
@@ -68,6 +68,8 @@ class NodeUpdate(BaseModel):
|
|||||||
|
|
||||||
class NodeResponse(NodeBase):
|
class NodeResponse(NodeBase):
|
||||||
id: str
|
id: str
|
||||||
|
design_id: str | None = None
|
||||||
|
ieee_address: str | None = None
|
||||||
last_seen: datetime | None = None
|
last_seen: datetime | None = None
|
||||||
response_time_ms: int | None = None
|
response_time_ms: int | None = None
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const EDITABLE_NODE_TYPES: NodeType[] = [
|
|||||||
'generic',
|
'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<string, LucideIcon> = {
|
const NODE_ICONS: Record<string, LucideIcon> = {
|
||||||
isp: Globe, router: Router, firewall: Flame, switch: Network, server: Server, proxmox: Layers,
|
isp: Globe, router: Router, firewall: Flame, switch: Network, server: Server, proxmox: Layers,
|
||||||
|
|||||||
Reference in New Issue
Block a user