Files
ScanLook/app.py
Javier 406219547d 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
2026-02-07 01:47:49 -06:00

294 lines
10 KiB
Python

"""
ScanLook - Modular Inventory Management System
Flask Application
Production-Ready Release with Module System
"""
from flask import Flask, render_template, request, redirect, url_for, session, flash, jsonify, send_from_directory
from werkzeug.security import check_password_hash
import os
from datetime import timedelta
# Initialize App FIRST to prevent circular import errors
app = Flask(__name__)
# Now import your custom modules
from db import query_db, execute_db, get_db
from blueprints.users import users_bp
from utils import login_required
# Register Core Blueprints (non-modular)
app.register_blueprint(users_bp)
# V1.0: Use environment variable for production, fallback to demo key for development
app.secret_key = os.environ.get('SCANLOOK_SECRET_KEY', 'scanlook-demo-key-replace-for-production')
app.config['DATABASE'] = os.path.join(os.path.dirname(__file__), 'database', 'scanlook.db')
# Session timeout: 1 hour (auto-logout after idle period)
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=1)
# 1. Define the version
APP_VERSION = '0.16.0' # Bumped version for modular architecture
# 2. Inject it into all templates automatically
@app.context_processor
def inject_version():
return dict(version=APP_VERSION)
# Auto-initialize database if it doesn't exist
db_path = os.path.join(os.path.dirname(__file__), 'database', 'scanlook.db')
if not os.path.exists(db_path):
print("Database not found, initializing...")
from database.init_db import init_database, create_default_users
init_database()
create_default_users()
print("Database initialized!")
print("📦 Install modules from /admin/modules")
# Run migrations to apply any pending database changes
from migrations import run_migrations
run_migrations()
# Load and register active modules
from module_manager import get_module_manager
module_manager = get_module_manager()
module_manager.load_active_modules(app)
# ==================== ROUTES: AUTHENTICATION ====================
@app.route('/')
def index():
"""Landing page - redirect based on login status"""
if 'user_id' in session:
return redirect(url_for('home'))
return redirect(url_for('login'))
@app.route('/login', methods=['GET', 'POST'])
def login():
"""Login page"""
if request.method == 'POST':
username = request.form.get('username', '').strip()
password = request.form.get('password', '')
user = query_db('SELECT * FROM Users WHERE username = ? AND is_active = 1', [username], one=True)
if user and check_password_hash(user['password'], password):
session.permanent = True # Enable session timeout
session['user_id'] = user['user_id']
session['username'] = user['username']
session['full_name'] = user['full_name']
session['role'] = user['role']
flash(f'Welcome back, {user["full_name"]}!', 'success')
return redirect(url_for('home'))
else:
flash('Invalid username or password', 'danger')
return render_template('login.html')
@app.route('/logout')
def logout():
"""Logout"""
session.clear()
flash('You have been logged out', 'info')
return redirect(url_for('login'))
# ==================== ROUTES: HOME ====================
@app.route('/home')
@login_required
def home():
"""Module selection landing page"""
user_id = session.get('user_id')
# Get modules this user has access to
modules = query_db('''
SELECT m.module_id, m.module_name, m.module_key, m.description, m.icon
FROM Modules m
JOIN UserModules um ON m.module_id = um.module_id
WHERE um.user_id = ? AND m.is_active = 1
ORDER BY m.display_order
''', [user_id])
return render_template('home.html', modules=modules)
# ==================== ROUTES: ADMIN DASHBOARD ====================
@app.route('/admin')
@login_required
def admin_dashboard():
"""Admin dashboard - shows all available modules"""
role = session.get('role')
if role not in ['owner', 'admin']:
flash('Access denied. Admin role required.', 'danger')
return redirect(url_for('home'))
# Get modules this user has access to
user_id = session.get('user_id')
modules = query_db('''
SELECT m.module_id, m.module_name, m.module_key, m.description, m.icon
FROM Modules m
JOIN UserModules um ON m.module_id = um.module_id
WHERE um.user_id = ? AND m.is_active = 1
ORDER BY m.display_order
''', [user_id])
return render_template('admin_dashboard.html', modules=modules)
# ==================== MODULE MANAGER UI ====================
@app.route('/admin/modules')
@login_required
def module_manager_ui():
"""Module manager interface for admins"""
if session.get('role') not in ['owner', 'admin']:
flash('Access denied. Admin role required.', 'danger')
return redirect(url_for('home'))
modules = module_manager.scan_available_modules()
return render_template('module_manager.html', modules=modules)
@app.route('/admin/modules/<module_key>/install', methods=['POST'])
@login_required
def install_module(module_key):
"""Install a module"""
if session.get('role') not in ['owner', 'admin']:
return jsonify({'success': False, 'message': 'Access denied'}), 403
result = module_manager.install_module(module_key)
# Hot-reload: Register the blueprint immediately if installation succeeded
if result['success']:
try:
from pathlib import Path
import importlib.util
import sys
module = module_manager.get_module_by_key(module_key)
if module:
init_path = Path(module['path']) / '__init__.py'
# Import the module
spec = importlib.util.spec_from_file_location(
f"modules.{module_key}",
init_path
)
module_package = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module_package
spec.loader.exec_module(module_package)
# Create and register blueprint
if hasattr(module_package, 'create_blueprint'):
blueprint = module_package.create_blueprint()
app.register_blueprint(blueprint)
print(f"🔥 Hot-loaded: {module['name']} at {module.get('routes_prefix')}")
result['message'] += ' (Module loaded - no restart needed!)'
else:
print(f"⚠️ Module {module_key} missing create_blueprint()")
result['message'] += ' (Restart required - missing create_blueprint)'
else:
print(f"⚠️ Could not find module {module_key} after installation")
result['message'] += ' (Restart required - module not found)'
except Exception as e:
print(f"❌ Hot-reload failed for {module_key}: {e}")
import traceback
traceback.print_exc()
result['message'] += f' (Restart required - hot-reload failed)'
return jsonify(result)
@app.route('/admin/modules/<module_key>/uninstall', methods=['POST'])
@login_required
def uninstall_module(module_key):
"""Uninstall a module"""
if session.get('role') not in ['owner', 'admin']:
return jsonify({'success': False, 'message': 'Access denied'}), 403
result = module_manager.uninstall_module(module_key, drop_tables=True)
return jsonify(result)
@app.route('/admin/modules/<module_key>/activate', methods=['POST'])
@login_required
def activate_module(module_key):
"""Activate a module"""
if session.get('role') not in ['owner', 'admin']:
return jsonify({'success': False, 'message': 'Access denied'}), 403
result = module_manager.activate_module(module_key)
return jsonify(result)
@app.route('/admin/modules/<module_key>/deactivate', methods=['POST'])
@login_required
def deactivate_module(module_key):
"""Deactivate a module"""
if session.get('role') not in ['owner', 'admin']:
return jsonify({'success': False, 'message': 'Access denied'}), 403
result = module_manager.deactivate_module(module_key)
return jsonify(result)
# ==================== PWA SUPPORT ROUTES ====================
@app.route('/manifest.json')
def serve_manifest():
"""Serve the PWA manifest file from the static directory"""
return send_from_directory('static', 'manifest.json')
@app.route('/sw.js')
def serve_sw():
"""Serve the Service Worker file from the static directory"""
return send_from_directory('static', 'sw.js')
# ==================== Temp delete me later ====================
@app.route('/whatami')
def whatami():
"""Temporary route to identify device user-agent"""
ua = request.headers.get('User-Agent', 'Unknown')
return f"""
<html>
<head><meta name="viewport" content="width=device-width, initial-scale=1"></head>
<body style="font-family: monospace; padding: 20px; word-wrap: break-word;">
<h1>Device Info</h1>
<h3>User-Agent:</h3>
<p style="background: #eee; padding: 10px;">{ua}</p>
<h3>Screen Size:</h3>
<p id="screen" style="background: #eee; padding: 10px;">Loading...</p>
<h3>Viewport Size:</h3>
<p id="viewport" style="background: #eee; padding: 10px;">Loading...</p>
<script>
document.getElementById('screen').textContent =
'Screen: ' + screen.width + ' x ' + screen.height + ' | Pixel Ratio: ' + window.devicePixelRatio;
document.getElementById('viewport').textContent =
'Viewport: ' + window.innerWidth + ' x ' + window.innerHeight;
</script>
</body>
</html>
"""
@app.route('/debug/routes')
@login_required
def list_routes():
"""Debug: List all registered routes"""
if session.get('role') not in ['owner', 'admin']:
return "Access denied", 403
routes = []
for rule in app.url_map.iter_rules():
routes.append(f"{rule.endpoint}: {rule.rule}")
return "<br>".join(sorted(routes))
# ==================== RUN APPLICATION ====================
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5000)