Render vendor-model photos on asset detail heroes
All checks were successful
CI / backend (push) Successful in 1m14s
CI / naming (push) Successful in 2s
CI / frontend (push) Successful in 8s

Computer and Printer payloads now surface the linked model imageurl the
way machines already did, and the machine/PC/printer detail heroes
render the photo when present (network devices and measuring tools
have no model link, so nothing to surface). Absent images render
nothing rather than a broken icon.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-07-12 06:39:11 -04:00
parent b130ef43f3
commit 7bde765f1a
9 changed files with 125 additions and 1 deletions

View File

@@ -12,6 +12,13 @@ ADR-007 and ADR-002.
### Added
- Vendor-model photos on asset detail heroes: computers and printers now
surface the linked model's `imageurl` in their extension payloads (the
field machines already exposed), and the machine, PC, printer, network
device, and measuring tool detail pages render the photo in the hero card
when present (hidden cleanly when absent). Network devices and measuring
tools have no model link yet, so their heroes stay photo-less until one
is added.
- Dualpath "single machine" site toggle (`dualpath_single_machine`, default
on). A Dualpath relationship pair is one physical dual-bay machine (single
controller, bay-selector switch); when on, the machines list, dashboard and

View File

@@ -28,6 +28,9 @@
<!-- Hero Section -->
<div class="hero-card">
<div class="hero-image" v-if="machine.machine?.imageurl">
<img :src="machine.machine.imageurl" :alt="machine.machine.modelname || 'Model photo'" />
</div>
<div class="hero-content">
<div class="hero-title">
<h1>{{ machine.assetnumber }}</h1>

View File

@@ -15,6 +15,9 @@
<template v-else-if="tool">
<!-- Hero Section -->
<div class="hero-card">
<div class="hero-image" v-if="tool.measuringtool?.imageurl">
<img :src="tool.measuringtool.imageurl" alt="Model photo" />
</div>
<div class="hero-content">
<div class="hero-title">
<h1>{{ tool.assetnumber }}</h1>

View File

@@ -2,7 +2,8 @@
<div class="detail-page" v-if="device">
<div class="hero-card">
<div class="hero-image">
<div class="device-icon">
<img v-if="device.networkdevice?.imageurl" :src="device.networkdevice.imageurl" alt="Model photo" />
<div class="device-icon" v-else>
<span class="icon"><component :is="getDeviceIcon()" :size="24" /></span>
</div>
</div>

View File

@@ -13,6 +13,9 @@
<template v-else-if="computer">
<!-- Hero Section -->
<div class="hero-card">
<div class="hero-image" v-if="computer.computer?.imageurl">
<img :src="computer.computer.imageurl" :alt="computer.computer.modelname || 'Model photo'" />
</div>
<div class="hero-content">
<div class="hero-title">
<h1>{{ computer.assetnumber }}</h1>

View File

@@ -16,6 +16,9 @@
<template v-else-if="printer">
<!-- Hero Section -->
<div class="hero-card">
<div class="hero-image" v-if="printer.printer?.imageurl">
<img :src="printer.printer.imageurl" :alt="printer.printer.modelname || 'Model photo'" />
</div>
<div class="hero-content">
<div class="hero-title">
<h1>{{ displayTitle }}</h1>

View File

@@ -130,6 +130,8 @@ class Computer(BaseModel):
result['vendorname'] = self.vendor.vendor
if self.model:
result['modelname'] = self.model.modelnumber
if self.model.imageurl:
result['imageurl'] = self.model.imageurl
# Names of enabled remote-access protocols (for list badges)
result['accessprotocolnames'] = [

View File

@@ -118,5 +118,7 @@ class Printer(BaseModel):
result['vendorname'] = self.vendor.vendor
if self.model:
result['modelname'] = self.model.modelnumber
if self.model.imageurl:
result['imageurl'] = self.model.imageurl
return result

View File

@@ -0,0 +1,100 @@
"""Vendor-model photo surfaces in asset-extension detail payloads.
Machines, computers, and printers link a vendor Model via modelnumberid; when
that Model carries an imageurl, the extension dict in the GET detail payload
must expose it as 'imageurl' (the field name machines established, consumed by
the detail-page heroes and the machine badge). When no model is linked, the
field must be absent so the frontend v-if renders nothing.
Network devices and measuring tools have no model link, so they are out of
scope here by design.
"""
from shopdb.extensions import db as _db
from shopdb.core.models import Asset, AssetType, Model
from plugins.machines.models import Machine
from plugins.computers.models import Computer
from plugins.printers.models import Printer
IMAGE_URL = '/api/models/images/model-1.png'
def _seed_asset(assetnumber, assettype):
atype = AssetType.query.filter_by(assettype=assettype).first()
if not atype:
atype = AssetType(assettype=assettype)
_db.session.add(atype)
_db.session.flush()
asset = Asset(assetnumber=assetnumber, assettypeid=atype.assettypeid)
_db.session.add(asset)
_db.session.flush()
return asset
def _seed_model(imageurl=IMAGE_URL):
model = Model(modelnumber='IMG-MODEL-1', imageurl=imageurl)
_db.session.add(model)
_db.session.flush()
return model
def test_machine_detail_carries_model_imageurl(client, db):
asset = _seed_asset('2001', 'machine')
model = _seed_model()
machine = Machine(assetid=asset.assetid, modelnumberid=model.modelnumberid)
_db.session.add(machine)
_db.session.commit()
resp = client.get(f'/api/machines/{machine.machineid}')
assert resp.status_code == 200, resp.get_json()
assert resp.get_json()['data']['machine']['imageurl'] == IMAGE_URL
def test_computer_detail_carries_model_imageurl(client, db):
asset = _seed_asset('PC-2001', 'computer')
model = _seed_model()
computer = Computer(assetid=asset.assetid, modelnumberid=model.modelnumberid)
_db.session.add(computer)
_db.session.commit()
resp = client.get(f'/api/computers/{computer.computerid}')
assert resp.status_code == 200, resp.get_json()
assert resp.get_json()['data']['computer']['imageurl'] == IMAGE_URL
def test_printer_detail_carries_model_imageurl(client, db):
asset = _seed_asset('PR-2001', 'printer')
model = _seed_model()
printer = Printer(assetid=asset.assetid, modelnumberid=model.modelnumberid)
_db.session.add(printer)
_db.session.commit()
resp = client.get(f'/api/printers/{printer.printerid}')
assert resp.status_code == 200, resp.get_json()
assert resp.get_json()['data']['printer']['imageurl'] == IMAGE_URL
def test_detail_omits_imageurl_without_model(client, db):
"""No model linked -> no imageurl key (frontend v-if shows nothing)."""
asset = _seed_asset('PR-2002', 'printer')
printer = Printer(assetid=asset.assetid)
_db.session.add(printer)
_db.session.commit()
resp = client.get(f'/api/printers/{printer.printerid}')
assert resp.status_code == 200, resp.get_json()
assert 'imageurl' not in resp.get_json()['data']['printer']
def test_detail_omits_imageurl_when_model_has_no_image(client, db):
"""Model linked but no photo -> no imageurl key."""
asset = _seed_asset('PC-2002', 'computer')
model = _seed_model(imageurl=None)
computer = Computer(assetid=asset.assetid, modelnumberid=model.modelnumberid)
_db.session.add(computer)
_db.session.commit()
resp = client.get(f'/api/computers/{computer.computerid}')
assert resp.status_code == 200, resp.get_json()
assert 'imageurl' not in resp.get_json()['data']['computer']