feat: Enhance application event logging and improve historical plot components with new data handling and UI updates

This commit is contained in:
Miguel 2025-08-16 16:53:50 +02:00
parent d588574b4f
commit fe1df15942
5 changed files with 417 additions and 103 deletions

View File

@ -8628,8 +8628,172 @@
"activated_datasets": 2,
"total_datasets": 3
}
},
{
"timestamp": "2025-08-16T16:13:19.529732",
"level": "info",
"event_type": "application_started",
"message": "Application initialization completed successfully",
"details": {}
},
{
"timestamp": "2025-08-16T16:13:19.594211",
"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-16T16:13:19.607075",
"level": "info",
"event_type": "dataset_activated",
"message": "Dataset activated: Fast",
"details": {
"dataset_id": "Fast",
"variables_count": 2,
"streaming_count": 2,
"prefix": "fast"
}
},
{
"timestamp": "2025-08-16T16:13:19.619074",
"level": "info",
"event_type": "csv_recording_started",
"message": "CSV recording started: 2 datasets activated",
"details": {
"activated_datasets": 2,
"total_datasets": 3
}
},
{
"timestamp": "2025-08-16T16:14:53.763412",
"level": "info",
"event_type": "application_started",
"message": "Application initialization completed successfully",
"details": {}
},
{
"timestamp": "2025-08-16T16:14:53.829817",
"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-16T16:14:53.839819",
"level": "info",
"event_type": "dataset_activated",
"message": "Dataset activated: Fast",
"details": {
"dataset_id": "Fast",
"variables_count": 2,
"streaming_count": 2,
"prefix": "fast"
}
},
{
"timestamp": "2025-08-16T16:14:53.851597",
"level": "info",
"event_type": "csv_recording_started",
"message": "CSV recording started: 2 datasets activated",
"details": {
"activated_datasets": 2,
"total_datasets": 3
}
},
{
"timestamp": "2025-08-16T16:20:52.922366",
"level": "info",
"event_type": "application_started",
"message": "Application initialization completed successfully",
"details": {}
},
{
"timestamp": "2025-08-16T16:20:52.988919",
"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-16T16:20:52.999920",
"level": "info",
"event_type": "dataset_activated",
"message": "Dataset activated: Fast",
"details": {
"dataset_id": "Fast",
"variables_count": 2,
"streaming_count": 2,
"prefix": "fast"
}
},
{
"timestamp": "2025-08-16T16:20:53.010921",
"level": "info",
"event_type": "csv_recording_started",
"message": "CSV recording started: 2 datasets activated",
"details": {
"activated_datasets": 2,
"total_datasets": 3
}
},
{
"timestamp": "2025-08-16T16:41:27.411359",
"level": "info",
"event_type": "application_started",
"message": "Application initialization completed successfully",
"details": {}
},
{
"timestamp": "2025-08-16T16:41:27.475416",
"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-16T16:41:27.485418",
"level": "info",
"event_type": "dataset_activated",
"message": "Dataset activated: Fast",
"details": {
"dataset_id": "Fast",
"variables_count": 2,
"streaming_count": 2,
"prefix": "fast"
}
},
{
"timestamp": "2025-08-16T16:41:27.495418",
"level": "info",
"event_type": "csv_recording_started",
"message": "CSV recording started: 2 datasets activated",
"details": {
"activated_datasets": 2,
"total_datasets": 3
}
}
],
"last_updated": "2025-08-16T16:01:24.342184",
"total_entries": 724
"last_updated": "2025-08-16T16:41:27.495418",
"total_entries": 740
}

View File

