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