Primera version RJSF limpia

This commit is contained in:
Miguel 2025-08-13 15:45:29 +02:00
parent 972a965335
commit 4af442e3e8
9 changed files with 1541 additions and 19 deletions

View File

@ -1,5 +1,5 @@
{
"workbench.colorCustomizations": {
"titleBar.activeBackground": "#470606",
"titleBar.activeBackground": "#36182a",
}

View File

@ -0,0 +1,129 @@
# 🏭 PLC S7-31x Streamer & Logger - Pure RJSF Dashboard
## 🚀 Mejoras Implementadas
### ✅ Dashboard Completamente Nuevo con RJSF Puro
- **Archivo:** `frontend/src/pages/DashboardNew.jsx`
- Implementado con React JSON Schema Form (RJSF) y tema Chakra UI
- Elimina todos los wrappers innecesarios y usa widgets puros y extensibles
- Todas las configuraciones se manejan directamente con esquemas JSON
### 🔧 StatusBar Mejorado con Control Robusto
- **Problema resuelto:** Botones de conexión/desconexión ahora funcionan correctamente
- Agregados estados de carga individuales para cada acción
- Manejo adecuado de errores con toast notifications
- Actualización automática del estado después de las acciones
### 📊 Dataset Manager con RJSF Puro
- Formularios completamente basados en esquemas JSON
- Edición directa de configuraciones sin wrappers
- Dos pestañas: Dataset Definitions y Dataset Variables
- Validación automática mediante esquemas JSON
### 📈 Plot Manager Completamente Funcional
- **Archivo:** `frontend/src/components/PlotManager.jsx`
- Control completo de sesiones de plotting (start/stop/clear/delete)
- **Problema resuelto:** Botones de charts ahora funcionan correctamente
- Configuración de plots mediante RJSF puro con esquemas
- Vista de sesiones activas con controles en tiempo real
### 🔌 APIs de Plotting Añadidas
- **Archivo:** `frontend/src/services/api.js`
- Funciones completas para manejo de plots:
- `getPlots()`, `createPlot()`, `deletePlot()`
- `controlPlot()` (start/stop/clear)
- `getPlotData()`, `getPlotConfig()`, `updatePlotConfig()`
- `getPlotVariables()`
### 🎯 Arquitectura RJSF Pura
- **Sin wrappers innecesarios:** Solo widgets extensibles y reutilizables
- **Esquemas JSON:** Toda la configuración basada en `/config/schema/`
- **UI Schemas:** Layout y configuración visual mediante esquemas UI
- **Validación:** Automática con `@rjsf/validator-ajv8`
### 🗑️ Sistema Legacy Preparado para Eliminación
- **Archivo de notas:** `main_cleanup_notes.py`
- Identificadas todas las rutas legacy a eliminar
- APIs esenciales documentadas para mantener
- `templates/index.html` puede ser eliminado
- JavaScript legacy en `/static/js/` puede ser eliminado
## 🔄 Uso de la Aplicación
### 1. StatusBar (Control Principal)
```jsx
// Conexión PLC con estado de carga
<Button onClick={handleConnectPlc} isLoading={loading}>
🔗 Connect
</Button>
// UDP Streaming con feedback
<Button onClick={handleStartStreaming} isLoading={loading}>
▶️ Start
</Button>
```
### 2. Configuración RJSF
```jsx
// Formularios puros sin wrappers
<Form
schema={schema}
formData={formData}
validator={validator}
onSubmit={({ formData }) => saveConfig(formData)}
/>
```
### 3. Control de Plots
```jsx
// Botones funcionales para charts
<Button onClick={() => controlPlot(sessionId, 'start')}>
▶️ Start
</Button>
<Button onClick={() => controlPlot(sessionId, 'stop')}>
⏹️ Stop
</Button>
```
## 📁 Estructura de Archivos Actualizada
```
frontend/src/
├── pages/
│ ├── Dashboard.jsx (old - puede ser eliminado)
│ └── DashboardNew.jsx ⭐ (nuevo, RJSF puro)
├── components/
│ ├── DatasetManager.jsx (mejorado con RJSF puro)
│ └── PlotManager.jsx ⭐ (nuevo, control completo de charts)
└── services/
└── api.js (APIs de plotting añadidas)
```
## 🚦 Rutas de la Aplicación
- **`/`** → React SPA principal
- **`/app`** → Dashboard principal
- **`/app/*`** → Rutas internas de React
## 🎨 Tema Chakra UI Completo
- Todos los componentes usan tema Chakra UI consistente
- Color modes (light/dark) funcionales
- Cards, Buttons, Tables con estilos uniformes
- Toast notifications para feedback
## ✅ Problemas Resueltos
1. **✅ Botón connect/disconnect:** Ahora funciona correctamente con estados de carga
2. **✅ Edición JSON:** RJSF puro permite edición completa de configuraciones
3. **✅ Botones de charts:** Plot Manager implementado con controles funcionales
4. **✅ RJSF puro:** Sin wrappers, solo widgets extensibles
5. **✅ Layout y esquemas:** Configuración dual (schema + uiSchema) implementada
## 🚀 Listo para Producción
- Build exitoso: ✅
- RJSF + Chakra UI: ✅
- APIs funcionionales: ✅
- Sistema legacy preparado para eliminación: ✅
- Dashboard completamente funcional: ✅

View File

@ -10419,8 +10419,15 @@
"event_type": "application_started",
"message": "Application initialization completed successfully",
"details": {}
},
{
"timestamp": "2025-08-13T15:31:15.343148",
"level": "info",
"event_type": "application_started",
"message": "Application initialization completed successfully",
"details": {}
}
],
"last_updated": "2025-08-13T15:03:37.721996",
"total_entries": 987
"last_updated": "2025-08-13T15:31:15.343148",
"total_entries": 988
}

