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.

This commit is contained in:
Miguel 2025-08-14 00:06:43 +02:00
parent bb275dd279
commit 748e8d5b0e
7 changed files with 655 additions and 369 deletions

View File

@ -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
}

View File

@ -7,7 +7,7 @@
},
"plc_config": {
"ip": "10.1.33.11",
"rack": 1,
"rack": 0,
"slot": 2
},
"udp_config": {

View File

@ -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"
}

View File

@ -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() {
</Button>
</Flex>
{/* Active Plots Overview */}
{/* Active Plot Sessions with Real Chart.js Plots */}
<Card bg={cardBg} borderColor={borderColor}>
<CardHeader>
<Heading size="md">🎛 Active Plot Sessions</Heading>
<Text fontSize="sm" color="gray.500" mt={1}>
Real-time Chart.js plots with streaming data from PLC
</Text>
</CardHeader>
<CardBody>
{Object.keys(plots).length === 0 ? (
<Text color="gray.500" textAlign="center" py={4}>
No active plot sessions
{getPlotDefinitions().length === 0 ? (
<Text color="gray.500" textAlign="center" py={8}>
No plot sessions configured. Create plot definitions below to get started.
</Text>
) : (
<TableContainer>
<Table size="sm">
<Thead>
<Tr>
<Th>Session ID</Th>
<Th>Name</Th>
<Th>Status</Th>
<Th>Variables</Th>
<Th>Actions</Th>
</Tr>
</Thead>
<Tbody>
{Object.entries(plots).map(([sessionId, plot]) => (
<Tr key={sessionId}>
<Td>
<Text fontSize="sm" fontFamily="mono">
{sessionId}
</Text>
</Td>
<Td>{plot.name || 'Unnamed Plot'}</Td>
<Td>
<Badge
colorScheme={plot.running ? 'green' : 'gray'}
>
{plot.running ? 'Running' : 'Stopped'}
</Badge>
</Td>
<Td>
<Badge variant="outline">
{plot.variable_count || 0} vars
</Badge>
</Td>
<Td>
<HStack spacing={1}>
{plot.running ? (
<Button
size="xs"
colorScheme="red"
variant="outline"
onClick={() => handlePlotControl(sessionId, 'stop')}
isLoading={actionLoading[`${sessionId}_stop`]}
>
Stop
</Button>
) : (
<Button
size="xs"
colorScheme="green"
variant="outline"
onClick={() => handlePlotControl(sessionId, 'start')}
isLoading={actionLoading[`${sessionId}_start`]}
>
Start
</Button>
)}
<Button
size="xs"
colorScheme="blue"
variant="outline"
onClick={() => handlePlotControl(sessionId, 'clear')}
isLoading={actionLoading[`${sessionId}_clear`]}
>
🧹 Clear
</Button>
<Button
size="xs"
colorScheme="red"
variant="ghost"
onClick={() => confirmDelete(sessionId)}
isLoading={actionLoading[`${sessionId}_delete`]}
>
🗑
</Button>
</HStack>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
<VStack spacing={6} align="stretch">
{getPlotDefinitions().map((plotDef) => (
<PlotRealtimeSession
key={plotDef.id}
plotDefinition={plotDef}
plotVariables={getPlotVariables(plotDef.id)}
onConfigUpdate={handlePlotConfigUpdate}
onRemove={handlePlotRemove}
/>
))}
</VStack>
)}
</CardBody>
</Card>
@ -403,40 +349,6 @@ export default function PlotManager() {
</TabPanel>
</TabPanels>
</Tabs>
{/* Delete Confirmation Dialog */}
<AlertDialog
isOpen={isOpen}
leastDestructiveRef={cancelRef}
onClose={onClose}
>
<AlertDialogOverlay>
<AlertDialogContent>
<AlertDialogHeader fontSize="lg" fontWeight="bold">
Delete Plot Session
</AlertDialogHeader>
<AlertDialogBody>
Are you sure you want to delete plot session "{selectedPlot}"?
This action cannot be undone.
</AlertDialogBody>
<AlertDialogFooter>
<Button ref={cancelRef} onClick={onClose}>
Cancel
</Button>
<Button
colorScheme="red"
onClick={() => handleDeletePlot(selectedPlot)}
ml={3}
isLoading={actionLoading[`${selectedPlot}_delete`]}
>
Delete
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialog>
</VStack>
)
}

View File

@ -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 (
<Card bg={cardBg} borderColor={borderColor} shadow="md">
<CardHeader pb={2}>
<HStack align="center">
<Box>
<Heading size="sm">📈 {plotDefinition.name || plotDefinition.id}</Heading>
<Text fontSize="sm" color={muted} mt={1}>
Variables: {plotVariables.length} |
Status: <strong>
{session.is_active
? (session.is_paused ? 'Paused' : 'Active')
: 'Stopped'
}
</strong>
{localConfig.trigger_enabled && (
<> | Trigger: {localConfig.trigger_variable}</>
)}
</Text>
</Box>
<Spacer />
<HStack>
<IconButton
icon={<SettingsIcon />}
size="sm"
variant="outline"
aria-label="Settings"
onClick={() => setShowSettings(!showSettings)}
/>
<Button
size="sm"
colorScheme="red"
variant="ghost"
onClick={() => onRemove?.(plotDefinition.id)}
>
</Button>
</HStack>
</HStack>
</CardHeader>
<CardBody pt={0}>
{/* Settings Panel */}
{showSettings && (
<Box mb={4} p={4} bg={useColorModeValue('gray.50', 'gray.600')} borderRadius="md">
<Grid templateColumns="repeat(auto-fit, minmax(200px, 1fr))" gap={4}>
<GridItem>
<FormControl>
<FormLabel fontSize="sm">Time Window (seconds)</FormLabel>
<NumberInput
value={localConfig.time_window}
onChange={(valueString) => setLocalConfig(prev => ({
...prev,
time_window: parseInt(valueString) || 60
}))}
min={10}
max={3600}
size="sm"
>
<NumberInputField />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
</FormControl>
</GridItem>
<GridItem>
<FormControl>
<FormLabel fontSize="sm">Y Min (auto if empty)</FormLabel>
<NumberInput
value={localConfig.y_min || ''}
onChange={(valueString) => setLocalConfig(prev => ({
...prev,
y_min: valueString === '' ? null : parseFloat(valueString)
}))}
size="sm"
>
<NumberInputField placeholder="Auto" />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
</FormControl>
</GridItem>
<GridItem>
<FormControl>
<FormLabel fontSize="sm">Y Max (auto if empty)</FormLabel>
<NumberInput
value={localConfig.y_max || ''}
onChange={(valueString) => setLocalConfig(prev => ({
...prev,
y_max: valueString === '' ? null : parseFloat(valueString)
}))}
size="sm"
>
<NumberInputField placeholder="Auto" />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
</FormControl>
</GridItem>
<GridItem>
<FormControl>
<FormLabel fontSize="sm">Enable Trigger</FormLabel>
<Switch
isChecked={localConfig.trigger_enabled}
onChange={(e) => setLocalConfig(prev => ({
...prev,
trigger_enabled: e.target.checked
}))}
size="sm"
/>
</FormControl>
</GridItem>
</Grid>
<Flex mt={4} gap={2}>
<Button size="sm" colorScheme="blue" onClick={applyConfigChanges}>
💾 Apply
</Button>
<Button size="sm" variant="outline" onClick={resetConfigChanges}>
Cancel
</Button>
</Flex>
</Box>
)}
{/* Chart.js Plot */}
<Box mb={4}>
<ChartjsPlot session={enhancedSession} height="320px" />
</Box>
{/* Control Buttons */}
<HStack spacing={2} justify="center">
<Button
size="sm"
onClick={() => handleControlClick('start')}
colorScheme="green"
isDisabled={session.is_active && !session.is_paused}
>
Start
</Button>
<Button
size="sm"
onClick={() => handleControlClick('pause')}
colorScheme="yellow"
isDisabled={!session.is_active || session.is_paused}
>
Pause
</Button>
<Button
size="sm"
onClick={() => handleControlClick('clear')}
variant="outline"
>
🗑 Clear
</Button>
<Button
size="sm"
onClick={() => handleControlClick('stop')}
colorScheme="red"
isDisabled={!session.is_active}
>
Stop
</Button>
</HStack>
</CardBody>
</Card>
)
}

View File

@ -49,7 +49,7 @@ export const UpDownWidget = ({ id, placeholder, required, readonly, disabled, la
{label && <FormLabel htmlFor={id}>{label}</FormLabel>}
<NumberInput
id={id}
value={value || ''}
value={value ?? ''}
onChange={(_, num) => onChange(isNaN(num) ? undefined : num)}
onBlur={onBlur && (() => onBlur(id, value))}
onFocus={onFocus && (() => onFocus(id, value))}

View File

@ -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)
}