Rename the equipment domain to machines; retype the models catalog (ADR-011)
All checks were successful
CI / backend (push) Successful in 23s
CI / naming (push) Successful in 2s
CI / frontend (push) Successful in 8s

The equipment plugin is now the machines plugin, ending the UI-vs-code
vocabulary split while the contract is pre-1.0 and nothing external
depends on the old names.

- plugins/equipment -> plugins/machines: manifest, class, /api/machines,
  machines.* permissions, registry key (with an auto-migrating load shim
  for existing installs).
- Tables: equipment -> machines (equipmentid -> machineid) and
  equipmenttypes -> machinetypes, renamed in the plugin's own migration
  chain (machines0002rename), idempotent for both upgrading and fresh
  installs.
- The legacy core machinetypes lookup actually types the vendor MODELS
  catalog, so it is renamed losslessly to modeltypes
  (models.modeltypeid, /api/modeltypes, Model Types settings page)
  rather than collapsed, freeing the machinetypes name. Core migration
  7d17_machines_rename also flips data in place: assettypes row
  equipment -> machine, auditlog entitytype, identifier_/search_
  settings keys, permission rows, and renames alembic_version_equipment.
- Frontend: machinesApi/modeltypesApi, item.machine response shape,
  assettype value compares 'equipment' -> 'machine' (map, search,
  custom fields, relationships), routes machines.js with plugin gating
  retagged, /print/machine-badge, Machine Types (subtypes) and Model
  Types (catalog) settings pages, machines-by-type report id.
- Docs swept; ADRs left as history per the authoring rule.

Upgrade: flask db upgrade then flask plugin upgrade-all.

Verified: dev DB flipped live (262 machines, 35 modeltypes, 95 models
retyped, zero equipment tables remain); fresh scratch-MySQL install
produces the new names; 341 tests green; naming/style green; frontend
builds; live E2E on machines list/detail, PC relationships, map,
reports, and both settings pages.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-07-11 15:17:42 -04:00
parent 3c43c8d5c8
commit 48d3160bc5
84 changed files with 4755 additions and 4317 deletions

219
plugins/machines/plugin.py Normal file
View File

@@ -0,0 +1,219 @@
"""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]:
"""Return dashboard widget definitions."""
return [
{
'name': 'Machine Status',
'component': 'MachineStatusWidget',
'endpoint': '/api/machines/dashboard/summary',
'size': 'medium',
'position': 5,
},
]
def get_navigation_items(self) -> List[Dict]:
"""Return navigation menu items."""
return [
{
'name': 'Machines',
'icon': 'cog',
'route': '/machines',
'position': 10,
},
]