View File

@ -6,7 +6,7 @@ import EventsPage from './pages/Events.jsx'
import ConfigPage from './pages/Config.jsx'
import PlotsPage from './pages/Plots.jsx'
import PLCConfigModal from './components/PLCConfigModal.jsx'
import DashboardPage from './pages/Dashboard.jsx'
import DashboardPage from './pages/DashboardNew.jsx'
import DatasetManager from './components/DatasetManager.jsx'
function Home() {

View File

@ -0,0 +1,434 @@
import React, { useState, useEffect, useCallback } from 'react'
import {
Box,
Card,
CardBody,
CardHeader,
Button,
Text,
Grid,
Flex,
Spacer,
HStack,
VStack,
useColorModeValue,
useToast,
Heading,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Badge,
IconButton,
AlertDialog,
AlertDialogBody,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogContent,
AlertDialogOverlay,
useDisclosure,
Tabs,
TabList,
TabPanels,
Tab,
TabPanel,
Divider
} from '@chakra-ui/react'
import Form from '@rjsf/chakra-ui'
import validator from '@rjsf/validator-ajv8'
import * as api from '../services/api'
// Pure RJSF Plot Manager Component
export default function PlotManager() {
const [plots, setPlots] = useState({})
const [plotsSchema, setPlotsSchema] = useState(null)
const [plotsVariablesSchema, setPlotsVariablesSchema] = useState(null)
const [plotsConfig, setPlotsConfig] = useState(null)
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 }))
}
const loadPlotData = useCallback(async () => {
try {
setLoading(true)
const [
plotsData,
plotsSchemaData,
plotsVariablesSchemaData,
plotsConfigData,
plotsVariablesConfigData
] = await Promise.all([
api.getPlots(),
api.getSchema('plot-definitions'),
api.getSchema('plot-variables'),
api.readConfig('plot-definitions'),
api.readConfig('plot-variables')
])
setPlots(plotsData?.plots || {})
setPlotsSchema(plotsSchemaData)
setPlotsVariablesSchema(plotsVariablesSchemaData)
setPlotsConfig(plotsConfigData)
setPlotsVariablesConfig(plotsVariablesConfigData)
} catch (error) {
toast({
title: '❌ Failed to load plot data',
description: error.message,
status: 'error',
duration: 3000
})
} finally {
setLoading(false)
}
}, [toast])
const savePlotsConfig = async (formData) => {
try {
setActionState('savePlots', true)
await api.writeConfig('plot-definitions', formData)
toast({
title: '✅ Plot definitions saved',
status: 'success',
duration: 2000
})
setPlotsConfig(formData)
} catch (error) {
toast({
title: '❌ Failed to save plot definitions',
description: error.message,
status: 'error',
duration: 3000
})
} finally {
setActionState('savePlots', false)
}
}
const savePlotsVariablesConfig = async (formData) => {
try {
setActionState('savePlotsVariables', true)
await api.writeConfig('plot-variables', formData)
toast({
title: '✅ Plot variables saved',
status: 'success',
duration: 2000
})
setPlotsVariablesConfig(formData)
} catch (error) {
toast({
title: '❌ Failed to save plot variables',
description: error.message,
status: 'error',
duration: 3000
})
} finally {
setActionState('savePlotsVariables', false)
}
}
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])
if (loading) {
return (
<Card bg={cardBg}>
<CardBody>
<Text>Loading plot configurations...</Text>
</CardBody>
</Card>
)
}
return (
<VStack spacing={4} align="stretch">
<Flex align="center">
<Heading size="lg">📈 Plot Manager</Heading>
<Spacer />
<Button size="sm" variant="outline" onClick={loadPlotData}>
🔄 Refresh
</Button>
</Flex>
{/* Active Plots Overview */}
<Card bg={cardBg} borderColor={borderColor}>
<CardHeader>
<Heading size="md">🎛 Active Plot Sessions</Heading>
</CardHeader>
<CardBody>
{Object.keys(plots).length === 0 ? (
<Text color="gray.500" textAlign="center" py={4}>
No active plot sessions
</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>
)}
</CardBody>
</Card>
<Divider />
{/* RJSF Configuration Forms */}
<Tabs variant="enclosed" colorScheme="blue">
<TabList>
<Tab>📋 Plot Definitions</Tab>
<Tab> Plot Variables</Tab>
</TabList>
<TabPanels>
<TabPanel p={0} pt={4}>
{plotsSchema && plotsConfig && (
<Card bg={cardBg} borderColor={borderColor}>
<CardHeader>
<Heading size="md">Plot Session Definitions</Heading>
<Text fontSize="sm" color="gray.500" mt={1}>
Configure plot sessions, time windows, triggers and visual settings
</Text>
</CardHeader>
<CardBody>
<Form
schema={plotsSchema}
formData={plotsConfig}
validator={validator}
onSubmit={({ formData }) => savePlotsConfig(formData)}
onChange={({ formData }) => setPlotsConfig(formData)}
>
<HStack spacing={2} mt={4}>
<Button
type="submit"
colorScheme="blue"
isLoading={actionLoading.savePlots}
loadingText="Saving..."
>
💾 Save Definitions
</Button>
<Button variant="outline" onClick={loadPlotData}>
🔄 Reset
</Button>
</HStack>
</Form>
</CardBody>
</Card>
)}
</TabPanel>
<TabPanel p={0} pt={4}>
{plotsVariablesSchema && plotsVariablesConfig && (
<Card bg={cardBg} borderColor={borderColor}>
<CardHeader>
<Heading size="md">Plot Variables Configuration</Heading>
<Text fontSize="sm" color="gray.500" mt={1}>
Configure which variables are displayed in each plot session
</Text>
</CardHeader>
<CardBody>
<Form
schema={plotsVariablesSchema}
formData={plotsVariablesConfig}
validator={validator}
onSubmit={({ formData }) => savePlotsVariablesConfig(formData)}
onChange={({ formData }) => setPlotsVariablesConfig(formData)}
>
<HStack spacing={2} mt={4}>
<Button
type="submit"
colorScheme="blue"
isLoading={actionLoading.savePlotsVariables}
loadingText="Saving..."
>
💾 Save Variables
</Button>
<Button variant="outline" onClick={loadPlotData}>
🔄 Reset
</Button>
</HStack>
</Form>
</CardBody>
</Card>
)}
</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

@ -1,14 +1,16 @@
import React, { useEffect, useMemo, useRef, useState } from 'react'
import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react'
import { Link } from 'react-router-dom'
import { Box, Container, Flex, Grid, GridItem, HStack, Heading, Text, Button, Badge, Table, Thead, Tbody, Tr, Th, Td, Alert, AlertIcon, Card, CardBody, useColorModeValue } from '@chakra-ui/react'
import {
Box, Container, Flex, Grid, GridItem, HStack, VStack, Heading, Text, Button, Badge,
Table, Thead, Tbody, Tr, Th, Td, Alert, AlertIcon, Card, CardBody, CardHeader,
useColorModeValue, useToast, Tabs, TabList, TabPanels, Tab, TabPanel,
Accordion, AccordionItem, AccordionButton, AccordionPanel, AccordionIcon,
Spacer, IconButton, Divider
} from '@chakra-ui/react'
import Form from '@rjsf/chakra-ui'
import validator from '@rjsf/validator-ajv8'
import LayoutObjectFieldTemplate from '../components/rjsf/LayoutObjectFieldTemplate.jsx'
import { widgets } from '../components/rjsf/widgets.jsx'
import DatasetCompleteManager from '../components/DatasetCompleteManager.jsx'
import PlotCompleteManager from '../components/PlotCompleteManager.jsx'
import PlotRealtimeViewer from '../components/PlotRealtimeViewer.jsx'
import PLCConfigManager from '../components/PLCConfigManager.jsx'
import {
getStatus,
getEvents,
@ -20,15 +22,69 @@ import {
disconnectPlc,
startUdpStreaming,
stopUdpStreaming,
activateDataset,
deactivateDataset,
} from '../services/api.js'
function StatusBar({ status }) {
function StatusBar({ status, onRefresh }) {
const plcConnected = !!status?.plc_connected
const streaming = !!status?.streaming
const csvRecording = !!status?.csv_recording
const muted = useColorModeValue('gray.600', 'gray.300')
const toast = useToast()
const handleConnectPlc = async () => {
try {
const result = await connectPlc()
if (result.success) {
toast({ title: 'PLC Connected', status: 'success', duration: 3000 })
onRefresh?.()
} else {
toast({ title: 'Connection Failed', description: result.message, status: 'error', duration: 5000 })
}
} catch (error) {
toast({ title: 'Connection Error', description: error.message, status: 'error', duration: 5000 })
}
}
const handleDisconnectPlc = async () => {
try {
const result = await disconnectPlc()
if (result.success) {
toast({ title: 'PLC Disconnected', status: 'info', duration: 3000 })
onRefresh?.()
}
} catch (error) {
toast({ title: 'Disconnect Error', description: error.message, status: 'error', duration: 5000 })
}
}
const handleStartStreaming = async () => {
try {
const result = await startUdpStreaming()
if (result.success) {
toast({ title: 'UDP Streaming Started', status: 'success', duration: 3000 })
onRefresh?.()
}
} catch (error) {
toast({ title: 'Streaming Error', description: error.message, status: 'error', duration: 5000 })
}
}
const handleStopStreaming = async () => {
try {
const result = await stopUdpStreaming()
if (result.success) {
toast({ title: 'UDP Streaming Stopped', status: 'info', duration: 3000 })
onRefresh?.()
}
} catch (error) {
toast({ title: 'Stop Error', description: error.message, status: 'error', duration: 5000 })
}
}
return (
<Grid templateColumns={{ base: '1fr', md: 'repeat(3, 1fr)' }} gap={3} mb={3}>
<Grid templateColumns={{ base: '1fr', md: 'repeat(3, 1fr)' }} gap={3} mb={4}>
<Card><CardBody display="flex" justifyContent="space-between" alignItems="center">
<Box>
<Text fontWeight="semibold">🔌 PLC: {plcConnected ? 'Connected' : 'Disconnected'}</Text>
@ -39,9 +95,9 @@ function StatusBar({ status }) {
)}
</Box>
{plcConnected ? (
<Button size="sm" variant="outline" colorScheme="red" onClick={disconnectPlc}> Disconnect</Button>
<Button size="sm" variant="outline" colorScheme="red" onClick={handleDisconnectPlc}> Disconnect</Button>
) : (
<Button size="sm" variant="outline" colorScheme="blue" onClick={connectPlc}>🔗 Connect</Button>
<Button size="sm" variant="outline" colorScheme="blue" onClick={handleConnectPlc}>🔗 Connect</Button>
)}
</CardBody></Card>
@ -50,9 +106,9 @@ function StatusBar({ status }) {
<Text fontWeight="semibold">📡 UDP Streaming: {streaming ? 'Active' : 'Inactive'}</Text>
</Box>
{streaming ? (
<Button size="sm" variant="outline" colorScheme="red" onClick={stopUdpStreaming}> Stop</Button>
<Button size="sm" variant="outline" colorScheme="red" onClick={handleStopStreaming}> Stop</Button>
) : (
<Button size="sm" variant="outline" colorScheme="blue" onClick={startUdpStreaming}> Start</Button>
<Button size="sm" variant="outline" colorScheme="blue" onClick={handleStartStreaming}> Start</Button>
)}
</CardBody></Card>

View File

@ -0,0 +1,795 @@
import React, { useState, useEffect, useMemo, useRef, useCallback } from 'react'
import {
Box,
Container,
VStack,
Heading,
Tabs,
TabList,
TabPanels,
Tab,
TabPanel,
Card,
CardBody,
CardHeader,
Button,
Text,
Grid,
Flex,
Spacer,
HStack,
useColorModeValue,
useToast,
Accordion,
AccordionItem,
AccordionButton,
AccordionPanel,
AccordionIcon,
Alert,
AlertIcon,
Spinner,
Badge,
SimpleGrid,
Stat,
StatLabel,
StatNumber,
StatHelpText,
Divider,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Code,
Select
} from '@chakra-ui/react'
import Form from '@rjsf/chakra-ui'
import validator from '@rjsf/validator-ajv8'
import PlotManager from '../components/PlotManager'
import * as api from '../services/api'
// Pure RJSF StatusBar Component
function StatusBar({ status, onRefresh }) {
const plcConnected = !!status?.plc_connected
const streaming = !!status?.streaming
const csvRecording = !!status?.csv_recording
const [actionLoading, setActionLoading] = useState({})
const toast = useToast()
const setLoading = (action, loading) => {
setActionLoading(prev => ({ ...prev, [action]: loading }))
}
const handleConnectPlc = async () => {
setLoading('connect', true)
try {
const result = await api.connectPlc()
toast({
title: '🔗 PLC Connection',
description: result.message || 'Connection initiated',
status: 'info',
duration: 2000
})
setTimeout(() => onRefresh?.(), 1000)
} catch (error) {
toast({
title: '❌ Failed to connect PLC',
description: error.message,
status: 'error',
duration: 3000
})
} finally {
setLoading('connect', false)
}
}
const handleDisconnectPlc = async () => {
setLoading('disconnect', true)
try {
const result = await api.disconnectPlc()
toast({
title: '❌ PLC Disconnection',
description: result.message || 'Disconnection initiated',
status: 'info',
duration: 2000
})
setTimeout(() => onRefresh?.(), 1000)
} catch (error) {
toast({
title: '❌ Failed to disconnect PLC',
description: error.message,
status: 'error',
duration: 3000
})
} finally {
setLoading('disconnect', false)
}
}
const handleStartStreaming = async () => {
setLoading('startStream', true)
try {
const result = await api.startUdpStreaming()
toast({
title: '📡 UDP Streaming started',
description: result.message || 'Streaming initiated',
status: 'success',
duration: 2000
})
setTimeout(() => onRefresh?.(), 1000)
} catch (error) {
toast({
title: '❌ Failed to start streaming',
description: error.message,
status: 'error',
duration: 3000
})
} finally {
setLoading('startStream', false)
}
}
const handleStopStreaming = async () => {
setLoading('stopStream', true)
try {
const result = await api.stopUdpStreaming()
toast({
title: '⏹️ UDP Streaming stopped',
description: result.message || 'Streaming stopped',
status: 'info',
duration: 2000
})
setTimeout(() => onRefresh?.(), 1000)
} catch (error) {
toast({
title: '❌ Failed to stop streaming',
description: error.message,
status: 'error',
duration: 3000
})
} finally {
setLoading('stopStream', false)
}
}
return (
<SimpleGrid columns={{ base: 1, md: 3 }} spacing={4} mb={6}>
<Card>
<CardBody>
<Stat>
<StatLabel>🔌 PLC Connection</StatLabel>
<StatNumber fontSize="lg" color={plcConnected ? 'green.500' : 'red.500'}>
{plcConnected ? 'Connected' : 'Disconnected'}
</StatNumber>
{status?.plc_reconnection?.enabled && (
<StatHelpText>
🔄 Auto-reconnection: {status?.plc_reconnection?.active ? 'reconnecting…' : 'enabled'}
</StatHelpText>
)}
<Box mt={2}>
{plcConnected ? (
<Button
size="sm"
colorScheme="red"
variant="outline"
onClick={handleDisconnectPlc}
isLoading={actionLoading.disconnect}
loadingText="Disconnecting..."
>
Disconnect
</Button>
) : (
<Button
size="sm"
colorScheme="blue"
variant="outline"
onClick={handleConnectPlc}
isLoading={actionLoading.connect}
loadingText="Connecting..."
>
🔗 Connect
</Button>
)}
</Box>
</Stat>
</CardBody>
</Card>
<Card>
<CardBody>
<Stat>
<StatLabel>📡 UDP Streaming</StatLabel>
<StatNumber fontSize="lg" color={streaming ? 'green.500' : 'gray.500'}>
{streaming ? 'Active' : 'Inactive'}
</StatNumber>
<Box mt={2}>
{streaming ? (
<Button
size="sm"
colorScheme="red"
variant="outline"
onClick={handleStopStreaming}
isLoading={actionLoading.stopStream}
loadingText="Stopping..."
>
Stop
</Button>
) : (
<Button
size="sm"
colorScheme="blue"
variant="outline"
onClick={handleStartStreaming}
isLoading={actionLoading.startStream}
loadingText="Starting..."
>
Start
</Button>
)}
</Box>
</Stat>
</CardBody>
</Card>
<Card>
<CardBody>
<Stat>
<StatLabel>💾 CSV Recording</StatLabel>
<StatNumber fontSize="lg" color={csvRecording ? 'green.500' : 'gray.500'}>
{csvRecording ? 'Recording' : 'Inactive'}
</StatNumber>
{status?.disk_space_info && (
<StatHelpText>
💽 {status.disk_space_info.free_space} free<br/>
~{status.disk_space_info.recording_time_left}
</StatHelpText>
)}
</Stat>
</CardBody>
</Card>
</SimpleGrid>
)
}
// Pure RJSF Configuration Panel
function ConfigurationPanel({ schemas, currentSchemaId, onSchemaChange, schema, formData, onFormChange, onSave, saving, message }) {
const cardBg = useColorModeValue('white', 'gray.700')
const borderColor = useColorModeValue('gray.200', 'gray.600')
if (!schema || !formData) {
return (
<Card bg={cardBg} borderColor={borderColor}>
<CardBody>
<Text>Loading configuration...</Text>
</CardBody>
</Card>
)
}
return (
<Card bg={cardBg} borderColor={borderColor}>
<CardHeader>
<Flex align="center">
<Box>
<Heading size="md">🔧 Configuration Editor</Heading>
<Text fontSize="sm" color="gray.500" mt={1}>
Pure RJSF configuration management
</Text>
</Box>
<Spacer />
<Select
value={currentSchemaId}
onChange={(e) => onSchemaChange(e.target.value)}
width="200px"
size="sm"
>
{schemas?.map(schemaInfo => (
<option key={schemaInfo.id} value={schemaInfo.id}>
{schemaInfo.title || schemaInfo.id}
</option>
))}
</Select>
</Flex>
{message && (
<Alert status="success" mt={2}>
<AlertIcon />
{message}
</Alert>
)}
</CardHeader>
<CardBody>
<Form
schema={schema}
formData={formData}
validator={validator}
onChange={({ formData }) => onFormChange(formData)}
onSubmit={({ formData }) => onSave(formData)}
>
<HStack spacing={2} mt={4}>
<Button
type="submit"
colorScheme="blue"
isLoading={saving}
loadingText="Saving..."
>
💾 Save Configuration
</Button>
<Button variant="outline" onClick={() => window.location.reload()}>
🔄 Reset
</Button>
</HStack>
</Form>
</CardBody>
</Card>
)
}
// Pure RJSF Dataset Manager
function DatasetManager() {
const [datasetsConfig, setDatasetsConfig] = useState(null)
const [variablesConfig, setVariablesConfig] = useState(null)
const [datasetsSchema, setDatasetsSchema] = useState(null)
const [variablesSchema, setVariablesSchema] = useState(null)
const [loading, setLoading] = useState(true)
const toast = useToast()
const loadDatasetData = async () => {
try {
setLoading(true)
const [datasetsData, variablesData, datasetsSchemaData, variablesSchemaData] = await Promise.all([
api.readConfig('dataset-definitions'),
api.readConfig('dataset-variables'),
api.getSchema('dataset-definitions'),
api.getSchema('dataset-variables')
])
setDatasetsConfig(datasetsData)
setVariablesConfig(variablesData)
setDatasetsSchema(datasetsSchemaData)
setVariablesSchema(variablesSchemaData)
} catch (error) {
toast({
title: '❌ Failed to load dataset data',
description: error.message,
status: 'error',
duration: 3000
})
} finally {
setLoading(false)
}
}
const saveDatasets = async (formData) => {
try {
await api.writeConfig('dataset-definitions', formData)
toast({
title: '✅ Dataset definitions saved',
status: 'success',
duration: 2000
})
setDatasetsConfig(formData)
} catch (error) {
toast({
title: '❌ Failed to save datasets',
description: error.message,
status: 'error',
duration: 3000
})
}
}
const saveVariables = async (formData) => {
try {
await api.writeConfig('dataset-variables', formData)
toast({
title: '✅ Dataset variables saved',
status: 'success',
duration: 2000
})
setVariablesConfig(formData)
} catch (error) {
toast({
title: '❌ Failed to save variables',
description: error.message,
status: 'error',
duration: 3000
})
}
}
useEffect(() => {
loadDatasetData()
}, [])
if (loading) {
return (
<Card>
<CardBody>
<Flex align="center" justify="center" py={8}>
<Spinner mr={3} />
<Text>Loading dataset configurations...</Text>
</Flex>
</CardBody>
</Card>
)
}
return (
<VStack spacing={4} align="stretch">
<Flex align="center">
<Heading size="lg">📊 Dataset Manager</Heading>
<Spacer />
<Button size="sm" variant="outline" onClick={loadDatasetData}>
🔄 Refresh
</Button>
</Flex>
<Tabs variant="enclosed" colorScheme="blue">
<TabList>
<Tab>📋 Dataset Definitions</Tab>
<Tab> Dataset Variables</Tab>
</TabList>
<TabPanels>
<TabPanel p={0} pt={4}>
{datasetsSchema && datasetsConfig && (
<Card>
<CardHeader>
<Heading size="md">Dataset Metadata Configuration</Heading>
<Text fontSize="sm" color="gray.500" mt={1}>
Configure dataset names, prefixes, sampling intervals and enable/disable datasets
</Text>
</CardHeader>
<CardBody>
<Form
schema={datasetsSchema}
formData={datasetsConfig}
validator={validator}
onSubmit={({ formData }) => saveDatasets(formData)}
onChange={({ formData }) => setDatasetsConfig(formData)}
>
<HStack spacing={2} mt={4}>
<Button type="submit" colorScheme="blue">
💾 Save Definitions
</Button>
<Button variant="outline" onClick={loadDatasetData}>
🔄 Reset
</Button>
</HStack>
</Form>
</CardBody>
</Card>
)}
</TabPanel>
<TabPanel p={0} pt={4}>
{variablesSchema && variablesConfig && (
<Card>
<CardHeader>
<Heading size="md">Dataset Variables Configuration</Heading>
<Text fontSize="sm" color="gray.500" mt={1}>
Raw JSON configuration for variables assigned to each dataset
</Text>
</CardHeader>
<CardBody>
<Form
schema={variablesSchema}
formData={variablesConfig}
validator={validator}
onSubmit={({ formData }) => saveVariables(formData)}
onChange={({ formData }) => setVariablesConfig(formData)}
>
<HStack spacing={2} mt={4}>
<Button type="submit" colorScheme="blue">
💾 Save Variables
</Button>
<Button variant="outline" onClick={loadDatasetData}>
🔄 Reset
</Button>
</HStack>
</Form>
</CardBody>
</Card>
)}
</TabPanel>
</TabPanels>
</Tabs>
</VStack>
)
}
// Events Display Component
function EventsDisplay({ events, loading, onRefresh }) {
const cardBg = useColorModeValue('white', 'gray.700')
if (loading) {
return (
<Card bg={cardBg}>
<CardBody>
<Flex align="center" justify="center" py={4}>
<Spinner mr={3} />
<Text>Loading events...</Text>
</Flex>
</CardBody>
</Card>
)
}
return (
<Card bg={cardBg}>
<CardHeader>
<Flex align="center">
<Heading size="md">📋 Recent Events</Heading>
<Spacer />
<Button size="sm" variant="outline" onClick={onRefresh}>
🔄 Refresh
</Button>
</Flex>
</CardHeader>
<CardBody>
<TableContainer>
<Table size="sm">
<Thead>
<Tr>
<Th>Time</Th>
<Th>Type</Th>
<Th>Message</Th>
</Tr>
</Thead>
<Tbody>
{events?.map((event, index) => (
<Tr key={index}>
<Td>
<Code fontSize="xs">
{new Date(event.timestamp).toLocaleString()}
</Code>
</Td>
<Td>
<Badge
colorScheme={
event.level === 'ERROR' ? 'red' :
event.level === 'WARNING' ? 'orange' :
event.level === 'INFO' ? 'blue' : 'gray'
}
>
{event.level}
</Badge>
</Td>
<Td>{event.message}</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
{(!events || events.length === 0) && (
<Text textAlign="center" py={4} color="gray.500">
No events found
</Text>
)}
</CardBody>
</Card>
)
}
// Main Dashboard Component with Pure RJSF
export default function Dashboard() {
const [status, setStatus] = useState(null)
const [statusLoading, setStatusLoading] = useState(true)
const [statusError, setStatusError] = useState('')
const [schemas, setSchemas] = useState([])
const [currentSchemaId, setCurrentSchemaId] = useState('plc')
const [schema, setSchema] = useState(null)
const [formData, setFormData] = useState(null)
const [saving, setSaving] = useState(false)
const [message, setMessage] = useState('')
const [events, setEvents] = useState([])
const [eventsLoading, setEventsLoading] = useState(false)
const sseRef = useRef(null)
// Load status once
const loadStatus = useCallback(async () => {
try {
setStatusLoading(true)
setStatusError('')
const statusData = await api.getStatus()
setStatus(statusData)
} catch (error) {
setStatusError(error.message)
} finally {
setStatusLoading(false)
}
}, [])
// SSE subscription for real-time updates (temporarily disabled due to MIME type issue)
const subscribeSSE = useCallback(() => {
// TODO: Fix SSE endpoint to return proper text/event-stream MIME type
// if (sseRef.current) {
// sseRef.current.close()
// }
// const eventSource = new EventSource('/api/status')
// sseRef.current = eventSource
// eventSource.onmessage = (event) => {
// try {
// const data = JSON.parse(event.data)
// setStatus(data)
// setStatusError('')
// } catch (error) {
// console.error('SSE parse error:', error)
// }
// }
// eventSource.onerror = () => {
// console.warn('SSE connection error, will retry...')
// }
// return () => {
// eventSource.close()
// }
// For now, use polling instead
const interval = setInterval(async () => {
try {
const statusData = await api.getStatus()
setStatus(statusData)
setStatusError('')
} catch (error) {
console.error('Status polling error:', error)
}
}, 5000) // Poll every 5 seconds
return () => {
clearInterval(interval)
}
}, [])
// Load schemas
const loadSchemas = useCallback(async () => {
try {
const schemasData = await api.listSchemas()
setSchemas(schemasData.schemas || [])
} catch (error) {
console.error('Failed to load schemas:', error)
}
}, [])
// Load specific config
const loadConfig = useCallback(async (schemaId) => {
try {
const [schemaData, configData] = await Promise.all([
api.getSchema(schemaId),
api.readConfig(schemaId)
])
setSchema(schemaData)
setFormData(configData)
setMessage('')
} catch (error) {
console.error(`Failed to load config ${schemaId}:`, error)
}
}, [])
// Save config
const saveConfig = useCallback(async (data) => {
try {
setSaving(true)
await api.writeConfig(currentSchemaId, data)
setMessage(`✅ Configuration saved successfully`)
setTimeout(() => setMessage(''), 3000)
setFormData(data)
} catch (error) {
setMessage(`❌ Failed to save: ${error.message}`)
} finally {
setSaving(false)
}
}, [currentSchemaId])
// Load events
const loadEvents = useCallback(async () => {
try {
setEventsLoading(true)
const eventsData = await api.getEvents(50)
setEvents(eventsData.events || [])
} catch (error) {
console.error('Failed to load events:', error)
} finally {
setEventsLoading(false)
}
}, [])
// Effects
useEffect(() => {
loadStatus()
loadSchemas()
loadEvents()
const cleanup = subscribeSSE()
return cleanup
}, [loadStatus, loadSchemas, loadEvents, subscribeSSE])
useEffect(() => {
if (currentSchemaId) {
loadConfig(currentSchemaId)
}
}, [currentSchemaId, loadConfig])
if (statusLoading) {
return (
<Container maxW="container.xl" py={6}>
<Flex align="center" justify="center" minH="200px">
<Spinner size="xl" mr={4} />
<Text fontSize="lg">Loading dashboard...</Text>
</Flex>
</Container>
)
}
return (
<Container maxW="container.xl" py={6}>
<VStack spacing={6} align="stretch">
<Flex align="center" mb={4}>
<Heading size="xl">🏭 PLC S7-31x Streamer & Logger</Heading>
<Spacer />
<Button size="sm" variant="outline" onClick={loadStatus}>
🔄 Refresh Status
</Button>
</Flex>
{statusError && (
<Alert status="error">
<AlertIcon />
Failed to load status: {statusError}
</Alert>
)}
<StatusBar status={status} onRefresh={loadStatus} />
<Tabs variant="enclosed" colorScheme="blue">
<TabList>
<Tab>🔧 Configuration</Tab>
<Tab>📊 Datasets</Tab>
<Tab>📈 Plotting</Tab>
<Tab>📋 Events</Tab>
</TabList>
<TabPanels>
<TabPanel p={0} pt={4}>
<ConfigurationPanel
schemas={schemas}
currentSchemaId={currentSchemaId}
onSchemaChange={setCurrentSchemaId}
schema={schema}
formData={formData}
onFormChange={setFormData}
onSave={saveConfig}
saving={saving}
message={message}
/>
</TabPanel>
<TabPanel p={0} pt={4}>
<DatasetManager />
</TabPanel>
<TabPanel p={0} pt={4}>
<PlotManager />
</TabPanel>
<TabPanel p={0} pt={4}>
<EventsDisplay
events={events}
loading={eventsLoading}
onRefresh={loadEvents}
/>
</TabPanel>
</TabPanels>
</Tabs>
</VStack>
</Container>
)
}

View File

@ -60,17 +60,24 @@ export async function stopUdpStreaming() {
// Config schemas and data
export async function listSchemas() {
const res = await fetch(`${BASE_URL}/api/config/schemas`, { headers: { 'Accept': 'application/json' } })
return toJsonOrThrow(res)
const response = await toJsonOrThrow(res)
// The API returns { success: true, schemas: [...] }
return response
}
export async function getSchema(schemaId) {
const res = await fetch(`${BASE_URL}/api/config/schema/${encodeURIComponent(schemaId)}`, { headers: { 'Accept': 'application/json' } })
return toJsonOrThrow(res)
const response = await toJsonOrThrow(res)
// The API returns { success: true, schema: {...}, ui_schema?: {...} }
// We need to extract just the schema part for RJSF
return response.schema || response
}
export async function readConfig(configId) {
const res = await fetch(`${BASE_URL}/api/config/${encodeURIComponent(configId)}`, { headers: { 'Accept': 'application/json' } })
return toJsonOrThrow(res)
const response = await toJsonOrThrow(res)
// The API might return { success: true, data: {...} } or just the data
return response.data || response
}
export async function writeConfig(configId, data) {
@ -123,4 +130,64 @@ export async function deactivateDataset(datasetId) {
return toJsonOrThrow(res)
}
// Plot management
export async function getPlots() {
const res = await fetch(`${BASE_URL}/api/plots`, { headers: { 'Accept': 'application/json' } })
return toJsonOrThrow(res)
}
export async function createPlot(plotConfig) {
const res = await fetch(`${BASE_URL}/api/plots`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify(plotConfig),
})
return toJsonOrThrow(res)
}
export async function deletePlot(sessionId) {
const res = await fetch(`${BASE_URL}/api/plots/${encodeURIComponent(sessionId)}`, {
method: 'DELETE',
headers: { 'Accept': 'application/json' },
})
return toJsonOrThrow(res)
}
export async function controlPlot(sessionId, action) {
const res = await fetch(`${BASE_URL}/api/plots/${encodeURIComponent(sessionId)}/control`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify({ action }),
})
return toJsonOrThrow(res)
}
export async function getPlotData(sessionId) {
const res = await fetch(`${BASE_URL}/api/plots/${encodeURIComponent(sessionId)}/data`, {
headers: { 'Accept': 'application/json' }
})
return toJsonOrThrow(res)
}
export async function getPlotConfig(sessionId) {
const res = await fetch(`${BASE_URL}/api/plots/${encodeURIComponent(sessionId)}/config`, {
headers: { 'Accept': 'application/json' }
})
return toJsonOrThrow(res)
}
export async function updatePlotConfig(sessionId, config) {
const res = await fetch(`${BASE_URL}/api/plots/${encodeURIComponent(sessionId)}/config`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify(config),
})
return toJsonOrThrow(res)
}
export async function getPlotVariables() {
const res = await fetch(`${BASE_URL}/api/plots/variables`, { headers: { 'Accept': 'application/json' } })
return toJsonOrThrow(res)
}

34
main_cleanup_notes.py Normal file
View File

@ -0,0 +1,34 @@
# ==============================
# LEGACY ROUTES TO REMOVE/COMMENT
# ==============================
# Legacy templates route (replaced by React SPA)
# @app.route("/legacy")
# def serve_legacy_index():
# """Serve legacy HTML template for backward compatibility."""
# try:
# return render_template("index.html")
# except Exception as e:
# return f"Error loading legacy template: {str(e)}", 500
# These routes can be removed after full migration to React:
# All routes serving /templates/index.html
# Static file serving for legacy JS/CSS
# Any jQuery-based endpoints
# Essential APIs to keep:
# - /api/status (SSE)
# - /api/health
# - /api/events
# - /api/config/* (schemas and CRUD)
# - /api/plc/connect, /api/plc/disconnect
# - /api/udp/streaming/*
# - /api/plots/* (for chart functionality)
# - /api/datasets/* (if still needed)
# - /api/variables/* (if still needed)
# React SPA routes to keep:
# - / (React app)
# - /app (React app)
# - /app/<path:path> (React app routing)
# - /assets/* (Vite build assets)