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:
cproudlock
2026-02-04 07:32:44 -05:00
parent c4bfdc2db2
commit 9efdb5f52d
89 changed files with 3951 additions and 1138 deletions

View File

@@ -0,0 +1,321 @@
# ShopDB Flask Data Migration Guide
## Overview
This document describes how to migrate data from the legacy `shopdb` database to the new `shopdb_flask` database schema.
## Database Configuration
**Development:**
- Legacy database: `shopdb` (Classic ASP/VBScript schema)
- New database: `shopdb_flask` (Flask/SQLAlchemy schema)
- Connection: `mysql+pymysql://root:rootpassword@127.0.0.1:3306/shopdb_flask`
**Production:**
- Follow the same migration steps on production MySQL server
- Update connection string in `.env` accordingly
## Schema Differences
### Legacy Schema (shopdb)
- `machines` table holds ALL assets (equipment, PCs, network devices)
- `printers` table is separate
- No unified asset abstraction
### New Schema (shopdb_flask)
- `assets` table: Core asset data (shared fields)
- `assettypes` table: Asset category registry
- Plugin extension tables:
- `equipment` - Manufacturing equipment details
- `computers` - PC-specific fields
- `networkdevices` - Network device details
- `printers` - Printer-specific fields
- Each extension links to `assets` via `assetid`
## Migration Steps
### Step 1: Seed Reference Data
```bash
cd /home/camp/projects/shopdb-flask
source venv/bin/activate
flask seed reference-data
```
This creates:
- Asset types (equipment, computer, network_device, printer)
- Asset statuses (In Use, Spare, Retired, etc.)
- Machine types, operating systems, relationship types
### Step 2: Migrate Asset Types
```sql
-- Insert asset types if not exists
INSERT INTO assettypes (assettype, pluginname, tablename, description) VALUES
('equipment', 'equipment', 'equipment', 'Manufacturing equipment'),
('computer', 'computers', 'computers', 'PCs and workstations'),
('network_device', 'network', 'networkdevices', 'Network infrastructure'),
('printer', 'printers', 'printers', 'Printers and MFPs')
ON DUPLICATE KEY UPDATE assettype=assettype;
```
### Step 3: Migrate Equipment
```sql
-- Migrate equipment from legacy machines table
INSERT INTO assets (assetid, assetnumber, name, serialnumber, assettypeid, statusid,
locationid, businessunitid, mapleft, maptop, notes,
createddate, modifieddate, isactive)
SELECT
m.machineid,
m.machinenumber,
m.alias,
m.serialnumber,
(SELECT assettypeid FROM assettypes WHERE assettype = 'equipment'),
m.statusid,
m.locationid,
m.businessunitid,
m.mapleft,
m.maptop,
m.notes,
m.createddate,
m.modifieddate,
m.isactive
FROM shopdb.machines m
JOIN shopdb.machinetypes mt ON m.machinetypeid = mt.machinetypeid
WHERE mt.category = 'Equipment'
AND m.pctypeid IS NULL;
-- Insert equipment extension data
INSERT INTO equipment (assetid, equipmenttypeid, vendorid, modelnumberid,
controllertypeid, controllervendorid, controllermodelid)
SELECT
m.machineid,
m.machinetypeid,
m.vendorid,
m.modelnumberid,
m.controllertypeid,
m.controllervendorid,
m.controllermodelid
FROM shopdb.machines m
JOIN shopdb.machinetypes mt ON m.machinetypeid = mt.machinetypeid
WHERE mt.category = 'Equipment'
AND m.pctypeid IS NULL;
```
### Step 4: Migrate PCs/Computers
```sql
-- Migrate PCs to assets table
INSERT INTO assets (assetid, assetnumber, name, serialnumber, assettypeid, statusid,
locationid, businessunitid, mapleft, maptop, notes,
createddate, modifieddate, isactive)
SELECT
m.machineid,
m.machinenumber,
m.alias,
m.serialnumber,
(SELECT assettypeid FROM assettypes WHERE assettype = 'computer'),
m.statusid,
m.locationid,
m.businessunitid,
m.mapleft,
m.maptop,
m.notes,
m.createddate,
m.modifieddate,
m.isactive
FROM shopdb.machines m
WHERE m.pctypeid IS NOT NULL;
-- Insert computer extension data
INSERT INTO computers (assetid, computertypeid, hostname, osid,
loggedinuser, lastreporteddate, lastboottime,
isvnc, iswinrm, isshopfloor)
SELECT
m.machineid,
m.pctypeid,
m.hostname,
m.osid,
m.loggedinuser,
m.lastreporteddate,
m.lastboottime,
m.isvnc,
m.iswinrm,
m.isshopfloor
FROM shopdb.machines m
WHERE m.pctypeid IS NOT NULL;
```
### Step 5: Migrate Network Devices
```sql
-- Migrate network devices to assets
INSERT INTO assets (assetid, assetnumber, name, serialnumber, assettypeid, statusid,
locationid, businessunitid, mapleft, maptop, notes,
createddate, modifieddate, isactive)
SELECT
m.machineid,
m.machinenumber,
m.alias,
m.serialnumber,
(SELECT assettypeid FROM assettypes WHERE assettype = 'network_device'),
m.statusid,
m.locationid,
m.businessunitid,
m.mapleft,
m.maptop,
m.notes,
m.createddate,
m.modifieddate,
m.isactive
FROM shopdb.machines m
JOIN shopdb.machinetypes mt ON m.machinetypeid = mt.machinetypeid
WHERE mt.category = 'Network';
-- Insert network device extension data
INSERT INTO networkdevices (assetid, networkdevicetypeid, hostname, vendorid, modelnumberid)
SELECT
m.machineid,
m.machinetypeid,
m.hostname,
m.vendorid,
m.modelnumberid
FROM shopdb.machines m
JOIN shopdb.machinetypes mt ON m.machinetypeid = mt.machinetypeid
WHERE mt.category = 'Network';
```
### Step 6: Migrate Printers
```sql
-- Migrate printers to assets (printers are in separate table in legacy)
INSERT INTO assets (assetnumber, name, serialnumber, assettypeid, statusid,
locationid, businessunitid, notes, createddate, modifieddate, isactive)
SELECT
p.hostname,
p.windowsname,
NULL,
(SELECT assettypeid FROM assettypes WHERE assettype = 'printer'),
1, -- Default status
p.locationid,
p.businessunitid,
NULL,
p.createddate,
p.modifieddate,
p.isactive
FROM shopdb.printers p;
-- Insert printer extension data (need to get the new assetid)
INSERT INTO printers (assetid, printertypeid, vendorid, modelnumberid, hostname,
windowsname, sharename, iscsf, installpath, pin,
iscolor, isduplex, isnetwork)
SELECT
a.assetid,
p.printertypeid,
p.vendorid,
p.modelnumberid,
p.hostname,
p.windowsname,
p.sharename,
p.iscsf,
p.installpath,
p.pin,
p.iscolor,
p.isduplex,
p.isnetwork
FROM shopdb.printers p
JOIN assets a ON a.assetnumber = p.hostname
WHERE a.assettypeid = (SELECT assettypeid FROM assettypes WHERE assettype = 'printer');
```
### Step 7: Migrate Communications (IP Addresses)
```sql
-- Migrate communications/IP addresses
INSERT INTO communications (machineid, assetid, comtypeid, address,
subnetid, isprimary, createddate, modifieddate, isactive)
SELECT
c.machineid,
c.machineid, -- assetid = machineid for migrated assets
c.comtypeid,
c.address,
c.subnetid,
c.isprimary,
c.createddate,
c.modifieddate,
c.isactive
FROM shopdb.communications c;
```
### Step 8: Migrate Notifications
```sql
INSERT INTO notifications (notificationid, notificationtypeid, businessunitid, appid,
notification, starttime, endtime, ticketnumber, link,
isactive, isshopfloor, employeesso, employeename)
SELECT * FROM shopdb.notifications;
```
### Step 9: Migrate Supporting Tables
```sql
-- Vendors
INSERT INTO vendors SELECT * FROM shopdb.vendors
ON DUPLICATE KEY UPDATE vendor=VALUES(vendor);
-- Models
INSERT INTO models SELECT * FROM shopdb.models
ON DUPLICATE KEY UPDATE modelnumber=VALUES(modelnumber);
-- Locations
INSERT INTO locations SELECT * FROM shopdb.locations
ON DUPLICATE KEY UPDATE locationname=VALUES(locationname);
-- Business Units
INSERT INTO businessunits SELECT * FROM shopdb.businessunits
ON DUPLICATE KEY UPDATE businessunit=VALUES(businessunit);
-- Subnets
INSERT INTO subnets SELECT * FROM shopdb.subnets
ON DUPLICATE KEY UPDATE subnet=VALUES(subnet);
```
## Verification Queries
```sql
-- Check migration counts
SELECT 'Legacy machines' as source, COUNT(*) as cnt FROM shopdb.machines
UNION ALL SELECT 'New assets', COUNT(*) FROM shopdb_flask.assets
UNION ALL SELECT 'Equipment', COUNT(*) FROM shopdb_flask.equipment
UNION ALL SELECT 'Computers', COUNT(*) FROM shopdb_flask.computers
UNION ALL SELECT 'Network devices', COUNT(*) FROM shopdb_flask.networkdevices
UNION ALL SELECT 'Printers', COUNT(*) FROM shopdb_flask.printers;
-- Verify asset type distribution
SELECT at.assettype, COUNT(a.assetid) as count
FROM shopdb_flask.assets a
JOIN shopdb_flask.assettypes at ON a.assettypeid = at.assettypeid
GROUP BY at.assettype;
```
## Production Deployment Notes
1. **Backup production database first**
2. Create `shopdb_flask` database on production
3. Run `flask db-utils create-all` to create schema
4. Execute migration SQL scripts in order
5. Verify data counts match
6. Update Flask `.env` to point to production database
7. Restart Flask services
## Rollback
If migration fails:
1. Drop all tables in `shopdb_flask`: `flask db-utils drop-all`
2. Recreate schema: `flask db-utils create-all`
3. Investigate and fix migration scripts
4. Re-run migration
---
Last updated: 2026-01-28

