Four fixes, all from looking at the real board. "and N more" now links to a page showing them all. Telling someone 35 more PCs are silent and leaving them to find the list is worse than not saying it. Each card names its own destination and a test checks it against the routes that actually exist - a viewall pointing at a route nobody wrote is the same rot the endpoint check already guards, just failing in the browser instead of the API. PRINTER ROWS ESCAPED THE CARD. A flex child will not shrink below its content width unless told to, so text-overflow never engaged and a row carrying three cartridge readings plus a location simply ran past the border. min-width:0 on the row parts is what enables the ellipsis; meta shrinks first because it matters least, and the card clips as a backstop. THE STAT TILES WERE INCOHERENT. Two counted asset TYPES, two counted asset STATUSES, and nothing said which - with the status one labelled "Active", which reads as "not deleted" but meant status = In Use across every type. Each tile now counts one thing and its label says so. PCs GONE SILENT IS NARROWER, and better for it. A PC that never reported at all is usually a hand-made or imported record rather than a bay that broke, and a PC that is not In Use is silent ON PURPOSE - that is the status doing its job. Both were burying the real signal: a machine that was working, is not now, and nobody has marked as anything else.
246 lines
8.5 KiB
Python
246 lines
8.5 KiB
Python
"""Machines plugin main class."""
|
|
|
|
import json
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import List, Dict, Optional, Type
|
|
|
|
from flask import Flask, Blueprint
|
|
import click
|
|
|
|
from shopdb.plugins.base import BasePlugin, PluginMeta
|
|
from shopdb.api import db, AssetType, AssetStatus
|
|
|
|
from .models import Machine, MachineType
|
|
from .api import machines_bp
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class MachinesPlugin(BasePlugin):
|
|
"""
|
|
Machines plugin - manages manufacturing machine assets.
|
|
|
|
Machines include CNCs, CMMs, lathes, grinders, EDMs, part markers, etc.
|
|
Uses the new Asset architecture with Machine extension table.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self._manifest = self._load_manifest()
|
|
|
|
def _load_manifest(self) -> Dict:
|
|
"""Load plugin manifest from JSON file."""
|
|
manifestpath = Path(__file__).parent / 'manifest.json'
|
|
if manifestpath.exists():
|
|
with open(manifestpath, 'r') as f:
|
|
return json.load(f)
|
|
return {}
|
|
|
|
@property
|
|
def meta(self) -> PluginMeta:
|
|
"""Return plugin metadata."""
|
|
return PluginMeta(
|
|
name=self._manifest.get('name', 'machines'),
|
|
version=self._manifest.get('version', '1.0.0'),
|
|
description=self._manifest.get(
|
|
'description',
|
|
'Machine management for manufacturing assets'
|
|
),
|
|
author=self._manifest.get('author', 'ShopDB Team'),
|
|
dependencies=self._manifest.get('dependencies', []),
|
|
core_version=self._manifest.get('core_version', '>=1.0.0'),
|
|
api_prefix=self._manifest.get('api_prefix', '/api/machines'),
|
|
)
|
|
|
|
def get_blueprint(self) -> Optional[Blueprint]:
|
|
"""Return Flask Blueprint with API routes."""
|
|
return machines_bp
|
|
|
|
def get_models(self) -> List[Type]:
|
|
"""Return list of SQLAlchemy model classes."""
|
|
return [Machine, MachineType]
|
|
|
|
def init_app(self, app: Flask, db_instance) -> None:
|
|
"""Initialize plugin with Flask app."""
|
|
logger.info(f"Machines plugin initialized (v{self.meta.version})")
|
|
|
|
def on_install(self, app: Flask) -> None:
|
|
"""Called when plugin is installed."""
|
|
with app.app_context():
|
|
self._ensure_asset_type()
|
|
self._ensure_asset_statuses()
|
|
self._ensure_machine_types()
|
|
logger.info("Machines plugin installed")
|
|
|
|
def _ensure_asset_type(self) -> None:
|
|
"""Ensure machine asset type exists."""
|
|
existing = AssetType.query.filter_by(assettype='machine').first()
|
|
if not existing:
|
|
at = AssetType(
|
|
assettype='machine',
|
|
pluginname='machines',
|
|
tablename='machines',
|
|
description='Manufacturing machines (CNCs, CMMs, lathes, etc.)',
|
|
icon='cog'
|
|
)
|
|
db.session.add(at)
|
|
logger.debug("Created asset type: machine")
|
|
db.session.commit()
|
|
|
|
def _ensure_asset_statuses(self) -> None:
|
|
"""Ensure standard asset statuses exist."""
|
|
statuses = [
|
|
('In Use', 'Asset is currently in use', '#28a745'),
|
|
('Spare', 'Spare/backup asset', '#17a2b8'),
|
|
('Retired', 'Asset has been retired', '#6c757d'),
|
|
('Maintenance', 'Asset is under maintenance', '#ffc107'),
|
|
('Decommissioned', 'Asset has been decommissioned', '#dc3545'),
|
|
]
|
|
|
|
for name, description, color in statuses:
|
|
existing = AssetStatus.query.filter_by(status=name).first()
|
|
if not existing:
|
|
s = AssetStatus(
|
|
status=name,
|
|
description=description,
|
|
color=color
|
|
)
|
|
db.session.add(s)
|
|
logger.debug(f"Created asset status: {name}")
|
|
|
|
db.session.commit()
|
|
|
|
def _ensure_machine_types(self) -> None:
|
|
"""Ensure basic machine types exist."""
|
|
machine_types = [
|
|
('CNC', 'Computer Numerical Control machine', 'cnc'),
|
|
('CMM', 'Coordinate Measuring Machine', 'cmm'),
|
|
('Lathe', 'Lathe machine', 'lathe'),
|
|
('Grinder', 'Grinding machine', 'grinder'),
|
|
('EDM', 'Electrical Discharge Machine', 'edm'),
|
|
('Part Marker', 'Part marking/engraving machine', 'marker'),
|
|
('Mill', 'Milling machine', 'mill'),
|
|
('Press', 'Press machine', 'press'),
|
|
('Robot', 'Industrial robot', 'robot'),
|
|
('Other', 'Other machine type', 'cog'),
|
|
]
|
|
|
|
for name, description, icon in machine_types:
|
|
existing = MachineType.query.filter_by(machinetype=name).first()
|
|
if not existing:
|
|
mt = MachineType(
|
|
machinetype=name,
|
|
description=description,
|
|
icon=icon
|
|
)
|
|
db.session.add(mt)
|
|
logger.debug(f"Created machine type: {name}")
|
|
|
|
db.session.commit()
|
|
|
|
def on_uninstall(self, app: Flask) -> None:
|
|
"""Called when plugin is uninstalled."""
|
|
logger.info("Machines plugin uninstalled")
|
|
|
|
def get_cli_commands(self) -> List:
|
|
"""Return CLI commands for this plugin."""
|
|
|
|
@click.group('machines')
|
|
def machinescli():
|
|
"""Machines plugin commands."""
|
|
pass
|
|
|
|
@machinescli.command('list-types')
|
|
def list_types():
|
|
"""List all machine types."""
|
|
from flask import current_app
|
|
|
|
with current_app.app_context():
|
|
types = MachineType.query.filter_by(isactive=True).all()
|
|
if not types:
|
|
click.echo('No machine types found.')
|
|
return
|
|
|
|
click.echo('Machine Types:')
|
|
for t in types:
|
|
click.echo(f" [{t.machinetypeid}] {t.machinetype}")
|
|
|
|
@machinescli.command('stats')
|
|
def stats():
|
|
"""Show machine statistics."""
|
|
from flask import current_app
|
|
from shopdb.api import Asset
|
|
|
|
with current_app.app_context():
|
|
total = db.session.query(Machine).join(Asset).filter(
|
|
Asset.isactive == True
|
|
).count()
|
|
|
|
click.echo(f"Total active machines: {total}")
|
|
|
|
# By type
|
|
by_type = 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()
|
|
|
|
if by_type:
|
|
click.echo("\nBy Type:")
|
|
for t, c in by_type:
|
|
click.echo(f" {t}: {c}")
|
|
|
|
return [machinescli]
|
|
|
|
def get_dashboard_widgets(self) -> List[Dict]:
|
|
"""Dashboard card: machines not in service.
|
|
|
|
A machine sitting in Repair or marked Lost is a thing someone is
|
|
supposed to be chasing, and nothing surfaces it today - it is visible
|
|
only to whoever thinks to filter the machines list by status.
|
|
|
|
Replaces a declaration naming a component nobody wrote.
|
|
"""
|
|
return [
|
|
{
|
|
'id': 'machines-outofservice',
|
|
'viewall': '/machines',
|
|
'title': 'Machines out of service',
|
|
'endpoint': '/api/machines/dashboard/outofservice',
|
|
'render': 'exceptions',
|
|
'severity': 'warning',
|
|
'permission': 'machines.view',
|
|
'empty': 'hide',
|
|
'position': 45,
|
|
'map': {
|
|
'title': 'assetnumber',
|
|
'detail': 'status',
|
|
'meta': [{'key': 'name'}],
|
|
'link': '/machines/{assetid}',
|
|
},
|
|
},
|
|
]
|
|
|
|
def get_navigation_items(self) -> List[Dict]:
|
|
"""Return navigation menu items."""
|
|
return [
|
|
{
|
|
'name': 'Machines',
|
|
'icon': 'cog',
|
|
'route': '/machines',
|
|
'position': 10,
|
|
},
|
|
]
|
|
|
|
def get_permissions(self) -> List:
|
|
"""Return the RBAC permissions this plugin owns."""
|
|
return [
|
|
('machines.view', 'View machines', 'machines'),
|
|
('machines.create', 'Create machines', 'machines'),
|
|
('machines.edit', 'Edit machines', 'machines'),
|
|
('machines.delete', 'Delete machines', 'machines'),
|
|
]
|