Actualización del sistema de plotting en tiempo real con nuevas funcionalidades, incluyendo la creación y edición de sesiones de plot a través de un formulario colapsable. Se implementaron nuevos endpoints API para obtener y actualizar la configuración de las sesiones de plot. Además, se mejoró la interfaz de usuario con sub-tabs dinámicos para gestionar múltiples sesiones de plot y se realizaron ajustes en los estilos CSS para una mejor experiencia visual. Se actualizaron los archivos de configuración y estado del sistema para reflejar estos cambios.
This commit is contained in:
parent
a13baed5c6
commit
5e575fd112
|
@ -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
|
||||
}
|
|
@ -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:
|
||||
|
|
107
main.py
107
main.py
|
@ -1371,6 +1371,113 @@ def get_plot_data(session_id):
|
|||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@app.route("/api/plots/<session_id>/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/<session_id>/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"""
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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 = `
|
||||
<div class="log-entry log-info">
|
||||
<div class="log-header">
|
||||
<span>📋 System</span>
|
||||
<span class="log-timestamp">${new Date().toLocaleString('es-ES')}</span>
|
||||
</div>
|
||||
<div class="log-message">No events found</div>
|
||||
</div>
|
||||
`;
|
||||
} 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 = `
|
||||
<div class="log-entry log-info">
|
||||
<div class="log-header">
|
||||
<span>🧹 System</span>
|
||||
<span class="log-timestamp">${new Date().toLocaleString('es-ES')}</span>
|
||||
</div>
|
||||
<div class="log-message">Events view cleared. Click refresh to reload events.</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
eventsCount.textContent = '0';
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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}
|
||||
<span class="sub-tab-close" data-session-id="${sessionId}">×</span>
|
||||
`;
|
||||
|
||||
// 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 = `
|
||||
<article>
|
||||
<header>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>📈 ${plotName}</span>
|
||||
<div>
|
||||
<button type="button" class="outline" onclick="window.editPlotSession('${sessionId}')">
|
||||
✏️ Edit Plot
|
||||
</button>
|
||||
<button type="button" class="secondary" onclick="window.removePlotSession('${sessionId}')">
|
||||
🗑️ Remove Plot
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="plot-session" id="plot-session-${sessionId}">
|
||||
<div class="plot-header">
|
||||
<h4>📈 ${plotName}</h4>
|
||||
<div class="plot-controls">
|
||||
<button class="btn btn-sm" onclick="plotManager.controlPlot('${sessionId}', 'start')" title="Start">
|
||||
▶️ Start
|
||||
</button>
|
||||
<button class="btn btn-sm" onclick="plotManager.controlPlot('${sessionId}', 'pause')" title="Pause">
|
||||
⏸️ Pause
|
||||
</button>
|
||||
<button class="btn btn-sm" onclick="plotManager.controlPlot('${sessionId}', 'clear')" title="Clear">
|
||||
🗑️ Clear
|
||||
</button>
|
||||
<button class="btn btn-sm" onclick="plotManager.controlPlot('${sessionId}', 'stop')" title="Stop">
|
||||
⏹️ Stop
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="plot-info">
|
||||
<span class="plot-stats" id="plot-stats-${sessionId}">
|
||||
Loading plot information...
|
||||
</span>
|
||||
</div>
|
||||
<div class="plot-canvas">
|
||||
<canvas id="chart-${sessionId}"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
|
||||
// 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}
|
||||
<span class="sub-tab-close" data-session-id="${sessionId}">×</span>
|
||||
`;
|
||||
|
||||
// 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':
|
||||
|
|
|
@ -7,5 +7,5 @@
|
|||
]
|
||||
},
|
||||
"auto_recovery_enabled": true,
|
||||
"last_update": "2025-07-21T09:21:45.854021"
|
||||
"last_update": "2025-07-21T12:29:22.714509"
|
||||
}
|
|
@ -465,41 +465,137 @@
|
|||
</form>
|
||||
</details>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<!-- Application Events Log -->
|
||||
<!-- 📈 PLOTTING TAB -->
|
||||
<div class="tab-content" id="plotting-tab">
|
||||
<article>
|
||||
<header>📋 Application Events Log</header>
|
||||
<div class="info-section">
|
||||
<p><strong>📝 Event Tracking:</strong> Connection events, configuration changes, errors and system
|
||||
status</p>
|
||||
<p><strong>💾 Persistent Storage:</strong> Events are saved to disk and persist between application
|
||||
restarts</p>
|
||||
</div>
|
||||
|
||||
<div class="log-controls">
|
||||
<button class="outline" onclick="refreshEventLog()">🔄 Refresh Log</button>
|
||||
<button class="outline" onclick="clearLogView()">🧹 Clear View</button>
|
||||
<select id="log-limit" onchange="refreshEventLog()">
|
||||
<option value="25">Last 25 events</option>
|
||||
<option value="50" selected>Last 50 events</option>
|
||||
<option value="100">Last 100 events</option>
|
||||
<option value="200">Last 200 events</option>
|
||||
</select>
|
||||
<div class="log-stats" id="log-stats">
|
||||
Loading log statistics...
|
||||
<header>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>📈 Real-Time Plotting</span>
|
||||
<button type="button" id="toggle-plot-form-btn" class="outline">➕ New Plot</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
|
||||
<!-- Plot Creation/Edit Form (Collapsible) -->
|
||||
<div id="plot-form-container" class="collapsible-section" style="display: none;">
|
||||
<article class="plot-form-article">
|
||||
<header>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span id="plot-form-title">🆕 Create New Plot</span>
|
||||
<button type="button" id="close-plot-form-btn" class="close-btn">×</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<form id="plot-form">
|
||||
<div class="form-group">
|
||||
<label for="plot-form-name">Plot Name:</label>
|
||||
<input type="text" id="plot-form-name" placeholder="Temperature Monitoring" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Variables to Plot:</label>
|
||||
<div class="variables-selection">
|
||||
<div id="selected-variables-display" class="selected-variables">
|
||||
<p class="no-variables">No variables selected</p>
|
||||
</div>
|
||||
<button type="button" id="select-variables-btn" class="outline">
|
||||
🎨 Select Variables & Colors
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="plot-form-time-window">Time Window (seconds):</label>
|
||||
<input type="number" id="plot-form-time-window" value="60" min="10" max="3600" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Y-Axis Range (optional):</label>
|
||||
<div class="range-inputs">
|
||||
<input type="number" id="plot-form-y-min" placeholder="Auto Min" step="any">
|
||||
<span>to</span>
|
||||
<input type="number" id="plot-form-y-max" placeholder="Auto Max" step="any">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" id="plot-form-trigger-enabled"
|
||||
onchange="togglePlotFormTriggerConfig()">
|
||||
Enable Trigger System
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="plot-form-trigger-config" style="display: none;">
|
||||
<div class="form-group">
|
||||
<label for="plot-form-trigger-variable">Trigger Variable:</label>
|
||||
<select id="plot-form-trigger-variable">
|
||||
<option value="">No trigger</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" id="plot-form-trigger-on-true" checked>
|
||||
Trigger on True (uncheck for False)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary" id="plot-form-submit">Create Plot</button>
|
||||
<button type="button" class="btn btn-secondary" id="cancel-plot-form">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="log-container" id="events-log">
|
||||
<div class="log-entry log-info">
|
||||
<div class="log-header">
|
||||
<span>📡 System</span>
|
||||
<span class="log-timestamp">Loading...</span>
|
||||
</div>
|
||||
<div class="log-message">Loading application events...</div>
|
||||
<!-- Sub-tabs para plots -->
|
||||
<nav class="sub-tabs" id="plot-sub-tabs" style="display: none;">
|
||||
<!-- Los sub-tabs se crearán dinámicamente aquí -->
|
||||
</nav>
|
||||
|
||||
<!-- Contenido de sub-tabs -->
|
||||
<div id="plot-sub-content">
|
||||
<!-- Contenido de plots se mostrará aquí -->
|
||||
</div>
|
||||
|
||||
<!-- Plot Sessions Container (vista inicial) -->
|
||||
<div id="plot-sessions-container">
|
||||
<div style="text-align: center; padding: 2rem; color: var(--pico-muted-color);">
|
||||
<p>📈 No plot sessions created yet</p>
|
||||
<p>Click "New Plot" to create your first real-time chart</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<!-- 📋 EVENTS TAB -->
|
||||
<div class="tab-content" id="events-tab">
|
||||
<article>
|
||||
<header>📋 Events & System Logs</header>
|
||||
<div class="info-section">
|
||||
<p><strong>📋 Event Logging:</strong> Monitor system events, errors, and operational status</p>
|
||||
<p><strong>🔍 Real-time Updates:</strong> Events are automatically updated as they occur</p>
|
||||
<p><strong>📊 Filtering:</strong> Filter events by type and time range</p>
|
||||
</div>
|
||||
|
||||
<div class="log-controls">
|
||||
<button id="refresh-events-btn">🔄 Refresh Events</button>
|
||||
<button id="clear-events-btn" class="secondary">🗑️ Clear Events</button>
|
||||
<div class="log-stats">
|
||||
<span id="events-count">Loading...</span> events
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="log-container" id="events-container">
|
||||
<p>Loading events...</p>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- Edit Variable Modal -->
|
||||
|
@ -579,21 +675,96 @@
|
|||
<header>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>📈 Real-Time Plotting</span>
|
||||
<button type="button" id="new-plot-btn" class="outline">➕ New Plot</button>
|
||||
<button type="button" id="toggle-plot-form-btn" class="outline">➕ New Plot</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="info-section">
|
||||
<p><strong>📈 Real-Time Plotting:</strong> Create interactive charts using cached data from active
|
||||
datasets</p>
|
||||
<p><strong>🎯 Trigger System:</strong> Use boolean variables to automatically restart traces when
|
||||
conditions are met</p>
|
||||
<p><strong>⚡ Performance:</strong> Uses recording cache - no additional PLC load</p>
|
||||
<p><strong>📊 Multiple Sessions:</strong> Create multiple independent plot sessions with different
|
||||
variables and settings</p>
|
||||
|
||||
|
||||
<!-- Plot Creation/Edit Form (Collapsible) -->
|
||||
<div id="plot-form-container" class="collapsible-section" style="display: none;">
|
||||
<article class="plot-form-article">
|
||||
<header>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span id="plot-form-title">🆕 Create New Plot</span>
|
||||
<button type="button" id="close-plot-form-btn" class="close-btn">×</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<form id="plot-form">
|
||||
<div class="form-group">
|
||||
<label for="plot-form-name">Plot Name:</label>
|
||||
<input type="text" id="plot-form-name" placeholder="Temperature Monitoring" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Variables to Plot:</label>
|
||||
<div class="variables-selection">
|
||||
<div id="selected-variables-display" class="selected-variables">
|
||||
<p class="no-variables">No variables selected</p>
|
||||
</div>
|
||||
<button type="button" id="select-variables-btn" class="outline">
|
||||
🎨 Select Variables & Colors
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="plot-form-time-window">Time Window (seconds):</label>
|
||||
<input type="number" id="plot-form-time-window" value="60" min="10" max="3600" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Y-Axis Range (optional):</label>
|
||||
<div class="range-inputs">
|
||||
<input type="number" id="plot-form-y-min" placeholder="Auto Min" step="any">
|
||||
<span>to</span>
|
||||
<input type="number" id="plot-form-y-max" placeholder="Auto Max" step="any">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" id="plot-form-trigger-enabled"
|
||||
onchange="togglePlotFormTriggerConfig()">
|
||||
Enable Trigger System
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="plot-form-trigger-config" style="display: none;">
|
||||
<div class="form-group">
|
||||
<label for="plot-form-trigger-variable">Trigger Variable:</label>
|
||||
<select id="plot-form-trigger-variable">
|
||||
<option value="">No trigger</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" id="plot-form-trigger-on-true" checked>
|
||||
Trigger on True (uncheck for False)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary" id="plot-form-submit">Create Plot</button>
|
||||
<button type="button" class="btn btn-secondary" id="cancel-plot-form">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<!-- Plot Sessions Container -->
|
||||
<!-- Sub-tabs para plots -->
|
||||
<nav class="sub-tabs" id="plot-sub-tabs" style="display: none;">
|
||||
<!-- Los sub-tabs se crearán dinámicamente aquí -->
|
||||
</nav>
|
||||
|
||||
<!-- Contenido de sub-tabs -->
|
||||
<div id="plot-sub-content">
|
||||
<!-- Contenido de plots se mostrará aquí -->
|
||||
</div>
|
||||
|
||||
<!-- Plot Sessions Container (vista inicial) -->
|
||||
<div id="plot-sessions-container">
|
||||
<div style="text-align: center; padding: 2rem; color: var(--pico-muted-color);">
|
||||
<p>📈 No plot sessions created yet</p>
|
||||
|
@ -627,64 +798,52 @@
|
|||
</article>
|
||||
</div>
|
||||
|
||||
<!-- New Plot Modal -->
|
||||
<div id="new-plot-modal" class="modal" style="display: none;">
|
||||
<div class="modal-content">
|
||||
<h4>Create New Plot Session</h4>
|
||||
<form id="new-plot-form">
|
||||
<div class="form-group">
|
||||
<label for="plot-name">Plot Name:</label>
|
||||
<input type="text" id="plot-name" placeholder="Temperature Monitoring" required>
|
||||
</div>
|
||||
<!-- Variable Selection Modal (NEW) -->
|
||||
<div id="variable-selection-modal" class="modal" style="display: none;">
|
||||
<div class="modal-content variable-modal">
|
||||
<header class="modal-header">
|
||||
<h3>🎨 Select Variables & Colors</h3>
|
||||
<button type="button" class="close-btn" id="close-variable-modal">×</button>
|
||||
</header>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Variables to Plot:</label>
|
||||
<div id="plot-variables-selector">
|
||||
<p>Loading available variables...</p>
|
||||
<div class="modal-body">
|
||||
<div class="variable-selection-container">
|
||||
<div class="datasets-sidebar">
|
||||
<h4>📊 Datasets</h4>
|
||||
<div id="datasets-list" class="datasets-list">
|
||||
<p>Loading datasets...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="variables-main">
|
||||
<div class="variables-header">
|
||||
<h4>🔧 Variables</h4>
|
||||
<div class="selection-controls">
|
||||
<button type="button" id="select-all-variables" class="btn btn-sm outline">Select
|
||||
All</button>
|
||||
<button type="button" id="deselect-all-variables" class="btn btn-sm outline">Deselect
|
||||
All</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="variables-list" class="variables-list">
|
||||
<p class="no-dataset-message">Select a dataset to see its variables</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="plot-time-window">Time Window (seconds):</label>
|
||||
<input type="number" id="plot-time-window" value="60" min="10" max="3600" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Y-Axis Range (optional):</label>
|
||||
<div class="range-inputs">
|
||||
<input type="number" id="plot-y-min" placeholder="Auto Min" step="any">
|
||||
<span>to</span>
|
||||
<input type="number" id="plot-y-max" placeholder="Auto Max" step="any">
|
||||
<div class="selected-summary">
|
||||
<h4>📝 Selected Variables Summary</h4>
|
||||
<div id="selected-variables-summary" class="selected-summary-list">
|
||||
<p>No variables selected</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" id="plot-trigger-enabled" onchange="toggleTriggerConfig()">
|
||||
Enable Trigger System
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="trigger-config" style="display: none;">
|
||||
<div class="form-group">
|
||||
<label for="plot-trigger-variable">Trigger Variable:</label>
|
||||
<select id="plot-trigger-variable">
|
||||
<option value="">No trigger</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" id="plot-trigger-on-true" checked>
|
||||
Trigger on True (uncheck for False)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Create Plot</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="closeNewPlotModal()">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" id="cancel-variable-selection">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="confirm-variable-selection">Confirm Selection</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
File diff suppressed because one or more lines are too long
Binary file not shown.
After Width: | Height: | Size: 81 KiB |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,170 @@
|
|||
/**
|
||||
* Gestión de la configuración CSV y operaciones relacionadas
|
||||
*/
|
||||
|
||||
// Cargar configuración CSV
|
||||
function loadCsvConfig() {
|
||||
fetch('/api/csv/config')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const config = data.config;
|
||||
|
||||
// Actualizar elementos de visualización
|
||||
document.getElementById('csv-directory-path').textContent = config.current_directory || 'N/A';
|
||||
document.getElementById('csv-rotation-enabled').textContent = config.rotation_enabled ? '✅ Yes' : '❌ No';
|
||||
document.getElementById('csv-max-size').textContent = config.max_size_mb ? `${config.max_size_mb} MB` : 'No limit';
|
||||
document.getElementById('csv-max-days').textContent = config.max_days ? `${config.max_days} days` : 'No limit';
|
||||
document.getElementById('csv-max-hours').textContent = config.max_hours ? `${config.max_hours} hours` : 'No limit';
|
||||
document.getElementById('csv-cleanup-interval').textContent = `${config.cleanup_interval_hours} hours`;
|
||||
|
||||
// Actualizar campos del formulario
|
||||
document.getElementById('records-directory').value = config.records_directory || '';
|
||||
document.getElementById('rotation-enabled').checked = config.rotation_enabled || false;
|
||||
document.getElementById('max-size-mb').value = config.max_size_mb || '';
|
||||
document.getElementById('max-days').value = config.max_days || '';
|
||||
document.getElementById('max-hours').value = config.max_hours || '';
|
||||
document.getElementById('cleanup-interval').value = config.cleanup_interval_hours || 24;
|
||||
|
||||
// Cargar información del directorio
|
||||
loadCsvDirectoryInfo();
|
||||
} else {
|
||||
showMessage('Error loading CSV configuration: ' + data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showMessage('Error loading CSV configuration', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Cargar información del directorio CSV
|
||||
function loadCsvDirectoryInfo() {
|
||||
fetch('/api/csv/directory/info')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const info = data.info;
|
||||
const statsDiv = document.getElementById('directory-stats');
|
||||
|
||||
let html = `
|
||||
<div class="stat-item">
|
||||
<strong>📁 Directory:</strong>
|
||||
<span>${info.base_directory}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<strong>📊 Total Files:</strong>
|
||||
<span>${info.total_files}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<strong>💾 Total Size:</strong>
|
||||
<span>${info.total_size_mb} MB</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (info.oldest_file) {
|
||||
html += `
|
||||
<div class="stat-item">
|
||||
<strong>📅 Oldest File:</strong>
|
||||
<span>${new Date(info.oldest_file).toLocaleString()}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (info.newest_file) {
|
||||
html += `
|
||||
<div class="stat-item">
|
||||
<strong>🆕 Newest File:</strong>
|
||||
<span>${new Date(info.newest_file).toLocaleString()}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (info.day_folders && info.day_folders.length > 0) {
|
||||
html += '<h4>📂 Day Folders:</h4>';
|
||||
info.day_folders.forEach(folder => {
|
||||
html += `
|
||||
<div class="day-folder-item">
|
||||
<span><strong>${folder.name}</strong></span>
|
||||
<span>${folder.files} files, ${folder.size_mb} MB</span>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
statsDiv.innerHTML = html;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
document.getElementById('directory-stats').innerHTML = '<p>Error loading directory information</p>';
|
||||
});
|
||||
}
|
||||
|
||||
// Ejecutar limpieza manual
|
||||
function triggerManualCleanup() {
|
||||
if (!confirm('¿Estás seguro de que quieres ejecutar la limpieza manual? Esto eliminará archivos antiguos según la configuración actual.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('/api/csv/cleanup', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showMessage('Limpieza ejecutada correctamente', 'success');
|
||||
loadCsvDirectoryInfo(); // Recargar información del directorio
|
||||
} else {
|
||||
showMessage('Error en la limpieza: ' + data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showMessage('Error ejecutando la limpieza', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Inicializar listeners para la configuración CSV
|
||||
function initCsvListeners() {
|
||||
// Manejar envío del formulario de configuración CSV
|
||||
document.getElementById('csv-config-form').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(e.target);
|
||||
const configData = {};
|
||||
|
||||
// Convertir datos del formulario a objeto, manejando valores vacíos
|
||||
for (let [key, value] of formData.entries()) {
|
||||
if (key === 'rotation_enabled') {
|
||||
configData[key] = document.getElementById('rotation-enabled').checked;
|
||||
} else if (value.trim() === '') {
|
||||
configData[key] = null;
|
||||
} else if (key.includes('max_') || key.includes('cleanup_interval')) {
|
||||
configData[key] = parseFloat(value) || null;
|
||||
} else {
|
||||
configData[key] = value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
fetch('/api/csv/config', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(configData)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showMessage('Configuración CSV actualizada correctamente', 'success');
|
||||
loadCsvConfig(); // Recargar para mostrar valores actualizados
|
||||
} else {
|
||||
showMessage('Error actualizando configuración CSV: ' + data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showMessage('Error actualizando configuración CSV', 'error');
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,519 @@
|
|||
/**
|
||||
* Gestión de datasets y variables asociadas
|
||||
*/
|
||||
|
||||
// Variables de gestión de datasets
|
||||
let currentDatasets = {};
|
||||
let currentDatasetId = null;
|
||||
|
||||
// Cargar todos los datasets desde API
|
||||
window.loadDatasets = function () {
|
||||
fetch('/api/datasets')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
currentDatasets = data.datasets;
|
||||
currentDatasetId = data.current_dataset_id;
|
||||
updateDatasetSelector();
|
||||
updateDatasetInfo();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading datasets:', error);
|
||||
showMessage('Error loading datasets', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Actualizar el selector de datasets
|
||||
function updateDatasetSelector() {
|
||||
const selector = document.getElementById('dataset-selector');
|
||||
selector.innerHTML = '<option value="">Select a dataset...</option>';
|
||||
|
||||
Object.keys(currentDatasets).forEach(datasetId => {
|
||||
const dataset = currentDatasets[datasetId];
|
||||
const option = document.createElement('option');
|
||||
option.value = datasetId;
|
||||
option.textContent = `${dataset.name} (${dataset.prefix})`;
|
||||
if (datasetId === currentDatasetId) {
|
||||
option.selected = true;
|
||||
}
|
||||
selector.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
// Actualizar información del dataset
|
||||
function updateDatasetInfo() {
|
||||
const statusBar = document.getElementById('dataset-status-bar');
|
||||
const variablesManagement = document.getElementById('variables-management');
|
||||
const noDatasetMessage = document.getElementById('no-dataset-message');
|
||||
|
||||
if (currentDatasetId && currentDatasets[currentDatasetId]) {
|
||||
const dataset = currentDatasets[currentDatasetId];
|
||||
|
||||
// Mostrar info del dataset en la barra de estado
|
||||
document.getElementById('dataset-name').textContent = dataset.name;
|
||||
document.getElementById('dataset-prefix').textContent = dataset.prefix;
|
||||
document.getElementById('dataset-sampling').textContent =
|
||||
dataset.sampling_interval ? `${dataset.sampling_interval}s` : 'Global interval';
|
||||
document.getElementById('dataset-var-count').textContent = Object.keys(dataset.variables).length;
|
||||
document.getElementById('dataset-stream-count').textContent = dataset.streaming_variables.length;
|
||||
|
||||
// Actualizar estado del dataset
|
||||
const statusSpan = document.getElementById('dataset-status');
|
||||
const isActive = dataset.enabled;
|
||||
statusSpan.textContent = isActive ? '🟢 Active' : '⭕ Inactive';
|
||||
statusSpan.className = `status-item ${isActive ? 'status-active' : 'status-inactive'}`;
|
||||
|
||||
// Actualizar botones de acción
|
||||
document.getElementById('activate-dataset-btn').style.display = isActive ? 'none' : 'inline-block';
|
||||
document.getElementById('deactivate-dataset-btn').style.display = isActive ? 'inline-block' : 'none';
|
||||
|
||||
// Mostrar secciones
|
||||
statusBar.style.display = 'block';
|
||||
variablesManagement.style.display = 'block';
|
||||
noDatasetMessage.style.display = 'none';
|
||||
|
||||
// Cargar variables para este dataset
|
||||
loadDatasetVariables(currentDatasetId);
|
||||
} else {
|
||||
statusBar.style.display = 'none';
|
||||
variablesManagement.style.display = 'none';
|
||||
noDatasetMessage.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// Cargar variables para un dataset específico
|
||||
function loadDatasetVariables(datasetId) {
|
||||
if (!datasetId || !currentDatasets[datasetId]) {
|
||||
// Limpiar la tabla si no hay dataset válido
|
||||
document.getElementById('variables-tbody').innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const dataset = currentDatasets[datasetId];
|
||||
const variables = dataset.variables || {};
|
||||
const streamingVars = dataset.streaming_variables || [];
|
||||
const tbody = document.getElementById('variables-tbody');
|
||||
|
||||
// Limpiar filas existentes
|
||||
tbody.innerHTML = '';
|
||||
|
||||
// Añadir una fila para cada variable
|
||||
Object.keys(variables).forEach(varName => {
|
||||
const variable = variables[varName];
|
||||
const row = document.createElement('tr');
|
||||
|
||||
// Formatear visualización del área de memoria
|
||||
let memoryAreaDisplay = '';
|
||||
if (variable.area === 'db') {
|
||||
memoryAreaDisplay = `DB${variable.db || 'N/A'}.${variable.offset}`;
|
||||
} else if (variable.area === 'mw' || variable.area === 'm') {
|
||||
memoryAreaDisplay = `MW${variable.offset}`;
|
||||
} else if (variable.area === 'pew' || variable.area === 'pe') {
|
||||
memoryAreaDisplay = `PEW${variable.offset}`;
|
||||
} else if (variable.area === 'paw' || variable.area === 'pa') {
|
||||
memoryAreaDisplay = `PAW${variable.offset}`;
|
||||
} else if (variable.area === 'e') {
|
||||
memoryAreaDisplay = `E${variable.offset}.${variable.bit}`;
|
||||
} else if (variable.area === 'a') {
|
||||
memoryAreaDisplay = `A${variable.offset}.${variable.bit}`;
|
||||
} else if (variable.area === 'mb') {
|
||||
memoryAreaDisplay = `M${variable.offset}.${variable.bit}`;
|
||||
} else {
|
||||
memoryAreaDisplay = `${variable.area.toUpperCase()}${variable.offset}`;
|
||||
}
|
||||
|
||||
// Comprobar si la variable está en la lista de streaming
|
||||
const isStreaming = streamingVars.includes(varName);
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${varName}</td>
|
||||
<td>${memoryAreaDisplay}</td>
|
||||
<td>${variable.offset}</td>
|
||||
<td>${variable.type.toUpperCase()}</td>
|
||||
<td id="value-${varName}" style="font-family: monospace; font-weight: bold; color: var(--pico-muted-color);">
|
||||
--
|
||||
</td>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" id="stream-${varName}" role="switch" ${isStreaming ? 'checked' : ''}
|
||||
onchange="toggleStreaming('${varName}', this.checked)">
|
||||
Enable
|
||||
</label>
|
||||
</td>
|
||||
<td>
|
||||
<button class="outline" onclick="editVariable('${varName}')">✏️ Edit</button>
|
||||
<button class="secondary" onclick="removeVariable('${varName}')">🗑️ Remove</button>
|
||||
</td>
|
||||
`;
|
||||
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
// Inicializar listeners de eventos para datasets
|
||||
function initDatasetListeners() {
|
||||
// Cambio de selector de dataset
|
||||
document.getElementById('dataset-selector').addEventListener('change', function () {
|
||||
const selectedDatasetId = this.value;
|
||||
if (selectedDatasetId) {
|
||||
// Detener streaming de variables actual si está activo
|
||||
if (isStreamingVariables) {
|
||||
stopVariableStreaming();
|
||||
}
|
||||
|
||||
// Establecer como dataset actual
|
||||
fetch('/api/datasets/current', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ dataset_id: selectedDatasetId })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
currentDatasetId = selectedDatasetId;
|
||||
// Recargar datasets para obtener datos frescos, luego actualizar info
|
||||
loadDatasets();
|
||||
|
||||
// Actualizar texto del botón de streaming
|
||||
const toggleBtn = document.getElementById('toggle-streaming-btn');
|
||||
if (toggleBtn) {
|
||||
toggleBtn.innerHTML = '▶️ Start Live Streaming';
|
||||
}
|
||||
|
||||
// Auto-refrescar valores para el nuevo dataset
|
||||
autoStartLiveDisplay();
|
||||
} else {
|
||||
showMessage(data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showMessage('Error setting current dataset', 'error');
|
||||
});
|
||||
} else {
|
||||
// Detener streaming de variables si está activo
|
||||
if (isStreamingVariables) {
|
||||
stopVariableStreaming();
|
||||
}
|
||||
|
||||
currentDatasetId = null;
|
||||
updateDatasetInfo();
|
||||
// Limpiar valores cuando no hay dataset seleccionado
|
||||
clearVariableValues();
|
||||
}
|
||||
});
|
||||
|
||||
// Botón de nuevo dataset
|
||||
document.getElementById('new-dataset-btn').addEventListener('click', function () {
|
||||
document.getElementById('dataset-modal').style.display = 'block';
|
||||
});
|
||||
|
||||
// Cerrar modal de dataset
|
||||
document.getElementById('close-dataset-modal').addEventListener('click', function () {
|
||||
document.getElementById('dataset-modal').style.display = 'none';
|
||||
});
|
||||
|
||||
document.getElementById('cancel-dataset-btn').addEventListener('click', function () {
|
||||
document.getElementById('dataset-modal').style.display = 'none';
|
||||
});
|
||||
|
||||
// Crear nuevo dataset
|
||||
document.getElementById('dataset-form').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const data = {
|
||||
dataset_id: document.getElementById('dataset-id').value.trim(),
|
||||
name: document.getElementById('dataset-name-input').value.trim(),
|
||||
prefix: document.getElementById('dataset-prefix-input').value.trim(),
|
||||
sampling_interval: document.getElementById('dataset-sampling-input').value || null
|
||||
};
|
||||
|
||||
if (data.sampling_interval) {
|
||||
data.sampling_interval = parseFloat(data.sampling_interval);
|
||||
}
|
||||
|
||||
fetch('/api/datasets', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showMessage(data.message, 'success');
|
||||
document.getElementById('dataset-modal').style.display = 'none';
|
||||
document.getElementById('dataset-form').reset();
|
||||
loadDatasets();
|
||||
} else {
|
||||
showMessage(data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showMessage('Error creating dataset', 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// Botón de eliminar dataset
|
||||
document.getElementById('delete-dataset-btn').addEventListener('click', function () {
|
||||
if (!currentDatasetId) {
|
||||
showMessage('No dataset selected', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const dataset = currentDatasets[currentDatasetId];
|
||||
if (confirm(`Are you sure you want to delete dataset "${dataset.name}"?`)) {
|
||||
fetch(`/api/datasets/${currentDatasetId}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showMessage(data.message, 'success');
|
||||
loadDatasets();
|
||||
} else {
|
||||
showMessage(data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showMessage('Error deleting dataset', 'error');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Botón de activar dataset
|
||||
document.getElementById('activate-dataset-btn').addEventListener('click', function () {
|
||||
if (!currentDatasetId) return;
|
||||
|
||||
fetch(`/api/datasets/${currentDatasetId}/activate`, {
|
||||
method: 'POST'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showMessage(data.message, 'success');
|
||||
loadDatasets();
|
||||
} else {
|
||||
showMessage(data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showMessage('Error activating dataset', 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// Botón de desactivar dataset
|
||||
document.getElementById('deactivate-dataset-btn').addEventListener('click', function () {
|
||||
if (!currentDatasetId) return;
|
||||
|
||||
fetch(`/api/datasets/${currentDatasetId}/deactivate`, {
|
||||
method: 'POST'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showMessage(data.message, 'success');
|
||||
loadDatasets();
|
||||
} else {
|
||||
showMessage(data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showMessage('Error deactivating dataset', 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// Formulario de variables
|
||||
document.getElementById('variable-form').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!currentDatasetId) {
|
||||
showMessage('No dataset selected. Please select a dataset first.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const area = document.getElementById('var-area').value;
|
||||
const data = {
|
||||
name: document.getElementById('var-name').value,
|
||||
area: area,
|
||||
db: area === 'db' ? parseInt(document.getElementById('var-db').value) : 1,
|
||||
offset: parseInt(document.getElementById('var-offset').value),
|
||||
type: document.getElementById('var-type').value,
|
||||
streaming: false // Default to not streaming
|
||||
};
|
||||
|
||||
// Añadir parámetro bit para áreas de bit
|
||||
if (area === 'e' || area === 'a' || area === 'mb') {
|
||||
data.bit = parseInt(document.getElementById('var-bit').value);
|
||||
}
|
||||
|
||||
fetch(`/api/datasets/${currentDatasetId}/variables`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
if (data.success) {
|
||||
document.getElementById('variable-form').reset();
|
||||
loadDatasets(); // Recargar para actualizar conteos
|
||||
updateStatus();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Eliminar variable del dataset actual
|
||||
function removeVariable(name) {
|
||||
if (!currentDatasetId) {
|
||||
showMessage('No dataset selected', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (confirm(`Are you sure you want to remove the variable "${name}" from this dataset?`)) {
|
||||
fetch(`/api/datasets/${currentDatasetId}/variables/${name}`, { method: 'DELETE' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
if (data.success) {
|
||||
loadDatasets(); // Recargar para actualizar conteos
|
||||
updateStatus();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Variables para edición de variables
|
||||
let currentEditingVariable = null;
|
||||
|
||||
// Editar variable
|
||||
function editVariable(name) {
|
||||
if (!currentDatasetId) {
|
||||
showMessage('No dataset selected', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
currentEditingVariable = name;
|
||||
|
||||
// Obtener datos de la variable del dataset actual
|
||||
const dataset = currentDatasets[currentDatasetId];
|
||||
if (dataset && dataset.variables && dataset.variables[name]) {
|
||||
const variable = dataset.variables[name];
|
||||
const streamingVars = dataset.streaming_variables || [];
|
||||
|
||||
// Crear objeto de variable con la misma estructura que la API
|
||||
const variableData = {
|
||||
name: name,
|
||||
area: variable.area,
|
||||
db: variable.db,
|
||||
offset: variable.offset,
|
||||
type: variable.type,
|
||||
bit: variable.bit,
|
||||
streaming: streamingVars.includes(name)
|
||||
};
|
||||
|
||||
populateEditForm(variableData);
|
||||
document.getElementById('edit-modal').style.display = 'block';
|
||||
} else {
|
||||
showMessage('Variable not found in current dataset', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Rellenar formulario de edición
|
||||
function populateEditForm(variable) {
|
||||
document.getElementById('edit-var-name').value = variable.name;
|
||||
document.getElementById('edit-var-area').value = variable.area;
|
||||
document.getElementById('edit-var-offset').value = variable.offset;
|
||||
document.getElementById('edit-var-type').value = variable.type;
|
||||
|
||||
if (variable.db) {
|
||||
document.getElementById('edit-var-db').value = variable.db;
|
||||
}
|
||||
|
||||
if (variable.bit !== undefined) {
|
||||
document.getElementById('edit-var-bit').value = variable.bit;
|
||||
}
|
||||
|
||||
// Actualizar visibilidad de campos según el área
|
||||
toggleEditFields();
|
||||
}
|
||||
|
||||
// Cerrar modal de edición
|
||||
function closeEditModal() {
|
||||
document.getElementById('edit-modal').style.display = 'none';
|
||||
currentEditingVariable = null;
|
||||
}
|
||||
|
||||
// Inicializar listeners para edición de variables
|
||||
function initVariableEditListeners() {
|
||||
// Manejar envío del formulario de edición
|
||||
document.getElementById('edit-variable-form').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!currentEditingVariable || !currentDatasetId) {
|
||||
showMessage('No variable or dataset selected for editing', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const area = document.getElementById('edit-var-area').value;
|
||||
const newName = document.getElementById('edit-var-name').value;
|
||||
|
||||
// Primero eliminar la variable antigua
|
||||
fetch(`/api/datasets/${currentDatasetId}/variables/${currentEditingVariable}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(deleteResult => {
|
||||
if (deleteResult.success) {
|
||||
// Luego añadir la variable actualizada
|
||||
const data = {
|
||||
name: newName,
|
||||
area: area,
|
||||
db: area === 'db' ? parseInt(document.getElementById('edit-var-db').value) : 1,
|
||||
offset: parseInt(document.getElementById('edit-var-offset').value),
|
||||
type: document.getElementById('edit-var-type').value,
|
||||
streaming: false // Se restaurará abajo si estaba habilitado
|
||||
};
|
||||
|
||||
// Añadir parámetro bit para áreas de bit
|
||||
if (area === 'e' || area === 'a' || area === 'mb') {
|
||||
data.bit = parseInt(document.getElementById('edit-var-bit').value);
|
||||
}
|
||||
|
||||
return fetch(`/api/datasets/${currentDatasetId}/variables`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
} else {
|
||||
throw new Error(deleteResult.message);
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showMessage('Variable updated successfully', 'success');
|
||||
closeEditModal();
|
||||
loadDatasets();
|
||||
updateStatus();
|
||||
} else {
|
||||
showMessage(data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showMessage(`Error updating variable: ${error}`, 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// Cerrar modal al hacer clic fuera de él
|
||||
window.onclick = function (event) {
|
||||
const editModal = document.getElementById('edit-modal');
|
||||
const datasetModal = document.getElementById('dataset-modal');
|
||||
if (event.target === editModal) {
|
||||
closeEditModal();
|
||||
}
|
||||
if (event.target === datasetModal) {
|
||||
datasetModal.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,168 @@
|
|||
/**
|
||||
* Gestión de eventos de la aplicación y log de eventos
|
||||
*/
|
||||
|
||||
// Refrescar log de eventos
|
||||
function refreshEventLog() {
|
||||
const limit = document.getElementById('log-limit').value;
|
||||
|
||||
fetch(`/api/events?limit=${limit}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const logContainer = document.getElementById('events-log');
|
||||
const logStats = document.getElementById('log-stats');
|
||||
|
||||
// Limpiar entradas existentes
|
||||
logContainer.innerHTML = '';
|
||||
|
||||
// Actualizar estadísticas
|
||||
logStats.textContent = `Showing ${data.showing} of ${data.total_events} events`;
|
||||
|
||||
// Añadir eventos (orden inverso para mostrar primero los más nuevos)
|
||||
const events = data.events.reverse();
|
||||
|
||||
if (events.length === 0) {
|
||||
logContainer.innerHTML = `
|
||||
<div class="log-entry log-info">
|
||||
<div class="log-header">
|
||||
<span>📋 System</span>
|
||||
<span class="log-timestamp">${new Date().toLocaleString('es-ES')}</span>
|
||||
</div>
|
||||
<div class="log-message">No events found</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
events.forEach(event => {
|
||||
logContainer.appendChild(createLogEntry(event));
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-scroll al inicio para mostrar eventos más nuevos
|
||||
logContainer.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');
|
||||
});
|
||||
}
|
||||
|
||||
// Crear entrada de log
|
||||
function createLogEntry(event) {
|
||||
const logEntry = document.createElement('div');
|
||||
logEntry.className = `log-entry log-${event.level}`;
|
||||
|
||||
const hasDetails = event.details && Object.keys(event.details).length > 0;
|
||||
|
||||
logEntry.innerHTML = `
|
||||
<div class="log-header">
|
||||
<span>${getEventIcon(event.event_type)} ${event.event_type.replace(/_/g, ' ').toUpperCase()}</span>
|
||||
<span class="log-timestamp">${formatTimestamp(event.timestamp)}</span>
|
||||
</div>
|
||||
<div class="log-message">${event.message}</div>
|
||||
${hasDetails ? `<div class="log-details">${JSON.stringify(event.details, null, 2)}</div>` : ''}
|
||||
`;
|
||||
|
||||
return logEntry;
|
||||
}
|
||||
|
||||
// Limpiar vista de log
|
||||
function clearLogView() {
|
||||
const logContainer = document.getElementById('events-log');
|
||||
logContainer.innerHTML = `
|
||||
<div class="log-entry log-info">
|
||||
<div class="log-header">
|
||||
<span>🧹 System</span>
|
||||
<span class="log-timestamp">${new Date().toLocaleString('es-ES')}</span>
|
||||
</div>
|
||||
<div class="log-message">Log view cleared. Click refresh to reload events.</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const logStats = document.getElementById('log-stats');
|
||||
logStats.textContent = 'Log view cleared';
|
||||
}
|
||||
|
||||
// Inicializar listeners para eventos
|
||||
function initEventListeners() {
|
||||
// Botones de control de log para el tab de events
|
||||
const refreshBtn = document.getElementById('refresh-events-btn');
|
||||
const clearBtn = document.getElementById('clear-events-btn');
|
||||
|
||||
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 = `
|
||||
<div class="log-entry log-info">
|
||||
<div class="log-header">
|
||||
<span>📋 System</span>
|
||||
<span class="log-timestamp">${new Date().toLocaleString('es-ES')}</span>
|
||||
</div>
|
||||
<div class="log-message">No events found</div>
|
||||
</div>
|
||||
`;
|
||||
} 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 = `
|
||||
<div class="log-entry log-info">
|
||||
<div class="log-header">
|
||||
<span>🧹 System</span>
|
||||
<span class="log-timestamp">${new Date().toLocaleString('es-ES')}</span>
|
||||
</div>
|
||||
<div class="log-message">Events view cleared. Click refresh to reload events.</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
eventsCount.textContent = '0';
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* Archivo principal que inicializa todos los componentes
|
||||
*/
|
||||
|
||||
// Inicializar la aplicación al cargar el documento
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Inicializar tema
|
||||
loadTheme();
|
||||
|
||||
// Iniciar streaming de estado automáticamente
|
||||
startStatusStreaming();
|
||||
|
||||
// Cargar datos iniciales
|
||||
loadDatasets();
|
||||
updateStatus();
|
||||
loadCsvConfig();
|
||||
refreshEventLog();
|
||||
|
||||
// Inicializar listeners de eventos
|
||||
initPlcListeners();
|
||||
initDatasetListeners();
|
||||
initVariableEditListeners();
|
||||
initStreamingListeners();
|
||||
initCsvListeners();
|
||||
initEventListeners();
|
||||
|
||||
// Configurar actualizaciones periódicas como respaldo
|
||||
setInterval(updateStatus, 30000); // Cada 30 segundos como respaldo
|
||||
setInterval(refreshEventLog, 10000); // Cada 10 segundos
|
||||
|
||||
// Inicializar visibilidad de campos en formularios
|
||||
toggleFields();
|
||||
});
|
||||
|
||||
// Limpiar conexiones SSE cuando se descarga la página
|
||||
window.addEventListener('beforeunload', function () {
|
||||
if (variableEventSource) {
|
||||
variableEventSource.close();
|
||||
}
|
||||
if (statusEventSource) {
|
||||
statusEventSource.close();
|
||||
}
|
||||
});
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,79 @@
|
|||
/**
|
||||
* Gestión de la conexión con el PLC y configuración relacionada
|
||||
*/
|
||||
|
||||
// Inicializar listeners de eventos para PLC
|
||||
function initPlcListeners() {
|
||||
// Configuración del PLC
|
||||
document.getElementById('plc-config-form').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
const data = {
|
||||
ip: document.getElementById('plc-ip').value,
|
||||
rack: parseInt(document.getElementById('plc-rack').value),
|
||||
slot: parseInt(document.getElementById('plc-slot').value)
|
||||
};
|
||||
|
||||
fetch('/api/plc/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// Configuración UDP
|
||||
document.getElementById('udp-config-form').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
const data = {
|
||||
host: document.getElementById('udp-host').value,
|
||||
port: parseInt(document.getElementById('udp-port').value)
|
||||
};
|
||||
|
||||
fetch('/api/udp/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// Botón de conexión PLC
|
||||
document.getElementById('connect-btn').addEventListener('click', function () {
|
||||
fetch('/api/plc/connect', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
});
|
||||
});
|
||||
|
||||
// Botón de desconexión PLC
|
||||
document.getElementById('disconnect-btn').addEventListener('click', function () {
|
||||
fetch('/api/plc/disconnect', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
});
|
||||
});
|
||||
|
||||
// Botón de actualización de intervalo
|
||||
document.getElementById('update-sampling-btn').addEventListener('click', function () {
|
||||
const interval = parseFloat(document.getElementById('sampling-interval').value);
|
||||
fetch('/api/sampling', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ interval: interval })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
});
|
||||
});
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,256 @@
|
|||
/**
|
||||
* Gestión del estado del sistema y actualizaciones en tiempo real
|
||||
*/
|
||||
|
||||
// Variables para el streaming de estado
|
||||
let statusEventSource = null;
|
||||
let isStreamingStatus = false;
|
||||
|
||||
// Actualizar el estado del sistema
|
||||
function updateStatus() {
|
||||
fetch('/api/status')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const plcStatus = document.getElementById('plc-status');
|
||||
const streamStatus = document.getElementById('stream-status');
|
||||
const csvStatus = document.getElementById('csv-status');
|
||||
const diskSpaceStatus = document.getElementById('disk-space');
|
||||
|
||||
// Actualizar estado de conexión PLC
|
||||
if (data.plc_connected) {
|
||||
plcStatus.innerHTML = '🔌 PLC: Connected <div style="margin-top: 8px;"><button type="button" id="status-disconnect-btn">❌ Disconnect</button></div>';
|
||||
plcStatus.className = 'status-item status-connected';
|
||||
|
||||
// Añadir event listener al nuevo botón de desconexión
|
||||
document.getElementById('status-disconnect-btn').addEventListener('click', function () {
|
||||
fetch('/api/plc/disconnect', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
plcStatus.innerHTML = '🔌 PLC: Disconnected <div style="margin-top: 8px;"><button type="button" id="status-connect-btn">🔗 Connect</button></div>';
|
||||
plcStatus.className = 'status-item status-disconnected';
|
||||
|
||||
// Añadir event listener al botón de conexión
|
||||
document.getElementById('status-connect-btn').addEventListener('click', function () {
|
||||
fetch('/api/plc/connect', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Actualizar estado de streaming UDP
|
||||
if (data.streaming) {
|
||||
streamStatus.innerHTML = '📡 UDP Streaming: Active <div style="margin-top: 8px;"><button type="button" id="status-streaming-btn">⏹️ Stop</button></div>';
|
||||
streamStatus.className = 'status-item status-streaming';
|
||||
|
||||
// Añadir event listener al botón de parar streaming UDP
|
||||
document.getElementById('status-streaming-btn').addEventListener('click', function () {
|
||||
fetch('/api/udp/streaming/stop', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
streamStatus.innerHTML = '📡 UDP Streaming: Inactive <div style="margin-top: 8px;"><button type="button" id="status-start-btn">▶️ Start</button></div>';
|
||||
streamStatus.className = 'status-item status-idle';
|
||||
|
||||
// Añadir event listener al botón de iniciar streaming UDP
|
||||
document.getElementById('status-start-btn').addEventListener('click', function () {
|
||||
fetch('/api/udp/streaming/start', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Actualizar estado de grabación CSV
|
||||
if (data.csv_recording) {
|
||||
csvStatus.textContent = `💾 CSV: Recording`;
|
||||
csvStatus.className = 'status-item status-streaming';
|
||||
} else {
|
||||
csvStatus.textContent = `💾 CSV: Inactive`;
|
||||
csvStatus.className = 'status-item status-idle';
|
||||
}
|
||||
|
||||
// Actualizar estado de espacio en disco
|
||||
if (data.disk_space_info) {
|
||||
diskSpaceStatus.innerHTML = `💽 Disk: ${data.disk_space_info.free_space} free<br>
|
||||
⏱️ ~${data.disk_space_info.recording_time_left}`;
|
||||
diskSpaceStatus.className = 'status-item status-idle';
|
||||
} else {
|
||||
diskSpaceStatus.textContent = '💽 Disk Space: Calculating...';
|
||||
diskSpaceStatus.className = 'status-item status-idle';
|
||||
}
|
||||
})
|
||||
.catch(error => console.error('Error updating status:', error));
|
||||
}
|
||||
|
||||
// Iniciar streaming de estado en tiempo real
|
||||
function startStatusStreaming() {
|
||||
if (isStreamingStatus) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cerrar conexión existente si hay alguna
|
||||
if (statusEventSource) {
|
||||
statusEventSource.close();
|
||||
}
|
||||
|
||||
// Crear nueva conexión EventSource
|
||||
statusEventSource = new EventSource('/api/stream/status?interval=2.0');
|
||||
|
||||
statusEventSource.onopen = function (event) {
|
||||
console.log('Status streaming connected');
|
||||
isStreamingStatus = true;
|
||||
};
|
||||
|
||||
statusEventSource.onmessage = function (event) {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
switch (data.type) {
|
||||
case 'connected':
|
||||
console.log('Status stream connected:', data.message);
|
||||
break;
|
||||
|
||||
case 'status':
|
||||
// Actualizar estado en tiempo real
|
||||
updateStatusFromStream(data.status);
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
console.error('Status stream error:', data.message);
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing status SSE data:', error);
|
||||
}
|
||||
};
|
||||
|
||||
statusEventSource.onerror = function (event) {
|
||||
console.error('Status stream error:', event);
|
||||
isStreamingStatus = false;
|
||||
|
||||
// Intentar reconectar después de un retraso
|
||||
setTimeout(() => {
|
||||
startStatusStreaming();
|
||||
}, 10000);
|
||||
};
|
||||
}
|
||||
|
||||
// Detener streaming de estado en tiempo real
|
||||
function stopStatusStreaming() {
|
||||
if (statusEventSource) {
|
||||
statusEventSource.close();
|
||||
statusEventSource = null;
|
||||
}
|
||||
isStreamingStatus = false;
|
||||
}
|
||||
|
||||
// Actualizar estado desde datos de streaming
|
||||
function updateStatusFromStream(status) {
|
||||
const plcStatus = document.getElementById('plc-status');
|
||||
const streamStatus = document.getElementById('stream-status');
|
||||
const csvStatus = document.getElementById('csv-status');
|
||||
const diskSpaceStatus = document.getElementById('disk-space');
|
||||
|
||||
// Actualizar estado de conexión PLC
|
||||
if (status.plc_connected) {
|
||||
plcStatus.innerHTML = '🔌 PLC: Connected <div style="margin-top: 8px;"><button type="button" id="status-disconnect-btn">❌ Disconnect</button></div>';
|
||||
plcStatus.className = 'status-item status-connected';
|
||||
|
||||
// Añadir event listener al nuevo botón de desconexión
|
||||
const disconnectBtn = document.getElementById('status-disconnect-btn');
|
||||
if (disconnectBtn) {
|
||||
disconnectBtn.addEventListener('click', function () {
|
||||
fetch('/api/plc/disconnect', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
plcStatus.innerHTML = '🔌 PLC: Disconnected <div style="margin-top: 8px;"><button type="button" id="status-connect-btn">🔗 Connect</button></div>';
|
||||
plcStatus.className = 'status-item status-disconnected';
|
||||
|
||||
// Añadir event listener al botón de conexión
|
||||
const connectBtn = document.getElementById('status-connect-btn');
|
||||
if (connectBtn) {
|
||||
connectBtn.addEventListener('click', function () {
|
||||
fetch('/api/plc/connect', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Actualizar estado de streaming UDP
|
||||
if (status.streaming) {
|
||||
streamStatus.innerHTML = '📡 UDP Streaming: Active <div style="margin-top: 8px;"><button type="button" id="status-streaming-btn">⏹️ Stop</button></div>';
|
||||
streamStatus.className = 'status-item status-streaming';
|
||||
|
||||
// Añadir event listener al botón de parar streaming UDP
|
||||
const stopBtn = document.getElementById('status-streaming-btn');
|
||||
if (stopBtn) {
|
||||
stopBtn.addEventListener('click', function () {
|
||||
fetch('/api/udp/streaming/stop', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
streamStatus.innerHTML = '📡 UDP Streaming: Inactive <div style="margin-top: 8px;"><button type="button" id="status-start-btn">▶️ Start</button></div>';
|
||||
streamStatus.className = 'status-item status-idle';
|
||||
|
||||
// Añadir event listener al botón de iniciar streaming UDP
|
||||
const startBtn = document.getElementById('status-start-btn');
|
||||
if (startBtn) {
|
||||
startBtn.addEventListener('click', function () {
|
||||
fetch('/api/udp/streaming/start', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Actualizar estado de grabación CSV
|
||||
if (status.csv_recording) {
|
||||
csvStatus.textContent = `💾 CSV: Recording`;
|
||||
csvStatus.className = 'status-item status-streaming';
|
||||
} else {
|
||||
csvStatus.textContent = `💾 CSV: Inactive`;
|
||||
csvStatus.className = 'status-item status-idle';
|
||||
}
|
||||
|
||||
// Actualizar estado de espacio en disco
|
||||
if (status.disk_space_info) {
|
||||
diskSpaceStatus.innerHTML = `💽 Disk: ${status.disk_space_info.free_space} free<br>
|
||||
⏱️ ~${status.disk_space_info.recording_time_left}`;
|
||||
diskSpaceStatus.className = 'status-item status-idle';
|
||||
} else {
|
||||
diskSpaceStatus.textContent = '💽 Disk Space: Calculating...';
|
||||
diskSpaceStatus.className = 'status-item status-idle';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/**
|
||||
* Gestión del streaming UDP a PlotJuggler (independiente del recording CSV)
|
||||
*/
|
||||
|
||||
// Inicializar listeners para el control de streaming UDP
|
||||
function initStreamingListeners() {
|
||||
// Iniciar streaming UDP
|
||||
document.getElementById('start-streaming-btn').addEventListener('click', function () {
|
||||
fetch('/api/udp/streaming/start', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error starting UDP streaming:', error);
|
||||
showMessage('Error starting UDP streaming', 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// Detener streaming UDP
|
||||
document.getElementById('stop-streaming-btn').addEventListener('click', function () {
|
||||
fetch('/api/udp/streaming/stop', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error stopping UDP streaming:', error);
|
||||
showMessage('Error stopping UDP streaming', 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// Cargar estado de streaming de variables
|
||||
loadStreamingStatus();
|
||||
}
|
||||
|
||||
// Cargar estado de variables en streaming
|
||||
function loadStreamingStatus() {
|
||||
fetch('/api/variables/streaming')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
data.streaming_variables.forEach(varName => {
|
||||
const checkbox = document.getElementById(`stream-${varName}`);
|
||||
if (checkbox) {
|
||||
checkbox.checked = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => console.error('Error loading streaming status:', error));
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,296 @@
|
|||
/**
|
||||
* Tab System Management
|
||||
* Maneja la navegación entre tabs en la aplicación
|
||||
*/
|
||||
|
||||
class TabManager {
|
||||
constructor() {
|
||||
this.currentTab = 'datasets';
|
||||
this.plotTabs = new Set(); // Track dynamic plot tabs
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Event listeners para los botones de tab estáticos
|
||||
this.bindStaticTabs();
|
||||
|
||||
// Inicializar con el tab activo por defecto
|
||||
this.switchTab(this.currentTab);
|
||||
|
||||
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 => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
|
||||
document.querySelectorAll('.tab-content').forEach(content => {
|
||||
content.classList.remove('active');
|
||||
});
|
||||
|
||||
// Activar el tab seleccionado
|
||||
const activeBtn = document.querySelector(`[data-tab="${tabName}"]`);
|
||||
const activeContent = document.getElementById(`${tabName}-tab`);
|
||||
|
||||
if (activeBtn && activeContent) {
|
||||
activeBtn.classList.add('active');
|
||||
activeContent.classList.add('active');
|
||||
this.currentTab = tabName;
|
||||
|
||||
// Eventos específicos por tab
|
||||
this.handleTabSpecificEvents(tabName);
|
||||
|
||||
console.log(`📑 Switched to tab: ${tabName}`);
|
||||
}
|
||||
}
|
||||
|
||||
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}
|
||||
<span class="sub-tab-close" data-session-id="${sessionId}">×</span>
|
||||
`;
|
||||
|
||||
// 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 = `
|
||||
<article>
|
||||
<header>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>📈 ${plotName}</span>
|
||||
<div>
|
||||
<button type="button" class="outline" onclick="window.editPlotSession('${sessionId}')">
|
||||
✏️ Edit Plot
|
||||
</button>
|
||||
<button type="button" class="secondary" onclick="window.removePlotSession('${sessionId}')">
|
||||
🗑️ Remove Plot
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="plot-session" id="plot-session-${sessionId}">
|
||||
<div class="plot-header">
|
||||
<h4>📈 ${plotName}</h4>
|
||||
<div class="plot-controls">
|
||||
<button class="btn btn-sm" onclick="plotManager.controlPlot('${sessionId}', 'start')" title="Start">
|
||||
▶️ Start
|
||||
</button>
|
||||
<button class="btn btn-sm" onclick="plotManager.controlPlot('${sessionId}', 'pause')" title="Pause">
|
||||
⏸️ Pause
|
||||
</button>
|
||||
<button class="btn btn-sm" onclick="plotManager.controlPlot('${sessionId}', 'clear')" title="Clear">
|
||||
🗑️ Clear
|
||||
</button>
|
||||
<button class="btn btn-sm" onclick="plotManager.controlPlot('${sessionId}', 'stop')" title="Stop">
|
||||
⏹️ Stop
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="plot-info">
|
||||
<span class="plot-stats" id="plot-stats-${sessionId}">
|
||||
Loading plot information...
|
||||
</span>
|
||||
</div>
|
||||
<div class="plot-canvas">
|
||||
<canvas id="chart-${sessionId}"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
|
||||
// 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}
|
||||
<span class="sub-tab-close" data-session-id="${sessionId}">×</span>
|
||||
`;
|
||||
|
||||
// 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':
|
||||
// Inicializar plotting si no está inicializado
|
||||
if (typeof plotManager !== 'undefined' && !plotManager.isInitialized) {
|
||||
plotManager.init();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'events':
|
||||
// Cargar eventos si no están cargados
|
||||
if (typeof loadEvents === 'function') {
|
||||
loadEvents();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'datasets':
|
||||
// Actualizar datasets si es necesario
|
||||
if (typeof loadDatasets === 'function') {
|
||||
loadDatasets();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
getCurrentTab() {
|
||||
return this.currentTab;
|
||||
}
|
||||
}
|
||||
|
||||
// Inicialización
|
||||
let tabManager = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
tabManager = new TabManager();
|
||||
});
|
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* Gestión del tema de la aplicación (claro/oscuro/auto)
|
||||
*/
|
||||
|
||||
// Establecer el tema
|
||||
function setTheme(theme) {
|
||||
const html = document.documentElement;
|
||||
const buttons = document.querySelectorAll('.theme-selector button');
|
||||
|
||||
// Eliminar clase active de todos los botones
|
||||
buttons.forEach(btn => btn.classList.remove('active'));
|
||||
|
||||
// Establecer tema
|
||||
html.setAttribute('data-theme', theme);
|
||||
|
||||
// Añadir clase active al botón seleccionado
|
||||
document.getElementById(`theme-${theme}`).classList.add('active');
|
||||
|
||||
// Guardar preferencia en localStorage
|
||||
localStorage.setItem('theme', theme);
|
||||
}
|
||||
|
||||
// Cargar tema guardado al cargar la página
|
||||
function loadTheme() {
|
||||
const savedTheme = localStorage.getItem('theme') || 'light';
|
||||
setTheme(savedTheme);
|
||||
}
|
||||
|
||||
// Inicializar tema al cargar la página
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
loadTheme();
|
||||
});
|
|
@ -0,0 +1,64 @@
|
|||
/**
|
||||
* Funciones de utilidad general para la aplicación
|
||||
*/
|
||||
|
||||
// Función para mostrar mensajes en la interfaz
|
||||
function showMessage(message, type = 'success') {
|
||||
const messagesDiv = document.getElementById('messages');
|
||||
let alertClass;
|
||||
|
||||
switch (type) {
|
||||
case 'success':
|
||||
alertClass = 'alert-success';
|
||||
break;
|
||||
case 'warning':
|
||||
alertClass = 'alert-warning';
|
||||
break;
|
||||
case 'info':
|
||||
alertClass = 'alert-info';
|
||||
break;
|
||||
case 'error':
|
||||
default:
|
||||
alertClass = 'alert-error';
|
||||
break;
|
||||
}
|
||||
|
||||
messagesDiv.innerHTML = `<div class="alert ${alertClass}">${message}</div>`;
|
||||
setTimeout(() => {
|
||||
messagesDiv.innerHTML = '';
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Formatear timestamp para los logs
|
||||
function formatTimestamp(isoString) {
|
||||
const date = new Date(isoString);
|
||||
return date.toLocaleString('es-ES', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
// Obtener icono para tipo de evento
|
||||
function getEventIcon(eventType) {
|
||||
const icons = {
|
||||
'plc_connection': '🔗',
|
||||
'plc_connection_failed': '❌',
|
||||
'plc_disconnection': '🔌',
|
||||
'plc_disconnection_error': '⚠️',
|
||||
'streaming_started': '▶️',
|
||||
'streaming_stopped': '⏹️',
|
||||
'streaming_error': '❌',
|
||||
'csv_started': '💾',
|
||||
'csv_stopped': '📁',
|
||||
'csv_error': '❌',
|
||||
'config_change': '⚙️',
|
||||
'variable_added': '➕',
|
||||
'variable_removed': '➖',
|
||||
'application_started': '🚀'
|
||||
};
|
||||
return icons[eventType] || '📋';
|
||||
}
|
|
@ -0,0 +1,322 @@
|
|||
/**
|
||||
* Gestión de variables y streaming de valores en tiempo real
|
||||
*/
|
||||
|
||||
// Variables para el streaming de variables
|
||||
let variableEventSource = null;
|
||||
let isStreamingVariables = false;
|
||||
|
||||
// Toggle de campos de variables según el área de memoria
|
||||
function toggleFields() {
|
||||
const area = document.getElementById('var-area').value;
|
||||
const dbField = document.getElementById('db-field');
|
||||
const dbInput = document.getElementById('var-db');
|
||||
const bitField = document.getElementById('bit-field');
|
||||
const typeSelect = document.getElementById('var-type');
|
||||
|
||||
// Manejar campo DB
|
||||
if (area === 'db') {
|
||||
dbField.style.display = 'block';
|
||||
dbInput.required = true;
|
||||
} else {
|
||||
dbField.style.display = 'none';
|
||||
dbInput.required = false;
|
||||
dbInput.value = 1; // Valor por defecto para áreas no DB
|
||||
}
|
||||
|
||||
// Manejar campo Bit y restricciones de tipo de datos
|
||||
if (area === 'e' || area === 'a' || area === 'mb') {
|
||||
bitField.style.display = 'block';
|
||||
// Para áreas de bit, forzar tipo de dato a bool
|
||||
typeSelect.value = 'bool';
|
||||
// Deshabilitar otros tipos de datos para áreas de bit
|
||||
Array.from(typeSelect.options).forEach(option => {
|
||||
option.disabled = (option.value !== 'bool');
|
||||
});
|
||||
} else {
|
||||
bitField.style.display = 'none';
|
||||
// Re-habilitar todos los tipos de datos para áreas no-bit
|
||||
Array.from(typeSelect.options).forEach(option => {
|
||||
option.disabled = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle de campos de edición de variables
|
||||
function toggleEditFields() {
|
||||
const area = document.getElementById('edit-var-area').value;
|
||||
const dbField = document.getElementById('edit-db-field');
|
||||
const dbInput = document.getElementById('edit-var-db');
|
||||
const bitField = document.getElementById('edit-bit-field');
|
||||
const typeSelect = document.getElementById('edit-var-type');
|
||||
|
||||
// Manejar campo DB
|
||||
if (area === 'db') {
|
||||
dbField.style.display = 'block';
|
||||
dbInput.required = true;
|
||||
} else {
|
||||
dbField.style.display = 'none';
|
||||
dbInput.required = false;
|
||||
dbInput.value = 1; // Valor por defecto para áreas no DB
|
||||
}
|
||||
|
||||
// Manejar campo Bit y restricciones de tipo de datos
|
||||
if (area === 'e' || area === 'a' || area === 'mb') {
|
||||
bitField.style.display = 'block';
|
||||
// Para áreas de bit, forzar tipo de dato a bool
|
||||
typeSelect.value = 'bool';
|
||||
// Deshabilitar otros tipos de datos para áreas de bit
|
||||
Array.from(typeSelect.options).forEach(option => {
|
||||
option.disabled = (option.value !== 'bool');
|
||||
});
|
||||
} else {
|
||||
bitField.style.display = 'none';
|
||||
// Re-habilitar todos los tipos de datos para áreas no-bit
|
||||
Array.from(typeSelect.options).forEach(option => {
|
||||
option.disabled = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Actualizar streaming para una variable
|
||||
function toggleStreaming(varName, enabled) {
|
||||
if (!currentDatasetId) {
|
||||
showMessage('No dataset selected', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/api/datasets/${currentDatasetId}/variables/${varName}/streaming`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ enabled: enabled })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus(); // Actualizar contador de variables de streaming
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error toggling streaming:', error);
|
||||
showMessage('Error updating streaming setting', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-start live display when dataset changes (if PLC is connected)
|
||||
function autoStartLiveDisplay() {
|
||||
if (currentDatasetId) {
|
||||
// Check if PLC is connected by fetching status
|
||||
fetch('/api/status')
|
||||
.then(response => response.json())
|
||||
.then(status => {
|
||||
if (status.plc_connected && !isStreamingVariables) {
|
||||
startVariableStreaming();
|
||||
showMessage('Live display started automatically for active dataset', 'info');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error checking PLC status:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Limpiar todos los valores de variables y establecer mensaje de estado
|
||||
function clearVariableValues(statusMessage = '--') {
|
||||
// Encontrar todas las celdas de valor y limpiarlas
|
||||
const valueCells = document.querySelectorAll('[id^="value-"]');
|
||||
valueCells.forEach(cell => {
|
||||
cell.textContent = statusMessage;
|
||||
cell.style.color = 'var(--pico-muted-color)';
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Iniciar streaming de variables en tiempo real
|
||||
function startVariableStreaming() {
|
||||
if (!currentDatasetId || isStreamingVariables) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cerrar conexión existente si hay alguna
|
||||
if (variableEventSource) {
|
||||
variableEventSource.close();
|
||||
}
|
||||
|
||||
// Crear nueva conexión EventSource
|
||||
variableEventSource = new EventSource(`/api/stream/variables?dataset_id=${currentDatasetId}&interval=1.0`);
|
||||
|
||||
variableEventSource.onopen = function (event) {
|
||||
console.log('Variable streaming connected');
|
||||
isStreamingVariables = true;
|
||||
updateStreamingIndicator(true);
|
||||
};
|
||||
|
||||
variableEventSource.onmessage = function (event) {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
switch (data.type) {
|
||||
case 'connected':
|
||||
console.log('Variable stream connected:', data.message);
|
||||
break;
|
||||
|
||||
case 'values':
|
||||
// Actualizar valores de variables en tiempo real desde caché
|
||||
updateVariableValuesFromStream(data);
|
||||
break;
|
||||
|
||||
case 'cache_error':
|
||||
console.error('Cache error in variable stream:', data.message);
|
||||
showMessage(`Cache error: ${data.message}`, 'error');
|
||||
clearVariableValues('CACHE ERROR');
|
||||
break;
|
||||
|
||||
case 'plc_disconnected':
|
||||
clearVariableValues('PLC OFFLINE');
|
||||
showMessage('PLC disconnected - cache not being populated', 'warning');
|
||||
break;
|
||||
|
||||
case 'dataset_inactive':
|
||||
clearVariableValues('DATASET INACTIVE');
|
||||
showMessage('Dataset is not active - activate to populate cache', 'warning');
|
||||
break;
|
||||
|
||||
case 'no_variables':
|
||||
clearVariableValues('NO VARIABLES');
|
||||
showMessage('No variables defined in this dataset', 'info');
|
||||
break;
|
||||
|
||||
case 'no_cache':
|
||||
clearVariableValues('READING...');
|
||||
const samplingInfo = data.sampling_interval ? ` (every ${data.sampling_interval}s)` : '';
|
||||
showMessage(`Waiting for cache to be populated${samplingInfo}`, 'info');
|
||||
break;
|
||||
|
||||
case 'stream_error':
|
||||
console.error('SSE stream error:', data.message);
|
||||
showMessage(`Streaming error: ${data.message}`, 'error');
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn('Unknown SSE message type:', data.type);
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing SSE data:', error);
|
||||
}
|
||||
};
|
||||
|
||||
variableEventSource.onerror = function (event) {
|
||||
console.error('Variable stream error:', event);
|
||||
isStreamingVariables = false;
|
||||
updateStreamingIndicator(false);
|
||||
|
||||
// Intentar reconectar después de un retraso
|
||||
setTimeout(() => {
|
||||
if (currentDatasetId) {
|
||||
startVariableStreaming();
|
||||
}
|
||||
}, 5000);
|
||||
};
|
||||
}
|
||||
|
||||
// Detener streaming de variables en tiempo real
|
||||
function stopVariableStreaming() {
|
||||
if (variableEventSource) {
|
||||
variableEventSource.close();
|
||||
variableEventSource = null;
|
||||
}
|
||||
isStreamingVariables = false;
|
||||
updateStreamingIndicator(false);
|
||||
}
|
||||
|
||||
// Actualizar valores de variables desde datos de streaming
|
||||
function updateVariableValuesFromStream(data) {
|
||||
const values = data.values;
|
||||
const timestamp = data.timestamp;
|
||||
const source = data.source;
|
||||
const stats = data.stats;
|
||||
|
||||
// Actualizar cada valor de variable
|
||||
Object.keys(values).forEach(varName => {
|
||||
const valueCell = document.getElementById(`value-${varName}`);
|
||||
if (valueCell) {
|
||||
const value = values[varName];
|
||||
valueCell.textContent = value;
|
||||
|
||||
// Código de color basado en el estado del valor
|
||||
if (value === 'ERROR' || value === 'FORMAT_ERROR') {
|
||||
valueCell.style.color = 'var(--pico-color-red-500)';
|
||||
valueCell.style.fontWeight = 'bold';
|
||||
} else {
|
||||
valueCell.style.color = 'var(--pico-color-green-600)';
|
||||
valueCell.style.fontWeight = 'bold';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Actualizar timestamp e información de origen
|
||||
const lastRefreshTime = document.getElementById('last-refresh-time');
|
||||
if (lastRefreshTime) {
|
||||
const sourceIcon = source === 'cache' ? '📊' : '🔗';
|
||||
const sourceText = source === 'cache' ? 'streaming cache' : 'direct PLC';
|
||||
|
||||
if (stats && stats.failed > 0) {
|
||||
lastRefreshTime.innerHTML = `
|
||||
<span style="color: var(--pico-color-green-600);">🔄 Live streaming</span><br/>
|
||||
<small style="color: var(--pico-color-amber-600);">
|
||||
⚠️ ${stats.success}/${stats.total} variables (${stats.failed} failed)
|
||||
</small><br/>
|
||||
<small style="color: var(--pico-muted-color);">
|
||||
${sourceIcon} ${sourceText} • ${new Date(timestamp).toLocaleTimeString()}
|
||||
</small>
|
||||
`;
|
||||
} else {
|
||||
lastRefreshTime.innerHTML = `
|
||||
<span style="color: var(--pico-color-green-600);">🔄 Live streaming</span><br/>
|
||||
<small style="color: var(--pico-color-green-600);">
|
||||
✅ All ${stats ? stats.success : 'N/A'} variables OK
|
||||
</small><br/>
|
||||
<small style="color: var(--pico-muted-color);">
|
||||
${sourceIcon} ${sourceText} • ${new Date(timestamp).toLocaleTimeString()}
|
||||
</small>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Actualizar indicador de streaming
|
||||
function updateStreamingIndicator(isStreaming) {
|
||||
const toggleBtn = document.getElementById('toggle-streaming-btn');
|
||||
if (toggleBtn) {
|
||||
if (isStreaming) {
|
||||
toggleBtn.innerHTML = '⏹️ Stop Live Display';
|
||||
toggleBtn.title = 'Stop live variable display';
|
||||
} else {
|
||||
toggleBtn.innerHTML = '▶️ Start Live Display';
|
||||
toggleBtn.title = 'Start live variable display';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Alternar streaming en tiempo real
|
||||
function toggleRealTimeStreaming() {
|
||||
if (isStreamingVariables) {
|
||||
stopVariableStreaming();
|
||||
showMessage('Real-time streaming stopped', 'info');
|
||||
} else {
|
||||
startVariableStreaming();
|
||||
showMessage('Real-time streaming started', 'success');
|
||||
}
|
||||
|
||||
// Actualizar texto del botón
|
||||
const toggleBtn = document.getElementById('toggle-streaming-btn');
|
||||
if (toggleBtn) {
|
||||
if (isStreamingVariables) {
|
||||
toggleBtn.innerHTML = '⏹️ Stop Live Streaming';
|
||||
} else {
|
||||
toggleBtn.innerHTML = '▶️ Start Live Streaming';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue