from flask import Flask, render_template, request, jsonify, url_for from flask_sock import Sock from lib.config_manager import ConfigurationManager import os import json # Added import from datetime import datetime import time # Added for shutdown delay # --- Imports for System Tray Icon --- import threading import webbrowser import sys import requests # To send shutdown request from PIL import Image import pystray app = Flask( __name__, static_url_path="", static_folder="static", template_folder="templates" ) sock = Sock(app) config_manager = ConfigurationManager() # Lista global para mantener las conexiones WebSocket activas websocket_connections = set() # --- Globals for Tray Icon --- tray_icon = None @sock.route("/ws") def handle_websocket(ws): try: websocket_connections.add(ws) while True: message = ws.receive() if message: broadcast_message(message) except Exception as e: print(f"WebSocket error: {e}") finally: websocket_connections.remove(ws) def broadcast_message(message): """Envía un mensaje a todas las conexiones WebSocket activas y guarda en log.""" dead_connections = set() timestamp = datetime.now().strftime("[%H:%M:%S] ") # Normalize input to a list of messages if isinstance(message, list): messages = message else: # Si es un solo mensaje, dividirlo en líneas messages = [line.strip() for line in message.splitlines() if line.strip()] # Procesar cada mensaje for raw_msg in messages: # Limpiar timestamps duplicados al inicio del mensaje while raw_msg.startswith("[") and "]" in raw_msg: try: closing_bracket = raw_msg.index("]") + 1 if raw_msg[1 : closing_bracket - 1].replace(":", "").isdigit(): raw_msg = raw_msg[closing_bracket:].strip() # Update raw_msg itself else: break except: break # Log the raw message using the config_manager's logger # The logger will handle its own timestamping for the file. config_manager.append_log(raw_msg) # Format message with timestamp *for WebSocket broadcast* formatted_msg_for_ws = f"{timestamp}{raw_msg}" # Enviar a todos los clientes WebSocket for ws in list(websocket_connections): try: if ws.connected: # Check if ws is still connected before sending ws.send(f"{formatted_msg_for_ws}\n") # Use the correct variable name here except Exception: dead_connections.add(ws) # Collect dead connections # Limpiar conexiones muertas websocket_connections.difference_update(dead_connections) @app.route("/api/execute_script", methods=["POST"]) def execute_script(): try: script_group = request.json["group"] script_name = request.json["script"] # Ejecutar el script y obtener resultado result = config_manager.execute_script( script_group, script_name, broadcast_message ) return jsonify(result) except Exception as e: error_msg = f"Error ejecutando script: {str(e)}" broadcast_message(error_msg) return jsonify({"error": error_msg}) @app.route("/") def index(): script_groups = config_manager.get_script_groups() return render_template("index.html", script_groups=script_groups) @app.route("/api/config/", methods=["GET", "POST"]) def handle_config(level): group = request.args.get("group") if request.method == "GET": try: return jsonify(config_manager.get_config(level, group)) except FileNotFoundError: return jsonify({}) else: data = request.json config_manager.update_config(level, data, group) return jsonify({"status": "success"}) @app.route("/api/schema/", methods=["GET", "POST"]) def handle_schema(level): group = request.args.get("group") if request.method == "GET": return jsonify(config_manager.get_schema(level, group)) else: data = request.json config_manager.update_schema(level, data, group) return jsonify({"status": "success"}) @app.route("/api/scripts/") def get_scripts(group): # list_scripts ahora devuelve detalles y filtra los ocultos scripts = config_manager.list_scripts(group) # El frontend espera 'name' y 'description', mapeamos desde 'display_name' y 'short_description' return jsonify([{"name": s['display_name'], "description": s['short_description'], "filename": s['filename'], "long_description": s['long_description']} for s in scripts]) @app.route("/api/working-directory", methods=["POST"]) def set_working_directory(): data = request.json if not data: return jsonify({"status": "error", "message": "No data provided"}) path = data.get("path") group = data.get("group") if not path or not group: return jsonify( { "status": "error", "message": f"Missing required fields. Path: {path}, Group: {group}", } ) print(f"Setting working directory - Path: {path}, Group: {group}") # Debug line return jsonify(config_manager.set_work_dir(group, path)) @app.route("/api/working-directory/", methods=["GET"]) def get_working_directory(group): path = config_manager.get_work_dir(group) return jsonify({"path": path, "status": "success" if path else "not_set"}) @app.route("/api/browse-directories") def browse_directories(): import tkinter as tk from tkinter import filedialog # Obtener el directorio inicial current_dir = request.args.get("current_path") if not current_dir or not os.path.exists(current_dir): current_dir = os.path.dirname(os.path.abspath(__file__)) # Crear y configurar la ventana principal de tkinter root = tk.Tk() root.attributes("-topmost", True) # Mantener la ventana siempre arriba root.withdraw() # Abrir el diálogo de selección de directorio directory = filedialog.askdirectory( initialdir=current_dir, title="Seleccionar Directorio de Trabajo" ) # Destruir la ventana de tkinter root.destroy() if directory: return jsonify({"status": "success", "path": directory}) return jsonify({"status": "cancelled"}) @app.route("/api/logs", methods=["GET", "DELETE"]) def handle_logs(): if request.method == "GET": return jsonify({"logs": config_manager.read_log()}) else: # DELETE success = config_manager.clear_log() return jsonify({"status": "success" if success else "error"}) @app.route("/api/group-description/", methods=["GET", "POST"]) def handle_group_description(group): if request.method == "GET": try: details = config_manager.get_group_details(group) if "error" in details: return jsonify(details), 404 # Group not found return jsonify(details) except Exception as e: return jsonify({"status": "error", "message": str(e)}), 500 else: # POST try: data = request.json result = config_manager.update_group_description(group, data) return jsonify(result) except Exception as e: return jsonify({"status": "error", "message": str(e)}), 500 @app.route("/api/script-details//", methods=["GET", "POST"]) def handle_script_details(group, script_filename): if request.method == "GET": try: details = config_manager.get_script_details(group, script_filename) return jsonify(details) except Exception as e: print(f"Error getting script details for {group}/{script_filename}: {e}") return jsonify({"status": "error", "message": str(e)}), 500 else: # POST try: data = request.json result = config_manager.update_script_details(group, script_filename, data) return jsonify(result) except Exception as e: print(f"Error updating script details for {group}/{script_filename}: {e}") return jsonify({"status": "error", "message": str(e)}), 500 @app.route("/api/directory-history/") def get_directory_history(group): history = config_manager.get_directory_history(group) return jsonify(history) # --- System Tray Icon Functions --- def run_flask(): """Runs the Flask app.""" print("Starting Flask server on http://127.0.0.1:5000/") try: # use_reloader=False is important when running in a thread # For production, consider using waitress or gunicorn instead of app.run app.run(host='127.0.0.1', port=5000, debug=True, use_reloader=False) except Exception as e: print(f"Error running Flask app: {e}") # Optionally try to stop the tray icon if Flask fails critically if tray_icon: print("Attempting to stop tray icon due to Flask error.") tray_icon.stop() def open_app_browser(icon, item): """Callback function to open the browser.""" print("Opening application in browser...") webbrowser.open("http://127.0.0.1:5000/") def shutdown_flask_server(): """Attempts to gracefully shut down the Werkzeug server.""" try: # This requires the development server (werkzeug) # Send a request to a special shutdown route requests.post("http://127.0.0.1:5000/_shutdown", timeout=1) except Exception as e: print(f"Could not send shutdown request to Flask server: {e}") print("Flask server might need to be closed manually.") def stop_icon_thread(): """Helper function to stop the icon after a delay, allowing HTTP response.""" time.sleep(0.1) # Small delay to allow the HTTP response to be sent if tray_icon: print("Stopping tray icon from shutdown route...") tray_icon.stop() else: print("Tray icon not available to stop.") # As a last resort if the icon isn't running for some reason # print("Attempting os._exit(0) as fallback.") # os._exit(0) # Force exit - use with caution @app.route('/_shutdown', methods=['POST']) def shutdown_route(): """Internal route to shut down the application via the tray icon.""" print("Shutdown endpoint called.") # Stop the main application thread by stopping the tray icon. # Do this in a separate thread to allow the HTTP response to return first. stopper = threading.Thread(target=stop_icon_thread, daemon=True) stopper.start() print("Shutdown signal sent to tray icon thread.") return jsonify(status="success", message="Application shutdown initiated..."), 200 def exit_application(icon, item): """Callback function to exit the application.""" print("Exit requested via tray menu.") # Just stop the icon. This will end the main thread, and the daemon Flask thread will exit. print("Stopping tray icon...") if icon: # pystray passes the icon object icon.stop() elif tray_icon: # Fallback just in case tray_icon.stop() if __name__ == "__main__": # --- Start Flask in a background thread --- flask_thread = threading.Thread(target=run_flask, daemon=True) flask_thread.start() # --- Setup and run the system tray icon --- icon_path = r"d:\Proyectos\Scripts\ParamManagerScripts\icon.png" # Use absolute path try: image = Image.open(icon_path) menu = pystray.Menu( pystray.MenuItem("Abrir ParamManager", open_app_browser, default=True), pystray.MenuItem("Salir", exit_application) ) tray_icon = pystray.Icon("ParamManager", image, "ParamManager", menu) print("Starting system tray icon...") tray_icon.run() # This blocks the main thread until icon.stop() is called except FileNotFoundError: print(f"Error: Icono no encontrado en '{icon_path}'. El icono de notificación no se iniciará.", file=sys.stderr) print("La aplicación Flask seguirá ejecutándose en segundo plano. Presiona Ctrl+C para detenerla si es necesario.") # Keep the main thread alive so the Flask thread doesn't exit immediately # This allows Flask to continue running even without the tray icon. try: while flask_thread.is_alive(): flask_thread.join(timeout=1.0) # Wait indefinitely except KeyboardInterrupt: print("\nCtrl+C detectado. Intentando detener Flask...") shutdown_flask_server() # Try to shutdown Flask on Ctrl+C too print("Saliendo.") except Exception as e: print(f"Error al iniciar el icono de notificación: {e}", file=sys.stderr) print("Aplicación finalizada.")