Heal legacy single-map roots into 根/根图 on tree bootstrap.

Old installs hung the canvas on the top-level region; migrate those views under a system root map so tree load and create-root keep working.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-08-07 09:31:23 +08:00
parent 990798987b
commit 35a3980dc0
2 changed files with 227 additions and 53 deletions

View file

@ -164,8 +164,133 @@ def ensure_region_physical_view(db: Session, folder_id: str, *, commit: bool = T
return row
def _ensure_manual_root_map(
db: Session, top: TopoFolder, *, now: Any | None = None
) -> tuple[TopoFolder, bool]:
"""Ensure a top-level manual「根」has unique L2「根图」; migrate legacy L1 canvas onto it.
Pre-architecture shape: region === canvas (physical view hangs on the L1 folder).
Current shape: L1 nav-only + system L2「根图」holds the canvas (and any prior L2 kids).
"""
stamp = now or _utcnow()
changed = False
kids = (
db.query(TopoFolder)
.filter(TopoFolder.parent_id == top.id, TopoFolder.kind == "region")
.order_by(TopoFolder.sort_order.asc(), TopoFolder.created_at.asc())
.all()
)
root_map = next((k for k in kids if str(k.name or "") == MANUAL_ROOT_MAP_NAME), None)
if root_map is None:
root_map = TopoFolder(
id=uuid4().hex,
parent_id=top.id,
kind="region",
name=MANUAL_ROOT_MAP_NAME,
sort_order=0,
is_system=True,
created_at=stamp,
updated_at=stamp,
)
db.add(root_map)
db.flush()
changed = True
for kid in kids:
kid.parent_id = root_map.id
kid.updated_at = stamp
changed = True
elif not bool(root_map.is_system):
root_map.is_system = True
root_map.updated_at = stamp
changed = True
l1_views = db.query(TopoView).filter(TopoView.folder_id == top.id).all()
for v in l1_views:
v.folder_id = root_map.id
v.updated_at = stamp
changed = True
if l1_views:
db.flush()
has_phys = (
db.query(TopoView.id)
.filter(TopoView.folder_id == root_map.id, TopoView.kind == VIEW_KIND_PHYSICAL)
.first()
is not None
)
if not has_phys:
db.add(
TopoView(
id=uuid4().hex,
folder_id=root_map.id,
kind=VIEW_KIND_PHYSICAL,
role="core",
name=MANUAL_ROOT_MAP_NAME,
remark="",
sort_order=0,
filter={},
viewport={},
created_at=stamp,
updated_at=stamp,
)
)
changed = True
return root_map, changed
def _heal_manual_root_canvases(db: Session, root: TopoFolder) -> bool:
"""Adopt orphan regions and upgrade legacy single-map roots to 根/根图."""
from .ume_topology_world import is_ume_world_container
now = _utcnow()
changed = False
# Floating regions (no parent) from older installs — hang under system root.
for orphan in (
db.query(TopoFolder)
.filter(
TopoFolder.kind == "region",
or_(TopoFolder.parent_id.is_(None), TopoFolder.parent_id == ""),
)
.all()
):
orphan.parent_id = root.id
orphan.updated_at = now
changed = True
tops = (
db.query(TopoFolder)
.filter(TopoFolder.parent_id == root.id, TopoFolder.kind == "region")
.order_by(TopoFolder.sort_order.asc(), TopoFolder.created_at.asc())
.all()
)
for top in tops:
if is_ume_world_container(top):
continue
kids = (
db.query(TopoFolder)
.filter(TopoFolder.parent_id == top.id, TopoFolder.kind == "region")
.all()
)
has_root_map = any(str(k.name or "") == MANUAL_ROOT_MAP_NAME for k in kids)
l1_view_cnt = (
db.query(func.count(TopoView.id)).filter(TopoView.folder_id == top.id).scalar() or 0
)
# Already correct: unique 根图, no stray L1 views.
if has_root_map and int(l1_view_cnt) == 0:
# Still mark 根图 system if needed.
rm = next(k for k in kids if str(k.name or "") == MANUAL_ROOT_MAP_NAME)
if not bool(rm.is_system):
rm.is_system = True
rm.updated_at = now
changed = True
continue
_, did = _ensure_manual_root_map(db, top, now=now)
changed = changed or did
return changed
def bootstrap_topology_tree(db: Session) -> dict[str, str]:
"""Ensure hidden system root; flatten legacy nesting (once when needed)."""
"""Ensure hidden system root; heal legacy Unassigned / single-map roots when needed."""
now = _utcnow()
root = (
db.query(TopoFolder)
@ -189,7 +314,7 @@ def bootstrap_topology_tree(db: Session) -> dict[str, str]:
db.commit()
return {"root_id": root.id}
# Hot path: root already exists. Only touch legacy Unassigned rows when present.
dirty = False
legacy = (
db.query(TopoFolder)
.filter(
@ -198,19 +323,29 @@ def bootstrap_topology_tree(db: Session) -> dict[str, str]:
)
.all()
)
if not legacy:
return {"root_id": root.id}
for folder in legacy:
view_cnt = db.query(TopoView).filter(TopoView.folder_id == folder.id).count()
if view_cnt == 0:
db.delete(folder)
dirty = True
elif bool(folder.is_system):
folder.is_system = False
folder.updated_at = now
dirty = True
# One-time-ish normalize for views hanging under cleaned legacy folders.
for v in db.query(TopoView).all():
# Cheap normalize only for rows that still look pre-migration.
stale_views = (
db.query(TopoView)
.filter(
or_(
TopoView.parent_view_id.isnot(None),
TopoView.role.is_(None),
TopoView.role == "",
)
)
.all()
)
for v in stale_views:
changed = False
if v.parent_view_id:
v.parent_view_id = None
@ -230,8 +365,13 @@ def bootstrap_topology_tree(db: Session) -> dict[str, str]:
changed = True
if changed:
v.updated_at = now
dirty = True
db.commit()
if _heal_manual_root_canvases(db, root):
dirty = True
if dirty:
db.commit()
return {"root_id": root.id}
@ -271,49 +411,14 @@ def create_folder(db: Session, body: TopologyFolderCreate) -> TopologyFolderOut:
parent_kind = "region"
# Manual 根 (top-level nav): unique L2「根图」only — further creates remount under it.
if parent_kind == "region" and str(parent.parent_id or "") == str(root.id):
existing_l2 = (
db.query(TopoFolder)
.filter(TopoFolder.parent_id == parent.id, TopoFolder.kind == "region")
.order_by(TopoFolder.sort_order.asc(), TopoFolder.created_at.asc())
.all()
)
if existing_l2:
parent = existing_l2[0]
parent_kind = "region"
else:
# Legacy 根 without 根图 — heal then remount.
heal_now = _utcnow()
root_map = TopoFolder(
id=uuid4().hex,
parent_id=parent.id,
kind="region",
name=MANUAL_ROOT_MAP_NAME,
sort_order=0,
is_system=True,
created_at=heal_now,
updated_at=heal_now,
)
db.add(root_map)
db.flush()
db.add(
TopoView(
id=uuid4().hex,
folder_id=root_map.id,
kind=VIEW_KIND_PHYSICAL,
role="core",
name=MANUAL_ROOT_MAP_NAME,
remark="",
sort_order=0,
filter={},
viewport={},
created_at=heal_now,
updated_at=heal_now,
)
)
db.flush()
parent = root_map
parent_kind = "region"
if (
parent_kind == "region"
and str(parent.parent_id or "") == str(root.id)
and not is_ume_world_container(parent)
):
root_map, _ = _ensure_manual_root_map(db, parent)
parent = root_map
parent_kind = "region"
now = _utcnow()
row = TopoFolder(
id=uuid4().hex,
@ -390,7 +495,15 @@ def create_folder(db: Session, body: TopologyFolderCreate) -> TopologyFolderOut:
place_child_region_on_parent_canvas(db, parent, row)
db.flush()
reconcile_world_flat_view(db)
try:
reconcile_world_flat_view(db)
except Exception:
# Creating a manual root must not fail because UME world reconcile hiccups.
import logging
logging.getLogger("netx.topology").exception(
"reconcile_world_flat_view failed after create_folder id=%s", row.id
)
db.commit()
db.refresh(row)
return _folder_out(row)
@ -598,7 +711,10 @@ def get_topology_tree(db: Session) -> TopologyTreeOut:
# Soft-hide world map on UME World when rule 2A is not met.
show_flat = True
if is_ume_world_container(folder):
show_flat = world_map_should_exist(db)
try:
show_flat = world_map_should_exist(db)
except Exception:
show_flat = False
# physical first, then custom; stable by sort_order/name.
ordered = sorted(

View file

@ -391,6 +391,64 @@ class FabricTopologyTests(unittest.TestCase):
self.assertEqual(ne.ip_address, "")
self.assertEqual(ne.device_type, "generic")
def test_legacy_single_map_heals_into_root_map(self) -> None:
"""Pre-architecture: physical view hung on top-level region (no「根图」)."""
from uuid import uuid4
from netx_api.models import TopoFolder, TopoView
from netx_api.topology_common import _utcnow
from netx_api.topology_membership import VIEW_KIND_PHYSICAL
now = _utcnow()
boot = svc.bootstrap_topology_tree(self.db)
root = self.db.get(TopoFolder, boot["root_id"])
self.assertIsNotNone(root)
assert root is not None
legacy = TopoFolder(
id=uuid4().hex,
parent_id=root.id,
kind="region",
name="旧单图站点",
sort_order=0,
is_system=False,
created_at=now,
updated_at=now,
)
old_view = TopoView(
id=uuid4().hex,
folder_id=legacy.id,
parent_view_id=None,
kind=VIEW_KIND_PHYSICAL,
role="core",
name="拓扑图",
remark="",
sort_order=0,
filter={},
viewport={},
created_at=now,
updated_at=now,
)
self.db.add(legacy)
self.db.add(old_view)
self.db.commit()
tree = svc.get_topology_tree(self.db)
assert tree.root is not None
site = next(c for c in tree.root.children if c.id == legacy.id)
self.assertEqual(site.views, [])
self.assertEqual(len(site.children), 1)
root_map = site.children[0]
self.assertEqual(root_map.name, "根图")
self.assertTrue(any(v.id == old_view.id for v in root_map.views))
# Creating another top-level root still works beside the healed legacy site.
created = svc.create_folder(
self.db, TopologyFolderCreate(name="新根", kind="region")
)
tree2 = svc.get_topology_tree(self.db)
assert tree2.root is not None
self.assertTrue(any(c.id == created.id for c in tree2.root.children))
self.assertTrue(any(c.id == legacy.id for c in tree2.root.children))
def test_topology_tree_root_region_and_leaf(self) -> None:
tree = svc.get_topology_tree(self.db)
self.assertIsNotNone(tree.root)