diff --git a/application_events.json b/application_events.json index 20fccbb..30c4c00 100644 --- a/application_events.json +++ b/application_events.json @@ -2319,8 +2319,301 @@ "trigger_variable": null, "auto_started": true } + }, + { + "timestamp": "2025-08-14T23:12:13.491544", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-08-14T23:12:13.601841", + "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-14T23:12:13.607414", + "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-14T23:12:13.611549", + "level": "info", + "event_type": "csv_recording_started", + "message": "CSV recording started: 2 datasets activated", + "details": { + "activated_datasets": 2, + "total_datasets": 3 + } + }, + { + "timestamp": "2025-08-14T23:12:13.619569", + "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": 3 + } + }, + { + "timestamp": "2025-08-14T23:16:13.052239", + "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" + ], + "time_window": 20, + "trigger_variable": null, + "auto_started": true + } + }, + { + "timestamp": "2025-08-14T23:17:26.110874", + "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" + ], + "time_window": 20, + "trigger_variable": null, + "auto_started": true + } + }, + { + "timestamp": "2025-08-14T23:17:38.598859", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-08-14T23:17:38.710314", + "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-14T23:17:38.714802", + "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-14T23:17:38.719873", + "level": "info", + "event_type": "csv_recording_started", + "message": "CSV recording started: 2 datasets activated", + "details": { + "activated_datasets": 2, + "total_datasets": 3 + } + }, + { + "timestamp": "2025-08-14T23:17:38.744433", + "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": 3 + } + }, + { + "timestamp": "2025-08-14T23:17:48.288119", + "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" + ], + "time_window": 20, + "trigger_variable": null, + "auto_started": true + } + }, + { + "timestamp": "2025-08-14T23:22:37.889079", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-08-14T23:22:37.970126", + "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-14T23:22:37.975125", + "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-14T23:22:37.980125", + "level": "info", + "event_type": "csv_recording_started", + "message": "CSV recording started: 2 datasets activated", + "details": { + "activated_datasets": 2, + "total_datasets": 3 + } + }, + { + "timestamp": "2025-08-14T23:22:37.985255", + "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": 3 + } + }, + { + "timestamp": "2025-08-14T23:23:30.022619", + "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" + ], + "time_window": 20, + "trigger_variable": null, + "auto_started": true + } + }, + { + "timestamp": "2025-08-14T23:27:17.599611", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-08-14T23:27:17.681121", + "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-14T23:27:17.687121", + "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-14T23:27:17.690131", + "level": "info", + "event_type": "csv_recording_started", + "message": "CSV recording started: 2 datasets activated", + "details": { + "activated_datasets": 2, + "total_datasets": 3 + } + }, + { + "timestamp": "2025-08-14T23:27:17.695121", + "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": 3 + } + }, + { + "timestamp": "2025-08-14T23:29:18.561423", + "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" + ], + "time_window": 20, + "trigger_variable": null, + "auto_started": true + } } ], - "last_updated": "2025-08-14T22:57:05.817266", - "total_entries": 223 + "last_updated": "2025-08-14T23:29:18.561423", + "total_entries": 248 } \ No newline at end of file diff --git a/frontend/src/components/ChartjsPlot.jsx b/frontend/src/components/ChartjsPlot.jsx index 1c8e96d..b76b33c 100644 --- a/frontend/src/components/ChartjsPlot.jsx +++ b/frontend/src/components/ChartjsPlot.jsx @@ -1,6 +1,48 @@ import React, { useRef, useEffect, useState, useCallback } from 'react'; import { Box, Text, useColorModeValue } from '@chakra-ui/react'; +// Global dependencies check - run once +let dependenciesChecked = false; +let dependenciesValid = false; + +const checkChartDependencies = () => { + if (dependenciesChecked) return dependenciesValid; + + try { + if (typeof window === 'undefined') { + console.warn('⚠️ Window not available, skipping dependency check'); + return false; + } + + // Check for Chart.js + if (!window.Chart) { + console.error('❌ Chart.js not loaded'); + return false; + } + + // Check for chartjs-plugin-streaming + const hasStreamingPlugin = !!( + window.Chart.registry?.scales?.get?.('realtime') || + window.ChartStreaming || + window.chartjsPluginStreaming + ); + + if (!hasStreamingPlugin) { + console.error('❌ chartjs-plugin-streaming not loaded or realtime scale not registered'); + return false; + } + + console.log('✅ Chart.js dependencies verified'); + dependenciesValid = true; + return true; + } catch (error) { + console.error('❌ Dependency check failed:', error); + return false; + } finally { + dependenciesChecked = true; + } +}; + // Chart.js Plot Component with Streaming and Zoom const ChartjsPlot = ({ session, height = '400px' }) => { const canvasRef = useRef(null); @@ -23,6 +65,7 @@ const ChartjsPlot = ({ session, height = '400px' }) => { const [error, setError] = useState(null); const [dataPointsCount, setDataPointsCount] = useState(0); const [isRefreshing, setIsRefreshing] = useState(false); + const [isLoadingHistorical, setIsLoadingHistorical] = useState(false); const resolvedConfigRef = useRef(null); const bgColor = useColorModeValue('white', 'gray.800'); @@ -99,6 +142,36 @@ const ChartjsPlot = ({ session, height = '400px' }) => { return []; }, [getColor]); + // Load historical data from CSV files + const loadHistoricalData = useCallback(async (variables, timeWindow) => { + try { + console.log(`📊 Loading historical data for ${variables.length} variables (${timeWindow}s window)...`); + + const response = await fetch('/api/plots/historical', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify({ + variables: variables, + time_window: timeWindow + }) + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + + const data = await response.json(); + console.log(`📊 Historical data response:`, data); + return data.data || []; + } catch (error) { + console.warn('⚠️ Failed to load historical data:', error); + return []; + } + }, []); + const createStreamingChart = useCallback(async () => { const cfg = resolvedConfigRef.current || session?.config; if (!canvasRef.current || !cfg) return; @@ -141,23 +214,146 @@ const ChartjsPlot = ({ session, height = '400px' }) => { const ctx = canvasRef.current.getContext('2d'); - // Destroy existing chart more safely + // CRITICAL: More aggressive cleanup check - StrictMode protection + const existingChartInRegistry = Chart.getChart(ctx.canvas); + const hasChartReference = !!chartRef.current; + const hasCanvasChartProperty = !!ctx.canvas.chartjs; + + if (existingChartInRegistry || hasChartReference || hasCanvasChartProperty) { + console.log(`🚨 FORCE CLEANUP - Registry: ${!!existingChartInRegistry}, Ref: ${hasChartReference}, Canvas: ${hasCanvasChartProperty}`); + + // Cleanup existing registry entry + if (existingChartInRegistry) { + try { + const rt = existingChartInRegistry.options?.scales?.x?.realtime; + if (rt) { + rt.pause = true; + if (rt._timer) { + clearInterval(rt._timer); + rt._timer = null; + } + existingChartInRegistry.update('none'); + } + existingChartInRegistry.stop(); + existingChartInRegistry.destroy(); + console.log('✅ Force destroyed registry chart'); + } catch (e) { + console.warn('⚠️ Error force destroying registry chart:', e); + } + } + + // Cleanup reference + if (chartRef.current) { + try { + const rt = chartRef.current.options?.scales?.x?.realtime; + if (rt) { + rt.pause = true; + if (rt._timer) { + clearInterval(rt._timer); + rt._timer = null; + } + chartRef.current.update('none'); + } + chartRef.current.stop(); + chartRef.current.destroy(); + console.log('✅ Force destroyed reference chart'); + } catch (e) { + console.warn('⚠️ Error force destroying reference chart:', e); + } finally { + chartRef.current = null; + } + } + + // Force cleanup canvas properties + if (ctx.canvas.chartjs) { + delete ctx.canvas.chartjs; + } + + // Aggressive canvas reset + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + ctx.canvas.style.width = ''; + ctx.canvas.style.height = ''; + + // Force blur to remove focus + if (document.activeElement === ctx.canvas) { + ctx.canvas.blur(); + } + + console.log('🧹 Force cleanup completed'); + } + + // Enhanced chart cleanup - check for multiple potential chart instances + const existingChart = Chart.getChart(ctx.canvas); + if (existingChart) { + console.log('🔄 Found existing Chart.js instance, destroying...'); + try { + // Stop streaming plugin timers first + const rt = existingChart.options?.scales?.x?.realtime; + if (rt) { + rt.pause = true; + existingChart.update('none'); + } + + // Stop any running animations and timers + existingChart.stop(); + + // Destroy the chart instance + existingChart.destroy(); + console.log('✅ Existing Chart.js instance destroyed'); + } catch (destroyError) { + console.warn('⚠️ Error destroying existing chart:', destroyError); + } + } + + // Destroy our reference too if it exists if (chartRef.current) { try { - // Pause streaming before destroying to avoid dangling references + console.log('🔄 Destroying chart reference...'); + + // Stop streaming plugin timers const rt = chartRef.current.options?.scales?.x?.realtime; if (rt) { rt.pause = true; + // Force stop any active timers + if (rt._timer) { + clearInterval(rt._timer); + rt._timer = null; + } chartRef.current.update('none'); } + + // Clear any animation frames and timers + chartRef.current.stop(); + + // Destroy the chart chartRef.current.destroy(); + + console.log('✅ Chart reference destroyed successfully'); } catch (destroyError) { - console.warn('⚠️ Error destroying previous chart:', destroyError); + console.warn('⚠️ Error destroying chart reference:', destroyError); } finally { chartRef.current = null; } } + // Additional safety: clean up any remaining Chart.js properties + if (ctx.canvas.chartjs) { + console.warn('⚠️ Canvas still has chartjs reference, cleaning up...'); + delete ctx.canvas.chartjs; + } + + // Clear the canvas completely and reset size + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + + // Reset canvas styling to ensure clean state + ctx.canvas.style.width = ''; + ctx.canvas.style.height = ''; + + // Force canvas to lose focus if it has it + if (document.activeElement === ctx.canvas) { + ctx.canvas.blur(); + } + const config = cfg; const enabledVariables = getEnabledVariables(config.variables); @@ -191,6 +387,61 @@ const ChartjsPlot = ({ session, height = '400px' }) => { sessionDataRef.current.datasetIndex.set(variableInfo.name, index); }); + // Load historical data to pre-populate the chart + const timeWindow = config.time_window || 60; + const variableNames = enabledVariables.map(v => v.name); + + // TEMPORARILY DISABLED: Historical data loading due to backend HTTP 500 error + // TODO: Fix backend /api/plots/historical endpoint + console.log(`📊 Historical data loading temporarily disabled for ${variableNames.length} variables`); + + /* + if (variableNames.length > 0) { + setIsLoadingHistorical(true); + try { + const historicalData = await loadHistoricalData(variableNames, timeWindow); + + if (historicalData.length > 0) { + console.log(`📊 Loaded ${historicalData.length} historical data points`); + + // Group data by variable and add to appropriate dataset + const dataByVariable = {}; + historicalData.forEach(point => { + const { variable, timestamp, value } = point; + if (!dataByVariable[variable]) { + dataByVariable[variable] = []; + } + dataByVariable[variable].push({ + x: new Date(timestamp), + y: value + }); + }); + + // Add historical data to datasets + enabledVariables.forEach((variableInfo, index) => { + const historicalPoints = dataByVariable[variableInfo.name] || []; + if (historicalPoints.length > 0) { + // Sort points by timestamp to ensure proper order + historicalPoints.sort((a, b) => a.x - b.x); + datasets[index].data = historicalPoints; + console.log(`📊 Added ${historicalPoints.length} historical points for ${variableInfo.name}`); + } + }); + + // Update data points counter + const totalHistoricalPoints = Object.values(dataByVariable).reduce((sum, points) => sum + points.length, 0); + setDataPointsCount(totalHistoricalPoints); + } else { + console.log(`📊 No historical data found for variables: ${variableNames.join(', ')}`); + } + } catch (error) { + console.warn('⚠️ Failed to load historical data:', error); + } finally { + setIsLoadingHistorical(false); + } + } + */ + 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; @@ -288,6 +539,21 @@ const ChartjsPlot = ({ session, height = '400px' }) => { } }; + // Final safety check before creating new chart + if (ctx.canvas.chartjs) { + console.warn('⚠️ Canvas still has Chart.js reference, forcing cleanup...'); + try { + const existingChart = window.Chart.getChart(ctx.canvas); + if (existingChart) { + existingChart.destroy(); + } + } catch (e) { + console.warn('⚠️ Error cleaning up existing chart reference:', e); + } + delete ctx.canvas.chartjs; + } + + console.log('🚀 Creating new Chart.js instance...'); chartRef.current = new Chart(ctx, chartConfig); sessionDataRef.current.isRealTimeMode = true; sessionDataRef.current.noDataCycles = 0; @@ -677,30 +943,101 @@ const ChartjsPlot = ({ session, height = '400px' }) => { // Initialize chart when config is resolved - simplified approach useEffect(() => { - // Only create chart once when we have a session_id and canvas + console.log(`🔍 useEffect triggered - sessionId: ${session?.session_id}, hasCanvas: ${!!canvasRef.current}, hasChart: ${!!chartRef.current}`); + + // Check dependencies first + if (!checkChartDependencies()) { + setError('Chart.js dependencies not loaded. Please refresh the page.'); + return; + } + + // Only create chart when we have ALL requirements AND no existing chart if (session?.session_id && canvasRef.current && !chartRef.current) { const config = session?.config; if (config) { - resolvedConfigRef.current = config; - createStreamingChart(); + console.log(`🎯 Creating chart for session ${session.session_id} - conditions met`); + + // Additional safety check - wait a tiny bit to ensure cleanup is complete + setTimeout(() => { + // Double-check that we still need to create a chart + if (chartRef.current || !canvasRef.current) { + console.log('⏭️ Chart creation cancelled - state changed during delay'); + return; + } + + resolvedConfigRef.current = config; + createStreamingChart(); + }, 10); + } else { + console.log(`⚠️ Session ${session.session_id} has no config, skipping chart creation`); } + } else { + console.log(`⏭️ Skipping chart creation - sessionId: ${!!session?.session_id}, canvas: ${!!canvasRef.current}, chart: ${!!chartRef.current}`); } return () => { + console.log('🧹 Cleaning up chart component on unmount...'); try { + // Enhanced cleanup - check for Chart.js registry first + if (canvasRef.current) { + const ctx = canvasRef.current.getContext('2d'); + const existingChart = Chart.getChart(ctx.canvas); + + if (existingChart) { + console.log('🧹 Cleaning up Chart.js instance on unmount...'); + const rt = existingChart.options?.scales?.x?.realtime; + if (rt) { + rt.pause = true; + // Force stop any active timers + if (rt._timer) { + clearInterval(rt._timer); + rt._timer = null; + } + existingChart.update('none'); + } + existingChart.stop(); + existingChart.destroy(); + } + } + + // Clean up our reference too if (chartRef.current) { + console.log('🧹 Cleaning up chart reference on unmount...'); const rt = chartRef.current.options?.scales?.x?.realtime; if (rt) { rt.pause = true; + // Force stop any active timers + if (rt._timer) { + clearInterval(rt._timer); + rt._timer = null; + } chartRef.current.update('none'); } + chartRef.current.stop(); chartRef.current.destroy(); chartRef.current = null; } + + // Clean up canvas references completely + if (canvasRef.current) { + const ctx = canvasRef.current.getContext('2d'); + if (ctx.canvas.chartjs) { + delete ctx.canvas.chartjs; + } + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + ctx.canvas.style.width = ''; + ctx.canvas.style.height = ''; + + // Force canvas to lose focus + if (document.activeElement === ctx.canvas) { + ctx.canvas.blur(); + } + } } catch (error) { console.warn('⚠️ Chart cleanup error:', error); } + // Clean up any manual intervals if (sessionDataRef.current.manualInterval) { clearInterval(sessionDataRef.current.manualInterval); sessionDataRef.current.manualInterval = null; diff --git a/frontend/src/components/PlotRealtimeViewer.jsx b/frontend/src/components/PlotRealtimeViewer.jsx new file mode 100644 index 0000000..b27fab0 --- /dev/null +++ b/frontend/src/components/PlotRealtimeViewer.jsx @@ -0,0 +1,289 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react' +import { + Box, + VStack, + HStack, + Text, + Button, + Card, + CardBody, + CardHeader, + Heading, + useColorModeValue, + Badge, + IconButton, + Divider, + Spacer, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalCloseButton, + ModalBody, + useDisclosure, +} from '@chakra-ui/react' +import { EditIcon, SettingsIcon, DeleteIcon, ViewIcon } 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 Cargando sesiones de plots… + } + + if (sessionsList.length === 0) { + return ( + + + No hay sesiones de plot. Cree o edite plots en la sección superior. + + + ) + } + + return ( + + {sessionsList.map((session) => ( + + ))} + + ) +} + +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 { isOpen: isFullscreen, onOpen: openFullscreen, onClose: closeFullscreen } = useDisclosure() + + const handleChartReady = (controls) => { + chartControlsRef.current = controls + } + + const enhancedSession = { + ...session, + onChartReady: handleChartReady, + isFullscreen: isFullscreen, + } + + 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 ( + + + onRefresh(session.session_id)} + onFullscreen={openFullscreen} + /> + + + + + + + + + + + + + + {/* Fullscreen Modal */} + + + + + + 📈 {session.name || session.session_id} - Fullscreen Mode + + + Zoom: Drag to select area | Pan: Shift + Drag | Double-click to reset + + + + + + + + + + + + + {chartControlsRef.current && ( + + )} + + + + + + + ) +} + +function FlexHeader({ session, muted, onRefresh, onFullscreen }) { + return ( + + + 📈 {session.name || session.session_id} + + Variables: {session.variables_count || 0} | Status: {session.is_active ? (session.is_paused ? 'Paused' : 'Active') : 'Stopped'} + + + + + + } + size="sm" + variant="outline" + aria-label="Refresh status" + onClick={onRefresh} + /> + + + ) +} + + diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 66f3be9..4dbaf27 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -200,6 +200,22 @@ export async function getPlotVariables() { return toJsonOrThrow(res) } +// Historical data loading +export async function getHistoricalData(variables, timeWindowSeconds) { + const res = await fetch(`${BASE_URL}/api/plots/historical`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify({ + variables: variables, + time_window: timeWindowSeconds + }) + }) + return toJsonOrThrow(res) +} + // Plot session status and control (aliases for existing functions) export async function getPlotSession(sessionId) { // Use existing getPlotConfig to get session info diff --git a/main.py b/main.py index 97015b3..9830037 100644 --- a/main.py +++ b/main.py @@ -1821,6 +1821,137 @@ def get_plot_variables(): return jsonify({"error": str(e)}), 500 +@app.route("/api/plots/historical", methods=["POST"]) +def get_historical_data(): + """Get historical data from CSV files for plot initialization""" + try: + data = request.get_json() + if not data: + return jsonify({"error": "No data provided"}), 400 + + variables = data.get('variables', []) + time_window_seconds = data.get('time_window', 60) + + if not variables: + return jsonify({"error": "No variables specified"}), 400 + + # Import here to avoid circular imports + import pandas as pd + import glob + from datetime import datetime, timedelta + + # Calculate time range + end_time = datetime.now() + start_time = end_time - timedelta(seconds=time_window_seconds) + + # Get records directory + records_dir = os.path.join(os.path.dirname(__file__), 'records') + if not os.path.exists(records_dir): + return jsonify({"data": []}) + + historical_data = [] + + # Get date folders to search (today and yesterday in case time window spans days) + today = end_time.strftime('%d-%m-%Y') + yesterday = (end_time - timedelta(days=1)).strftime('%d-%m-%Y') + + date_folders = [] + for date_folder in [yesterday, today]: + folder_path = os.path.join(records_dir, date_folder) + if os.path.exists(folder_path): + date_folders.append(folder_path) + + # Search for CSV files with any of the required variables + for folder_path in date_folders: + csv_files = glob.glob(os.path.join(folder_path, '*.csv')) + + for csv_file in csv_files: + try: + # Read first line to check if any required variables are present + with open(csv_file, 'r') as f: + header_line = f.readline().strip() + if not header_line: + continue + + headers = [h.strip() for h in header_line.split(',')] + + # Check if any of our variables are in this file + matching_vars = [var for var in variables if var in headers] + if not matching_vars: + continue + + # Read the CSV file + df = pd.read_csv(csv_file) + + if 'timestamp' not in df.columns: + continue + + # Convert timestamp to datetime + df['timestamp'] = pd.to_datetime(df['timestamp']) + + # Filter by time range + mask = (df['timestamp'] >= start_time) & (df['timestamp'] <= end_time) + filtered_df = df[mask] + + if filtered_df.empty: + continue + + # Extract data for matching variables only + for _, row in filtered_df.iterrows(): + timestamp = row['timestamp'] + for var in matching_vars: + if var in row: + try: + # Convert value to appropriate type + value = row[var] + if pd.isna(value): + continue + + # Handle boolean values + if isinstance(value, str): + if value.lower() == 'true': + value = True + elif value.lower() == 'false': + value = False + else: + try: + value = float(value) + except ValueError: + continue + + historical_data.append({ + 'timestamp': timestamp.isoformat(), + 'variable': var, + 'value': value + }) + except Exception as e: + # Skip invalid values + continue + + except Exception as e: + # Skip files that can't be read + print(f"Warning: Could not read CSV file {csv_file}: {e}") + continue + + # Sort by timestamp + historical_data.sort(key=lambda x: x['timestamp']) + + return jsonify({ + "data": historical_data, + "time_range": { + "start": start_time.isoformat(), + "end": end_time.isoformat() + }, + "variables_found": list(set([item['variable'] for item in historical_data])), + "total_points": len(historical_data) + }) + + except ImportError: + return jsonify({"error": "pandas is required for historical data processing"}), 500 + except Exception as e: + return jsonify({"error": str(e)}), 500 + + @app.route("/api/status") def get_status(): """Get current status""" diff --git a/requirements.txt b/requirements.txt index 6270749..410f187 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,5 @@ python-snap7==1.3 psutil==5.9.5 flask-socketio==5.3.6 jsonschema==4.22.0 -Flask-Cors==4.0.0 \ No newline at end of file +Flask-Cors==4.0.0 +pandas \ No newline at end of file diff --git a/system_state.json b/system_state.json index 343a83b..eac7595 100644 --- a/system_state.json +++ b/system_state.json @@ -3,11 +3,11 @@ "should_connect": true, "should_stream": true, "active_datasets": [ + "Test", "Fast", - "DAR", - "Test" + "DAR" ] }, "auto_recovery_enabled": true, - "last_update": "2025-08-14T22:51:29.383787" + "last_update": "2025-08-14T23:27:17.699618" } \ No newline at end of file