diff --git a/frontend/src/views/settings/AuditLogs.vue b/frontend/src/views/settings/AuditLogs.vue
index d062ba0f..7b321b79 100644
--- a/frontend/src/views/settings/AuditLogs.vue
+++ b/frontend/src/views/settings/AuditLogs.vue
@@ -319,7 +319,8 @@ onMounted(loadLogs)
}
.modal {
- background: var(--bg-card);
+ /* SOLID, not var(--bg-card) - translucent in dark mode over the overlay. */
+ background: var(--bg-card-solid);
border: 1px solid var(--border);
border-radius: 8px;
min-width: 500px;
diff --git a/frontend/src/views/settings/UsersList.vue b/frontend/src/views/settings/UsersList.vue
index 06fa15a9..58fd9aaf 100644
--- a/frontend/src/views/settings/UsersList.vue
+++ b/frontend/src/views/settings/UsersList.vue
@@ -636,7 +636,9 @@ onMounted(loadData)
}
.modal {
- background: var(--bg-card);
+ /* SOLID, not var(--bg-card): the card variable is translucent in dark mode,
+ which left this dialog see-through over its own overlay. */
+ background: var(--bg-card-solid);
border: 1px solid var(--border);
border-radius: 8px;
width: 500px;
diff --git a/plugins/warranty/api/routes.py b/plugins/warranty/api/routes.py
index fa51ae0d..9111a55d 100644
--- a/plugins/warranty/api/routes.py
+++ b/plugins/warranty/api/routes.py
@@ -33,12 +33,57 @@ def _parse_date(value):
return None
+def _related_machine(asset):
+ """The machine a covered asset is associated with, or None.
+
+ A shopfloor PC is bought, warranted and replaced as a PC, but it is FOUND
+ by the machine it drives - nobody walks the floor looking for an asset
+ number. So a warranty row for a PC carries the machine's number, and its
+ map position so the row can point at where to go.
+
+ Walks the asset relationship graph in BOTH directions: the canonical edge is
+ PC --controls--> machine, but a dual-bay pair carries controls on both bays
+ and hand-made links are not guaranteed to be oriented. Returns the first
+ active related asset whose type is 'machine'.
+ """
+ from shopdb.api import Asset as CoreAsset
+
+ related = []
+ for rel in (getattr(asset, 'outgoing_relationships', None) or []):
+ if rel.isactive and rel.targetasset:
+ related.append(rel.targetasset)
+ for rel in (getattr(asset, 'incoming_relationships', None) or []):
+ if rel.isactive and rel.sourceasset:
+ related.append(rel.sourceasset)
+
+ for candidate in related:
+ typename = candidate.assettype.assettype if candidate.assettype else None
+ if typename != 'machine':
+ continue
+ return {
+ 'assetid': candidate.assetid,
+ 'machinenumber': candidate.assetnumber,
+ 'name': candidate.name,
+ # Map position for the hover preview. Either may be None: a machine
+ # that has never been placed on the floor map still has a number
+ # worth showing, so the caller decides what to do with a missing
+ # position rather than the row being dropped.
+ 'mapx': candidate.mapx,
+ 'mapy': candidate.mapy,
+ 'locationid': candidate.locationid,
+ 'locationname': (candidate.location.locationname
+ if candidate.location else None),
+ }
+ return None
+
+
def _asset_summary(asset):
return {
'assetid': asset.assetid,
'assetnumber': asset.assetnumber,
'name': asset.name,
'assettypename': asset.assettype.assettype if asset.assettype else None,
+ 'machine': _related_machine(asset),
}
diff --git a/plugins/warranty/frontend/components/MachineMapChip.vue b/plugins/warranty/frontend/components/MachineMapChip.vue
new file mode 100644
index 00000000..87d5ca18
--- /dev/null
+++ b/plugins/warranty/frontend/components/MachineMapChip.vue
@@ -0,0 +1,124 @@
+
+
+
+ {{ machine.machinenumber || machine.name || '-' }}
+
+
+
+ {{ machine.name || machine.machinenumber }}
+ {{ machine.locationname }}
+
+
+
+
+
+
+
+
+ {{ machine.name || machine.machinenumber }}
+ {{ machine.locationname || 'Not placed on the floor map' }}
+
+
+ -
+
+
+
+
+
diff --git a/plugins/warranty/frontend/views/WarrantiesList.vue b/plugins/warranty/frontend/views/WarrantiesList.vue
index 9e549f99..fdbf34e6 100644
--- a/plugins/warranty/frontend/views/WarrantiesList.vue
+++ b/plugins/warranty/frontend/views/WarrantiesList.vue
@@ -40,6 +40,7 @@