diff --git a/backend/app/db/database.py b/backend/app/db/database.py index 041aed9..6504004 100644 --- a/backend/app/db/database.py +++ b/backend/app/db/database.py @@ -99,6 +99,12 @@ async def init_db() -> None: await conn.exec_driver_sql("ALTER TABLE nodes ADD COLUMN height REAL") with suppress(OperationalError): await conn.exec_driver_sql("ALTER TABLE nodes ADD COLUMN bottom_handles INTEGER NOT NULL DEFAULT 1") + with suppress(OperationalError): + await conn.exec_driver_sql("ALTER TABLE nodes ADD COLUMN top_handles INTEGER NOT NULL DEFAULT 1") + with suppress(OperationalError): + await conn.exec_driver_sql("ALTER TABLE nodes ADD COLUMN left_handles INTEGER NOT NULL DEFAULT 0") + with suppress(OperationalError): + await conn.exec_driver_sql("ALTER TABLE nodes ADD COLUMN right_handles INTEGER NOT NULL DEFAULT 0") with suppress(OperationalError): await conn.exec_driver_sql("ALTER TABLE pending_devices ADD COLUMN discovery_source TEXT") with suppress(OperationalError): diff --git a/backend/app/db/models.py b/backend/app/db/models.py index f749cc7..875ea5d 100644 --- a/backend/app/db/models.py +++ b/backend/app/db/models.py @@ -59,6 +59,9 @@ class Node(Base): width: Mapped[float | None] = mapped_column(Float, nullable=True) height: Mapped[float | None] = mapped_column(Float, nullable=True) bottom_handles: Mapped[int] = mapped_column(Integer, default=1) + top_handles: Mapped[int] = mapped_column(Integer, default=1) + left_handles: Mapped[int] = mapped_column(Integer, default=0) + right_handles: Mapped[int] = mapped_column(Integer, default=0) ieee_address: Mapped[str | None] = mapped_column(String, index=True, nullable=True) last_seen: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) last_scan: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) diff --git a/backend/app/schemas/canvas.py b/backend/app/schemas/canvas.py index 580daba..8e23860 100644 --- a/backend/app/schemas/canvas.py +++ b/backend/app/schemas/canvas.py @@ -34,6 +34,9 @@ class NodeSave(BaseModel): width: float | None = None height: float | None = None bottom_handles: int = 1 + top_handles: int = 1 + left_handles: int = 0 + right_handles: int = 0 pos_x: float = 0 pos_y: float = 0 diff --git a/backend/app/schemas/nodes.py b/backend/app/schemas/nodes.py index 4fb3597..23d3ea6 100644 --- a/backend/app/schemas/nodes.py +++ b/backend/app/schemas/nodes.py @@ -32,6 +32,9 @@ class NodeBase(BaseModel): width: float | None = None height: float | None = None bottom_handles: int = 1 + top_handles: int = 1 + left_handles: int = 0 + right_handles: int = 0 class NodeCreate(NodeBase): @@ -66,6 +69,9 @@ class NodeUpdate(BaseModel): width: float | None = None height: float | None = None bottom_handles: int | None = None + top_handles: int | None = None + left_handles: int | None = None + right_handles: int | None = None class NodeResponse(NodeBase): diff --git a/backend/tests/test_canvas.py b/backend/tests/test_canvas.py index d26e9c6..a41e3b4 100644 --- a/backend/tests/test_canvas.py +++ b/backend/tests/test_canvas.py @@ -51,6 +51,32 @@ async def test_save_canvas_creates_nodes_and_edges(client: AsyncClient, headers: assert canvas["viewport"] == {"x": 1, "y": 2, "zoom": 1.5} +async def test_save_canvas_round_trips_per_side_handles(client: AsyncClient, headers: dict): + # Regression (#243): top/left/right_handles must persist across save+reload, + # not just bottom_handles. + n1 = node_payload(label="Cam", type="camera", top_handles=2, bottom_handles=3, left_handles=1, right_handles=4) + res = await client.post("/api/v1/canvas/save", json={"nodes": [n1], "edges": [], "viewport": {"x": 0, "y": 0, "zoom": 1}}, headers=headers) + assert res.status_code == 200 + + node = (await client.get("/api/v1/canvas", headers=headers)).json()["nodes"][0] + assert node["top_handles"] == 2 + assert node["bottom_handles"] == 3 + assert node["left_handles"] == 1 + assert node["right_handles"] == 4 + + +async def test_save_canvas_defaults_per_side_handles(client: AsyncClient, headers: dict): + # Nodes saved without the new fields fall back to top/bottom=1, left/right=0. + n1 = node_payload(label="Srv", type="server") + await client.post("/api/v1/canvas/save", json={"nodes": [n1], "edges": [], "viewport": {"x": 0, "y": 0, "zoom": 1}}, headers=headers) + + node = (await client.get("/api/v1/canvas", headers=headers)).json()["nodes"][0] + assert node["top_handles"] == 1 + assert node["bottom_handles"] == 1 + assert node["left_handles"] == 0 + assert node["right_handles"] == 0 + + async def test_load_canvas_exposes_inventory_timestamps(client: AsyncClient, headers: dict): n1 = node_payload(label="Router", type="router") await client.post("/api/v1/canvas/save", json={"nodes": [n1], "edges": [], "viewport": {}}, headers=headers)