diff --git a/.vscode/settings.json b/.vscode/settings.json index 557b1ef..c76ee19 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,5 @@ { "workbench.colorCustomizations": { - "titleBar.activeBackground": "#470606", + "titleBar.activeBackground": "#36182a", } \ No newline at end of file diff --git a/RJSF_IMPLEMENTATION_SUMMARY.md b/RJSF_IMPLEMENTATION_SUMMARY.md new file mode 100644 index 0000000..5febe94 --- /dev/null +++ b/RJSF_IMPLEMENTATION_SUMMARY.md @@ -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 + + +// UDP Streaming con feedback + +``` + +### 2. Configuración RJSF +```jsx +// Formularios puros sin wrappers +
saveConfig(formData)} +/> +``` + +### 3. Control de Plots +```jsx +// Botones funcionales para charts + + +``` + +## 📁 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: ✅ diff --git a/application_events.json b/application_events.json index 0a2050b..d9a1279 100644 --- a/application_events.json +++ b/application_events.json @@ -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 } \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 0d98041..78fc3bc 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -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() { diff --git a/frontend/src/components/PlotManager.jsx b/frontend/src/components/PlotManager.jsx new file mode 100644 index 0000000..f52a41f --- /dev/null +++ b/frontend/src/components/PlotManager.jsx @@ -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 ( + + + Loading plot configurations... + + + ) + } + + return ( + + + 📈 Plot Manager + + + + + {/* Active Plots Overview */} + + + 🎛️ Active Plot Sessions + + + {Object.keys(plots).length === 0 ? ( + + No active plot sessions + + ) : ( + + + + + + + + + + + + + {Object.entries(plots).map(([sessionId, plot]) => ( + + + + + + + + ))} + +
Session IDNameStatusVariablesActions
+ + {sessionId} + + {plot.name || 'Unnamed Plot'} + + {plot.running ? 'Running' : 'Stopped'} + + + + {plot.variable_count || 0} vars + + + + {plot.running ? ( + + ) : ( + + )} + + + +
+
+ )} +
+
+ + + + {/* RJSF Configuration Forms */} + + + 📋 Plot Definitions + ⚙️ Plot Variables + + + + + {plotsSchema && plotsConfig && ( + + + Plot Session Definitions + + Configure plot sessions, time windows, triggers and visual settings + + + + savePlotsConfig(formData)} + onChange={({ formData }) => setPlotsConfig(formData)} + > + + + + + + + + )} + + + + {plotsVariablesSchema && plotsVariablesConfig && ( + + + Plot Variables Configuration + + Configure which variables are displayed in each plot session + + + +
savePlotsVariablesConfig(formData)} + onChange={({ formData }) => setPlotsVariablesConfig(formData)} + > + + + + +
+
+
+ )} +
+
+
+ + {/* 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/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx index 0f26c03..f829df2 100644 --- a/frontend/src/pages/Dashboard.jsx +++ b/frontend/src/pages/Dashboard.jsx @@ -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 ( - + 🔌 PLC: {plcConnected ? 'Connected' : 'Disconnected'} @@ -39,9 +95,9 @@ function StatusBar({ status }) { )} {plcConnected ? ( - + ) : ( - + )} @@ -50,9 +106,9 @@ function StatusBar({ status }) { 📡 UDP Streaming: {streaming ? 'Active' : 'Inactive'} {streaming ? ( - + ) : ( - + )} diff --git a/frontend/src/pages/DashboardNew.jsx b/frontend/src/pages/DashboardNew.jsx new file mode 100644 index 0000000..d044839 --- /dev/null +++ b/frontend/src/pages/DashboardNew.jsx @@ -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 ( + + + + + 🔌 PLC Connection + + {plcConnected ? 'Connected' : 'Disconnected'} + + {status?.plc_reconnection?.enabled && ( + + 🔄 Auto-reconnection: {status?.plc_reconnection?.active ? 'reconnecting…' : 'enabled'} + + )} + + {plcConnected ? ( + + ) : ( + + )} + + + + + + + + + 📡 UDP Streaming + + {streaming ? 'Active' : 'Inactive'} + + + {streaming ? ( + + ) : ( + + )} + + + + + + + + + 💾 CSV Recording + + {csvRecording ? 'Recording' : 'Inactive'} + + {status?.disk_space_info && ( + + 💽 {status.disk_space_info.free_space} free
+ ⏱️ ~{status.disk_space_info.recording_time_left} +
+ )} +
+
+
+
+ ) +} + +// 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 ( + + + Loading configuration... + + + ) + } + + return ( + + + + + 🔧 Configuration Editor + + Pure RJSF configuration management + + + + + + {message && ( + + + {message} + + )} + + +
onFormChange(formData)} + onSubmit={({ formData }) => onSave(formData)} + > + + + + +
+
+
+ ) +} + +// 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 ( + + + + + Loading dataset configurations... + + + + ) + } + + return ( + + + 📊 Dataset Manager + + + + + + + 📋 Dataset Definitions + ⚙️ Dataset Variables + + + + + {datasetsSchema && datasetsConfig && ( + + + Dataset Metadata Configuration + + Configure dataset names, prefixes, sampling intervals and enable/disable datasets + + + +
saveDatasets(formData)} + onChange={({ formData }) => setDatasetsConfig(formData)} + > + + + + +
+
+
+ )} +
+ + + {variablesSchema && variablesConfig && ( + + + Dataset Variables Configuration + + Raw JSON configuration for variables assigned to each dataset + + + +
saveVariables(formData)} + onChange={({ formData }) => setVariablesConfig(formData)} + > + + + + +
+
+
+ )} +
+
+
+
+ ) +} + +// Events Display Component +function EventsDisplay({ events, loading, onRefresh }) { + const cardBg = useColorModeValue('white', 'gray.700') + + if (loading) { + return ( + + + + + Loading events... + + + + ) + } + + return ( + + + + 📋 Recent Events + + + + + + + + + + + + + + + + {events?.map((event, index) => ( + + + + + + ))} + +
TimeTypeMessage
+ + {new Date(event.timestamp).toLocaleString()} + + + + {event.level} + + {event.message}
+
+ {(!events || events.length === 0) && ( + + No events found + + )} +
+
+ ) +} + +// 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 ( + + + + Loading dashboard... + + + ) + } + + return ( + + + + 🏭 PLC S7-31x Streamer & Logger + + + + + {statusError && ( + + + Failed to load status: {statusError} + + )} + + + + + + 🔧 Configuration + 📊 Datasets + 📈 Plotting + 📋 Events + + + + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 370b79a..2dc4aa0 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -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) +} + diff --git a/main_cleanup_notes.py b/main_cleanup_notes.py new file mode 100644 index 0000000..4589c86 --- /dev/null +++ b/main_cleanup_notes.py @@ -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/ (React app routing) +# - /assets/* (Vite build assets)