Add the dualpath-as-single-machine site toggle
All checks were successful
CI / backend (push) Successful in 1m13s
CI / naming (push) Successful in 1s
CI / frontend (push) Successful in 7s

Most facilities consider a Dualpath pair one physical dual-bay machine.
New site setting dualpath_single_machine (default on): the machines
list, dashboard counts, machines-by-type report, and floor map collapse
each pair to its primary bay (lower assetnumber), with combined
2007 / 2008 labels; pagination totals stay honest. Detail pages remain
per-bay and always show a dual-bay sibling banner linking the partner.
Pair resolution lives in core services and joins the plugin contract
surface (0.8.0 -> 0.9.0).

On the WJ dataset: 31 pairs collapse, machine counts 262 -> 231, map
470 assets. Toggle verified live in both states, left on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-07-12 06:23:39 -04:00
parent 5a192f3100
commit b130ef43f3
17 changed files with 525 additions and 15 deletions

View File

@@ -3,7 +3,7 @@
from flask import Blueprint, request
from flask_jwt_extended import jwt_required
from shopdb.api import db, Asset, AssetType, Vendor, Model, AuditLog, success_response, error_response, paginated_response, ErrorCodes, get_pagination_params, paginate_query
from shopdb.api import db, Asset, AssetType, Vendor, Model, AuditLog, success_response, error_response, paginated_response, ErrorCodes, get_pagination_params, paginate_query, resolve_dualpath_pairs, dualpath_single_machine_enabled
from ..models import Machine, MachineType
@@ -213,18 +213,61 @@ def list_machines():
query = query.order_by(col.desc() if sort_dir == 'desc' else col)
# Dualpath single-machine collapse: hide the secondary bay so a dual-bay
# pair lists (and paginates) as one machine. Excluding before pagination
# keeps totals honest. Gated on the site setting (default on).
collapse = None
if dualpath_single_machine_enabled():
collapse = resolve_dualpath_pairs()
if collapse.secondaryassetids:
query = query.filter(Asset.assetid.notin_(collapse.secondaryassetids))
items, total = paginate_query(query, page, per_page)
# Resolve partner machineids for this page's primaries in one query.
machineidbyasset = {}
if collapse:
partnerassetids = [
collapse.partnerbyasset[m.assetid]['assetid']
for m in items if m.assetid in collapse.partnerbyasset
]
if partnerassetids:
for partner in Machine.query.filter(
Machine.assetid.in_(partnerassetids)).all():
machineidbyasset[partner.assetid] = partner.machineid
# Build response with both asset and machine data
data = []
for mach in items:
item = mach.asset.to_dict() if mach.asset else {}
item['machine'] = mach.to_dict()
# annotate the visible bay with its hidden partner (for '2007 / 2008')
partner = collapse.partnerbyasset.get(mach.assetid) if collapse else None
item['dualpathpartner'] = {
'assetid': partner['assetid'],
'machineid': machineidbyasset.get(partner['assetid']),
'assetnumber': partner['assetnumber'],
} if partner else None
data.append(item)
return paginated_response(data, page, per_page, total)
def _dualpath_partner_for(assetid):
"""Resolve this machine's Dualpath sibling, or None. Always evaluated
(independent of the collapse toggle) so the detail-page banner shows even
when a site lists both bays. Returns {assetid, machineid, assetnumber}."""
partner = resolve_dualpath_pairs().partnerbyasset.get(assetid)
if not partner:
return None
partnermach = Machine.query.filter_by(assetid=partner['assetid']).first()
return {
'assetid': partner['assetid'],
'machineid': partnermach.machineid if partnermach else None,
'assetnumber': partner['assetnumber'],
}
@machines_bp.route('/<int:machine_id>', methods=['GET'])
@jwt_required(optional=True)
def get_machine(machine_id: int):
@@ -240,6 +283,7 @@ def get_machine(machine_id: int):
result = mach.asset.to_dict() if mach.asset else {}
result['machine'] = mach.to_dict()
result['dualpathpartner'] = _dualpath_partner_for(mach.assetid)
return success_response(result)
@@ -259,6 +303,7 @@ def get_machine_by_asset(asset_id: int):
result = mach.asset.to_dict() if mach.asset else {}
result['machine'] = mach.to_dict()
result['dualpathpartner'] = _dualpath_partner_for(mach.assetid)
return success_response(result)
@@ -462,20 +507,30 @@ def delete_machine(machine_id: int):
@jwt_required(optional=True)
def dashboard_summary():
"""Get machine dashboard summary data."""
# Dualpath single-machine collapse: hide the secondary bay from counts so a
# dual-bay pair counts once. Gated on the site setting (default on).
secondaryassetids = set()
if dualpath_single_machine_enabled():
secondaryassetids = resolve_dualpath_pairs().secondaryassetids
# Total active machine count
total = db.session.query(Machine).join(Asset).filter(
total_query = db.session.query(Machine).join(Asset).filter(
Asset.isactive == True
).count()
)
if secondaryassetids:
total_query = total_query.filter(Asset.assetid.notin_(secondaryassetids))
total = total_query.count()
# Count by machine type
by_type = db.session.query(
by_type_query = db.session.query(
MachineType.machinetype,
db.func.count(Machine.machineid)
).join(Machine, Machine.machinetypeid == MachineType.machinetypeid
).join(Asset, Asset.assetid == Machine.assetid
).filter(Asset.isactive == True
).group_by(MachineType.machinetype
).all()
).filter(Asset.isactive == True)
if secondaryassetids:
by_type_query = by_type_query.filter(Asset.assetid.notin_(secondaryassetids))
by_type = by_type_query.group_by(MachineType.machinetype).all()
# Count by status
from shopdb.api import AssetStatus