View File

@@ -0,0 +1,89 @@
# Fix LocationOnly Equipment Types Migration
## Issue
During the original migration from the VBScript ShopDB site, 110 machines were imported with `LocationOnly` type even though they have models assigned that specify the correct equipment type.
## Root Cause
The migration copied the `machinetypeid` directly from the `machines` table, but many machines had `machinetypeid=1` (LocationOnly) even when they had a model with a proper type.
## Affected Records
- **110 machines** in the `machines` table
- **110 equipment** records in the `equipment` table
## Example
| Machine | Current Type | Should Be | Model |
|---------|-------------|-----------|-------|
| 7502 | LocationOnly | Horizontal Machining Center | a81nx |
| 3108 | LocationOnly | Vertical Lathe | VTM-100 |
| 4001 | LocationOnly | 5-axis Mill | VP9000 |
## Fix SQL
### Step 1: Preview affected records
```sql
-- Preview what will be changed
SELECT
m.machineid,
m.machinenumber,
mt_current.machinetype as current_type,
mt_model.machinetype as correct_type,
mo.modelnumber
FROM machines m
JOIN models mo ON mo.modelnumberid = m.modelnumberid
JOIN machinetypes mt_current ON mt_current.machinetypeid = m.machinetypeid
JOIN machinetypes mt_model ON mt_model.machinetypeid = mo.machinetypeid
WHERE m.machinetypeid = 1 -- Currently LocationOnly
AND mo.machinetypeid != 1; -- Model has a real type
```
### Step 2: Fix the machines table
```sql
-- Update machines table to use the model's machine type
UPDATE machines m
JOIN models mo ON mo.modelnumberid = m.modelnumberid
SET m.machinetypeid = mo.machinetypeid
WHERE m.machinetypeid = 1 -- Currently LocationOnly
AND mo.machinetypeid != 1; -- Model has a real type
```
### Step 3: Fix the equipment table
```sql
-- Update equipment table to match
-- Note: equipmenttypeid values match machinetypeid values
UPDATE equipment e
JOIN assets a ON a.assetid = e.assetid
JOIN machines m ON m.machinenumber = SUBSTRING_INDEX(a.assetnumber, '-', 1)
SET e.equipmenttypeid = m.machinetypeid
WHERE e.equipmenttypeid = 1 -- Currently LocationOnly
AND m.machinetypeid != 1; -- Machine now has correct type
```
### Alternative Step 3 (if asset numbers don't match)
```sql
-- Update equipment based on their own model reference
UPDATE equipment e
JOIN models mo ON mo.modelnumberid = e.modelnumberid
SET e.equipmenttypeid = mo.machinetypeid
WHERE e.equipmenttypeid = 1 -- Currently LocationOnly
AND mo.machinetypeid != 1 -- Model has a real type
AND e.modelnumberid IS NOT NULL;
```
## Verification
```sql
-- Count remaining LocationOnly records that should have been fixed
SELECT COUNT(*) as remaining_fixable
FROM machines m
JOIN models mo ON mo.modelnumberid = m.modelnumberid
WHERE m.machinetypeid = 1
AND mo.machinetypeid != 1;
-- Should return 0 after fix
```
## Notes for Future Migrations
1. When importing equipment, always check if the model has a type and use that as the default
2. Only use "LocationOnly" for genuine location markers (rooms, offices) without models
3. Validate after import: any equipment with a model should NOT be LocationOnly
## Date Created
2026-01-27

