Add USB, Notifications, Network plugins and reusable EmployeeSearch component

New Plugins:
- USB plugin: Device checkout/checkin with employee lookup, checkout history
- Notifications plugin: Announcements with types, scheduling, shopfloor display
- Network plugin: Network device management with subnets and VLANs
- Equipment and Computers plugins: Asset type separation

Frontend:
- EmployeeSearch component: Reusable employee lookup with autocomplete
- USB views: List, detail, checkout/checkin modals
- Notifications views: List, form with recognition mode
- Network views: Device list, detail, form
- Calendar view with FullCalendar integration
- Shopfloor and TV dashboard views
- Reports index page
- Map editor for asset positioning
- Light/dark mode fixes for map tooltips

Backend:
- Employee search API with external lookup service
- Collector API for PowerShell data collection
- Reports API endpoints
- Slides API for TV dashboard
- Fixed AppVersion model (removed BaseModel inheritance)
- Added checkout_name column to usbcheckouts table

Styling:
- Unified detail page styles
- Improved pagination (page numbers instead of prev/next)
- Dark/light mode theme improvements

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-01-21 16:37:49 -05:00
parent 02d83335ee
commit 9c220a4194
110 changed files with 17693 additions and 600 deletions

View File

@@ -0,0 +1,139 @@
"""
Migrate notifications from legacy database.
This script migrates notification data from the VBScript database
to the new notifications plugin schema.
Usage:
python -m scripts.migration.migrate_notifications --source <connection_string>
"""
import argparse
import logging
from datetime import datetime
from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def get_notification_type_mapping(target_session):
"""Get mapping of type names to IDs in target database."""
result = target_session.execute(text(
"SELECT notificationtypeid, typename FROM notificationtypes"
))
return {row.typename.lower(): row.notificationtypeid for row in result}
def run_migration(source_conn_str, target_conn_str, dry_run=False):
"""
Run notification migration.
Args:
source_conn_str: Source database connection string
target_conn_str: Target database connection string
dry_run: If True, don't commit changes
"""
source_engine = create_engine(source_conn_str)
target_engine = create_engine(target_conn_str)
SourceSession = sessionmaker(bind=source_engine)
TargetSession = sessionmaker(bind=target_engine)
source_session = SourceSession()
target_session = TargetSession()
try:
# Get type mappings
type_mapping = get_notification_type_mapping(target_session)
# Default type if not found
default_type_id = type_mapping.get('general', 1)
# Fetch notifications from source
# Adjust column names based on actual legacy schema
notifications = source_session.execute(text("""
SELECT n.*, nt.typename
FROM notifications n
LEFT JOIN notificationtypes nt ON n.notificationtypeid = nt.notificationtypeid
"""))
migrated = 0
errors = 0
for notif in notifications:
notif_dict = dict(notif._mapping)
try:
# Map notification type
type_name = (notif_dict.get('typename') or 'general').lower()
type_id = type_mapping.get(type_name, default_type_id)
# Insert into target
target_session.execute(text("""
INSERT INTO notifications (
title, message, notificationtypeid,
startdate, enddate, ispinned, showbanner, allday,
linkurl, affectedsystems, isactive, createddate
) VALUES (
:title, :message, :notificationtypeid,
:startdate, :enddate, :ispinned, :showbanner, :allday,
:linkurl, :affectedsystems, :isactive, :createddate
)
"""), {
'title': notif_dict.get('title', 'Untitled'),
'message': notif_dict.get('message', ''),
'notificationtypeid': type_id,
'startdate': notif_dict.get('startdate', datetime.utcnow()),
'enddate': notif_dict.get('enddate'),
'ispinned': notif_dict.get('ispinned', False),
'showbanner': notif_dict.get('showbanner', True),
'allday': notif_dict.get('allday', True),
'linkurl': notif_dict.get('linkurl'),
'affectedsystems': notif_dict.get('affectedsystems'),
'isactive': notif_dict.get('isactive', True),
'createddate': notif_dict.get('createddate', datetime.utcnow()),
})
migrated += 1
except Exception as e:
logger.error(f"Error migrating notification: {e}")
errors += 1
if dry_run:
logger.info("Dry run - rolling back changes")
target_session.rollback()
else:
target_session.commit()
logger.info(f"Migration complete: {migrated} migrated, {errors} errors")
finally:
source_session.close()
target_session.close()
def main():
parser = argparse.ArgumentParser(description='Migrate notifications')
parser.add_argument('--source', required=True, help='Source database connection string')
parser.add_argument('--target', help='Target database connection string')
parser.add_argument('--dry-run', action='store_true', help='Dry run without committing')
args = parser.parse_args()
target = args.target
if not target:
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
from shopdb import create_app
app = create_app()
target = app.config['SQLALCHEMY_DATABASE_URI']
run_migration(args.source, target, args.dry_run)
if __name__ == '__main__':
main()