feat: Implement modular plugin architecture

- Convert invcount to self-contained module
- Add Module Manager for install/uninstall
- Create module_registry database table
- Support hot-reloading of modules
- Move data imports into invcount module
- Update all templates and routes to new structure

Version bumped to 0.16.0
This commit is contained in:
Javier
2026-02-07 01:47:49 -06:00
parent 2a649fdbcc
commit 406219547d
35 changed files with 3887 additions and 492 deletions

View File

@@ -1,12 +1,8 @@
"""
ScanLook Database Migration System
ScanLook Core Database Migration System
Simple migration system that tracks and applies database changes.
Each migration has a version number and an up() function.
Usage:
from migrations import run_migrations
run_migrations() # Call on app startup
IMPORTANT: This file only contains CORE system migrations.
Module-specific migrations are in each module's migrations.py file.
"""
import sqlite3
@@ -75,19 +71,13 @@ def table_exists(table):
# ============================================
# MIGRATIONS
# CORE SYSTEM MIGRATIONS ONLY
# ============================================
# Add new migrations to this list.
# Each migration is a tuple: (version, name, up_function)
#
# RULES:
# - Never modify an existing migration
# - Always add new migrations at the end with the next version number
# - Check if changes are needed before applying (idempotent)
# Module-specific migrations are handled by each module's migrations.py
# ============================================
def migration_001_add_modules_tables():
"""Add Modules and UserModules tables"""
"""Add Modules and UserModules tables (if not created by init_db)"""
conn = get_db()
if not table_exists('Modules'):
@@ -141,132 +131,42 @@ def migration_002_add_usermodules_granted_columns():
conn.close()
def migration_003_add_default_modules():
"""Add default modules if they don't exist"""
def migration_003_add_module_registry():
"""Add module_registry table for new module manager system"""
conn = get_db()
# Check if modules exist
existing = conn.execute('SELECT COUNT(*) as cnt FROM Modules').fetchone()
if existing['cnt'] == 0:
if not table_exists('module_registry'):
conn.execute('''
INSERT INTO Modules (module_name, module_key, description, icon, is_active, display_order)
VALUES ('Inventory Counts', 'counting', 'Cycle counts and physical inventory', 'fa-clipboard-check', 1, 1)
CREATE TABLE module_registry (
id INTEGER PRIMARY KEY AUTOINCREMENT,
module_key TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
version TEXT NOT NULL,
author TEXT,
description TEXT,
is_installed INTEGER DEFAULT 0,
is_active INTEGER DEFAULT 0,
installed_at TEXT,
config_json TEXT
)
''')
conn.execute('''
INSERT INTO Modules (module_name, module_key, description, icon, is_active, display_order)
VALUES ('Consumption Sheets', 'cons_sheets', 'Production consumption tracking', 'fa-clipboard-list', 1, 2)
''')
print(" Added default modules")
print(" Created module_registry table")
conn.commit()
conn.close()
def migration_004_assign_modules_to_admins():
"""Auto-assign all modules to owner and admin users"""
conn = get_db()
# Get admin users
admins = conn.execute('SELECT user_id FROM Users WHERE role IN ("owner", "admin")').fetchall()
modules = conn.execute('SELECT module_id FROM Modules').fetchall()
for user in admins:
for module in modules:
try:
conn.execute('''
INSERT INTO UserModules (user_id, module_id)
VALUES (?, ?)
''', [user['user_id'], module['module_id']])
except sqlite3.IntegrityError:
pass # Already assigned
conn.commit()
conn.close()
print(" Assigned modules to admin users")
def migration_005_add_cons_process_fields_duplicate_key():
"""Add is_duplicate_key column to cons_process_fields if missing"""
conn = get_db()
if table_exists('cons_process_fields'):
if not column_exists('cons_process_fields', 'is_duplicate_key'):
conn.execute('ALTER TABLE cons_process_fields ADD COLUMN is_duplicate_key INTEGER DEFAULT 0')
print(" Added is_duplicate_key column to cons_process_fields")
conn.commit()
conn.close()
def migration_006_add_is_deleted_to_locationcounts():
"""Add is_deleted column to LocationCounts table"""
conn = get_db()
if table_exists('LocationCounts'):
if not column_exists('LocationCounts', 'is_deleted'):
conn.execute('ALTER TABLE LocationCounts ADD COLUMN is_deleted INTEGER DEFAULT 0')
print(" Added is_deleted column to LocationCounts")
conn.commit()
conn.close()
def migration_007_add_detail_end_row():
"""Add detail_end_row column to cons_processes table"""
conn = get_db()
if table_exists('cons_processes'):
if not column_exists('cons_processes', 'detail_end_row'):
conn.execute('ALTER TABLE cons_processes ADD COLUMN detail_end_row INTEGER')
print(" Added detail_end_row column to cons_processes")
conn.commit()
conn.close()
def migration_008_add_page_height():
"""Add page_height column to cons_processes table"""
conn = get_db()
if table_exists('cons_processes'):
if not column_exists('cons_processes', 'page_height'):
conn.execute('ALTER TABLE cons_processes ADD COLUMN page_height INTEGER')
print(" Added page_height column to cons_processes")
conn.commit()
conn.close()
def migration_009_add_print_columns():
"""Add print_start_col and print_end_col to cons_processes"""
conn = get_db()
if table_exists('cons_processes'):
if not column_exists('cons_processes', 'print_start_col'):
conn.execute('ALTER TABLE cons_processes ADD COLUMN print_start_col TEXT DEFAULT "A"')
print(" Added print_start_col")
if not column_exists('cons_processes', 'print_end_col'):
conn.execute('ALTER TABLE cons_processes ADD COLUMN print_end_col TEXT')
print(" Added print_end_col")
conn.commit()
conn.close()
# List of all migrations in order
# List of CORE migrations only
MIGRATIONS = [
(1, 'add_modules_tables', migration_001_add_modules_tables),
(2, 'add_usermodules_granted_columns', migration_002_add_usermodules_granted_columns),
(3, 'add_default_modules', migration_003_add_default_modules),
(4, 'assign_modules_to_admins', migration_004_assign_modules_to_admins),
(5, 'add_cons_process_fields_duplicate_key', migration_005_add_cons_process_fields_duplicate_key),
(6, 'add_is_deleted_to_locationcounts', migration_006_add_is_deleted_to_locationcounts),
(7, 'add_detail_end_row', migration_007_add_detail_end_row),
(8, 'add_page_height', migration_008_add_page_height),
(9, 'add_print_columns', migration_009_add_print_columns),
(3, 'add_module_registry', migration_003_add_module_registry),
]
def run_migrations():
"""Run all pending migrations"""
print("🔄 Checking database migrations...")
"""Run all pending core migrations"""
print("🔄 Checking core database migrations...")
# Make sure migrations table exists
init_migrations_table()
@@ -278,10 +178,10 @@ def run_migrations():
pending = [(v, n, f) for v, n, f in MIGRATIONS if v not in applied]
if not pending:
print("Database is up to date")
print("Core database is up to date")
return
print(f"📦 Running {len(pending)} migration(s)...")
print(f"📦 Running {len(pending)} core migration(s)...")
for version, name, func in pending:
print(f"\n Migration {version}: {name}")
@@ -293,8 +193,8 @@ def run_migrations():
print(f" ❌ Migration {version} failed: {e}")
raise
print("\n✅ All migrations complete")
print("\n✅ All core migrations complete")
if __name__ == '__main__':
run_migrations()
run_migrations()