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) <noreply@anthropic.com>
This commit is contained in:
@@ -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 @@
|
||||
<div class="asset-name">{{ asset.name || asset.assetnumber }}</div>
|
||||
<div class="asset-meta">
|
||||
<span class="badge badge-sm">{{ asset.assettype }}</span>
|
||||
<span v-if="asset.mapleft && asset.maptop" class="placed-indicator" title="Placed on map"><MapPin :size="12" /></span>
|
||||
<span v-if="asset.mapx && asset.mapy" class="placed-indicator" title="Placed on map"><MapPin :size="12" /></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -75,7 +75,7 @@
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-danger"
|
||||
v-if="selectedAsset.mapleft && selectedAsset.maptop"
|
||||
v-if="selectedAsset.mapx && selectedAsset.mapy"
|
||||
@click="clearPosition"
|
||||
>
|
||||
Remove from Map
|
||||
@@ -94,7 +94,7 @@
|
||||
:assetTypeMode="true"
|
||||
:theme="currentTheme"
|
||||
:pickerMode="!!selectedAsset"
|
||||
:initialPosition="selectedAsset ? { left: selectedAsset.mapleft, top: selectedAsset.maptop } : null"
|
||||
:initialPosition="selectedAsset ? { left: selectedAsset.mapx, top: selectedAsset.mapy } : null"
|
||||
@positionPicked="handlePositionPicked"
|
||||
@markerClick="handleMarkerClick"
|
||||
/>
|
||||
@@ -125,14 +125,14 @@ const filteredAssets = computed(() => {
|
||||
}
|
||||
|
||||
if (showUnplacedOnly.value) {
|
||||
result = result.filter(a => !a.mapleft || !a.maptop)
|
||||
result = result.filter(a => !a.mapx || !a.mapy)
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
const placedAssets = computed(() => {
|
||||
return assets.value.filter(a => a.mapleft && a.maptop)
|
||||
return assets.value.filter(a => a.mapx && a.mapy)
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -163,8 +163,8 @@ function getTypeIcon(assettype) {
|
||||
|
||||
function selectAsset(asset) {
|
||||
selectedAsset.value = asset
|
||||
pickedPosition.value = asset.mapleft && asset.maptop
|
||||
? { left: asset.mapleft, top: asset.maptop }
|
||||
pickedPosition.value = asset.mapx && asset.mapy
|
||||
? { left: asset.mapx, top: asset.mapy }
|
||||
: null
|
||||
}
|
||||
|
||||
@@ -181,15 +181,15 @@ async function savePosition() {
|
||||
|
||||
try {
|
||||
await assetsApi.update(selectedAsset.value.assetid, {
|
||||
mapleft: Math.round(pickedPosition.value.left),
|
||||
maptop: Math.round(pickedPosition.value.top)
|
||||
mapx: Math.round(pickedPosition.value.left),
|
||||
mapy: Math.round(pickedPosition.value.top)
|
||||
})
|
||||
|
||||
// Update local state
|
||||
const asset = assets.value.find(a => a.assetid === selectedAsset.value.assetid)
|
||||
if (asset) {
|
||||
asset.mapleft = Math.round(pickedPosition.value.left)
|
||||
asset.maptop = Math.round(pickedPosition.value.top)
|
||||
asset.mapx = Math.round(pickedPosition.value.left)
|
||||
asset.mapy = Math.round(pickedPosition.value.top)
|
||||
}
|
||||
|
||||
selectedAsset.value = null
|
||||
@@ -207,15 +207,15 @@ async function clearPosition() {
|
||||
|
||||
try {
|
||||
await assetsApi.update(selectedAsset.value.assetid, {
|
||||
mapleft: null,
|
||||
maptop: null
|
||||
mapx: null,
|
||||
mapy: null
|
||||
})
|
||||
|
||||
// Update local state
|
||||
const asset = assets.value.find(a => a.assetid === selectedAsset.value.assetid)
|
||||
if (asset) {
|
||||
asset.mapleft = null
|
||||
asset.maptop = null
|
||||
asset.mapx = null
|
||||
asset.mapy = null
|
||||
}
|
||||
|
||||
selectedAsset.value = null
|
||||
|
||||
Reference in New Issue
Block a user