From 56b0d6d3987e23ac736da8e5f00900a7a4ad8fb1 Mon Sep 17 00:00:00 2001 From: Javier Date: Sun, 8 Feb 2026 00:54:12 -0600 Subject: [PATCH] feat: Complete modular plugin architecture v0.17.0 Major architectural refactor: - Convert invcount and conssheets to self-contained modules - Add Module Manager with upload/install/uninstall - Implement auto-restart after module installation - Add drag-and-drop module upload system - Create triple-confirmation uninstall flow - Redesign Module Manager UI with card layout - Support dynamic module loading from /modules/ This enables easy distribution and installation of new modules without code changes to core application. --- app.py | 121 +++++- templates/module_manager.html | 705 +++++++++++++++++++++++++++----- templates/module_upload_ui.html | 393 ++++++++++++++++++ 3 files changed, 1120 insertions(+), 99 deletions(-) create mode 100644 templates/module_upload_ui.html diff --git a/app.py b/app.py index b78a078..252954a 100644 --- a/app.py +++ b/app.py @@ -28,7 +28,7 @@ app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=1) # 1. Define the version -APP_VERSION = '0.16.0' # Bumped version for modular architecture +APP_VERSION = '0.17.0' # Bumped version for modular architecture # 2. Inject it into all templates automatically @app.context_processor @@ -236,6 +236,125 @@ def restart_server(): except Exception as e: return jsonify({'success': False, 'message': f'Restart failed: {str(e)}'}) + + +""" +Add this route to app.py +""" + +@app.route('/admin/modules/upload', methods=['POST']) +@login_required +def upload_module(): + """Upload and extract a module package""" + if session.get('role') not in ['owner', 'admin']: + return jsonify({'success': False, 'message': 'Access denied'}), 403 + + if 'module_file' not in request.files: + return jsonify({'success': False, 'message': 'No file uploaded'}) + + file = request.files['module_file'] + + if file.filename == '': + return jsonify({'success': False, 'message': 'No file selected'}) + + if not file.filename.endswith('.zip'): + return jsonify({'success': False, 'message': 'File must be a ZIP archive'}) + + try: + import zipfile + import tempfile + import shutil + from pathlib import Path + import json + + # Create temp directory + with tempfile.TemporaryDirectory() as temp_dir: + # Save uploaded file + zip_path = os.path.join(temp_dir, 'module.zip') + file.save(zip_path) + + # Extract zip + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + zip_ref.extractall(temp_dir) + + # Find the module folder (should contain manifest.json) + module_folder = None + manifest_path = None + + # Check if manifest.json is at root of zip + if os.path.exists(os.path.join(temp_dir, 'manifest.json')): + module_folder = temp_dir + manifest_path = os.path.join(temp_dir, 'manifest.json') + else: + # Look for manifest.json in subdirectories + for item in os.listdir(temp_dir): + item_path = os.path.join(temp_dir, item) + if os.path.isdir(item_path): + potential_manifest = os.path.join(item_path, 'manifest.json') + if os.path.exists(potential_manifest): + module_folder = item_path + manifest_path = potential_manifest + break + + if not manifest_path: + return jsonify({ + 'success': False, + 'message': 'Invalid module package: manifest.json not found' + }) + + # Read and validate manifest + with open(manifest_path, 'r') as f: + manifest = json.load(f) + + required_fields = ['module_key', 'name', 'version', 'author'] + for field in required_fields: + if field not in manifest: + return jsonify({ + 'success': False, + 'message': f'Invalid manifest.json: missing required field "{field}"' + }) + + module_key = manifest['module_key'] + + # Check if module already exists + modules_dir = os.path.join(os.path.dirname(__file__), 'modules') + target_path = os.path.join(modules_dir, module_key) + + if os.path.exists(target_path): + return jsonify({ + 'success': False, + 'message': f'Module "{module_key}" already exists. Please uninstall it first or use a different module key.' + }) + + # Required files check + required_files = ['manifest.json', '__init__.py'] + for req_file in required_files: + if not os.path.exists(os.path.join(module_folder, req_file)): + return jsonify({ + 'success': False, + 'message': f'Invalid module package: {req_file} not found' + }) + + # Copy module to /modules directory + shutil.copytree(module_folder, target_path) + + print(f"✅ Module '{manifest['name']}' uploaded successfully to {target_path}") + + return jsonify({ + 'success': True, + 'message': f"Module '{manifest['name']}' uploaded successfully! Click Install to activate it." + }) + + except zipfile.BadZipFile: + return jsonify({'success': False, 'message': 'Invalid ZIP file'}) + except json.JSONDecodeError: + return jsonify({'success': False, 'message': 'Invalid manifest.json: not valid JSON'}) + except Exception as e: + print(f"❌ Module upload error: {e}") + import traceback + traceback.print_exc() + return jsonify({'success': False, 'message': f'Upload failed: {str(e)}'}) + # ==================== PWA SUPPORT ROUTES ==================== @app.route('/manifest.json') diff --git a/templates/module_manager.html b/templates/module_manager.html index 380d8ef..a37657f 100644 --- a/templates/module_manager.html +++ b/templates/module_manager.html @@ -3,86 +3,240 @@ {% block title %}Module Manager - ScanLook{% endblock %} {% block content %} -
-
-

Module Manager

- - Back to Home - +
+
+
+

Module Manager

+

Install, manage, and configure ScanLook modules

+
+
-

Install, uninstall, and manage ScanLook modules

- -
+ {% if modules %} +
{% for module in modules %} -
-
-
-
- {{ module.name }} - v{{ module.version }} -
-
-
-

{{ module.description }}

- -

- - Author: {{ module.author }}
- Module Key: {{ module.module_key }} -
-

+
+ +
+ {% if module.icon %} + + {% else %} + + {% endif %} +
-
- {% if module.is_installed and module.is_active %} - - Active - - {% elif module.is_installed %} - - Installed (Inactive) - - {% else %} - - Not Installed - - {% endif %} -
+ +

{{ module.name }}

+

{{ module.description }}

+ + + {% endfor %}
- - {% if not modules %} -
- No modules found in the /modules directory. + {% else %} +
+
+

No Modules Found

+

No modules found in the /modules directory.

+

Upload a module package to get started.

{% endif %}
+ + + + + + + + {% endblock %} \ No newline at end of file diff --git a/templates/module_upload_ui.html b/templates/module_upload_ui.html new file mode 100644 index 0000000..19b23ad --- /dev/null +++ b/templates/module_upload_ui.html @@ -0,0 +1,393 @@ + + +
+ +
+ + + + + + +