View File

@@ -0,0 +1,92 @@
# Migrate USB Devices from Equipment Table
## Issue
6 USB devices were incorrectly stored in the `equipment` table instead of the proper `usbdevices` table.
## Status
- [x] `pin` field added to `usbdevices` table for encrypted devices
- [x] `pin` field added to USBDevice model
- [x] USB Device equipment type deactivated (equipmenttypeid=44)
- [ ] Migrate 6 USB devices to usbdevices table
## Affected Records
| Asset Number | Name | Serial Number |
|--------------|------|---------------|
| 82841957-5891 | Green Kingston 64GB | 82841957 |
| 48854302-5892 | Blue Kingston 64GB | 48854302 |
| 75953637-5893 | Blue Kingston 64GB USB 3.0 | 75953637 |
| 41299370-5904 | Lenovo Portable DVD | 41299370 |
| 15492331-5905 | TEAC Portable Floppy Drive | 15492331 |
| 25777358-5906 | Netgear WiFi Adapter | 25777358 |
## Migration SQL
### Step 1: Insert into usbdevices
```sql
INSERT INTO usbdevices (
serialnumber,
label,
assetnumber,
usbdevicetypeid,
storagelocation,
notes,
createddate,
modifieddate,
isactive
)
SELECT
a.serialnumber,
a.name,
a.assetnumber,
NULL, -- Assign proper type later
NULL, -- Storage location TBD
a.notes,
a.createddate,
a.modifieddate,
a.isactive
FROM assets a
JOIN equipment e ON e.assetid = a.assetid
WHERE e.equipmenttypeid = 44; -- USB Device type
```
### Step 2: Delete from equipment table
```sql
DELETE e FROM equipment e
JOIN assets a ON a.assetid = e.assetid
WHERE e.equipmenttypeid = 44;
```
### Step 3: Delete from assets table
```sql
DELETE a FROM assets a
JOIN equipment e ON e.assetid = a.assetid
WHERE e.equipmenttypeid = 44;
-- Note: Run step 2 first since it references assets
```
### Alternative: Manual Migration
Since there are only 6 devices, you may prefer to:
1. Manually create them in the USB Devices section
2. Delete the equipment/asset records
## USB Device Types to Create
```sql
-- Check if types exist, create if needed
INSERT IGNORE INTO usbdevicetypes (typename, description, icon) VALUES
('Flash Drive', 'USB flash drive / thumb drive', 'usb'),
('External HDD', 'External hard disk drive', 'harddisk'),
('External SSD', 'External solid state drive', 'harddisk'),
('Card Reader', 'SD/CF card reader', 'sdcard'),
('Optical Drive', 'External CD/DVD/Blu-ray drive', 'disc'),
('WiFi Adapter', 'USB wireless network adapter', 'wifi'),
('Other', 'Other USB device', 'usb');
```
## Notes
- USB Device equipment type has been deactivated (isactive=0)
- New USB devices should be created in the USB Devices section
- USB devices do NOT need map positions (no mapleft/maptop)
## Date Created
2026-01-27

