Put the dual-bay sibling in the triage band, and fix the gage counterpart
All checks were successful
CI / naming (push) Successful in 4s
CI / frontend (push) Successful in 12s
CI / migrations-mysql (push) Successful in 40s
CI / backend (push) Successful in 4m8s

TWO STRIPS OF CHROME SAYING THE SAME KIND OF THING

The machine page opened with a banner - "Dual-bay machine - sibling bay: 2022" -
directly above a hero whose band said "Controlled by". Both answer "what am I
looking at", and splitting them across a banner and a band cost a strip of
vertical space to say so.

The sibling is now a chip in the band beside the controlling PC. The band takes
an `extraitems` prop rather than learning what dualpath is: the pairing is
resolved by the machines plugin, not from the relationships endpoint, so core
cannot derive it, and a dualpath-shaped prop on a core component would be the
machines plugin leaking upward. A page passes [{label, links:[{text, to}]}] and
the band renders it with its own markup, which also keeps the chips visually
identical to the ones it derives itself.

The label stays "Dual-bay sibling" rather than "Dualpath". The banner spelled
the meaning out because a dualpath pair is ONE physical machine in two bays; a
chip reading "Dualpath" would have dropped that.

THE GAGE PAGE FILTERED FOR A COUNTERPART THAT CANNOT EXIST

MeasuringToolDetail was wired counterparttype="machine" labelled "Used on".
Wrong on both counts. The import synthesizes one measuring-tool asset per
metrology PC and a 'controls' edge PC -> tool (stage_metrology), so a gage's
counterpart is a COMPUTER, exactly as on the machine page. Filtering for a
machine peer meant the band rendered nothing on every gage that has a PC.

It looked merely empty rather than wrong because this dev database holds one
measuring tool with no relationships at all, so there was nothing to be missing.
A hand-held gage genuinely has no such edge and correctly shows nothing; a
metrology station now shows the PC that drives it.

The relationships card stays. It is the only surface with Add Relationship and
the per-row delete, and it lists types the band filters out - the band is
triage, the card is management. It does now repeat what the band shows, which
is worth revisiting separately.

Verified on screen. Frontend 280 passed.
This commit is contained in:
cproudlock
2026-08-29 19:24:07 -04:00
parent 96a4a38c8c
commit 78017eee2a
3 changed files with 57 additions and 32 deletions

View File

@@ -19,6 +19,20 @@
</span>
</div>
<div v-for="item in extraitems" :key="item.label" class="triage-item">
<span class="triage-label">{{ item.label }}</span>
<span class="triage-values">
<component
v-for="link in item.links"
:key="link.text"
:is="link.to ? 'router-link' : 'span'"
:to="link.to || undefined"
class="triage-chip"
:class="{ link: Boolean(link.to) }"
>{{ link.text }}</component>
</span>
</div>
<div v-if="accessmethods && accessmethods.length" class="triage-item">
<span class="triage-label">Remote access</span>
<span class="triage-values">
@@ -86,7 +100,15 @@ const props = defineProps({
// printer through usesprinter and defaultprinter. Hardcoding 'controls'
// left the band blank on the printer page, which is the one page where the
// link is not obvious from the asset itself.
relationshiptypes: { type: Array, default: () => ['controls'] }
relationshiptypes: { type: Array, default: () => ['controls'] },
// Triage facts a page knows but core cannot derive from a relationship
// edge. The machine page's dual-bay sibling is the case this exists for:
// it is resolved by the machines plugin, not from the relationships
// endpoint, and it used to sit in its own banner above the hero - two
// strips of chrome saying two halves of the same "what am I looking at".
// Shape: [{ label, links: [{ text, to }] }]. A link with no `to` renders
// as plain text rather than a dead chip.
extraitems: { type: Array, default: () => [] }
})
const machines = ref([])
@@ -102,6 +124,7 @@ const hasAnything = computed(() =>
Boolean(slots.actions) ||
Boolean(props.liveness) ||
machines.value.length > 0 ||
props.extraitems.length > 0 ||
(props.accessmethods && props.accessmethods.length > 0) ||
Boolean(props.locationname)
)

View File

@@ -20,16 +20,6 @@
<div v-if="loading" class="loading">Loading...</div>
<template v-else-if="machine">
<!-- Dual-bay sibling banner: a Dualpath pair is one physical machine -->
<div v-if="machine.dualpathpartner" class="dualpath-banner">
Dual-bay machine - sibling bay:
<router-link
v-if="machine.dualpathpartner.machineid"
:to="`/machines/${machine.dualpathpartner.machineid}`"
>{{ machine.dualpathpartner.assetnumber }}</router-link>
<span v-else>{{ machine.dualpathpartner.assetnumber }}</span>
</div>
<!-- Hero Section -->
<div class="hero-card">
<div class="hero-image" v-if="machine.machine?.imageurl">
@@ -80,6 +70,7 @@
:assetid="machine.assetid"
counterparttype="computer"
counterpartlabel="Controlled by"
:extraitems="triageextras"
/>
</div>
</div>
@@ -259,7 +250,7 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { machinesApi } from '@/api'
import { withBase } from '@/utils/basePath'
@@ -281,6 +272,25 @@ const { isEnabled } = useIdentifierFlags()
const loading = ref(true)
const machine = ref(null)
// The dual-bay sibling, as a triage chip. It had its own banner above the hero
// until the band existed; two strips of chrome answering two halves of the same
// "what am I looking at" read as clutter, and the sibling belongs beside the
// PC that drives the machine rather than above it.
const triageextras = computed(() => {
const partner = machine.value?.dualpathpartner
if (!partner) return []
return [{
// Says what the pair MEANS, not just that a link exists: a dualpath pair is
// one physical machine in two bays, which is why the old banner spelled it
// out rather than showing a bare number.
label: 'Dual-bay sibling',
links: [{
text: partner.assetnumber,
to: partner.machineid ? `/machines/${partner.machineid}` : null
}]
}]
})
const { heroWarranty, warrantyDate } = useWarrantyBadge(() => machine.value?.assetid)
// relationships render via the shared AssetRelationships card
@@ -334,21 +344,6 @@ async function confirmRetire() {
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
}
.dualpath-banner {
margin-bottom: 16px;
padding: 10px 14px;
border-radius: 6px;
background: var(--bg-card);
border: 1px solid var(--border);
border-left: 4px solid var(--primary);
color: var(--text);
font-size: 0.9rem;
}
.dualpath-banner a {
color: var(--link);
font-weight: 600;
}
.feature-tag {
display: inline-block;

View File

@@ -47,13 +47,20 @@
</div>
</div>
<!-- The 6am band. A gage usually has no controls edge at all,
and the band renders nothing when it has nothing to say - it is
here so the five asset pages stay one shape rather than five. -->
<!-- The 6am band: which PC drives this instrument.
A metrology tool is reached through the PC that runs it - the
import synthesizes one measuring-tool asset per metrology PC and
a 'controls' edge PC -> tool (stage_metrology), so the
counterpart here is a COMPUTER, exactly as on the machine page.
This was first written as counterparttype="machine" labelled
"Used on", which filtered for a counterpart that cannot exist
and so rendered nothing on every gage that had a PC. A hand-held
gage genuinely has no such edge, and the band correctly shows
nothing for it. -->
<AssetTriageBand
:assetid="tool.assetid"
counterparttype="machine"
counterpartlabel="Used on"
counterparttype="computer"
counterpartlabel="Controlled by"
/>
</div>
</div>