@ -1,5 +1,5 @@
import React, { useRef, useEffect, useState, useCallback } from 'react';
import { Box, Text, Switch, FormLabel, HStack, useColorModeValue, Alert, AlertIcon } from '@chakra-ui/react';
import { Box, Text, Switch, FormLabel, HStack, VStack, useColorModeValue, Alert, AlertIcon } from '@chakra-ui/react';
// Global dependencies check - run once
let dependenciesChecked = false;
@ -77,6 +77,15 @@ const ChartjsHistoricalPlot = ({
// Create/Update chart when data changes
useEffect(() => {
console.log('📊 ChartjsHistoricalPlot useEffect triggered:', {
hasData: historicalData && historicalData.length > 0,
dataLength: historicalData?.length || 0,
chartExists: !!chartRef.current,
sessionId: session?.id,
configChanged: JSON.stringify(config),
zoomEnabled: isZoomEnabled
});
if (!historicalData || historicalData.length === 0) {
setDataPointsCount(0);
return;
@ -94,6 +103,18 @@ const ChartjsHistoricalPlot = ({
setCurrentTimeRange(timeRange);
}, [timeRange]);
// Cleanup chart on unmount
useEffect(() => {
console.log('📊 ChartjsHistoricalPlot mounted');
return () => {
console.log('📊 ChartjsHistoricalPlot unmounting - destroying chart');
if (chartRef.current) {
chartRef.current.destroy();
chartRef.current = null;
}
};
}, []);
// Clean up chart on unmount
useEffect(() => {
return () => {
@ -115,58 +136,93 @@ const ChartjsHistoricalPlot = ({
}
try {
// Destroy existing chart
if (chartRef.current) {
chartRef.current.destroy();
chartRef.current = null;
}
console.log(`📊 createOrUpdateChart called - Chart exists? ${!!chartRef.current}`);
console.log(`📊 Processing ${historicalData.length} historical data points for variables:`, session?.variables);
// Process historical data into Chart.js format
const processedData = processHistoricalData(historicalData, session?.variables || []);
setDataPointsCount(historicalData.length);
if (processedData.datasets.length === 0) {
console.log('📊 No datasets created from data');
setError('No valid data to display');
return;
// Siempre crear el gráfico, incluso si no hay datasets visibles
// Esto permite al usuario hacer pan/zoom para encontrar datos
if (processedData.datasets.length === 0 && historicalData.length === 0) {
console.log('📊 No data available - creating empty chart for navigation');
// Crear datasets vacíos para todas las variables
const emptyDatasets = (session?.variables || []).map((variable, index) => ({
label: variable,
data: [],
borderColor: ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0'][index % 4],
backgroundColor: ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0'][index % 4] + '20',
borderWidth: 2,
fill: false
}));
processedData.datasets = emptyDatasets;
} else if (processedData.datasets.length === 0 && historicalData.length > 0) {
console.log('📊 Data available but not visible in current range - allowing navigation');
// Los datos existen pero pueden estar fuera del rango visible
// Crear datasets para permitir navegación
const emptyDatasets = (session?.variables || []).map((variable, index) => ({
label: variable,
data: [],
borderColor: ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0'][index % 4],
backgroundColor: ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0'][index % 4] + '20',
borderWidth: 2,
fill: false
}));
processedData.datasets = emptyDatasets;
}
console.log(`📊 Created ${processedData.datasets.length} datasets:`, processedData.datasets.map(d => ({ label: d.label, points: d.data.length })));
// Log sample data for debugging
processedData.datasets.forEach((dataset, index) => {
if (dataset.data.length > 0) {
console.log(`📊 Dataset ${index} (${dataset.label}):`, {
totalPoints: dataset.data.length,
firstPoint: dataset.data[0],
lastPoint: dataset.data[dataset.data.length - 1],
samplePoints: dataset.data.slice(0, 3)
// Check if chart exists
if (chartRef.current) {
// UPDATE existing chart data (faster, preserves zoom/pan)
console.log(`📊 Updating existing chart with ${processedData.datasets.length} datasets`);
chartRef.current.data.datasets = processedData.datasets;
// Update time range for axis if we have time range data
const minTime = currentTimeRange?.start ? new Date(currentTimeRange.start) : null;
const maxTime = currentTimeRange?.end ? new Date(currentTimeRange.end) : null;
if (minTime && maxTime && chartRef.current.options.scales?.x) {
console.log('📊 Updating chart time range:', {
minTime: minTime.toISOString(),
maxTime: maxTime.toISOString()
});
chartRef.current.options.scales.x.min = minTime;
chartRef.current.options.scales.x.max = maxTime;
}
});
// Update without animation for better performance
chartRef.current.update('none');
console.log(`📊 Chart data updated (preserving zoom/pan state)`);
} else {
// CREATE new chart (first time only)
console.log(`📊 Creating new chart with ${processedData.datasets.length} datasets`);
// Create chart configuration
const chartConfig = createChartConfig(processedData, config, isZoomEnabled);
console.log('📊 Chart config created:', {
datasetCount: chartConfig.data.datasets.length,
hasData: chartConfig.data.datasets.some(d => d.data.length > 0),
decimationEnabled: chartConfig.options.plugins.decimation?.enabled
});
// Create chart configuration
const chartConfig = createChartConfig(processedData, config, isZoomEnabled);
console.log('📊 Chart config created:', {
datasetCount: chartConfig.data.datasets.length,
hasData: chartConfig.data.datasets.some(d => d.data.length > 0),
decimationEnabled: chartConfig.options.plugins.decimation?.enabled
});
// Create new chart
const ctx = canvasRef.current.getContext('2d');
chartRef.current = new window.Chart(ctx, chartConfig);
// Create new chart
const ctx = canvasRef.current.getContext('2d');
chartRef.current = new window.Chart(ctx, chartConfig);
console.log('📊 Chart reference created:', !!chartRef.current);
// Setup zoom/pan event listeners
if (isZoomEnabled && chartRef.current) {
setupZoomPanEvents(chartRef.current);
// Setup zoom/pan event listeners
if (isZoomEnabled && chartRef.current) {
setupZoomPanEvents(chartRef.current);
}
console.log(`📊 Historical chart created for session ${sessionDataRef.current.sessionId} with ${processedData.datasets.length} datasets`);
}
setError(null);
console.log(`📊 Historical chart created for session ${sessionDataRef.current.sessionId} with ${processedData.datasets.length} datasets`);
} catch (error) {
console.error('Error creating historical chart:', error);
@ -408,7 +464,8 @@ const ChartjsHistoricalPlot = ({
);
}
if (!historicalData || historicalData.length === 0) {
// Solo mostrar mensaje de "no data" si nunca se han cargado datos y el chart no existe
if ((!historicalData || historicalData.length === 0) && !chartRef.current) {
return (
<Box
height={height}
@ -420,7 +477,12 @@ const ChartjsHistoricalPlot = ({
borderColor={borderColor}
borderRadius="md"
>
<Text color={textColor}>No historical data available</Text>
<VStack>
<Text color={textColor}>No historical data available</Text>
<Text fontSize="sm" color={useColorModeValue('gray.500', 'gray.400')}>
Use the time selector to navigate to data
</Text>
</VStack>
</Box>
);
}
@ -460,6 +522,11 @@ const ChartjsHistoricalPlot = ({
(Min/Max decimation active)
</Text>
)}
{dataPointsCount === 0 && chartRef.current && (
<Text as="span" ml={2} fontSize="xs" color="orange.500">
No data in current view - use pan/zoom to navigate
</Text>
)}
</Text>
</HStack>

View File

@ -140,7 +140,18 @@ export default function PlotHistoricalSession({
const [showDataPreview, setShowDataPreview] = useState(false)
// Configuration state
const [config, setConfig] = useState(session.config || {})
// Config state (estabilizado para evitar re-renders)
const [config, setConfig] = useState(() => session.config || {})
// Estabilizar config para evitar re-renders innecesarios
const stableConfig = useMemo(() => config, [JSON.stringify(config)])
// Estabilizar historicalData para evitar re-renders innecesarios
const stableHistoricalData = useMemo(() => historicalData, [
historicalData?.length,
historicalData?.[0]?.timestamp,
historicalData?.[historicalData.length - 1]?.timestamp
])
// Helper function to ensure valid Date objects
const ensureValidDate = (dateValue, fallback) => {
@ -166,7 +177,7 @@ export default function PlotHistoricalSession({
// Load historical data when derived time range changes
useEffect(() => {
if (dateRange) { // Only load after we have date range
if (dateRange && !isLoading) { // Only load after we have date range and not already loading
loadHistoricalData()
}
}, [session.id, derivedTimeRange, dateRange])
@ -245,6 +256,12 @@ export default function PlotHistoricalSession({
}
try {
// Prevent concurrent loads
if (isLoading) {
console.log('📊 Skipping load - already loading')
return
}
setIsLoading(true)
setError(null)
setLoadingProgress(0)
@ -588,33 +605,45 @@ export default function PlotHistoricalSession({
</Box>
)}
{!isLoading && !error && (
<Box height="400px">
<ChartjsHistoricalPlot
session={session}
historicalData={historicalData}
timeRange={derivedTimeRange}
config={config}
onZoomToTimeRange={handleZoomToTimeRange}
onPanToTimeRange={handlePanToTimeRange}
height="400px"
/>
</Box>
)}
{isLoading && (
<Box height="400px" display="flex" alignItems="center" justifyContent="center">
<VStack>
<Spinner size="lg" color="blue.500" />
<Text fontSize="sm" color={subtleTextColor}>
Loading historical data...
</Text>
<Text fontSize="xs" color={smallTextColor}>
{loadingProgress}% complete
</Text>
</VStack>
</Box>
)}
{/* Chart container - siempre montado para evitar remounting */}
<Box height="400px" position="relative">
<ChartjsHistoricalPlot
key={session.id} // Estable key para evitar remounting
session={session}
historicalData={stableHistoricalData}
timeRange={derivedTimeRange}
config={stableConfig}
onZoomToTimeRange={handleZoomToTimeRange}
onPanToTimeRange={handlePanToTimeRange}
height="400px"
/>
{/* Loading overlay */}
{isLoading && (
<Box
position="absolute"
top="0"
left="0"
right="0"
bottom="0"
display="flex"
alignItems="center"
justifyContent="center"
bg={useColorModeValue('whiteAlpha.800', 'blackAlpha.800')}
backdropFilter="blur(2px)"
>
<VStack>
<Spinner size="lg" color="blue.500" />
<Text fontSize="sm" color={subtleTextColor}>
Loading historical data...
</Text>
<Text fontSize="xs" color={smallTextColor}>
{loadingProgress}% complete
</Text>
</VStack>
</Box>
)}
</Box>
</CardBody>
)}

View File

@ -1,5 +1,6 @@
import { useMemo, useState, useCallback, useRef, useEffect } from "react";
import { Box, Flex, Text, Slider, SliderTrack, SliderFilledTrack, SliderThumb, useColorModeValue } from "@chakra-ui/react";
import { Box, Flex, Text, Slider, SliderTrack, SliderFilledTrack, SliderThumb, Button, IconButton, useColorModeValue } from "@chakra-ui/react";
import { CheckIcon } from "@chakra-ui/icons";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
@ -19,25 +20,42 @@ export default function TimePointSelector({
const [minMs, maxMs] = useMemo(() => [minDate.getTime(), maxDate.getTime()], [minDate, maxDate]);
const stepMs = useMemo(() => stepMinutes * 60 * 1000, [stepMinutes]);
// Estado único (Date)
// Estado único (Date) - este es el valor "oficial"
const [value, setValue] = useState(() => {
// clamp al rango por si initial cae fuera
const t = initial.getTime();
return new Date(Math.min(Math.max(t, minMs), maxMs));
});
// Sincronizar con prop initial cuando cambie externamente (pan/zoom)
useEffect(() => {
const t = initial.getTime();
const clampedValue = new Date(Math.min(Math.max(t, minMs), maxMs));
setValue(clampedValue);
}, [initial, minMs, maxMs]);
const valueMs = value.getTime();
// Estado temporal del slider (solo para UI, no dispara eventos)
const [sliderValue, setSliderValue] = useState(() => value.getTime());
// Estado para detectar si hay cambios pendientes
const [hasPendingChanges, setHasPendingChanges] = useState(false);
// Cooldown para evitar múltiples solicitudes
const cooldownRef = useRef(null);
const lastCallbackValueRef = useRef(null);
const isExternalUpdateRef = useRef(false); // Flag para detectar actualizaciones externas
// Sincronizar con prop initial cuando cambie externamente (pan/zoom)
useEffect(() => {
const t = initial.getTime();
const clampedValue = new Date(Math.min(Math.max(t, minMs), maxMs));
// Marcar que es una actualización externa
isExternalUpdateRef.current = true;
setValue(clampedValue);
setSliderValue(clampedValue.getTime()); // Sincronizar slider también
setHasPendingChanges(false); // Reset pending changes
// Reset flag después de un tick
setTimeout(() => {
isExternalUpdateRef.current = false;
}, 0);
}, [initial, minMs, maxMs]);
const valueMs = value.getTime();
// Redondea al paso del slider
const snapToStep = useCallback((ms) => {
@ -74,26 +92,42 @@ export default function TimePointSelector({
};
}, []);
// Cambio desde el DatePicker (sin cooldown, cambio directo)
// Función para aplicar los cambios pendientes del slider
const applySliderChanges = useCallback(() => {
if (!hasPendingChanges) return;
const newValue = new Date(sliderValue);
setValue(newValue);
setHasPendingChanges(false);
if (onTimeChange) {
console.log('📊 TimeSelector: Applying slider changes', newValue);
onTimeChange(newValue);
}
}, [sliderValue, hasPendingChanges, onTimeChange]);
// Cambio desde el DatePicker (inmediato, actualiza ambos valores)
const onPick = (d) => {
if (!d) return;
if (!d || isExternalUpdateRef.current) return;
const newValue = new Date(snapToStep(d.getTime()));
setValue(newValue);
setSliderValue(newValue.getTime());
setHasPendingChanges(false);
// DatePicker no necesita cooldown, es cambio directo
// DatePicker es cambio directo
if (onTimeChange) {
console.log('📊 TimeSelector: DatePicker change (immediate)', newValue);
onTimeChange(newValue);
}
};
// Cambio desde el Slider (con cooldown)
// Cambio desde el Slider (solo actualiza UI, no dispara eventos)
const onSlide = (ms) => {
const newValue = new Date(ms);
setValue(newValue);
if (isExternalUpdateRef.current) return;
// Usar cooldown para el slider
debouncedOnTimeChange(newValue);
setSliderValue(ms);
setHasPendingChanges(Math.abs(ms - value.getTime()) > 1000); // Solo marcar si hay diferencia significativa
};
return (
@ -161,12 +195,25 @@ export default function TimePointSelector({
</Box>
<Box flex="1" minW="260px">
<Text mb={1} color={textColor}>Navegar con slider</Text>
<Flex align="center" mb={1}>
<Text color={textColor}>Navegar con slider</Text>
{hasPendingChanges && (
<Button
size="xs"
ml={2}
colorScheme="blue"
leftIcon={<CheckIcon />}
onClick={applySliderChanges}
>
Apply
</Button>
)}
</Flex>
<Slider
min={minMs}
max={maxMs}
step={stepMs}
value={valueMs}
value={sliderValue}
onChange={onSlide}
colorScheme="blue"
>
@ -175,16 +222,23 @@ export default function TimePointSelector({
</SliderTrack>
<SliderThumb bg="blue.500" />
</Slider>
<Text mt={2} fontSize="sm" color={textColor}>
{value.toLocaleString('es-ES', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false
})}
</Text>
<Flex mt={2} justify="space-between" align="center">
<Text fontSize="sm" color={textColor}>
{new Date(sliderValue).toLocaleString('es-ES', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false
})}
</Text>
{hasPendingChanges && (
<Text fontSize="xs" color="orange.500">
Cambios pendientes
</Text>
)}
</Flex>
</Box>
</Flex>

View File

@ -3,12 +3,12 @@
"should_connect": true,
"should_stream": false,
"active_datasets": [
"DAR",
"Test",
"Fast",
"DAR"
"Fast"
]
},
"auto_recovery_enabled": true,
"last_update": "2025-08-16T16:01:29.169391",
"last_update": "2025-08-16T16:41:43.854840",
"plotjuggler_path": "C:\\Program Files\\PlotJuggler\\plotjuggler.exe"
}