Migrate PC form + collector off legacy Machine onto the asset/computer model

- PCForm saves/loads via computersApi (asset core + computer extension +
  primary IP in one call); PC Type now uses the dedicated computertypes table
  instead of MachineType, fixing the cross-table id mismatch.
- Collector (/api/collector/*) writes the Computer model: lookup by hostname
  or asset number, update loggedinuser/lastreporteddate/lastboottime + asset
  serial, installed apps via ComputerInstalledApp.
- Add computers.vendorid + modelnumberid (PCs carry make/model) and
  computerinstalledapps.installedversion; computer GET now includes
  communications. Wire COLLECTOR_API_KEY into config.

Retires the last write paths to the Machine model for PCs (ADR-001).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-06-26 09:16:09 -04:00
parent b516b9b771
commit e1948a4774
5 changed files with 374 additions and 377 deletions

View File

@@ -4,7 +4,10 @@ from flask import Blueprint, request
from flask_jwt_extended import jwt_required
from shopdb.extensions import db
from shopdb.core.models import Asset, AssetType, OperatingSystem, Application, AppVersion, AuditLog
from shopdb.core.models import (
Asset, AssetType, OperatingSystem, Application, AppVersion, AuditLog,
Communication, CommunicationType,
)
from shopdb.utils.responses import (
success_response,
error_response,
@@ -225,6 +228,10 @@ def get_computer(computer_id: int):
result = comp.asset.to_dict() if comp.asset else {}
result['computer'] = comp.to_dict()
result['communications'] = [
c.to_dict() for c in
Communication.query.filter_by(assetid=comp.assetid).all()
]
return success_response(result)
@@ -339,6 +346,8 @@ def create_computer():
computertypeid=data.get('computertypeid'),
hostname=data.get('hostname'),
osid=data.get('osid'),
vendorid=data.get('vendorid'),
modelnumberid=data.get('modelnumberid'),
loggedinuser=data.get('loggedinuser'),
lastreporteddate=data.get('lastreporteddate'),
lastboottime=data.get('lastboottime'),
@@ -350,6 +359,17 @@ def create_computer():
db.session.add(comp)
db.session.flush()
# Optional primary IP communication
if data.get('ipaddress'):
ip_comtype = CommunicationType.query.filter_by(comtype='IP').first()
if ip_comtype:
db.session.add(Communication(
assetid=asset.assetid,
comtypeid=ip_comtype.comtypeid,
ipaddress=data['ipaddress'],
isprimary=True,
))
# Audit log
AuditLog.log('created', 'Computer', entityid=comp.computerid,
entityname=data.get('hostname') or data['assetnumber'])
@@ -415,8 +435,9 @@ def update_computer(computer_id: int):
setattr(asset, key, data[key])
# Update computer fields
computer_fields = ['computertypeid', 'hostname', 'osid', 'loggedinuser',
'lastreporteddate', 'lastboottime', 'isvnc', 'iswinrm', 'isshopfloor']
computer_fields = ['computertypeid', 'hostname', 'osid', 'vendorid',
'modelnumberid', 'loggedinuser', 'lastreporteddate',
'lastboottime', 'isvnc', 'iswinrm', 'isshopfloor']
for key in computer_fields:
if key in data:
old_val = getattr(comp, key)
@@ -425,6 +446,23 @@ def update_computer(computer_id: int):
changes[key] = {'old': old_val, 'new': new_val}
setattr(comp, key, data[key])
# Upsert the primary IP communication so a single PUT covers it
if 'ipaddress' in data:
ip = (data.get('ipaddress') or '').strip()
primary = Communication.query.filter_by(
assetid=asset.assetid, isprimary=True).first()
if ip:
if primary:
primary.ipaddress = ip
else:
ip_comtype = CommunicationType.query.filter_by(comtype='IP').first()
if ip_comtype:
db.session.add(Communication(
assetid=asset.assetid, comtypeid=ip_comtype.comtypeid,
ipaddress=ip, isprimary=True))
elif primary:
primary.ipaddress = None
# Audit log if there were changes
if changes:
AuditLog.log('updated', 'Computer', entityid=comp.computerid,

View File

@@ -0,0 +1,44 @@
"""Add PC hardware make/model + installed-app version string
Adds computers.vendorid + computers.modelnumberid (PCs carry vendor/model like
equipment) and computerinstalledapps.installedversion (raw version string from
automated collection, when there is no curated AppVersion). Supports moving the
PC form and the collector off the legacy Machine model onto the asset/computer
model (ADR-001).
Revision ID: 0002_pc_hardware
Revises: 0001_baseline_computers
Create Date: 2026-06-26
"""
from alembic import op
import sqlalchemy as sa
revision = '0002_pc_hardware'
down_revision = '0001_baseline_computers'
branch_labels = None
depends_on = None
def upgrade():
with op.batch_alter_table('computers') as batch_op:
batch_op.add_column(sa.Column('vendorid', sa.Integer(), nullable=True))
batch_op.add_column(sa.Column('modelnumberid', sa.Integer(), nullable=True))
batch_op.create_foreign_key('fk_computers_vendor', 'vendors',
['vendorid'], ['vendorid'])
batch_op.create_foreign_key('fk_computers_model', 'models',
['modelnumberid'], ['modelnumberid'])
with op.batch_alter_table('computerinstalledapps') as batch_op:
batch_op.add_column(sa.Column('installedversion', sa.String(length=100),
nullable=True))
def downgrade():
with op.batch_alter_table('computerinstalledapps') as batch_op:
batch_op.drop_column('installedversion')
with op.batch_alter_table('computers') as batch_op:
batch_op.drop_constraint('fk_computers_model', type_='foreignkey')
batch_op.drop_constraint('fk_computers_vendor', type_='foreignkey')
batch_op.drop_column('modelnumberid')
batch_op.drop_column('vendorid')

View File

@@ -62,6 +62,18 @@ class Computer(BaseModel):
nullable=True
)
# Hardware make/model (PCs carry vendor + model like equipment)
vendorid = db.Column(
db.Integer,
db.ForeignKey('vendors.vendorid'),
nullable=True
)
modelnumberid = db.Column(
db.Integer,
db.ForeignKey('models.modelnumberid'),
nullable=True
)
# Status tracking
loggedinuser = db.Column(db.String(100), nullable=True)
lastreporteddate = db.Column(db.DateTime, nullable=True)
@@ -93,6 +105,8 @@ class Computer(BaseModel):
)
computertype = db.relationship('ComputerType', backref='computers')
operatingsystem = db.relationship('OperatingSystem', backref='computers')
vendor = db.relationship('Vendor')
model = db.relationship('Model')
# Installed applications (one-to-many)
installedapps = db.relationship(
@@ -120,6 +134,10 @@ class Computer(BaseModel):
result['computertypename'] = self.computertype.computertype
if self.operatingsystem:
result['osname'] = self.operatingsystem.osname
if self.vendor:
result['vendorname'] = self.vendor.vendor
if self.model:
result['modelname'] = self.model.modelnumber
return result
@@ -149,6 +167,8 @@ class ComputerInstalledApp(db.Model):
db.ForeignKey('appversions.appversionid'),
nullable=True
)
# Raw version string from automated collection (when no curated AppVersion)
installedversion = db.Column(db.String(100), nullable=True)
isactive = db.Column(db.Boolean, default=True, nullable=False)
installeddate = db.Column(db.DateTime, default=db.func.now())