feat: Implement multi-browser support for plot sessions
- Enhanced session ID generation to include browser tab identifiers, allowing multiple instances of the same plot in different tabs. - Updated PlotManager to manage sessions more effectively, including cleanup of inactive sessions. - Modified frontend components to handle dynamic session IDs and ensure independent operation across tabs. - Added new API endpoints for retrieving sessions by plot ID and manual cleanup of inactive sessions. - Improved error handling and logging for better debugging and user experience.
This commit is contained in:
parent
405edd682e
commit
e97cd5260b
|
@ -0,0 +1,64 @@
|
|||
# Multi-Browser Plot Fix - Resumen de Cambios
|
||||
|
||||
## Problema Original
|
||||
Cuando se abría el mismo plot en múltiples pestañas del mismo navegador, ambas instancias se bloqueaban y dejaban de funcionar. Funcionaba bien en navegadores diferentes pero no en pestañas del mismo navegador.
|
||||
|
||||
## Análisis del Problema
|
||||
El problema era que aunque el backend generaba session IDs únicos, el frontend seguía consultando con el `plot_id` original, causando que la segunda pestaña encontrara la sesión de la primera pestaña y asumiera que era suya.
|
||||
|
||||
## Solución Implementada
|
||||
|
||||
### 1. Backend Changes (core/plot_manager.py)
|
||||
- **Session IDs únicos mejorados**: Ahora incluyen `browser_tab_id` en el formato: `{plot_id}_{browser_tab_id}_{timestamp}_{counter}`
|
||||
- **Búsqueda por plot_id**: El método `get_session_config` ahora puede buscar por session_id único O por plot_id (backward compatibility)
|
||||
- **Limpieza automática**: Sesiones inactivas se limpian automáticamente cada 5 minutos
|
||||
|
||||
### 2. Frontend Changes (PlotRealtimeSession.jsx)
|
||||
- **Browser Tab ID único**: Cada instancia genera un ID único: `tab_{timestamp}_{random}`
|
||||
- **Session ID management**: Se mantiene el `actualSessionId` devuelto por el backend
|
||||
- **No auto-refresh inicial**: Se eliminó el auto-refresh al cargar para evitar conflictos
|
||||
- **Identificador visual**: Se muestra el Tab ID y Session ID para debugging
|
||||
|
||||
### 3. API Changes (main.py)
|
||||
- **Soporte para browser_tab_id**: El endpoint de creación acepta `browser_tab_id`
|
||||
- **Nuevos endpoints**:
|
||||
- `/api/plots/sessions/{plot_id}` - Lista sesiones de un plot
|
||||
- `/api/plots/cleanup` - Limpieza manual de sesiones
|
||||
|
||||
## Flujo de Trabajo Actualizado
|
||||
|
||||
### Primera Pestaña:
|
||||
1. Genera `browserTabId` único
|
||||
2. Crea sesión con `browser_tab_id` incluido
|
||||
3. Recibe `actualSessionId` único del backend
|
||||
4. Usa `actualSessionId` para todas las operaciones
|
||||
|
||||
### Segunda Pestaña:
|
||||
1. Genera su propio `browserTabId` único
|
||||
2. No busca sesiones existentes al inicio
|
||||
3. Crea su propia sesión independiente
|
||||
4. Recibe su propio `actualSessionId` único
|
||||
5. Opera completamente independiente de la primera pestaña
|
||||
|
||||
## Beneficios
|
||||
- ✅ Múltiples pestañas del mismo navegador funcionan independientemente
|
||||
- ✅ Múltiples navegadores siguen funcionando
|
||||
- ✅ No hay conflictos de estado entre instancias
|
||||
- ✅ Limpieza automática previene acumulación de sesiones
|
||||
- ✅ Debugging mejorado con IDs visibles
|
||||
- ✅ Backward compatibility mantenida
|
||||
|
||||
## Testing
|
||||
Para probar:
|
||||
1. Abrir plot en primera pestaña
|
||||
2. Hacer clic en "Start"
|
||||
3. Abrir segunda pestaña del mismo navegador
|
||||
4. Abrir el mismo plot
|
||||
5. Hacer clic en "Start" en segunda pestaña
|
||||
6. Ambos plots deben funcionar independientemente
|
||||
|
||||
## Archivos Modificados
|
||||
- `core/plot_manager.py` - Session management mejorado
|
||||
- `frontend/src/components/PlotRealtimeSession.jsx` - Tab ID y session management
|
||||
- `main.py` - API endpoints actualizados
|
||||
- `MULTI_BROWSER_SUPPORT.md` - Documentación
|
|
@ -0,0 +1,75 @@
|
|||
# Multi-Browser Plot Support
|
||||
|
||||
## Problema Resuelto
|
||||
Anteriormente no era posible tener el mismo plot activo en múltiples navegadores simultáneamente. Cuando se intentaba esto, ambas instancias se bloqueaban y dejaban de funcionar.
|
||||
|
||||
## Solución Implementada
|
||||
|
||||
### 1. Session IDs Únicos
|
||||
- **Antes**: El sistema usaba `plotDefinition.id` directamente como `session_id`
|
||||
- **Ahora**: Se generan session IDs únicos con formato: `{plot_id}_{timestamp}_{counter}`
|
||||
|
||||
### 2. Gestión de Sesiones Múltiples
|
||||
- Cada navegador/pestaña puede crear su propia sesión independiente
|
||||
- Las sesiones se distinguen por timestamps únicos
|
||||
- Se incluye limpieza automática de sesiones inactivas
|
||||
|
||||
### 3. API Mejorada
|
||||
|
||||
#### Nuevo Parámetro en POST /api/plots
|
||||
```json
|
||||
{
|
||||
"id": "plot_1",
|
||||
"name": "Mi Plot",
|
||||
"variables": ["var1", "var2"],
|
||||
"allow_multiple": true // Permite múltiples sesiones (default: true)
|
||||
}
|
||||
```
|
||||
|
||||
#### Nuevos Endpoints
|
||||
- `GET /api/plots/sessions/{plot_id}` - Lista todas las sesiones de un plot
|
||||
- `POST /api/plots/cleanup` - Limpieza manual de sesiones inactivas
|
||||
|
||||
### 4. Frontend Actualizado
|
||||
- El componente `PlotRealtimeSession` ahora maneja session IDs dinámicos
|
||||
- Se muestra información del session ID real cuando es diferente del plot ID
|
||||
- Mejor gestión de estado para sesiones independientes
|
||||
|
||||
### 5. Limpieza Automática
|
||||
- Las sesiones inactivas se limpian automáticamente cada 5 minutos
|
||||
- Sesiones mayores a 1 hora sin actividad son eliminadas por defecto
|
||||
- Evita acumulación de sesiones obsoletas
|
||||
|
||||
## Uso
|
||||
|
||||
### Para Permitir Múltiples Instancias (Comportamiento por Defecto)
|
||||
```javascript
|
||||
await api.createPlot({
|
||||
id: "plot_1",
|
||||
name: "Mi Plot",
|
||||
variables: ["var1", "var2"],
|
||||
allow_multiple: true // Permite múltiples navegadores
|
||||
})
|
||||
```
|
||||
|
||||
### Para Reutilizar Sesión Existente
|
||||
```javascript
|
||||
await api.createPlot({
|
||||
id: "plot_1",
|
||||
name: "Mi Plot",
|
||||
variables: ["var1", "var2"],
|
||||
allow_multiple: false // Reutiliza sesión activa si existe
|
||||
})
|
||||
```
|
||||
|
||||
## Beneficios
|
||||
1. **Múltiples Navegadores**: Cada usuario puede tener su propia instancia del mismo plot
|
||||
2. **Sin Conflictos**: Las sesiones no se interfieren entre sí
|
||||
3. **Limpieza Automática**: No hay acumulación de sesiones obsoletas
|
||||
4. **Compatibilidad**: Mantiene compatibilidad con código existente
|
||||
5. **Flexibilidad**: Se puede elegir entre múltiples sesiones o reutilización
|
||||
|
||||
## Consideraciones
|
||||
- Cada sesión adicional consume memoria
|
||||
- La limpieza automática evita acumulación excesiva
|
||||
- El frontend mantiene referencia al session ID real para comunicación correcta con el backend
|
|
@ -6025,8 +6025,587 @@
|
|||
"trigger_variable": null,
|
||||
"auto_started": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T15:49:22.566965",
|
||||
"level": "info",
|
||||
"event_type": "plot_session_created",
|
||||
"message": "Plot session 'UR29' created and started",
|
||||
"details": {
|
||||
"session_id": "plot_1",
|
||||
"variables": [
|
||||
"UR29_Brix",
|
||||
"UR29_ma",
|
||||
"AUX Blink_1.0S",
|
||||
"AUX Blink_1.6S"
|
||||
],
|
||||
"time_window": 36,
|
||||
"trigger_variable": null,
|
||||
"auto_started": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T15:49:32.233072",
|
||||
"level": "info",
|
||||
"event_type": "plot_session_created",
|
||||
"message": "Plot session 'UR29' created and started",
|
||||
"details": {
|
||||
"session_id": "plot_1",
|
||||
"variables": [
|
||||
"UR29_Brix",
|
||||
"UR29_ma",
|
||||
"AUX Blink_1.0S",
|
||||
"AUX Blink_1.6S"
|
||||
],
|
||||
"time_window": 36,
|
||||
"trigger_variable": null,
|
||||
"auto_started": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T15:49:39.683760",
|
||||
"level": "info",
|
||||
"event_type": "plot_session_created",
|
||||
"message": "Plot session 'UR29' created and started",
|
||||
"details": {
|
||||
"session_id": "plot_1",
|
||||
"variables": [
|
||||
"UR29_Brix",
|
||||
"UR29_ma",
|
||||
"AUX Blink_1.0S",
|
||||
"AUX Blink_1.6S"
|
||||
],
|
||||
"time_window": 36,
|
||||
"trigger_variable": null,
|
||||
"auto_started": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T15:51:11.724780",
|
||||
"level": "info",
|
||||
"event_type": "plot_session_created",
|
||||
"message": "Plot session 'Clock' created and started",
|
||||
"details": {
|
||||
"session_id": "Clock",
|
||||
"variables": [
|
||||
"AUX Blink_1.0S",
|
||||
"AUX Blink_1.6S"
|
||||
],
|
||||
"time_window": 10,
|
||||
"trigger_variable": null,
|
||||
"auto_started": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T15:57:54.453324",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T15:57:54.518452",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: DAR",
|
||||
"details": {
|
||||
"dataset_id": "DAR",
|
||||
"variables_count": 2,
|
||||
"streaming_count": 2,
|
||||
"prefix": "gateway_phoenix"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T15:57:54.525479",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: Fast",
|
||||
"details": {
|
||||
"dataset_id": "Fast",
|
||||
"variables_count": 2,
|
||||
"streaming_count": 1,
|
||||
"prefix": "fast"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T15:57:54.532986",
|
||||
"level": "info",
|
||||
"event_type": "csv_recording_started",
|
||||
"message": "CSV recording started: 2 datasets activated",
|
||||
"details": {
|
||||
"activated_datasets": 2,
|
||||
"total_datasets": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T15:59:56.276302",
|
||||
"level": "info",
|
||||
"event_type": "plot_session_created",
|
||||
"message": "Plot session 'UR29' created and started",
|
||||
"details": {
|
||||
"session_id": "plot_1_1755266396276_2",
|
||||
"variables": [
|
||||
"UR29_Brix",
|
||||
"UR29_ma",
|
||||
"AUX Blink_1.0S",
|
||||
"AUX Blink_1.6S"
|
||||
],
|
||||
"time_window": 36,
|
||||
"trigger_variable": null,
|
||||
"auto_started": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T16:01:51.200918",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T16:01:51.248414",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: DAR",
|
||||
"details": {
|
||||
"dataset_id": "DAR",
|
||||
"variables_count": 2,
|
||||
"streaming_count": 2,
|
||||
"prefix": "gateway_phoenix"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T16:01:51.256921",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: Fast",
|
||||
"details": {
|
||||
"dataset_id": "Fast",
|
||||
"variables_count": 2,
|
||||
"streaming_count": 1,
|
||||
"prefix": "fast"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T16:01:51.263933",
|
||||
"level": "info",
|
||||
"event_type": "csv_recording_started",
|
||||
"message": "CSV recording started: 2 datasets activated",
|
||||
"details": {
|
||||
"activated_datasets": 2,
|
||||
"total_datasets": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T16:01:51.344460",
|
||||
"level": "info",
|
||||
"event_type": "plot_session_created",
|
||||
"message": "Plot session 'UR29' created and started",
|
||||
"details": {
|
||||
"session_id": "plot_1_1755266511344_2",
|
||||
"variables": [
|
||||
"UR29_Brix",
|
||||
"UR29_ma",
|
||||
"AUX Blink_1.0S",
|
||||
"AUX Blink_1.6S"
|
||||
],
|
||||
"time_window": 36,
|
||||
"trigger_variable": null,
|
||||
"auto_started": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T16:02:02.915970",
|
||||
"level": "info",
|
||||
"event_type": "plot_session_created",
|
||||
"message": "Plot session 'UR29' created and started",
|
||||
"details": {
|
||||
"session_id": "plot_1_1755266522914_3",
|
||||
"variables": [
|
||||
"UR29_Brix",
|
||||
"UR29_ma",
|
||||
"AUX Blink_1.0S",
|
||||
"AUX Blink_1.6S"
|
||||
],
|
||||
"time_window": 36,
|
||||
"trigger_variable": null,
|
||||
"auto_started": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T16:03:04.548242",
|
||||
"level": "info",
|
||||
"event_type": "plot_session_created",
|
||||
"message": "Plot session 'Clock' created and started",
|
||||
"details": {
|
||||
"session_id": "Clock_1755266584547_4",
|
||||
"variables": [
|
||||
"AUX Blink_1.0S",
|
||||
"AUX Blink_1.6S"
|
||||
],
|
||||
"time_window": 10,
|
||||
"trigger_variable": null,
|
||||
"auto_started": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T16:05:00.699266",
|
||||
"level": "info",
|
||||
"event_type": "plot_session_created",
|
||||
"message": "Plot session 'UR29' created and started",
|
||||
"details": {
|
||||
"session_id": "plot_1_1755266700699_5",
|
||||
"variables": [
|
||||
"UR29_Brix",
|
||||
"UR29_ma",
|
||||
"AUX Blink_1.0S",
|
||||
"AUX Blink_1.6S"
|
||||
],
|
||||
"time_window": 36,
|
||||
"trigger_variable": null,
|
||||
"auto_started": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T16:24:41.195883",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T16:24:41.246019",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: DAR",
|
||||
"details": {
|
||||
"dataset_id": "DAR",
|
||||
"variables_count": 2,
|
||||
"streaming_count": 2,
|
||||
"prefix": "gateway_phoenix"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T16:24:41.258139",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: Fast",
|
||||
"details": {
|
||||
"dataset_id": "Fast",
|
||||
"variables_count": 2,
|
||||
"streaming_count": 1,
|
||||
"prefix": "fast"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T16:24:41.265238",
|
||||
"level": "info",
|
||||
"event_type": "csv_recording_started",
|
||||
"message": "CSV recording started: 2 datasets activated",
|
||||
"details": {
|
||||
"activated_datasets": 2,
|
||||
"total_datasets": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T16:24:53.119430",
|
||||
"level": "info",
|
||||
"event_type": "plot_session_created",
|
||||
"message": "Plot session 'UR29' created and started",
|
||||
"details": {
|
||||
"session_id": "plot_1_1755267893118_2",
|
||||
"variables": [
|
||||
"UR29_Brix",
|
||||
"UR29_ma",
|
||||
"AUX Blink_1.0S",
|
||||
"AUX Blink_1.6S"
|
||||
],
|
||||
"time_window": 36,
|
||||
"trigger_variable": null,
|
||||
"auto_started": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T16:30:28.630568",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T16:30:28.678921",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: DAR",
|
||||
"details": {
|
||||
"dataset_id": "DAR",
|
||||
"variables_count": 2,
|
||||
"streaming_count": 2,
|
||||
"prefix": "gateway_phoenix"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T16:30:28.688439",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: Fast",
|
||||
"details": {
|
||||
"dataset_id": "Fast",
|
||||
"variables_count": 2,
|
||||
"streaming_count": 1,
|
||||
"prefix": "fast"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T16:30:28.696892",
|
||||
"level": "info",
|
||||
"event_type": "csv_recording_started",
|
||||
"message": "CSV recording started: 2 datasets activated",
|
||||
"details": {
|
||||
"activated_datasets": 2,
|
||||
"total_datasets": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T16:34:16.615010",
|
||||
"level": "info",
|
||||
"event_type": "plot_session_created",
|
||||
"message": "Plot session 'UR29' created and started",
|
||||
"details": {
|
||||
"session_id": "plot_1_1755268456615_2",
|
||||
"variables": [
|
||||
"UR29_Brix",
|
||||
"UR29_ma",
|
||||
"AUX Blink_1.0S",
|
||||
"AUX Blink_1.6S"
|
||||
],
|
||||
"time_window": 36,
|
||||
"trigger_variable": null,
|
||||
"auto_started": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T16:35:22.569951",
|
||||
"level": "info",
|
||||
"event_type": "plot_session_created",
|
||||
"message": "Plot session 'UR29' created and started",
|
||||
"details": {
|
||||
"session_id": "plot_1_1755268522569_3",
|
||||
"variables": [
|
||||
"UR29_Brix",
|
||||
"UR29_ma",
|
||||
"AUX Blink_1.0S",
|
||||
"AUX Blink_1.6S"
|
||||
],
|
||||
"time_window": 36,
|
||||
"trigger_variable": null,
|
||||
"auto_started": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T16:35:23.234508",
|
||||
"level": "info",
|
||||
"event_type": "plot_session_created",
|
||||
"message": "Plot session 'UR29' created and started",
|
||||
"details": {
|
||||
"session_id": "plot_1_1755268523232_4",
|
||||
"variables": [
|
||||
"UR29_Brix",
|
||||
"UR29_ma",
|
||||
"AUX Blink_1.0S",
|
||||
"AUX Blink_1.6S"
|
||||
],
|
||||
"time_window": 36,
|
||||
"trigger_variable": null,
|
||||
"auto_started": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T16:35:23.251273",
|
||||
"level": "info",
|
||||
"event_type": "plot_session_created",
|
||||
"message": "Plot session 'UR29' created and started",
|
||||
"details": {
|
||||
"session_id": "plot_1_1755268523250_5",
|
||||
"variables": [
|
||||
"UR29_Brix",
|
||||
"UR29_ma",
|
||||
"AUX Blink_1.0S",
|
||||
"AUX Blink_1.6S"
|
||||
],
|
||||
"time_window": 36,
|
||||
"trigger_variable": null,
|
||||
"auto_started": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T16:40:08.612042",
|
||||
"level": "info",
|
||||
"event_type": "plot_session_created",
|
||||
"message": "Plot session 'UR29' created and started",
|
||||
"details": {
|
||||
"session_id": "plot_1_1755268808612_6",
|
||||
"variables": [
|
||||
"UR29_Brix",
|
||||
"UR29_ma",
|
||||
"AUX Blink_1.0S",
|
||||
"AUX Blink_1.6S"
|
||||
],
|
||||
"time_window": 36,
|
||||
"trigger_variable": null,
|
||||
"auto_started": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T16:40:15.632779",
|
||||
"level": "info",
|
||||
"event_type": "plot_session_created",
|
||||
"message": "Plot session 'UR29' created and started",
|
||||
"details": {
|
||||
"session_id": "plot_1_1755268815631_7",
|
||||
"variables": [
|
||||
"UR29_Brix",
|
||||
"UR29_ma",
|
||||
"AUX Blink_1.0S",
|
||||
"AUX Blink_1.6S"
|
||||
],
|
||||
"time_window": 36,
|
||||
"trigger_variable": null,
|
||||
"auto_started": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T16:45:25.963734",
|
||||
"level": "info",
|
||||
"event_type": "plot_session_created",
|
||||
"message": "Plot session 'UR29' created and started",
|
||||
"details": {
|
||||
"session_id": "plot_1_1755269125963_8",
|
||||
"variables": [
|
||||
"UR29_Brix",
|
||||
"UR29_ma",
|
||||
"AUX Blink_1.0S",
|
||||
"AUX Blink_1.6S"
|
||||
],
|
||||
"time_window": 36,
|
||||
"trigger_variable": null,
|
||||
"auto_started": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T16:48:01.283698",
|
||||
"level": "info",
|
||||
"event_type": "plot_session_created",
|
||||
"message": "Plot session 'UR29' created and started",
|
||||
"details": {
|
||||
"session_id": "plot_1_1755269281282_9",
|
||||
"variables": [
|
||||
"UR29_Brix",
|
||||
"UR29_ma",
|
||||
"AUX Blink_1.0S",
|
||||
"AUX Blink_1.6S"
|
||||
],
|
||||
"time_window": 36,
|
||||
"trigger_variable": null,
|
||||
"auto_started": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T16:48:01.644316",
|
||||
"level": "info",
|
||||
"event_type": "plot_session_created",
|
||||
"message": "Plot session 'UR29' created and started",
|
||||
"details": {
|
||||
"session_id": "plot_1_1755269281644_10",
|
||||
"variables": [
|
||||
"UR29_Brix",
|
||||
"UR29_ma",
|
||||
"AUX Blink_1.0S",
|
||||
"AUX Blink_1.6S"
|
||||
],
|
||||
"time_window": 36,
|
||||
"trigger_variable": null,
|
||||
"auto_started": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T16:50:35.251305",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T16:50:35.316564",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: DAR",
|
||||
"details": {
|
||||
"dataset_id": "DAR",
|
||||
"variables_count": 2,
|
||||
"streaming_count": 2,
|
||||
"prefix": "gateway_phoenix"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T16:50:35.324572",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: Fast",
|
||||
"details": {
|
||||
"dataset_id": "Fast",
|
||||
"variables_count": 2,
|
||||
"streaming_count": 1,
|
||||
"prefix": "fast"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T16:50:35.333019",
|
||||
"level": "info",
|
||||
"event_type": "csv_recording_started",
|
||||
"message": "CSV recording started: 2 datasets activated",
|
||||
"details": {
|
||||
"activated_datasets": 2,
|
||||
"total_datasets": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T16:50:37.736391",
|
||||
"level": "info",
|
||||
"event_type": "plot_session_created",
|
||||
"message": "Plot session 'UR29' created and started",
|
||||
"details": {
|
||||
"session_id": "plot_1_1755269437735_2",
|
||||
"variables": [
|
||||
"UR29_Brix",
|
||||
"UR29_ma",
|
||||
"AUX Blink_1.0S",
|
||||
"AUX Blink_1.6S"
|
||||
],
|
||||
"time_window": 36,
|
||||
"trigger_variable": null,
|
||||
"auto_started": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-15T16:51:06.118418",
|
||||
"level": "info",
|
||||
"event_type": "plot_session_created",
|
||||
"message": "Plot session 'UR29' created and started",
|
||||
"details": {
|
||||
"session_id": "plot_1_1755269466118_3",
|
||||
"variables": [
|
||||
"UR29_Brix",
|
||||
"UR29_ma",
|
||||
"AUX Blink_1.0S",
|
||||
"AUX Blink_1.6S"
|
||||
],
|
||||
"time_window": 36,
|
||||
"trigger_variable": null,
|
||||
"auto_started": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"last_updated": "2025-08-15T15:48:55.687372",
|
||||
"total_entries": 499
|
||||
"last_updated": "2025-08-15T16:51:06.118418",
|
||||
"total_entries": 540
|
||||
}
|
|
@ -7,7 +7,7 @@
|
|||
"point_hover_radius": 4,
|
||||
"point_radius": 2.5,
|
||||
"stacked": true,
|
||||
"stepped": false,
|
||||
"stepped": true,
|
||||
"time_window": 36,
|
||||
"trigger_enabled": false,
|
||||
"trigger_on_true": true,
|
||||
|
|
|
@ -26,6 +26,8 @@ class PlotSession:
|
|||
|
||||
def __init__(self, session_id: str, config: Dict[str, Any]):
|
||||
self.session_id = session_id
|
||||
# Store original plot ID for reference and grouping
|
||||
self.plot_id = config.get("id", "unknown")
|
||||
self.name = config.get("name", f"Plot {session_id}")
|
||||
|
||||
# Handle new variable structure with colors and enabled state
|
||||
|
@ -225,6 +227,7 @@ class PlotSession:
|
|||
|
||||
return {
|
||||
"session_id": self.session_id,
|
||||
"plot_id": self.plot_id, # Include original plot ID for reference
|
||||
"name": self.name,
|
||||
"datasets": datasets,
|
||||
"y_min": y_min,
|
||||
|
@ -243,6 +246,7 @@ class PlotSession:
|
|||
"""Obtener estado de la sesión"""
|
||||
return {
|
||||
"session_id": self.session_id,
|
||||
"plot_id": self.plot_id, # Include original plot ID for reference
|
||||
"name": self.name,
|
||||
"is_active": self.is_active,
|
||||
"is_paused": self.is_paused,
|
||||
|
@ -275,19 +279,46 @@ class PlotManager:
|
|||
os.path.join(data_dir, "plot_variables.json")
|
||||
)
|
||||
|
||||
# Initialize cleanup timer for inactive sessions
|
||||
self._last_cleanup = time.time()
|
||||
self._cleanup_interval = 300 # 5 minutes
|
||||
|
||||
# Legacy plot file for migration
|
||||
self.plots_file = resource_path(os.path.join(data_dir, "plot_sessions.json"))
|
||||
|
||||
# Load existing plots from disk
|
||||
self.load_plots()
|
||||
|
||||
def create_session(self, config: Dict[str, Any]) -> str:
|
||||
def create_session(self, config: Dict[str, Any], allow_multiple: bool = True) -> str:
|
||||
"""Crear una nueva sesión de plotting"""
|
||||
with self.lock:
|
||||
# Use the ID from config if available, otherwise generate one
|
||||
session_id = config.get("id") or f"plot_{self.session_counter}"
|
||||
if not config.get("id"):
|
||||
self.session_counter += 1
|
||||
base_id = config.get("id") or f"plot_{self.session_counter}"
|
||||
browser_tab_id = config.get("browser_tab_id", "")
|
||||
|
||||
if not allow_multiple:
|
||||
# Check if there's already an active session for this plot
|
||||
existing_sessions = [
|
||||
sid for sid, session in self.sessions.items()
|
||||
if session.plot_id == base_id and session.is_active
|
||||
]
|
||||
|
||||
if existing_sessions:
|
||||
# Return the existing active session ID
|
||||
if self.logger:
|
||||
self.logger.info(f"Returning existing active session for plot {base_id}: {existing_sessions[0]}")
|
||||
return existing_sessions[0]
|
||||
|
||||
# Generate unique session ID to allow multiple browser instances
|
||||
# Format: {plot_id}_{browser_tab_id}_{timestamp}_{counter} to ensure uniqueness
|
||||
timestamp = int(time.time() * 1000) # millisecond precision
|
||||
session_id_parts = [base_id]
|
||||
|
||||
if browser_tab_id:
|
||||
session_id_parts.append(browser_tab_id)
|
||||
|
||||
session_id_parts.extend([str(timestamp), str(self.session_counter)])
|
||||
session_id = "_".join(session_id_parts)
|
||||
self.session_counter += 1
|
||||
|
||||
session = PlotSession(session_id, config)
|
||||
# 🔑 CAMBIO: Crear sesiones en modo activo por defecto para mejor UX
|
||||
|
@ -319,6 +350,40 @@ class PlotManager:
|
|||
|
||||
return session_id
|
||||
|
||||
def get_sessions_by_plot_id(self, plot_id: str) -> List[str]:
|
||||
"""Get all session IDs for a specific plot ID"""
|
||||
with self.lock:
|
||||
return [sid for sid, session in self.sessions.items() if session.plot_id == plot_id]
|
||||
|
||||
def cleanup_inactive_sessions(self, max_age_seconds: int = 3600) -> int:
|
||||
"""Remove inactive sessions older than max_age_seconds"""
|
||||
current_time = time.time()
|
||||
removed_count = 0
|
||||
|
||||
with self.lock:
|
||||
sessions_to_remove = []
|
||||
for session_id, session in self.sessions.items():
|
||||
# Calculate session age from timestamp in session_id
|
||||
try:
|
||||
parts = session_id.split('_')
|
||||
if len(parts) >= 3:
|
||||
timestamp_ms = int(parts[-2])
|
||||
session_age = current_time - (timestamp_ms / 1000.0)
|
||||
if not session.is_active and session_age > max_age_seconds:
|
||||
sessions_to_remove.append(session_id)
|
||||
except (ValueError, IndexError):
|
||||
# Skip sessions with invalid timestamp format
|
||||
continue
|
||||
|
||||
for session_id in sessions_to_remove:
|
||||
del self.sessions[session_id]
|
||||
removed_count += 1
|
||||
|
||||
if removed_count > 0 and self.logger:
|
||||
self.logger.info(f"Cleaned up {removed_count} inactive plot sessions")
|
||||
|
||||
return removed_count
|
||||
|
||||
def remove_session(self, session_id: str) -> bool:
|
||||
"""Eliminar una sesión de plotting"""
|
||||
with self.lock:
|
||||
|
@ -382,6 +447,12 @@ class PlotManager:
|
|||
|
||||
def update_data(self, dataset_id: str, variables_data: Dict[str, Any]) -> None:
|
||||
"""Actualizar datos de todas las sesiones activas usando cache del recording"""
|
||||
# Periodic cleanup of inactive sessions
|
||||
current_time = time.time()
|
||||
if current_time - self._last_cleanup > self._cleanup_interval:
|
||||
self.cleanup_inactive_sessions()
|
||||
self._last_cleanup = current_time
|
||||
|
||||
with self.lock:
|
||||
for session in self.sessions.values():
|
||||
if not session.is_active:
|
||||
|
@ -636,23 +707,45 @@ class PlotManager:
|
|||
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,
|
||||
}
|
||||
# First try direct session_id lookup
|
||||
if session_id in self.sessions:
|
||||
session = self.sessions[session_id]
|
||||
return {
|
||||
"session_id": session_id,
|
||||
"plot_id": session.plot_id, # Include original plot_id for reference
|
||||
"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,
|
||||
}
|
||||
|
||||
# If not found, try to find a session by plot_id (backward compatibility)
|
||||
# This handles cases where frontend queries with plot_id instead of unique session_id
|
||||
for sid, session in self.sessions.items():
|
||||
if session.plot_id == session_id:
|
||||
# Return the first session found for this plot_id
|
||||
return {
|
||||
"session_id": sid, # Return the actual unique session_id
|
||||
"plot_id": session.plot_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,
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
def get_active_sessions_count(self) -> int:
|
||||
"""Obtener número de sesiones activas"""
|
||||
|
|
|
@ -73,10 +73,29 @@ const ChartjsPlot = ({ session, height = '400px' }) => {
|
|||
const chartHealthRef = useRef({
|
||||
lastDataTimestamp: 0,
|
||||
consecutiveErrors: 0,
|
||||
isHealthy: true,
|
||||
lastHealthCheck: 0
|
||||
});
|
||||
|
||||
// Safe Chart.js operation helper
|
||||
const safeChartUpdate = useCallback((chart, updateMode = 'none', operation = 'update') => {
|
||||
if (!chart || !canvasRef.current || !document.contains(canvasRef.current)) {
|
||||
console.warn(`🚫 Safe chart ${operation} aborted - invalid DOM state for session ${sessionDataRef.current.sessionId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!chart.ctx || !chart.canvas || chart.canvas !== canvasRef.current) {
|
||||
console.warn(`🚫 Safe chart ${operation} aborted - chart context mismatch for session ${sessionDataRef.current.sessionId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
chart.update(updateMode);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`📈 Error in chart ${operation} for session ${sessionDataRef.current.sessionId}:`, error);
|
||||
return false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const bgColor = useColorModeValue('white', 'gray.800');
|
||||
const textColor = useColorModeValue('gray.600', 'gray.300');
|
||||
|
||||
|
@ -529,7 +548,11 @@ const ChartjsPlot = ({ session, height = '400px' }) => {
|
|||
const rt = existingChart.options?.scales?.x?.realtime;
|
||||
if (rt) {
|
||||
rt.pause = true;
|
||||
existingChart.update('none');
|
||||
try {
|
||||
existingChart.update('none');
|
||||
} catch (updateError) {
|
||||
console.warn('⚠️ Error updating chart during destruction:', updateError);
|
||||
}
|
||||
}
|
||||
|
||||
// Stop any running animations and timers
|
||||
|
@ -1003,6 +1026,18 @@ const ChartjsPlot = ({ session, height = '400px' }) => {
|
|||
const sessionId = sessionDataRef.current.sessionId;
|
||||
if (!sessionId) return;
|
||||
|
||||
// Enhanced DOM and chart validation
|
||||
if (!chart || !canvasRef.current || !document.contains(canvasRef.current)) {
|
||||
console.warn(`🚫 onStreamingRefresh aborted - invalid DOM state for session ${sessionId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate that chart is still properly initialized and not destroyed
|
||||
if (!chart.ctx || !chart.canvas || chart.canvas !== canvasRef.current) {
|
||||
console.warn(`🚫 onStreamingRefresh aborted - chart context mismatch for session ${sessionId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const now = Date.now();
|
||||
const refreshRate = sessionDataRef.current.refreshRate;
|
||||
|
@ -1031,7 +1066,10 @@ const ChartjsPlot = ({ session, height = '400px' }) => {
|
|||
if (sessionDataRef.current.isPaused && !sessionDataRef.current.userPaused) {
|
||||
const rt = chart.options?.scales?.x?.realtime;
|
||||
if (rt) rt.pause = false;
|
||||
chart.update('none');
|
||||
// Double-check chart validity before update
|
||||
if (chart.ctx && canvasRef.current && document.contains(canvasRef.current)) {
|
||||
chart.update('none');
|
||||
}
|
||||
sessionDataRef.current.isPaused = false;
|
||||
sessionDataRef.current.ingestPaused = false;
|
||||
}
|
||||
|
@ -1040,7 +1078,10 @@ const ChartjsPlot = ({ session, height = '400px' }) => {
|
|||
if (sessionDataRef.current.noDataCycles >= 3) {
|
||||
const rt = chart.options?.scales?.x?.realtime;
|
||||
if (rt) rt.pause = true;
|
||||
chart.update('none');
|
||||
// Double-check chart validity before update
|
||||
if (chart.ctx && canvasRef.current && document.contains(canvasRef.current)) {
|
||||
chart.update('none');
|
||||
}
|
||||
sessionDataRef.current.isPaused = true;
|
||||
sessionDataRef.current.ingestPaused = true;
|
||||
}
|
||||
|
@ -1130,7 +1171,7 @@ const ChartjsPlot = ({ session, height = '400px' }) => {
|
|||
|
||||
// Update chart
|
||||
if (pointsAdded > 0) {
|
||||
chart.update('quiet');
|
||||
safeChartUpdate(chart, 'quiet', 'data ingestion');
|
||||
|
||||
// Update health monitoring
|
||||
chartHealthRef.current.lastDataTimestamp = Date.now();
|
||||
|
@ -1190,7 +1231,7 @@ const ChartjsPlot = ({ session, height = '400px' }) => {
|
|||
if (rt) {
|
||||
rt.pause = true;
|
||||
}
|
||||
chart.update('none');
|
||||
safeChartUpdate(chart, 'none', 'pause streaming');
|
||||
} else {
|
||||
if (sessionData.manualInterval) {
|
||||
clearInterval(sessionData.manualInterval);
|
||||
|
@ -1256,7 +1297,7 @@ const ChartjsPlot = ({ session, height = '400px' }) => {
|
|||
});
|
||||
|
||||
// Update chart with historical data
|
||||
chart.update('quiet');
|
||||
safeChartUpdate(chart, 'quiet', 'historical data load');
|
||||
|
||||
// Update data points counter
|
||||
const totalHistoricalPoints = Object.values(dataByVariable).reduce((sum, points) => sum + points.length, 0);
|
||||
|
@ -1277,7 +1318,7 @@ const ChartjsPlot = ({ session, height = '400px' }) => {
|
|||
if (rt) {
|
||||
rt.pause = false;
|
||||
}
|
||||
chart.update('none');
|
||||
safeChartUpdate(chart, 'none', 'resume streaming');
|
||||
} else {
|
||||
if (!sessionData.manualInterval) {
|
||||
startManualRefresh();
|
||||
|
@ -1545,7 +1586,7 @@ const ChartjsPlot = ({ session, height = '400px' }) => {
|
|||
zoomPlugin.zoom.pinch.enabled = isZoomEnabled;
|
||||
|
||||
// Update the chart to apply the new configuration
|
||||
chart.update('none');
|
||||
safeChartUpdate(chart, 'none', 'zoom configuration');
|
||||
|
||||
console.log(`🔍 Zoom/Pan ${isZoomEnabled ? 'enabled' : 'disabled'}`);
|
||||
}
|
||||
|
@ -1878,6 +1919,7 @@ const ChartjsPlot = ({ session, height = '400px' }) => {
|
|||
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
id={`chart-canvas-${session?.session_id || 'loading'}`}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
|
|
|
@ -54,6 +54,12 @@ export default function PlotRealtimeSession({
|
|||
onConfigUpdate,
|
||||
onReloadConfig // Nueva prop para recargar configuración desde backend
|
||||
}) {
|
||||
// Generate unique browser/tab identifier to avoid conflicts between multiple instances
|
||||
const [browserTabId] = useState(() => {
|
||||
// Use timestamp + random number for uniqueness across tabs/browsers
|
||||
return `tab_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
})
|
||||
|
||||
const [session, setSession] = useState({
|
||||
session_id: plotDefinition.id,
|
||||
name: plotDefinition.name,
|
||||
|
@ -133,9 +139,13 @@ export default function PlotRealtimeSession({
|
|||
const muted = useColorModeValue('gray.600', 'gray.300')
|
||||
const settingsBg = useColorModeValue('gray.50', 'gray.600')
|
||||
|
||||
// State for the actual session ID returned from backend
|
||||
const [actualSessionId, setActualSessionId] = useState(null)
|
||||
|
||||
// Enhanced session object for ChartjsPlot - memoized to prevent recreations
|
||||
const enhancedSession = useMemo(() => ({
|
||||
session_id: plotDefinition.id,
|
||||
session_id: actualSessionId || plotDefinition.id, // Use actual session ID when available
|
||||
plot_id: plotDefinition.id, // Keep original plot ID for reference
|
||||
name: plotDefinition.name,
|
||||
is_active: session.is_active,
|
||||
is_paused: session.is_paused,
|
||||
|
@ -150,6 +160,7 @@ export default function PlotRealtimeSession({
|
|||
chartControlsRef.current = controls
|
||||
}
|
||||
}), [
|
||||
actualSessionId, // Include actualSessionId in dependencies
|
||||
plotDefinition.id,
|
||||
plotDefinition.name,
|
||||
plotDefinition,
|
||||
|
@ -162,8 +173,14 @@ export default function PlotRealtimeSession({
|
|||
|
||||
// Load session status from backend (optional - session may not exist until started)
|
||||
const refreshSessionStatus = useCallback(async () => {
|
||||
// Only refresh if we have an actual session ID
|
||||
if (!actualSessionId) {
|
||||
// No session yet - this is expected for new instances
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await api.getPlotSession(plotDefinition.id)
|
||||
const response = await api.getPlotSession(actualSessionId)
|
||||
if (response?.config) {
|
||||
setSession(prev => ({
|
||||
...prev,
|
||||
|
@ -172,16 +189,21 @@ export default function PlotRealtimeSession({
|
|||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
// Session may not exist in backend yet
|
||||
// Session may not exist in backend yet or may have been cleaned up
|
||||
if (error.message.includes('404')) {
|
||||
// Try to create the session automatically
|
||||
await createPlotSessionFromConfig()
|
||||
// Reset actual session ID - will create new one when needed
|
||||
setActualSessionId(null)
|
||||
setSession(prev => ({
|
||||
...prev,
|
||||
is_active: false,
|
||||
is_paused: false
|
||||
}))
|
||||
} else {
|
||||
// Backend not available - use local state silently
|
||||
// This allows the component to work even when backend is offline
|
||||
// Backend not available - maintain current state
|
||||
console.log('Backend not available for session status check')
|
||||
}
|
||||
}
|
||||
}, [plotDefinition.id])
|
||||
}, [actualSessionId])
|
||||
|
||||
// Create plot session in backend based on static configuration
|
||||
const createPlotSessionFromConfig = useCallback(async () => {
|
||||
|
@ -191,6 +213,7 @@ export default function PlotRealtimeSession({
|
|||
|
||||
const plotConfig = {
|
||||
id: plotDefinition.id, // Use id instead of session_id
|
||||
browser_tab_id: browserTabId, // Include unique tab identifier
|
||||
name: plotDefinition.name,
|
||||
variables: variableNames,
|
||||
time_window: plotDefinition.time_window || 60,
|
||||
|
@ -198,17 +221,24 @@ export default function PlotRealtimeSession({
|
|||
trigger_variable: plotDefinition.trigger_variable,
|
||||
trigger_on_true: plotDefinition.trigger_on_true || true,
|
||||
y_min: plotDefinition.y_min,
|
||||
y_max: plotDefinition.y_max
|
||||
y_max: plotDefinition.y_max,
|
||||
allow_multiple: true // Always allow multiple instances
|
||||
}
|
||||
|
||||
// Create the plot session
|
||||
await api.createPlot(plotConfig)
|
||||
const result = await api.createPlot(plotConfig)
|
||||
|
||||
console.log(`✅ Created plot session: ${plotDefinition.id}`)
|
||||
// Store the actual session ID returned by the backend
|
||||
if (result?.session_id) {
|
||||
setActualSessionId(result.session_id)
|
||||
console.log(`✅ Created plot session: ${result.session_id} for plot: ${plotDefinition.id} (tab: ${browserTabId})`)
|
||||
} else {
|
||||
console.log(`✅ Created plot session: ${plotDefinition.id} (tab: ${browserTabId})`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Could not create plot session ${plotDefinition.id}:`, error)
|
||||
console.warn(`Could not create plot session ${plotDefinition.id} (tab: ${browserTabId}):`, error)
|
||||
}
|
||||
}, [plotDefinition, plotVariables])
|
||||
}, [plotDefinition, plotVariables, browserTabId])
|
||||
|
||||
// Control plot session (start, pause, stop, clear)
|
||||
const handleControlClick = useCallback(async (action) => {
|
||||
|
@ -218,8 +248,9 @@ export default function PlotRealtimeSession({
|
|||
if (action === 'start') {
|
||||
try {
|
||||
// Try to create the plot session with current configuration
|
||||
await api.createPlot({
|
||||
const result = await api.createPlot({
|
||||
id: plotDefinition.id, // Use id instead of session_id
|
||||
browser_tab_id: browserTabId, // Include unique tab identifier
|
||||
name: plotDefinition.name,
|
||||
variables: plotVariables.map(v => v.variable_name), // Simplified format
|
||||
time_window: localConfig.time_window,
|
||||
|
@ -227,26 +258,38 @@ export default function PlotRealtimeSession({
|
|||
trigger_variable: localConfig.trigger_variable,
|
||||
trigger_on_true: localConfig.trigger_on_true,
|
||||
y_min: localConfig.y_min,
|
||||
y_max: localConfig.y_max
|
||||
y_max: localConfig.y_max,
|
||||
allow_multiple: true // Always allow multiple instances
|
||||
})
|
||||
// Store the actual session ID if returned
|
||||
if (result?.session_id) {
|
||||
setActualSessionId(result.session_id)
|
||||
}
|
||||
} catch (createError) {
|
||||
// Plot may already exist, that's OK
|
||||
console.log('Plot session may already exist:', createError.message)
|
||||
}
|
||||
}
|
||||
|
||||
// Use the actual session ID if available, fallback to plot ID
|
||||
const sessionIdToUse = actualSessionId || plotDefinition.id
|
||||
|
||||
// Send control command to backend
|
||||
await api.controlPlotSession(plotDefinition.id, action)
|
||||
await api.controlPlotSession(sessionIdToUse, action)
|
||||
|
||||
// For 'start' action, verify that the session is actually active
|
||||
if (action === 'start') {
|
||||
// Wait a bit and verify the session started
|
||||
await new Promise(resolve => setTimeout(resolve, 300))
|
||||
const verifyResponse = await api.getPlotSession(plotDefinition.id)
|
||||
const verifyResponse = await api.getPlotSession(sessionIdToUse)
|
||||
if (!verifyResponse?.config?.is_active) {
|
||||
// Try the control command once more if not active
|
||||
console.log('Session not active, retrying control command...')
|
||||
await api.controlPlotSession(plotDefinition.id, action)
|
||||
await api.controlPlotSession(sessionIdToUse, action)
|
||||
}
|
||||
// Update session ID if different
|
||||
if (verifyResponse?.config?.session_id && verifyResponse.config.session_id !== plotDefinition.id) {
|
||||
setActualSessionId(verifyResponse.config.session_id)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -446,17 +489,20 @@ export default function PlotRealtimeSession({
|
|||
}
|
||||
}, [plotDefinition.id, refreshSessionStatus, handleControlClick, session.is_active, session.is_paused, onReloadConfig, toast])
|
||||
|
||||
// Auto-refresh session status
|
||||
// Auto-refresh session status only if we have a session
|
||||
useEffect(() => {
|
||||
// Try to get session status first, if it fails, create the session
|
||||
refreshSessionStatus()
|
||||
intervalRef.current = setInterval(refreshSessionStatus, 5000)
|
||||
// Only start auto-refresh if we have an actual session ID
|
||||
if (actualSessionId) {
|
||||
refreshSessionStatus()
|
||||
intervalRef.current = setInterval(refreshSessionStatus, 5000)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current)
|
||||
}
|
||||
}
|
||||
}, [refreshSessionStatus])
|
||||
}, [actualSessionId, refreshSessionStatus])
|
||||
|
||||
return (
|
||||
<Card bg={cardBg} borderColor={borderColor} shadow="md">
|
||||
|
@ -476,6 +522,12 @@ export default function PlotRealtimeSession({
|
|||
<> | Trigger: {localConfig.trigger_variable}</>
|
||||
)}
|
||||
</Text>
|
||||
<Text fontSize="xs" color="gray.500" mt={1}>
|
||||
Tab: {browserTabId}
|
||||
{actualSessionId && actualSessionId !== plotDefinition.id && (
|
||||
<> | Session: {actualSessionId.substring(0, 40)}...</>
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
<Spacer />
|
||||
<HStack>
|
||||
|
|
46
main.py
46
main.py
|
@ -1605,7 +1605,10 @@ def create_plot():
|
|||
if config["y_max"] is not None:
|
||||
config["y_max"] = float(config["y_max"])
|
||||
|
||||
session_id = streamer.data_streamer.plot_manager.create_session(config)
|
||||
# Check if multiple sessions should be allowed (default: True for backward compatibility)
|
||||
allow_multiple = data.get("allow_multiple", True)
|
||||
|
||||
session_id = streamer.data_streamer.plot_manager.create_session(config, allow_multiple)
|
||||
|
||||
return jsonify(
|
||||
{
|
||||
|
@ -2152,6 +2155,47 @@ def get_historical_data():
|
|||
return jsonify({"error": f"Internal server error: {str(e)}"}), 500
|
||||
|
||||
|
||||
@app.route("/api/plots/sessions/<plot_id>", methods=["GET"])
|
||||
def get_plot_sessions(plot_id):
|
||||
"""Get all session IDs for a specific plot ID"""
|
||||
error_response = check_streamer_initialized()
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
try:
|
||||
session_ids = streamer.data_streamer.plot_manager.get_sessions_by_plot_id(plot_id)
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"plot_id": plot_id,
|
||||
"session_ids": session_ids,
|
||||
"session_count": len(session_ids)
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@app.route("/api/plots/cleanup", methods=["POST"])
|
||||
def cleanup_plot_sessions():
|
||||
"""Manually cleanup inactive plot sessions"""
|
||||
error_response = check_streamer_initialized()
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
max_age_seconds = data.get("max_age_seconds", 3600) # Default 1 hour
|
||||
|
||||
removed_count = streamer.data_streamer.plot_manager.cleanup_inactive_sessions(max_age_seconds)
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"removed_sessions": removed_count,
|
||||
"message": f"Cleaned up {removed_count} inactive plot sessions"
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@app.route("/api/status")
|
||||
def get_status():
|
||||
"""Get current status"""
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
"should_connect": true,
|
||||
"should_stream": false,
|
||||
"active_datasets": [
|
||||
"Fast",
|
||||
"DAR",
|
||||
"Fast",
|
||||
"Test"
|
||||
]
|
||||
},
|
||||
"auto_recovery_enabled": true,
|
||||
"last_update": "2025-08-15T15:18:52.911110"
|
||||
"last_update": "2025-08-15T16:50:35.341984"
|
||||
}
|
Loading…
Reference in New Issue