diff --git a/web_dashboard/app.py b/web_dashboard/app.py index 126101795..74bea1ff8 100644 --- a/web_dashboard/app.py +++ b/web_dashboard/app.py @@ -32,6 +32,10 @@ from core.monitoring.trigger_manager import TriggerManager from core.monitoring.log_exporter import LogExporter from core.monitoring.automation_scheduler import AutomationScheduler +# Modules pour backup et versioning +from core.system.backup_exporter import get_backup_exporter +from core.system.version_manager import get_version_manager + app = Flask(__name__) app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'dev-key-change-in-production') socketio = SocketIO(app, cors_allowed_origins="*", async_mode='threading') @@ -1229,6 +1233,283 @@ def automation_stop(): return jsonify({'error': str(e)}), 500 +# ============================================================================= +# API Backup & Export - Sauvegardes client +# ============================================================================= + +@app.route('/api/backup/stats') +def backup_stats(): + """Statistiques des données disponibles pour backup.""" + try: + exporter = get_backup_exporter() + stats = exporter.get_backup_stats() + return jsonify({ + 'stats': stats, + 'timestamp': datetime.now().isoformat() + }) + except Exception as e: + api_logger.error(f"Error getting backup stats: {e}") + return jsonify({'error': str(e)}), 500 + + +@app.route('/api/backup/full', methods=['POST']) +def backup_full(): + """Génère et télécharge un backup complet.""" + try: + data = request.get_json() or {} + include_models = data.get('include_models', False) + + exporter = get_backup_exporter() + zip_path = exporter.export_full_backup(include_models=include_models) + + api_logger.info(f"Full backup created (include_models={include_models})") + + return send_file( + zip_path, + mimetype='application/zip', + as_attachment=True, + download_name=f'rpa_vision_backup_{datetime.now().strftime("%Y%m%d_%H%M%S")}.zip' + ) + except Exception as e: + api_logger.error(f"Error creating full backup: {e}") + return jsonify({'error': str(e)}), 500 + + +@app.route('/api/backup/workflows') +def backup_workflows(): + """Génère et télécharge un backup des workflows uniquement.""" + try: + exporter = get_backup_exporter() + zip_path = exporter.export_workflows() + + api_logger.info("Workflows backup created") + + return send_file( + zip_path, + mimetype='application/zip', + as_attachment=True, + download_name=f'rpa_workflows_{datetime.now().strftime("%Y%m%d_%H%M%S")}.zip' + ) + except Exception as e: + api_logger.error(f"Error creating workflows backup: {e}") + return jsonify({'error': str(e)}), 500 + + +@app.route('/api/backup/correction-packs') +def backup_correction_packs(): + """Génère et télécharge un backup des correction packs.""" + try: + exporter = get_backup_exporter() + zip_path = exporter.export_correction_packs() + + api_logger.info("Correction packs backup created") + + return send_file( + zip_path, + mimetype='application/zip', + as_attachment=True, + download_name=f'rpa_correction_packs_{datetime.now().strftime("%Y%m%d_%H%M%S")}.zip' + ) + except Exception as e: + api_logger.error(f"Error creating correction packs backup: {e}") + return jsonify({'error': str(e)}), 500 + + +@app.route('/api/backup/trained-models') +def backup_trained_models(): + """ + Génère et télécharge les modèles entraînés (opt-in). + + Ces modèles sont anonymisés et contiennent uniquement les patterns + appris, pas les données brutes ou les screenshots. + """ + try: + exporter = get_backup_exporter() + zip_path = exporter.export_trained_models() + + api_logger.info("Trained models backup created (opt-in)") + + return send_file( + zip_path, + mimetype='application/zip', + as_attachment=True, + download_name=f'rpa_trained_models_{datetime.now().strftime("%Y%m%d_%H%M%S")}.zip' + ) + except Exception as e: + api_logger.error(f"Error creating trained models backup: {e}") + return jsonify({'error': str(e)}), 500 + + +@app.route('/api/backup/config') +def backup_config(): + """Génère et télécharge la configuration (sanitisée).""" + try: + exporter = get_backup_exporter() + zip_path = exporter.export_config() + + api_logger.info("Config backup created (sanitized)") + + return send_file( + zip_path, + mimetype='application/zip', + as_attachment=True, + download_name=f'rpa_config_{datetime.now().strftime("%Y%m%d_%H%M%S")}.zip' + ) + except Exception as e: + api_logger.error(f"Error creating config backup: {e}") + return jsonify({'error': str(e)}), 500 + + +# ============================================================================= +# API Version & Updates - Gestion des versions +# ============================================================================= + +@app.route('/api/version') +def get_version(): + """Retourne la version actuelle du système.""" + try: + vm = get_version_manager() + version_info = vm.get_current_version() + return jsonify({ + 'version': version_info.to_dict(), + 'timestamp': datetime.now().isoformat() + }) + except Exception as e: + api_logger.error(f"Error getting version: {e}") + return jsonify({'error': str(e)}), 500 + + +@app.route('/api/version/system-info') +def get_system_info(): + """Retourne les informations système complètes.""" + try: + vm = get_version_manager() + system_info = vm.get_system_info() + return jsonify({ + 'system_info': system_info, + 'timestamp': datetime.now().isoformat() + }) + except Exception as e: + api_logger.error(f"Error getting system info: {e}") + return jsonify({'error': str(e)}), 500 + + +@app.route('/api/version/check-update') +def check_update(): + """Vérifie si une mise à jour est disponible.""" + try: + vm = get_version_manager() + update = vm.check_for_updates() + + if update: + return jsonify({ + 'update_available': True, + 'update': update.to_dict(), + 'timestamp': datetime.now().isoformat() + }) + else: + return jsonify({ + 'update_available': False, + 'message': 'Système à jour', + 'timestamp': datetime.now().isoformat() + }) + except Exception as e: + api_logger.error(f"Error checking update: {e}") + return jsonify({'error': str(e)}), 500 + + +@app.route('/api/version/backups') +def list_version_backups(): + """Liste les backups de version disponibles pour rollback.""" + try: + vm = get_version_manager() + backups = vm.list_backups() + return jsonify({ + 'backups': backups, + 'total': len(backups), + 'timestamp': datetime.now().isoformat() + }) + except Exception as e: + api_logger.error(f"Error listing version backups: {e}") + return jsonify({'error': str(e)}), 500 + + +@app.route('/api/version/create-backup', methods=['POST']) +def create_version_backup(): + """Crée un backup de la version actuelle (point de restauration).""" + try: + data = request.get_json() or {} + label = data.get('label') + + vm = get_version_manager() + backup_path = vm.create_backup(label=label) + + api_logger.info(f"Version backup created: {backup_path}") + + return jsonify({ + 'success': True, + 'backup_path': str(backup_path), + 'label': label, + 'timestamp': datetime.now().isoformat() + }) + except Exception as e: + api_logger.error(f"Error creating version backup: {e}") + return jsonify({'error': str(e)}), 500 + + +@app.route('/api/version/upload-update', methods=['POST']) +def upload_update_package(): + """ + Upload d'un package de mise à jour. + + Le package doit être un fichier ZIP contenant: + - update_manifest.json + - Les fichiers de mise à jour + """ + try: + if 'file' not in request.files: + return jsonify({'error': 'Aucun fichier fourni'}), 400 + + file = request.files['file'] + if file.filename == '': + return jsonify({'error': 'Nom de fichier vide'}), 400 + + if not file.filename.endswith('.zip'): + return jsonify({'error': 'Le fichier doit être un ZIP'}), 400 + + # Sauvegarder dans le répertoire updates + vm = get_version_manager() + update_path = vm.updates_dir / file.filename + file.save(update_path) + + # Extraire et valider le manifest + import zipfile + with zipfile.ZipFile(update_path, 'r') as zf: + if 'update_manifest.json' not in zf.namelist(): + update_path.unlink() + return jsonify({'error': 'Package invalide: manifest manquant'}), 400 + + # Extraire le manifest + zf.extract('update_manifest.json', vm.updates_dir) + + with open(vm.updates_dir / 'update_manifest.json', 'r') as f: + manifest = json.load(f) + + api_logger.info(f"Update package uploaded: {file.filename} (version {manifest.get('version')})") + + return jsonify({ + 'success': True, + 'filename': file.filename, + 'manifest': manifest, + 'message': 'Package uploadé. Utilisez /api/version/check-update pour vérifier.', + 'timestamp': datetime.now().isoformat() + }) + + except Exception as e: + api_logger.error(f"Error uploading update package: {e}") + return jsonify({'error': str(e)}), 500 + + # ============================================================================= # API Metrics (Prometheus) # =============================================================================