View File

@@ -0,0 +1,165 @@
# Production Migration Guide
## Overview
This guide documents the process for migrating data from the legacy VBScript ShopDB site to the new Flask-based ShopDB.
## Database Architecture
### Legacy System (VBScript)
- Single `machines` table containing all equipment, PCs, printers
- `machinetypes` table for classification
- `models` table with `machinetypeid` reference
### New System (Flask)
- Core `assets` table (unified asset registry)
- Plugin-specific extension tables:
- `equipment` (equipmenttypeid → equipmenttypes)
- `computers` (computertypeid → computertypes)
- `printers` (printertypeid → printertypes)
- `network_device` (networkdevicetypeid → networkdevicetypes)
## Key Mappings
### Asset Type Mapping
| Legacy Category | New Asset Type | Extension Table |
|-----------------|---------------|-----------------|
| Equipment machines | Equipment | equipment |
| PC/Computer | Computer | computers |
| Printer | Printer | printers |
| Network (IDF, Switch, AP) | Network Device | network_device |
### Type ID Alignment
The `equipmenttypes` table IDs match `machinetypes` IDs for easy migration:
- equipmenttypeid = machinetypeid (where applicable)
## Migration Steps
### Step 1: Export from Legacy Database
```sql
-- Export machines with all related data
SELECT
m.*,
mt.machinetype,
mo.modelnumber,
mo.machinetypeid as model_typeid,
v.vendor,
bu.businessunit,
s.status
FROM machines m
LEFT JOIN machinetypes mt ON mt.machinetypeid = m.machinetypeid
LEFT JOIN models mo ON mo.modelnumberid = m.modelnumberid
LEFT JOIN vendors v ON v.vendorid = m.vendorid
LEFT JOIN businessunits bu ON bu.businessunitid = m.businessunitid
LEFT JOIN statuses s ON s.statusid = m.statusid;
```
### Step 2: Create Assets
For each machine, create an asset record:
```sql
INSERT INTO assets (assetnumber, name, assettypeid, statusid, locationid, businessunitid, mapleft, maptop)
SELECT
CONCAT(machinenumber, '-', machineid), -- Unique asset number
alias,
CASE
WHEN category = 'PC' THEN 2 -- Computer
WHEN category = 'Printer' THEN 4 -- Printer
ELSE 1 -- Equipment
END,
statusid,
locationid,
businessunitid,
mapleft,
maptop
FROM machines;
```
### Step 3: Create Extension Records
```sql
-- For Equipment
INSERT INTO equipment (assetid, equipmenttypeid, vendorid, modelnumberid)
SELECT
a.assetid,
COALESCE(mo.machinetypeid, m.machinetypeid), -- Use model's type if available!
m.vendorid,
m.modelnumberid
FROM machines m
JOIN assets a ON a.assetnumber = CONCAT(m.machinenumber, '-', m.machineid)
LEFT JOIN models mo ON mo.modelnumberid = m.modelnumberid
WHERE m.category = 'Equipment' OR m.category IS NULL;
```
### Step 4: Post-Migration Fixes
#### Fix LocationOnly Equipment Types
Equipment imported with LocationOnly type should inherit type from their model:
```sql
-- Fix equipment that has a model with a proper type
UPDATE equipment e
JOIN assets a ON a.assetid = e.assetid
JOIN machines m ON m.machinenumber = SUBSTRING_INDEX(a.assetnumber, '-', 1)
JOIN models mo ON mo.modelnumberid = m.modelnumberid
SET e.equipmenttypeid = mo.machinetypeid
WHERE e.equipmenttypeid = 1 -- LocationOnly
AND mo.machinetypeid != 1; -- Model has real type
```
## Validation Queries
### Check for Orphaned Assets
```sql
SELECT a.* FROM assets a
LEFT JOIN equipment e ON e.assetid = a.assetid
LEFT JOIN computers c ON c.assetid = a.assetid
LEFT JOIN printers p ON p.assetid = a.assetid
WHERE a.assettypeid = 1 -- Equipment type
AND e.assetid IS NULL;
```
### Check LocationOnly with Models
```sql
-- Should return 0 after migration fix
SELECT COUNT(*)
FROM equipment e
JOIN assets a ON a.assetid = e.assetid
JOIN machines m ON m.machinenumber = SUBSTRING_INDEX(a.assetnumber, '-', 1)
JOIN models mo ON mo.modelnumberid = m.modelnumberid
WHERE e.equipmenttypeid = 1
AND mo.machinetypeid != 1;
```
### Verify Type Distribution
```sql
SELECT et.equipmenttype, COUNT(*) as count
FROM equipment e
JOIN equipmenttypes et ON et.equipmenttypeid = e.equipmenttypeid
GROUP BY et.equipmenttype
ORDER BY count DESC;
```
## Common Issues
### Issue: Equipment marked as LocationOnly but has model
**Cause**: Migration copied machinetypeid from machines table instead of using model's type
**Fix**: See FIX_LOCATIONONLY_EQUIPMENT_TYPES.md
### Issue: Missing model relationship
**Cause**: Equipment table modelnumberid not populated during migration
**Fix**: Link through machines table using asset number pattern
### Issue: Duplicate asset numbers
**Cause**: Asset number generation didn't account for existing duplicates
**Fix**: Use unique suffix or check before insert
## Scripts Location
- `/migrations/FIX_LOCATIONONLY_EQUIPMENT_TYPES.md` - Fix for LocationOnly type issue
- `/scripts/import_from_mysql.py` - Original import script (may need updates)
- `/scripts/migration/` - Migration utilities
## Notes
- Always backup before running migration fixes
- Test on staging/dev before production
- Verify counts before and after each fix
- Keep legacy `machines` table for reference during transition
## Date Created
2026-01-27

