From 748e8d5b0e306ff633f02942c1aff1c32132bc3f Mon Sep 17 00:00:00 2001 From: Miguel Date: Thu, 14 Aug 2025 00:06:43 +0200 Subject: [PATCH] Update application event logging, refine PLC configuration, and enhance PlotManager functionality. Added multiple application start events, corrected PLC rack configuration, and introduced PlotRealtimeSession for improved real-time plotting capabilities. --- application_events.json | 301 +++++-------- config/data/plc_config.json | 2 +- config/schema/plc.schema.json | 11 +- frontend/src/components/PlotManager.jsx | 274 ++++-------- .../src/components/PlotRealtimeSession.jsx | 423 ++++++++++++++++++ frontend/src/components/rjsf/widgets.jsx | 2 +- frontend/src/services/api.js | 11 + 7 files changed, 655 insertions(+), 369 deletions(-) create mode 100644 frontend/src/components/PlotRealtimeSession.jsx diff --git a/application_events.json b/application_events.json index 14e743d..e68c088 100644 --- a/application_events.json +++ b/application_events.json @@ -1,181 +1,5 @@ { "events": [ - { - "timestamp": "2025-07-17T15:42:38.053687", - "level": "info", - "event_type": "csv_started", - "message": "CSV recording started for 4 variables", - "details": { - "variables_count": 4, - "output_directory": "records\\17-07-2025" - } - }, - { - "timestamp": "2025-07-17T15:42:38.055690", - "level": "info", - "event_type": "streaming_started", - "message": "Streaming started with 4 variables", - "details": { - "variables_count": 4, - "streaming_variables_count": 4, - "sampling_interval": 0.1, - "udp_host": "127.0.0.1", - "udp_port": 9870 - } - }, - { - "timestamp": "2025-07-17T15:43:12.383366", - "level": "info", - "event_type": "variable_added", - "message": "Variable added: test -> DB2124.14 (real)", - "details": { - "name": "test", - "db": 2124, - "offset": 14, - "type": "real", - "total_variables": 5 - } - }, - { - "timestamp": "2025-07-17T15:43:12.385360", - "level": "info", - "event_type": "csv_file_created", - "message": "New CSV file created after variable modification: _15_43_12.csv", - "details": { - "file_path": "records\\17-07-2025\\_15_43_12.csv", - "variables_count": 5, - "reason": "variable_modification" - } - }, - { - "timestamp": "2025-07-17T15:43:12.407642", - "level": "error", - "event_type": "streaming_error", - "message": "Error in streaming loop: dictionary changed size during iteration", - "details": { - "error": "dictionary changed size during iteration", - "consecutive_errors": 1 - } - }, - { - "timestamp": "2025-07-17T15:43:33.392876", - "level": "error", - "event_type": "streaming_error", - "message": "Error in streaming loop: dictionary changed size during iteration", - "details": { - "error": "dictionary changed size during iteration", - "consecutive_errors": 1 - } - }, - { - "timestamp": "2025-07-17T15:43:33.394375", - "level": "info", - "event_type": "variable_removed", - "message": "Variable removed: test", - "details": { - "name": "test", - "removed_config": { - "db": 2124, - "offset": 14, - "type": "real", - "streaming": false - }, - "total_variables": 4 - } - }, - { - "timestamp": "2025-07-17T15:43:33.397370", - "level": "info", - "event_type": "csv_file_created", - "message": "New CSV file created after variable modification: _15_43_33.csv", - "details": { - "file_path": "records\\17-07-2025\\_15_43_33.csv", - "variables_count": 4, - "reason": "variable_modification" - } - }, - { - "timestamp": "2025-07-17T15:43:37.383086", - "level": "info", - "event_type": "config_change", - "message": "UDP configuration updated: 127.0.0.1:9870", - "details": { - "old_config": { - "host": "127.0.0.1", - "port": 9870 - }, - "new_config": { - "host": "127.0.0.1", - "port": 9870 - } - } - }, - { - "timestamp": "2025-07-17T15:43:38.917840", - "level": "info", - "event_type": "config_change", - "message": "PLC configuration updated: 10.1.33.11:0/2", - "details": { - "old_config": { - "ip": "10.1.33.11", - "rack": 0, - "slot": 2 - }, - "new_config": { - "ip": "10.1.33.11", - "rack": 0, - "slot": 2 - } - } - }, - { - "timestamp": "2025-07-17T16:02:11.949781", - "level": "info", - "event_type": "Application started", - "message": "Application initialization completed successfully", - "details": {} - }, - { - "timestamp": "2025-07-17T16:02:11.964986", - "level": "info", - "event_type": "plc_connection", - "message": "Successfully connected to PLC 10.1.33.11", - "details": { - "ip": "10.1.33.11", - "rack": 0, - "slot": 2 - } - }, - { - "timestamp": "2025-07-17T16:02:11.966271", - "level": "info", - "event_type": "csv_started", - "message": "CSV recording started for 4 variables", - "details": { - "variables_count": 4, - "output_directory": "records\\17-07-2025" - } - }, - { - "timestamp": "2025-07-17T16:02:11.967664", - "level": "info", - "event_type": "streaming_started", - "message": "Streaming started with 4 variables", - "details": { - "variables_count": 4, - "streaming_variables_count": 4, - "sampling_interval": 0.1, - "udp_host": "127.0.0.1", - "udp_port": 9870 - } - }, - { - "timestamp": "2025-07-17T16:08:42.495109", - "level": "info", - "event_type": "Application started", - "message": "Application initialization completed successfully", - "details": {} - }, { "timestamp": "2025-07-17T16:08:42.524816", "level": "info", @@ -10470,8 +10294,131 @@ "event_type": "application_started", "message": "Application initialization completed successfully", "details": {} + }, + { + "timestamp": "2025-08-13T23:47:06.282724", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-08-13T23:48:59.679823", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-08-13T23:49:16.453495", + "level": "error", + "event_type": "csv_recording_error", + "message": "Cannot start CSV recording: No datasets configured", + "details": {} + }, + { + "timestamp": "2025-08-13T23:49:16.464824", + "level": "info", + "event_type": "plc_connection", + "message": "Successfully connected to PLC 10.1.33.11 (no datasets for recording)", + "details": { + "ip": "10.1.33.11", + "rack": 1, + "slot": 2, + "auto_started_recording": false, + "recording_datasets": 0 + } + }, + { + "timestamp": "2025-08-13T23:51:02.704338", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-08-13T23:51:21.839288", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-08-13T23:54:25.455173", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-08-13T23:55:05.790024", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-08-13T23:56:14.341605", + "level": "error", + "event_type": "csv_recording_error", + "message": "Cannot start CSV recording: No datasets configured", + "details": {} + }, + { + "timestamp": "2025-08-13T23:56:14.354938", + "level": "info", + "event_type": "plc_connection", + "message": "Successfully connected to PLC 10.1.33.11 (no datasets for recording)", + "details": { + "ip": "10.1.33.11", + "rack": 1, + "slot": 2, + "auto_started_recording": false, + "recording_datasets": 0 + } + }, + { + "timestamp": "2025-08-13T23:57:58.596445", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-08-14T00:00:13.670046", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-08-14T00:03:19.284260", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-08-14T00:03:39.376430", + "level": "error", + "event_type": "csv_recording_error", + "message": "Cannot start CSV recording: No datasets configured", + "details": {} + }, + { + "timestamp": "2025-08-14T00:03:39.391445", + "level": "info", + "event_type": "plc_connection", + "message": "Successfully connected to PLC 10.1.33.11 (no datasets for recording)", + "details": { + "ip": "10.1.33.11", + "rack": 0, + "slot": 2, + "auto_started_recording": false, + "recording_datasets": 0 + } } ], - "last_updated": "2025-08-13T23:35:44.039580", + "last_updated": "2025-08-14T00:03:39.391445", "total_entries": 1000 } \ No newline at end of file diff --git a/config/data/plc_config.json b/config/data/plc_config.json index 5ffc49d..0e7b1cb 100644 --- a/config/data/plc_config.json +++ b/config/data/plc_config.json @@ -7,7 +7,7 @@ }, "plc_config": { "ip": "10.1.33.11", - "rack": 1, + "rack": 0, "slot": 2 }, "udp_config": { diff --git a/config/schema/plc.schema.json b/config/schema/plc.schema.json index 2f7bdd5..a81eeaf 100644 --- a/config/schema/plc.schema.json +++ b/config/schema/plc.schema.json @@ -56,18 +56,11 @@ "type": "string" }, "rack": { - "default": 0, - "description": "Rack of PLC", - "maximum": 7, - "minimum": 0, "title": "Rack", - "type": "integer" + "type": "integer", + "minimum": 0 }, "slot": { - "default": 2, - "description": "Normally 2", - "maximum": 31, - "minimum": 0, "title": "Slot", "type": "integer" } diff --git a/frontend/src/components/PlotManager.jsx b/frontend/src/components/PlotManager.jsx index c596d18..b1e0dac 100644 --- a/frontend/src/components/PlotManager.jsx +++ b/frontend/src/components/PlotManager.jsx @@ -23,13 +23,6 @@ import { TableContainer, Badge, IconButton, - AlertDialog, - AlertDialogBody, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogContent, - AlertDialogOverlay, - useDisclosure, Tabs, TabList, TabPanels, @@ -41,6 +34,7 @@ import Form from '@rjsf/chakra-ui' import validator from '@rjsf/validator-ajv8' import allWidgets from './widgets/AllWidgets' import LayoutObjectFieldTemplate from './rjsf/LayoutObjectFieldTemplate' +import PlotRealtimeSession from './PlotRealtimeSession' import * as api from '../services/api' // Pure RJSF Plot Manager Component @@ -52,14 +46,10 @@ export default function PlotManager() { const [plotsVariablesConfig, setPlotsVariablesConfig] = useState(null) const [loading, setLoading] = useState(true) const [actionLoading, setActionLoading] = useState({}) - const [selectedPlot, setSelectedPlot] = useState(null) const toast = useToast() const cardBg = useColorModeValue('white', 'gray.700') const borderColor = useColorModeValue('gray.200', 'gray.600') - - const { isOpen, onOpen, onClose } = useDisclosure() - const cancelRef = React.useRef() const setActionState = (key, loading) => { setActionLoading(prev => ({ ...prev, [key]: loading })) @@ -99,6 +89,80 @@ export default function PlotManager() { } }, [toast]) + // Helper function to get plot definitions from config + const getPlotDefinitions = () => { + return plotsConfig?.plots || [] + } + + // Helper function to get variables for a specific plot + const getPlotVariables = (plotId) => { + const plotVarsConfig = plotsVariablesConfig?.variables || [] + const plotVarEntry = plotVarsConfig.find(entry => entry.plot_id === plotId) + return plotVarEntry?.variables || [] + } + + // Handle plot configuration updates + const handlePlotConfigUpdate = async (plotId, newConfig) => { + try { + // Update the plot definition in local state + const updatedPlots = getPlotDefinitions().map(plot => + plot.id === plotId ? { ...plot, ...newConfig } : plot + ) + + const updatedConfig = { ...plotsConfig, plots: updatedPlots } + await savePlotsConfig(updatedConfig) + + // Reload data to get fresh state + await loadPlotData() + } catch (error) { + throw new Error(`Failed to update plot configuration: ${error.message}`) + } + } + + // Handle plot removal + const handlePlotRemove = async (plotId) => { + try { + // Remove from plot definitions + const updatedPlots = getPlotDefinitions().filter(plot => plot.id !== plotId) + const updatedPlotsConfig = { ...plotsConfig, plots: updatedPlots } + + // Remove from plot variables + const updatedPlotVars = (plotsVariablesConfig?.variables || []).filter( + entry => entry.plot_id !== plotId + ) + const updatedVarsConfig = { ...plotsVariablesConfig, variables: updatedPlotVars } + + // Save both configurations + await Promise.all([ + savePlotsConfig(updatedPlotsConfig), + savePlotsVariablesConfig(updatedVarsConfig) + ]) + + // Stop the plot session in backend + try { + await api.controlPlotSession(plotId, 'stop') + } catch (error) { + // Plot session may not exist, that's OK + } + + // Reload data + await loadPlotData() + + toast({ + title: 'βœ… Plot removed successfully', + status: 'success', + duration: 2000 + }) + } catch (error) { + toast({ + title: '❌ Failed to remove plot', + description: error.message, + status: 'error', + duration: 3000 + }) + } + } + const savePlotsConfig = async (formData) => { try { setActionState('savePlots', true) @@ -143,58 +207,6 @@ export default function PlotManager() { } } - const handlePlotControl = async (sessionId, action) => { - try { - setActionState(`${sessionId}_${action}`, true) - const result = await api.controlPlot(sessionId, action) - toast({ - title: `πŸŽ›οΈ Plot ${action}`, - description: result.message || `Plot ${action} successful`, - status: 'success', - duration: 2000 - }) - // Refresh plots list to get updated status - await loadPlotData() - } catch (error) { - toast({ - title: `❌ Failed to ${action} plot`, - description: error.message, - status: 'error', - duration: 3000 - }) - } finally { - setActionState(`${sessionId}_${action}`, false) - } - } - - const handleDeletePlot = async (sessionId) => { - try { - setActionState(`${sessionId}_delete`, true) - await api.deletePlot(sessionId) - toast({ - title: 'πŸ—‘οΈ Plot deleted', - status: 'success', - duration: 2000 - }) - await loadPlotData() - } catch (error) { - toast({ - title: '❌ Failed to delete plot', - description: error.message, - status: 'error', - duration: 3000 - }) - } finally { - setActionState(`${sessionId}_delete`, false) - onClose() - } - } - - const confirmDelete = (sessionId) => { - setSelectedPlot(sessionId) - onOpen() - } - useEffect(() => { loadPlotData() }, [loadPlotData]) @@ -219,97 +231,31 @@ export default function PlotManager() { - {/* Active Plots Overview */} + {/* Active Plot Sessions with Real Chart.js Plots */} πŸŽ›οΈ Active Plot Sessions + + Real-time Chart.js plots with streaming data from PLC + - {Object.keys(plots).length === 0 ? ( - - No active plot sessions + {getPlotDefinitions().length === 0 ? ( + + No plot sessions configured. Create plot definitions below to get started. ) : ( - - - - - - - - - - - - - {Object.entries(plots).map(([sessionId, plot]) => ( - - - - - - - - ))} - -
Session IDNameStatusVariablesActions
- - {sessionId} - - {plot.name || 'Unnamed Plot'} - - {plot.running ? 'Running' : 'Stopped'} - - - - {plot.variable_count || 0} vars - - - - {plot.running ? ( - - ) : ( - - )} - - - -
-
+ + {getPlotDefinitions().map((plotDef) => ( + + ))} + )}
@@ -403,40 +349,6 @@ export default function PlotManager() { - - {/* Delete Confirmation Dialog */} - - - - - Delete Plot Session - - - - Are you sure you want to delete plot session "{selectedPlot}"? - This action cannot be undone. - - - - - - - - - ) } diff --git a/frontend/src/components/PlotRealtimeSession.jsx b/frontend/src/components/PlotRealtimeSession.jsx new file mode 100644 index 0000000..8ac75ec --- /dev/null +++ b/frontend/src/components/PlotRealtimeSession.jsx @@ -0,0 +1,423 @@ +import React, { useEffect, useRef, useState, useCallback } from 'react' +import { + Box, + VStack, + HStack, + Text, + Button, + Card, + CardBody, + CardHeader, + Heading, + useColorModeValue, + Badge, + IconButton, + Divider, + Spacer, + NumberInput, + NumberInputField, + NumberInputStepper, + NumberIncrementStepper, + NumberDecrementStepper, + FormControl, + FormLabel, + Switch, + Grid, + GridItem, + Flex, + useToast +} from '@chakra-ui/react' +import { SettingsIcon } from '@chakra-ui/icons' +import ChartjsPlot from './ChartjsPlot.jsx' +import * as api from '../services/api' + +/** + * PlotRealtimeSession - Individual real-time Chart.js plot component + * Mimics the functionality from the legacy plotting.js system + */ +export default function PlotRealtimeSession({ + plotDefinition, + plotVariables = [], + onRemove, + onConfigUpdate +}) { + const [session, setSession] = useState({ + session_id: plotDefinition.id, + name: plotDefinition.name, + is_active: false, + is_paused: false, + variables_count: plotVariables.length + }) + + const [showSettings, setShowSettings] = useState(false) + const [localConfig, setLocalConfig] = useState({ + time_window: plotDefinition.time_window || 60, + y_min: plotDefinition.y_min, + y_max: plotDefinition.y_max, + trigger_enabled: plotDefinition.trigger_enabled || false, + trigger_variable: plotDefinition.trigger_variable, + trigger_on_true: plotDefinition.trigger_on_true || true + }) + + const chartControlsRef = useRef(null) + const intervalRef = useRef(null) + const toast = useToast() + + const cardBg = useColorModeValue('white', 'gray.700') + const borderColor = useColorModeValue('gray.200', 'gray.600') + const muted = useColorModeValue('gray.600', 'gray.300') + + // Enhanced session object for ChartjsPlot + const enhancedSession = { + ...session, + config: { + ...plotDefinition, + ...localConfig, + variables: plotVariables + }, + onChartReady: (controls) => { + chartControlsRef.current = controls + } + } + + // Load session status from backend (optional - session may not exist until started) + const refreshSessionStatus = useCallback(async () => { + try { + const response = await api.getPlotSession(plotDefinition.id) + if (response?.config) { + setSession(prev => ({ + ...prev, + is_active: response.config.is_active || false, + is_paused: response.config.is_paused || false + })) + } + } catch (error) { + // Session may not exist in backend yet + if (error.message.includes('404')) { + // Try to create the session automatically + await createPlotSessionFromConfig() + } else { + // Backend not available - use local state silently + // This allows the component to work even when backend is offline + } + } + }, [plotDefinition.id]) + + // Create plot session in backend based on static configuration + const createPlotSessionFromConfig = useCallback(async () => { + try { + // Convert plotVariables array to the format expected by the API + const variableNames = plotVariables.map(v => v.variable_name) + + const plotConfig = { + session_id: plotDefinition.id, + name: plotDefinition.name, + variables: variableNames, + time_window: plotDefinition.time_window || 60, + trigger_enabled: plotDefinition.trigger_enabled || false, + trigger_variable: plotDefinition.trigger_variable, + trigger_on_true: plotDefinition.trigger_on_true || true, + y_min: plotDefinition.y_min, + y_max: plotDefinition.y_max + } + + // Create the plot session + await api.createPlot(plotConfig) + + console.log(`βœ… Created plot session: ${plotDefinition.id}`) + } catch (error) { + console.warn(`Could not create plot session ${plotDefinition.id}:`, error) + } + }, [plotDefinition, plotVariables]) + + // Control plot session (start, pause, stop, clear) + const handleControlClick = async (action) => { + // Apply immediate local feedback + if (chartControlsRef.current) { + switch (action) { + case 'pause': + chartControlsRef.current.pauseStreaming() + setSession(prev => ({ ...prev, is_paused: true })) + break + case 'start': + case 'resume': + chartControlsRef.current.resumeStreaming() + setSession(prev => ({ ...prev, is_active: true, is_paused: false })) + break + case 'clear': + chartControlsRef.current.clearChart() + break + case 'stop': + chartControlsRef.current.pauseStreaming() + setSession(prev => ({ ...prev, is_active: false, is_paused: false })) + break + } + } + + // Send command to backend + try { + // For 'start' action, create the plot session first if it doesn't exist + if (action === 'start') { + try { + // Try to create the plot session with current configuration + await api.createPlot({ + session_id: plotDefinition.id, + name: plotDefinition.name, + variables: plotVariables.map(v => v.variable_name), // Simplified format + time_window: localConfig.time_window, + trigger_enabled: localConfig.trigger_enabled, + trigger_variable: localConfig.trigger_variable, + trigger_on_true: localConfig.trigger_on_true, + y_min: localConfig.y_min, + y_max: localConfig.y_max + }) + } catch (createError) { + // Plot may already exist, that's OK + console.log('Plot session may already exist:', createError.message) + } + } + + await api.controlPlotSession(plotDefinition.id, action) + // Refresh status after backend command + setTimeout(refreshSessionStatus, 500) + } catch (error) { + toast({ + title: `❌ Failed to ${action} plot`, + description: error.message, + status: 'error', + duration: 3000 + }) + + // Revert local state on error + await refreshSessionStatus() + } + } + + // Apply configuration changes + const applyConfigChanges = async () => { + try { + // Update backend configuration + await onConfigUpdate?.(plotDefinition.id, localConfig) + + // Apply changes to chart if possible + if (chartControlsRef.current?.updateConfig) { + chartControlsRef.current.updateConfig(localConfig) + } + + toast({ + title: 'βœ… Configuration updated', + status: 'success', + duration: 2000 + }) + + setShowSettings(false) + } catch (error) { + toast({ + title: '❌ Failed to update configuration', + description: error.message, + status: 'error', + duration: 3000 + }) + } + } + + const resetConfigChanges = () => { + setLocalConfig({ + time_window: plotDefinition.time_window || 60, + y_min: plotDefinition.y_min, + y_max: plotDefinition.y_max, + trigger_enabled: plotDefinition.trigger_enabled || false, + trigger_variable: plotDefinition.trigger_variable, + trigger_on_true: plotDefinition.trigger_on_true || true + }) + setShowSettings(false) + } + + // Auto-refresh session status + useEffect(() => { + // Try to get session status first, if it fails, create the session + refreshSessionStatus() + intervalRef.current = setInterval(refreshSessionStatus, 5000) + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current) + } + } + }, [refreshSessionStatus]) + + return ( + + + + + πŸ“ˆ {plotDefinition.name || plotDefinition.id} + + Variables: {plotVariables.length} | + Status: + {session.is_active + ? (session.is_paused ? 'Paused' : 'Active') + : 'Stopped' + } + + {localConfig.trigger_enabled && ( + <> | Trigger: {localConfig.trigger_variable} + )} + + + + + } + size="sm" + variant="outline" + aria-label="Settings" + onClick={() => setShowSettings(!showSettings)} + /> + + + + + + + {/* Settings Panel */} + {showSettings && ( + + + + + Time Window (seconds) + setLocalConfig(prev => ({ + ...prev, + time_window: parseInt(valueString) || 60 + }))} + min={10} + max={3600} + size="sm" + > + + + + + + + + + + + + Y Min (auto if empty) + setLocalConfig(prev => ({ + ...prev, + y_min: valueString === '' ? null : parseFloat(valueString) + }))} + size="sm" + > + + + + + + + + + + + + Y Max (auto if empty) + setLocalConfig(prev => ({ + ...prev, + y_max: valueString === '' ? null : parseFloat(valueString) + }))} + size="sm" + > + + + + + + + + + + + + Enable Trigger + setLocalConfig(prev => ({ + ...prev, + trigger_enabled: e.target.checked + }))} + size="sm" + /> + + + + + + + + + + )} + + {/* Chart.js Plot */} + + + + + {/* Control Buttons */} + + + + + + + + + ) +} diff --git a/frontend/src/components/rjsf/widgets.jsx b/frontend/src/components/rjsf/widgets.jsx index 27f83f2..3c46af4 100644 --- a/frontend/src/components/rjsf/widgets.jsx +++ b/frontend/src/components/rjsf/widgets.jsx @@ -49,7 +49,7 @@ export const UpDownWidget = ({ id, placeholder, required, readonly, disabled, la {label && {label}} onChange(isNaN(num) ? undefined : num)} onBlur={onBlur && (() => onBlur(id, value))} onFocus={onFocus && (() => onFocus(id, value))} diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 748d114..7bf8e37 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -193,4 +193,15 @@ export async function getPlotVariables() { return toJsonOrThrow(res) } +// Plot session status and control (aliases for existing functions) +export async function getPlotSession(sessionId) { + // Use existing getPlotConfig to get session info + return await getPlotConfig(sessionId) +} + +export async function controlPlotSession(sessionId, action) { + // Use existing controlPlot function + return await controlPlot(sessionId, action) +} +