Move dashboard + PC reports off Machine onto the asset model

- Dashboard counts assets by type/status (Asset/AssetType/AssetStatus); adds
  a printer count to the totals.
- Software-compliance and pc-relationships reports query Computer /
  ComputerInstalledApp / assetrelationships instead of Machine.

Removes the dashboard + reports read dependency on the Machine model. (The
software-compliance report has a separate pre-existing bug: Application has no
isrequired field, so it errors before the query runs - unrelated to this change.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-06-26 09:25:44 -04:00
parent 7db011cb78
commit b68b0895ee
2 changed files with 92 additions and 79 deletions

View File

@@ -4,74 +4,79 @@ from flask import Blueprint, current_app
from flask_jwt_extended import jwt_required from flask_jwt_extended import jwt_required
from shopdb.extensions import db from shopdb.extensions import db
from shopdb.core.models import Machine, MachineType, MachineStatus from shopdb.core.models import Asset, AssetType, AssetStatus
from shopdb.utils.responses import success_response from shopdb.utils.responses import success_response
dashboard_bp = Blueprint('dashboard', __name__) dashboard_bp = Blueprint('dashboard', __name__)
# Map asset type name -> dashboard category label
_TYPE_CATEGORY = {
'equipment': 'Equipment',
'computer': 'PC',
'printer': 'Printer',
'network_device': 'Network',
}
def _count_by_type(assettype):
return db.session.query(Asset).join(AssetType).filter(
Asset.isactive == True,
AssetType.assettype == assettype
).count()
@dashboard_bp.route('/summary', methods=['GET']) @dashboard_bp.route('/summary', methods=['GET'])
@dashboard_bp.route('', methods=['GET']) @dashboard_bp.route('', methods=['GET'])
@jwt_required(optional=True) @jwt_required(optional=True)
def get_dashboard(): def get_dashboard():
"""Get dashboard summary data.""" """Get dashboard summary data (asset-based)."""
# Count machines by category equipment_count = _count_by_type('equipment')
equipment_count = db.session.query(Machine).join(MachineType).filter( pc_count = _count_by_type('computer')
Machine.isactive == True, network_count = _count_by_type('network_device')
MachineType.category == 'Equipment' printer_count = _count_by_type('printer')
).count() total = equipment_count + pc_count + network_count + printer_count
pc_count = db.session.query(Machine).join(MachineType).filter(
Machine.isactive == True,
MachineType.category == 'PC'
).count()
network_count = db.session.query(Machine).join(MachineType).filter(
Machine.isactive == True,
MachineType.category == 'Network'
).count()
# Count by status # Count by status
status_counts = db.session.query( status_counts = db.session.query(
MachineStatus.status, AssetStatus.status,
db.func.count(Machine.machineid) db.func.count(Asset.assetid)
).outerjoin( ).outerjoin(
Machine, Asset,
db.and_(Machine.statusid == MachineStatus.statusid, Machine.isactive == True) db.and_(Asset.statusid == AssetStatus.statusid, Asset.isactive == True)
).group_by(MachineStatus.status).all() ).group_by(AssetStatus.status).all()
# Recent machines
recent_machines = Machine.query.filter_by(isactive=True).order_by(
Machine.createddate.desc()
).limit(10).all()
# Build status dict
status_dict = {status: count for status, count in status_counts} status_dict = {status: count for status, count in status_counts}
# Recent assets
recent = Asset.query.filter_by(isactive=True).order_by(
Asset.createddate.desc()
).limit(10).all()
return success_response({ return success_response({
# Fields expected by frontend # Fields expected by frontend
'totalmachines': equipment_count + pc_count + network_count, 'totalmachines': total,
'totalequipment': equipment_count, 'totalequipment': equipment_count,
'totalpc': pc_count, 'totalpc': pc_count,
'totalnetwork': network_count, 'totalnetwork': network_count,
'totalprinter': printer_count,
'activemachines': status_dict.get('In Use', 0), 'activemachines': status_dict.get('In Use', 0),
'inrepair': status_dict.get('In Repair', 0), 'inrepair': status_dict.get('In Repair', 0),
# Also include structured data # Structured data
'counts': { 'counts': {
'equipment': equipment_count, 'equipment': equipment_count,
'pcs': pc_count, 'pcs': pc_count,
'networkdevices': network_count, 'networkdevices': network_count,
'total': equipment_count + pc_count + network_count 'printers': printer_count,
'total': total
}, },
'bystatus': status_dict, 'bystatus': status_dict,
'recent': [ 'recent': [
{ {
'machineid': m.machineid, 'assetid': a.assetid,
'machinenumber': m.machinenumber, 'assetnumber': a.assetnumber,
'machinetype': m.machinetype.machinetype if m.machinetype else None, 'assettype': a.assettype.assettype if a.assettype else None,
'createddate': m.createddate.isoformat() + 'Z' if m.createddate else None 'createddate': a.createddate.isoformat() + 'Z' if a.createddate else None
} }
for m in recent_machines for a in recent
] ]
}) })
@@ -79,23 +84,23 @@ def get_dashboard():
@dashboard_bp.route('/stats', methods=['GET']) @dashboard_bp.route('/stats', methods=['GET'])
@jwt_required(optional=True) @jwt_required(optional=True)
def get_stats(): def get_stats():
"""Get detailed statistics.""" """Get detailed statistics by asset type."""
# Machine type breakdown
type_counts = db.session.query( type_counts = db.session.query(
MachineType.machinetype, AssetType.assettype,
MachineType.category, db.func.count(Asset.assetid)
db.func.count(Machine.machineid)
).outerjoin( ).outerjoin(
Machine, Asset,
db.and_(Machine.machinetypeid == MachineType.machinetypeid, Machine.isactive == True) db.and_(Asset.assettypeid == AssetType.assettypeid, Asset.isactive == True)
).filter(MachineType.isactive == True).group_by( ).group_by(AssetType.assettypeid).all()
MachineType.machinetypeid
).all()
return success_response({ return success_response({
'bytype': [ 'bytype': [
{'type': t, 'category': c, 'count': count} {
for t, c, count in type_counts 'type': t,
'category': _TYPE_CATEGORY.get(t, t),
'count': count
}
for t, count in type_counts
] ]
}) })
@@ -142,7 +147,6 @@ def get_navigation():
def health_check(): def health_check():
"""Health check endpoint (no auth required).""" """Health check endpoint (no auth required)."""
try: try:
# Test database connection
db.session.execute(db.text('SELECT 1')) db.session.execute(db.text('SELECT 1'))
db_status = 'healthy' db_status = 'healthy'
except Exception as e: except Exception as e:

View File

@@ -8,9 +8,10 @@ from flask_jwt_extended import jwt_required
from shopdb.extensions import db from shopdb.extensions import db
from shopdb.core.models import ( from shopdb.core.models import (
Asset, AssetType, AssetStatus, Machine, MachineStatus, Asset, AssetType, AssetStatus,
Application, KnowledgeBase, InstalledApp Application, KnowledgeBase
) )
from plugins.computers.models import Computer, ComputerInstalledApp
from shopdb.utils.responses import success_response, error_response, ErrorCodes from shopdb.utils.responses import success_response, error_response, ErrorCodes
reports_bp = Blueprint('reports', __name__) reports_bp = Blueprint('reports', __name__)
@@ -337,29 +338,33 @@ def software_compliance():
if app_filter and str(app.appid) != app_filter: if app_filter and str(app.appid) != app_filter:
continue continue
# Get all PCs # Get all PCs (computers)
total_pcs = Machine.query.filter( total_pcs = Computer.query.join(
Machine.isactive == True, Asset, Asset.assetid == Computer.assetid
Machine.pctypeid.isnot(None) ).filter(Asset.isactive == True).count()
).count()
# Get PCs with this app installed # Get PCs with this app installed
installed_count = db.session.query(InstalledApp).join( installed_count = db.session.query(ComputerInstalledApp).join(
Machine, Machine.machineid == InstalledApp.machineid Computer, Computer.computerid == ComputerInstalledApp.computerid
).join(
Asset, Asset.assetid == Computer.assetid
).filter( ).filter(
InstalledApp.appid == app.appid, ComputerInstalledApp.appid == app.appid,
Machine.isactive == True ComputerInstalledApp.isactive == True,
Asset.isactive == True
).count() ).count()
# Get list of non-compliant PCs # Get list of non-compliant PCs
compliant_pc_ids = db.session.query(InstalledApp.machineid).filter( compliant_pc_ids = db.session.query(ComputerInstalledApp.computerid).filter(
InstalledApp.appid == app.appid ComputerInstalledApp.appid == app.appid,
ComputerInstalledApp.isactive == True
).subquery() ).subquery()
non_compliant_pcs = Machine.query.filter( non_compliant_pcs = Computer.query.join(
Machine.isactive == True, Asset, Asset.assetid == Computer.assetid
Machine.pctypeid.isnot(None), ).filter(
~Machine.machineid.in_(compliant_pc_ids) Asset.isactive == True,
~Computer.computerid.in_(compliant_pc_ids)
).limit(100).all() ).limit(100).all()
compliance_rate = (installed_count / total_pcs * 100) if total_pcs > 0 else 0 compliance_rate = (installed_count / total_pcs * 100) if total_pcs > 0 else 0
@@ -372,7 +377,8 @@ def software_compliance():
'notinstalled': total_pcs - installed_count, 'notinstalled': total_pcs - installed_count,
'compliancerate': round(compliance_rate, 1), 'compliancerate': round(compliance_rate, 1),
'noncompliantpcs': [ 'noncompliantpcs': [
{'machineid': pc.machineid, 'hostname': pc.hostname or pc.machinenumber} {'computerid': pc.computerid,
'hostname': pc.hostname or (pc.asset.assetnumber if pc.asset else None)}
for pc in non_compliant_pcs for pc in non_compliant_pcs
] ]
}) })
@@ -534,23 +540,26 @@ def pc_relationships():
Query parameters: Query parameters:
- format: 'json' (default) or 'csv' - format: 'json' (default) or 'csv'
""" """
# Asset relationships where a computer (source) relates to an equipment
# (target) - the asset-model equivalent of the legacy PC->machine links.
sql = db.text(""" sql = db.text("""
SELECT SELECT
eq.machinenumber AS machine_number, eq.assetnumber AS machine_number,
v.vendor AS vendor, v.vendor AS vendor,
mo.modelnumber AS model, mo.modelnumber AS model,
pc.machinenumber AS hostname, COALESCE(cpc.hostname, pc.assetnumber) AS hostname,
c.ipaddress AS ip c.ipaddress AS ip
FROM machinerelationships mr FROM assetrelationships ar
JOIN machines eq ON mr.parentmachineid = eq.machineid JOIN assets pc ON ar.sourceassetid = pc.assetid
JOIN machines pc ON mr.childmachineid = pc.machineid JOIN computers cpc ON cpc.assetid = pc.assetid
LEFT JOIN communications c ON pc.machineid = c.machineid AND c.isprimary = 1 AND c.comtypeid = 1 JOIN assets eq ON ar.targetassetid = eq.assetid
LEFT JOIN models mo ON eq.modelnumberid = mo.modelnumberid JOIN equipment eqx ON eqx.assetid = eq.assetid
LEFT JOIN communications c ON c.assetid = pc.assetid AND c.isprimary = 1 AND c.comtypeid = 1
LEFT JOIN models mo ON eqx.modelnumberid = mo.modelnumberid
LEFT JOIN vendors v ON mo.vendorid = v.vendorid LEFT JOIN vendors v ON mo.vendorid = v.vendorid
WHERE mr.isactive = 1 WHERE ar.isactive = 1
AND pc.pctypeid IS NOT NULL AND eq.assetnumber IS NOT NULL AND eq.assetnumber != ''
AND eq.machinenumber IS NOT NULL AND eq.machinenumber != '' ORDER BY eq.assetnumber
ORDER BY eq.machinenumber
""") """)
results = db.session.execute(sql).fetchall() results = db.session.execute(sql).fetchall()