Add print badges, pagination, route splitting, JWT auth fixes, and list page alignment
- Fix equipment badge barcode not rendering (loading race condition) - Fix printer QR code not rendering on initial load (same race condition) - Add model image to equipment badge via imageurl from Model table - Fix white-on-white machine number text on badge, tighten barcode spacing - Add PaginationBar component used across all list pages - Split monolithic router into per-plugin route modules - Fix 25 GET API endpoints returning 401 (jwt_required -> optional=True) - Align list page columns across Equipment, PCs, and Network pages - Add print views: EquipmentBadge, PrinterQRSingle, PrinterQRBatch, USBLabelBatch - Add PC Relationships report, migration docs, and CLAUDE.md project guide - Various plugin model, API, and frontend refinements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -621,8 +621,8 @@ def dashboard_summary():
|
||||
|
||||
return success_response({
|
||||
'total': total,
|
||||
'by_type': [{'type': t, 'count': c} for t, c in by_type],
|
||||
'by_os': [{'os': o, 'count': c} for o, c in by_os],
|
||||
'bytype': [{'type': t, 'count': c} for t, c in by_type],
|
||||
'byos': [{'os': o, 'count': c} for o, c in by_os],
|
||||
'shopfloor': shopfloor_count,
|
||||
'non_shopfloor': total - shopfloor_count
|
||||
'nonshopfloor': total - shopfloor_count
|
||||
})
|
||||
|
||||
@@ -117,9 +117,9 @@ class Computer(BaseModel):
|
||||
|
||||
# Add related object names
|
||||
if self.computertype:
|
||||
result['computertype_name'] = self.computertype.computertype
|
||||
result['computertypename'] = self.computertype.computertype
|
||||
if self.operatingsystem:
|
||||
result['os_name'] = self.operatingsystem.osname
|
||||
result['osname'] = self.operatingsystem.osname
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -78,8 +78,8 @@ class ComputersPlugin(BasePlugin):
|
||||
if not existing:
|
||||
at = AssetType(
|
||||
assettype='computer',
|
||||
plugin_name='computers',
|
||||
table_name='computers',
|
||||
pluginname='computers',
|
||||
tablename='computers',
|
||||
description='PCs, servers, and workstations',
|
||||
icon='desktop'
|
||||
)
|
||||
@@ -201,9 +201,9 @@ class ComputersPlugin(BasePlugin):
|
||||
"""Return navigation menu items."""
|
||||
return [
|
||||
{
|
||||
'name': 'Computers',
|
||||
'name': 'PCs',
|
||||
'icon': 'desktop',
|
||||
'route': '/computers',
|
||||
'route': '/pcs',
|
||||
'position': 15,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -306,8 +306,8 @@ def create_equipment():
|
||||
lastmaintenancedate=data.get('lastmaintenancedate'),
|
||||
nextmaintenancedate=data.get('nextmaintenancedate'),
|
||||
maintenanceintervaldays=data.get('maintenanceintervaldays'),
|
||||
controller_vendorid=data.get('controller_vendorid'),
|
||||
controller_modelid=data.get('controller_modelid')
|
||||
controllervendorid=data.get('controllervendorid'),
|
||||
controllermodelid=data.get('controllermodelid')
|
||||
)
|
||||
|
||||
db.session.add(equip)
|
||||
@@ -358,7 +358,7 @@ def update_equipment(equipment_id: int):
|
||||
equipment_fields = ['equipmenttypeid', 'vendorid', 'modelnumberid',
|
||||
'requiresmanualconfig', 'islocationonly',
|
||||
'lastmaintenancedate', 'nextmaintenancedate', 'maintenanceintervaldays',
|
||||
'controller_vendorid', 'controller_modelid']
|
||||
'controllervendorid', 'controllermodelid']
|
||||
for key in equipment_fields:
|
||||
if key in data:
|
||||
setattr(equip, key, data[key])
|
||||
@@ -427,6 +427,6 @@ def dashboard_summary():
|
||||
|
||||
return success_response({
|
||||
'total': total,
|
||||
'by_type': [{'type': t, 'count': c} for t, c in by_type],
|
||||
'by_status': [{'status': s, 'count': c} for s, c in by_status]
|
||||
'bytype': [{'type': t, 'count': c} for t, c in by_type],
|
||||
'bystatus': [{'status': s, 'count': c} for s, c in by_status]
|
||||
})
|
||||
|
||||
@@ -78,13 +78,13 @@ class Equipment(BaseModel):
|
||||
maintenanceintervaldays = db.Column(db.Integer, nullable=True)
|
||||
|
||||
# Controller info (for CNC machines)
|
||||
controller_vendorid = db.Column(
|
||||
controllervendorid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('vendors.vendorid'),
|
||||
nullable=True,
|
||||
comment='Controller vendor (e.g., FANUC)'
|
||||
)
|
||||
controller_modelid = db.Column(
|
||||
controllermodelid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('models.modelnumberid'),
|
||||
nullable=True,
|
||||
@@ -99,8 +99,8 @@ class Equipment(BaseModel):
|
||||
equipmenttype = db.relationship('EquipmentType', backref='equipment')
|
||||
vendor = db.relationship('Vendor', foreign_keys=[vendorid], backref='equipment_items')
|
||||
model = db.relationship('Model', foreign_keys=[modelnumberid], backref='equipment_items')
|
||||
controller_vendor = db.relationship('Vendor', foreign_keys=[controller_vendorid], backref='equipment_controllers')
|
||||
controller_model = db.relationship('Model', foreign_keys=[controller_modelid], backref='equipment_controller_models')
|
||||
controllervendor = db.relationship('Vendor', foreign_keys=[controllervendorid], backref='equipment_controllers')
|
||||
controllermodel = db.relationship('Model', foreign_keys=[controllermodelid], backref='equipment_controller_models')
|
||||
|
||||
__table_args__ = (
|
||||
db.Index('idx_equipment_type', 'equipmenttypeid'),
|
||||
@@ -116,16 +116,18 @@ class Equipment(BaseModel):
|
||||
|
||||
# Add related object names
|
||||
if self.equipmenttype:
|
||||
result['equipmenttype_name'] = self.equipmenttype.equipmenttype
|
||||
result['equipmenttypename'] = self.equipmenttype.equipmenttype
|
||||
if self.vendor:
|
||||
result['vendor_name'] = self.vendor.vendor
|
||||
result['vendorname'] = self.vendor.vendor
|
||||
if self.model:
|
||||
result['model_name'] = self.model.modelnumber
|
||||
result['modelname'] = self.model.modelnumber
|
||||
if self.model.imageurl:
|
||||
result['imageurl'] = self.model.imageurl
|
||||
|
||||
# Add controller info
|
||||
if self.controller_vendor:
|
||||
result['controller_vendor_name'] = self.controller_vendor.vendor
|
||||
if self.controller_model:
|
||||
result['controller_model_name'] = self.controller_model.modelnumber
|
||||
if self.controllervendor:
|
||||
result['controllervendorname'] = self.controllervendor.vendor
|
||||
if self.controllermodel:
|
||||
result['controllermodelname'] = self.controllermodel.modelnumber
|
||||
|
||||
return result
|
||||
|
||||
@@ -79,8 +79,8 @@ class EquipmentPlugin(BasePlugin):
|
||||
if not existing:
|
||||
at = AssetType(
|
||||
assettype='equipment',
|
||||
plugin_name='equipment',
|
||||
table_name='equipment',
|
||||
pluginname='equipment',
|
||||
tablename='equipment',
|
||||
description='Manufacturing equipment (CNCs, CMMs, lathes, etc.)',
|
||||
icon='cog'
|
||||
)
|
||||
@@ -214,7 +214,7 @@ class EquipmentPlugin(BasePlugin):
|
||||
{
|
||||
'name': 'Equipment',
|
||||
'icon': 'cog',
|
||||
'route': '/equipment',
|
||||
'route': '/machines',
|
||||
'position': 10,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -227,7 +227,7 @@ def get_network_device(device_id: int):
|
||||
)
|
||||
|
||||
result = netdev.asset.to_dict() if netdev.asset else {}
|
||||
result['network_device'] = netdev.to_dict()
|
||||
result['networkdevice'] = netdev.to_dict()
|
||||
|
||||
return success_response(result)
|
||||
|
||||
@@ -246,7 +246,7 @@ def get_network_device_by_asset(asset_id: int):
|
||||
)
|
||||
|
||||
result = netdev.asset.to_dict() if netdev.asset else {}
|
||||
result['network_device'] = netdev.to_dict()
|
||||
result['networkdevice'] = netdev.to_dict()
|
||||
|
||||
return success_response(result)
|
||||
|
||||
@@ -265,7 +265,7 @@ def get_network_device_by_hostname(hostname: str):
|
||||
)
|
||||
|
||||
result = netdev.asset.to_dict() if netdev.asset else {}
|
||||
result['network_device'] = netdev.to_dict()
|
||||
result['networkdevice'] = netdev.to_dict()
|
||||
|
||||
return success_response(result)
|
||||
|
||||
@@ -353,7 +353,7 @@ def create_network_device():
|
||||
db.session.commit()
|
||||
|
||||
result = asset.to_dict()
|
||||
result['network_device'] = netdev.to_dict()
|
||||
result['networkdevice'] = netdev.to_dict()
|
||||
|
||||
return success_response(result, message='Network device created', http_code=201)
|
||||
|
||||
@@ -413,7 +413,7 @@ def update_network_device(device_id: int):
|
||||
db.session.commit()
|
||||
|
||||
result = asset.to_dict()
|
||||
result['network_device'] = netdev.to_dict()
|
||||
result['networkdevice'] = netdev.to_dict()
|
||||
|
||||
return success_response(result, message='Network device updated')
|
||||
|
||||
@@ -479,10 +479,10 @@ def dashboard_summary():
|
||||
|
||||
return success_response({
|
||||
'total': total,
|
||||
'by_type': [{'type': t, 'count': c} for t, c in by_type],
|
||||
'by_vendor': [{'vendor': v, 'count': c} for v, c in by_vendor],
|
||||
'bytype': [{'type': t, 'count': c} for t, c in by_type],
|
||||
'byvendor': [{'vendor': v, 'count': c} for v, c in by_vendor],
|
||||
'poe': poe_count,
|
||||
'non_poe': total - poe_count
|
||||
'nonpoe': total - poe_count
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -114,8 +114,8 @@ class NetworkDevice(BaseModel):
|
||||
|
||||
# Add related object names
|
||||
if self.networkdevicetype:
|
||||
result['networkdevicetype_name'] = self.networkdevicetype.networkdevicetype
|
||||
result['networkdevicetypename'] = self.networkdevicetype.networkdevicetype
|
||||
if self.vendor:
|
||||
result['vendor_name'] = self.vendor.vendor
|
||||
result['vendorname'] = self.vendor.vendor
|
||||
|
||||
return result
|
||||
|
||||
@@ -78,8 +78,8 @@ class NetworkPlugin(BasePlugin):
|
||||
if not existing:
|
||||
at = AssetType(
|
||||
assettype='network_device',
|
||||
plugin_name='network',
|
||||
table_name='networkdevices',
|
||||
pluginname='network',
|
||||
tablename='networkdevices',
|
||||
description='Network infrastructure devices (switches, APs, cameras, etc.)',
|
||||
icon='network-wired'
|
||||
)
|
||||
|
||||
@@ -374,7 +374,7 @@ def dashboard_summary():
|
||||
|
||||
return success_response({
|
||||
'active': total_active,
|
||||
'by_type': [{'type': t, 'color': c, 'count': n} for t, c, n in by_type]
|
||||
'bytype': [{'type': t, 'color': c, 'count': n} for t, c, n in by_type]
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ printers_asset_bp = Blueprint('printers_asset', __name__)
|
||||
# =============================================================================
|
||||
|
||||
@printers_asset_bp.route('/types', methods=['GET'])
|
||||
@jwt_required()
|
||||
@jwt_required(optional=True)
|
||||
def list_printer_types():
|
||||
"""List all printer types."""
|
||||
page, per_page = get_pagination_params(request)
|
||||
@@ -46,7 +46,7 @@ def list_printer_types():
|
||||
|
||||
|
||||
@printers_asset_bp.route('/types/<int:type_id>', methods=['GET'])
|
||||
@jwt_required()
|
||||
@jwt_required(optional=True)
|
||||
def get_printer_type(type_id: int):
|
||||
"""Get a single printer type."""
|
||||
t = PrinterType.query.get(type_id)
|
||||
@@ -471,6 +471,6 @@ def dashboard_summary():
|
||||
'online': total, # Placeholder - would need monitoring integration
|
||||
'lowsupplies': 0, # Placeholder - would need Zabbix integration
|
||||
'criticalsupplies': 0, # Placeholder - would need Zabbix integration
|
||||
'by_type': [{'type': t, 'count': c} for t, c in by_type],
|
||||
'by_vendor': [{'vendor': v, 'count': c} for v, c in by_vendor],
|
||||
'bytype': [{'type': t, 'count': c} for t, c in by_type],
|
||||
'byvendor': [{'vendor': v, 'count': c} for v, c in by_vendor],
|
||||
})
|
||||
|
||||
@@ -113,10 +113,10 @@ class Printer(BaseModel):
|
||||
|
||||
# Add related object names
|
||||
if self.printertype:
|
||||
result['printertype_name'] = self.printertype.printertype
|
||||
result['printertypename'] = self.printertype.printertype
|
||||
if self.vendor:
|
||||
result['vendor_name'] = self.vendor.vendor
|
||||
result['vendorname'] = self.vendor.vendor
|
||||
if self.model:
|
||||
result['model_name'] = self.model.modelnumber
|
||||
result['modelname'] = self.model.modelnumber
|
||||
|
||||
return result
|
||||
|
||||
@@ -116,8 +116,8 @@ class PrintersPlugin(BasePlugin):
|
||||
if not existing:
|
||||
at = AssetType(
|
||||
assettype='printer',
|
||||
plugin_name='printers',
|
||||
table_name='printers',
|
||||
pluginname='printers',
|
||||
tablename='printers',
|
||||
description='Printers (laser, inkjet, label, MFP, plotter)',
|
||||
icon='printer'
|
||||
)
|
||||
|
||||
@@ -161,10 +161,10 @@ def get_usb_device(device_id: int):
|
||||
# Get recent checkout history
|
||||
checkouts = USBCheckout.query.filter_by(
|
||||
usbdeviceid=device_id
|
||||
).order_by(USBCheckout.checkout_time.desc()).limit(20).all()
|
||||
).order_by(USBCheckout.checkouttime.desc()).limit(20).all()
|
||||
|
||||
result = device.to_dict()
|
||||
result['checkout_history'] = [c.to_dict() for c in checkouts]
|
||||
result['checkouthistory'] = [c.to_dict() for c in checkouts]
|
||||
|
||||
return success_response(result)
|
||||
|
||||
@@ -258,16 +258,16 @@ def checkout_device(device_id: int):
|
||||
usbdeviceid=device_id,
|
||||
machineid=0, # Legacy field, set to 0 for new checkouts
|
||||
sso=data['sso'],
|
||||
checkout_name=data.get('checkout_name'),
|
||||
checkout_time=datetime.utcnow(),
|
||||
checkout_reason=data.get('checkout_reason'),
|
||||
was_wiped=False
|
||||
checkoutname=data.get('checkoutname'),
|
||||
checkouttime=datetime.utcnow(),
|
||||
checkoutreason=data.get('checkoutreason'),
|
||||
waswiped=False
|
||||
)
|
||||
|
||||
# Update device status
|
||||
device.ischeckedout = True
|
||||
device.currentuserid = data['sso']
|
||||
device.currentusername = data.get('checkout_name')
|
||||
device.currentusername = data.get('checkoutname')
|
||||
device.currentcheckoutdate = datetime.utcnow()
|
||||
device.modifieddate = datetime.utcnow()
|
||||
|
||||
@@ -300,15 +300,15 @@ def checkin_device(device_id: int):
|
||||
# Find active checkout
|
||||
active_checkout = USBCheckout.query.filter_by(
|
||||
usbdeviceid=device_id,
|
||||
checkin_time=None
|
||||
checkintime=None
|
||||
).first()
|
||||
|
||||
data = request.get_json() or {}
|
||||
|
||||
if active_checkout:
|
||||
active_checkout.checkin_time = datetime.utcnow()
|
||||
active_checkout.checkin_notes = data.get('checkin_notes', active_checkout.checkin_notes)
|
||||
active_checkout.was_wiped = data.get('was_wiped', False)
|
||||
active_checkout.checkintime = datetime.utcnow()
|
||||
active_checkout.checkinnotes = data.get('checkinnotes', active_checkout.checkinnotes)
|
||||
active_checkout.waswiped = data.get('waswiped', False)
|
||||
|
||||
# Update device status
|
||||
device.ischeckedout = False
|
||||
@@ -337,7 +337,7 @@ def get_device_history(device_id: int):
|
||||
|
||||
query = USBCheckout.query.filter_by(
|
||||
usbdeviceid=device_id
|
||||
).order_by(USBCheckout.checkout_time.desc())
|
||||
).order_by(USBCheckout.checkouttime.desc())
|
||||
|
||||
items, total = paginate_query(query, page, per_page)
|
||||
data = [c.to_dict() for c in items]
|
||||
@@ -361,13 +361,13 @@ def list_all_checkouts():
|
||||
|
||||
# Filter by active only
|
||||
if request.args.get('active', '').lower() == 'true':
|
||||
query = query.filter(USBCheckout.checkin_time == None)
|
||||
query = query.filter(USBCheckout.checkintime == None)
|
||||
|
||||
# Filter by user
|
||||
if sso := request.args.get('sso'):
|
||||
query = query.filter(USBCheckout.sso == sso)
|
||||
|
||||
query = query.order_by(USBCheckout.checkout_time.desc())
|
||||
query = query.order_by(USBCheckout.checkouttime.desc())
|
||||
|
||||
items, total = paginate_query(query, page, per_page)
|
||||
data = [c.to_dict() for c in items]
|
||||
@@ -380,7 +380,7 @@ def list_all_checkouts():
|
||||
def list_active_checkouts():
|
||||
"""List all currently active checkouts."""
|
||||
checkouts = USBCheckout.query.filter(
|
||||
USBCheckout.checkin_time == None
|
||||
).order_by(USBCheckout.checkout_time.desc()).all()
|
||||
USBCheckout.checkintime == None
|
||||
).order_by(USBCheckout.checkouttime.desc()).all()
|
||||
|
||||
return success_response([c.to_dict() for c in checkouts])
|
||||
|
||||
@@ -123,16 +123,16 @@ class USBCheckout(BaseModel):
|
||||
|
||||
# User info
|
||||
sso = db.Column(db.String(20), nullable=False, comment='SSO of user')
|
||||
checkout_name = db.Column(db.String(100), nullable=True, comment='Name of user')
|
||||
checkoutname = db.Column(db.String(100), nullable=True, comment='Name of user')
|
||||
|
||||
# Checkout details
|
||||
checkout_time = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
|
||||
checkin_time = db.Column(db.DateTime, nullable=True)
|
||||
checkouttime = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
|
||||
checkintime = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
# Metadata
|
||||
checkout_reason = db.Column(db.Text, nullable=True, comment='Reason for checkout')
|
||||
checkin_notes = db.Column(db.Text, nullable=True)
|
||||
was_wiped = db.Column(db.Boolean, nullable=True, comment='Was device wiped after return')
|
||||
checkoutreason = db.Column(db.Text, nullable=True, comment='Reason for checkout')
|
||||
checkinnotes = db.Column(db.Text, nullable=True)
|
||||
waswiped = db.Column(db.Boolean, nullable=True, comment='Was device wiped after return')
|
||||
|
||||
# Relationships
|
||||
device = db.relationship('USBDevice', backref=db.backref('checkouts', lazy='dynamic'))
|
||||
@@ -143,13 +143,13 @@ class USBCheckout(BaseModel):
|
||||
@property
|
||||
def is_active(self):
|
||||
"""Check if this checkout is currently active (not returned)."""
|
||||
return self.checkin_time is None
|
||||
return self.checkintime is None
|
||||
|
||||
@property
|
||||
def duration_days(self):
|
||||
"""Get duration of checkout in days."""
|
||||
end = self.checkin_time or datetime.utcnow()
|
||||
delta = end - self.checkout_time
|
||||
end = self.checkintime or datetime.utcnow()
|
||||
delta = end - self.checkouttime
|
||||
return delta.days
|
||||
|
||||
def to_dict(self):
|
||||
|
||||
Reference in New Issue
Block a user