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.
This commit is contained in:
121
app.py
121
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')
|
||||
|
||||
Reference in New Issue
Block a user