Update system state configuration, add cursor ignore file, and implement PlotRealtimeViewer component for real-time plot management
This commit is contained in:
parent
771cf6cba6
commit
972a965335
|
@ -0,0 +1,3 @@
|
|||
# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)
|
||||
*.csv
|
||||
|
|
@ -1,3 +1,28 @@
|
|||
2025-08-13
|
||||
|
||||
- Solicitud (resumen): Integrar visualización de plots en tiempo real directamente en `Dashboard.jsx`, debajo de la gestión de plots, para tener en un solo lugar la configuración, creación/modificación de variables, creación/modificación de plots y visualización.
|
||||
|
||||
- Decisiones y cambios:
|
||||
- Se creó el componente `frontend/src/components/PlotRealtimeViewer.jsx` que lista todas las sesiones de plots del backend (`/api/plots`) y renderiza un `ChartjsPlot` por sesión, con controles locales y de backend (start/pause/stop/clear y refresh).
|
||||
- Se integró en `frontend/src/pages/Dashboard.jsx` debajo de `PlotCompleteManager`, con el título “Real-time plots”, cumpliendo la visualización embebida dentro del dashboard.
|
||||
- `PlotRealtimeViewer` refresca estados periódicamente y usa `/api/plots/{session_id}/config` para sincronizar estado/pausa. `ChartjsPlot` ya gestiona la carga de config y streaming desde `/api/plots/{session_id}/data`.
|
||||
|
||||
- Notas técnicas relevantes:
|
||||
- No se tocó el backend; se reutilizan los endpoints existentes (`/api/plots`, `/api/plots/{id}/config`, `/api/plots/{id}/control`, `/api/plots/{id}/data`).
|
||||
- La gestión de definiciones/variables de plots sigue en `PlotCompleteManager`; al guardar, el backend recarga las sesiones. `PlotRealtimeViewer` refresca para reflejar los cambios.
|
||||
|
||||
- Fix: error Chart.js "Cannot read properties of undefined (reading 'handleEvent')".
|
||||
- `frontend/src/components/ChartjsPlot.jsx` ahora:
|
||||
- Solo aplica opciones de zoom si el plugin está registrado; intenta registrar con varias claves globales si existe.
|
||||
- Pausa el scale realtime y limpia intervalos antes de destruir el chart, para evitar ciclos RAF del plugin streaming tras unmount.
|
||||
- En modo fallback (scale `time`), desactiva explícitamente el plugin realtime en `options.plugins.realtime = false`.
|
||||
|
||||
- Fix control de tiempo real en Dashboard (gráficos siempre activos / botones sin efecto):
|
||||
- Problema: los charts iniciaban activos y los botones Start/Pause no surtían efecto (solo Clear funcionaba).
|
||||
- Causas: desincronización entre `pause` del scale realtime y la ingesta local; el efecto que escucha `session.is_active/is_paused` revertía de inmediato las acciones locales.
|
||||
- Cambios: en `frontend/src/components/ChartjsPlot.jsx` se sincronizan `ingestPaused` e `isPaused` con el estado inicial del chart (`pause: !session.is_active || session.is_paused`). Se añade ventana de override temporal (`userOverrideUntil` 3s) tras Start/Pause para evitar que el efecto de sincronización con backend revierta inmediatamente la acción del usuario.
|
||||
- Impacto: los gráficos ahora respetan el estado inicial detenido cuando corresponde; los botones Start/Pause/Stop funcionan sin reanudaciones/pausas espurias.
|
||||
|
||||
# Memoria de Evolución del Proyecto
|
||||
|
||||
## Sistema de Tablas Editables para Datasets y Plots (10/08/2025)
|
||||
|
@ -651,3 +676,20 @@ ChartjsPlot render → Chart.js integration → streaming setup
|
|||
- 🐛 **Error isolation**: Separación clara entre errores de red, backend y frontend
|
||||
- 📋 **User feedback**: Notificaciones claras de problemas y estado del sistema
|
||||
- ⚡ **Graceful handling**: Sistema continúa funcionando aunque algunos servicios fallen
|
||||
|
||||
## 2025-08-13 (tarde)
|
||||
|
||||
- Solicitud (resumen): "Los datasets que leen @dataset_definitions.json no están mostrando el valor real de Dataset Enabled en @Dashboard.jsx".
|
||||
|
||||
- Decisiones y cambios:
|
||||
- Se añadió una insignia de estado en `frontend/src/components/FormTable.jsx` que muestra "Enabled/Disabled" en cada tarjeta de item cuando la data tiene `enabled: boolean`. Esto asegura que, incluso en modo readonly, el estado actual del dataset se vea claramente conforme al JSON `config/data/dataset_definitions.json`.
|
||||
|
||||
- Conocimientos clave:
|
||||
- La UI de datasets usa `FormTable` con `schema.additionalProperties` y `uiSchema` específicos; la propiedad `enabled` se edita con widget checkbox según `dataset-definitions.uischema.json`.
|
||||
- El backend calcula `active_datasets` automáticamente a partir de `datasets[].enabled` y persiste en `dataset_definitions.json` sin campos estáticos, por lo que `enabled` es la fuente de verdad.
|
||||
- Mostrar la insignia evita confusión cuando el formulario está en modo solo lectura o cuando hay latencia entre guardado y recarga.
|
||||
|
||||
- Fix adicional: valores que se reseteaban al cambiar de campo en edición (RJSF)
|
||||
- Causa: El `Form` usaba `formData={data[key]}` y no controlábamos `onChange` en modo edición, por lo que cualquier re-render restauraba el valor original.
|
||||
- Solución: `FormTable.jsx` ahora usa un estado local `editingFormData` cuando `editingKey === key`. Se inicializa al pulsar Edit, `onChange` actualiza `editingFormData`, y `formData` del `Form` se alimenta de ese estado hasta guardar o cancelar.
|
||||
- Impacto: Al editar un item, los cambios entre campos se mantienen correctamente hasta pulsar Save.
|
||||
|
|
|
@ -9556,8 +9556,871 @@
|
|||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T10:04:01.510701",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: DAR",
|
||||
"details": {
|
||||
"dataset_id": "DAR",
|
||||
"variables_count": 3,
|
||||
"streaming_count": 2,
|
||||
"prefix": "gateway_phoenix"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T10:04:01.523388",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: Fast",
|
||||
"details": {
|
||||
"dataset_id": "Fast",
|
||||
"variables_count": 2,
|
||||
"streaming_count": 0,
|
||||
"prefix": "fast"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T10:04:01.534388",
|
||||
"level": "info",
|
||||
"event_type": "csv_recording_started",
|
||||
"message": "CSV recording started: 2 datasets activated",
|
||||
"details": {
|
||||
"activated_datasets": 2,
|
||||
"total_datasets": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T10:04:01.547346",
|
||||
"level": "info",
|
||||
"event_type": "plc_connection",
|
||||
"message": "Successfully connected to PLC 10.1.33.11 and auto-started CSV recording for 2 datasets",
|
||||
"details": {
|
||||
"ip": "10.1.33.11",
|
||||
"rack": 0,
|
||||
"slot": 2,
|
||||
"auto_started_recording": true,
|
||||
"recording_datasets": 2,
|
||||
"dataset_names": [
|
||||
"DAR",
|
||||
"Fast"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T10:04:50.156461",
|
||||
"level": "info",
|
||||
"event_type": "udp_streaming_started",
|
||||
"message": "UDP streaming to PlotJuggler started",
|
||||
"details": {
|
||||
"udp_host": "127.0.0.1",
|
||||
"udp_port": 9870,
|
||||
"datasets_available": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T11:30:17.872774",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T11:30:17.937806",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: DAR",
|
||||
"details": {
|
||||
"dataset_id": "DAR",
|
||||
"variables_count": 3,
|
||||
"streaming_count": 2,
|
||||
"prefix": "gateway_phoenix"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T11:30:17.951867",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: Fast",
|
||||
"details": {
|
||||
"dataset_id": "Fast",
|
||||
"variables_count": 2,
|
||||
"streaming_count": 0,
|
||||
"prefix": "fast"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T11:30:17.963808",
|
||||
"level": "info",
|
||||
"event_type": "csv_recording_started",
|
||||
"message": "CSV recording started: 2 datasets activated",
|
||||
"details": {
|
||||
"activated_datasets": 2,
|
||||
"total_datasets": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T11:30:17.977806",
|
||||
"level": "info",
|
||||
"event_type": "udp_streaming_started",
|
||||
"message": "UDP streaming to PlotJuggler started",
|
||||
"details": {
|
||||
"udp_host": "127.0.0.1",
|
||||
"udp_port": 9870,
|
||||
"datasets_available": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T11:36:04.553484",
|
||||
"level": "info",
|
||||
"event_type": "csv_recording_stopped",
|
||||
"message": "CSV recording stopped (dataset threads continue for UDP streaming)",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T11:36:04.566484",
|
||||
"level": "info",
|
||||
"event_type": "udp_streaming_stopped",
|
||||
"message": "UDP streaming to PlotJuggler stopped (CSV recording continues)",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T11:36:05.479892",
|
||||
"level": "info",
|
||||
"event_type": "dataset_deactivated",
|
||||
"message": "Dataset deactivated: DAR",
|
||||
"details": {
|
||||
"dataset_id": "DAR"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T11:36:05.585651",
|
||||
"level": "info",
|
||||
"event_type": "dataset_deactivated",
|
||||
"message": "Dataset deactivated: Fast",
|
||||
"details": {
|
||||
"dataset_id": "Fast"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T11:36:05.601680",
|
||||
"level": "info",
|
||||
"event_type": "plc_disconnection",
|
||||
"message": "Disconnected from PLC 10.1.33.11 (stopped recording and streaming)",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T11:36:07.028566",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: DAR",
|
||||
"details": {
|
||||
"dataset_id": "DAR",
|
||||
"variables_count": 3,
|
||||
"streaming_count": 2,
|
||||
"prefix": "gateway_phoenix"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T11:36:07.043893",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: Fast",
|
||||
"details": {
|
||||
"dataset_id": "Fast",
|
||||
"variables_count": 2,
|
||||
"streaming_count": 0,
|
||||
"prefix": "fast"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T11:36:07.060471",
|
||||
"level": "info",
|
||||
"event_type": "csv_recording_started",
|
||||
"message": "CSV recording started: 2 datasets activated",
|
||||
"details": {
|
||||
"activated_datasets": 2,
|
||||
"total_datasets": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T11:36:07.073719",
|
||||
"level": "info",
|
||||
"event_type": "plc_connection",
|
||||
"message": "Successfully connected to PLC 10.1.33.11 and auto-started CSV recording for 2 datasets",
|
||||
"details": {
|
||||
"ip": "10.1.33.11",
|
||||
"rack": 0,
|
||||
"slot": 2,
|
||||
"auto_started_recording": true,
|
||||
"recording_datasets": 2,
|
||||
"dataset_names": [
|
||||
"DAR",
|
||||
"Fast"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T11:38:47.160563",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T11:38:47.227567",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: DAR",
|
||||
"details": {
|
||||
"dataset_id": "DAR",
|
||||
"variables_count": 3,
|
||||
"streaming_count": 2,
|
||||
"prefix": "gateway_phoenix"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T11:38:47.241129",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: Fast",
|
||||
"details": {
|
||||
"dataset_id": "Fast",
|
||||
"variables_count": 2,
|
||||
"streaming_count": 0,
|
||||
"prefix": "fast"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T11:38:47.251583",
|
||||
"level": "info",
|
||||
"event_type": "csv_recording_started",
|
||||
"message": "CSV recording started: 2 datasets activated",
|
||||
"details": {
|
||||
"activated_datasets": 2,
|
||||
"total_datasets": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T11:43:03.461288",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T11:43:03.511481",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: DAR",
|
||||
"details": {
|
||||
"dataset_id": "DAR",
|
||||
"variables_count": 3,
|
||||
"streaming_count": 2,
|
||||
"prefix": "gateway_phoenix"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T11:43:03.527332",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: Fast",
|
||||
"details": {
|
||||
"dataset_id": "Fast",
|
||||
"variables_count": 2,
|
||||
"streaming_count": 0,
|
||||
"prefix": "fast"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T11:43:03.539480",
|
||||
"level": "info",
|
||||
"event_type": "csv_recording_started",
|
||||
"message": "CSV recording started: 2 datasets activated",
|
||||
"details": {
|
||||
"activated_datasets": 2,
|
||||
"total_datasets": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T13:24:56.257638",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T13:24:56.400551",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: DAR",
|
||||
"details": {
|
||||
"dataset_id": "DAR",
|
||||
"variables_count": 3,
|
||||
"streaming_count": 2,
|
||||
"prefix": "gateway_phoenix"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T13:24:56.415147",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: Fast",
|
||||
"details": {
|
||||
"dataset_id": "Fast",
|
||||
"variables_count": 2,
|
||||
"streaming_count": 0,
|
||||
"prefix": "fast"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T13:24:56.428513",
|
||||
"level": "info",
|
||||
"event_type": "csv_recording_started",
|
||||
"message": "CSV recording started: 2 datasets activated",
|
||||
"details": {
|
||||
"activated_datasets": 2,
|
||||
"total_datasets": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T13:47:21.309062",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T13:47:21.393140",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: DAR",
|
||||
"details": {
|
||||
"dataset_id": "DAR",
|
||||
"variables_count": 3,
|
||||
"streaming_count": 2,
|
||||
"prefix": "gateway_phoenix"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T13:47:21.409313",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: Fast",
|
||||
"details": {
|
||||
"dataset_id": "Fast",
|
||||
"variables_count": 2,
|
||||
"streaming_count": 0,
|
||||
"prefix": "fast"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T13:47:21.420314",
|
||||
"level": "info",
|
||||
"event_type": "csv_recording_started",
|
||||
"message": "CSV recording started: 2 datasets activated",
|
||||
"details": {
|
||||
"activated_datasets": 2,
|
||||
"total_datasets": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T13:48:30.992091",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T13:48:31.058240",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: DAR",
|
||||
"details": {
|
||||
"dataset_id": "DAR",
|
||||
"variables_count": 3,
|
||||
"streaming_count": 2,
|
||||
"prefix": "gateway_phoenix"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T13:48:31.069240",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: Fast",
|
||||
"details": {
|
||||
"dataset_id": "Fast",
|
||||
"variables_count": 2,
|
||||
"streaming_count": 0,
|
||||
"prefix": "fast"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T13:48:31.081242",
|
||||
"level": "info",
|
||||
"event_type": "csv_recording_started",
|
||||
"message": "CSV recording started: 2 datasets activated",
|
||||
"details": {
|
||||
"activated_datasets": 2,
|
||||
"total_datasets": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T13:49:20.228991",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T13:49:20.293590",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: DAR",
|
||||
"details": {
|
||||
"dataset_id": "DAR",
|
||||
"variables_count": 3,
|
||||
"streaming_count": 2,
|
||||
"prefix": "gateway_phoenix"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T13:49:20.306683",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: Fast",
|
||||
"details": {
|
||||
"dataset_id": "Fast",
|
||||
"variables_count": 2,
|
||||
"streaming_count": 0,
|
||||
"prefix": "fast"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T13:49:20.318681",
|
||||
"level": "info",
|
||||
"event_type": "csv_recording_started",
|
||||
"message": "CSV recording started: 2 datasets activated",
|
||||
"details": {
|
||||
"activated_datasets": 2,
|
||||
"total_datasets": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T13:50:57.575176",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T13:50:57.642620",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: DAR",
|
||||
"details": {
|
||||
"dataset_id": "DAR",
|
||||
"variables_count": 3,
|
||||
"streaming_count": 2,
|
||||
"prefix": "gateway_phoenix"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T13:50:57.661324",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: Fast",
|
||||
"details": {
|
||||
"dataset_id": "Fast",
|
||||
"variables_count": 2,
|
||||
"streaming_count": 0,
|
||||
"prefix": "fast"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T13:50:57.674356",
|
||||
"level": "info",
|
||||
"event_type": "csv_recording_started",
|
||||
"message": "CSV recording started: 2 datasets activated",
|
||||
"details": {
|
||||
"activated_datasets": 2,
|
||||
"total_datasets": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T13:55:34.864405",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T13:55:34.944688",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: DAR",
|
||||
"details": {
|
||||
"dataset_id": "DAR",
|
||||
"variables_count": 3,
|
||||
"streaming_count": 2,
|
||||
"prefix": "gateway_phoenix"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T13:55:34.961689",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: Fast",
|
||||
"details": {
|
||||
"dataset_id": "Fast",
|
||||
"variables_count": 2,
|
||||
"streaming_count": 0,
|
||||
"prefix": "fast"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T13:55:34.974937",
|
||||
"level": "info",
|
||||
"event_type": "csv_recording_started",
|
||||
"message": "CSV recording started: 2 datasets activated",
|
||||
"details": {
|
||||
"activated_datasets": 2,
|
||||
"total_datasets": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T13:55:38.731880",
|
||||
"level": "error",
|
||||
"event_type": "dataset_loop_error",
|
||||
"message": "Multiple consecutive read failures for dataset 'Fast' (5). Stopping dataset.",
|
||||
"details": {
|
||||
"dataset_id": "Fast",
|
||||
"consecutive_errors": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T13:55:42.951679",
|
||||
"level": "error",
|
||||
"event_type": "dataset_loop_error",
|
||||
"message": "Multiple consecutive read failures for dataset 'DAR' (5). Stopping dataset.",
|
||||
"details": {
|
||||
"dataset_id": "DAR",
|
||||
"consecutive_errors": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T13:56:18.746882",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T13:56:18.811395",
|
||||
"level": "error",
|
||||
"event_type": "csv_recording_error",
|
||||
"message": "Cannot start CSV recording: No datasets with variables configured",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T13:56:39.664861",
|
||||
"level": "info",
|
||||
"event_type": "csv_recording_stopped",
|
||||
"message": "CSV recording stopped (dataset threads continue for UDP streaming)",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T13:56:39.677858",
|
||||
"level": "info",
|
||||
"event_type": "udp_streaming_stopped",
|
||||
"message": "UDP streaming to PlotJuggler stopped (CSV recording continues)",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T13:56:39.690990",
|
||||
"level": "info",
|
||||
"event_type": "dataset_deactivated",
|
||||
"message": "Dataset deactivated: Fast",
|
||||
"details": {
|
||||
"dataset_id": "Fast"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T13:56:39.704302",
|
||||
"level": "info",
|
||||
"event_type": "dataset_deactivated",
|
||||
"message": "Dataset deactivated: DAR",
|
||||
"details": {
|
||||
"dataset_id": "DAR"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T13:56:39.718587",
|
||||
"level": "info",
|
||||
"event_type": "plc_disconnection",
|
||||
"message": "Disconnected from PLC 10.1.33.11 (stopped recording and streaming)",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T13:57:37.934920",
|
||||
"level": "error",
|
||||
"event_type": "csv_recording_error",
|
||||
"message": "Cannot start CSV recording: No datasets with variables configured",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T13:57:37.961461",
|
||||
"level": "info",
|
||||
"event_type": "plc_connection",
|
||||
"message": "Successfully connected to PLC 10.1.33.11 (no datasets for recording)",
|
||||
"details": {
|
||||
"ip": "10.1.33.11",
|
||||
"rack": 0,
|
||||
"slot": 2,
|
||||
"auto_started_recording": false,
|
||||
"recording_datasets": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T14:02:07.164010",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T14:02:16.144763",
|
||||
"level": "error",
|
||||
"event_type": "csv_recording_error",
|
||||
"message": "Cannot start CSV recording: No datasets with variables configured",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T14:02:16.157356",
|
||||
"level": "info",
|
||||
"event_type": "plc_connection",
|
||||
"message": "Successfully connected to PLC 10.1.33.11 (no datasets for recording)",
|
||||
"details": {
|
||||
"ip": "10.1.33.11",
|
||||
"rack": 0,
|
||||
"slot": 2,
|
||||
"auto_started_recording": false,
|
||||
"recording_datasets": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T14:07:23.248088",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T14:07:34.119374",
|
||||
"level": "error",
|
||||
"event_type": "csv_recording_error",
|
||||
"message": "Cannot start CSV recording: No datasets with variables configured",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T14:07:34.131666",
|
||||
"level": "info",
|
||||
"event_type": "plc_connection",
|
||||
"message": "Successfully connected to PLC 10.1.33.11 (no datasets for recording)",
|
||||
"details": {
|
||||
"ip": "10.1.33.11",
|
||||
"rack": 0,
|
||||
"slot": 2,
|
||||
"auto_started_recording": false,
|
||||
"recording_datasets": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T14:10:03.473372",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T14:10:10.286283",
|
||||
"level": "info",
|
||||
"event_type": "csv_recording_stopped",
|
||||
"message": "CSV recording stopped (dataset threads continue for UDP streaming)",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T14:10:10.332149",
|
||||
"level": "info",
|
||||
"event_type": "udp_streaming_stopped",
|
||||
"message": "UDP streaming to PlotJuggler stopped (CSV recording continues)",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T14:10:10.360865",
|
||||
"level": "info",
|
||||
"event_type": "dataset_deactivated",
|
||||
"message": "Dataset deactivated: DAR",
|
||||
"details": {
|
||||
"dataset_id": "DAR"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T14:10:10.381239",
|
||||
"level": "info",
|
||||
"event_type": "dataset_deactivated",
|
||||
"message": "Dataset deactivated: Fast",
|
||||
"details": {
|
||||
"dataset_id": "Fast"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T14:10:10.405727",
|
||||
"level": "info",
|
||||
"event_type": "plc_disconnection",
|
||||
"message": "Disconnected from PLC 10.1.33.11 (stopped recording and streaming)",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T14:10:14.866208",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: DAR",
|
||||
"details": {
|
||||
"dataset_id": "DAR",
|
||||
"variables_count": 3,
|
||||
"streaming_count": 2,
|
||||
"prefix": "gateway_phoenix"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T14:10:14.881834",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: Fast",
|
||||
"details": {
|
||||
"dataset_id": "Fast",
|
||||
"variables_count": 2,
|
||||
"streaming_count": 0,
|
||||
"prefix": "fast"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T14:10:14.895847",
|
||||
"level": "info",
|
||||
"event_type": "csv_recording_started",
|
||||
"message": "CSV recording started: 2 datasets activated",
|
||||
"details": {
|
||||
"activated_datasets": 2,
|
||||
"total_datasets": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T14:10:14.915075",
|
||||
"level": "info",
|
||||
"event_type": "plc_connection",
|
||||
"message": "Successfully connected to PLC 10.1.33.11 and auto-started CSV recording for 2 datasets",
|
||||
"details": {
|
||||
"ip": "10.1.33.11",
|
||||
"rack": 0,
|
||||
"slot": 2,
|
||||
"auto_started_recording": true,
|
||||
"recording_datasets": 2,
|
||||
"dataset_names": [
|
||||
"DAR",
|
||||
"Fast"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T14:14:02.563422",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T14:14:02.648401",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: DAR",
|
||||
"details": {
|
||||
"dataset_id": "DAR",
|
||||
"variables_count": 3,
|
||||
"streaming_count": 2,
|
||||
"prefix": "gateway_phoenix"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T14:14:02.666461",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: Fast",
|
||||
"details": {
|
||||
"dataset_id": "Fast",
|
||||
"variables_count": 2,
|
||||
"streaming_count": 0,
|
||||
"prefix": "fast"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T14:14:02.681756",
|
||||
"level": "info",
|
||||
"event_type": "csv_recording_started",
|
||||
"message": "CSV recording started: 2 datasets activated",
|
||||
"details": {
|
||||
"activated_datasets": 2,
|
||||
"total_datasets": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T14:54:09.121582",
|
||||
"level": "info",
|
||||
"event_type": "csv_recording_stopped",
|
||||
"message": "CSV recording stopped (dataset threads continue for UDP streaming)",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T14:54:09.135579",
|
||||
"level": "info",
|
||||
"event_type": "udp_streaming_stopped",
|
||||
"message": "UDP streaming to PlotJuggler stopped (CSV recording continues)",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T14:54:09.381016",
|
||||
"level": "info",
|
||||
"event_type": "dataset_deactivated",
|
||||
"message": "Dataset deactivated: DAR",
|
||||
"details": {
|
||||
"dataset_id": "DAR"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T14:54:09.738004",
|
||||
"level": "info",
|
||||
"event_type": "dataset_deactivated",
|
||||
"message": "Dataset deactivated: Fast",
|
||||
"details": {
|
||||
"dataset_id": "Fast"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T14:54:09.753196",
|
||||
"level": "info",
|
||||
"event_type": "plc_disconnection",
|
||||
"message": "Disconnected from PLC 10.1.33.11 (stopped recording and streaming)",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-13T15:03:37.721996",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
}
|
||||
],
|
||||
"last_updated": "2025-08-13T00:19:23.495379",
|
||||
"total_entries": 899
|
||||
"last_updated": "2025-08-13T15:03:37.721996",
|
||||
"total_entries": 987
|
||||
}
|
|
@ -2,17 +2,17 @@
|
|||
"datasets": {
|
||||
"DAR": {
|
||||
"created": "2025-08-08T15:47:18.566053",
|
||||
"enabled": true,
|
||||
"enabled": false,
|
||||
"name": "DAR",
|
||||
"prefix": "gateway_phoenix",
|
||||
"sampling_interval": 1
|
||||
},
|
||||
"Fast": {
|
||||
"created": "2025-08-09T02:06:26.840011",
|
||||
"enabled": true,
|
||||
"enabled": false,
|
||||
"name": "Fast",
|
||||
"prefix": "fast",
|
||||
"sampling_interval": 0.1
|
||||
"sampling_interval": 0.6
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,51 +1,11 @@
|
|||
{
|
||||
"dataset_variables": {
|
||||
"DAR": {
|
||||
"variables": {
|
||||
"UR29_Brix": {
|
||||
"area": "db",
|
||||
"db": 1011,
|
||||
"offset": 1322,
|
||||
"streaming": true,
|
||||
"type": "real"
|
||||
},
|
||||
"UR29_ma": {
|
||||
"area": "db",
|
||||
"db": 1011,
|
||||
"offset": 1296,
|
||||
"streaming": true,
|
||||
"type": "real"
|
||||
},
|
||||
"fUR29_Brix": {
|
||||
"area": "db",
|
||||
"db": 1011,
|
||||
"offset": 1322,
|
||||
"streaming": false,
|
||||
"type": "real"
|
||||
}
|
||||
},
|
||||
"streaming_variables": [
|
||||
"UR29_Brix",
|
||||
"UR29_ma"
|
||||
]
|
||||
"variables": {},
|
||||
"streaming_variables": []
|
||||
},
|
||||
"Fast": {
|
||||
"variables": {
|
||||
"fUR29_Brix": {
|
||||
"area": "db",
|
||||
"db": 1011,
|
||||
"offset": 1322,
|
||||
"streaming": false,
|
||||
"type": "real"
|
||||
},
|
||||
"fUR29_ma": {
|
||||
"area": "db",
|
||||
"db": 1011,
|
||||
"offset": 1296,
|
||||
"streaming": false,
|
||||
"type": "real"
|
||||
}
|
||||
},
|
||||
"variables": {},
|
||||
"streaming_variables": []
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,23 +1,31 @@
|
|||
{
|
||||
"csv_config": {
|
||||
"cleanup_interval_hours": {
|
||||
"ui:widget": "updown"
|
||||
"ui:widget": "updown",
|
||||
"ui:column": 3
|
||||
},
|
||||
"last_cleanup": {
|
||||
"ui:column": 3
|
||||
},
|
||||
"last_cleanup": {},
|
||||
"max_days": {
|
||||
"ui:widget": "updown"
|
||||
"ui:widget": "updown",
|
||||
"ui:column": 2
|
||||
},
|
||||
"max_hours": {
|
||||
"ui:widget": "updown"
|
||||
"ui:widget": "updown",
|
||||
"ui:column": 2
|
||||
},
|
||||
"max_size_mb": {
|
||||
"ui:widget": "updown"
|
||||
"ui:widget": "updown",
|
||||
"ui:column": 2
|
||||
},
|
||||
"records_directory": {
|
||||
"ui:placeholder": "records"
|
||||
"ui:placeholder": "records",
|
||||
"ui:column": 10
|
||||
},
|
||||
"rotation_enabled": {
|
||||
"ui:widget": "checkbox"
|
||||
"ui:widget": "checkbox",
|
||||
"ui:column": 2
|
||||
},
|
||||
"ui:layout": [
|
||||
[
|
||||
|
@ -25,10 +33,6 @@
|
|||
"name": "cleanup_interval_hours",
|
||||
"width": 3
|
||||
},
|
||||
{
|
||||
"name": "last_cleanup",
|
||||
"width": 3
|
||||
},
|
||||
{
|
||||
"name": "max_days",
|
||||
"width": 2
|
||||
|
@ -40,16 +44,20 @@
|
|||
{
|
||||
"name": "max_size_mb",
|
||||
"width": 2
|
||||
},
|
||||
{
|
||||
"name": "last_cleanup",
|
||||
"width": 3
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"name": "records_directory",
|
||||
"width": 10
|
||||
},
|
||||
{
|
||||
"name": "rotation_enabled",
|
||||
"width": 2
|
||||
},
|
||||
{
|
||||
"name": "records_directory",
|
||||
"width": 10
|
||||
}
|
||||
]
|
||||
],
|
||||
|
@ -61,20 +69,21 @@
|
|||
"max_size_mb",
|
||||
"records_directory",
|
||||
"rotation_enabled"
|
||||
]
|
||||
],
|
||||
"ui:column": 12
|
||||
},
|
||||
"plc_config": {
|
||||
"ip": {
|
||||
"ui:placeholder": "192.168.1.100",
|
||||
"ui:column": 6
|
||||
"ui:column": 6,
|
||||
"ui:placeholder": "192.168.1.100"
|
||||
},
|
||||
"rack": {
|
||||
"ui:widget": "updown",
|
||||
"ui:column": 3
|
||||
"ui:column": 3,
|
||||
"ui:widget": "updown"
|
||||
},
|
||||
"slot": {
|
||||
"ui:widget": "updown",
|
||||
"ui:column": 3
|
||||
"ui:column": 3,
|
||||
"ui:widget": "updown"
|
||||
},
|
||||
"ui:layout": [
|
||||
[
|
||||
|
@ -96,10 +105,12 @@
|
|||
"ip",
|
||||
"rack",
|
||||
"slot"
|
||||
]
|
||||
],
|
||||
"ui:column": 6
|
||||
},
|
||||
"sampling_interval": {
|
||||
"ui:widget": "updown"
|
||||
"ui:widget": "updown",
|
||||
"ui:column": 2
|
||||
},
|
||||
"udp_config": {
|
||||
"host": {
|
||||
|
@ -123,8 +134,15 @@
|
|||
"ui:order": [
|
||||
"host",
|
||||
"port"
|
||||
]
|
||||
],
|
||||
"ui:column": 4
|
||||
},
|
||||
"ui:order": [
|
||||
"csv_config",
|
||||
"plc_config",
|
||||
"sampling_interval",
|
||||
"udp_config"
|
||||
],
|
||||
"ui:layout": [
|
||||
[
|
||||
{
|
||||
|
@ -133,24 +151,18 @@
|
|||
},
|
||||
{
|
||||
"name": "udp_config",
|
||||
"width": 6
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"name": "csv_config",
|
||||
"width": 10
|
||||
"width": 4
|
||||
},
|
||||
{
|
||||
"name": "sampling_interval",
|
||||
"width": 2
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"name": "csv_config",
|
||||
"width": 12
|
||||
}
|
||||
]
|
||||
],
|
||||
"ui:order": [
|
||||
"csv_config",
|
||||
"plc_config",
|
||||
"sampling_interval",
|
||||
"udp_config"
|
||||
]
|
||||
}
|
|
@ -13,7 +13,9 @@ const ChartjsPlot = ({ session, height = '400px' }) => {
|
|||
insertNaNOnNextIngest: false,
|
||||
isPaused: false,
|
||||
isRealTimeMode: true,
|
||||
refreshRate: 1000
|
||||
refreshRate: 1000,
|
||||
userOverrideUntil: 0,
|
||||
userPaused: false
|
||||
});
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
@ -93,15 +95,32 @@ const ChartjsPlot = ({ session, height = '400px' }) => {
|
|||
|
||||
const Chart = window.Chart;
|
||||
|
||||
// Register zoom plugin if available
|
||||
// Ensure zoom plugin is registered only if available to avoid plugin errors
|
||||
let zoomAvailable = false;
|
||||
try {
|
||||
if (window.ChartZoom && !Chart.registry.plugins.get('zoom')) {
|
||||
Chart.register(window.ChartZoom);
|
||||
if (Chart.registry?.plugins?.get?.('zoom')) {
|
||||
zoomAvailable = true;
|
||||
} else {
|
||||
const maybeZoom = (window.ChartZoom || window.chartjsPluginZoom || window.chartjs_plugin_zoom || window.chartjsPluginZoomDefault || (window['chartjs-plugin-zoom'] && window['chartjs-plugin-zoom'].default));
|
||||
if (maybeZoom) {
|
||||
Chart.register(maybeZoom);
|
||||
zoomAvailable = true;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('⚠️ Zoom plugin registration failed:', e);
|
||||
}
|
||||
|
||||
// Ensure realtime scale is available (no fallback)
|
||||
try {
|
||||
const hasRealtimeScale = !!(Chart.registry?.scales?.get && Chart.registry.scales.get('realtime'));
|
||||
if (!hasRealtimeScale) {
|
||||
throw new Error('chartjs-plugin-streaming (realtime scale) not loaded');
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error('Realtime scale not available. Ensure chartjs-plugin-streaming v2.x is loaded after Chart.js.');
|
||||
}
|
||||
|
||||
const ctx = canvasRef.current.getContext('2d');
|
||||
|
||||
// Destroy existing chart
|
||||
|
@ -135,6 +154,9 @@ const ChartjsPlot = ({ session, height = '400px' }) => {
|
|||
sessionDataRef.current.datasetIndex.set(variableInfo.name, index);
|
||||
});
|
||||
|
||||
const yMinInitial = (typeof config.y_min === 'number' && isFinite(config.y_min)) ? config.y_min : undefined;
|
||||
const yMaxInitial = (typeof config.y_max === 'number' && isFinite(config.y_max)) ? config.y_max : undefined;
|
||||
|
||||
const chartConfig = {
|
||||
type: 'line',
|
||||
data: { datasets },
|
||||
|
@ -181,8 +203,8 @@ const ChartjsPlot = ({ session, height = '400px' }) => {
|
|||
display: true,
|
||||
text: 'Valor'
|
||||
},
|
||||
min: config.y_min,
|
||||
max: config.y_max
|
||||
min: yMinInitial,
|
||||
max: yMaxInitial
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
|
@ -194,17 +216,13 @@ const ChartjsPlot = ({ session, height = '400px' }) => {
|
|||
mode: 'index',
|
||||
intersect: false
|
||||
},
|
||||
zoom: {
|
||||
pan: {
|
||||
enabled: true,
|
||||
mode: 'x'
|
||||
},
|
||||
...(zoomAvailable ? {
|
||||
zoom: {
|
||||
pinch: { enabled: true },
|
||||
wheel: { enabled: true },
|
||||
mode: 'x'
|
||||
// Evita listeners wheel/touch no-passive del plugin; usa drag + pan con modificador
|
||||
pan: { enabled: true, mode: 'x', modifierKey: 'shift' },
|
||||
zoom: { drag: { enabled: true }, wheel: { enabled: false }, pinch: { enabled: false }, mode: 'x' }
|
||||
}
|
||||
}
|
||||
} : {})
|
||||
},
|
||||
interaction: {
|
||||
mode: 'nearest',
|
||||
|
@ -224,40 +242,14 @@ const ChartjsPlot = ({ session, height = '400px' }) => {
|
|||
}
|
||||
};
|
||||
|
||||
// Try to create chart with realtime scale
|
||||
try {
|
||||
chartRef.current = new Chart(ctx, chartConfig);
|
||||
sessionDataRef.current.isRealTimeMode = true;
|
||||
console.log(`✅ Plot ${session.session_id}: Real-time Streaming enabled`);
|
||||
} catch (e) {
|
||||
console.warn(`⚠️ Plot ${session.session_id}: Real-time scale not available. Falling back to time scale.`, e);
|
||||
|
||||
// Fallback configuration without realtime
|
||||
chartConfig.options.scales.x = {
|
||||
type: 'time',
|
||||
time: {
|
||||
unit: 'second',
|
||||
displayFormats: {
|
||||
second: 'HH:mm:ss'
|
||||
}
|
||||
},
|
||||
ticks: {
|
||||
display: true,
|
||||
autoSkip: true,
|
||||
maxRotation: 0
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Tiempo'
|
||||
}
|
||||
};
|
||||
|
||||
chartRef.current = new Chart(ctx, chartConfig);
|
||||
sessionDataRef.current.isRealTimeMode = false;
|
||||
|
||||
// Start manual refresh for fallback mode
|
||||
startManualRefresh();
|
||||
}
|
||||
chartRef.current = new Chart(ctx, chartConfig);
|
||||
sessionDataRef.current.isRealTimeMode = true;
|
||||
sessionDataRef.current.noDataCycles = 0;
|
||||
// Sync ingest pause state with initial chart pause
|
||||
const initialPaused = !session.is_active || session.is_paused;
|
||||
sessionDataRef.current.ingestPaused = initialPaused;
|
||||
sessionDataRef.current.isPaused = initialPaused;
|
||||
console.log(`✅ Plot ${session.session_id}: Real-time Streaming enabled`);
|
||||
|
||||
setIsLoading(false);
|
||||
setError(null);
|
||||
|
@ -290,22 +282,65 @@ const ChartjsPlot = ({ session, height = '400px' }) => {
|
|||
const plotData = await response.json();
|
||||
|
||||
// Add new data to chart
|
||||
addNewDataToStreaming(plotData, now);
|
||||
const pointsAdded = addNewDataToStreaming(plotData, now);
|
||||
updatePointsCounter(plotData);
|
||||
|
||||
// Auto-pause when no data arrives for several cycles; resume when data appears
|
||||
if (pointsAdded > 0) {
|
||||
sessionDataRef.current.noDataCycles = 0;
|
||||
// Do not auto-resume if user explicitly paused
|
||||
if (sessionDataRef.current.isPaused && !sessionDataRef.current.userPaused) {
|
||||
const rt = chart.options?.scales?.x?.realtime;
|
||||
if (rt) rt.pause = false;
|
||||
chart.update('none');
|
||||
sessionDataRef.current.isPaused = false;
|
||||
sessionDataRef.current.ingestPaused = false;
|
||||
}
|
||||
} else {
|
||||
sessionDataRef.current.noDataCycles = (sessionDataRef.current.noDataCycles || 0) + 1;
|
||||
if (sessionDataRef.current.noDataCycles >= 3) {
|
||||
const rt = chart.options?.scales?.x?.realtime;
|
||||
if (rt) rt.pause = true;
|
||||
chart.update('none');
|
||||
sessionDataRef.current.isPaused = true;
|
||||
sessionDataRef.current.ingestPaused = true;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`📈 Error in streaming refresh for ${session.session_id}:`, error);
|
||||
}
|
||||
}, [session?.session_id]);
|
||||
|
||||
const addNewDataToStreaming = useCallback((plotData, timestamp) => {
|
||||
if (!chartRef.current || !plotData) return;
|
||||
if (!chartRef.current || !plotData) return 0;
|
||||
|
||||
const chart = chartRef.current;
|
||||
const sessionData = sessionDataRef.current;
|
||||
|
||||
// Check if paused
|
||||
if (sessionData.ingestPaused) return;
|
||||
if (sessionData.ingestPaused) return 0;
|
||||
|
||||
// Helpers to parse flexible payloads
|
||||
const getYValue = (point) => {
|
||||
const candidate = (point?.y !== undefined) ? point.y : (point?.value !== undefined ? point.value : point?.v);
|
||||
const yNum = typeof candidate === 'number' ? candidate : Number(candidate);
|
||||
return Number.isFinite(yNum) ? yNum : null;
|
||||
};
|
||||
const getXValueMs = (point) => {
|
||||
let raw = (point?.x !== undefined) ? point.x : (point?.ts !== undefined ? point.ts : (point?.timestamp !== undefined ? point.timestamp : point?.t));
|
||||
if (typeof raw === 'string') {
|
||||
const asNum = Number(raw);
|
||||
if (Number.isFinite(asNum)) raw = asNum; else {
|
||||
const parsed = Date.parse(raw);
|
||||
if (Number.isFinite(parsed)) raw = parsed; else return null;
|
||||
}
|
||||
}
|
||||
if (typeof raw !== 'number' || !Number.isFinite(raw)) return null;
|
||||
// Normalize seconds to milliseconds
|
||||
const xMs = raw < 1e12 ? raw * 1000 : raw;
|
||||
return xMs;
|
||||
};
|
||||
|
||||
let pointsAdded = 0;
|
||||
chart.data.datasets.forEach((chartDataset, datasetIndex) => {
|
||||
|
@ -320,12 +355,11 @@ const ChartjsPlot = ({ session, height = '400px' }) => {
|
|||
const newPoints = [];
|
||||
for (let i = 0; i < backendDataset.data.length; i++) {
|
||||
const p = backendDataset.data[i];
|
||||
const yNum = typeof p.y === 'number' ? p.y : Number(p.y);
|
||||
if (!isFinite(yNum)) continue;
|
||||
const yNum = getYValue(p);
|
||||
if (yNum === null) continue;
|
||||
|
||||
let xNum = typeof p.x === 'number' ? p.x : Number(p.x);
|
||||
if (!isFinite(xNum)) continue;
|
||||
if (xNum < 1e12) xNum = xNum * 1000; // seconds -> ms
|
||||
const xNum = getXValueMs(p);
|
||||
if (xNum === null) continue;
|
||||
if (xNum > lastPushedX) newPoints.push({ x: xNum, y: yNum });
|
||||
}
|
||||
|
||||
|
@ -356,10 +390,7 @@ const ChartjsPlot = ({ session, height = '400px' }) => {
|
|||
});
|
||||
|
||||
// Update chart
|
||||
if (!sessionData.isRealTimeMode) {
|
||||
cleanupOldDataFallback();
|
||||
chart.update('quiet');
|
||||
} else if (pointsAdded > 0) {
|
||||
if (pointsAdded > 0) {
|
||||
chart.update('quiet');
|
||||
}
|
||||
|
||||
|
@ -367,22 +398,10 @@ const ChartjsPlot = ({ session, height = '400px' }) => {
|
|||
if (pointsAdded > 0 && sessionData.insertNaNOnNextIngest) {
|
||||
sessionData.insertNaNOnNextIngest = false;
|
||||
}
|
||||
return pointsAdded;
|
||||
}, []);
|
||||
|
||||
const cleanupOldDataFallback = useCallback(() => {
|
||||
if (!chartRef.current || !session?.config) return;
|
||||
|
||||
const chart = chartRef.current;
|
||||
const timeWindow = (session.config.time_window || 60) * 1000;
|
||||
const now = Date.now();
|
||||
const cutoffTime = now - timeWindow;
|
||||
|
||||
chart.data.datasets.forEach((dataset) => {
|
||||
if (dataset.data && dataset.data.length > 0) {
|
||||
dataset.data = dataset.data.filter(point => point.x > cutoffTime);
|
||||
}
|
||||
});
|
||||
}, [session?.config]);
|
||||
// Fallback cleanup removed per requirement: always realtime; we pause instead when no data
|
||||
|
||||
const updatePointsCounter = useCallback((plotData) => {
|
||||
const totalPoints = plotData.data_points_count || 0;
|
||||
|
@ -410,11 +429,11 @@ const ChartjsPlot = ({ session, height = '400px' }) => {
|
|||
|
||||
if (sessionData.isRealTimeMode) {
|
||||
const chart = chartRef.current;
|
||||
const xScale = chart.scales?.x;
|
||||
if (xScale?.realtime) {
|
||||
xScale.realtime.pause = true;
|
||||
const rt = chart.options?.scales?.x?.realtime;
|
||||
if (rt) {
|
||||
rt.pause = true;
|
||||
}
|
||||
chart.update('quiet');
|
||||
chart.update('none');
|
||||
} else {
|
||||
if (sessionData.manualInterval) {
|
||||
clearInterval(sessionData.manualInterval);
|
||||
|
@ -424,6 +443,7 @@ const ChartjsPlot = ({ session, height = '400px' }) => {
|
|||
|
||||
sessionData.ingestPaused = true;
|
||||
sessionData.isPaused = true;
|
||||
sessionData.userOverrideUntil = Date.now() + 3000;
|
||||
}, []);
|
||||
|
||||
const resumeStreaming = useCallback(() => {
|
||||
|
@ -432,11 +452,11 @@ const ChartjsPlot = ({ session, height = '400px' }) => {
|
|||
|
||||
if (sessionData.isRealTimeMode) {
|
||||
const chart = chartRef.current;
|
||||
const xScale = chart.scales?.x;
|
||||
if (xScale?.realtime) {
|
||||
xScale.realtime.pause = false;
|
||||
const rt = chart.options?.scales?.x?.realtime;
|
||||
if (rt) {
|
||||
rt.pause = false;
|
||||
}
|
||||
chart.update('quiet');
|
||||
chart.update('none');
|
||||
} else {
|
||||
if (!sessionData.manualInterval) {
|
||||
startManualRefresh();
|
||||
|
@ -446,6 +466,7 @@ const ChartjsPlot = ({ session, height = '400px' }) => {
|
|||
sessionData.insertNaNOnNextIngest = true;
|
||||
sessionData.ingestPaused = false;
|
||||
sessionData.isPaused = false;
|
||||
sessionData.userOverrideUntil = Date.now() + 3000;
|
||||
}, [startManualRefresh]);
|
||||
|
||||
const clearChart = useCallback(() => {
|
||||
|
@ -476,11 +497,15 @@ const ChartjsPlot = ({ session, height = '400px' }) => {
|
|||
// Update chart when session status changes
|
||||
useEffect(() => {
|
||||
if (!chartRef.current || !session) return;
|
||||
// Respect short user override window to avoid flapping when backend state is stale
|
||||
if (Date.now() < (sessionDataRef.current.userOverrideUntil || 0)) return;
|
||||
|
||||
const shouldPause = !session.is_active || session.is_paused;
|
||||
if (shouldPause) {
|
||||
sessionDataRef.current.userPaused = true;
|
||||
pauseStreaming();
|
||||
} else {
|
||||
sessionDataRef.current.userPaused = false;
|
||||
resumeStreaming();
|
||||
}
|
||||
}, [session?.is_active, session?.is_paused, pauseStreaming, resumeStreaming]);
|
||||
|
@ -533,10 +558,17 @@ const ChartjsPlot = ({ session, height = '400px' }) => {
|
|||
createStreamingChart();
|
||||
}
|
||||
return () => {
|
||||
if (chartRef.current) {
|
||||
chartRef.current.destroy();
|
||||
chartRef.current = null;
|
||||
}
|
||||
try {
|
||||
if (chartRef.current) {
|
||||
const rt = chartRef.current.options?.scales?.x?.realtime;
|
||||
if (rt) {
|
||||
rt.pause = true;
|
||||
chartRef.current.update('none');
|
||||
}
|
||||
chartRef.current.destroy();
|
||||
chartRef.current = null;
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
if (sessionDataRef.current.manualInterval) {
|
||||
clearInterval(sessionDataRef.current.manualInterval);
|
||||
}
|
||||
|
@ -597,7 +629,10 @@ const ChartjsPlot = ({ session, height = '400px' }) => {
|
|||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: '6px'
|
||||
borderRadius: '6px',
|
||||
touchAction: 'none',
|
||||
WebkitUserSelect: 'none',
|
||||
userSelect: 'none'
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
|
|
|
@ -17,13 +17,13 @@ import {
|
|||
} from '@chakra-ui/react'
|
||||
// No necesitamos Form completo, solo FormTable
|
||||
import FormTable from './FormTable.jsx'
|
||||
import { getSchema, readConfig, writeConfig } from '../services/api.js'
|
||||
import { getSchema, readConfig, writeConfig, activateDataset, deactivateDataset } from '../services/api.js'
|
||||
|
||||
/**
|
||||
* DatasetCompleteManager - Gestiona datasets y variables de forma simplificada
|
||||
* Incluye: tabla de datasets individuales + variables (sin campos estáticos de configuración)
|
||||
*/
|
||||
export default function DatasetCompleteManager() {
|
||||
export default function DatasetCompleteManager({ status }) {
|
||||
const [fullData, setFullData] = useState({})
|
||||
const [datasetVariables, setDatasetVariables] = useState({})
|
||||
const [selectedDatasetId, setSelectedDatasetId] = useState('')
|
||||
|
@ -38,11 +38,55 @@ export default function DatasetCompleteManager() {
|
|||
const [message, setMessage] = useState('')
|
||||
|
||||
const muted = useColorModeValue('gray.600', 'gray.300')
|
||||
const [liveValues, setLiveValues] = useState({})
|
||||
const sseRef = React.useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
loadData()
|
||||
}, [])
|
||||
|
||||
// Subscribe to SSE for live variable values of the selected dataset
|
||||
useEffect(() => {
|
||||
// Close previous stream
|
||||
if (sseRef.current) {
|
||||
try { sseRef.current.close() } catch { /* ignore */ }
|
||||
sseRef.current = null
|
||||
}
|
||||
|
||||
// Only stream when online and dataset selected
|
||||
const plcConnected = !!status?.plc_connected
|
||||
if (!plcConnected || !selectedDatasetId) {
|
||||
setLiveValues({})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const es = new EventSource(`/api/stream/variables?dataset_id=${encodeURIComponent(selectedDatasetId)}&interval=1.0`)
|
||||
sseRef.current = es
|
||||
es.onmessage = (evt) => {
|
||||
try {
|
||||
const payload = JSON.parse(evt.data)
|
||||
if (payload?.type === 'values' && payload.values) {
|
||||
setLiveValues(payload.values || {})
|
||||
} else if (payload?.type === 'no_cache' || payload?.type === 'dataset_inactive' || payload?.type === 'plc_disconnected') {
|
||||
setLiveValues({})
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
es.onerror = () => {
|
||||
try { es.close() } catch { /* ignore */ }
|
||||
sseRef.current = null
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
|
||||
return () => {
|
||||
if (sseRef.current) {
|
||||
try { sseRef.current.close() } catch { /* ignore */ }
|
||||
sseRef.current = null
|
||||
}
|
||||
}
|
||||
}, [status?.plc_connected, selectedDatasetId])
|
||||
|
||||
const loadData = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
|
@ -66,7 +110,8 @@ export default function DatasetCompleteManager() {
|
|||
const variableSchemaPath = variableSchemaResp.schema?.properties?.dataset_variables?.additionalProperties?.properties?.variables?.additionalProperties
|
||||
const variableUiSchemaPath = variableSchemaResp.ui_schema?.dataset_variables?.additionalProperties?.variables?.additionalProperties
|
||||
|
||||
setVariableSchema(variableSchemaPath)
|
||||
// FormTable requiere un schema con additionalProperties en la raíz
|
||||
setVariableSchema(variableSchemaPath ? { additionalProperties: variableSchemaPath } : null)
|
||||
setVariableUiSchema(variableUiSchemaPath || {})
|
||||
|
||||
setFullData(datasetDataResp.data || {})
|
||||
|
@ -106,11 +151,30 @@ export default function DatasetCompleteManager() {
|
|||
|
||||
const saveDatasets = async (newDatasets) => {
|
||||
try {
|
||||
// Solo enviar datasets, el backend calcula active_datasets automáticamente
|
||||
const newFullData = {
|
||||
datasets: newDatasets
|
||||
// Detectar cambios en "enabled" para llamar a endpoints de activación, evitando estados inconsistentes
|
||||
const prev = fullData.datasets || {}
|
||||
const changedIds = []
|
||||
for (const [id, cfg] of Object.entries(newDatasets)) {
|
||||
const before = prev[id]?.enabled === true
|
||||
const after = cfg?.enabled === true
|
||||
if (before !== after) changedIds.push({ id, after })
|
||||
}
|
||||
|
||||
// Persistir datasets primero
|
||||
const newFullData = { datasets: newDatasets }
|
||||
await saveFullData(newFullData)
|
||||
|
||||
// Aplicar activación/desactivación en backend para arrancar/parar hilos
|
||||
await Promise.allSettled(changedIds.map(({ id, after }) => after ? activateDataset(id) : deactivateDataset(id)))
|
||||
|
||||
// Refrescar selección y datos locales tras cambios
|
||||
setFullData(newFullData)
|
||||
if (selectedDatasetId && !newDatasets[selectedDatasetId]) {
|
||||
const ids = Object.keys(newDatasets)
|
||||
setSelectedDatasetId(ids[0] || '')
|
||||
}
|
||||
setMessage('Datasets saved and activation applied')
|
||||
setTimeout(() => setMessage(''), 2000)
|
||||
} catch (error) {
|
||||
console.error('Error saving datasets:', error)
|
||||
setMessage(`Error saving datasets: ${error.message}`)
|
||||
|
@ -231,6 +295,7 @@ export default function DatasetCompleteManager() {
|
|||
onChange={saveDatasetVariables}
|
||||
title={`Variables for: ${fullData.datasets?.[selectedDatasetId]?.name || selectedDatasetId}`}
|
||||
keyField="name"
|
||||
liveValues={liveValues}
|
||||
/>
|
||||
) : (
|
||||
<Alert status="warning">
|
||||
|
|
|
@ -34,11 +34,13 @@ export default function FormTable({
|
|||
title = "Data",
|
||||
keyField = "id",
|
||||
allowAdd = true,
|
||||
allowDelete = true
|
||||
allowDelete = true,
|
||||
liveValues = null
|
||||
}) {
|
||||
const [editingKey, setEditingKey] = useState(null)
|
||||
const [addingNew, setAddingNew] = useState(false)
|
||||
const [newKey, setNewKey] = useState('')
|
||||
const [editingFormData, setEditingFormData] = useState(null)
|
||||
|
||||
const muted = useColorModeValue('gray.600', 'gray.300')
|
||||
const borderColor = useColorModeValue('gray.200', 'gray.600')
|
||||
|
@ -173,6 +175,11 @@ export default function FormTable({
|
|||
<HStack justify="space-between">
|
||||
<HStack>
|
||||
<Heading size="xs">{key}</Heading>
|
||||
{data[key] && typeof data[key].enabled === 'boolean' && (
|
||||
<Badge colorScheme={data[key].enabled ? 'green' : 'red'} size="sm">
|
||||
{data[key].enabled ? 'Enabled' : 'Disabled'}
|
||||
</Badge>
|
||||
)}
|
||||
{editingKey === key && (
|
||||
<Badge colorScheme="orange" size="sm">Editing</Badge>
|
||||
)}
|
||||
|
@ -192,7 +199,7 @@ export default function FormTable({
|
|||
icon={<EditIcon />}
|
||||
size="xs"
|
||||
variant="outline"
|
||||
onClick={() => setEditingKey(key)}
|
||||
onClick={() => { setEditingFormData({ ...(data[key] || {}) }); setEditingKey(key) }}
|
||||
/>
|
||||
{allowDelete && (
|
||||
<IconButton
|
||||
|
@ -209,12 +216,19 @@ export default function FormTable({
|
|||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody pt={0}>
|
||||
{liveValues && liveValues[key] !== undefined && (
|
||||
<Box mb={2}>
|
||||
<Text fontSize="sm" color={muted}>
|
||||
Live value: <Text as="span" fontWeight="semibold">{String(liveValues[key])}</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Form
|
||||
schema={itemSchema}
|
||||
uiSchema={itemUiSchema}
|
||||
formData={data[key] || {}}
|
||||
formData={editingKey === key ? (editingFormData || {}) : (data[key] || {})}
|
||||
validator={validator}
|
||||
onChange={editingKey === key ? undefined : () => { }}
|
||||
onChange={editingKey === key ? ({ formData }) => setEditingFormData(formData) : () => { }}
|
||||
onSubmit={editingKey === key ? ({ formData }) => handleEdit(key, formData) : undefined}
|
||||
templates={{ ObjectFieldTemplate: LayoutObjectFieldTemplate }}
|
||||
widgets={widgets}
|
||||
|
@ -226,7 +240,7 @@ export default function FormTable({
|
|||
<Button type="submit" size="sm" colorScheme="blue">
|
||||
Save
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" onClick={() => setEditingKey(null)}>
|
||||
<Button size="sm" variant="ghost" onClick={() => { setEditingKey(null); setEditingFormData(null) }}>
|
||||
Cancel
|
||||
</Button>
|
||||
</HStack>
|
||||
|
|
|
@ -0,0 +1,215 @@
|
|||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import {
|
||||
Box,
|
||||
VStack,
|
||||
HStack,
|
||||
Text,
|
||||
Button,
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
Heading,
|
||||
useColorModeValue,
|
||||
Badge,
|
||||
IconButton,
|
||||
Divider,
|
||||
Spacer,
|
||||
} from '@chakra-ui/react'
|
||||
import { EditIcon, SettingsIcon, DeleteIcon } from '@chakra-ui/icons'
|
||||
import ChartjsPlot from './ChartjsPlot.jsx'
|
||||
|
||||
export default function PlotRealtimeViewer() {
|
||||
const [sessions, setSessions] = useState(new Map())
|
||||
const [loading, setLoading] = useState(false)
|
||||
const intervalRef = useRef(null)
|
||||
const muted = useColorModeValue('gray.600', 'gray.300')
|
||||
|
||||
const loadSessions = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const res = await fetch('/api/plots')
|
||||
const data = await res.json()
|
||||
if (data && data.sessions) {
|
||||
setSessions(prev => {
|
||||
const next = new Map(prev)
|
||||
const incomingIds = new Set()
|
||||
for (const s of data.sessions) {
|
||||
incomingIds.add(s.session_id)
|
||||
const existing = next.get(s.session_id)
|
||||
if (existing) {
|
||||
// Mutate existing object to preserve reference
|
||||
existing.name = s.name
|
||||
existing.is_active = s.is_active
|
||||
existing.is_paused = s.is_paused
|
||||
existing.variables_count = s.variables_count
|
||||
} else {
|
||||
next.set(s.session_id, { ...s })
|
||||
}
|
||||
}
|
||||
// Remove sessions not present anymore
|
||||
for (const id of Array.from(next.keys())) {
|
||||
if (!incomingIds.has(id)) next.delete(id)
|
||||
}
|
||||
return next
|
||||
})
|
||||
} else {
|
||||
setSessions(new Map())
|
||||
}
|
||||
} catch {
|
||||
setSessions(new Map())
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const refreshSession = async (sessionId) => {
|
||||
try {
|
||||
const res = await fetch(`/api/plots/${sessionId}/config`)
|
||||
const data = await res.json()
|
||||
if (data && data.success && data.config) {
|
||||
setSessions(prev => {
|
||||
const n = new Map(prev)
|
||||
const existing = n.get(sessionId)
|
||||
const varsCount = Array.isArray(data.config.variables)
|
||||
? data.config.variables.length
|
||||
: (data.config.variables ? Object.keys(data.config.variables).length : (existing?.variables_count || 0))
|
||||
if (existing) {
|
||||
existing.name = data.config.name
|
||||
existing.is_active = data.config.is_active
|
||||
existing.is_paused = data.config.is_paused
|
||||
existing.variables_count = varsCount
|
||||
} else {
|
||||
n.set(sessionId, {
|
||||
session_id: sessionId,
|
||||
name: data.config.name,
|
||||
is_active: data.config.is_active,
|
||||
is_paused: data.config.is_paused,
|
||||
variables_count: varsCount,
|
||||
})
|
||||
}
|
||||
return n
|
||||
})
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
const controlSession = async (sessionId, action) => {
|
||||
try {
|
||||
await fetch(`/api/plots/${sessionId}/control`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action }),
|
||||
})
|
||||
await refreshSession(sessionId)
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadSessions()
|
||||
intervalRef.current = setInterval(loadSessions, 5000)
|
||||
return () => { if (intervalRef.current) clearInterval(intervalRef.current) }
|
||||
}, [])
|
||||
|
||||
const sessionsList = useMemo(() => Array.from(sessions.values()), [sessions])
|
||||
|
||||
if (loading && sessionsList.length === 0) {
|
||||
return <Text color={muted}>Cargando sesiones de plots…</Text>
|
||||
}
|
||||
|
||||
if (sessionsList.length === 0) {
|
||||
return (
|
||||
<Card>
|
||||
<CardBody>
|
||||
<Text color={muted}>No hay sesiones de plot. Cree o edite plots en la sección superior.</Text>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<VStack spacing={4} align="stretch">
|
||||
{sessionsList.map((session) => (
|
||||
<PlotRealtimeCard
|
||||
key={session.session_id}
|
||||
session={session}
|
||||
onControl={controlSession}
|
||||
onRefresh={refreshSession}
|
||||
/>
|
||||
))}
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
function PlotRealtimeCard({ session, onControl, onRefresh }) {
|
||||
const cardBg = useColorModeValue('white', 'gray.700')
|
||||
const borderColor = useColorModeValue('gray.200', 'gray.600')
|
||||
const muted = useColorModeValue('gray.600', 'gray.300')
|
||||
const chartControlsRef = useRef(null)
|
||||
|
||||
const handleChartReady = (controls) => {
|
||||
chartControlsRef.current = controls
|
||||
}
|
||||
|
||||
const enhancedSession = {
|
||||
...session,
|
||||
onChartReady: handleChartReady,
|
||||
}
|
||||
|
||||
const handleControlClick = async (action) => {
|
||||
if (chartControlsRef.current) {
|
||||
switch (action) {
|
||||
case 'pause':
|
||||
chartControlsRef.current.pauseStreaming()
|
||||
break
|
||||
case 'start':
|
||||
case 'resume':
|
||||
chartControlsRef.current.resumeStreaming()
|
||||
break
|
||||
case 'clear':
|
||||
chartControlsRef.current.clearChart()
|
||||
break
|
||||
case 'stop':
|
||||
chartControlsRef.current.pauseStreaming()
|
||||
break
|
||||
}
|
||||
}
|
||||
// No esperar a que el backend responda para aplicar efecto local
|
||||
onControl(session.session_id, action)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card bg={cardBg} borderColor={borderColor}>
|
||||
<CardHeader>
|
||||
<FlexHeader session={session} muted={muted} onRefresh={() => onRefresh(session.session_id)} />
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<ChartjsPlot session={enhancedSession} height="360px" />
|
||||
<HStack mt={3} spacing={2}>
|
||||
<Button size="sm" onClick={() => handleControlClick('start')} colorScheme="green">▶️ Start</Button>
|
||||
<Button size="sm" onClick={() => handleControlClick('pause')} colorScheme="yellow">⏸️ Pause</Button>
|
||||
<Button size="sm" onClick={() => handleControlClick('clear')} variant="outline">🗑️ Clear</Button>
|
||||
<Button size="sm" onClick={() => handleControlClick('stop')} colorScheme="red">⏹️ Stop</Button>
|
||||
</HStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
function FlexHeader({ session, muted, onRefresh }) {
|
||||
return (
|
||||
<HStack align="center">
|
||||
<Box>
|
||||
<Heading size="sm">📈 {session.name || session.session_id}</Heading>
|
||||
<Text fontSize="sm" color={muted} mt={1}>
|
||||
Variables: {session.variables_count || 0} | Status: <strong>{session.is_active ? (session.is_paused ? 'Paused' : 'Active') : 'Stopped'}</strong>
|
||||
</Text>
|
||||
</Box>
|
||||
<Spacer />
|
||||
<HStack>
|
||||
<IconButton icon={<SettingsIcon />} size="sm" variant="outline" aria-label="Refresh status" onClick={onRefresh} />
|
||||
</HStack>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState, useEffect, useMemo } from 'react'
|
||||
import React, { useState, useEffect, useMemo, useRef } from 'react'
|
||||
import {
|
||||
FormControl, FormLabel, FormHelperText, Select, VStack, HStack,
|
||||
Text, Badge, Box, Icon, Input, useColorModeValue, Spinner
|
||||
|
@ -14,6 +14,9 @@ export function VariableSelectorWidget(props) {
|
|||
const [loading, setLoading] = useState(true)
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [selectedDataset, setSelectedDataset] = useState('all')
|
||||
const [liveValue, setLiveValue] = useState(undefined)
|
||||
const [liveStatus, setLiveStatus] = useState('idle')
|
||||
const esRef = useRef(null)
|
||||
|
||||
const borderColor = useColorModeValue('gray.300', 'gray.600')
|
||||
const focusBorderColor = useColorModeValue('blue.500', 'blue.300')
|
||||
|
@ -63,6 +66,58 @@ export function VariableSelectorWidget(props) {
|
|||
return variables
|
||||
}, [datasetVariables])
|
||||
|
||||
// Subscribe to SSE for live value of selected variable
|
||||
useEffect(() => {
|
||||
// close previous stream
|
||||
if (esRef.current) {
|
||||
try { esRef.current.close() } catch { /* ignore */ }
|
||||
esRef.current = null
|
||||
}
|
||||
|
||||
const variable = value && allVariables.find(v => v.name === value)
|
||||
const datasetId = variable?.dataset
|
||||
if (!datasetId || !value) {
|
||||
setLiveValue(undefined)
|
||||
setLiveStatus('idle')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const es = new EventSource(`/api/stream/variables?dataset_id=${encodeURIComponent(datasetId)}&interval=1.0`)
|
||||
esRef.current = es
|
||||
setLiveStatus('connecting')
|
||||
|
||||
es.onmessage = (evt) => {
|
||||
try {
|
||||
const payload = JSON.parse(evt.data)
|
||||
if (payload?.type === 'values' && payload.values) {
|
||||
setLiveValue(payload.values[value])
|
||||
setLiveStatus('ok')
|
||||
} else if (payload?.type === 'no_cache') {
|
||||
setLiveStatus('waiting')
|
||||
} else if (payload?.type === 'plc_disconnected' || payload?.type === 'dataset_inactive') {
|
||||
setLiveValue(undefined)
|
||||
setLiveStatus('offline')
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
es.onerror = () => {
|
||||
try { es.close() } catch { /* ignore */ }
|
||||
esRef.current = null
|
||||
setLiveStatus('error')
|
||||
}
|
||||
} catch {
|
||||
setLiveStatus('error')
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (esRef.current) {
|
||||
try { esRef.current.close() } catch { /* ignore */ }
|
||||
esRef.current = null
|
||||
}
|
||||
}
|
||||
}, [value, allVariables])
|
||||
|
||||
// Filter variables based on search term and selected dataset
|
||||
const filteredVariables = useMemo(() => {
|
||||
let filtered = allVariables
|
||||
|
@ -222,6 +277,11 @@ export function VariableSelectorWidget(props) {
|
|||
PLC Address: {selectedVariable.area}{selectedVariable.db ? `${selectedVariable.db}.` : ''}{selectedVariable.offset}
|
||||
{selectedVariable.streaming ? ' • Real-time streaming enabled' : ' • Static logging only'}
|
||||
</Text>
|
||||
<Text fontSize="sm">
|
||||
Live value: {liveStatus === 'ok' && liveValue !== undefined ? (
|
||||
<Text as="span" fontWeight="semibold">{String(liveValue)}</Text>
|
||||
) : liveStatus === 'waiting' ? 'waiting…' : liveStatus === 'offline' ? 'offline' : liveStatus === 'error' ? 'error' : '—'}
|
||||
</Text>
|
||||
</VStack>
|
||||
</Box>
|
||||
)}
|
||||
|
|
|
@ -7,6 +7,7 @@ import LayoutObjectFieldTemplate from '../components/rjsf/LayoutObjectFieldTempl
|
|||
import { widgets } from '../components/rjsf/widgets.jsx'
|
||||
import DatasetCompleteManager from '../components/DatasetCompleteManager.jsx'
|
||||
import PlotCompleteManager from '../components/PlotCompleteManager.jsx'
|
||||
import PlotRealtimeViewer from '../components/PlotRealtimeViewer.jsx'
|
||||
import PLCConfigManager from '../components/PLCConfigManager.jsx'
|
||||
import {
|
||||
getStatus,
|
||||
|
@ -240,7 +241,7 @@ export default function DashboardPage() {
|
|||
<Flex wrap="wrap" gap={2} align="center" mb={3}>
|
||||
<Text fontWeight="semibold" textTransform="uppercase">📊 Dataset Management</Text>
|
||||
</Flex>
|
||||
<DatasetCompleteManager />
|
||||
<DatasetCompleteManager status={status} />
|
||||
</CardBody>
|
||||
</Card>
|
||||
)}
|
||||
|
@ -253,6 +254,10 @@ export default function DashboardPage() {
|
|||
<Text fontWeight="semibold" textTransform="uppercase">📈 Plot Management</Text>
|
||||
</Flex>
|
||||
<PlotCompleteManager />
|
||||
<Box mt={4}>
|
||||
<Text fontWeight="semibold" mb={2}>🔴 Real-time plots</Text>
|
||||
<PlotRealtimeViewer />
|
||||
</Box>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)}
|
||||
|
|
|
@ -604,8 +604,8 @@ function PlotSessionPanel({ session, onControl, onRemove, onEdit, onUpdateStatus
|
|||
}
|
||||
}
|
||||
|
||||
// Then handle backend control
|
||||
await onControl(session.session_id, action)
|
||||
// Then handle backend control (no await to avoid UI lag)
|
||||
onControl(session.session_id, action)
|
||||
}
|
||||
|
||||
const handleChartReady = (controls) => {
|
||||
|
|
|
@ -8,8 +8,26 @@ function toJsonOrThrow(res) {
|
|||
}
|
||||
|
||||
export async function getStatus() {
|
||||
const res = await fetch(`${BASE_URL}/api/status`, { headers: { 'Accept': 'application/json' } })
|
||||
return toJsonOrThrow(res)
|
||||
try {
|
||||
const res = await fetch(`${BASE_URL}/api/status`, { headers: { 'Accept': 'application/json' } })
|
||||
return toJsonOrThrow(res)
|
||||
} catch (err) {
|
||||
// Fallback: try health endpoint and synthesize a minimal status to keep UI usable
|
||||
try {
|
||||
const healthRes = await fetch(`${BASE_URL}/api/health`, { headers: { 'Accept': 'application/json' } })
|
||||
if (healthRes.ok) {
|
||||
const health = await healthRes.json()
|
||||
return {
|
||||
plc_connected: false,
|
||||
streaming: false,
|
||||
csv_recording: false,
|
||||
disk_space_info: null,
|
||||
health,
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
export async function getEvents(limit = 50) {
|
||||
|
@ -88,4 +106,21 @@ export async function putJson(path, body) {
|
|||
return toJsonOrThrow(res)
|
||||
}
|
||||
|
||||
// Datasets activation control
|
||||
export async function activateDataset(datasetId) {
|
||||
const res = await fetch(`${BASE_URL}/api/datasets/${encodeURIComponent(datasetId)}/activate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Accept': 'application/json' },
|
||||
})
|
||||
return toJsonOrThrow(res)
|
||||
}
|
||||
|
||||
export async function deactivateDataset(datasetId) {
|
||||
const res = await fetch(`${BASE_URL}/api/datasets/${encodeURIComponent(datasetId)}/deactivate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Accept': 'application/json' },
|
||||
})
|
||||
return toJsonOrThrow(res)
|
||||
}
|
||||
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -1,12 +1,9 @@
|
|||
{
|
||||
"last_state": {
|
||||
"should_connect": true,
|
||||
"should_connect": false,
|
||||
"should_stream": false,
|
||||
"active_datasets": [
|
||||
"Fast",
|
||||
"DAR"
|
||||
]
|
||||
"active_datasets": []
|
||||
},
|
||||
"auto_recovery_enabled": true,
|
||||
"last_update": "2025-08-13T00:09:19.091418"
|
||||
"last_update": "2025-08-13T14:54:09.753196"
|
||||
}
|
Loading…
Reference in New Issue