From 5e575fd112ad3f62641ff429351426e3c19533c0 Mon Sep 17 00:00:00 2001 From: Miguel Date: Mon, 21 Jul 2025 12:30:26 +0200 Subject: [PATCH] =?UTF-8?q?Actualizaci=C3=B3n=20del=20sistema=20de=20plott?= =?UTF-8?q?ing=20en=20tiempo=20real=20con=20nuevas=20funcionalidades,=20in?= =?UTF-8?q?cluyendo=20la=20creaci=C3=B3n=20y=20edici=C3=B3n=20de=20sesione?= =?UTF-8?q?s=20de=20plot=20a=20trav=C3=A9s=20de=20un=20formulario=20colaps?= =?UTF-8?q?able.=20Se=20implementaron=20nuevos=20endpoints=20API=20para=20?= =?UTF-8?q?obtener=20y=20actualizar=20la=20configuraci=C3=B3n=20de=20las?= =?UTF-8?q?=20sesiones=20de=20plot.=20Adem=C3=A1s,=20se=20mejor=C3=B3=20la?= =?UTF-8?q?=20interfaz=20de=20usuario=20con=20sub-tabs=20din=C3=A1micos=20?= =?UTF-8?q?para=20gestionar=20m=C3=BAltiples=20sesiones=20de=20plot=20y=20?= =?UTF-8?q?se=20realizaron=20ajustes=20en=20los=20estilos=20CSS=20para=20u?= =?UTF-8?q?na=20mejor=20experiencia=20visual.=20Se=20actualizaron=20los=20?= =?UTF-8?q?archivos=20de=20configuraci=C3=B3n=20y=20estado=20del=20sistema?= =?UTF-8?q?=20para=20reflejar=20estos=20cambios.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application_events.json | 661 +++++++- core/plot_manager.py | 191 ++- main.py | 107 ++ plc_datasets.json | 2 +- plot_sessions.json | 20 + static/css/styles.css | 501 +++++- static/js/datasets.js | 2 +- static/js/events.js | 80 +- static/js/plotting.js | 949 +++++++++-- static/js/tabs.js | 222 ++- system_state.json | 2 +- templates/index.html | 335 +++- web_test/PLC S7-315 Streamer & Logger.html | 1430 +++++++++++++++++ .../SIDEL.png | Bin 0 -> 83332 bytes .../chart.js.descargar | 14 + .../chartjs-adapter-date-fns | 7 + .../csv.js.descargar | 170 ++ .../datasets.js.descargar | 519 ++++++ .../events.js.descargar | 168 ++ .../main.js.descargar | 43 + .../pico.min.css | 4 + .../plc.js.descargar | 79 + .../plotting.js.descargar | 1150 +++++++++++++ .../status.js.descargar | 256 +++ .../streaming.js.descargar | 54 + .../styles.css | 1281 +++++++++++++++ .../tabs.js.descargar | 296 ++++ .../theme.js.descargar | 32 + .../utils.js.descargar | 64 + .../variables.js.descargar | 322 ++++ 30 files changed, 8693 insertions(+), 268 deletions(-) create mode 100644 plot_sessions.json create mode 100644 web_test/PLC S7-315 Streamer & Logger.html create mode 100644 web_test/PLC S7-315 Streamer & Logger_files/SIDEL.png create mode 100644 web_test/PLC S7-315 Streamer & Logger_files/chart.js.descargar create mode 100644 web_test/PLC S7-315 Streamer & Logger_files/chartjs-adapter-date-fns create mode 100644 web_test/PLC S7-315 Streamer & Logger_files/csv.js.descargar create mode 100644 web_test/PLC S7-315 Streamer & Logger_files/datasets.js.descargar create mode 100644 web_test/PLC S7-315 Streamer & Logger_files/events.js.descargar create mode 100644 web_test/PLC S7-315 Streamer & Logger_files/main.js.descargar create mode 100644 web_test/PLC S7-315 Streamer & Logger_files/pico.min.css create mode 100644 web_test/PLC S7-315 Streamer & Logger_files/plc.js.descargar create mode 100644 web_test/PLC S7-315 Streamer & Logger_files/plotting.js.descargar create mode 100644 web_test/PLC S7-315 Streamer & Logger_files/status.js.descargar create mode 100644 web_test/PLC S7-315 Streamer & Logger_files/streaming.js.descargar create mode 100644 web_test/PLC S7-315 Streamer & Logger_files/styles.css create mode 100644 web_test/PLC S7-315 Streamer & Logger_files/tabs.js.descargar create mode 100644 web_test/PLC S7-315 Streamer & Logger_files/theme.js.descargar create mode 100644 web_test/PLC S7-315 Streamer & Logger_files/utils.js.descargar create mode 100644 web_test/PLC S7-315 Streamer & Logger_files/variables.js.descargar diff --git a/application_events.json b/application_events.json index 51b1ddc..354ed45 100644 --- a/application_events.json +++ b/application_events.json @@ -3273,8 +3273,665 @@ "time_window": 10, "trigger_variable": null } + }, + { + "timestamp": "2025-07-21T09:39:40.657456", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-07-21T09:39:40.680497", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: DAR", + "details": { + "dataset_id": "dar", + "variables_count": 6, + "streaming_count": 4, + "prefix": "dar" + } + }, + { + "timestamp": "2025-07-21T09:39:40.684807", + "level": "info", + "event_type": "csv_recording_started", + "message": "CSV recording started: 1 datasets activated", + "details": { + "activated_datasets": 1, + "total_datasets": 2 + } + }, + { + "timestamp": "2025-07-21T09:40:30.518857", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-07-21T09:40:30.542851", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: DAR", + "details": { + "dataset_id": "dar", + "variables_count": 6, + "streaming_count": 4, + "prefix": "dar" + } + }, + { + "timestamp": "2025-07-21T09:40:30.546860", + "level": "info", + "event_type": "csv_recording_started", + "message": "CSV recording started: 1 datasets activated", + "details": { + "activated_datasets": 1, + "total_datasets": 2 + } + }, + { + "timestamp": "2025-07-21T09:41:09.159957", + "level": "info", + "event_type": "plot_session_created", + "message": "Plot session 'test' created", + "details": { + "session_id": "plot_0", + "variables": [ + "CTS306_PV" + ], + "time_window": 60, + "trigger_variable": null + } + }, + { + "timestamp": "2025-07-21T10:09:40.951446", + "level": "info", + "event_type": "plot_session_created", + "message": "Plot session 'test' created", + "details": { + "session_id": "plot_1", + "variables": [ + "CTS306_PV" + ], + "time_window": 10, + "trigger_variable": null + } + }, + { + "timestamp": "2025-07-21T10:10:03.044358", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-07-21T10:10:03.070132", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: DAR", + "details": { + "dataset_id": "dar", + "variables_count": 6, + "streaming_count": 4, + "prefix": "dar" + } + }, + { + "timestamp": "2025-07-21T10:10:03.074143", + "level": "info", + "event_type": "csv_recording_started", + "message": "CSV recording started: 1 datasets activated", + "details": { + "activated_datasets": 1, + "total_datasets": 2 + } + }, + { + "timestamp": "2025-07-21T10:10:48.758561", + "level": "info", + "event_type": "plot_session_created", + "message": "Plot session 'test' created", + "details": { + "session_id": "plot_2", + "variables": [ + "CTS306_PV" + ], + "time_window": 10, + "trigger_variable": null + } + }, + { + "timestamp": "2025-07-21T10:28:02.672364", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-07-21T10:28:02.693756", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: DAR", + "details": { + "dataset_id": "dar", + "variables_count": 6, + "streaming_count": 4, + "prefix": "dar" + } + }, + { + "timestamp": "2025-07-21T10:28:02.697814", + "level": "info", + "event_type": "csv_recording_started", + "message": "CSV recording started: 1 datasets activated", + "details": { + "activated_datasets": 1, + "total_datasets": 2 + } + }, + { + "timestamp": "2025-07-21T10:28:56.265087", + "level": "info", + "event_type": "plot_session_created", + "message": "Plot session 'temp' created", + "details": { + "session_id": "plot_3", + "variables": [ + "CTS306_PV" + ], + "time_window": 10, + "trigger_variable": null + } + }, + { + "timestamp": "2025-07-21T10:49:21.053339", + "level": "info", + "event_type": "csv_recording_stopped", + "message": "CSV recording stopped (dataset threads continue for UDP streaming)", + "details": {} + }, + { + "timestamp": "2025-07-21T10:49:21.057745", + "level": "info", + "event_type": "udp_streaming_stopped", + "message": "UDP streaming to PlotJuggler stopped (CSV recording continues)", + "details": {} + }, + { + "timestamp": "2025-07-21T10:49:21.063745", + "level": "info", + "event_type": "dataset_deactivated", + "message": "Dataset deactivated: DAR", + "details": { + "dataset_id": "dar" + } + }, + { + "timestamp": "2025-07-21T10:49:21.068745", + "level": "info", + "event_type": "plc_disconnection", + "message": "Disconnected from PLC 10.1.33.11 (stopped recording and streaming)", + "details": {} + }, + { + "timestamp": "2025-07-21T10:49:27.413472", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: DAR", + "details": { + "dataset_id": "dar", + "variables_count": 6, + "streaming_count": 4, + "prefix": "dar" + } + }, + { + "timestamp": "2025-07-21T10:49:27.420917", + "level": "info", + "event_type": "csv_recording_started", + "message": "CSV recording started: 1 datasets activated", + "details": { + "activated_datasets": 1, + "total_datasets": 2 + } + }, + { + "timestamp": "2025-07-21T10:49:27.426918", + "level": "info", + "event_type": "plc_connection", + "message": "Successfully connected to PLC 10.1.33.11 and auto-started CSV recording for 1 datasets", + "details": { + "ip": "10.1.33.11", + "rack": 0, + "slot": 2, + "auto_started_recording": true, + "recording_datasets": 1, + "dataset_names": [ + "DAR" + ] + } + }, + { + "timestamp": "2025-07-21T10:51:12.198006", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-07-21T10:51:12.222774", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: DAR", + "details": { + "dataset_id": "dar", + "variables_count": 6, + "streaming_count": 4, + "prefix": "dar" + } + }, + { + "timestamp": "2025-07-21T10:51:12.227786", + "level": "info", + "event_type": "csv_recording_started", + "message": "CSV recording started: 1 datasets activated", + "details": { + "activated_datasets": 1, + "total_datasets": 2 + } + }, + { + "timestamp": "2025-07-21T10:51:52.901833", + "level": "info", + "event_type": "plot_session_created", + "message": "Plot session 'Cond' created", + "details": { + "session_id": "plot_4", + "variables": [ + "CTS306_PV" + ], + "time_window": 60, + "trigger_variable": null + } + }, + { + "timestamp": "2025-07-21T11:22:36.755640", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-07-21T11:22:36.781108", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: DAR", + "details": { + "dataset_id": "dar", + "variables_count": 6, + "streaming_count": 4, + "prefix": "dar" + } + }, + { + "timestamp": "2025-07-21T11:22:36.785849", + "level": "info", + "event_type": "csv_recording_started", + "message": "CSV recording started: 1 datasets activated", + "details": { + "activated_datasets": 1, + "total_datasets": 2 + } + }, + { + "timestamp": "2025-07-21T11:22:56.274910", + "level": "info", + "event_type": "plot_session_removed", + "message": "Plot session 'test' removed", + "details": { + "session_id": "plot_0" + } + }, + { + "timestamp": "2025-07-21T11:22:59.278159", + "level": "info", + "event_type": "plot_session_removed", + "message": "Plot session 'test' removed", + "details": { + "session_id": "plot_1" + } + }, + { + "timestamp": "2025-07-21T11:23:01.427750", + "level": "info", + "event_type": "plot_session_removed", + "message": "Plot session 'test' removed", + "details": { + "session_id": "plot_2" + } + }, + { + "timestamp": "2025-07-21T11:23:04.342691", + "level": "info", + "event_type": "plot_session_removed", + "message": "Plot session 'temp' removed", + "details": { + "session_id": "plot_3" + } + }, + { + "timestamp": "2025-07-21T11:23:07.835356", + "level": "info", + "event_type": "plot_session_removed", + "message": "Plot session 'Cond' removed", + "details": { + "session_id": "plot_4" + } + }, + { + "timestamp": "2025-07-21T11:23:28.573576", + "level": "info", + "event_type": "plot_session_created", + "message": "Plot session 'Cond' created", + "details": { + "session_id": "plot_5", + "variables": [ + "CTS306_PV" + ], + "time_window": 60, + "trigger_variable": null + } + }, + { + "timestamp": "2025-07-21T11:23:59.616795", + "level": "info", + "event_type": "plot_session_updated", + "message": "Plot session 'Cond' configuration updated", + "details": { + "session_id": "plot_5", + "new_config": { + "name": "Cond", + "variables": [ + "UR29_Brix" + ], + "time_window": 10, + "y_min": null, + "y_max": null, + "trigger_variable": null, + "trigger_enabled": false, + "trigger_on_true": true + } + } + }, + { + "timestamp": "2025-07-21T11:24:11.314102", + "level": "info", + "event_type": "plot_session_removed", + "message": "Plot session 'Cond' removed", + "details": { + "session_id": "plot_5" + } + }, + { + "timestamp": "2025-07-21T11:25:39.077731", + "level": "info", + "event_type": "plot_session_created", + "message": "Plot session 'Condix' created", + "details": { + "session_id": "plot_6", + "variables": [ + "CTS306_PV", + "UR29_Brix" + ], + "time_window": 60, + "trigger_variable": null + } + }, + { + "timestamp": "2025-07-21T11:26:04.419881", + "level": "info", + "event_type": "plot_session_updated", + "message": "Plot session 'Condix' configuration updated", + "details": { + "session_id": "plot_6", + "new_config": { + "name": "Condix", + "variables": [ + "UR62_Brix" + ], + "time_window": 60, + "y_min": null, + "y_max": null, + "trigger_variable": null, + "trigger_enabled": false, + "trigger_on_true": true + } + } + }, + { + "timestamp": "2025-07-21T11:40:49.279919", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-07-21T11:40:49.300672", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: DAR", + "details": { + "dataset_id": "dar", + "variables_count": 6, + "streaming_count": 4, + "prefix": "dar" + } + }, + { + "timestamp": "2025-07-21T11:40:49.306512", + "level": "info", + "event_type": "csv_recording_started", + "message": "CSV recording started: 1 datasets activated", + "details": { + "activated_datasets": 1, + "total_datasets": 2 + } + }, + { + "timestamp": "2025-07-21T11:49:04.783801", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-07-21T11:49:04.807533", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: DAR", + "details": { + "dataset_id": "dar", + "variables_count": 6, + "streaming_count": 4, + "prefix": "dar" + } + }, + { + "timestamp": "2025-07-21T11:49:04.811438", + "level": "info", + "event_type": "csv_recording_started", + "message": "CSV recording started: 1 datasets activated", + "details": { + "activated_datasets": 1, + "total_datasets": 2 + } + }, + { + "timestamp": "2025-07-21T12:09:29.076716", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-07-21T12:09:29.104501", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: DAR", + "details": { + "dataset_id": "dar", + "variables_count": 6, + "streaming_count": 4, + "prefix": "dar" + } + }, + { + "timestamp": "2025-07-21T12:09:29.112409", + "level": "info", + "event_type": "csv_recording_started", + "message": "CSV recording started: 1 datasets activated", + "details": { + "activated_datasets": 1, + "total_datasets": 2 + } + }, + { + "timestamp": "2025-07-21T12:15:31.494921", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-07-21T12:15:31.516212", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: DAR", + "details": { + "dataset_id": "dar", + "variables_count": 6, + "streaming_count": 4, + "prefix": "dar" + } + }, + { + "timestamp": "2025-07-21T12:15:31.521785", + "level": "info", + "event_type": "csv_recording_started", + "message": "CSV recording started: 1 datasets activated", + "details": { + "activated_datasets": 1, + "total_datasets": 2 + } + }, + { + "timestamp": "2025-07-21T12:15:48.946451", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-07-21T12:15:48.968995", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: DAR", + "details": { + "dataset_id": "dar", + "variables_count": 6, + "streaming_count": 4, + "prefix": "dar" + } + }, + { + "timestamp": "2025-07-21T12:15:48.977924", + "level": "info", + "event_type": "csv_recording_started", + "message": "CSV recording started: 1 datasets activated", + "details": { + "activated_datasets": 1, + "total_datasets": 2 + } + }, + { + "timestamp": "2025-07-21T12:15:52.270831", + "level": "info", + "event_type": "csv_recording_stopped", + "message": "CSV recording stopped (dataset threads continue for UDP streaming)", + "details": {} + }, + { + "timestamp": "2025-07-21T12:15:52.278439", + "level": "info", + "event_type": "udp_streaming_stopped", + "message": "UDP streaming to PlotJuggler stopped (CSV recording continues)", + "details": {} + }, + { + "timestamp": "2025-07-21T12:15:52.396232", + "level": "info", + "event_type": "dataset_deactivated", + "message": "Dataset deactivated: DAR", + "details": { + "dataset_id": "dar" + } + }, + { + "timestamp": "2025-07-21T12:15:52.402205", + "level": "info", + "event_type": "plc_disconnection", + "message": "Disconnected from PLC 10.1.33.11 (stopped recording and streaming)", + "details": {} + }, + { + "timestamp": "2025-07-21T12:28:56.732253", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-07-21T12:29:22.706547", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: DAR", + "details": { + "dataset_id": "dar", + "variables_count": 6, + "streaming_count": 4, + "prefix": "dar" + } + }, + { + "timestamp": "2025-07-21T12:29:22.715569", + "level": "info", + "event_type": "csv_recording_started", + "message": "CSV recording started: 1 datasets activated", + "details": { + "activated_datasets": 1, + "total_datasets": 2 + } + }, + { + "timestamp": "2025-07-21T12:29:22.723751", + "level": "info", + "event_type": "plc_connection", + "message": "Successfully connected to PLC 10.1.33.11 and auto-started CSV recording for 1 datasets", + "details": { + "ip": "10.1.33.11", + "rack": 0, + "slot": 2, + "auto_started_recording": true, + "recording_datasets": 1, + "dataset_names": [ + "DAR" + ] + } } ], - "last_updated": "2025-07-21T09:25:26.290146", - "total_entries": 309 + "last_updated": "2025-07-21T12:29:22.723751", + "total_entries": 372 } \ No newline at end of file diff --git a/core/plot_manager.py b/core/plot_manager.py index 01f4de2..3a0c58d 100644 --- a/core/plot_manager.py +++ b/core/plot_manager.py @@ -1,12 +1,27 @@ import threading import time import json +import os from collections import deque from datetime import datetime, timedelta from typing import Dict, Any, Optional, List, Set import logging +def resource_path(relative_path): + """Get absolute path to resource, works for dev and for PyInstaller""" + import sys + + try: + # PyInstaller creates a temp folder and stores path in _MEIPASS + base_path = sys._MEIPASS + except Exception: + # Not running in a bundle + base_path = os.path.abspath(".") + + return os.path.join(base_path, relative_path) + + class PlotSession: """Representa una sesión de plotting individual con configuración específica""" @@ -184,6 +199,12 @@ class PlotManager: self.event_logger = event_logger self.logger = logger + # Persistent storage + self.plots_file = resource_path("plot_sessions.json") + + # Load existing plots from disk + self.load_plots() + def create_session(self, config: Dict[str, Any]) -> str: """Crear una nueva sesión de plotting""" with self.lock: @@ -191,11 +212,18 @@ class PlotManager: self.session_counter += 1 session = PlotSession(session_id, config) + # Por defecto, crear sesiones en modo stopped + session.is_active = False + session.is_paused = False + self.sessions[session_id] = session + # Guardar automáticamente la configuración + self.save_plots() + if self.logger: self.logger.info( - f"Created plot session '{session.name}' with {len(session.variables)} variables" + f"Created plot session '{session.name}' with {len(session.variables)} variables (stopped by default)" ) if self.event_logger: @@ -232,6 +260,10 @@ class PlotManager: ) del self.sessions[session_id] + + # Guardar automáticamente después de eliminar + self.save_plots() + return True return False @@ -311,6 +343,163 @@ class PlotManager: with self.lock: return [session.get_status() for session in self.sessions.values()] + def load_plots(self): + """Cargar plots persistentes desde archivo""" + try: + if os.path.exists(self.plots_file): + with open(self.plots_file, "r", encoding="utf-8") as f: + data = json.load(f) + plots_data = data.get("plots", {}) + + for session_id, plot_config in plots_data.items(): + # Crear sesión con configuración guardada + session = PlotSession(session_id, plot_config) + # IMPORTANTE: Iniciar todas las sesiones en modo stopped + session.is_active = False + session.is_paused = False + + self.sessions[session_id] = session + + # Actualizar contador para evitar IDs duplicados + try: + session_num = int(session_id.split("_")[1]) + if session_num >= self.session_counter: + self.session_counter = session_num + 1 + except (IndexError, ValueError): + pass + + if self.logger and self.sessions: + self.logger.info( + f"Loaded {len(self.sessions)} persistent plot sessions (all stopped)" + ) + + else: + if self.logger: + self.logger.info( + "No persistent plots file found, starting with empty plots" + ) + + except Exception as e: + if self.logger: + self.logger.error(f"Error loading persistent plots: {e}") + self.sessions = {} + + def save_plots(self): + """Guardar plots al archivo de persistencia""" + try: + plots_data = {} + for session_id, session in self.sessions.items(): + # Guardar configuración de la sesión (sin datos temporales) + plots_data[session_id] = { + "name": session.name, + "variables": session.variables, + "time_window": session.time_window, + "y_min": session.y_min, + "y_max": session.y_max, + "trigger_variable": session.trigger_variable, + "trigger_enabled": session.trigger_enabled, + "trigger_on_true": session.trigger_on_true, + "session_id": session_id, # Para referencia + } + + data = { + "plots": plots_data, + "session_counter": self.session_counter, + "last_saved": datetime.now().isoformat(), + "version": "1.0", + } + + with open(self.plots_file, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2, ensure_ascii=False) + + if self.logger: + self.logger.debug(f"Saved {len(plots_data)} plot sessions to disk") + + except Exception as e: + if self.logger: + self.logger.error(f"Error saving persistent plots: {e}") + + def update_session_config(self, session_id: str, config: Dict[str, Any]) -> bool: + """Actualizar configuración de una sesión existente""" + with self.lock: + if session_id not in self.sessions: + return False + + session = self.sessions[session_id] + + # Actualizar configuración + session.name = config.get("name", session.name) + session.variables = config.get("variables", session.variables) + session.time_window = config.get("time_window", session.time_window) + session.y_min = config.get("y_min", session.y_min) + session.y_max = config.get("y_max", session.y_max) + session.trigger_variable = config.get( + "trigger_variable", session.trigger_variable + ) + session.trigger_enabled = config.get( + "trigger_enabled", session.trigger_enabled + ) + session.trigger_on_true = config.get( + "trigger_on_true", session.trigger_on_true + ) + + # Actualizar deques de datos si las variables cambiaron + new_variables = set(session.variables) + current_variables = set(session.data.keys()) + + # Agregar nuevas variables + max_points = int(session.time_window * 10) + for var in new_variables - current_variables: + session.data[var] = deque(maxlen=max_points) + + # Remover variables que ya no están + for var in current_variables - new_variables: + del session.data[var] + + # Actualizar tamaño de deques existentes si cambió time_window + for var in session.data: + if session.data[var].maxlen != max_points: + # Crear nuevo deque con el tamaño correcto preservando datos + old_data = list(session.data[var]) + session.data[var] = deque(old_data, maxlen=max_points) + + # Guardar cambios + self.save_plots() + + if self.logger: + self.logger.info(f"Updated plot session '{session.name}' configuration") + + if self.event_logger: + self.event_logger.log_event( + "info", + "plot_session_updated", + f"Plot session '{session.name}' configuration updated", + {"session_id": session_id, "new_config": config}, + ) + + return True + + def get_session_config(self, session_id: str) -> Optional[Dict[str, Any]]: + """Obtener configuración completa de una sesión""" + with self.lock: + if session_id not in self.sessions: + return None + + session = self.sessions[session_id] + return { + "session_id": session_id, + "name": session.name, + "variables": session.variables, + "time_window": session.time_window, + "y_min": session.y_min, + "y_max": session.y_max, + "trigger_variable": session.trigger_variable, + "trigger_enabled": session.trigger_enabled, + "trigger_on_true": session.trigger_on_true, + "is_active": session.is_active, + "is_paused": session.is_paused, + } + def get_active_sessions_count(self) -> int: """Obtener número de sesiones activas""" with self.lock: diff --git a/main.py b/main.py index 5e6ea55..7fef706 100644 --- a/main.py +++ b/main.py @@ -1371,6 +1371,113 @@ def get_plot_data(session_id): return jsonify({"error": str(e)}), 500 +@app.route("/api/plots//config", methods=["GET"]) +def get_plot_config(session_id): + """Get plot configuration for a specific session""" + error_response = check_streamer_initialized() + if error_response: + return error_response + + try: + config = streamer.data_streamer.plot_manager.get_session_config(session_id) + if config: + return jsonify({"success": True, "config": config}) + else: + return jsonify({"error": "Plot session not found"}), 404 + except Exception as e: + return jsonify({"error": str(e)}), 500 + + +@app.route("/api/plots//config", methods=["PUT"]) +def update_plot_config(session_id): + """Update plot configuration for a specific session""" + error_response = check_streamer_initialized() + if error_response: + return error_response + + try: + data = request.get_json() + + # Validar datos requeridos + if not data.get("variables"): + return jsonify({"error": "At least one variable is required"}), 400 + + if not data.get("time_window"): + return jsonify({"error": "Time window is required"}), 400 + + # Validar que las variables existen en datasets activos + available_vars = streamer.data_streamer.plot_manager.get_available_variables( + streamer.data_streamer.get_active_datasets(), + streamer.config_manager.datasets, + ) + + invalid_vars = [var for var in data["variables"] if var not in available_vars] + if invalid_vars: + return ( + jsonify( + { + "error": f"Variables not available: {', '.join(invalid_vars)}", + "available_variables": available_vars, + } + ), + 400, + ) + + # Validar trigger si está habilitado + if data.get("trigger_enabled") and data.get("trigger_variable"): + boolean_vars = streamer.data_streamer.plot_manager.get_boolean_variables( + streamer.data_streamer.get_active_datasets(), + streamer.config_manager.datasets, + ) + + if data["trigger_variable"] not in boolean_vars: + return ( + jsonify( + { + "error": f"Trigger variable '{data['trigger_variable']}' is not a boolean variable", + "boolean_variables": boolean_vars, + } + ), + 400, + ) + + # Crear configuración actualizada + config = { + "name": data.get("name", f"Plot {session_id}"), + "variables": data["variables"], + "time_window": int(data["time_window"]), + "y_min": data.get("y_min"), + "y_max": data.get("y_max"), + "trigger_variable": data.get("trigger_variable"), + "trigger_enabled": data.get("trigger_enabled", False), + "trigger_on_true": data.get("trigger_on_true", True), + } + + # Convertir valores numéricos si están presentes + if config["y_min"] is not None: + config["y_min"] = float(config["y_min"]) + if config["y_max"] is not None: + config["y_max"] = float(config["y_max"]) + + # Actualizar configuración + success = streamer.data_streamer.plot_manager.update_session_config( + session_id, config + ) + + if success: + return jsonify( + { + "success": True, + "message": f"Plot session '{config['name']}' updated successfully", + } + ) + else: + return jsonify({"error": "Plot session not found"}), 404 + + except Exception as e: + return jsonify({"error": str(e)}), 500 + + @app.route("/api/plots/variables", methods=["GET"]) def get_plot_variables(): """Get available variables for plotting""" diff --git a/plc_datasets.json b/plc_datasets.json index 3e0c0a5..7ffcffa 100644 --- a/plc_datasets.json +++ b/plc_datasets.json @@ -70,5 +70,5 @@ ], "current_dataset_id": "dar", "version": "1.0", - "last_update": "2025-07-21T09:21:45.845720" + "last_update": "2025-07-21T12:29:22.704454" } \ No newline at end of file diff --git a/plot_sessions.json b/plot_sessions.json new file mode 100644 index 0000000..dafc043 --- /dev/null +++ b/plot_sessions.json @@ -0,0 +1,20 @@ +{ + "plots": { + "plot_6": { + "name": "Condix", + "variables": [ + "UR62_Brix" + ], + "time_window": 60, + "y_min": null, + "y_max": null, + "trigger_variable": null, + "trigger_enabled": false, + "trigger_on_true": true, + "session_id": "plot_6" + } + }, + "session_counter": 7, + "last_saved": "2025-07-21T11:26:04.418899", + "version": "1.0" +} \ No newline at end of file diff --git a/static/css/styles.css b/static/css/styles.css index aad22bf..d0ed8fc 100644 --- a/static/css/styles.css +++ b/static/css/styles.css @@ -703,27 +703,27 @@ textarea { align-items: flex-start; gap: 0.5rem; } - + .plot-controls { width: 100%; justify-content: flex-start; } - + .plot-stats { flex-direction: column; gap: 0.25rem; } - + .modal-content { margin: 10% auto; width: 95%; padding: 1rem; } - + .form-actions { flex-direction: column; } - + .range-inputs { flex-direction: column; align-items: stretch; @@ -750,8 +750,10 @@ textarea { border-bottom: 3px solid transparent; transition: all 0.2s ease; white-space: nowrap; - min-width: 120px; + flex: 1; text-align: center; + min-width: 0; + max-width: none; } .tab-btn:hover { @@ -765,6 +767,104 @@ textarea { background: var(--pico-card-background-color); } +/* Plot tabs específicos */ +.tab-btn.plot-tab { + position: relative; + padding-right: 2.5rem; +} + +.tab-close { + position: absolute; + right: 0.5rem; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + color: var(--pico-muted-color); + cursor: pointer; + font-size: 1.2rem; + line-height: 1; + padding: 0.25rem; + border-radius: 50%; + transition: all 0.2s ease; +} + +.tab-close:hover { + background: var(--pico-muted-background-color); + color: var(--pico-color-red-500); +} + +/* SUB-TABS STYLES */ +.sub-tabs { + display: flex; + border-bottom: var(--pico-border-width) solid var(--pico-border-color); + margin-bottom: 1rem; + background: var(--pico-muted-background-color); + border-radius: var(--pico-border-radius); + overflow-x: auto; +} + +.sub-tab-btn { + padding: 0.75rem 1rem; + border: none; + background: none; + color: var(--pico-muted-color); + cursor: pointer; + font-weight: 500; + border-bottom: 2px solid transparent; + transition: all 0.2s ease; + white-space: nowrap; + flex: 1; + text-align: center; + min-width: 0; + font-size: 0.9rem; +} + +.sub-tab-btn:hover { + color: var(--pico-primary); + background: var(--pico-card-background-color); +} + +.sub-tab-btn.active { + color: var(--pico-primary); + border-bottom-color: var(--pico-primary); + background: var(--pico-card-background-color); +} + +.sub-tab-btn.plot-sub-tab { + position: relative; + padding-right: 2rem; +} + +.sub-tab-close { + position: absolute; + right: 0.25rem; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + color: var(--pico-muted-color); + cursor: pointer; + font-size: 1rem; + line-height: 1; + padding: 0.2rem; + border-radius: 50%; + transition: all 0.2s ease; +} + +.sub-tab-close:hover { + background: var(--pico-muted-background-color); + color: var(--pico-color-red-500); +} + +.sub-tab-content { + display: none; +} + +.sub-tab-content.active { + display: block; +} + .tab-content { display: none; } @@ -773,6 +873,391 @@ textarea { display: block; } +/* COLLAPSIBLE PLOT FORM STYLES */ +.collapsible-section { + margin-bottom: 1.5rem; + border: var(--pico-border-width) solid var(--pico-border-color); + border-radius: var(--pico-border-radius); + overflow: hidden; + transition: all 0.3s ease; +} + +.plot-form-article { + margin: 0; + background: var(--pico-card-background-color); +} + +.plot-form-article header { + background: var(--pico-primary-background); + color: var(--pico-primary-inverse); + padding: 1rem 1.5rem; + margin: 0; +} + +.close-btn { + background: none; + border: none; + color: inherit; + font-size: 1.5rem; + cursor: pointer; + padding: 0.25rem; + border-radius: 50%; + transition: all 0.2s ease; +} + +.close-btn:hover { + background: rgba(255, 255, 255, 0.2); +} + +.variables-selection { + border: var(--pico-border-width) solid var(--pico-border-color); + border-radius: var(--pico-border-radius); + padding: 1rem; + background: var(--pico-muted-background-color); +} + +.selected-variables { + margin-bottom: 1rem; + min-height: 2rem; + padding: 0.5rem; + border: var(--pico-border-width) solid var(--pico-border-color); + border-radius: var(--pico-border-radius); + background: var(--pico-card-background-color); +} + +.selected-variables .no-variables { + color: var(--pico-muted-color); + font-style: italic; + margin: 0; +} + +.variable-chip { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.25rem 0.75rem; + margin: 0.25rem; + background: var(--pico-primary-background); + color: var(--pico-primary-inverse); + border-radius: var(--pico-border-radius); + font-size: 0.875rem; + font-weight: 500; +} + +.variable-chip .color-indicator { + width: 12px; + height: 12px; + border-radius: 50%; + border: 1px solid rgba(255, 255, 255, 0.3); +} + +.variable-chip .remove-variable { + background: none; + border: none; + color: inherit; + cursor: pointer; + font-size: 1.1rem; + padding: 0; + margin-left: 0.25rem; + opacity: 0.8; + transition: opacity 0.2s ease; +} + +.variable-chip .remove-variable:hover { + opacity: 1; +} + +/* VARIABLE SELECTION MODAL STYLES */ +.variable-modal { + max-width: 900px; + width: 95%; + max-height: 85vh; + display: flex; + flex-direction: column; +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1.5rem; + background: var(--pico-primary-background); + color: var(--pico-primary-inverse); + margin: 0; + border-radius: var(--pico-border-radius) var(--pico-border-radius) 0 0; +} + +.modal-header h3 { + margin: 0; + color: inherit; +} + +.modal-body { + flex: 1; + padding: 1.5rem; + overflow-y: auto; +} + +.modal-footer { + padding: 1rem 1.5rem; + background: var(--pico-muted-background-color); + border-top: var(--pico-border-width) solid var(--pico-border-color); + display: flex; + justify-content: flex-end; + gap: 1rem; +} + +.variable-selection-container { + display: grid; + grid-template-columns: 200px 1fr; + gap: 1.5rem; + margin-bottom: 1.5rem; +} + +.datasets-sidebar { + border: var(--pico-border-width) solid var(--pico-border-color); + border-radius: var(--pico-border-radius); + padding: 1rem; + background: var(--pico-muted-background-color); + height: fit-content; +} + +.datasets-sidebar h4 { + margin: 0 0 1rem 0; + color: var(--pico-h4-color); + font-size: 1rem; +} + +.datasets-list { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.dataset-item { + padding: 0.75rem; + border: var(--pico-border-width) solid var(--pico-border-color); + border-radius: var(--pico-border-radius); + background: var(--pico-card-background-color); + cursor: pointer; + transition: all 0.2s ease; + font-size: 0.875rem; +} + +.dataset-item:hover { + background: var(--pico-primary-hover); + color: var(--pico-primary-inverse); +} + +.dataset-item.active { + background: var(--pico-primary-background); + color: var(--pico-primary-inverse); +} + +.dataset-item .dataset-name { + font-weight: bold; + display: block; +} + +.dataset-item .dataset-info { + font-size: 0.75rem; + opacity: 0.8; + margin-top: 0.25rem; +} + +.variables-main { + border: var(--pico-border-width) solid var(--pico-border-color); + border-radius: var(--pico-border-radius); + background: var(--pico-card-background-color); +} + +.variables-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem; + background: var(--pico-muted-background-color); + border-bottom: var(--pico-border-width) solid var(--pico-border-color); +} + +.variables-header h4 { + margin: 0; + color: var(--pico-h4-color); + font-size: 1rem; +} + +.selection-controls { + display: flex; + gap: 0.5rem; +} + +.selection-controls .btn { + padding: 0.25rem 0.75rem; + font-size: 0.75rem; +} + +.variables-list { + padding: 1rem; + max-height: 300px; + overflow-y: auto; +} + +.variable-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem; + margin-bottom: 0.5rem; + border: var(--pico-border-width) solid var(--pico-border-color); + border-radius: var(--pico-border-radius); + background: var(--pico-muted-background-color); + transition: all 0.2s ease; +} + +.variable-item:last-child { + margin-bottom: 0; +} + +.variable-item.selected { + background: var(--pico-primary-background); + color: var(--pico-primary-inverse); + border-color: var(--pico-primary); +} + +.variable-info { + flex: 1; +} + +.variable-name { + font-weight: bold; + margin-bottom: 0.25rem; +} + +.variable-details { + font-size: 0.75rem; + opacity: 0.8; +} + +.variable-controls { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.variable-checkbox { + transform: scale(1.2); +} + +.color-selector { + width: 40px; + height: 30px; + border: none; + border-radius: var(--pico-border-radius); + cursor: pointer; + transition: all 0.2s ease; +} + +.color-selector:hover { + transform: scale(1.1); +} + +.color-selector:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; +} + +.selected-summary { + border: var(--pico-border-width) solid var(--pico-border-color); + border-radius: var(--pico-border-radius); + padding: 1rem; + background: var(--pico-muted-background-color); +} + +.selected-summary h4 { + margin: 0 0 1rem 0; + color: var(--pico-h4-color); + font-size: 1rem; +} + +.selected-summary-list { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + min-height: 2rem; +} + +.selected-summary-item { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0.75rem; + background: var(--pico-card-background-color); + border: var(--pico-border-width) solid var(--pico-border-color); + border-radius: var(--pico-border-radius); + font-size: 0.875rem; +} + +.selected-summary-item .color-indicator { + width: 16px; + height: 16px; + border-radius: 50%; + border: 1px solid var(--pico-border-color); +} + +.no-dataset-message { + text-align: center; + color: var(--pico-muted-color); + font-style: italic; + padding: 2rem; + margin: 0; +} + +/* Responsive design para variable modal */ +@media (max-width: 768px) { + .variable-modal { + width: 98%; + max-height: 90vh; + } + + .variable-selection-container { + grid-template-columns: 1fr; + gap: 1rem; + } + + .datasets-sidebar { + max-height: 150px; + overflow-y: auto; + } + + .variables-header { + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + } + + .selection-controls { + width: 100%; + justify-content: flex-end; + } + + .variable-item { + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + } + + .variable-controls { + width: 100%; + justify-content: flex-end; + } + + .modal-footer { + flex-direction: column; + gap: 0.5rem; + } +} + /* Responsive tabs */ @media (max-width: 768px) { .tabs { @@ -782,14 +1267,14 @@ textarea { border-radius: var(--pico-border-radius) 0 0 var(--pico-border-radius); margin-bottom: 1rem; } - + .tab-btn { border-bottom: none; border-right: 3px solid transparent; text-align: left; min-width: auto; } - + .tab-btn.active { border-right-color: var(--pico-primary); } diff --git a/static/js/datasets.js b/static/js/datasets.js index 843e69b..bdf5d98 100644 --- a/static/js/datasets.js +++ b/static/js/datasets.js @@ -7,7 +7,7 @@ let currentDatasets = {}; let currentDatasetId = null; // Cargar todos los datasets desde API -function loadDatasets() { +window.loadDatasets = function () { fetch('/api/datasets') .then(response => response.json()) .then(data => { diff --git a/static/js/events.js b/static/js/events.js index 2e1e765..2b2a410 100644 --- a/static/js/events.js +++ b/static/js/events.js @@ -89,10 +89,80 @@ function clearLogView() { // Inicializar listeners para eventos function initEventListeners() { - // Botones de control de log - document.querySelector('button[onclick="refreshEventLog()"]').addEventListener('click', refreshEventLog); - document.querySelector('button[onclick="clearLogView()"]').addEventListener('click', clearLogView); + // Botones de control de log para el tab de events + const refreshBtn = document.getElementById('refresh-events-btn'); + const clearBtn = document.getElementById('clear-events-btn'); - // Selector de límite de log - document.getElementById('log-limit').addEventListener('change', refreshEventLog); + if (refreshBtn) { + refreshBtn.addEventListener('click', loadEvents); + } + + if (clearBtn) { + clearBtn.addEventListener('click', clearEventsView); + } +} + +// Función para cargar eventos en el tab de events +window.loadEvents = function() { + fetch('/api/events?limit=50') + .then(response => response.json()) + .then(data => { + if (data.success) { + const eventsContainer = document.getElementById('events-container'); + const eventsCount = document.getElementById('events-count'); + + // Limpiar contenedor + eventsContainer.innerHTML = ''; + + // Actualizar contador + eventsCount.textContent = data.showing || 0; + + // Añadir eventos (orden inverso para mostrar primero los más nuevos) + const events = data.events.reverse(); + + if (events.length === 0) { + eventsContainer.innerHTML = ` +
+
+ 📋 System + ${new Date().toLocaleString('es-ES')} +
+
No events found
+
+ `; + } else { + events.forEach(event => { + eventsContainer.appendChild(createLogEntry(event)); + }); + } + + // Auto-scroll al inicio para mostrar eventos más nuevos + eventsContainer.scrollTop = 0; + } else { + console.error('Error loading events:', data.error); + showMessage('Error loading events log', 'error'); + } + }) + .catch(error => { + console.error('Error fetching events:', error); + showMessage('Error fetching events log', 'error'); + }); +} + +// Función para limpiar vista de eventos +function clearEventsView() { + const eventsContainer = document.getElementById('events-container'); + const eventsCount = document.getElementById('events-count'); + + eventsContainer.innerHTML = ` +
+
+ 🧹 System + ${new Date().toLocaleString('es-ES')} +
+
Events view cleared. Click refresh to reload events.
+
+ `; + + eventsCount.textContent = '0'; } \ No newline at end of file diff --git a/static/js/plotting.js b/static/js/plotting.js index b6dae28..31c5a39 100644 --- a/static/js/plotting.js +++ b/static/js/plotting.js @@ -12,9 +12,15 @@ class PlotManager { // Colores para las variables this.colors = [ '#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF', - '#FF9F40', '#FF6384', '#C9CBCF', '#4BC0C0', '#FF6384' + '#FF9F40', '#8A2BE2', '#FF1493', '#00CED1', '#32CD32', + '#FFB347', '#DA70D6', '#40E0D0', '#EE82EE', '#90EE90' ]; + // Variables seleccionadas para el formulario + this.selectedVariables = new Map(); // variable_name -> { color, dataset } + this.availableDatasets = []; + this.currentEditingSession = null; + this.init(); } @@ -22,6 +28,12 @@ class PlotManager { // Cargar sesiones existentes al inicializar this.loadExistingSessions(); + // Inicializar formulario colapsable + this.initCollapsibleForm(); + + // Inicializar modal de selección de variables + this.initVariableModal(); + // Iniciar actualización automática this.startAutoUpdate(); @@ -36,9 +48,8 @@ class PlotManager { if (data.sessions) { for (const session of data.sessions) { - if (session.is_active) { - this.createPlotSession(session.session_id, session); - } + // Cargar todas las sesiones (activas e inactivas) y crear tabs + await this.createPlotSessionTab(session.session_id, session); } } } catch (error) { @@ -271,6 +282,88 @@ class PlotManager { } } + async createPlotSessionTab(sessionId, sessionInfo) { + // Crear tab dinámico para el plot + if (typeof tabManager !== 'undefined') { + tabManager.createPlotTab(sessionId, sessionInfo.name || `Plot ${sessionId}`); + } + + // Crear el Chart.js en el canvas del tab + const ctx = document.getElementById(`chart-${sessionId}`).getContext('2d'); + const chart = new Chart(ctx, { + type: 'line', + data: { + datasets: [] + }, + options: { + responsive: true, + maintainAspectRatio: false, + animation: { + duration: 0 // Sin animación para mejor performance + }, + scales: { + x: { + type: 'time', + time: { + unit: 'second', + displayFormats: { + second: 'HH:mm:ss' + } + }, + title: { + display: true, + text: 'Time' + } + }, + y: { + title: { + display: true, + text: 'Value' + } + } + }, + plugins: { + legend: { + display: true, + position: 'top' + }, + tooltip: { + mode: 'index', + intersect: false + } + }, + interaction: { + mode: 'nearest', + axis: 'x', + intersect: false + } + } + }); + + this.sessions.set(sessionId, chart); + + // Actualizar estadísticas del plot + this.updatePlotStats(sessionId, sessionInfo); + + console.log(`📈 Created plot tab and chart for session: ${sessionId}`); + } + + updatePlotStats(sessionId, sessionInfo) { + const statsElement = document.getElementById(`plot-stats-${sessionId}`); + if (statsElement) { + const status = sessionInfo.is_active ? + (sessionInfo.is_paused ? 'Paused' : 'Active') : + 'Stopped'; + + statsElement.innerHTML = ` + Variables: ${sessionInfo.variables_count || 0} | + Data Points: 0 | + Status: ${status} | + ${sessionInfo.trigger_enabled ? `Trigger: ${sessionInfo.trigger_variable} (${sessionInfo.trigger_on_true ? 'True' : 'False'})` : 'No Trigger'} + `; + } + } + async createNewPlot(config) { try { const response = await fetch('/api/plots', { @@ -284,18 +377,25 @@ class PlotManager { const result = await response.json(); if (result.success) { - // Crear la sesión visual + // Crear tab dinámico y chart const sessionConfig = { name: config.name, variables_count: config.variables.length, trigger_enabled: config.trigger_enabled, trigger_variable: config.trigger_variable, - trigger_on_true: config.trigger_on_true + trigger_on_true: config.trigger_on_true, + is_active: false, // Por defecto stopped + is_paused: false }; - this.createPlotSession(result.session_id, sessionConfig); - showNotification(result.message, 'success'); + await this.createPlotSessionTab(result.session_id, sessionConfig); + // Cambiar al sub-tab del nuevo plot + if (typeof tabManager !== 'undefined') { + tabManager.switchSubTab(`plot-${result.session_id}`); + } + + showNotification(result.message, 'success'); return result.session_id; } else { showNotification(result.error, 'error'); @@ -308,9 +408,491 @@ class PlotManager { } } - getColor(variable, alpha = 1.0) { - const index = this.hashCode(variable) % this.colors.length; - return this.colors[index]; + initCollapsibleForm() { + const toggleBtn = document.getElementById('toggle-plot-form-btn'); + const formContainer = document.getElementById('plot-form-container'); + const closeBtn = document.getElementById('close-plot-form-btn'); + const cancelBtn = document.getElementById('cancel-plot-form'); + const plotForm = document.getElementById('plot-form'); + + if (toggleBtn) { + toggleBtn.addEventListener('click', () => { + this.showPlotForm(); + }); + } + + if (closeBtn) { + closeBtn.addEventListener('click', () => { + this.hidePlotForm(); + }); + } + + if (cancelBtn) { + cancelBtn.addEventListener('click', () => { + this.hidePlotForm(); + }); + } + + if (plotForm) { + plotForm.addEventListener('submit', (e) => { + e.preventDefault(); + this.handleFormSubmit(); + }); + } + + // Botón para abrir modal de variables + const selectVariablesBtn = document.getElementById('select-variables-btn'); + if (selectVariablesBtn) { + selectVariablesBtn.addEventListener('click', () => { + this.showVariableModal(); + }); + } + } + + initVariableModal() { + const modal = document.getElementById('variable-selection-modal'); + const closeBtn = document.getElementById('close-variable-modal'); + const cancelBtn = document.getElementById('cancel-variable-selection'); + const confirmBtn = document.getElementById('confirm-variable-selection'); + const selectAllBtn = document.getElementById('select-all-variables'); + const deselectAllBtn = document.getElementById('deselect-all-variables'); + + if (closeBtn) { + closeBtn.addEventListener('click', () => { + this.hideVariableModal(); + }); + } + + if (cancelBtn) { + cancelBtn.addEventListener('click', () => { + this.hideVariableModal(); + }); + } + + if (confirmBtn) { + confirmBtn.addEventListener('click', () => { + this.confirmVariableSelection(); + }); + } + + if (selectAllBtn) { + selectAllBtn.addEventListener('click', () => { + this.selectAllVisibleVariables(); + }); + } + + if (deselectAllBtn) { + deselectAllBtn.addEventListener('click', () => { + this.deselectAllVisibleVariables(); + }); + } + + // Prevenir cierre con teclas como Ctrl+V, Escape (opcional) + if (modal) { + modal.addEventListener('keydown', (e) => { + // Prevenir cierre accidental con Ctrl+V y otras combinaciones + if (e.ctrlKey || e.metaKey) { + e.stopPropagation(); + } + + // Solo permitir Escape para cerrar si se presiona intencionalmente + if (e.key === 'Escape' && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey) { + this.hideVariableModal(); + } + }); + + // Prevenir cierre al hacer clic en el modal (solo en el backdrop) + modal.addEventListener('click', (e) => { + if (e.target === modal) { + this.hideVariableModal(); + } + }); + } + } + + showPlotForm(sessionId = null) { + console.log('showPlotForm called with sessionId:', sessionId); + + // Asegurar que estemos en el tab de plotting + if (typeof tabManager !== 'undefined' && tabManager.getCurrentTab() !== 'plotting') { + console.log('Switching to plotting tab'); + tabManager.switchTab('plotting'); + } + + const formContainer = document.getElementById('plot-form-container'); + const formTitle = document.getElementById('plot-form-title'); + const submitBtn = document.getElementById('plot-form-submit'); + + // Verificar que los elementos existan + if (!formContainer) { + console.error('plot-form-container element not found'); + showNotification('Error: Form container not found', 'error'); + return; + } + + if (!formTitle) { + console.warn('plot-form-title element not found'); + } + + if (!submitBtn) { + console.warn('plot-form-submit element not found'); + } + + this.currentEditingSession = sessionId; + + if (sessionId) { + // Modo edición + console.log('Setting up form for editing'); + if (formTitle) formTitle.textContent = '✏️ Edit Plot'; + if (submitBtn) submitBtn.textContent = 'Update Plot'; + this.loadPlotConfigForEdit(sessionId); + } else { + // Modo creación + console.log('Setting up form for creation'); + if (formTitle) formTitle.textContent = '🆕 Create New Plot'; + if (submitBtn) submitBtn.textContent = 'Create Plot'; + this.resetPlotForm(); + } + + console.log('Making form visible'); + formContainer.style.display = 'block'; + + // Scroll hacia el formulario para que sea visible + setTimeout(() => { + formContainer.scrollIntoView({ behavior: 'smooth', block: 'start' }); + console.log('Scrolled to form'); + }, 100); + + // Cargar variables disponibles para triggers + this.loadTriggerVariables(); + + console.log('showPlotForm completed'); + } + + hidePlotForm() { + const formContainer = document.getElementById('plot-form-container'); + formContainer.style.display = 'none'; + this.currentEditingSession = null; + this.selectedVariables.clear(); + this.updateSelectedVariablesDisplay(); + } + + resetPlotForm() { + const form = document.getElementById('plot-form'); + form.reset(); + document.getElementById('plot-form-time-window').value = '60'; + document.getElementById('plot-form-trigger-on-true').checked = true; + this.selectedVariables.clear(); + this.updateSelectedVariablesDisplay(); + } + + async loadPlotConfigForEdit(sessionId) { + try { + const response = await fetch(`/api/plots/${sessionId}/config`); + const data = await response.json(); + + if (data.success && data.config) { + const config = data.config; + + // Llenar formulario + document.getElementById('plot-form-name').value = config.name || ''; + document.getElementById('plot-form-time-window').value = config.time_window || 60; + document.getElementById('plot-form-y-min').value = config.y_min || ''; + document.getElementById('plot-form-y-max').value = config.y_max || ''; + document.getElementById('plot-form-trigger-enabled').checked = config.trigger_enabled || false; + document.getElementById('plot-form-trigger-variable').value = config.trigger_variable || ''; + document.getElementById('plot-form-trigger-on-true').checked = config.trigger_on_true !== false; + + // Configurar trigger + this.togglePlotFormTriggerConfig(); + + // Cargar variables seleccionadas + this.selectedVariables.clear(); + if (config.variables) { + for (let i = 0; i < config.variables.length; i++) { + const variable = config.variables[i]; + const color = this.getColor(variable, i); + this.selectedVariables.set(variable, { + color: color, + dataset: 'Unknown' // Will be resolved when loading datasets + }); + } + } + this.updateSelectedVariablesDisplay(); + } + } catch (error) { + console.error('Error loading plot config for edit:', error); + showNotification('Error loading plot configuration', 'error'); + } + } + + async showVariableModal() { + const modal = document.getElementById('variable-selection-modal'); + + // Cargar datasets y variables + await this.loadDatasetsAndVariables(); + + modal.style.display = 'block'; + } + + hideVariableModal() { + const modal = document.getElementById('variable-selection-modal'); + modal.style.display = 'none'; + } + + async loadDatasetsAndVariables() { + try { + // Cargar datasets + const response = await fetch('/api/datasets'); + const data = await response.json(); + + if (data.success) { + this.availableDatasets = Object.entries(data.datasets).map(([id, dataset]) => ({ + id: id, + name: dataset.name, + variables: dataset.variables || {}, + active: data.active_datasets.includes(id) + })); + + this.updateDatasetsDisplay(); + } + } catch (error) { + console.error('Error loading datasets:', error); + showNotification('Error loading datasets', 'error'); + } + } + + updateDatasetsDisplay() { + const container = document.getElementById('datasets-list'); + container.innerHTML = ''; + + if (this.availableDatasets.length === 0) { + container.innerHTML = '

No datasets available

'; + return; + } + + for (const dataset of this.availableDatasets) { + const item = document.createElement('div'); + item.className = 'dataset-item'; + item.dataset.datasetId = dataset.id; + + const variableCount = Object.keys(dataset.variables).length; + const statusIcon = dataset.active ? '🟢' : '⚫'; + + item.innerHTML = ` + ${statusIcon} ${dataset.name} + ${variableCount} variables + `; + + item.addEventListener('click', () => { + this.selectDataset(dataset.id); + }); + + container.appendChild(item); + } + } + + selectDataset(datasetId) { + // Actualizar UI + document.querySelectorAll('.dataset-item').forEach(item => { + item.classList.remove('active'); + }); + document.querySelector(`[data-dataset-id="${datasetId}"]`).classList.add('active'); + + // Mostrar variables del dataset + const dataset = this.availableDatasets.find(d => d.id === datasetId); + if (dataset) { + this.updateVariablesDisplay(dataset); + } + } + + updateVariablesDisplay(dataset) { + const container = document.getElementById('variables-list'); + container.innerHTML = ''; + + const variables = Object.entries(dataset.variables); + + if (variables.length === 0) { + container.innerHTML = '

No variables in this dataset

'; + return; + } + + for (const [varName, varConfig] of variables) { + const item = document.createElement('div'); + item.className = 'variable-item'; + item.dataset.variableName = varName; + + const isSelected = this.selectedVariables.has(varName); + const selectedData = this.selectedVariables.get(varName); + const color = selectedData?.color || this.getNextAvailableColor(); + + if (isSelected) { + item.classList.add('selected'); + } + + // Formatear detalles de la variable + let details = `${varConfig.type.toUpperCase()}`; + if (varConfig.area === 'db') { + details += ` - DB${varConfig.db}.${varConfig.offset}`; + } else if (varConfig.area === 'mw') { + details += ` - MW${varConfig.offset}`; + } else if (['e', 'a', 'mb'].includes(varConfig.area)) { + details += ` - ${varConfig.area.toUpperCase()}${varConfig.offset}.${varConfig.bit || 0}`; + } + + item.innerHTML = ` +
+
${varName}
+
${details}
+
+
+ + +
+ `; + + container.appendChild(item); + } + } + + toggleVariableSelection(variableName, datasetId, selected) { + if (selected) { + const color = this.getNextAvailableColor(); + const datasetName = this.availableDatasets.find(d => d.id === datasetId)?.name || datasetId; + + this.selectedVariables.set(variableName, { + color: color, + dataset: datasetName + }); + + // Habilitar selector de color + const colorSelector = document.querySelector(`[data-variable-name="${variableName}"] .color-selector`); + if (colorSelector) { + colorSelector.disabled = false; + colorSelector.value = color; + } + + // Marcar item como seleccionado + const item = document.querySelector(`[data-variable-name="${variableName}"]`); + if (item) { + item.classList.add('selected'); + } + } else { + this.selectedVariables.delete(variableName); + + // Deshabilitar selector de color + const colorSelector = document.querySelector(`[data-variable-name="${variableName}"] .color-selector`); + if (colorSelector) { + colorSelector.disabled = true; + } + + // Desmarcar item + const item = document.querySelector(`[data-variable-name="${variableName}"]`); + if (item) { + item.classList.remove('selected'); + } + } + + this.updateSelectedSummary(); + } + + updateVariableColor(variableName, color) { + if (this.selectedVariables.has(variableName)) { + const data = this.selectedVariables.get(variableName); + data.color = color; + this.selectedVariables.set(variableName, data); + this.updateSelectedSummary(); + } + } + + selectAllVisibleVariables() { + const checkboxes = document.querySelectorAll('.variables-list .variable-checkbox'); + checkboxes.forEach(checkbox => { + if (!checkbox.checked) { + checkbox.checked = true; + checkbox.onchange(); + } + }); + } + + deselectAllVisibleVariables() { + const checkboxes = document.querySelectorAll('.variables-list .variable-checkbox'); + checkboxes.forEach(checkbox => { + if (checkbox.checked) { + checkbox.checked = false; + checkbox.onchange(); + } + }); + } + + updateSelectedSummary() { + const container = document.getElementById('selected-variables-summary'); + container.innerHTML = ''; + + if (this.selectedVariables.size === 0) { + container.innerHTML = '

No variables selected

'; + return; + } + + for (const [varName, data] of this.selectedVariables) { + const item = document.createElement('div'); + item.className = 'selected-summary-item'; + item.innerHTML = ` + + ${varName} + (${data.dataset}) + `; + container.appendChild(item); + } + } + + confirmVariableSelection() { + this.updateSelectedVariablesDisplay(); + this.hideVariableModal(); + } + + updateSelectedVariablesDisplay() { + const container = document.getElementById('selected-variables-display'); + container.innerHTML = ''; + + if (this.selectedVariables.size === 0) { + container.innerHTML = '

No variables selected

'; + return; + } + + for (const [varName, data] of this.selectedVariables) { + const chip = document.createElement('div'); + chip.className = 'variable-chip'; + chip.innerHTML = ` + + ${varName} + + `; + container.appendChild(chip); + } + } + + removeSelectedVariable(variableName) { + this.selectedVariables.delete(variableName); + this.updateSelectedVariablesDisplay(); + } + + getNextAvailableColor() { + const usedColors = new Set([...this.selectedVariables.values()].map(v => v.color)); + return this.colors.find(color => !usedColors.has(color)) || this.colors[0]; + } + + getColor(variable, index = null) { + if (index !== null) { + return this.colors[index % this.colors.length]; + } + const hash = this.hashCode(variable); + return this.colors[hash % this.colors.length]; } hashCode(str) { @@ -318,11 +900,143 @@ class PlotManager { for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; - hash = hash & hash; // Convert to 32bit integer + hash = hash & hash; } return Math.abs(hash); } + async loadTriggerVariables() { + try { + const response = await fetch('/api/plots/variables'); + const data = await response.json(); + + const triggerSelect = document.getElementById('plot-form-trigger-variable'); + triggerSelect.innerHTML = ''; + + if (data.boolean_variables) { + for (const variable of data.boolean_variables) { + const option = document.createElement('option'); + option.value = variable; + option.textContent = variable; + triggerSelect.appendChild(option); + } + } + } catch (error) { + console.error('Error loading trigger variables:', error); + } + } + + async handleFormSubmit() { + if (this.selectedVariables.size === 0) { + showNotification('Please select at least one variable', 'error'); + return; + } + + const config = { + name: document.getElementById('plot-form-name').value || `Plot ${Date.now()}`, + variables: Array.from(this.selectedVariables.keys()), + time_window: parseInt(document.getElementById('plot-form-time-window').value) || 60, + y_min: document.getElementById('plot-form-y-min').value || null, + y_max: document.getElementById('plot-form-y-max').value || null, + trigger_enabled: document.getElementById('plot-form-trigger-enabled').checked, + trigger_variable: document.getElementById('plot-form-trigger-variable').value || null, + trigger_on_true: document.getElementById('plot-form-trigger-on-true').checked + }; + + // Convertir valores numéricos + if (config.y_min) config.y_min = parseFloat(config.y_min); + if (config.y_max) config.y_max = parseFloat(config.y_max); + + try { + let response; + let sessionId = null; + + if (this.currentEditingSession) { + // Modo edición: ELIMINAR el plot existente y crear uno nuevo desde cero + console.log(`Deleting existing plot ${this.currentEditingSession} to recreate it from scratch`); + + // 1. Eliminar el plot existente + const deleteResponse = await fetch(`/api/plots/${this.currentEditingSession}`, { + method: 'DELETE' + }); + + if (!deleteResponse.ok) { + throw new Error('Failed to delete existing plot'); + } + + // 2. Remover de la UI + if (typeof tabManager !== 'undefined') { + tabManager.removePlotTab(this.currentEditingSession); + } + + // 3. Remover del PlotManager + if (this.sessions.has(this.currentEditingSession)) { + const chart = this.sessions.get(this.currentEditingSession); + if (chart) { + chart.destroy(); + } + this.sessions.delete(this.currentEditingSession); + } + + // 4. Crear nuevo plot desde cero + response = await fetch('/api/plots', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(config) + }); + + const deleteResult = await deleteResponse.json(); + console.log(`Old plot deleted: ${deleteResult.message || 'Success'}`); + } else { + // Modo creación normal + response = await fetch('/api/plots', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(config) + }); + } + + const result = await response.json(); + + if (result.success) { + sessionId = result.session_id; + + if (this.currentEditingSession) { + showNotification(`Plot recreated successfully: ${config.name}`, 'success'); + } else { + showNotification(result.message, 'success'); + } + + // Crear nuevo tab para la sesión (tanto en modo edición como creación) + await this.createPlotSessionTab(sessionId, { + name: config.name, + variables_count: config.variables.length, + trigger_enabled: config.trigger_enabled, + trigger_variable: config.trigger_variable, + trigger_on_true: config.trigger_on_true, + is_active: false, + is_paused: false + }); + + // Cambiar al sub-tab del nuevo plot + if (typeof tabManager !== 'undefined') { + tabManager.switchSubTab(`plot-${sessionId}`); + } + + this.hidePlotForm(); + } else { + showNotification(result.error, 'error'); + } + } catch (error) { + console.error('Error submitting plot form:', error); + if (this.currentEditingSession) { + showNotification('Error recreating plot. Please try again.', 'error'); + } else { + showNotification('Error creating plot', 'error'); + } + } + } + destroy() { this.stopAutoUpdate(); @@ -336,135 +1050,83 @@ class PlotManager { } } -// Funciones para el modal de creación de plots -let plotModal = null; -let availableVariables = []; -let booleanVariables = []; +// Función global para toggle de trigger config +window.togglePlotFormTriggerConfig = function () { + const triggerEnabled = document.getElementById('plot-form-trigger-enabled'); + const triggerConfig = document.getElementById('plot-form-trigger-config'); -async function showNewPlotModal() { - if (!plotModal) { - plotModal = document.getElementById('new-plot-modal'); - } - - // Cargar variables disponibles - await loadPlotVariables(); - - // Limpiar formulario - document.getElementById('new-plot-form').reset(); - - // Mostrar modal - plotModal.style.display = 'block'; -} - -function closeNewPlotModal() { - if (plotModal) { - plotModal.style.display = 'none'; + if (triggerConfig) { + triggerConfig.style.display = triggerEnabled.checked ? 'block' : 'none'; } } -async function loadPlotVariables() { - try { - const response = await fetch('/api/plots/variables'); - const data = await response.json(); +// Función global corregida para editar plots +window.editPlotSession = function (sessionId) { + console.log('editPlotSession called with sessionId:', sessionId); + console.log('plotManager available:', !!plotManager); + console.log('tabManager available:', typeof tabManager !== 'undefined'); - availableVariables = data.available_variables || []; - booleanVariables = data.boolean_variables || []; - - // Actualizar selector de variables - updateVariableSelector(); - - // Actualizar selector de trigger - updateTriggerSelector(); - - } catch (error) { - console.error('Error loading plot variables:', error); - showNotification('Error loading available variables', 'error'); - } -} - -function updateVariableSelector() { - const container = document.getElementById('plot-variables-selector'); - container.innerHTML = ''; - - if (availableVariables.length === 0) { - container.innerHTML = '

No variables available. Activate datasets first.

'; + if (!sessionId || sessionId.trim() === '') { + console.error('Invalid or empty session ID provided to editPlotSession'); + showNotification('Error: Invalid session ID', 'error'); return; } - for (const variable of availableVariables) { - const div = document.createElement('div'); - div.className = 'variable-checkbox'; - div.innerHTML = ` - - `; - container.appendChild(div); - } -} - -function updateTriggerSelector() { - const triggerSelect = document.getElementById('plot-trigger-variable'); - const triggerContainer = document.getElementById('trigger-config'); - - if (booleanVariables.length === 0) { - triggerContainer.style.display = 'none'; + if (!plotManager) { + console.error('Plot manager not initialized'); + showNotification('Error: Plot manager not available', 'error'); return; } - triggerContainer.style.display = 'block'; - triggerSelect.innerHTML = ''; + console.log('Opening plot form for editing session:', sessionId); + console.log('Current tab:', typeof tabManager !== 'undefined' ? tabManager.getCurrentTab() : 'tabManager not available'); - for (const variable of booleanVariables) { - const option = document.createElement('option'); - option.value = variable; - option.textContent = variable; - triggerSelect.appendChild(option); + plotManager.showPlotForm(sessionId); + + console.log('Plot form should now be visible'); +} + +// Función global para remover sesiones de plot +window.removePlotSession = async function (sessionId) { + if (confirm('¿Estás seguro de que quieres eliminar este plot?')) { + try { + const response = await fetch(`/api/plots/${sessionId}`, { + method: 'DELETE' + }); + + const result = await response.json(); + + if (result.success) { + // Remover tab dinámico + if (typeof tabManager !== 'undefined') { + tabManager.removePlotTab(sessionId); + } + + // Remover de PlotManager + if (plotManager && plotManager.sessions.has(sessionId)) { + plotManager.sessions.delete(sessionId); + } + + showNotification(result.message, 'success'); + } else { + showNotification(result.error, 'error'); + } + } catch (error) { + console.error('Error removing plot:', error); + showNotification('Error removing plot session', 'error'); + } } } -function toggleTriggerConfig() { - const triggerEnabled = document.getElementById('plot-trigger-enabled'); - const triggerConfig = document.getElementById('trigger-config'); +// Función de utilidad para notificaciones +window.showNotification = function (message, type = 'info') { + console.log(`${type.toUpperCase()}: ${message}`); - triggerConfig.style.display = triggerEnabled.checked ? 'block' : 'none'; -} - -async function createNewPlot() { - const form = document.getElementById('new-plot-form'); - const formData = new FormData(form); - - // Obtener variables seleccionadas - const selectedVariables = Array.from(document.querySelectorAll('input[name="plot_variables"]:checked')) - .map(cb => cb.value); - - if (selectedVariables.length === 0) { - showNotification('Please select at least one variable', 'error'); - return; - } - - // Obtener configuración - const config = { - name: document.getElementById('plot-name').value || `Plot ${Date.now()}`, - variables: selectedVariables, - time_window: parseInt(document.getElementById('plot-time-window').value) || 60, - y_min: document.getElementById('plot-y-min').value || null, - y_max: document.getElementById('plot-y-max').value || null, - trigger_enabled: document.getElementById('plot-trigger-enabled').checked, - trigger_variable: document.getElementById('plot-trigger-variable').value || null, - trigger_on_true: document.getElementById('plot-trigger-on-true').checked - }; - - // Convertir valores numéricos - if (config.y_min) config.y_min = parseFloat(config.y_min); - if (config.y_max) config.y_max = parseFloat(config.y_max); - - // Crear plot - const sessionId = await plotManager.createNewPlot(config); - - if (sessionId) { - closeNewPlotModal(); + // Si tienes un sistema de notificaciones, úsalo aquí + if (typeof showAlert === 'function') { + showAlert(message, type); + } else if (typeof showMessage === 'function') { + showMessage(message, type); } } @@ -475,35 +1137,14 @@ document.addEventListener('DOMContentLoaded', function () { // Inicializar Plot Manager plotManager = new PlotManager(); - // Event listeners para el modal - document.getElementById('new-plot-btn').addEventListener('click', showNewPlotModal); - document.getElementById('new-plot-form').addEventListener('submit', function (e) { - e.preventDefault(); - createNewPlot(); - }); - - // Cerrar modal con Escape + // Cerrar modales con Escape (pero prevenir cierre accidental) document.addEventListener('keydown', function (e) { - if (e.key === 'Escape' && plotModal && plotModal.style.display === 'block') { - closeNewPlotModal(); + if (e.key === 'Escape' && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey) { + // Cerrar formulario colapsable si está abierto + const formContainer = document.getElementById('plot-form-container'); + if (formContainer && formContainer.style.display !== 'none') { + plotManager.hidePlotForm(); + } } }); - - // Cerrar modal haciendo clic fuera - window.addEventListener('click', function (e) { - if (e.target === plotModal) { - closeNewPlotModal(); - } - }); -}); - -// Función de utilidad para notificaciones -function showNotification(message, type = 'info') { - // Implementar según tu sistema de notificaciones - console.log(`${type.toUpperCase()}: ${message}`); - - // Si tienes un sistema de notificaciones, úsalo aquí - if (typeof showAlert === 'function') { - showAlert(message, type); - } -} \ No newline at end of file +}); \ No newline at end of file diff --git a/static/js/tabs.js b/static/js/tabs.js index 4830892..d746059 100644 --- a/static/js/tabs.js +++ b/static/js/tabs.js @@ -6,17 +6,13 @@ class TabManager { constructor() { this.currentTab = 'datasets'; + this.plotTabs = new Set(); // Track dynamic plot tabs this.init(); } init() { - // Event listeners para los botones de tab - document.querySelectorAll('.tab-btn').forEach(btn => { - btn.addEventListener('click', (e) => { - const tabName = e.target.dataset.tab; - this.switchTab(tabName); - }); - }); + // Event listeners para los botones de tab estáticos + this.bindStaticTabs(); // Inicializar con el tab activo por defecto this.switchTab(this.currentTab); @@ -24,6 +20,16 @@ class TabManager { console.log('📑 Tab Manager initialized'); } + bindStaticTabs() { + // Solo bindear tabs estáticos, los dinámicos se bindean al crearlos + document.querySelectorAll('.tab-btn:not([data-plot-id])').forEach(btn => { + btn.addEventListener('click', (e) => { + const tabName = e.target.dataset.tab; + this.switchTab(tabName); + }); + }); + } + switchTab(tabName) { // Remover clase active de todos los tabs document.querySelectorAll('.tab-btn').forEach(btn => { @@ -50,6 +56,208 @@ class TabManager { } } + createPlotTab(sessionId, plotName) { + // Crear botón de sub-tab dinámico + const subTabBtn = document.createElement('button'); + subTabBtn.className = 'sub-tab-btn plot-sub-tab'; + subTabBtn.dataset.subTab = `plot-${sessionId}`; + subTabBtn.dataset.plotId = sessionId; + subTabBtn.innerHTML = ` + 📈 ${plotName} + × + `; + + // Crear contenido del sub-tab + const subTabContent = document.createElement('div'); + subTabContent.className = 'sub-tab-content plot-sub-tab-content'; + subTabContent.id = `plot-${sessionId}-sub-tab`; + subTabContent.innerHTML = ` +
+
+
+ 📈 ${plotName} +
+ + +
+
+
+ +
+
+

📈 ${plotName}

+
+ + + + +
+
+
+ + Loading plot information... + +
+
+ +
+
+
+ `; + + // Mostrar sub-tabs si no están visibles + const subTabs = document.getElementById('plot-sub-tabs'); + const plotSessionsContainer = document.getElementById('plot-sessions-container'); + const plotSubContent = document.getElementById('plot-sub-content'); + + if (subTabs.style.display === 'none') { + subTabs.style.display = 'flex'; + plotSessionsContainer.style.display = 'none'; + plotSubContent.style.display = 'block'; + } + + // Agregar sub-tab al contenedor de sub-tabs + subTabs.appendChild(subTabBtn); + + // Agregar contenido del sub-tab + plotSubContent.appendChild(subTabContent); + + // Bind events + subTabBtn.addEventListener('click', (e) => { + if (!e.target.classList.contains('sub-tab-close')) { + this.switchSubTab(`plot-${sessionId}`); + } + }); + + // Close button event + subTabBtn.querySelector('.sub-tab-close').addEventListener('click', (e) => { + e.stopPropagation(); + // Llamar a la función que elimina el plot del backend Y del frontend + if (typeof window.removePlotSession === 'function') { + window.removePlotSession(sessionId); + } else { + console.error('removePlotSession function not available'); + // Fallback: solo remover del frontend + this.removePlotTab(sessionId); + } + }); + + this.plotTabs.add(sessionId); + + console.log(`📑 Created plot sub-tab for session: ${sessionId}`); + return subTabBtn; + } + + switchSubTab(subTabName) { + // Remover clase active de todos los sub-tabs + document.querySelectorAll('.sub-tab-btn').forEach(btn => { + btn.classList.remove('active'); + }); + + document.querySelectorAll('.sub-tab-content').forEach(content => { + content.classList.remove('active'); + }); + + // Activar el sub-tab seleccionado + const activeBtn = document.querySelector(`[data-sub-tab="${subTabName}"]`); + const activeContent = document.getElementById(`${subTabName}-sub-tab`); + + if (activeBtn && activeContent) { + activeBtn.classList.add('active'); + activeContent.classList.add('active'); + + // Eventos específicos por sub-tab + this.handleSubTabSpecificEvents(subTabName); + + console.log(`📑 Switched to sub-tab: ${subTabName}`); + } + } + + handleSubTabSpecificEvents(subTabName) { + if (subTabName.startsWith('plot-')) { + // Sub-tab de plot individual - cargar datos específicos + const sessionId = subTabName.replace('plot-', ''); + if (typeof plotManager !== 'undefined') { + plotManager.updateSessionData(sessionId); + } + } + } + + removePlotTab(sessionId) { + // Remover sub-tab button + const subTabBtn = document.querySelector(`[data-plot-id="${sessionId}"]`); + if (subTabBtn) { + subTabBtn.remove(); + } + + // Remover sub-tab content + const subTabContent = document.getElementById(`plot-${sessionId}-sub-tab`); + if (subTabContent) { + subTabContent.remove(); + } + + this.plotTabs.delete(sessionId); + + // Si no quedan sub-tabs, mostrar vista inicial + const subTabs = document.getElementById('plot-sub-tabs'); + const plotSessionsContainer = document.getElementById('plot-sessions-container'); + const plotSubContent = document.getElementById('plot-sub-content'); + + if (subTabs.children.length === 0) { + subTabs.style.display = 'none'; + plotSessionsContainer.style.display = 'block'; + plotSubContent.style.display = 'none'; + } + + console.log(`📑 Removed plot sub-tab for session: ${sessionId}`); + } + + updatePlotTabName(sessionId, newName) { + const subTabBtn = document.querySelector(`[data-plot-id="${sessionId}"]`); + if (subTabBtn) { + subTabBtn.innerHTML = ` + 📈 ${newName} + × + `; + + // Re-bind close event + subTabBtn.querySelector('.sub-tab-close').addEventListener('click', (e) => { + e.stopPropagation(); + // Llamar a la función que elimina el plot del backend Y del frontend + if (typeof window.removePlotSession === 'function') { + window.removePlotSession(sessionId); + } else { + console.error('removePlotSession function not available'); + // Fallback: solo remover del frontend + this.removePlotTab(sessionId); + } + }); + } + + // Actualizar header del contenido + const header = document.querySelector(`#plot-${sessionId}-sub-tab h4`); + if (header) { + header.textContent = `📈 ${newName}`; + } + + const articleHeader = document.querySelector(`#plot-${sessionId}-sub-tab header span`); + if (articleHeader) { + articleHeader.textContent = `📈 ${newName}`; + } + } + handleTabSpecificEvents(tabName) { switch (tabName) { case 'plotting': diff --git a/system_state.json b/system_state.json index 16c9e08..7486606 100644 --- a/system_state.json +++ b/system_state.json @@ -7,5 +7,5 @@ ] }, "auto_recovery_enabled": true, - "last_update": "2025-07-21T09:21:45.854021" + "last_update": "2025-07-21T12:29:22.714509" } \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 63980e2..337bbeb 100644 --- a/templates/index.html +++ b/templates/index.html @@ -465,41 +465,137 @@ + - + +
-
📋 Application Events Log
-
-

📝 Event Tracking: Connection events, configuration changes, errors and system - status

-

💾 Persistent Storage: Events are saved to disk and persist between application - restarts

-
- -
- - - -
- Loading log statistics... +
+
+ 📈 Real-Time Plotting +
+
+ + + + + -
-
-
- 📡 System - Loading... -
-
Loading application events...
+ + + + +
+ +
+ + +
+
+

📈 No plot sessions created yet

+

Click "New Plot" to create your first real-time chart

+
+ + +
+
+
📋 Events & System Logs
+
+

📋 Event Logging: Monitor system events, errors, and operational status

+

🔍 Real-time Updates: Events are automatically updated as they occur

+

📊 Filtering: Filter events by type and time range

+
+ +
+ + +
+ Loading... events +
+
+ +
+

Loading events...

+
+
+
+ @@ -579,21 +675,96 @@
📈 Real-Time Plotting - +
-
-

📈 Real-Time Plotting: Create interactive charts using cached data from active - datasets

-

🎯 Trigger System: Use boolean variables to automatically restart traces when - conditions are met

-

⚡ Performance: Uses recording cache - no additional PLC load

-

📊 Multiple Sessions: Create multiple independent plot sessions with different - variables and settings

+ + + + - + + + + +
+ +
+ +

📈 No plot sessions created yet

@@ -627,64 +798,52 @@
- -