View File

@@ -0,0 +1,23 @@
-- Rename database columns to remove underscores per naming convention
-- Date: 2026-02-03
-- Affects: assettypes, assetrelationships, equipment, usbcheckouts
-- 1. assettypes table
ALTER TABLE assettypes CHANGE COLUMN plugin_name pluginname VARCHAR(100) NULL COMMENT 'Plugin that owns this type';
ALTER TABLE assettypes CHANGE COLUMN table_name tablename VARCHAR(100) NULL COMMENT 'Extension table name for this type';
-- 2. assetrelationships table
ALTER TABLE assetrelationships CHANGE COLUMN source_assetid sourceassetid INT NOT NULL;
ALTER TABLE assetrelationships CHANGE COLUMN target_assetid targetassetid INT NOT NULL;
-- 3. equipment table
ALTER TABLE equipment CHANGE COLUMN controller_vendorid controllervendorid INT NULL COMMENT 'Controller vendor (e.g., FANUC)';
ALTER TABLE equipment CHANGE COLUMN controller_modelid controllermodelid INT NULL COMMENT 'Controller model (e.g., 31B)';
-- 4. usbcheckouts table
ALTER TABLE usbcheckouts CHANGE COLUMN checkout_name checkoutname VARCHAR(100) NULL COMMENT 'Name of user';
ALTER TABLE usbcheckouts CHANGE COLUMN checkout_time checkouttime DATETIME NOT NULL;
ALTER TABLE usbcheckouts CHANGE COLUMN checkin_time checkintime DATETIME NULL;
ALTER TABLE usbcheckouts CHANGE COLUMN checkout_reason checkoutreason TEXT NULL COMMENT 'Reason for checkout';
ALTER TABLE usbcheckouts CHANGE COLUMN checkin_notes checkinnotes TEXT NULL;
ALTER TABLE usbcheckouts CHANGE COLUMN was_wiped waswiped TINYINT(1) NULL COMMENT 'Was device wiped after return';