From 275928a03f3c86c6d55823338633f36b512ac165 Mon Sep 17 00:00:00 2001 From: cproudlock Date: Sat, 30 May 2026 14:14:22 -0400 Subject: [PATCH] Phase 7A: wire ADR-001 asset position contract surface Lock the position-resolution columns from ADR-001 in code so resolve_asset_position's relationship walk activates. Schema - Asset.mapleft -> Asset.mapx, Asset.maptop -> Asset.mapy - Location.mapx / Location.mapy added (fallback for priority 3 of the ADR-001 resolution chain) - AssetRelationship.label (free-text nuance per ADR-001) - AssetRelationship.inheritsposition (bool, server_default true, controls whether the resolved-position walk follows the edge) - RelationshipType.propagatesthroughid (self-FK; sibling-propagation rail) Seeds - Three canonical ADR-001 relationship types created idempotently: partof, controls, connectedto - controls.propagatesthroughid wired to partof (partof + connectedto stay null per ADR-001 table). Both via Alembic migration AND CLI seed command so a fresh test fixture and a sister-site deploy both end up correct. - Legacy connection types (Serial Cable, Direct Ethernet, USB, WiFi, Dualpath) retained for backward compat with pre-1.0 relationship rows. Resolver - shopdb.api.resolve_asset_position now walks inheritsposition=true edges of type partof (then controls), recursively, depth-capped at 3 with visited-set cycle protection. Inactive edges + non-inheritable types are skipped. Falls through to the existing location fallback when the walk yields nothing. Tests - 11 new test_api_namespace cases cover: partof walk, controls-after- partof ordering, connectedto skipped, inheritsposition=false skipped, recursion, cycle break, depth-3 cap, self-beats-related, related-beats- location, inactive-edge skip. - 111 tests pass. Naming/style check green. Migration - migrations/versions/7a01_adr001_position_contract.py: - alter_column renames on assets (no data loss) - add_column on locations + relationshiptypes + assetrelationships - idempotent seed of three ADR types + propagation FK wire-up - downgrade reverses + best-effort deletion of seeded types that have no FK refs Backend rename (mapleft/maptop -> mapx/mapy) - shopdb/core/api/assets.py - plugins/{computers,equipment,network,printers}/api/... - scripts/migration/migrate_assets.py - Legacy Machine model + machines API + import_from_mysql.py UNCHANGED (per ADR-001 Machine retires; not part of the asset contract) Frontend rename - frontend/src/components/ShopFloorMap.vue - frontend/src/views/{MapEditor.vue, pcs/{PCDetail,PCForm}.vue, printers/{PrinterDetail,PrinterForm}.vue, machines/{MachineDetail,MachineForm}.vue, network/NetworkDeviceForm.vue} - Form field labels + v-model bindings + computed flags switched in lockstep with the backend. Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/src/components/ShopFloorMap.vue | 6 +- frontend/src/views/MapEditor.vue | 34 ++--- frontend/src/views/machines/MachineDetail.vue | 6 +- frontend/src/views/machines/MachineForm.vue | 26 ++-- .../src/views/network/NetworkDeviceForm.vue | 24 +-- frontend/src/views/pcs/PCDetail.vue | 6 +- frontend/src/views/pcs/PCForm.vue | 26 ++-- frontend/src/views/printers/PrinterDetail.vue | 6 +- frontend/src/views/printers/PrinterForm.vue | 26 ++-- .../versions/7a01_adr001_position_contract.py | 133 ++++++++++++++++ plugins/computers/api/routes.py | 8 +- plugins/equipment/api/routes.py | 8 +- plugins/network/api/routes.py | 8 +- plugins/printers/api/asset_routes.py | 8 +- scripts/migration/migrate_assets.py | 8 +- shopdb/api/__init__.py | 108 ++++++++++--- shopdb/cli/__init__.py | 25 ++- shopdb/core/api/assets.py | 16 +- shopdb/core/models/asset.py | 28 ++-- shopdb/core/models/location.py | 6 + shopdb/core/models/relationship.py | 49 +++++- tests/test_api_namespace.py | 143 ++++++++++++++++++ 22 files changed, 554 insertions(+), 154 deletions(-) create mode 100644 migrations/versions/7a01_adr001_position_contract.py diff --git a/frontend/src/components/ShopFloorMap.vue b/frontend/src/components/ShopFloorMap.vue index 799d2f9..bf8a9be 100644 --- a/frontend/src/components/ShopFloorMap.vue +++ b/frontend/src/components/ShopFloorMap.vue @@ -374,11 +374,11 @@ function renderMarkers() { markers.value = [] props.machines.forEach(item => { - if (item.mapleft == null || item.maptop == null) return + if (item.mapx == null || item.mapy == null) return // Transform coordinates (database Y is top-down, Leaflet is bottom-up) - const leafletY = MAP_HEIGHT - item.maptop - const leafletX = item.mapleft + const leafletY = MAP_HEIGHT - item.mapy + const leafletX = item.mapx // Determine color based on mode let color, typeName, displayName, detailRoute diff --git a/frontend/src/views/MapEditor.vue b/frontend/src/views/MapEditor.vue index 5a0a7cb..ce43bdb 100644 --- a/frontend/src/views/MapEditor.vue +++ b/frontend/src/views/MapEditor.vue @@ -35,8 +35,8 @@ class="asset-item" :class="{ selected: selectedAsset?.assetid === asset.assetid, - placed: asset.mapleft && asset.maptop, - unplaced: !asset.mapleft || !asset.maptop + placed: asset.mapx && asset.mapy, + unplaced: !asset.mapx || !asset.mapy }" @click="selectAsset(asset)" > @@ -45,7 +45,7 @@
{{ asset.name || asset.assetnumber }}
{{ asset.assettype }} - +
@@ -75,7 +75,7 @@