feat: Implement Plot Manager and Plot Manager Simple components with collapsible forms for plot definitions and variables configuration
This commit is contained in:
parent
3cf14df246
commit
f5db758698
File diff suppressed because it is too large
Load Diff
|
@ -522,8 +522,311 @@
|
|||
"event_type": "csv_cleanup_failed",
|
||||
"message": "CSV cleanup failed: 'max_hours'",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T13:41:49.859880",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T13:41:49.893768",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: DAR",
|
||||
"details": {
|
||||
"dataset_id": "DAR",
|
||||
"variables_count": 2,
|
||||
"streaming_count": 2,
|
||||
"prefix": "gateway_phoenix"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T13:41:49.894887",
|
||||
"level": "info",
|
||||
"event_type": "csv_recording_started",
|
||||
"message": "CSV recording started: 1 datasets activated",
|
||||
"details": {
|
||||
"activated_datasets": 1,
|
||||
"total_datasets": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T13:41:49.926928",
|
||||
"level": "error",
|
||||
"event_type": "csv_cleanup_failed",
|
||||
"message": "CSV cleanup failed: 'max_hours'",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T13:46:38.876812",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T13:46:38.925616",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: DAR",
|
||||
"details": {
|
||||
"dataset_id": "DAR",
|
||||
"variables_count": 2,
|
||||
"streaming_count": 2,
|
||||
"prefix": "gateway_phoenix"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T13:46:38.926826",
|
||||
"level": "info",
|
||||
"event_type": "csv_recording_started",
|
||||
"message": "CSV recording started: 1 datasets activated",
|
||||
"details": {
|
||||
"activated_datasets": 1,
|
||||
"total_datasets": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T13:46:38.957730",
|
||||
"level": "error",
|
||||
"event_type": "csv_cleanup_failed",
|
||||
"message": "CSV cleanup failed: 'max_hours'",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T14:00:00.330129",
|
||||
"level": "error",
|
||||
"event_type": "csv_cleanup_failed",
|
||||
"message": "CSV cleanup failed: 'max_hours'",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T14:17:45.030946",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T14:17:45.078942",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: DAR",
|
||||
"details": {
|
||||
"dataset_id": "DAR",
|
||||
"variables_count": 2,
|
||||
"streaming_count": 2,
|
||||
"prefix": "gateway_phoenix"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T14:17:45.080942",
|
||||
"level": "info",
|
||||
"event_type": "csv_recording_started",
|
||||
"message": "CSV recording started: 1 datasets activated",
|
||||
"details": {
|
||||
"activated_datasets": 1,
|
||||
"total_datasets": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T14:17:45.113420",
|
||||
"level": "error",
|
||||
"event_type": "csv_cleanup_failed",
|
||||
"message": "CSV cleanup failed: 'max_hours'",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T14:29:48.098633",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T14:29:48.148327",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: DAR",
|
||||
"details": {
|
||||
"dataset_id": "DAR",
|
||||
"variables_count": 2,
|
||||
"streaming_count": 2,
|
||||
"prefix": "gateway_phoenix"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T14:29:48.149336",
|
||||
"level": "info",
|
||||
"event_type": "csv_recording_started",
|
||||
"message": "CSV recording started: 1 datasets activated",
|
||||
"details": {
|
||||
"activated_datasets": 1,
|
||||
"total_datasets": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T14:29:48.181610",
|
||||
"level": "error",
|
||||
"event_type": "csv_cleanup_failed",
|
||||
"message": "CSV cleanup failed: 'max_hours'",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T14:31:49.061274",
|
||||
"level": "info",
|
||||
"event_type": "udp_streaming_started",
|
||||
"message": "UDP streaming to PlotJuggler started",
|
||||
"details": {
|
||||
"udp_host": "127.0.0.1",
|
||||
"udp_port": 9870,
|
||||
"datasets_available": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T14:38:28.137719",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T14:38:28.200071",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: DAR",
|
||||
"details": {
|
||||
"dataset_id": "DAR",
|
||||
"variables_count": 2,
|
||||
"streaming_count": 2,
|
||||
"prefix": "gateway_phoenix"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T14:38:28.202181",
|
||||
"level": "info",
|
||||
"event_type": "csv_recording_started",
|
||||
"message": "CSV recording started: 1 datasets activated",
|
||||
"details": {
|
||||
"activated_datasets": 1,
|
||||
"total_datasets": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T14:38:28.205203",
|
||||
"level": "info",
|
||||
"event_type": "udp_streaming_started",
|
||||
"message": "UDP streaming to PlotJuggler started",
|
||||
"details": {
|
||||
"udp_host": "127.0.0.1",
|
||||
"udp_port": 9870,
|
||||
"datasets_available": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T14:38:28.232863",
|
||||
"level": "error",
|
||||
"event_type": "csv_cleanup_failed",
|
||||
"message": "CSV cleanup failed: 'max_hours'",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T14:42:54.688764",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T14:42:54.737860",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: DAR",
|
||||
"details": {
|
||||
"dataset_id": "DAR",
|
||||
"variables_count": 2,
|
||||
"streaming_count": 2,
|
||||
"prefix": "gateway_phoenix"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T14:42:54.739862",
|
||||
"level": "info",
|
||||
"event_type": "csv_recording_started",
|
||||
"message": "CSV recording started: 1 datasets activated",
|
||||
"details": {
|
||||
"activated_datasets": 1,
|
||||
"total_datasets": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T14:42:54.740875",
|
||||
"level": "info",
|
||||
"event_type": "udp_streaming_started",
|
||||
"message": "UDP streaming to PlotJuggler started",
|
||||
"details": {
|
||||
"udp_host": "127.0.0.1",
|
||||
"udp_port": 9870,
|
||||
"datasets_available": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T14:42:54.771400",
|
||||
"level": "error",
|
||||
"event_type": "csv_cleanup_failed",
|
||||
"message": "CSV cleanup failed: 'max_hours'",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T14:47:13.179212",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T14:47:13.212383",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: DAR",
|
||||
"details": {
|
||||
"dataset_id": "DAR",
|
||||
"variables_count": 2,
|
||||
"streaming_count": 2,
|
||||
"prefix": "gateway_phoenix"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T14:47:13.213646",
|
||||
"level": "info",
|
||||
"event_type": "csv_recording_started",
|
||||
"message": "CSV recording started: 1 datasets activated",
|
||||
"details": {
|
||||
"activated_datasets": 1,
|
||||
"total_datasets": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T14:47:13.217027",
|
||||
"level": "info",
|
||||
"event_type": "udp_streaming_started",
|
||||
"message": "UDP streaming to PlotJuggler started",
|
||||
"details": {
|
||||
"udp_host": "127.0.0.1",
|
||||
"udp_port": 9870,
|
||||
"datasets_available": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-08-14T14:47:13.244962",
|
||||
"level": "error",
|
||||
"event_type": "csv_cleanup_failed",
|
||||
"message": "CSV cleanup failed: 'max_hours'",
|
||||
"details": {}
|
||||
}
|
||||
],
|
||||
"last_updated": "2025-08-14T13:31:23.939267",
|
||||
"total_entries": 49
|
||||
"last_updated": "2025-08-14T14:47:13.244962",
|
||||
"total_entries": 82
|
||||
}
|
|
@ -4,20 +4,20 @@
|
|||
"dataset_id": "DAR",
|
||||
"variables": [
|
||||
{
|
||||
"name": "UR29_Brix",
|
||||
"area": "db",
|
||||
"db": 1011,
|
||||
"name": "UR29_Brix",
|
||||
"offset": 1322,
|
||||
"type": "real",
|
||||
"streaming": true
|
||||
"streaming": true,
|
||||
"type": "real"
|
||||
},
|
||||
{
|
||||
"name": "UR29_ma",
|
||||
"area": "db",
|
||||
"db": 1011,
|
||||
"name": "UR29_ma",
|
||||
"offset": 1296,
|
||||
"type": "real",
|
||||
"streaming": true
|
||||
"streaming": true,
|
||||
"type": "real"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -4,20 +4,20 @@
|
|||
"plot_id": "plot_1",
|
||||
"variables": [
|
||||
{
|
||||
"variable_name": "UR29_Brix",
|
||||
"label": "Brix",
|
||||
"color": "#3498db",
|
||||
"enabled": true,
|
||||
"label": "Brix",
|
||||
"line_width": 2,
|
||||
"y_axis": "left",
|
||||
"enabled": true
|
||||
"variable_name": "UR29_Brix",
|
||||
"y_axis": "left"
|
||||
},
|
||||
{
|
||||
"variable_name": "UR29_ma",
|
||||
"label": "ma",
|
||||
"color": "#e74c3c",
|
||||
"enabled": true,
|
||||
"label": "ma",
|
||||
"line_width": 2,
|
||||
"y_axis": "left",
|
||||
"enabled": true
|
||||
"variable_name": "UR29_ma",
|
||||
"y_axis": "left"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,78 +0,0 @@
|
|||
{
|
||||
"ui:title": "Enhanced UI Schema Layout Demo",
|
||||
"ui:description": "This demo showcases the full UI schema layout capabilities of the enhanced RJSF implementation",
|
||||
"ui:layout": [
|
||||
[
|
||||
{
|
||||
"name": "basic_text",
|
||||
"width": 6
|
||||
},
|
||||
{
|
||||
"name": "updown_number",
|
||||
"width": 3
|
||||
},
|
||||
{
|
||||
"name": "enabled_checkbox",
|
||||
"width": 3
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"name": "long_description",
|
||||
"width": 12
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"name": "dropdown_selection",
|
||||
"width": 4
|
||||
},
|
||||
{
|
||||
"name": "variable_selector",
|
||||
"width": 4
|
||||
},
|
||||
{
|
||||
"name": "readonly_field",
|
||||
"width": 4
|
||||
}
|
||||
]
|
||||
],
|
||||
"ui:order": [
|
||||
"basic_text",
|
||||
"updown_number",
|
||||
"enabled_checkbox",
|
||||
"long_description",
|
||||
"dropdown_selection",
|
||||
"variable_selector",
|
||||
"readonly_field"
|
||||
],
|
||||
"basic_text": {
|
||||
"ui:placeholder": "Enter some text here...",
|
||||
"ui:help": "This is a standard text input with placeholder and help text"
|
||||
},
|
||||
"updown_number": {
|
||||
"ui:widget": "updown",
|
||||
"ui:help": "Use +/- buttons to adjust the value"
|
||||
},
|
||||
"enabled_checkbox": {
|
||||
"ui:widget": "checkbox",
|
||||
"ui:help": "Toggle this setting on/off"
|
||||
},
|
||||
"long_description": {
|
||||
"ui:widget": "textarea",
|
||||
"ui:placeholder": "Enter a longer description here...",
|
||||
"ui:help": "Multi-line text area that spans the full width"
|
||||
},
|
||||
"dropdown_selection": {
|
||||
"ui:widget": "select",
|
||||
"ui:help": "Choose an option from the dropdown"
|
||||
},
|
||||
"variable_selector": {
|
||||
"ui:widget": "VariableSelectorWidget",
|
||||
"ui:help": "Select a variable from the available PLC variables"
|
||||
},
|
||||
"readonly_field": {
|
||||
"ui:readonly": true,
|
||||
"ui:help": "This field is read-only and cannot be edited"
|
||||
}
|
||||
}
|
|
@ -2,12 +2,7 @@ import React from 'react'
|
|||
import recLogo from './assets/logo/record.png'
|
||||
import { Routes, Route, Link } from 'react-router-dom'
|
||||
import { Box, Container, Flex, HStack, Select, Button, Heading, Text, useColorMode, useColorModeValue, Stack } from '@chakra-ui/react'
|
||||
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/DashboardNew.jsx'
|
||||
import DatasetManager from './components/DatasetManager.jsx'
|
||||
import DashboardPage from './pages/Dashboard.jsx'
|
||||
|
||||
function Home() {
|
||||
return (
|
||||
|
@ -85,10 +80,6 @@ function NavBar() {
|
|||
<Heading as="span" size="sm">PLC Streamer</Heading>
|
||||
</HStack>
|
||||
<HStack spacing={2}>
|
||||
<Button as={Link} to="/datasets" size="sm" variant="outline">📊 Datasets</Button>
|
||||
<Button as={Link} to="/events" size="sm" variant="outline">Events</Button>
|
||||
<Button as={Link} to="/config" size="sm" variant="outline">Config</Button>
|
||||
<Button as={Link} to="/plots" size="sm" variant="outline">📈 Plots</Button>
|
||||
<ColorModeSelector />
|
||||
</HStack>
|
||||
</Flex>
|
||||
|
@ -98,16 +89,12 @@ function NavBar() {
|
|||
}
|
||||
|
||||
function App() {
|
||||
const [showPLCModal, setShowPLCModal] = React.useState(false)
|
||||
return (
|
||||
<Box bg={useColorModeValue('gray.50', 'gray.800')} color={useColorModeValue('gray.800', 'gray.100')} minH="100vh">
|
||||
<NavBar />
|
||||
<Routes>
|
||||
<Route path="/" element={<DashboardPage />} />
|
||||
<Route path="/datasets" element={<DatasetManager />} />
|
||||
<Route path="/events" element={<EventsPage />} />
|
||||
<Route path="/config" element={<ConfigPage />} />
|
||||
<Route path="/plots" element={<PlotsPage />} />
|
||||
<Route path="*" element={<DashboardPage />} />
|
||||
</Routes>
|
||||
</Box>
|
||||
)
|
||||
|
|
|
@ -1,287 +0,0 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
import {
|
||||
Box, Container, Heading, HStack, VStack, Button, Tabs, TabList, TabPanels, Tab, TabPanel,
|
||||
Card, CardBody, CardHeader, Grid, GridItem, Text, Badge, Alert, AlertIcon,
|
||||
useColorModeValue, Flex, Spacer, IconButton, useToast
|
||||
} from '@chakra-ui/react'
|
||||
import Form from '@rjsf/chakra-ui'
|
||||
import validator from '@rjsf/validator-ajv8'
|
||||
import { getSchema, readConfig, writeConfig } from '../services/api.js'
|
||||
import LayoutObjectFieldTemplate from './rjsf/LayoutObjectFieldTemplate.jsx'
|
||||
import DatasetVariableManager from './DatasetVariableManager.jsx'
|
||||
|
||||
export default function DatasetManager() {
|
||||
const [datasetsSchema, setDatasetsSchema] = useState(null)
|
||||
const [datasetsUiSchema, setDatasetsUiSchema] = useState(null)
|
||||
const [datasetsData, setDatasetsData] = useState(null)
|
||||
|
||||
const [variablesSchema, setVariablesSchema] = useState(null)
|
||||
const [variablesUiSchema, setVariablesUiSchema] = useState(null)
|
||||
const [variablesData, setVariablesData] = useState(null)
|
||||
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [selectedDatasetId, setSelectedDatasetId] = useState(null)
|
||||
|
||||
const toast = useToast()
|
||||
const cardBg = useColorModeValue('white', 'gray.700')
|
||||
const borderColor = useColorModeValue('gray.200', 'gray.600')
|
||||
|
||||
useEffect(() => {
|
||||
loadData()
|
||||
}, [])
|
||||
|
||||
const loadData = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const [
|
||||
datasetsSchemaResp, datasetsDataResp,
|
||||
variablesSchemaResp, variablesDataResp
|
||||
] = await Promise.all([
|
||||
getSchema('dataset-definitions'),
|
||||
readConfig('dataset-definitions'),
|
||||
getSchema('dataset-variables'),
|
||||
readConfig('dataset-variables')
|
||||
])
|
||||
|
||||
setDatasetsSchema(datasetsSchemaResp.schema)
|
||||
setDatasetsUiSchema(datasetsSchemaResp.ui_schema)
|
||||
setDatasetsData(datasetsDataResp.data)
|
||||
|
||||
setVariablesSchema(variablesSchemaResp.schema)
|
||||
setVariablesUiSchema(variablesSchemaResp.ui_schema)
|
||||
setVariablesData(variablesDataResp.data)
|
||||
|
||||
// Set first dataset as selected if none selected
|
||||
if (datasetsDataResp.data?.datasets && !selectedDatasetId) {
|
||||
const firstDataset = Object.keys(datasetsDataResp.data.datasets)[0]
|
||||
setSelectedDatasetId(firstDataset)
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Error loading data',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDatasetsSave = async ({ formData }) => {
|
||||
try {
|
||||
await writeConfig('dataset-definitions', formData)
|
||||
setDatasetsData(formData)
|
||||
toast({
|
||||
title: 'Dataset definitions saved',
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true
|
||||
})
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Error saving datasets',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleVariablesSave = async ({ formData }) => {
|
||||
try {
|
||||
await writeConfig('dataset-variables', formData)
|
||||
setVariablesData(formData)
|
||||
toast({
|
||||
title: 'Dataset variables saved',
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true
|
||||
})
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Error saving variables',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Container maxW="container.xl" py={4}>
|
||||
<Text>Loading dataset manager...</Text>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container maxW="container.xl" py={4}>
|
||||
<VStack spacing={4} align="stretch">
|
||||
<Flex align="center">
|
||||
<Heading size="lg">📊 Dataset Manager</Heading>
|
||||
<Spacer />
|
||||
<Button size="sm" variant="outline" onClick={loadData}>
|
||||
🔄 Refresh
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
<DatasetOverview
|
||||
datasets={datasetsData?.datasets || {}}
|
||||
variables={variablesData?.dataset_variables || {}}
|
||||
selectedDatasetId={selectedDatasetId}
|
||||
onSelectDataset={setSelectedDatasetId}
|
||||
/>
|
||||
|
||||
<Tabs variant="enclosed" colorScheme="blue">
|
||||
<TabList>
|
||||
<Tab>📋 Dataset Definitions</Tab>
|
||||
<Tab>⚙️ Dataset Variables</Tab>
|
||||
<Tab>🔧 Variable Manager</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
<TabPanel p={0} pt={4}>
|
||||
<Card bg={cardBg} borderColor={borderColor}>
|
||||
<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>
|
||||
{datasetsSchema && (
|
||||
<Form
|
||||
schema={datasetsSchema}
|
||||
formData={datasetsData}
|
||||
validator={validator}
|
||||
onSubmit={handleDatasetsSave}
|
||||
onChange={({ formData }) => setDatasetsData(formData)}
|
||||
uiSchema={datasetsUiSchema}
|
||||
templates={{ ObjectFieldTemplate: LayoutObjectFieldTemplate }}
|
||||
>
|
||||
<HStack spacing={2} mt={4}>
|
||||
<Button type="submit" colorScheme="blue">💾 Save Definitions</Button>
|
||||
<Button variant="outline" onClick={loadData}>🔄 Reset</Button>
|
||||
</HStack>
|
||||
</Form>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel p={0} pt={4}>
|
||||
<Card bg={cardBg} borderColor={borderColor}>
|
||||
<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>
|
||||
{variablesSchema && (
|
||||
<Form
|
||||
schema={variablesSchema}
|
||||
formData={variablesData}
|
||||
validator={validator}
|
||||
onSubmit={handleVariablesSave}
|
||||
onChange={({ formData }) => setVariablesData(formData)}
|
||||
uiSchema={variablesUiSchema}
|
||||
templates={{ ObjectFieldTemplate: LayoutObjectFieldTemplate }}
|
||||
>
|
||||
<HStack spacing={2} mt={4}>
|
||||
<Button type="submit" colorScheme="blue">💾 Save Variables</Button>
|
||||
<Button variant="outline" onClick={loadData}>🔄 Reset</Button>
|
||||
</HStack>
|
||||
</Form>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel p={0} pt={4}>
|
||||
<DatasetVariableManager
|
||||
datasets={datasetsData?.datasets || {}}
|
||||
variables={variablesData?.dataset_variables || {}}
|
||||
selectedDatasetId={selectedDatasetId}
|
||||
onSelectDataset={setSelectedDatasetId}
|
||||
onVariablesUpdate={(newVariables) => {
|
||||
const updatedData = {
|
||||
...variablesData,
|
||||
dataset_variables: newVariables
|
||||
}
|
||||
handleVariablesSave({ formData: updatedData })
|
||||
}}
|
||||
/>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</VStack>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
function DatasetOverview({ datasets, variables, selectedDatasetId, onSelectDataset }) {
|
||||
const cardBg = useColorModeValue('white', 'gray.700')
|
||||
const selectedBg = useColorModeValue('blue.50', 'blue.900')
|
||||
const borderColor = useColorModeValue('gray.200', 'gray.600')
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Heading size="md" mb={3}>📊 Datasets Overview</Heading>
|
||||
<Grid templateColumns="repeat(auto-fill, minmax(300px, 1fr))" gap={4}>
|
||||
{Object.entries(datasets).map(([id, dataset]) => {
|
||||
const varCount = variables[id]?.variables ? Object.keys(variables[id].variables).length : 0
|
||||
const streamingCount = variables[id]?.streaming_variables?.length || 0
|
||||
const isSelected = selectedDatasetId === id
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={id}
|
||||
bg={isSelected ? selectedBg : cardBg}
|
||||
borderColor={isSelected ? 'blue.500' : borderColor}
|
||||
borderWidth={isSelected ? 2 : 1}
|
||||
cursor="pointer"
|
||||
onClick={() => onSelectDataset(id)}
|
||||
_hover={{ borderColor: 'blue.300' }}
|
||||
>
|
||||
<CardBody>
|
||||
<VStack align="start" spacing={2}>
|
||||
<HStack justify="space-between" w="full">
|
||||
<Heading size="sm">{dataset.name}</Heading>
|
||||
<Badge colorScheme={dataset.enabled ? 'green' : 'red'}>
|
||||
{dataset.enabled ? 'Active' : 'Inactive'}
|
||||
</Badge>
|
||||
</HStack>
|
||||
|
||||
<Text fontSize="sm" color="gray.500">
|
||||
ID: {id} • Prefix: {dataset.prefix}
|
||||
</Text>
|
||||
|
||||
<HStack spacing={4}>
|
||||
<Text fontSize="sm">
|
||||
🔧 {varCount} variables
|
||||
</Text>
|
||||
<Text fontSize="sm">
|
||||
📡 {streamingCount} streaming
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
{dataset.sampling_interval && (
|
||||
<Text fontSize="sm" color="blue.500">
|
||||
⏱️ {dataset.sampling_interval}s interval
|
||||
</Text>
|
||||
)}
|
||||
</VStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</Grid>
|
||||
</Box>
|
||||
)
|
||||
}
|
|
@ -1,100 +0,0 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalCloseButton,
|
||||
Button,
|
||||
Alert,
|
||||
AlertIcon,
|
||||
} from '@chakra-ui/react'
|
||||
import Form from '@rjsf/chakra-ui';
|
||||
import validator from '@rjsf/validator-ajv8';
|
||||
import { getSchema, readConfig, writeConfig } from '../services/api.js';
|
||||
// Chakra theme widgets are used by default
|
||||
|
||||
const uiSchema = {
|
||||
plc_config: {
|
||||
rack: { 'ui:widget': 'updown' },
|
||||
slot: { 'ui:widget': 'updown' },
|
||||
},
|
||||
udp_config: {
|
||||
port: { 'ui:widget': 'updown' },
|
||||
},
|
||||
sampling_interval: { 'ui:widget': 'updown' },
|
||||
csv_config: {
|
||||
max_size_mb: { 'ui:widget': 'updown' },
|
||||
max_days: { 'ui:widget': 'updown' },
|
||||
max_hours: { 'ui:widget': 'updown' },
|
||||
cleanup_interval_hours: { 'ui:widget': 'updown' },
|
||||
},
|
||||
};
|
||||
|
||||
export default function PLCConfigModal({ show, onClose }) {
|
||||
const [schema, setSchema] = useState(null);
|
||||
const [formData, setFormData] = useState(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [msg, setMsg] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
Promise.all([getSchema('plc'), readConfig('plc')])
|
||||
.then(([s, d]) => {
|
||||
setSchema(s.schema);
|
||||
setFormData(d.data);
|
||||
})
|
||||
.catch(() => setMsg('Error loading PLC config'));
|
||||
}
|
||||
}, [show]);
|
||||
|
||||
const handleSubmit = async ({ formData: newData }) => {
|
||||
setSaving(true);
|
||||
setMsg('');
|
||||
try {
|
||||
await writeConfig('plc', newData);
|
||||
setMsg('Saved successfully');
|
||||
onClose?.();
|
||||
} catch (e) {
|
||||
setMsg(e.message || 'Error saving PLC config');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={show} onClose={onClose} size="xl">
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>PLC Configuration</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
{msg && (
|
||||
<Alert status="info" mb={3} borderRadius="md">
|
||||
<AlertIcon /> {msg}
|
||||
</Alert>
|
||||
)}
|
||||
{schema && (
|
||||
<Form
|
||||
schema={schema}
|
||||
formData={formData}
|
||||
validator={validator}
|
||||
onSubmit={handleSubmit}
|
||||
onChange={({ formData }) => setFormData(formData)}
|
||||
uiSchema={uiSchema}
|
||||
>
|
||||
<Button type="submit" isDisabled={saving} colorScheme="blue">💾 Save</Button>
|
||||
</Form>
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant="ghost" onClick={onClose}>Close</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -29,7 +29,15 @@ import {
|
|||
Tab,
|
||||
TabPanel,
|
||||
Divider,
|
||||
Select
|
||||
Select,
|
||||
Accordion,
|
||||
AccordionItem,
|
||||
AccordionButton,
|
||||
AccordionPanel,
|
||||
AccordionIcon,
|
||||
Collapse,
|
||||
useDisclosure,
|
||||
Spinner
|
||||
} from '@chakra-ui/react'
|
||||
import Form from '@rjsf/chakra-ui'
|
||||
import validator from '@rjsf/validator-ajv8'
|
||||
|
@ -39,6 +47,97 @@ import PlotRealtimeSession from './PlotRealtimeSession'
|
|||
import { useVariableContext } from '../contexts/VariableContext'
|
||||
import * as api from '../services/api'
|
||||
|
||||
// Collapsible Form Component for Plot Definitions
|
||||
function CollapsiblePlotForm({ data, schema, uiSchema, onSave, title, icon, getItemLabel }) {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [formData, setFormData] = useState(data)
|
||||
|
||||
useEffect(() => {
|
||||
setFormData(data)
|
||||
}, [data])
|
||||
|
||||
if (!schema || !formData) {
|
||||
return (
|
||||
<Card>
|
||||
<CardBody>
|
||||
<Flex align="center" justify="center" py={4}>
|
||||
<Spinner size="sm" mr={2} />
|
||||
<Text>Loading {title}...</Text>
|
||||
</Flex>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const items = formData[Object.keys(formData)[0]] || []
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Flex align="center" justify="space-between">
|
||||
<Box>
|
||||
<Heading size="md">{icon} {title}</Heading>
|
||||
<Text fontSize="sm" color="gray.500" mt={1}>
|
||||
{items.length} item{items.length !== 1 ? 's' : ''} configured
|
||||
</Text>
|
||||
</Box>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
rightIcon={<AccordionIcon transform={isOpen ? 'rotate(180deg)' : 'rotate(0deg)'} />}
|
||||
>
|
||||
{isOpen ? 'Collapse' : 'Expand'} Form
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
{/* Show summary when collapsed */}
|
||||
{!isOpen && items.length > 0 && (
|
||||
<Box mt={3}>
|
||||
<Text fontSize="sm" fontWeight="semibold" mb={2}>Quick Overview:</Text>
|
||||
<HStack spacing={2} flexWrap="wrap">
|
||||
{items.slice(0, 5).map((item, index) => (
|
||||
<Badge key={index} colorScheme="green" variant="subtle">
|
||||
{getItemLabel ? getItemLabel(item) : (item.name || item.id || `Item ${index + 1}`)}
|
||||
</Badge>
|
||||
))}
|
||||
{items.length > 5 && (
|
||||
<Badge colorScheme="gray" variant="subtle">
|
||||
+{items.length - 5} more...
|
||||
</Badge>
|
||||
)}
|
||||
</HStack>
|
||||
</Box>
|
||||
)}
|
||||
</CardHeader>
|
||||
|
||||
<Collapse in={isOpen}>
|
||||
<CardBody pt={0}>
|
||||
<Form
|
||||
schema={schema}
|
||||
uiSchema={uiSchema}
|
||||
formData={formData}
|
||||
validator={validator}
|
||||
widgets={allWidgets}
|
||||
templates={{ ObjectFieldTemplate: LayoutObjectFieldTemplate }}
|
||||
onSubmit={({ formData }) => onSave(formData)}
|
||||
onChange={({ formData }) => setFormData(formData)}
|
||||
>
|
||||
<HStack spacing={2} mt={4}>
|
||||
<Button type="submit" colorScheme="blue">
|
||||
💾 Save {title}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setFormData(data)}>
|
||||
🔄 Reset
|
||||
</Button>
|
||||
</HStack>
|
||||
</Form>
|
||||
</CardBody>
|
||||
</Collapse>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
// Pure RJSF Plot Manager Component
|
||||
export default function PlotManager() {
|
||||
const { triggerVariableRefresh } = useVariableContext()
|
||||
|
@ -310,59 +409,26 @@ export default function PlotManager() {
|
|||
<Divider />
|
||||
|
||||
{/* RJSF Configuration Forms */}
|
||||
<Tabs variant="enclosed" colorScheme="blue">
|
||||
<TabList>
|
||||
<Tab>📋 Plot Definitions</Tab>
|
||||
<Tab>⚙️ Plot Variables</Tab>
|
||||
</TabList>
|
||||
<VStack spacing={4} align="stretch">
|
||||
{/* Plot Definitions - Collapsible */}
|
||||
<CollapsiblePlotForm
|
||||
data={plotsConfig}
|
||||
schema={plotsSchemaData?.schema}
|
||||
uiSchema={plotsSchemaData?.uiSchema}
|
||||
onSave={savePlotsConfig}
|
||||
title="Plot Definitions"
|
||||
icon="📋"
|
||||
getItemLabel={(item) => `${item.name || item.id} (${item.time_window || 60}s)`}
|
||||
/>
|
||||
|
||||
<TabPanels>
|
||||
<TabPanel p={0} pt={4}>
|
||||
{plotsSchemaData?.schema && 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={plotsSchemaData.schema}
|
||||
uiSchema={plotsSchemaData.uiSchema}
|
||||
formData={plotsConfig}
|
||||
validator={validator}
|
||||
widgets={allWidgets}
|
||||
templates={{ ObjectFieldTemplate: LayoutObjectFieldTemplate }}
|
||||
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}>
|
||||
{/* Plot Variables Configuration with Combo Selector - Type 3 Pattern */}
|
||||
<Card bg={cardBg} borderColor={borderColor}>
|
||||
<CardHeader>
|
||||
<Heading size="md">Plot Variables Configuration</Heading>
|
||||
<Text fontSize="sm" color="gray.500" mt={1}>
|
||||
Select a plot session, then configure which variables are displayed in that plot
|
||||
{/* Plot Variables Configuration */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Heading size="md">⚙️ Plot Variables Configuration</Heading>
|
||||
<Text fontSize="sm" color="gray.500" mt={1}>
|
||||
Select a plot session, then configure its variables and visual settings
|
||||
</Text>
|
||||
</CardHeader>
|
||||
</Text>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
|
|
|
@ -0,0 +1,611 @@
|
|||
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,
|
||||
Tabs,
|
||||
TabList,
|
||||
TabPanels,
|
||||
Tab,
|
||||
TabPanel,
|
||||
Divider,
|
||||
Select,
|
||||
Accordion,
|
||||
AccordionItem,
|
||||
AccordionButton,
|
||||
AccordionPanel,
|
||||
AccordionIcon,
|
||||
Collapse,
|
||||
useDisclosure,
|
||||
Spinner
|
||||
} from '@chakra-ui/react'
|
||||
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 { useVariableContext } from '../contexts/VariableContext'
|
||||
import * as api from '../services/api'
|
||||
|
||||
// Collapsible Form Component for Plot Definitions
|
||||
function CollapsiblePlotForm({ data, schema, uiSchema, onSave, title, icon, getItemLabel }) {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [formData, setFormData] = useState(data)
|
||||
|
||||
useEffect(() => {
|
||||
setFormData(data)
|
||||
}, [data])
|
||||
|
||||
if (!schema || !formData) {
|
||||
return (
|
||||
<Card>
|
||||
<CardBody>
|
||||
<Flex align="center" justify="center" py={4}>
|
||||
<Spinner size="sm" mr={2} />
|
||||
<Text>Loading {title}...</Text>
|
||||
</Flex>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const items = formData[Object.keys(formData)[0]] || []
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Flex align="center" justify="space-between">
|
||||
<Box>
|
||||
<Heading size="md">{icon} {title}</Heading>
|
||||
<Text fontSize="sm" color="gray.500" mt={1}>
|
||||
{items.length} item{items.length !== 1 ? 's' : ''} configured
|
||||
</Text>
|
||||
</Box>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
rightIcon={<AccordionIcon transform={isOpen ? 'rotate(180deg)' : 'rotate(0deg)'} />}
|
||||
>
|
||||
{isOpen ? 'Collapse' : 'Expand'} Form
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
{/* Show summary when collapsed */}
|
||||
{!isOpen && items.length > 0 && (
|
||||
<Box mt={3}>
|
||||
<Text fontSize="sm" fontWeight="semibold" mb={2}>Quick Overview:</Text>
|
||||
<HStack spacing={2} flexWrap="wrap">
|
||||
{items.slice(0, 5).map((item, index) => (
|
||||
<Badge key={index} colorScheme="green" variant="subtle">
|
||||
{getItemLabel ? getItemLabel(item) : (item.name || item.id || `Item ${index + 1}`)}
|
||||
</Badge>
|
||||
))}
|
||||
{items.length > 5 && (
|
||||
<Badge colorScheme="gray" variant="subtle">
|
||||
+{items.length - 5} more...
|
||||
</Badge>
|
||||
)}
|
||||
</HStack>
|
||||
</Box>
|
||||
)}
|
||||
</CardHeader>
|
||||
|
||||
<Collapse in={isOpen}>
|
||||
<CardBody pt={0}>
|
||||
<Form
|
||||
schema={schema}
|
||||
uiSchema={uiSchema}
|
||||
formData={formData}
|
||||
validator={validator}
|
||||
widgets={allWidgets}
|
||||
templates={{ ObjectFieldTemplate: LayoutObjectFieldTemplate }}
|
||||
onSubmit={({ formData }) => onSave(formData)}
|
||||
onChange={({ formData }) => setFormData(formData)}
|
||||
>
|
||||
<HStack spacing={2} mt={4}>
|
||||
<Button type="submit" colorScheme="blue">
|
||||
💾 Save {title}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setFormData(data)}>
|
||||
🔄 Reset
|
||||
</Button>
|
||||
</HStack>
|
||||
</Form>
|
||||
</CardBody>
|
||||
</Collapse>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
// Pure RJSF Plot Manager Component
|
||||
export default function PlotManager() {
|
||||
const { triggerVariableRefresh } = useVariableContext()
|
||||
const [plots, setPlots] = useState({})
|
||||
const [plotsSchemaData, setPlotsSchemaData] = useState(null)
|
||||
const [plotsVariablesSchemaData, setPlotsVariablesSchemaData] = useState(null)
|
||||
const [plotsConfig, setPlotsConfig] = useState(null)
|
||||
const [plotsVariablesConfig, setPlotsVariablesConfig] = useState(null)
|
||||
const [selectedPlotId, setSelectedPlotId] = useState('')
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [actionLoading, setActionLoading] = useState({})
|
||||
|
||||
const toast = useToast()
|
||||
const cardBg = useColorModeValue('white', 'gray.700')
|
||||
const borderColor = useColorModeValue('gray.200', 'gray.600')
|
||||
|
||||
const setActionState = (key, loading) => {
|
||||
setActionLoading(prev => ({ ...prev, [key]: loading }))
|
||||
}
|
||||
|
||||
const loadPlotData = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const [
|
||||
plotsData,
|
||||
plotsSchemaResponse,
|
||||
plotsVariablesSchemaResponse,
|
||||
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 || {})
|
||||
setPlotsSchemaData(plotsSchemaResponse)
|
||||
setPlotsVariablesSchemaData(plotsVariablesSchemaResponse)
|
||||
setPlotsConfig(plotsConfigData)
|
||||
setPlotsVariablesConfig(plotsVariablesConfigData)
|
||||
|
||||
// Auto-select first plot if none selected
|
||||
if (!selectedPlotId && plotsConfigData?.plots?.length > 0) {
|
||||
setSelectedPlotId(plotsConfigData.plots[0].id)
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '❌ Failed to load plot data',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 3000
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [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 || []
|
||||
}
|
||||
|
||||
// Type 3 Pattern Helper Functions
|
||||
// Get filtered variables for selected plot
|
||||
const getSelectedPlotVariables = () => {
|
||||
if (!plotsVariablesConfig?.variables || !selectedPlotId) {
|
||||
return { variables: [] }
|
||||
}
|
||||
|
||||
const plotVars = plotsVariablesConfig.variables.find(v => v.plot_id === selectedPlotId)
|
||||
return plotVars || { variables: [] }
|
||||
}
|
||||
|
||||
// Update variables for selected plot
|
||||
const updateSelectedPlotVariables = (newVariableData) => {
|
||||
if (!plotsVariablesConfig?.variables || !selectedPlotId) return
|
||||
|
||||
const updatedVariables = plotsVariablesConfig.variables.map(v =>
|
||||
v.plot_id === selectedPlotId
|
||||
? { ...v, ...newVariableData }
|
||||
: v
|
||||
)
|
||||
|
||||
// If plot not found, add new entry
|
||||
if (!plotsVariablesConfig.variables.find(v => v.plot_id === selectedPlotId)) {
|
||||
updatedVariables.push({
|
||||
plot_id: selectedPlotId,
|
||||
...newVariableData
|
||||
})
|
||||
}
|
||||
|
||||
const updatedConfig = { ...plotsVariablesConfig, variables: updatedVariables }
|
||||
setPlotsVariablesConfig(updatedConfig)
|
||||
}
|
||||
|
||||
// Available plots for combo selector
|
||||
const availablePlots = plotsConfig?.plots || []
|
||||
|
||||
// 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)
|
||||
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)
|
||||
// Trigger refresh of variable selectors (though they don't depend on plot vars directly)
|
||||
triggerVariableRefresh()
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '❌ Failed to save plot variables',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 3000
|
||||
})
|
||||
} finally {
|
||||
setActionState('savePlotsVariables', false)
|
||||
}
|
||||
}
|
||||
|
||||
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 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>
|
||||
{getPlotDefinitions().length === 0 ? (
|
||||
<Text color="gray.500" textAlign="center" py={8}>
|
||||
No plot sessions configured. Create plot definitions below to get started.
|
||||
</Text>
|
||||
) : (
|
||||
<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>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* RJSF Configuration Forms */}
|
||||
<VStack spacing={4} align="stretch">
|
||||
{/* Plot Definitions - Collapsible */}
|
||||
<CollapsiblePlotForm
|
||||
data={plotsConfig}
|
||||
schema={plotsSchemaData?.schema}
|
||||
uiSchema={plotsSchemaData?.uiSchema}
|
||||
onSave={savePlotsConfig}
|
||||
title="Plot Definitions"
|
||||
icon="📋"
|
||||
getItemLabel={(item) => `${item.name || item.id} (${item.time_window || 60}s)`}
|
||||
/>
|
||||
|
||||
{/* Plot Variables Configuration */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Heading size="md">⚙️ Plot Variables Configuration</Heading>
|
||||
<Text fontSize="sm" color="gray.500" mt={1}>
|
||||
Select a plot session, then configure its variables and visual settings
|
||||
</Text>
|
||||
</CardHeader>
|
||||
</Text>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
{/* Step 1: Plot Selector (Combo) */}
|
||||
<VStack spacing={4} align="stretch">
|
||||
<Box>
|
||||
<Text fontSize="sm" fontWeight="bold" mb={2}>
|
||||
🎯 Select Plot Session
|
||||
</Text>
|
||||
<Select
|
||||
value={selectedPlotId}
|
||||
onChange={(e) => setSelectedPlotId(e.target.value)}
|
||||
placeholder="Choose a plot session to configure..."
|
||||
size="md"
|
||||
>
|
||||
{availablePlots.map(plot => (
|
||||
<option key={plot.id} value={plot.id}>
|
||||
📈 {plot.name} ({plot.id})
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
{availablePlots.length === 0 && (
|
||||
<Text fontSize="sm" color="orange.500" mt={2}>
|
||||
⚠️ No plot sessions available. Configure plot definitions first in the "Plot Definitions" tab.
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Variables Configuration Form */}
|
||||
{selectedPlotId && (
|
||||
<Box>
|
||||
<Divider mb={4} />
|
||||
<Text fontSize="sm" fontWeight="bold" mb={2}>
|
||||
⚙️ Configure Variables for Plot "{selectedPlotId}"
|
||||
</Text>
|
||||
|
||||
{/* Simplified schema for selected plot variables */}
|
||||
{(() => {
|
||||
const selectedPlotVars = getSelectedPlotVariables()
|
||||
|
||||
// Schema for this plot's variables
|
||||
const singlePlotSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
variables: {
|
||||
type: "array",
|
||||
title: "Variables",
|
||||
description: `Variables to display in plot ${selectedPlotId}`,
|
||||
items: {
|
||||
type: "object",
|
||||
title: "Plot Variable",
|
||||
properties: {
|
||||
variable_name: {
|
||||
type: "string",
|
||||
title: "Variable Name",
|
||||
description: "Select variable from datasets with search and metadata"
|
||||
},
|
||||
label: {
|
||||
type: "string",
|
||||
title: "Display Label",
|
||||
description: "Label shown in the plot legend"
|
||||
},
|
||||
color: {
|
||||
type: "string",
|
||||
title: "Line Color",
|
||||
default: "#3182CE"
|
||||
},
|
||||
line_width: {
|
||||
type: "number",
|
||||
title: "Line Width",
|
||||
default: 2,
|
||||
minimum: 1,
|
||||
maximum: 10
|
||||
},
|
||||
y_axis: {
|
||||
type: "string",
|
||||
title: "Y-Axis",
|
||||
enum: ["left", "right"],
|
||||
default: "left"
|
||||
}
|
||||
},
|
||||
required: ["variable_name", "label"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const singlePlotUiSchema = {
|
||||
variables: {
|
||||
items: {
|
||||
"ui:layout": [[
|
||||
{ "name": "variable_name", "width": 4 },
|
||||
{ "name": "label", "width": 2 },
|
||||
{ "name": "color", "width": 2 },
|
||||
{ "name": "line_width", "width": 2 },
|
||||
{ "name": "y_axis", "width": 2 }
|
||||
]],
|
||||
variable_name: {
|
||||
"ui:widget": "variableSelector",
|
||||
"ui:placeholder": "Search and select variable from datasets...",
|
||||
"ui:help": "🔍 Search variables from configured datasets with live values and metadata"
|
||||
},
|
||||
label: {
|
||||
"ui:placeholder": "Chart legend label..."
|
||||
},
|
||||
color: {
|
||||
"ui:widget": "color"
|
||||
},
|
||||
line_width: {
|
||||
"ui:widget": "updown"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Form
|
||||
schema={singlePlotSchema}
|
||||
uiSchema={singlePlotUiSchema}
|
||||
formData={selectedPlotVars}
|
||||
validator={validator}
|
||||
widgets={allWidgets}
|
||||
templates={{ ObjectFieldTemplate: LayoutObjectFieldTemplate }}
|
||||
onSubmit={({ formData }) => {
|
||||
updateSelectedPlotVariables(formData)
|
||||
// Create updated config and save it
|
||||
const updatedVariables = plotsVariablesConfig.variables?.map(v =>
|
||||
v.plot_id === selectedPlotId
|
||||
? { ...v, ...formData }
|
||||
: v
|
||||
) || []
|
||||
|
||||
// If plot not found, add new entry
|
||||
if (!plotsVariablesConfig.variables?.find(v => v.plot_id === selectedPlotId)) {
|
||||
updatedVariables.push({
|
||||
plot_id: selectedPlotId,
|
||||
...formData
|
||||
})
|
||||
}
|
||||
|
||||
const updatedConfig = { ...plotsVariablesConfig, variables: updatedVariables }
|
||||
savePlotsVariablesConfig(updatedConfig)
|
||||
}}
|
||||
onChange={({ formData }) => updateSelectedPlotVariables(formData)}
|
||||
>
|
||||
<HStack spacing={2} mt={4}>
|
||||
<Button
|
||||
type="submit"
|
||||
colorScheme="blue"
|
||||
isLoading={actionLoading.savePlotsVariables}
|
||||
loadingText="Saving..."
|
||||
>
|
||||
💾 Save Variables for {selectedPlotId}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={loadPlotData}>
|
||||
🔄 Reset
|
||||
</Button>
|
||||
</HStack>
|
||||
</Form>
|
||||
)
|
||||
})()}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{!selectedPlotId && availablePlots.length > 0 && (
|
||||
<Box textAlign="center" py={8}>
|
||||
<Text color="gray.500">
|
||||
👆 Select a plot session above to configure its variables
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</VStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</VStack>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,633 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
Button,
|
||||
Text,
|
||||
Grid,
|
||||
Flex,
|
||||
Spacer,
|
||||
HStack,
|
||||
VStack,
|
||||
useColorModeValue,
|
||||
useToast,
|
||||
Heading,
|
||||
Badge,
|
||||
Divider,
|
||||
Accordion,
|
||||
AccordionItem,
|
||||
AccordionButton,
|
||||
AccordionPanel,
|
||||
Collapse,
|
||||
useDisclosure,
|
||||
Spinner,
|
||||
Select
|
||||
} from '@chakra-ui/react'
|
||||
import { ChevronDownIcon, ChevronUpIcon } from '@chakra-ui/icons'
|
||||
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 { useVariableContext } from '../contexts/VariableContext'
|
||||
import * as api from '../services/api'
|
||||
|
||||
// Collapsible Plot Items Form - Each item in the array is individually collapsible
|
||||
function CollapsiblePlotItemsForm({ data, schema, uiSchema, onSave, title, icon, getItemLabel }) {
|
||||
const [formData, setFormData] = useState(data)
|
||||
const [expandedItems, setExpandedItems] = useState(new Set())
|
||||
|
||||
useEffect(() => {
|
||||
setFormData(data)
|
||||
}, [data])
|
||||
|
||||
if (!schema || !formData) {
|
||||
return (
|
||||
<Card>
|
||||
<CardBody>
|
||||
<Flex align="center" justify="center" py={4}>
|
||||
<Spinner size="sm" mr={2} />
|
||||
<Text>Loading {title}...</Text>
|
||||
</Flex>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const arrayKey = Object.keys(formData)[0]
|
||||
const items = formData[arrayKey] || []
|
||||
|
||||
const toggleItemExpansion = (index) => {
|
||||
const newExpanded = new Set(expandedItems)
|
||||
if (newExpanded.has(index)) {
|
||||
newExpanded.delete(index)
|
||||
} else {
|
||||
newExpanded.add(index)
|
||||
}
|
||||
setExpandedItems(newExpanded)
|
||||
}
|
||||
|
||||
const updateItem = (index, newItemData) => {
|
||||
const newItems = [...items]
|
||||
newItems[index] = newItemData
|
||||
const newFormData = { ...formData, [arrayKey]: newItems }
|
||||
setFormData(newFormData)
|
||||
}
|
||||
|
||||
const addItem = () => {
|
||||
const newItem = {}
|
||||
const newItems = [...items, newItem]
|
||||
const newFormData = { ...formData, [arrayKey]: newItems }
|
||||
setFormData(newFormData)
|
||||
setExpandedItems(new Set([...expandedItems, items.length]))
|
||||
}
|
||||
|
||||
const removeItem = (index) => {
|
||||
const newItems = items.filter((_, i) => i !== index)
|
||||
const newFormData = { ...formData, [arrayKey]: newItems }
|
||||
setFormData(newFormData)
|
||||
// Update expanded items indices
|
||||
const newExpanded = new Set()
|
||||
expandedItems.forEach(i => {
|
||||
if (i < index) newExpanded.add(i)
|
||||
else if (i > index) newExpanded.add(i - 1)
|
||||
})
|
||||
setExpandedItems(newExpanded)
|
||||
}
|
||||
|
||||
const saveChanges = () => {
|
||||
onSave(formData)
|
||||
}
|
||||
|
||||
// Get item schema from the array schema
|
||||
const itemSchema = schema?.properties?.[arrayKey]?.items || {}
|
||||
|
||||
// Get item UI schema - extract from the nested structure
|
||||
const arrayUiSchema = uiSchema?.[arrayKey] || {}
|
||||
const itemUiSchema = arrayUiSchema.items || {}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Flex align="center" justify="space-between">
|
||||
<Box>
|
||||
<Heading size="md">{icon} {title}</Heading>
|
||||
<Text fontSize="sm" color="gray.500" mt={1}>
|
||||
{items.length} item{items.length !== 1 ? 's' : ''} configured
|
||||
</Text>
|
||||
</Box>
|
||||
<HStack spacing={2}>
|
||||
<Button size="sm" colorScheme="green" onClick={addItem}>
|
||||
➕ Add Item
|
||||
</Button>
|
||||
<Button size="sm" colorScheme="blue" onClick={saveChanges}>
|
||||
💾 Save All
|
||||
</Button>
|
||||
</HStack>
|
||||
</Flex>
|
||||
</CardHeader>
|
||||
|
||||
<CardBody>
|
||||
{items.length === 0 ? (
|
||||
<Box textAlign="center" py={8}>
|
||||
<Text color="gray.500" mb={4}>
|
||||
No items configured yet
|
||||
</Text>
|
||||
<Button colorScheme="green" onClick={addItem}>
|
||||
➕ Add First Item
|
||||
</Button>
|
||||
</Box>
|
||||
) : (
|
||||
<VStack spacing={3} align="stretch">
|
||||
{items.map((item, index) => {
|
||||
const isExpanded = expandedItems.has(index)
|
||||
const itemLabel = getItemLabel ? getItemLabel(item) : (item.name || item.id || `Item ${index + 1}`)
|
||||
|
||||
return (
|
||||
<Card key={index} variant="outline" size="sm">
|
||||
<CardHeader py={2}>
|
||||
<Flex align="center" justify="space-between">
|
||||
<HStack spacing={2}>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
onClick={() => toggleItemExpansion(index)}
|
||||
rightIcon={isExpanded ? <ChevronUpIcon /> : <ChevronDownIcon />}
|
||||
>
|
||||
{itemLabel}
|
||||
</Button>
|
||||
<Badge colorScheme="green" size="sm">#{index + 1}</Badge>
|
||||
</HStack>
|
||||
<Button
|
||||
size="xs"
|
||||
colorScheme="red"
|
||||
variant="ghost"
|
||||
onClick={() => removeItem(index)}
|
||||
>
|
||||
🗑️
|
||||
</Button>
|
||||
</Flex>
|
||||
</CardHeader>
|
||||
|
||||
<Collapse in={isExpanded}>
|
||||
<CardBody pt={0}>
|
||||
<Form
|
||||
schema={itemSchema}
|
||||
uiSchema={itemUiSchema}
|
||||
formData={item}
|
||||
validator={validator}
|
||||
widgets={allWidgets}
|
||||
templates={{ ObjectFieldTemplate: LayoutObjectFieldTemplate }}
|
||||
onChange={({ formData: newItemData }) => updateItem(index, newItemData)}
|
||||
>
|
||||
<div></div> {/* Prevents form buttons from showing */}
|
||||
</Form>
|
||||
</CardBody>
|
||||
</Collapse>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</VStack>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
// Collapsible Plot Component
|
||||
function CollapsiblePlotChart({ plotDefinition, plotVariables, onConfigUpdate, onRemove }) {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Flex align="center" justify="space-between">
|
||||
<Box>
|
||||
<Heading size="sm">📈 {plotDefinition.name || plotDefinition.id}</Heading>
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
{plotDefinition.time_window}s window • {plotVariables?.variables?.length || 0} variables
|
||||
</Text>
|
||||
</Box>
|
||||
<HStack>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="outline"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
rightIcon={isOpen ? <ChevronUpIcon /> : <ChevronDownIcon />}
|
||||
>
|
||||
{isOpen ? 'Hide' : 'Show'} Chart
|
||||
</Button>
|
||||
<Button size="xs" colorScheme="red" variant="outline" onClick={() => onRemove(plotDefinition.id)}>
|
||||
❌
|
||||
</Button>
|
||||
</HStack>
|
||||
</Flex>
|
||||
</CardHeader>
|
||||
|
||||
<Collapse in={isOpen}>
|
||||
<CardBody pt={0}>
|
||||
<PlotRealtimeSession
|
||||
plotDefinition={plotDefinition}
|
||||
plotVariables={plotVariables}
|
||||
onConfigUpdate={onConfigUpdate}
|
||||
onRemove={onRemove}
|
||||
isCollapsed={true}
|
||||
/>
|
||||
</CardBody>
|
||||
</Collapse>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
// Simple Plot Manager Component
|
||||
export default function PlotManager() {
|
||||
const { triggerVariableRefresh } = useVariableContext()
|
||||
const [plotsConfig, setPlotsConfig] = useState(null)
|
||||
const [plotVariablesConfig, setPlotVariablesConfig] = useState(null)
|
||||
const [plotsSchemaData, setPlotsSchemaData] = useState(null)
|
||||
const [plotVariablesSchemaData, setPlotVariablesSchemaData] = useState(null)
|
||||
const [selectedPlotId, setSelectedPlotId] = useState('')
|
||||
const [loading, setLoading] = useState(true)
|
||||
const toast = useToast()
|
||||
|
||||
const loadPlotData = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const [plotsData, plotVariablesData, plotsSchemaResponse, plotVariablesSchemaResponse] = await Promise.all([
|
||||
api.readConfig('plot-definitions'),
|
||||
api.readConfig('plot-variables'),
|
||||
api.getSchema('plot-definitions'),
|
||||
api.getSchema('plot-variables')
|
||||
])
|
||||
|
||||
setPlotsConfig(plotsData)
|
||||
setPlotVariablesConfig(plotVariablesData)
|
||||
setPlotsSchemaData(plotsSchemaResponse)
|
||||
setPlotVariablesSchemaData(plotVariablesSchemaResponse)
|
||||
|
||||
// Auto-select first plot if none selected
|
||||
if (!selectedPlotId && plotsData?.plots?.length > 0) {
|
||||
setSelectedPlotId(plotsData.plots[0].id)
|
||||
}
|
||||
setPlotsSchemaData(plotsSchemaResponse)
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '❌ Failed to load plot configurations',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 5000
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [toast])
|
||||
|
||||
const savePlotsConfig = async (formData) => {
|
||||
try {
|
||||
await api.writeConfig('plot-definitions', formData)
|
||||
setPlotsConfig(formData)
|
||||
toast({
|
||||
title: '✅ Plot definitions saved',
|
||||
status: 'success',
|
||||
duration: 3000
|
||||
})
|
||||
triggerVariableRefresh()
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '❌ Failed to save plot definitions',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 5000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const savePlotVariables = async (formData) => {
|
||||
try {
|
||||
await api.writeConfig('plot-variables', formData)
|
||||
setPlotVariablesConfig(formData)
|
||||
toast({
|
||||
title: '✅ Plot variables saved',
|
||||
status: 'success',
|
||||
duration: 3000
|
||||
})
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '❌ Failed to save plot variables',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 5000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get plot definitions from config
|
||||
const getPlotDefinitions = () => {
|
||||
return plotsConfig?.plots || []
|
||||
}
|
||||
|
||||
// Helper to get dataset IDs for dropdown
|
||||
const getDatasetIds = () => {
|
||||
// This would normally come from dataset definitions
|
||||
// For now, return some placeholder values
|
||||
return ['DAR', 'Fast', 'Slow'] // TODO: Get from actual dataset definitions
|
||||
}
|
||||
|
||||
// Helper functions for Type 3 form pattern (Plot Variables)
|
||||
const getSelectedPlotVariables = () => {
|
||||
if (!selectedPlotId || !plotVariablesConfig?.variables) return { variables: [] }
|
||||
|
||||
const plotVars = plotVariablesConfig.variables.find(
|
||||
item => item.plot_id === selectedPlotId
|
||||
)
|
||||
return plotVars || { plot_id: selectedPlotId, variables: [] }
|
||||
}
|
||||
|
||||
const updateSelectedPlotVariables = (formData) => {
|
||||
if (!plotVariablesConfig?.variables) {
|
||||
// Initialize plotVariablesConfig if it doesn't exist
|
||||
const newConfig = {
|
||||
variables: [{ plot_id: selectedPlotId, ...formData }]
|
||||
}
|
||||
setPlotVariablesConfig(newConfig)
|
||||
return
|
||||
}
|
||||
|
||||
const existingIndex = plotVariablesConfig.variables.findIndex(
|
||||
item => item.plot_id === selectedPlotId
|
||||
)
|
||||
|
||||
const updatedVars = [...plotVariablesConfig.variables]
|
||||
const newVarData = { plot_id: selectedPlotId, ...formData }
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
updatedVars[existingIndex] = newVarData
|
||||
} else {
|
||||
updatedVars.push(newVarData)
|
||||
}
|
||||
|
||||
setPlotVariablesConfig({
|
||||
...plotVariablesConfig,
|
||||
variables: updatedVars
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadPlotData()
|
||||
}, [loadPlotData])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card>
|
||||
<CardBody>
|
||||
<Flex align="center" justify="center" py={8}>
|
||||
<Spinner mr={3} />
|
||||
<Text>Loading plot configurations...</Text>
|
||||
</Flex>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<VStack spacing={6} align="stretch">
|
||||
<Flex align="center">
|
||||
<Heading size="lg">📈 Plot Manager</Heading>
|
||||
<Spacer />
|
||||
<Button size="sm" variant="outline" onClick={loadPlotData}>
|
||||
🔄 Refresh
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
{/* Real-time Charts Section */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Heading size="md">🔴 Real-time Plot Sessions</Heading>
|
||||
<Text fontSize="sm" color="gray.500" mt={1}>
|
||||
Live charts for configured plot sessions - click to expand/collapse
|
||||
</Text>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
{getPlotDefinitions().length === 0 ? (
|
||||
<Text color="gray.500" textAlign="center" py={8}>
|
||||
No plot sessions configured. Create plot definitions below to get started.
|
||||
</Text>
|
||||
) : (
|
||||
<VStack spacing={4} align="stretch">
|
||||
{getPlotDefinitions().map((plotDef) => (
|
||||
<CollapsiblePlotChart
|
||||
key={plotDef.id}
|
||||
plotDefinition={plotDef}
|
||||
plotVariables={{ variables: [] }} // Simplified for now
|
||||
onConfigUpdate={() => {}}
|
||||
onRemove={() => {}}
|
||||
/>
|
||||
))}
|
||||
</VStack>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Plot Definitions - Collapsible */}
|
||||
<CollapsiblePlotItemsForm
|
||||
data={plotsConfig}
|
||||
schema={plotsSchemaData?.schema}
|
||||
uiSchema={plotsSchemaData?.uiSchema}
|
||||
onSave={savePlotsConfig}
|
||||
title="Plot Definitions"
|
||||
icon="📋"
|
||||
getItemLabel={(item) => `${item.name || item.id} (${item.time_window || 60}s)`}
|
||||
/>
|
||||
|
||||
{/* Plot Variables Configuration - Type 3 Form Pattern */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Heading size="md">⚙️ Plot Variables Configuration</Heading>
|
||||
<Text fontSize="sm" color="gray.500" mt={1}>
|
||||
Select a plot, then configure which variables are displayed in that plot session
|
||||
</Text>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
{/* Step 1: Plot Selector (Combo) */}
|
||||
<VStack spacing={4} align="stretch">
|
||||
<Box>
|
||||
<Text fontSize="sm" fontWeight="bold" mb={2}>
|
||||
🎯 Select Plot Session
|
||||
</Text>
|
||||
<Select
|
||||
value={selectedPlotId}
|
||||
onChange={(e) => setSelectedPlotId(e.target.value)}
|
||||
placeholder="Choose a plot to configure..."
|
||||
size="md"
|
||||
>
|
||||
{getPlotDefinitions().map(plot => (
|
||||
<option key={plot.id} value={plot.id}>
|
||||
📈 {plot.name} ({plot.id})
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
{getPlotDefinitions().length === 0 && (
|
||||
<Text fontSize="sm" color="orange.500" mt={2}>
|
||||
⚠️ No plots available. Configure plot definitions first above.
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Variables Configuration Form */}
|
||||
{selectedPlotId && (
|
||||
<Box>
|
||||
<Divider mb={4} />
|
||||
<Text fontSize="sm" fontWeight="bold" mb={2}>
|
||||
⚙️ Configure Variables for Plot "{selectedPlotId}"
|
||||
</Text>
|
||||
|
||||
{/* Simplified schema for selected plot variables */}
|
||||
{(() => {
|
||||
const selectedPlotVars = getSelectedPlotVariables()
|
||||
|
||||
// Schema for this plot's variables - match the real schema
|
||||
const singlePlotSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
variables: {
|
||||
type: "array",
|
||||
title: "Variables",
|
||||
description: `Variables to display in plot ${selectedPlotId}`,
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
variable_name: {
|
||||
type: "string",
|
||||
title: "Variable Name",
|
||||
description: "Name of the variable from the dataset"
|
||||
},
|
||||
label: {
|
||||
type: "string",
|
||||
title: "Display Label",
|
||||
description: "Label shown in the plot legend"
|
||||
},
|
||||
color: {
|
||||
type: "string",
|
||||
title: "Plot Color",
|
||||
pattern: "^#[0-9A-Fa-f]{6}$",
|
||||
default: "#3498db"
|
||||
},
|
||||
line_width: {
|
||||
type: "number",
|
||||
title: "Line Width",
|
||||
default: 2,
|
||||
minimum: 1,
|
||||
maximum: 10
|
||||
},
|
||||
y_axis: {
|
||||
type: "string",
|
||||
title: "Y-Axis",
|
||||
enum: ["left", "right"],
|
||||
default: "left"
|
||||
},
|
||||
enabled: {
|
||||
type: "boolean",
|
||||
title: "Show in Plot",
|
||||
default: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UI Schema for layout
|
||||
const singlePlotUiSchema = {
|
||||
"ui:layout": [[
|
||||
{ "name": "variables", "width": 12 }
|
||||
]],
|
||||
variables: {
|
||||
items: {
|
||||
"ui:layout": [[
|
||||
{ "name": "variable_name", "width": 4 },
|
||||
{ "name": "label", "width": 3 },
|
||||
{ "name": "color", "width": 2 },
|
||||
{ "name": "line_width", "width": 1 },
|
||||
{ "name": "y_axis", "width": 1 },
|
||||
{ "name": "enabled", "width": 1 }
|
||||
]],
|
||||
variable_name: {
|
||||
"ui:widget": "variableSelector",
|
||||
"ui:placeholder": "Search and select variable from datasets...",
|
||||
"ui:help": "🔍 Search and select a variable from the configured datasets (includes live values and metadata)"
|
||||
},
|
||||
label: {
|
||||
"ui:widget": "text",
|
||||
"ui:placeholder": "Chart legend label...",
|
||||
"ui:help": "📊 Label shown in the plot legend for this variable"
|
||||
},
|
||||
color: {
|
||||
"ui:widget": "color",
|
||||
"ui:help": "🎨 Select the color for this variable in the plot",
|
||||
"ui:placeholder": "#3498db"
|
||||
},
|
||||
line_width: {
|
||||
"ui:widget": "updown",
|
||||
"ui:help": "📏 Thickness of the line in the plot (1-10)",
|
||||
"ui:options": { "step": 1, "min": 1, "max": 10 }
|
||||
},
|
||||
y_axis: {
|
||||
"ui:widget": "select",
|
||||
"ui:help": "📈 Which Y-axis to use for this variable"
|
||||
},
|
||||
enabled: {
|
||||
"ui:widget": "checkbox",
|
||||
"ui:help": "✅ Whether to show this variable in the plot"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Form
|
||||
schema={singlePlotSchema}
|
||||
uiSchema={singlePlotUiSchema}
|
||||
formData={selectedPlotVars}
|
||||
validator={validator}
|
||||
widgets={allWidgets}
|
||||
templates={{ ObjectFieldTemplate: LayoutObjectFieldTemplate }}
|
||||
onSubmit={({ formData }) => {
|
||||
updateSelectedPlotVariables(formData)
|
||||
savePlotVariables(plotVariablesConfig).then(() => {
|
||||
// Additional trigger after successful save
|
||||
triggerVariableRefresh?.()
|
||||
})
|
||||
}}
|
||||
onChange={({ formData }) => updateSelectedPlotVariables(formData)}
|
||||
>
|
||||
<HStack spacing={2} mt={4}>
|
||||
<Button type="submit" colorScheme="blue">
|
||||
💾 Save Variables for {selectedPlotId}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={loadPlotData}>
|
||||
🔄 Reset
|
||||
</Button>
|
||||
</HStack>
|
||||
</Form>
|
||||
)
|
||||
})()}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{!selectedPlotId && getPlotDefinitions().length > 0 && (
|
||||
<Box textAlign="center" py={8}>
|
||||
<Text color="gray.500">
|
||||
👆 Select a plot above to configure its variables
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</VStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</VStack>
|
||||
)
|
||||
}
|
|
@ -1,167 +0,0 @@
|
|||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { Container, Heading, HStack, Button, Menu, MenuButton, MenuList, MenuItem, useColorModeValue, Alert, AlertIcon, Spacer } from '@chakra-ui/react'
|
||||
import Form from '@rjsf/chakra-ui'
|
||||
import validator from '@rjsf/validator-ajv8'
|
||||
import { listSchemas, getSchema, readConfig, writeConfig } from '../services/api.js'
|
||||
import LayoutObjectFieldTemplate from '../components/rjsf/LayoutObjectFieldTemplate.jsx'
|
||||
import { widgets } from '../components/rjsf/widgets.jsx'
|
||||
|
||||
function buildUiSchema(schema) {
|
||||
if (!schema || typeof schema !== 'object') return undefined
|
||||
|
||||
const mapForType = (s) => {
|
||||
if (!s || typeof s !== 'object') return undefined
|
||||
const type = s.type
|
||||
// handle oneOf/anyOf by taking first option for ui mapping
|
||||
const resolved = type || (Array.isArray(s.oneOf) && s.oneOf[0]?.type) || (Array.isArray(s.anyOf) && s.anyOf[0]?.type)
|
||||
if (resolved === 'string') return { 'ui:widget': 'text' }
|
||||
if (resolved === 'integer' || resolved === 'number') return { 'ui:widget': 'updown' }
|
||||
if (resolved === 'boolean') return { 'ui:widget': 'checkbox' }
|
||||
if (resolved === 'object' && s.properties) {
|
||||
const ui = {}
|
||||
for (const [key, prop] of Object.entries(s.properties)) {
|
||||
ui[key] = mapForType(prop)
|
||||
}
|
||||
return ui
|
||||
}
|
||||
if (resolved === 'array' && s.items) {
|
||||
// Apply mapping to array items when simple types
|
||||
const itemUi = mapForType(s.items)
|
||||
return itemUi ? { items: itemUi } : undefined
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
return mapForType(schema)
|
||||
}
|
||||
|
||||
export default function ConfigPage() {
|
||||
const [schemas, setSchemas] = useState({})
|
||||
const [currentId, setCurrentId] = useState('plc')
|
||||
const [schema, setSchema] = useState(null)
|
||||
const [uiSchema, setUiSchema] = useState(undefined)
|
||||
const [formData, setFormData] = useState(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [message, setMessage] = useState('')
|
||||
|
||||
const available = useMemo(() => {
|
||||
if (!schemas) return []
|
||||
if (Array.isArray(schemas.schemas)) return schemas.schemas.map(s => s.id)
|
||||
if (schemas.schemas && typeof schemas.schemas === 'object') return Object.keys(schemas.schemas)
|
||||
// Fallback: return empty array if no schemas detected
|
||||
return []
|
||||
}, [schemas])
|
||||
|
||||
const load = async (id) => {
|
||||
setLoading(true)
|
||||
setMessage('')
|
||||
try {
|
||||
const [schemaResp, dataResp] = await Promise.all([
|
||||
getSchema(id),
|
||||
readConfig(id),
|
||||
])
|
||||
setSchema(schemaResp.schema)
|
||||
setUiSchema(schemaResp.ui_schema || buildUiSchema(schemaResp.schema))
|
||||
setFormData(dataResp.data)
|
||||
} catch (e) {
|
||||
setMessage(e.message || 'Error loading schema/config')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
listSchemas().then(setSchemas).catch(() => { })
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (currentId) {
|
||||
load(currentId)
|
||||
}
|
||||
}, [currentId])
|
||||
|
||||
const handleSave = async ({ formData: newData }) => {
|
||||
setLoading(true)
|
||||
setMessage('')
|
||||
try {
|
||||
await writeConfig(currentId, newData)
|
||||
setFormData(newData)
|
||||
setMessage('Saved successfully')
|
||||
} catch (e) {
|
||||
setMessage(e.message || 'Error saving configuration')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleFileImport = async (evt) => {
|
||||
const file = evt.target.files?.[0]
|
||||
if (!file) return
|
||||
try {
|
||||
const text = await file.text()
|
||||
const json = JSON.parse(text)
|
||||
setFormData(json)
|
||||
setMessage(`Imported ${file.name}`)
|
||||
} catch (e) {
|
||||
setMessage('Invalid JSON file')
|
||||
}
|
||||
}
|
||||
|
||||
const handleExport = () => {
|
||||
const blob = new Blob([JSON.stringify(formData ?? {}, null, 2)], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `${currentId}_config.json`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container py={3} maxW="container.lg">
|
||||
<HStack mb={3} align="center">
|
||||
<Heading as="h2" size="md">Config Editor</Heading>
|
||||
<Menu>
|
||||
<MenuButton as={Button} size="sm" variant="outline">Schema: {currentId}</MenuButton>
|
||||
<MenuList>
|
||||
{available.map(id => (
|
||||
<MenuItem key={id} onClick={() => setCurrentId(id)}>{id}</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
<Spacer />
|
||||
<HStack>
|
||||
<Button as="label" size="sm" variant="outline">
|
||||
⬆️ Import
|
||||
<input type="file" accept="application/json" onChange={handleFileImport} hidden />
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={handleExport}>⬇️ Export</Button>
|
||||
</HStack>
|
||||
</HStack>
|
||||
|
||||
{message && (
|
||||
<Alert status="info" mb={3} borderRadius="md"><AlertIcon />{message}</Alert>
|
||||
)}
|
||||
|
||||
{schema && (
|
||||
<Form
|
||||
schema={schema}
|
||||
formData={formData}
|
||||
validator={validator}
|
||||
onSubmit={handleSave}
|
||||
onChange={({ formData }) => setFormData(formData)}
|
||||
uiSchema={uiSchema}
|
||||
templates={{ ObjectFieldTemplate: LayoutObjectFieldTemplate }}
|
||||
widgets={widgets}
|
||||
>
|
||||
<HStack spacing={2}>
|
||||
<Button type="submit" isDisabled={loading}>💾 Save</Button>
|
||||
<Button variant="outline" type="button" onClick={() => load(currentId)} isDisabled={loading}>🔄 Reload</Button>
|
||||
</HStack>
|
||||
</Form>
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
|
@ -4,11 +4,6 @@ import {
|
|||
Container,
|
||||
VStack,
|
||||
Heading,
|
||||
Tabs,
|
||||
TabList,
|
||||
TabPanels,
|
||||
Tab,
|
||||
TabPanel,
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
|
@ -853,43 +848,28 @@ function DashboardContent() {
|
|||
|
||||
<StatusBar status={status} onRefresh={loadStatus} />
|
||||
|
||||
<Tabs variant="enclosed" colorScheme="blue">
|
||||
<TabList>
|
||||
<Tab>🔧 Configuration</Tab>
|
||||
<Tab>📊 Datasets</Tab>
|
||||
<Tab>📈 Plotting</Tab>
|
||||
<Tab>📋 Events</Tab>
|
||||
</TabList>
|
||||
{/* PLC Configuration Section */}
|
||||
<ConfigurationPanel
|
||||
schemaData={schemaData}
|
||||
formData={formData}
|
||||
onFormChange={setFormData}
|
||||
onSave={saveConfig}
|
||||
saving={saving}
|
||||
message={message}
|
||||
/>
|
||||
|
||||
<TabPanels>
|
||||
<TabPanel p={0} pt={4}>
|
||||
<ConfigurationPanel
|
||||
schemaData={schemaData}
|
||||
formData={formData}
|
||||
onFormChange={setFormData}
|
||||
onSave={saveConfig}
|
||||
saving={saving}
|
||||
message={message}
|
||||
/>
|
||||
</TabPanel>
|
||||
{/* Dataset Management Section */}
|
||||
<DatasetManager />
|
||||
|
||||
<TabPanel p={0} pt={4}>
|
||||
<DatasetManager />
|
||||
</TabPanel>
|
||||
{/* Plot Management Section */}
|
||||
<PlotManager />
|
||||
|
||||
<TabPanel p={0} pt={4}>
|
||||
<PlotManager />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel p={0} pt={4}>
|
||||
<EventsDisplay
|
||||
events={events}
|
||||
loading={eventsLoading}
|
||||
onRefresh={loadEvents}
|
||||
/>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
{/* Events Section */}
|
||||
<EventsDisplay
|
||||
events={events}
|
||||
loading={eventsLoading}
|
||||
onRefresh={loadEvents}
|
||||
/>
|
||||
</VStack>
|
||||
</Container>
|
||||
)
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import { getEvents } from '../services/api.js'
|
||||
import { Container, Heading, HStack, Button, Alert, AlertIcon, Table, Thead, Tbody, Tr, Th, Td, Box, Text, useColorModeValue } from '@chakra-ui/react'
|
||||
|
||||
export default function EventsPage() {
|
||||
const [events, setEvents] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const load = async () => {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
try {
|
||||
const data = await getEvents(100)
|
||||
if (data && data.success) {
|
||||
setEvents(data.events || [])
|
||||
} else {
|
||||
setError(data?.error || 'Unexpected response')
|
||||
}
|
||||
} catch (e) {
|
||||
setError(e.message || 'Error fetching events')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
load()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Container py={3} maxW="container.xl">
|
||||
<Heading as="h2" size="md" mb={3}>Events</Heading>
|
||||
<HStack mb={3}>
|
||||
<Button size="sm" colorScheme="blue" onClick={load} isDisabled={loading}>
|
||||
{loading ? 'Cargando...' : 'Refrescar'}
|
||||
</Button>
|
||||
<Button as="a" href="/api/events" target="_blank" rel="noreferrer" size="sm" variant="outline">/api/events</Button>
|
||||
</HStack>
|
||||
|
||||
{error && <Alert status="error" mb={3}><AlertIcon />{error}</Alert>}
|
||||
{loading && !error && <Alert status="info" mb={3}><AlertIcon />Cargando eventos...</Alert>}
|
||||
|
||||
{!loading && !error && (
|
||||
<Box overflowX="auto">
|
||||
<Table size="sm" variant="striped">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th width="10%">Time</Th>
|
||||
<Th width="10%">Level</Th>
|
||||
<Th>Message</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{events.map((ev, idx) => (
|
||||
<Tr key={idx}>
|
||||
<Td whiteSpace="nowrap">{ev.timestamp || '-'}</Td>
|
||||
<Td>{ev.level || ev.type || 'INFO'}</Td>
|
||||
<Td>
|
||||
<Text fontWeight="semibold">{ev.message || ev.event || '-'}</Text>
|
||||
{ev.details && (
|
||||
<Text fontSize="sm" color={useColorModeValue('gray.600', 'gray.300')}>
|
||||
{typeof ev.details === 'object' ? JSON.stringify(ev.details) : String(ev.details)}
|
||||
</Text>
|
||||
)}
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</Box>
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
23
main.py
23
main.py
|
@ -70,20 +70,13 @@ def serve_image(filename):
|
|||
return send_from_directory(".images", filename)
|
||||
|
||||
|
||||
@app.route("/static/<path:filename>")
|
||||
def serve_static(filename):
|
||||
"""Serve static files (CSS, JS, etc.)"""
|
||||
return send_from_directory("static", filename)
|
||||
|
||||
|
||||
@app.route("/favicon.ico")
|
||||
def serve_favicon():
|
||||
"""Serve application favicon from robust locations.
|
||||
"""Serve application favicon from React public folder.
|
||||
|
||||
Priority:
|
||||
1) frontend/public/favicon.ico
|
||||
2) frontend/public/record.png
|
||||
3) static/icons/record.png
|
||||
"""
|
||||
# Use absolute paths for reliability (works in dev and bundled)
|
||||
public_dir = project_path("frontend", "public")
|
||||
|
@ -95,28 +88,19 @@ def serve_favicon():
|
|||
if os.path.exists(public_record):
|
||||
return send_from_directory(public_dir, "record.png")
|
||||
|
||||
# Fallback: static/icons
|
||||
static_icons_dir = project_path("static", "icons")
|
||||
if os.path.exists(os.path.join(static_icons_dir, "record.png")):
|
||||
return send_from_directory(static_icons_dir, "record.png")
|
||||
|
||||
# Final fallback: 404
|
||||
return Response("Favicon not found", status=404, mimetype="text/plain")
|
||||
|
||||
|
||||
@app.route("/record.png")
|
||||
def serve_public_record_png():
|
||||
"""Serve /record.png from the React public folder with fallbacks."""
|
||||
"""Serve /record.png from the React public folder."""
|
||||
public_dir = project_path("frontend", "public")
|
||||
public_record = os.path.join(public_dir, "record.png")
|
||||
|
||||
if os.path.exists(public_record):
|
||||
return send_from_directory(public_dir, "record.png")
|
||||
|
||||
static_icons_dir = project_path("static", "icons")
|
||||
if os.path.exists(os.path.join(static_icons_dir, "record.png")):
|
||||
return send_from_directory(static_icons_dir, "record.png")
|
||||
|
||||
return Response("record.png not found", status=404, mimetype="text/plain")
|
||||
|
||||
|
||||
|
@ -2111,9 +2095,6 @@ def main():
|
|||
|
||||
while retry_count < max_retries:
|
||||
try:
|
||||
# Create templates directory if it doesn't exist
|
||||
os.makedirs("templates", exist_ok=True)
|
||||
|
||||
print("🚀 Starting Flask server for PLC S7-315 Streamer")
|
||||
print("📊 Web interface available at: http://localhost:5050")
|
||||
print("🔧 Configure your PLC and variables through the web interface")
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Before Width: | Height: | Size: 16 KiB |
Binary file not shown.
Before Width: | Height: | Size: 16 KiB |
|
@ -1,461 +0,0 @@
|
|||
/**
|
||||
* 📈 Chart.js Plugin Streaming Integration
|
||||
* Integra chartjs-plugin-streaming para plotting en tiempo real
|
||||
*
|
||||
* Combinación de módulos:
|
||||
* - helpers.streaming.js
|
||||
* - scale.realtime.js
|
||||
* - plugin.streaming.js
|
||||
* - plugin.zoom.js (integración con zoom)
|
||||
*/
|
||||
|
||||
(function (global, factory) {
|
||||
if (typeof exports === 'object' && typeof module !== 'undefined') {
|
||||
factory(exports, require('chart.js'));
|
||||
} else if (typeof define === 'function' && define.amd) {
|
||||
define(['exports', 'chart.js'], factory);
|
||||
} else {
|
||||
global = global || self;
|
||||
factory(global.ChartStreaming = {}, global.Chart);
|
||||
}
|
||||
})(this, function (exports, Chart) {
|
||||
'use strict';
|
||||
|
||||
// ============= HELPERS.STREAMING.JS =============
|
||||
function clamp(value, min, max) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
function resolveOption(scale, key) {
|
||||
const realtimeOptions = scale.options.realtime || {};
|
||||
const scaleOptions = scale.options;
|
||||
|
||||
if (realtimeOptions[key] !== undefined) {
|
||||
return realtimeOptions[key];
|
||||
}
|
||||
if (scaleOptions[key] !== undefined) {
|
||||
return scaleOptions[key];
|
||||
}
|
||||
|
||||
// Valores por defecto
|
||||
const defaults = {
|
||||
duration: 10000,
|
||||
delay: 0,
|
||||
refresh: 1000,
|
||||
frameRate: 30,
|
||||
pause: false,
|
||||
ttl: undefined,
|
||||
onRefresh: null
|
||||
};
|
||||
|
||||
return defaults[key];
|
||||
}
|
||||
|
||||
function getAxisMap(element, keys, meta) {
|
||||
const axis = meta.vAxisID || 'y';
|
||||
return keys[axis] || [];
|
||||
}
|
||||
|
||||
// ============= SCALE.REALTIME.JS (Corregido) =============
|
||||
class RealTimeScale extends Chart.Scale {
|
||||
constructor(cfg) {
|
||||
super(cfg);
|
||||
this.type = 'realtime';
|
||||
}
|
||||
|
||||
init(scaleOptions, scaleContext) {
|
||||
super.init(scaleOptions, scaleContext);
|
||||
|
||||
const me = this;
|
||||
const chart = me.chart;
|
||||
const streaming = chart.$streaming = chart.$streaming || {};
|
||||
streaming.enabled = true; // Marcar como streaming activo
|
||||
|
||||
// 🔧 DEBUG: Ver qué opciones estamos recibiendo
|
||||
console.log('📈 RealTimeScale DEBUG - scaleOptions:', scaleOptions);
|
||||
console.log('📈 RealTimeScale DEBUG - me.options:', me.options);
|
||||
console.log('📈 RealTimeScale DEBUG - me.options.realtime:', me.options.realtime);
|
||||
|
||||
// Inicializar opciones de tiempo real
|
||||
const onRefreshResolved = resolveOption(me, 'onRefresh');
|
||||
console.log('📈 RealTimeScale DEBUG - onRefresh resolved:', onRefreshResolved, typeof onRefreshResolved);
|
||||
|
||||
me.realtime = {
|
||||
duration: resolveOption(me, 'duration'),
|
||||
delay: resolveOption(me, 'delay'),
|
||||
refresh: resolveOption(me, 'refresh'),
|
||||
frameRate: resolveOption(me, 'frameRate'),
|
||||
pause: resolveOption(me, 'pause'),
|
||||
ttl: resolveOption(me, 'ttl'),
|
||||
onRefresh: onRefreshResolved
|
||||
};
|
||||
|
||||
console.log('📈 RealTimeScale initialized:', {
|
||||
duration: me.realtime.duration,
|
||||
refresh: me.realtime.refresh,
|
||||
pause: me.realtime.pause,
|
||||
hasOnRefresh: typeof me.realtime.onRefresh === 'function'
|
||||
});
|
||||
|
||||
// Configurar intervalo de obtención de datos (refresh)
|
||||
if (me.realtime.refresh > 0) {
|
||||
if (streaming.intervalId) {
|
||||
clearInterval(streaming.intervalId);
|
||||
}
|
||||
streaming.intervalId = setInterval(() => {
|
||||
if (!me.realtime.pause && typeof me.realtime.onRefresh === 'function') {
|
||||
me.realtime.onRefresh(chart);
|
||||
}
|
||||
}, me.realtime.refresh);
|
||||
console.log('📈 RealTimeScale data interval started:', me.realtime.refresh + 'ms');
|
||||
}
|
||||
|
||||
// Configurar intervalo de render (frameRate)
|
||||
const fps = Math.max(1, me.realtime.frameRate || 30);
|
||||
const frameIntervalMs = Math.round(1000 / fps);
|
||||
if (streaming.frameIntervalId) {
|
||||
clearInterval(streaming.frameIntervalId);
|
||||
}
|
||||
streaming.frameIntervalId = setInterval(() => {
|
||||
if (!me.realtime.pause) {
|
||||
me.updateRealTimeData();
|
||||
chart.update('quiet');
|
||||
}
|
||||
}, frameIntervalMs);
|
||||
console.log('🎞️ RealTimeScale render interval started:', frameIntervalMs + 'ms (' + fps + ' fps)');
|
||||
}
|
||||
|
||||
updateRealTimeData() {
|
||||
const me = this;
|
||||
const chart = me.chart;
|
||||
|
||||
if (!chart.data || !chart.data.datasets) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const duration = me.realtime.duration;
|
||||
const delay = me.realtime.delay;
|
||||
const ttl = me.realtime.ttl || duration * 2; // TTL por defecto
|
||||
|
||||
// Calcular ventana de tiempo
|
||||
me.max = now - delay;
|
||||
me.min = me.max - duration;
|
||||
|
||||
// Limpiar datos antiguos automáticamente
|
||||
const cutoff = now - ttl;
|
||||
chart.data.datasets.forEach(dataset => {
|
||||
if (dataset.data) {
|
||||
const oldLength = dataset.data.length;
|
||||
dataset.data = dataset.data.filter(point => point.x > cutoff);
|
||||
if (oldLength !== dataset.data.length) {
|
||||
console.log(`📈 Cleaned ${oldLength - dataset.data.length} old points from ${dataset.label}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
update(args) {
|
||||
this.updateRealTimeData();
|
||||
super.update(args);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
const me = this;
|
||||
const chart = me.chart;
|
||||
const streaming = chart.$streaming;
|
||||
|
||||
if (streaming) {
|
||||
if (streaming.intervalId) {
|
||||
clearInterval(streaming.intervalId);
|
||||
delete streaming.intervalId;
|
||||
console.log('📈 RealTimeScale data interval cleared');
|
||||
}
|
||||
if (streaming.frameIntervalId) {
|
||||
clearInterval(streaming.frameIntervalId);
|
||||
delete streaming.frameIntervalId;
|
||||
console.log('🎞️ RealTimeScale render interval cleared');
|
||||
}
|
||||
}
|
||||
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Configurar ID y defaults fuera de la clase para mejor compatibilidad
|
||||
RealTimeScale.id = 'realtime';
|
||||
RealTimeScale.defaults = {
|
||||
realtime: {
|
||||
duration: 10000,
|
||||
delay: 0,
|
||||
refresh: 1000,
|
||||
frameRate: 30,
|
||||
pause: false,
|
||||
ttl: undefined,
|
||||
onRefresh: null
|
||||
},
|
||||
time: {
|
||||
unit: 'second',
|
||||
displayFormats: {
|
||||
second: 'HH:mm:ss'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ============= PLUGIN.STREAMING.JS (Simplificado) =============
|
||||
const streamingPlugin = {
|
||||
id: 'streaming',
|
||||
|
||||
beforeInit(chart) {
|
||||
const streaming = chart.$streaming = chart.$streaming || {};
|
||||
streaming.enabled = false;
|
||||
|
||||
// Detectar si hay escalas realtime
|
||||
const scales = chart.options.scales || {};
|
||||
Object.keys(scales).forEach(scaleId => {
|
||||
if (scales[scaleId].type === 'realtime') {
|
||||
streaming.enabled = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
afterInit(chart) {
|
||||
const streaming = chart.$streaming;
|
||||
if (streaming && streaming.enabled) {
|
||||
// Configurar actualización automática
|
||||
const update = chart.update;
|
||||
chart.update = function (mode) {
|
||||
if (mode === 'quiet') {
|
||||
// Actualización silenciosa para streaming
|
||||
Chart.prototype.update.call(this, mode);
|
||||
} else {
|
||||
update.call(this, mode);
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
beforeUpdate(chart) {
|
||||
const streaming = chart.$streaming;
|
||||
if (!streaming || !streaming.enabled) return;
|
||||
|
||||
// Permitir que las líneas Bézier se extiendan fuera del área del gráfico
|
||||
const elements = chart.options.elements || {};
|
||||
if (elements.line) {
|
||||
elements.line.capBezierPoints = false;
|
||||
}
|
||||
},
|
||||
|
||||
destroy(chart) {
|
||||
const streaming = chart.$streaming;
|
||||
if (streaming && streaming.intervalId) {
|
||||
clearInterval(streaming.intervalId);
|
||||
delete streaming.intervalId;
|
||||
}
|
||||
delete chart.$streaming;
|
||||
}
|
||||
};
|
||||
|
||||
// ============= REGISTRO DE COMPONENTES =============
|
||||
|
||||
// Intentar registro simple de componentes
|
||||
function tryRegisterComponents() {
|
||||
if (typeof Chart !== 'undefined' && Chart.register) {
|
||||
try {
|
||||
console.log('📈 Attempting Chart.js streaming components registration...');
|
||||
|
||||
// Registrar escala realtime
|
||||
Chart.register(RealTimeScale);
|
||||
|
||||
// Registrar plugin de streaming
|
||||
Chart.register(streamingPlugin);
|
||||
|
||||
console.log('📈 Streaming components registration attempt completed');
|
||||
console.log('📈 RealTimeScale available:', !!Chart.registry.scales.realtime);
|
||||
|
||||
if (Chart.registry.scales.realtime) {
|
||||
console.log('✅ RealTimeScale registered successfully');
|
||||
window.chartStreamingRegistered = true;
|
||||
} else {
|
||||
console.log('⚠️ RealTimeScale registration may have failed - fallback mode will be used');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log('⚠️ Chart.js streaming components registration failed:', error.message);
|
||||
console.log('📋 Fallback mode will be used instead (this is perfectly fine)');
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ Chart.js not ready for component registration');
|
||||
}
|
||||
}
|
||||
|
||||
// Intentar registro una vez inmediatamente
|
||||
tryRegisterComponents();
|
||||
|
||||
// Y también cuando el DOM esté listo (por si acaso)
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', tryRegisterComponents);
|
||||
}
|
||||
|
||||
// ============= UTILIDADES PARA LA APLICACIÓN =============
|
||||
|
||||
/**
|
||||
* Crea una configuración de Chart.js optimizada para streaming
|
||||
*/
|
||||
function createStreamingChartConfig(options = {}) {
|
||||
const config = {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: []
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: false, // Desactivar animaciones para mejor performance
|
||||
|
||||
scales: {
|
||||
x: {
|
||||
type: 'realtime',
|
||||
realtime: {
|
||||
duration: options.duration || 60000, // 60 segundos por defecto
|
||||
delay: options.delay || 0,
|
||||
refresh: options.refresh || 1000, // 1 segundo
|
||||
frameRate: options.frameRate || 30,
|
||||
pause: options.pause || false,
|
||||
ttl: options.ttl || undefined,
|
||||
onRefresh: options.onRefresh || null
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Tiempo'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Valor'
|
||||
},
|
||||
min: options.yMin,
|
||||
max: options.yMax
|
||||
}
|
||||
},
|
||||
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'top'
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
}
|
||||
},
|
||||
|
||||
interaction: {
|
||||
mode: 'nearest',
|
||||
axis: 'x',
|
||||
intersect: false
|
||||
},
|
||||
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0, // Sin puntos para mejor performance
|
||||
hoverRadius: 3
|
||||
},
|
||||
line: {
|
||||
tension: 0.1,
|
||||
borderWidth: 2
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: ['streaming']
|
||||
};
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Agrega datos a un dataset de streaming
|
||||
*/
|
||||
function addStreamingData(chart, datasetIndex, data) {
|
||||
if (!chart || !chart.data || !chart.data.datasets[datasetIndex]) {
|
||||
console.warn(`📈 Cannot add streaming data - chart or dataset ${datasetIndex} not found`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const dataset = chart.data.datasets[datasetIndex];
|
||||
if (!dataset.data) {
|
||||
dataset.data = [];
|
||||
}
|
||||
|
||||
// Agregar nuevo punto con timestamp
|
||||
const timestamp = data.x || Date.now();
|
||||
const newPoint = {
|
||||
x: timestamp,
|
||||
y: data.y
|
||||
};
|
||||
|
||||
dataset.data.push(newPoint);
|
||||
|
||||
console.log(`📈 Added point to dataset ${datasetIndex} (${dataset.label}): x=${timestamp}, y=${data.y}`);
|
||||
|
||||
// Chart.js se encarga automáticamente de eliminar datos antiguos
|
||||
// basado en la configuración de TTL y duration de la escala realtime
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Controla la pausa/reanudación del streaming
|
||||
*/
|
||||
function setStreamingPause(chart, paused) {
|
||||
if (!chart || !chart.$streaming) return;
|
||||
|
||||
const scales = chart.scales;
|
||||
Object.keys(scales).forEach(scaleId => {
|
||||
const scale = scales[scaleId];
|
||||
if (scale instanceof RealTimeScale) {
|
||||
scale.realtime.pause = paused;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpia todos los datos de streaming
|
||||
*/
|
||||
function clearStreamingData(chart) {
|
||||
if (!chart || !chart.data) return;
|
||||
|
||||
chart.data.datasets.forEach(dataset => {
|
||||
if (dataset.data) {
|
||||
dataset.data.length = 0;
|
||||
}
|
||||
});
|
||||
|
||||
chart.update('quiet');
|
||||
}
|
||||
|
||||
// ============= EXPORTS =============
|
||||
|
||||
// Exportar para uso en la aplicación
|
||||
exports.RealTimeScale = RealTimeScale;
|
||||
exports.streamingPlugin = streamingPlugin;
|
||||
exports.createStreamingChartConfig = createStreamingChartConfig;
|
||||
exports.addStreamingData = addStreamingData;
|
||||
exports.setStreamingPause = setStreamingPause;
|
||||
exports.clearStreamingData = clearStreamingData;
|
||||
|
||||
// Hacer disponible globalmente
|
||||
if (typeof window !== 'undefined') {
|
||||
window.ChartStreaming = {
|
||||
createStreamingChartConfig,
|
||||
addStreamingData,
|
||||
setStreamingPause,
|
||||
clearStreamingData,
|
||||
RealTimeScale,
|
||||
streamingPlugin
|
||||
};
|
||||
}
|
||||
|
||||
console.log('📈 Chart.js Streaming Plugin loaded successfully');
|
||||
});
|
|
@ -1,85 +0,0 @@
|
|||
import {callback as call, each, noop, requestAnimFrame, valueOrDefault} from 'chart.js/helpers';
|
||||
|
||||
export function clamp(value, lower, upper) {
|
||||
return Math.min(Math.max(value, lower), upper);
|
||||
}
|
||||
|
||||
export function resolveOption(scale, key) {
|
||||
const realtimeOpts = scale.options.realtime;
|
||||
const streamingOpts = scale.chart.options.plugins.streaming;
|
||||
return valueOrDefault(realtimeOpts[key], streamingOpts[key]);
|
||||
}
|
||||
|
||||
export function getAxisMap(element, {x, y}, {xAxisID, yAxisID}) {
|
||||
const axisMap = {};
|
||||
|
||||
each(x, key => {
|
||||
axisMap[key] = {axisId: xAxisID};
|
||||
});
|
||||
each(y, key => {
|
||||
axisMap[key] = {axisId: yAxisID};
|
||||
});
|
||||
return axisMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel animation polyfill
|
||||
*/
|
||||
const cancelAnimFrame = (function() {
|
||||
if (typeof window === 'undefined') {
|
||||
return noop;
|
||||
}
|
||||
return window.cancelAnimationFrame;
|
||||
}());
|
||||
|
||||
export function startFrameRefreshTimer(context, func) {
|
||||
if (!context.frameRequestID) {
|
||||
const refresh = () => {
|
||||
const nextRefresh = context.nextRefresh || 0;
|
||||
const now = Date.now();
|
||||
|
||||
if (nextRefresh <= now) {
|
||||
const newFrameRate = call(func);
|
||||
const frameDuration = 1000 / (Math.max(newFrameRate, 0) || 30);
|
||||
const newNextRefresh = context.nextRefresh + frameDuration || 0;
|
||||
|
||||
context.nextRefresh = newNextRefresh > now ? newNextRefresh : now + frameDuration;
|
||||
}
|
||||
context.frameRequestID = requestAnimFrame.call(window, refresh);
|
||||
};
|
||||
context.frameRequestID = requestAnimFrame.call(window, refresh);
|
||||
}
|
||||
}
|
||||
|
||||
export function stopFrameRefreshTimer(context) {
|
||||
const frameRequestID = context.frameRequestID;
|
||||
|
||||
if (frameRequestID) {
|
||||
cancelAnimFrame.call(window, frameRequestID);
|
||||
delete context.frameRequestID;
|
||||
}
|
||||
}
|
||||
|
||||
export function stopDataRefreshTimer(context) {
|
||||
const refreshTimerID = context.refreshTimerID;
|
||||
|
||||
if (refreshTimerID) {
|
||||
clearInterval(refreshTimerID);
|
||||
delete context.refreshTimerID;
|
||||
delete context.refreshInterval;
|
||||
}
|
||||
}
|
||||
|
||||
export function startDataRefreshTimer(context, func, interval) {
|
||||
if (!context.refreshTimerID) {
|
||||
context.refreshTimerID = setInterval(() => {
|
||||
const newInterval = call(func);
|
||||
|
||||
if (context.refreshInterval !== newInterval && !isNaN(newInterval)) {
|
||||
stopDataRefreshTimer(context);
|
||||
startDataRefreshTimer(context, func, newInterval);
|
||||
}
|
||||
}, interval || 0);
|
||||
context.refreshInterval = interval || 0;
|
||||
}
|
||||
}
|
|
@ -1,216 +0,0 @@
|
|||
import {Chart, DatasetController, defaults, registry} from 'chart.js';
|
||||
import {each, noop, getRelativePosition, clipArea, unclipArea} from 'chart.js/helpers';
|
||||
import {getAxisMap} from '../helpers/helpers.streaming';
|
||||
import {attachChart as annotationAttachChart, detachChart as annotationDetachChart} from '../plugins/plugin.annotation';
|
||||
import {update as tooltipUpdate} from '../plugins/plugin.tooltip';
|
||||
import {attachChart as zoomAttachChart, detachChart as zoomDetachChart} from '../plugins/plugin.zoom';
|
||||
import RealTimeScale from '../scales/scale.realtime';
|
||||
import {version} from '../../package.json';
|
||||
|
||||
defaults.set('transitions', {
|
||||
quiet: {
|
||||
animation: {
|
||||
duration: 0
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const transitionKeys = {x: ['x', 'cp1x', 'cp2x'], y: ['y', 'cp1y', 'cp2y']};
|
||||
|
||||
function update(mode) {
|
||||
const me = this;
|
||||
|
||||
if (mode === 'quiet') {
|
||||
each(me.data.datasets, (dataset, datasetIndex) => {
|
||||
const controller = me.getDatasetMeta(datasetIndex).controller;
|
||||
|
||||
// Set transition mode to 'quiet'
|
||||
controller._setStyle = function(element, index, _mode, active) {
|
||||
DatasetController.prototype._setStyle.call(this, element, index, 'quiet', active);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
Chart.prototype.update.call(me, mode);
|
||||
|
||||
if (mode === 'quiet') {
|
||||
each(me.data.datasets, (dataset, datasetIndex) => {
|
||||
delete me.getDatasetMeta(datasetIndex).controller._setStyle;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function render(chart) {
|
||||
const streaming = chart.$streaming;
|
||||
|
||||
chart.render();
|
||||
|
||||
if (streaming.lastMouseEvent) {
|
||||
setTimeout(() => {
|
||||
const lastMouseEvent = streaming.lastMouseEvent;
|
||||
if (lastMouseEvent) {
|
||||
chart._eventHandler(lastMouseEvent);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
id: 'streaming',
|
||||
|
||||
version,
|
||||
|
||||
beforeInit(chart) {
|
||||
const streaming = chart.$streaming = chart.$streaming || {render};
|
||||
const canvas = streaming.canvas = chart.canvas;
|
||||
const mouseEventListener = streaming.mouseEventListener = event => {
|
||||
const pos = getRelativePosition(event, chart);
|
||||
streaming.lastMouseEvent = {
|
||||
type: 'mousemove',
|
||||
chart: chart,
|
||||
native: event,
|
||||
x: pos.x,
|
||||
y: pos.y
|
||||
};
|
||||
};
|
||||
|
||||
canvas.addEventListener('mousedown', mouseEventListener);
|
||||
canvas.addEventListener('mouseup', mouseEventListener);
|
||||
},
|
||||
|
||||
afterInit(chart) {
|
||||
chart.update = update;
|
||||
},
|
||||
|
||||
beforeUpdate(chart) {
|
||||
const {scales, elements} = chart.options;
|
||||
const tooltip = chart.tooltip;
|
||||
|
||||
each(scales, ({type}) => {
|
||||
if (type === 'realtime') {
|
||||
// Allow Bézier control to be outside the chart
|
||||
elements.line.capBezierPoints = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (tooltip) {
|
||||
tooltip.update = tooltipUpdate;
|
||||
}
|
||||
|
||||
try {
|
||||
const plugin = registry.getPlugin('annotation');
|
||||
annotationAttachChart(plugin, chart);
|
||||
} catch (e) {
|
||||
annotationDetachChart(chart);
|
||||
}
|
||||
|
||||
try {
|
||||
const plugin = registry.getPlugin('zoom');
|
||||
zoomAttachChart(plugin, chart);
|
||||
} catch (e) {
|
||||
zoomDetachChart(chart);
|
||||
}
|
||||
},
|
||||
|
||||
beforeDatasetUpdate(chart, args) {
|
||||
const {meta, mode} = args;
|
||||
|
||||
if (mode === 'quiet') {
|
||||
const {controller, $animations} = meta;
|
||||
|
||||
// Skip updating element options if show/hide transition is active
|
||||
if ($animations && $animations.visible && $animations.visible._active) {
|
||||
controller.updateElement = noop;
|
||||
controller.updateSharedOptions = noop;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
afterDatasetUpdate(chart, args) {
|
||||
const {meta, mode} = args;
|
||||
const {data: elements = [], dataset: element, controller} = meta;
|
||||
|
||||
for (let i = 0, ilen = elements.length; i < ilen; ++i) {
|
||||
elements[i].$streaming = getAxisMap(elements[i], transitionKeys, meta);
|
||||
}
|
||||
if (element) {
|
||||
element.$streaming = getAxisMap(element, transitionKeys, meta);
|
||||
}
|
||||
|
||||
if (mode === 'quiet') {
|
||||
delete controller.updateElement;
|
||||
delete controller.updateSharedOptions;
|
||||
}
|
||||
},
|
||||
|
||||
beforeDatasetDraw(chart, args) {
|
||||
const {ctx, chartArea, width, height} = chart;
|
||||
const {xAxisID, yAxisID, controller} = args.meta;
|
||||
const area = {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: width,
|
||||
bottom: height
|
||||
};
|
||||
|
||||
if (xAxisID && controller.getScaleForId(xAxisID) instanceof RealTimeScale) {
|
||||
area.left = chartArea.left;
|
||||
area.right = chartArea.right;
|
||||
}
|
||||
if (yAxisID && controller.getScaleForId(yAxisID) instanceof RealTimeScale) {
|
||||
area.top = chartArea.top;
|
||||
area.bottom = chartArea.bottom;
|
||||
}
|
||||
clipArea(ctx, area);
|
||||
},
|
||||
|
||||
afterDatasetDraw(chart) {
|
||||
unclipArea(chart.ctx);
|
||||
},
|
||||
|
||||
beforeEvent(chart, args) {
|
||||
const streaming = chart.$streaming;
|
||||
const event = args.event;
|
||||
|
||||
if (event.type === 'mousemove') {
|
||||
// Save mousemove event for reuse
|
||||
streaming.lastMouseEvent = event;
|
||||
} else if (event.type === 'mouseout') {
|
||||
// Remove mousemove event
|
||||
delete streaming.lastMouseEvent;
|
||||
}
|
||||
},
|
||||
|
||||
destroy(chart) {
|
||||
const {scales, $streaming: streaming, tooltip} = chart;
|
||||
const {canvas, mouseEventListener} = streaming;
|
||||
|
||||
delete chart.update;
|
||||
if (tooltip) {
|
||||
delete tooltip.update;
|
||||
}
|
||||
|
||||
canvas.removeEventListener('mousedown', mouseEventListener);
|
||||
canvas.removeEventListener('mouseup', mouseEventListener);
|
||||
|
||||
each(scales, scale => {
|
||||
if (scale instanceof RealTimeScale) {
|
||||
scale.destroy();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
defaults: {
|
||||
duration: 10000,
|
||||
delay: 0,
|
||||
frameRate: 30,
|
||||
refresh: 1000,
|
||||
onRefresh: null,
|
||||
pause: false,
|
||||
ttl: undefined
|
||||
},
|
||||
|
||||
descriptors: {
|
||||
_scriptable: name => name !== 'onRefresh'
|
||||
}
|
||||
};
|
|
@ -1,125 +0,0 @@
|
|||
import {each} from 'chart.js/helpers';
|
||||
import {clamp, resolveOption} from '../helpers/helpers.streaming';
|
||||
|
||||
const chartStates = new WeakMap();
|
||||
|
||||
function getState(chart) {
|
||||
let state = chartStates.get(chart);
|
||||
|
||||
if (!state) {
|
||||
state = {originalScaleOptions: {}};
|
||||
chartStates.set(chart, state);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function removeState(chart) {
|
||||
chartStates.delete(chart);
|
||||
}
|
||||
|
||||
function storeOriginalScaleOptions(chart) {
|
||||
const {originalScaleOptions} = getState(chart);
|
||||
const scales = chart.scales;
|
||||
|
||||
each(scales, scale => {
|
||||
const id = scale.id;
|
||||
|
||||
if (!originalScaleOptions[id]) {
|
||||
originalScaleOptions[id] = {
|
||||
duration: resolveOption(scale, 'duration'),
|
||||
delay: resolveOption(scale, 'delay')
|
||||
};
|
||||
}
|
||||
});
|
||||
each(originalScaleOptions, (opt, key) => {
|
||||
if (!scales[key]) {
|
||||
delete originalScaleOptions[key];
|
||||
}
|
||||
});
|
||||
return originalScaleOptions;
|
||||
}
|
||||
|
||||
function zoomRealTimeScale(scale, zoom, center, limits) {
|
||||
const {chart, axis} = scale;
|
||||
const {minDuration = 0, maxDuration = Infinity, minDelay = -Infinity, maxDelay = Infinity} = limits && limits[axis] || {};
|
||||
const realtimeOpts = scale.options.realtime;
|
||||
const duration = resolveOption(scale, 'duration');
|
||||
const delay = resolveOption(scale, 'delay');
|
||||
const newDuration = clamp(duration * (2 - zoom), minDuration, maxDuration);
|
||||
let maxPercent, newDelay;
|
||||
|
||||
storeOriginalScaleOptions(chart);
|
||||
|
||||
if (scale.isHorizontal()) {
|
||||
maxPercent = (scale.right - center.x) / (scale.right - scale.left);
|
||||
} else {
|
||||
maxPercent = (scale.bottom - center.y) / (scale.bottom - scale.top);
|
||||
}
|
||||
newDelay = delay + maxPercent * (duration - newDuration);
|
||||
realtimeOpts.duration = newDuration;
|
||||
realtimeOpts.delay = clamp(newDelay, minDelay, maxDelay);
|
||||
return newDuration !== scale.max - scale.min;
|
||||
}
|
||||
|
||||
function panRealTimeScale(scale, delta, limits) {
|
||||
const {chart, axis} = scale;
|
||||
const {minDelay = -Infinity, maxDelay = Infinity} = limits && limits[axis] || {};
|
||||
const delay = resolveOption(scale, 'delay');
|
||||
const newDelay = delay + (scale.getValueForPixel(delta) - scale.getValueForPixel(0));
|
||||
|
||||
storeOriginalScaleOptions(chart);
|
||||
|
||||
scale.options.realtime.delay = clamp(newDelay, minDelay, maxDelay);
|
||||
return true;
|
||||
}
|
||||
|
||||
function resetRealTimeScaleOptions(chart) {
|
||||
const originalScaleOptions = storeOriginalScaleOptions(chart);
|
||||
|
||||
each(chart.scales, scale => {
|
||||
const realtimeOptions = scale.options.realtime;
|
||||
|
||||
if (realtimeOptions) {
|
||||
const original = originalScaleOptions[scale.id];
|
||||
|
||||
if (original) {
|
||||
realtimeOptions.duration = original.duration;
|
||||
realtimeOptions.delay = original.delay;
|
||||
} else {
|
||||
delete realtimeOptions.duration;
|
||||
delete realtimeOptions.delay;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initZoomPlugin(plugin) {
|
||||
plugin.zoomFunctions.realtime = zoomRealTimeScale;
|
||||
plugin.panFunctions.realtime = panRealTimeScale;
|
||||
}
|
||||
|
||||
export function attachChart(plugin, chart) {
|
||||
const streaming = chart.$streaming;
|
||||
|
||||
if (streaming.zoomPlugin !== plugin) {
|
||||
const resetZoom = streaming.resetZoom = chart.resetZoom;
|
||||
|
||||
initZoomPlugin(plugin);
|
||||
chart.resetZoom = transition => {
|
||||
resetRealTimeScaleOptions(chart);
|
||||
resetZoom(transition);
|
||||
};
|
||||
streaming.zoomPlugin = plugin;
|
||||
}
|
||||
}
|
||||
|
||||
export function detachChart(chart) {
|
||||
const streaming = chart.$streaming;
|
||||
|
||||
if (streaming.zoomPlugin) {
|
||||
chart.resetZoom = streaming.resetZoom;
|
||||
removeState(chart);
|
||||
delete streaming.resetZoom;
|
||||
delete streaming.zoomPlugin;
|
||||
}
|
||||
}
|
|
@ -1,507 +0,0 @@
|
|||
import {defaults, TimeScale} from 'chart.js';
|
||||
import {_lookup, callback as call, each, isArray, isFinite, isNumber, noop, clipArea, unclipArea} from 'chart.js/helpers';
|
||||
import {resolveOption, startFrameRefreshTimer, stopFrameRefreshTimer, startDataRefreshTimer, stopDataRefreshTimer} from '../helpers/helpers.streaming';
|
||||
import {getElements} from '../plugins/plugin.annotation';
|
||||
|
||||
// Ported from Chart.js 2.8.0 35273ee.
|
||||
const INTERVALS = {
|
||||
millisecond: {
|
||||
common: true,
|
||||
size: 1,
|
||||
steps: [1, 2, 5, 10, 20, 50, 100, 250, 500]
|
||||
},
|
||||
second: {
|
||||
common: true,
|
||||
size: 1000,
|
||||
steps: [1, 2, 5, 10, 15, 30]
|
||||
},
|
||||
minute: {
|
||||
common: true,
|
||||
size: 60000,
|
||||
steps: [1, 2, 5, 10, 15, 30]
|
||||
},
|
||||
hour: {
|
||||
common: true,
|
||||
size: 3600000,
|
||||
steps: [1, 2, 3, 6, 12]
|
||||
},
|
||||
day: {
|
||||
common: true,
|
||||
size: 86400000,
|
||||
steps: [1, 2, 5]
|
||||
},
|
||||
week: {
|
||||
common: false,
|
||||
size: 604800000,
|
||||
steps: [1, 2, 3, 4]
|
||||
},
|
||||
month: {
|
||||
common: true,
|
||||
size: 2.628e9,
|
||||
steps: [1, 2, 3]
|
||||
},
|
||||
quarter: {
|
||||
common: false,
|
||||
size: 7.884e9,
|
||||
steps: [1, 2, 3, 4]
|
||||
},
|
||||
year: {
|
||||
common: true,
|
||||
size: 3.154e10
|
||||
}
|
||||
};
|
||||
|
||||
// Ported from Chart.js 2.8.0 35273ee.
|
||||
const UNITS = Object.keys(INTERVALS);
|
||||
|
||||
// Ported from Chart.js 2.8.0 35273ee.
|
||||
function determineStepSize(min, max, unit, capacity) {
|
||||
const range = max - min;
|
||||
const {size: milliseconds, steps} = INTERVALS[unit];
|
||||
let factor;
|
||||
|
||||
if (!steps) {
|
||||
return Math.ceil(range / (capacity * milliseconds));
|
||||
}
|
||||
|
||||
for (let i = 0, ilen = steps.length; i < ilen; ++i) {
|
||||
factor = steps[i];
|
||||
if (Math.ceil(range / (milliseconds * factor)) <= capacity) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return factor;
|
||||
}
|
||||
|
||||
// Ported from Chart.js 2.8.0 35273ee.
|
||||
function determineUnitForAutoTicks(minUnit, min, max, capacity) {
|
||||
const range = max - min;
|
||||
const ilen = UNITS.length;
|
||||
|
||||
for (let i = UNITS.indexOf(minUnit); i < ilen - 1; ++i) {
|
||||
const {common, size, steps} = INTERVALS[UNITS[i]];
|
||||
const factor = steps ? steps[steps.length - 1] : Number.MAX_SAFE_INTEGER;
|
||||
|
||||
if (common && Math.ceil(range / (factor * size)) <= capacity) {
|
||||
return UNITS[i];
|
||||
}
|
||||
}
|
||||
|
||||
return UNITS[ilen - 1];
|
||||
}
|
||||
|
||||
// Ported from Chart.js 2.8.0 35273ee.
|
||||
function determineMajorUnit(unit) {
|
||||
for (let i = UNITS.indexOf(unit) + 1, ilen = UNITS.length; i < ilen; ++i) {
|
||||
if (INTERVALS[UNITS[i]].common) {
|
||||
return UNITS[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ported from Chart.js 3.2.0 e1404ac.
|
||||
function addTick(ticks, time, timestamps) {
|
||||
if (!timestamps) {
|
||||
ticks[time] = true;
|
||||
} else if (timestamps.length) {
|
||||
const {lo, hi} = _lookup(timestamps, time);
|
||||
const timestamp = timestamps[lo] >= time ? timestamps[lo] : timestamps[hi];
|
||||
ticks[timestamp] = true;
|
||||
}
|
||||
}
|
||||
|
||||
const datasetPropertyKeys = [
|
||||
'pointBackgroundColor',
|
||||
'pointBorderColor',
|
||||
'pointBorderWidth',
|
||||
'pointRadius',
|
||||
'pointRotation',
|
||||
'pointStyle',
|
||||
'pointHitRadius',
|
||||
'pointHoverBackgroundColor',
|
||||
'pointHoverBorderColor',
|
||||
'pointHoverBorderWidth',
|
||||
'pointHoverRadius',
|
||||
'backgroundColor',
|
||||
'borderColor',
|
||||
'borderSkipped',
|
||||
'borderWidth',
|
||||
'hoverBackgroundColor',
|
||||
'hoverBorderColor',
|
||||
'hoverBorderWidth',
|
||||
'hoverRadius',
|
||||
'hitRadius',
|
||||
'radius',
|
||||
'rotation'
|
||||
];
|
||||
|
||||
function clean(scale) {
|
||||
const {chart, id, max} = scale;
|
||||
const duration = resolveOption(scale, 'duration');
|
||||
const delay = resolveOption(scale, 'delay');
|
||||
const ttl = resolveOption(scale, 'ttl');
|
||||
const pause = resolveOption(scale, 'pause');
|
||||
const min = Date.now() - (isNaN(ttl) ? duration + delay : ttl);
|
||||
let i, start, count, removalRange;
|
||||
|
||||
// Remove old data
|
||||
each(chart.data.datasets, (dataset, datasetIndex) => {
|
||||
const meta = chart.getDatasetMeta(datasetIndex);
|
||||
const axis = id === meta.xAxisID && 'x' || id === meta.yAxisID && 'y';
|
||||
|
||||
if (axis) {
|
||||
const controller = meta.controller;
|
||||
const data = dataset.data;
|
||||
const length = data.length;
|
||||
|
||||
if (pause) {
|
||||
// If the scale is paused, preserve the visible data points
|
||||
for (i = 0; i < length; ++i) {
|
||||
const point = controller.getParsed(i);
|
||||
if (point && !(point[axis] < max)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
start = i + 2;
|
||||
} else {
|
||||
start = 0;
|
||||
}
|
||||
|
||||
for (i = start; i < length; ++i) {
|
||||
const point = controller.getParsed(i);
|
||||
if (!point || !(point[axis] <= min)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
count = i - start;
|
||||
if (isNaN(ttl)) {
|
||||
// Keep the last two data points outside the range not to affect the existing bezier curve
|
||||
count = Math.max(count - 2, 0);
|
||||
}
|
||||
|
||||
data.splice(start, count);
|
||||
each(datasetPropertyKeys, key => {
|
||||
if (isArray(dataset[key])) {
|
||||
dataset[key].splice(start, count);
|
||||
}
|
||||
});
|
||||
each(dataset.datalabels, value => {
|
||||
if (isArray(value)) {
|
||||
value.splice(start, count);
|
||||
}
|
||||
});
|
||||
if (typeof data[0] !== 'object') {
|
||||
removalRange = {
|
||||
start: start,
|
||||
count: count
|
||||
};
|
||||
}
|
||||
|
||||
each(chart._active, (item, index) => {
|
||||
if (item.datasetIndex === datasetIndex && item.index >= start) {
|
||||
if (item.index >= start + count) {
|
||||
item.index -= count;
|
||||
} else {
|
||||
chart._active.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}, null, true);
|
||||
}
|
||||
});
|
||||
if (removalRange) {
|
||||
chart.data.labels.splice(removalRange.start, removalRange.count);
|
||||
}
|
||||
}
|
||||
|
||||
function transition(element, id, translate) {
|
||||
const animations = element.$animations || {};
|
||||
|
||||
each(element.$streaming, (item, key) => {
|
||||
if (item.axisId === id) {
|
||||
const delta = item.reverse ? -translate : translate;
|
||||
const animation = animations[key];
|
||||
|
||||
if (isFinite(element[key])) {
|
||||
element[key] -= delta;
|
||||
}
|
||||
if (animation) {
|
||||
animation._from -= delta;
|
||||
animation._to -= delta;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function scroll(scale) {
|
||||
const {chart, id, $realtime: realtime} = scale;
|
||||
const duration = resolveOption(scale, 'duration');
|
||||
const delay = resolveOption(scale, 'delay');
|
||||
const isHorizontal = scale.isHorizontal();
|
||||
const length = isHorizontal ? scale.width : scale.height;
|
||||
const now = Date.now();
|
||||
const tooltip = chart.tooltip;
|
||||
const annotations = getElements(chart);
|
||||
let offset = length * (now - realtime.head) / duration;
|
||||
|
||||
if (isHorizontal === !!scale.options.reverse) {
|
||||
offset = -offset;
|
||||
}
|
||||
|
||||
// Shift all the elements leftward or downward
|
||||
each(chart.data.datasets, (dataset, datasetIndex) => {
|
||||
const meta = chart.getDatasetMeta(datasetIndex);
|
||||
const {data: elements = [], dataset: element} = meta;
|
||||
|
||||
for (let i = 0, ilen = elements.length; i < ilen; ++i) {
|
||||
transition(elements[i], id, offset);
|
||||
}
|
||||
if (element) {
|
||||
transition(element, id, offset);
|
||||
delete element._path;
|
||||
}
|
||||
});
|
||||
|
||||
// Shift all the annotation elements leftward or downward
|
||||
for (let i = 0, ilen = annotations.length; i < ilen; ++i) {
|
||||
transition(annotations[i], id, offset);
|
||||
}
|
||||
|
||||
// Shift tooltip leftward or downward
|
||||
if (tooltip) {
|
||||
transition(tooltip, id, offset);
|
||||
}
|
||||
|
||||
scale.max = now - delay;
|
||||
scale.min = scale.max - duration;
|
||||
|
||||
realtime.head = now;
|
||||
}
|
||||
|
||||
export default class RealTimeScale extends TimeScale {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.$realtime = this.$realtime || {};
|
||||
}
|
||||
|
||||
init(scaleOpts, opts) {
|
||||
const me = this;
|
||||
|
||||
super.init(scaleOpts, opts);
|
||||
startDataRefreshTimer(me.$realtime, () => {
|
||||
const chart = me.chart;
|
||||
const onRefresh = resolveOption(me, 'onRefresh');
|
||||
|
||||
call(onRefresh, [chart], me);
|
||||
clean(me);
|
||||
chart.update('quiet');
|
||||
return resolveOption(me, 'refresh');
|
||||
});
|
||||
}
|
||||
|
||||
update(maxWidth, maxHeight, margins) {
|
||||
const me = this;
|
||||
const {$realtime: realtime, options} = me;
|
||||
const {bounds, offset, ticks: ticksOpts} = options;
|
||||
const {autoSkip, source, major: majorTicksOpts} = ticksOpts;
|
||||
const majorEnabled = majorTicksOpts.enabled;
|
||||
|
||||
if (resolveOption(me, 'pause')) {
|
||||
stopFrameRefreshTimer(realtime);
|
||||
} else {
|
||||
if (!realtime.frameRequestID) {
|
||||
realtime.head = Date.now();
|
||||
}
|
||||
startFrameRefreshTimer(realtime, () => {
|
||||
const chart = me.chart;
|
||||
const streaming = chart.$streaming;
|
||||
|
||||
scroll(me);
|
||||
if (streaming) {
|
||||
call(streaming.render, [chart]);
|
||||
}
|
||||
return resolveOption(me, 'frameRate');
|
||||
});
|
||||
}
|
||||
|
||||
options.bounds = undefined;
|
||||
options.offset = false;
|
||||
ticksOpts.autoSkip = false;
|
||||
ticksOpts.source = source === 'auto' ? '' : source;
|
||||
majorTicksOpts.enabled = true;
|
||||
|
||||
super.update(maxWidth, maxHeight, margins);
|
||||
|
||||
options.bounds = bounds;
|
||||
options.offset = offset;
|
||||
ticksOpts.autoSkip = autoSkip;
|
||||
ticksOpts.source = source;
|
||||
majorTicksOpts.enabled = majorEnabled;
|
||||
}
|
||||
|
||||
buildTicks() {
|
||||
const me = this;
|
||||
const duration = resolveOption(me, 'duration');
|
||||
const delay = resolveOption(me, 'delay');
|
||||
const max = me.$realtime.head - delay;
|
||||
const min = max - duration;
|
||||
const maxArray = [1e15, max];
|
||||
const minArray = [-1e15, min];
|
||||
|
||||
Object.defineProperty(me, 'min', {
|
||||
get: () => minArray.shift(),
|
||||
set: noop
|
||||
});
|
||||
Object.defineProperty(me, 'max', {
|
||||
get: () => maxArray.shift(),
|
||||
set: noop
|
||||
});
|
||||
|
||||
const ticks = super.buildTicks();
|
||||
|
||||
delete me.min;
|
||||
delete me.max;
|
||||
me.min = min;
|
||||
me.max = max;
|
||||
|
||||
return ticks;
|
||||
}
|
||||
|
||||
calculateLabelRotation() {
|
||||
const ticksOpts = this.options.ticks;
|
||||
const maxRotation = ticksOpts.maxRotation;
|
||||
|
||||
ticksOpts.maxRotation = ticksOpts.minRotation || 0;
|
||||
super.calculateLabelRotation();
|
||||
ticksOpts.maxRotation = maxRotation;
|
||||
}
|
||||
|
||||
fit() {
|
||||
const me = this;
|
||||
const options = me.options;
|
||||
|
||||
super.fit();
|
||||
|
||||
if (options.ticks.display && options.display && me.isHorizontal()) {
|
||||
me.paddingLeft = 3;
|
||||
me.paddingRight = 3;
|
||||
me._handleMargins();
|
||||
}
|
||||
}
|
||||
|
||||
draw(chartArea) {
|
||||
const me = this;
|
||||
const {chart, ctx} = me;
|
||||
const area = me.isHorizontal() ?
|
||||
{
|
||||
left: chartArea.left,
|
||||
top: 0,
|
||||
right: chartArea.right,
|
||||
bottom: chart.height
|
||||
} : {
|
||||
left: 0,
|
||||
top: chartArea.top,
|
||||
right: chart.width,
|
||||
bottom: chartArea.bottom
|
||||
};
|
||||
|
||||
me._gridLineItems = null;
|
||||
me._labelItems = null;
|
||||
|
||||
// Clip and draw the scale
|
||||
clipArea(ctx, area);
|
||||
super.draw(chartArea);
|
||||
unclipArea(ctx);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
const realtime = this.$realtime;
|
||||
|
||||
stopFrameRefreshTimer(realtime);
|
||||
stopDataRefreshTimer(realtime);
|
||||
}
|
||||
|
||||
_generate() {
|
||||
const me = this;
|
||||
const adapter = me._adapter;
|
||||
const duration = resolveOption(me, 'duration');
|
||||
const delay = resolveOption(me, 'delay');
|
||||
const refresh = resolveOption(me, 'refresh');
|
||||
const max = me.$realtime.head - delay;
|
||||
const min = max - duration;
|
||||
const capacity = me._getLabelCapacity(min);
|
||||
const {time: timeOpts, ticks: ticksOpts} = me.options;
|
||||
const minor = timeOpts.unit || determineUnitForAutoTicks(timeOpts.minUnit, min, max, capacity);
|
||||
const major = determineMajorUnit(minor);
|
||||
const stepSize = timeOpts.stepSize || determineStepSize(min, max, minor, capacity);
|
||||
const weekday = minor === 'week' ? timeOpts.isoWeekday : false;
|
||||
const majorTicksEnabled = ticksOpts.major.enabled;
|
||||
const hasWeekday = isNumber(weekday) || weekday === true;
|
||||
const interval = INTERVALS[minor];
|
||||
const ticks = {};
|
||||
let first = min;
|
||||
let time, count;
|
||||
|
||||
// For 'week' unit, handle the first day of week option
|
||||
if (hasWeekday) {
|
||||
first = +adapter.startOf(first, 'isoWeek', weekday);
|
||||
}
|
||||
|
||||
// Align first ticks on unit
|
||||
first = +adapter.startOf(first, hasWeekday ? 'day' : minor);
|
||||
|
||||
// Prevent browser from freezing in case user options request millions of milliseconds
|
||||
if (adapter.diff(max, min, minor) > 100000 * stepSize) {
|
||||
throw new Error(min + ' and ' + max + ' are too far apart with stepSize of ' + stepSize + ' ' + minor);
|
||||
}
|
||||
|
||||
time = first;
|
||||
|
||||
if (majorTicksEnabled && major && !hasWeekday && !timeOpts.round) {
|
||||
// Align the first tick on the previous `minor` unit aligned on the `major` unit:
|
||||
// we first aligned time on the previous `major` unit then add the number of full
|
||||
// stepSize there is between first and the previous major time.
|
||||
time = +adapter.startOf(time, major);
|
||||
time = +adapter.add(time, ~~((first - time) / (interval.size * stepSize)) * stepSize, minor);
|
||||
}
|
||||
|
||||
const timestamps = ticksOpts.source === 'data' && me.getDataTimestamps();
|
||||
for (count = 0; time < max + refresh; time = +adapter.add(time, stepSize, minor), count++) {
|
||||
addTick(ticks, time, timestamps);
|
||||
}
|
||||
|
||||
if (time === max + refresh || count === 1) {
|
||||
addTick(ticks, time, timestamps);
|
||||
}
|
||||
|
||||
return Object.keys(ticks).sort((a, b) => a - b).map(x => +x);
|
||||
}
|
||||
}
|
||||
|
||||
RealTimeScale.id = 'realtime';
|
||||
|
||||
RealTimeScale.defaults = {
|
||||
bounds: 'data',
|
||||
adapters: {},
|
||||
time: {
|
||||
parser: false, // false == a pattern string from or a custom callback that converts its argument to a timestamp
|
||||
unit: false, // false == automatic or override with week, month, year, etc.
|
||||
round: false, // none, or override with week, month, year, etc.
|
||||
isoWeekday: false, // override week start day - see http://momentjs.com/docs/#/get-set/iso-weekday/
|
||||
minUnit: 'millisecond',
|
||||
displayFormats: {}
|
||||
},
|
||||
realtime: {},
|
||||
ticks: {
|
||||
autoSkip: false,
|
||||
source: 'auto',
|
||||
major: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
defaults.describe('scale.realtime', {
|
||||
_scriptable: name => name !== 'onRefresh'
|
||||
});
|
|
@ -1,541 +0,0 @@
|
|||
/**
|
||||
* 🧩 Dynamic JSON Config Editor
|
||||
* Construye formularios en base a JSON Schema y llama a endpoints /api/config
|
||||
*/
|
||||
|
||||
(function () {
|
||||
let schemasIndex = [];
|
||||
let currentSchemaId = null;
|
||||
let currentData = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const tabBtn = document.querySelector('.tab-btn[data-tab="config-editor"]');
|
||||
if (!tabBtn) return;
|
||||
|
||||
// Cargar esquemas cuando se entra al tab
|
||||
tabBtn.addEventListener('click', ensureSchemasLoadedOnce);
|
||||
|
||||
// Listeners de controles básicos
|
||||
const saveBtn = document.getElementById('btn-save-config');
|
||||
if (saveBtn) saveBtn.addEventListener('click', onSave);
|
||||
|
||||
const exportBtn = document.getElementById('btn-export-config');
|
||||
if (exportBtn) exportBtn.addEventListener('click', onExport);
|
||||
|
||||
const importInput = document.getElementById('import-file');
|
||||
if (importInput) importInput.addEventListener('change', onImport);
|
||||
});
|
||||
|
||||
async function ensureSchemasLoadedOnce() {
|
||||
if (schemasIndex.length > 0) return;
|
||||
try {
|
||||
const res = await fetch('/api/config/schemas');
|
||||
const data = await res.json();
|
||||
if (!data.success) throw new Error(data.error || 'Failed to list schemas');
|
||||
schemasIndex = data.schemas || [];
|
||||
populateSchemaSelector();
|
||||
} catch (e) {
|
||||
showMessage(`Error loading schemas: ${e}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function populateSchemaSelector() {
|
||||
const selector = document.getElementById('schema-selector');
|
||||
if (!selector) return;
|
||||
selector.innerHTML = '';
|
||||
|
||||
for (const s of schemasIndex) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = s.id;
|
||||
opt.textContent = `${iconForSchema(s.id)} ${s.title || s.id}`;
|
||||
selector.appendChild(opt);
|
||||
}
|
||||
|
||||
selector.addEventListener('change', () => loadConfigAndSchema(selector.value));
|
||||
if (schemasIndex.length > 0) {
|
||||
selector.value = schemasIndex[0].id;
|
||||
loadConfigAndSchema(selector.value);
|
||||
}
|
||||
}
|
||||
|
||||
function iconForSchema(id) {
|
||||
if (id === 'plc') return '⚙️';
|
||||
if (id === 'datasets') return '📊';
|
||||
if (id === 'plots') return '📈';
|
||||
return '🧩';
|
||||
}
|
||||
|
||||
async function loadConfigAndSchema(schemaId) {
|
||||
currentSchemaId = schemaId;
|
||||
const container = document.getElementById('config-form-container');
|
||||
if (container) container.innerHTML = 'Loading...';
|
||||
|
||||
try {
|
||||
const [schemaRes, dataRes] = await Promise.all([
|
||||
fetch(`/api/config/schema/${schemaId}`),
|
||||
fetch(`/api/config/${schemaId}`)
|
||||
]);
|
||||
|
||||
const schemaData = await schemaRes.json();
|
||||
const configData = await dataRes.json();
|
||||
|
||||
if (!schemaData.success) throw new Error(schemaData.error || 'Schema error');
|
||||
if (!configData.success) throw new Error(configData.error || 'Data error');
|
||||
|
||||
currentData = configData.data;
|
||||
|
||||
if (container) container.innerHTML = '';
|
||||
|
||||
// Prefer JSONForm if available for a simple form UI
|
||||
if (window.$ && window._ && typeof $.fn.jsonForm === 'function') {
|
||||
const formEl = document.createElement('form');
|
||||
formEl.id = 'jsonform-form';
|
||||
container.appendChild(formEl);
|
||||
|
||||
const formDef = ["*", { "type": "submit", "title": "Save" }];
|
||||
|
||||
$(formEl).jsonForm({
|
||||
schema: schemaData.schema,
|
||||
form: formDef,
|
||||
value: currentData,
|
||||
onSubmit: function (errors, values) {
|
||||
if (errors) {
|
||||
showMessage('Validation errors in form', 'error');
|
||||
return false;
|
||||
}
|
||||
doSave(values);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
const saveBtn = document.getElementById('btn-save-config');
|
||||
if (saveBtn) {
|
||||
saveBtn.onclick = () => {
|
||||
const f = document.getElementById('jsonform-form');
|
||||
if (f) f.requestSubmit ? f.requestSubmit() : f.dispatchEvent(new Event('submit', { cancelable: true }));
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// Fallback: minimal manual renderer based on schema
|
||||
renderForm(container, schemaData.schema, currentData);
|
||||
}
|
||||
} catch (e) {
|
||||
if (container) container.innerHTML = '';
|
||||
showMessage(`Error loading editor: ${e}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Renderizado muy simple basado en schema: soporta object, string, number, integer, boolean, array básica
|
||||
function renderForm(container, schema, data) {
|
||||
if (!container) return;
|
||||
container.innerHTML = '';
|
||||
|
||||
const form = document.createElement('div');
|
||||
form.className = 'config-editor-form';
|
||||
|
||||
if (schema.type === 'object' && schema.properties) {
|
||||
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
||||
const value = data ? data[key] : undefined;
|
||||
const field = renderField(key, propSchema, value, [key]);
|
||||
if (field) form.appendChild(field);
|
||||
}
|
||||
} else {
|
||||
form.textContent = 'Unsupported schema root.';
|
||||
}
|
||||
|
||||
container.appendChild(form);
|
||||
}
|
||||
|
||||
function renderField(label, propSchema, value, path) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'form-group';
|
||||
|
||||
const title = document.createElement('label');
|
||||
title.textContent = propSchema.title || label;
|
||||
wrapper.appendChild(title);
|
||||
|
||||
// Optional description/help text
|
||||
if (propSchema.description) {
|
||||
const help = document.createElement('small');
|
||||
help.textContent = propSchema.description;
|
||||
help.style.display = 'block';
|
||||
help.style.color = 'var(--pico-muted-color)';
|
||||
help.style.marginTop = '-0.25rem';
|
||||
help.style.marginBottom = '0.25rem';
|
||||
wrapper.appendChild(help);
|
||||
}
|
||||
|
||||
const type = Array.isArray(propSchema.type) ? propSchema.type : [propSchema.type];
|
||||
|
||||
// Objetos
|
||||
if (type.includes('object')) {
|
||||
const inner = document.createElement('div');
|
||||
inner.className = 'object-group';
|
||||
|
||||
// Caso 1: propiedades conocidas
|
||||
if (propSchema.properties) {
|
||||
for (const [k, s] of Object.entries(propSchema.properties)) {
|
||||
const v = value ? value[k] : undefined;
|
||||
const child = renderField(k, s, v, path.concat(k));
|
||||
if (child) inner.appendChild(child);
|
||||
}
|
||||
wrapper.appendChild(inner);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
// Caso 2: additionalProperties -> colección dinámica (key -> objeto)
|
||||
if (propSchema.additionalProperties && typeof propSchema.additionalProperties === 'object') {
|
||||
const entries = (value && typeof value === 'object') ? Object.entries(value) : [];
|
||||
const list = document.createElement('div');
|
||||
list.className = 'dynamic-object-list';
|
||||
|
||||
function renderEntries() {
|
||||
list.innerHTML = '';
|
||||
for (const [entryKey, entryVal] of entries) {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'dynamic-object-row';
|
||||
|
||||
const keyInput = document.createElement('input');
|
||||
keyInput.type = 'text';
|
||||
keyInput.value = entryKey;
|
||||
keyInput.title = 'Key';
|
||||
|
||||
let currentKey = entryKey;
|
||||
keyInput.addEventListener('change', () => {
|
||||
const newKey = keyInput.value.trim();
|
||||
if (!newKey || newKey === currentKey) return;
|
||||
// Renombrar clave conservando valor
|
||||
const parentObj = getPathObject(path, true);
|
||||
if (parentObj[newKey] !== undefined) {
|
||||
showMessage('Key already exists', 'error');
|
||||
keyInput.value = currentKey;
|
||||
return;
|
||||
}
|
||||
parentObj[newKey] = parentObj[currentKey];
|
||||
delete parentObj[currentKey];
|
||||
currentKey = newKey;
|
||||
updateEntriesFromObject(parentObj);
|
||||
setPathValue(path, parentObj);
|
||||
renderEntries();
|
||||
});
|
||||
|
||||
const delBtn = document.createElement('button');
|
||||
delBtn.type = 'button';
|
||||
delBtn.className = 'secondary';
|
||||
delBtn.textContent = '🗑️';
|
||||
delBtn.addEventListener('click', () => {
|
||||
const parentObj = getPathObject(path, true);
|
||||
delete parentObj[currentKey];
|
||||
updateEntriesFromObject(parentObj);
|
||||
setPathValue(path, parentObj);
|
||||
renderEntries();
|
||||
});
|
||||
|
||||
const valueContainer = document.createElement('div');
|
||||
valueContainer.className = 'dynamic-object-value';
|
||||
const child = renderField(currentKey, propSchema.additionalProperties, entryVal, path.concat(currentKey));
|
||||
|
||||
row.appendChild(keyInput);
|
||||
row.appendChild(delBtn);
|
||||
if (child) valueContainer.appendChild(child);
|
||||
row.appendChild(valueContainer);
|
||||
list.appendChild(row);
|
||||
}
|
||||
}
|
||||
|
||||
function updateEntriesFromObject(parentObj) {
|
||||
const arr = Object.entries(parentObj);
|
||||
entries.length = 0;
|
||||
arr.forEach(e => entries.push(e));
|
||||
}
|
||||
|
||||
renderEntries();
|
||||
|
||||
const addWrap = document.createElement('div');
|
||||
const addBtn = document.createElement('button');
|
||||
addBtn.type = 'button';
|
||||
addBtn.className = 'outline';
|
||||
addBtn.textContent = '➕ Add';
|
||||
addBtn.addEventListener('click', () => {
|
||||
const key = prompt('Enter key name');
|
||||
if (!key) return;
|
||||
const parentObj = getPathObject(path, true);
|
||||
if (parentObj[key] !== undefined) {
|
||||
showMessage('Key already exists', 'error');
|
||||
return;
|
||||
}
|
||||
parentObj[key] = defaultForSchema(propSchema.additionalProperties);
|
||||
setPathValue(path, parentObj);
|
||||
updateEntriesFromObject(parentObj);
|
||||
renderEntries();
|
||||
});
|
||||
addWrap.appendChild(addBtn);
|
||||
|
||||
inner.appendChild(list);
|
||||
inner.appendChild(addWrap);
|
||||
wrapper.appendChild(inner);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
// Objeto sin propiedades definidas
|
||||
const note = document.createElement('div');
|
||||
note.textContent = '(object)';
|
||||
wrapper.appendChild(note);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
// Boolean con toggle opcional
|
||||
if (type.includes('boolean')) {
|
||||
const toggle = document.createElement('button');
|
||||
toggle.type = 'button';
|
||||
const labels = propSchema?.['x-ui']?.toggleLabels || ['On', 'Off'];
|
||||
let current = !!value;
|
||||
toggle.textContent = current ? labels[0] : labels[1];
|
||||
toggle.className = current ? 'outline' : 'secondary';
|
||||
toggle.addEventListener('click', () => {
|
||||
current = !current;
|
||||
setPathValue(path, current);
|
||||
toggle.textContent = current ? labels[0] : labels[1];
|
||||
toggle.className = current ? 'outline' : 'secondary';
|
||||
});
|
||||
wrapper.appendChild(toggle);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
// Enum (select) o string simple
|
||||
if (propSchema.enum) {
|
||||
const select = document.createElement('select');
|
||||
for (const opt of propSchema.enum) {
|
||||
const o = document.createElement('option');
|
||||
o.value = opt;
|
||||
o.textContent = String(opt).toUpperCase();
|
||||
if (value === opt) o.selected = true;
|
||||
select.appendChild(o);
|
||||
}
|
||||
select.addEventListener('change', () => setPathValue(path, select.value));
|
||||
wrapper.appendChild(select);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
if (type.includes('number') || type.includes('integer')) {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'number';
|
||||
if (typeof value === 'number') input.value = String(value);
|
||||
if (typeof propSchema.minimum !== 'undefined') input.min = String(propSchema.minimum);
|
||||
if (typeof propSchema.maximum !== 'undefined') input.max = String(propSchema.maximum);
|
||||
if (propSchema.step) input.step = String(propSchema.step);
|
||||
input.addEventListener('input', () => {
|
||||
const v = input.value === '' ? null : (type.includes('integer') ? parseInt(input.value) : parseFloat(input.value));
|
||||
setPathValue(path, v);
|
||||
});
|
||||
wrapper.appendChild(input);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
if (type.includes('array')) {
|
||||
const arrWrap = document.createElement('div');
|
||||
arrWrap.className = 'array-group';
|
||||
|
||||
const list = document.createElement('div');
|
||||
const arr = Array.isArray(value) ? value : [];
|
||||
|
||||
function renderItems() {
|
||||
list.innerHTML = '';
|
||||
arr.forEach((itemVal, idx) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'array-item-row';
|
||||
// Soporta items string simples por ahora
|
||||
const itemInput = document.createElement('input');
|
||||
itemInput.type = 'text';
|
||||
itemInput.value = itemVal;
|
||||
itemInput.addEventListener('input', () => {
|
||||
arr[idx] = itemInput.value;
|
||||
setPathValue(path, arr.slice());
|
||||
});
|
||||
const delBtn = document.createElement('button');
|
||||
delBtn.type = 'button';
|
||||
delBtn.className = 'secondary';
|
||||
delBtn.textContent = '🗑️';
|
||||
delBtn.addEventListener('click', () => {
|
||||
arr.splice(idx, 1);
|
||||
setPathValue(path, arr.slice());
|
||||
renderItems();
|
||||
});
|
||||
row.appendChild(itemInput);
|
||||
row.appendChild(delBtn);
|
||||
list.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
renderItems();
|
||||
|
||||
const addBtn = document.createElement('button');
|
||||
addBtn.type = 'button';
|
||||
addBtn.className = 'outline';
|
||||
addBtn.textContent = '➕ Add';
|
||||
addBtn.addEventListener('click', () => {
|
||||
arr.push('');
|
||||
setPathValue(path, arr.slice());
|
||||
renderItems();
|
||||
});
|
||||
|
||||
arrWrap.appendChild(list);
|
||||
arrWrap.appendChild(addBtn);
|
||||
wrapper.appendChild(arrWrap);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
// Fallback: string
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.value = value ?? '';
|
||||
input.placeholder = propSchema.placeholder || '';
|
||||
input.addEventListener('input', () => setPathValue(path, input.value));
|
||||
wrapper.appendChild(input);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
function setPathValue(path, v) {
|
||||
if (!currentData) currentData = {};
|
||||
let cursor = currentData;
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
const key = path[i];
|
||||
if (!cursor[key] || typeof cursor[key] !== 'object') cursor[key] = {};
|
||||
cursor = cursor[key];
|
||||
}
|
||||
cursor[path[path.length - 1]] = v;
|
||||
}
|
||||
|
||||
function getPathObject(path, createIfMissing = false) {
|
||||
if (!currentData) currentData = {};
|
||||
let cursor = currentData;
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
const key = path[i];
|
||||
if (i === path.length - 1) {
|
||||
if (typeof cursor[key] !== 'object' || cursor[key] === null) {
|
||||
if (createIfMissing) cursor[key] = {};
|
||||
else return {};
|
||||
}
|
||||
return cursor[key];
|
||||
}
|
||||
if (!cursor[key] || typeof cursor[key] !== 'object') {
|
||||
if (createIfMissing) cursor[key] = {};
|
||||
else return {};
|
||||
}
|
||||
cursor = cursor[key];
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function defaultForSchema(schema) {
|
||||
const t = Array.isArray(schema.type) ? schema.type[0] : schema.type;
|
||||
if (t === 'object') {
|
||||
const obj = {};
|
||||
if (schema.properties) {
|
||||
for (const [k, s] of Object.entries(schema.properties)) {
|
||||
if (typeof s.default !== 'undefined') obj[k] = s.default;
|
||||
else obj[k] = defaultForSchema(s);
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
if (t === 'array') return [];
|
||||
if (t === 'boolean') return !!schema.default;
|
||||
if (t === 'number' || t === 'integer') return typeof schema.default !== 'undefined' ? schema.default : 0;
|
||||
if (t === 'string') return schema.default || '';
|
||||
return null;
|
||||
}
|
||||
|
||||
async function doSave(payload) {
|
||||
if (!currentSchemaId) return;
|
||||
try {
|
||||
const res = await fetch(`/api/config/${currentSchemaId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const result = await res.json();
|
||||
if (result.success) showMessage('Configuration saved successfully', 'success');
|
||||
else showMessage(result.error || 'Failed to save configuration', 'error');
|
||||
} catch (e) {
|
||||
showMessage(`Error saving configuration: ${e}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
if (!currentSchemaId) return;
|
||||
try {
|
||||
// If JSONForm exists, its Save is already wired; fall back to currentData
|
||||
let payload = currentData || {};
|
||||
if (window.__jsonEditorInstance && typeof window.__jsonEditorInstance.get === 'function') {
|
||||
// Legacy safety; should not happen because we no longer render JSONEditor
|
||||
payload = window.__jsonEditorInstance.get();
|
||||
}
|
||||
await doSave(payload);
|
||||
} catch (e) {
|
||||
showMessage(`Error saving configuration: ${e}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function onExport() {
|
||||
if (!currentSchemaId) return;
|
||||
try {
|
||||
let val = currentData || {};
|
||||
// Prefer currentData; JSONForm updates are applied on submit
|
||||
if (window.__jsonEditorInstance && typeof window.__jsonEditorInstance.get === 'function') {
|
||||
val = window.__jsonEditorInstance.get();
|
||||
}
|
||||
const blob = new Blob([JSON.stringify(val, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${currentSchemaId}_export.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}, 0);
|
||||
} catch (e) {
|
||||
showMessage(`Error exporting configuration: ${e}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function onImport(evt) {
|
||||
const file = evt.target.files && evt.target.files[0];
|
||||
if (!file || !currentSchemaId) return;
|
||||
try {
|
||||
const text = await file.text();
|
||||
const json = JSON.parse(text);
|
||||
currentData = json;
|
||||
const formEl = document.getElementById('jsonform-form');
|
||||
if (formEl && window.$ && typeof $(formEl).jsonForm === 'function') {
|
||||
const schemaRes = await fetch(`/api/config/schema/${currentSchemaId}`);
|
||||
const schemaData = await schemaRes.json();
|
||||
if (!schemaData.success) throw new Error(schemaData.error || 'Schema error');
|
||||
$(formEl).jsonForm({
|
||||
schema: schemaData.schema,
|
||||
form: ["*", { "type": "submit", "title": "Save" }],
|
||||
value: currentData,
|
||||
onSubmit: function (errors, values) {
|
||||
if (errors) return showMessage('Validation errors in form', 'error');
|
||||
doSave(values);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const res = await fetch(`/api/config/schema/${currentSchemaId}`);
|
||||
const schemaData = await res.json();
|
||||
if (!schemaData.success) throw new Error(schemaData.error || 'Schema error');
|
||||
renderForm(document.getElementById('config-form-container'), schemaData.schema, currentData);
|
||||
}
|
||||
showMessage('JSON imported (not saved yet)', 'info');
|
||||
} catch (e) {
|
||||
showMessage(`Invalid JSON: ${e}`, 'error');
|
||||
} finally {
|
||||
evt.target.value = '';
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
|
170
static/js/csv.js
170
static/js/csv.js
|
@ -1,170 +0,0 @@
|
|||
/**
|
||||
* Gestión de la configuración CSV y operaciones relacionadas
|
||||
*/
|
||||
|
||||
// Cargar configuración CSV
|
||||
function loadCsvConfig() {
|
||||
fetch('/api/csv/config')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const config = data.config;
|
||||
|
||||
// Actualizar elementos de visualización
|
||||
document.getElementById('csv-directory-path').textContent = config.current_directory || 'N/A';
|
||||
document.getElementById('csv-rotation-enabled').textContent = config.rotation_enabled ? '✅ Yes' : '❌ No';
|
||||
document.getElementById('csv-max-size').textContent = config.max_size_mb ? `${config.max_size_mb} MB` : 'No limit';
|
||||
document.getElementById('csv-max-days').textContent = config.max_days ? `${config.max_days} days` : 'No limit';
|
||||
document.getElementById('csv-max-hours').textContent = config.max_hours ? `${config.max_hours} hours` : 'No limit';
|
||||
document.getElementById('csv-cleanup-interval').textContent = `${config.cleanup_interval_hours} hours`;
|
||||
|
||||
// Actualizar campos del formulario
|
||||
document.getElementById('records-directory').value = config.records_directory || '';
|
||||
document.getElementById('rotation-enabled').checked = config.rotation_enabled || false;
|
||||
document.getElementById('max-size-mb').value = config.max_size_mb || '';
|
||||
document.getElementById('max-days').value = config.max_days || '';
|
||||
document.getElementById('max-hours').value = config.max_hours || '';
|
||||
document.getElementById('cleanup-interval').value = config.cleanup_interval_hours || 24;
|
||||
|
||||
// Cargar información del directorio
|
||||
loadCsvDirectoryInfo();
|
||||
} else {
|
||||
showMessage('Error loading CSV configuration: ' + data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showMessage('Error loading CSV configuration', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Cargar información del directorio CSV
|
||||
function loadCsvDirectoryInfo() {
|
||||
fetch('/api/csv/directory/info')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const info = data.info;
|
||||
const statsDiv = document.getElementById('directory-stats');
|
||||
|
||||
let html = `
|
||||
<div class="stat-item">
|
||||
<strong>📁 Directory:</strong>
|
||||
<span>${info.base_directory}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<strong>📊 Total Files:</strong>
|
||||
<span>${info.total_files}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<strong>💾 Total Size:</strong>
|
||||
<span>${info.total_size_mb} MB</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (info.oldest_file) {
|
||||
html += `
|
||||
<div class="stat-item">
|
||||
<strong>📅 Oldest File:</strong>
|
||||
<span>${new Date(info.oldest_file).toLocaleString()}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (info.newest_file) {
|
||||
html += `
|
||||
<div class="stat-item">
|
||||
<strong>🆕 Newest File:</strong>
|
||||
<span>${new Date(info.newest_file).toLocaleString()}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (info.day_folders && info.day_folders.length > 0) {
|
||||
html += '<h4>📂 Day Folders:</h4>';
|
||||
info.day_folders.forEach(folder => {
|
||||
html += `
|
||||
<div class="day-folder-item">
|
||||
<span><strong>${folder.name}</strong></span>
|
||||
<span>${folder.files} files, ${folder.size_mb} MB</span>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
statsDiv.innerHTML = html;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
document.getElementById('directory-stats').innerHTML = '<p>Error loading directory information</p>';
|
||||
});
|
||||
}
|
||||
|
||||
// Ejecutar limpieza manual
|
||||
function triggerManualCleanup() {
|
||||
if (!confirm('¿Estás seguro de que quieres ejecutar la limpieza manual? Esto eliminará archivos antiguos según la configuración actual.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('/api/csv/cleanup', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showMessage('Limpieza ejecutada correctamente', 'success');
|
||||
loadCsvDirectoryInfo(); // Recargar información del directorio
|
||||
} else {
|
||||
showMessage('Error en la limpieza: ' + data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showMessage('Error ejecutando la limpieza', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Inicializar listeners para la configuración CSV
|
||||
function initCsvListeners() {
|
||||
// Manejar envío del formulario de configuración CSV
|
||||
document.getElementById('csv-config-form').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(e.target);
|
||||
const configData = {};
|
||||
|
||||
// Convertir datos del formulario a objeto, manejando valores vacíos
|
||||
for (let [key, value] of formData.entries()) {
|
||||
if (key === 'rotation_enabled') {
|
||||
configData[key] = document.getElementById('rotation-enabled').checked;
|
||||
} else if (value.trim() === '') {
|
||||
configData[key] = null;
|
||||
} else if (key.includes('max_') || key.includes('cleanup_interval')) {
|
||||
configData[key] = parseFloat(value) || null;
|
||||
} else {
|
||||
configData[key] = value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
fetch('/api/csv/config', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(configData)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showMessage('Configuración CSV actualizada correctamente', 'success');
|
||||
loadCsvConfig(); // Recargar para mostrar valores actualizados
|
||||
} else {
|
||||
showMessage('Error actualizando configuración CSV: ' + data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showMessage('Error actualizando configuración CSV', 'error');
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,519 +0,0 @@
|
|||
/**
|
||||
* Gestión de datasets y variables asociadas
|
||||
*/
|
||||
|
||||
// Variables de gestión de datasets
|
||||
let currentDatasets = {};
|
||||
let currentDatasetId = null;
|
||||
|
||||
// Cargar todos los datasets desde API
|
||||
window.loadDatasets = function () {
|
||||
fetch('/api/datasets')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
currentDatasets = data.datasets;
|
||||
currentDatasetId = data.current_dataset_id;
|
||||
updateDatasetSelector();
|
||||
updateDatasetInfo();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading datasets:', error);
|
||||
showMessage('Error loading datasets', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Actualizar el selector de datasets
|
||||
function updateDatasetSelector() {
|
||||
const selector = document.getElementById('dataset-selector');
|
||||
selector.innerHTML = '<option value="">Select a dataset...</option>';
|
||||
|
||||
Object.keys(currentDatasets).forEach(datasetId => {
|
||||
const dataset = currentDatasets[datasetId];
|
||||
const option = document.createElement('option');
|
||||
option.value = datasetId;
|
||||
option.textContent = `${dataset.name} (${dataset.prefix})`;
|
||||
if (datasetId === currentDatasetId) {
|
||||
option.selected = true;
|
||||
}
|
||||
selector.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
// Actualizar información del dataset
|
||||
function updateDatasetInfo() {
|
||||
const statusBar = document.getElementById('dataset-status-bar');
|
||||
const variablesManagement = document.getElementById('variables-management');
|
||||
const noDatasetMessage = document.getElementById('no-dataset-message');
|
||||
|
||||
if (currentDatasetId && currentDatasets[currentDatasetId]) {
|
||||
const dataset = currentDatasets[currentDatasetId];
|
||||
|
||||
// Mostrar info del dataset en la barra de estado
|
||||
document.getElementById('dataset-name').textContent = dataset.name;
|
||||
document.getElementById('dataset-prefix').textContent = dataset.prefix;
|
||||
document.getElementById('dataset-sampling').textContent =
|
||||
dataset.sampling_interval ? `${dataset.sampling_interval}s` : 'Global interval';
|
||||
document.getElementById('dataset-var-count').textContent = Object.keys(dataset.variables).length;
|
||||
document.getElementById('dataset-stream-count').textContent = dataset.streaming_variables.length;
|
||||
|
||||
// Actualizar estado del dataset
|
||||
const statusSpan = document.getElementById('dataset-status');
|
||||
const isActive = dataset.enabled;
|
||||
statusSpan.textContent = isActive ? '🟢 Active' : '⭕ Inactive';
|
||||
statusSpan.className = `status-item ${isActive ? 'status-active' : 'status-inactive'}`;
|
||||
|
||||
// Actualizar botones de acción
|
||||
document.getElementById('activate-dataset-btn').style.display = isActive ? 'none' : 'inline-block';
|
||||
document.getElementById('deactivate-dataset-btn').style.display = isActive ? 'inline-block' : 'none';
|
||||
|
||||
// Mostrar secciones
|
||||
statusBar.style.display = 'block';
|
||||
variablesManagement.style.display = 'block';
|
||||
noDatasetMessage.style.display = 'none';
|
||||
|
||||
// Cargar variables para este dataset
|
||||
loadDatasetVariables(currentDatasetId);
|
||||
} else {
|
||||
statusBar.style.display = 'none';
|
||||
variablesManagement.style.display = 'none';
|
||||
noDatasetMessage.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// Cargar variables para un dataset específico
|
||||
function loadDatasetVariables(datasetId) {
|
||||
if (!datasetId || !currentDatasets[datasetId]) {
|
||||
// Limpiar la tabla si no hay dataset válido
|
||||
document.getElementById('variables-tbody').innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const dataset = currentDatasets[datasetId];
|
||||
const variables = dataset.variables || {};
|
||||
const streamingVars = dataset.streaming_variables || [];
|
||||
const tbody = document.getElementById('variables-tbody');
|
||||
|
||||
// Limpiar filas existentes
|
||||
tbody.innerHTML = '';
|
||||
|
||||
// Añadir una fila para cada variable
|
||||
Object.keys(variables).forEach(varName => {
|
||||
const variable = variables[varName];
|
||||
const row = document.createElement('tr');
|
||||
|
||||
// Formatear visualización del área de memoria
|
||||
let memoryAreaDisplay = '';
|
||||
if (variable.area === 'db') {
|
||||
memoryAreaDisplay = `DB${variable.db || 'N/A'}.${variable.offset}`;
|
||||
} else if (variable.area === 'mw' || variable.area === 'm') {
|
||||
memoryAreaDisplay = `MW${variable.offset}`;
|
||||
} else if (variable.area === 'pew' || variable.area === 'pe') {
|
||||
memoryAreaDisplay = `PEW${variable.offset}`;
|
||||
} else if (variable.area === 'paw' || variable.area === 'pa') {
|
||||
memoryAreaDisplay = `PAW${variable.offset}`;
|
||||
} else if (variable.area === 'e') {
|
||||
memoryAreaDisplay = `E${variable.offset}.${variable.bit}`;
|
||||
} else if (variable.area === 'a') {
|
||||
memoryAreaDisplay = `A${variable.offset}.${variable.bit}`;
|
||||
} else if (variable.area === 'mb') {
|
||||
memoryAreaDisplay = `M${variable.offset}.${variable.bit}`;
|
||||
} else {
|
||||
memoryAreaDisplay = `${variable.area.toUpperCase()}${variable.offset}`;
|
||||
}
|
||||
|
||||
// Comprobar si la variable está en la lista de streaming
|
||||
const isStreaming = streamingVars.includes(varName);
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${varName}</td>
|
||||
<td>${memoryAreaDisplay}</td>
|
||||
<td>${variable.offset}</td>
|
||||
<td>${variable.type.toUpperCase()}</td>
|
||||
<td id="value-${varName}" style="font-family: monospace; font-weight: bold; color: var(--pico-muted-color);">
|
||||
--
|
||||
</td>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" id="stream-${varName}" role="switch" ${isStreaming ? 'checked' : ''}
|
||||
onchange="toggleStreaming('${varName}', this.checked)">
|
||||
Enable
|
||||
</label>
|
||||
</td>
|
||||
<td>
|
||||
<button class="outline" onclick="editVariable('${varName}')">✏️ Edit</button>
|
||||
<button class="secondary" onclick="removeVariable('${varName}')">🗑️ Remove</button>
|
||||
</td>
|
||||
`;
|
||||
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
// Inicializar listeners de eventos para datasets
|
||||
function initDatasetListeners() {
|
||||
// Cambio de selector de dataset
|
||||
document.getElementById('dataset-selector').addEventListener('change', function () {
|
||||
const selectedDatasetId = this.value;
|
||||
if (selectedDatasetId) {
|
||||
// Detener streaming de variables actual si está activo
|
||||
if (isStreamingVariables) {
|
||||
stopVariableStreaming();
|
||||
}
|
||||
|
||||
// Establecer como dataset actual
|
||||
fetch('/api/datasets/current', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ dataset_id: selectedDatasetId })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
currentDatasetId = selectedDatasetId;
|
||||
// Recargar datasets para obtener datos frescos, luego actualizar info
|
||||
loadDatasets();
|
||||
|
||||
// Actualizar texto del botón de streaming
|
||||
const toggleBtn = document.getElementById('toggle-streaming-btn');
|
||||
if (toggleBtn) {
|
||||
toggleBtn.innerHTML = '▶️ Start Live Streaming';
|
||||
}
|
||||
|
||||
// Auto-refrescar valores para el nuevo dataset
|
||||
autoStartLiveDisplay();
|
||||
} else {
|
||||
showMessage(data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showMessage('Error setting current dataset', 'error');
|
||||
});
|
||||
} else {
|
||||
// Detener streaming de variables si está activo
|
||||
if (isStreamingVariables) {
|
||||
stopVariableStreaming();
|
||||
}
|
||||
|
||||
currentDatasetId = null;
|
||||
updateDatasetInfo();
|
||||
// Limpiar valores cuando no hay dataset seleccionado
|
||||
clearVariableValues();
|
||||
}
|
||||
});
|
||||
|
||||
// Botón de nuevo dataset
|
||||
document.getElementById('new-dataset-btn').addEventListener('click', function () {
|
||||
document.getElementById('dataset-modal').style.display = 'block';
|
||||
});
|
||||
|
||||
// Cerrar modal de dataset
|
||||
document.getElementById('close-dataset-modal').addEventListener('click', function () {
|
||||
document.getElementById('dataset-modal').style.display = 'none';
|
||||
});
|
||||
|
||||
document.getElementById('cancel-dataset-btn').addEventListener('click', function () {
|
||||
document.getElementById('dataset-modal').style.display = 'none';
|
||||
});
|
||||
|
||||
// Crear nuevo dataset
|
||||
document.getElementById('dataset-form').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const data = {
|
||||
dataset_id: document.getElementById('dataset-id').value.trim(),
|
||||
name: document.getElementById('dataset-name-input').value.trim(),
|
||||
prefix: document.getElementById('dataset-prefix-input').value.trim(),
|
||||
sampling_interval: document.getElementById('dataset-sampling-input').value || null
|
||||
};
|
||||
|
||||
if (data.sampling_interval) {
|
||||
data.sampling_interval = parseFloat(data.sampling_interval);
|
||||
}
|
||||
|
||||
fetch('/api/datasets', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showMessage(data.message, 'success');
|
||||
document.getElementById('dataset-modal').style.display = 'none';
|
||||
document.getElementById('dataset-form').reset();
|
||||
loadDatasets();
|
||||
} else {
|
||||
showMessage(data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showMessage('Error creating dataset', 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// Botón de eliminar dataset
|
||||
document.getElementById('delete-dataset-btn').addEventListener('click', function () {
|
||||
if (!currentDatasetId) {
|
||||
showMessage('No dataset selected', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const dataset = currentDatasets[currentDatasetId];
|
||||
if (confirm(`Are you sure you want to delete dataset "${dataset.name}"?`)) {
|
||||
fetch(`/api/datasets/${currentDatasetId}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showMessage(data.message, 'success');
|
||||
loadDatasets();
|
||||
} else {
|
||||
showMessage(data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showMessage('Error deleting dataset', 'error');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Botón de activar dataset
|
||||
document.getElementById('activate-dataset-btn').addEventListener('click', function () {
|
||||
if (!currentDatasetId) return;
|
||||
|
||||
fetch(`/api/datasets/${currentDatasetId}/activate`, {
|
||||
method: 'POST'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showMessage(data.message, 'success');
|
||||
loadDatasets();
|
||||
} else {
|
||||
showMessage(data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showMessage('Error activating dataset', 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// Botón de desactivar dataset
|
||||
document.getElementById('deactivate-dataset-btn').addEventListener('click', function () {
|
||||
if (!currentDatasetId) return;
|
||||
|
||||
fetch(`/api/datasets/${currentDatasetId}/deactivate`, {
|
||||
method: 'POST'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showMessage(data.message, 'success');
|
||||
loadDatasets();
|
||||
} else {
|
||||
showMessage(data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showMessage('Error deactivating dataset', 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// Formulario de variables
|
||||
document.getElementById('variable-form').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!currentDatasetId) {
|
||||
showMessage('No dataset selected. Please select a dataset first.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const area = document.getElementById('var-area').value;
|
||||
const data = {
|
||||
name: document.getElementById('var-name').value,
|
||||
area: area,
|
||||
db: area === 'db' ? parseInt(document.getElementById('var-db').value) : 1,
|
||||
offset: parseInt(document.getElementById('var-offset').value),
|
||||
type: document.getElementById('var-type').value,
|
||||
streaming: false // Default to not streaming
|
||||
};
|
||||
|
||||
// Añadir parámetro bit para áreas de bit
|
||||
if (area === 'e' || area === 'a' || area === 'mb') {
|
||||
data.bit = parseInt(document.getElementById('var-bit').value);
|
||||
}
|
||||
|
||||
fetch(`/api/datasets/${currentDatasetId}/variables`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
if (data.success) {
|
||||
document.getElementById('variable-form').reset();
|
||||
loadDatasets(); // Recargar para actualizar conteos
|
||||
updateStatus();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Eliminar variable del dataset actual
|
||||
function removeVariable(name) {
|
||||
if (!currentDatasetId) {
|
||||
showMessage('No dataset selected', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (confirm(`Are you sure you want to remove the variable "${name}" from this dataset?`)) {
|
||||
fetch(`/api/datasets/${currentDatasetId}/variables/${name}`, { method: 'DELETE' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
if (data.success) {
|
||||
loadDatasets(); // Recargar para actualizar conteos
|
||||
updateStatus();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Variables para edición de variables
|
||||
let currentEditingVariable = null;
|
||||
|
||||
// Editar variable
|
||||
function editVariable(name) {
|
||||
if (!currentDatasetId) {
|
||||
showMessage('No dataset selected', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
currentEditingVariable = name;
|
||||
|
||||
// Obtener datos de la variable del dataset actual
|
||||
const dataset = currentDatasets[currentDatasetId];
|
||||
if (dataset && dataset.variables && dataset.variables[name]) {
|
||||
const variable = dataset.variables[name];
|
||||
const streamingVars = dataset.streaming_variables || [];
|
||||
|
||||
// Crear objeto de variable con la misma estructura que la API
|
||||
const variableData = {
|
||||
name: name,
|
||||
area: variable.area,
|
||||
db: variable.db,
|
||||
offset: variable.offset,
|
||||
type: variable.type,
|
||||
bit: variable.bit,
|
||||
streaming: streamingVars.includes(name)
|
||||
};
|
||||
|
||||
populateEditForm(variableData);
|
||||
document.getElementById('edit-modal').style.display = 'block';
|
||||
} else {
|
||||
showMessage('Variable not found in current dataset', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Rellenar formulario de edición
|
||||
function populateEditForm(variable) {
|
||||
document.getElementById('edit-var-name').value = variable.name;
|
||||
document.getElementById('edit-var-area').value = variable.area;
|
||||
document.getElementById('edit-var-offset').value = variable.offset;
|
||||
document.getElementById('edit-var-type').value = variable.type;
|
||||
|
||||
if (variable.db) {
|
||||
document.getElementById('edit-var-db').value = variable.db;
|
||||
}
|
||||
|
||||
if (variable.bit !== undefined) {
|
||||
document.getElementById('edit-var-bit').value = variable.bit;
|
||||
}
|
||||
|
||||
// Actualizar visibilidad de campos según el área
|
||||
toggleEditFields();
|
||||
}
|
||||
|
||||
// Cerrar modal de edición
|
||||
function closeEditModal() {
|
||||
document.getElementById('edit-modal').style.display = 'none';
|
||||
currentEditingVariable = null;
|
||||
}
|
||||
|
||||
// Inicializar listeners para edición de variables
|
||||
function initVariableEditListeners() {
|
||||
// Manejar envío del formulario de edición
|
||||
document.getElementById('edit-variable-form').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!currentEditingVariable || !currentDatasetId) {
|
||||
showMessage('No variable or dataset selected for editing', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const area = document.getElementById('edit-var-area').value;
|
||||
const newName = document.getElementById('edit-var-name').value;
|
||||
|
||||
// Primero eliminar la variable antigua
|
||||
fetch(`/api/datasets/${currentDatasetId}/variables/${currentEditingVariable}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(deleteResult => {
|
||||
if (deleteResult.success) {
|
||||
// Luego añadir la variable actualizada
|
||||
const data = {
|
||||
name: newName,
|
||||
area: area,
|
||||
db: area === 'db' ? parseInt(document.getElementById('edit-var-db').value) : 1,
|
||||
offset: parseInt(document.getElementById('edit-var-offset').value),
|
||||
type: document.getElementById('edit-var-type').value,
|
||||
streaming: false // Se restaurará abajo si estaba habilitado
|
||||
};
|
||||
|
||||
// Añadir parámetro bit para áreas de bit
|
||||
if (area === 'e' || area === 'a' || area === 'mb') {
|
||||
data.bit = parseInt(document.getElementById('edit-var-bit').value);
|
||||
}
|
||||
|
||||
return fetch(`/api/datasets/${currentDatasetId}/variables`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
} else {
|
||||
throw new Error(deleteResult.message);
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showMessage('Variable updated successfully', 'success');
|
||||
closeEditModal();
|
||||
loadDatasets();
|
||||
updateStatus();
|
||||
} else {
|
||||
showMessage(data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showMessage(`Error updating variable: ${error}`, 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// Cerrar modal al hacer clic fuera de él
|
||||
window.onclick = function (event) {
|
||||
const editModal = document.getElementById('edit-modal');
|
||||
const datasetModal = document.getElementById('dataset-modal');
|
||||
if (event.target === editModal) {
|
||||
closeEditModal();
|
||||
}
|
||||
if (event.target === datasetModal) {
|
||||
datasetModal.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,185 +0,0 @@
|
|||
/**
|
||||
* Gestión de eventos de la aplicación y log de eventos
|
||||
*/
|
||||
|
||||
// Refrescar log de eventos
|
||||
function refreshEventLog() {
|
||||
const limitElement = document.getElementById('log-limit');
|
||||
const limit = limitElement ? limitElement.value : 100;
|
||||
|
||||
fetch(`/api/events?limit=${limit}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const logContainer = document.getElementById('events-container');
|
||||
const logStats = document.getElementById('events-count');
|
||||
|
||||
// Verificar que los elementos existan
|
||||
if (!logContainer) {
|
||||
console.warn('Events container not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Limpiar entradas existentes
|
||||
logContainer.innerHTML = '';
|
||||
|
||||
// Actualizar estadísticas
|
||||
if (logStats) {
|
||||
logStats.textContent = `${data.showing} of ${data.total_events}`;
|
||||
}
|
||||
|
||||
// Añadir eventos (orden inverso para mostrar primero los más nuevos)
|
||||
const events = data.events.reverse();
|
||||
|
||||
if (events.length === 0) {
|
||||
logContainer.innerHTML = `
|
||||
<div class="log-entry log-info">
|
||||
<div class="log-header">
|
||||
<span>📋 System</span>
|
||||
<span class="log-timestamp">${new Date().toLocaleString('es-ES')}</span>
|
||||
</div>
|
||||
<div class="log-message">No events found</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
events.forEach(event => {
|
||||
logContainer.appendChild(createLogEntry(event));
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-scroll al inicio para mostrar eventos más nuevos
|
||||
logContainer.scrollTop = 0;
|
||||
} else {
|
||||
console.error('Error loading events:', data.error);
|
||||
showMessage('Error loading events log', 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching events:', error);
|
||||
showMessage('Error fetching events log', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Crear entrada de log
|
||||
function createLogEntry(event) {
|
||||
const logEntry = document.createElement('div');
|
||||
logEntry.className = `log-entry log-${event.level}`;
|
||||
|
||||
const hasDetails = event.details && Object.keys(event.details).length > 0;
|
||||
|
||||
logEntry.innerHTML = `
|
||||
<div class="log-header">
|
||||
<span>${getEventIcon(event.event_type)} ${event.event_type.replace(/_/g, ' ').toUpperCase()}</span>
|
||||
<span class="log-timestamp">${formatTimestamp(event.timestamp)}</span>
|
||||
</div>
|
||||
<div class="log-message">${event.message}</div>
|
||||
${hasDetails ? `<div class="log-details">${JSON.stringify(event.details, null, 2)}</div>` : ''}
|
||||
`;
|
||||
|
||||
return logEntry;
|
||||
}
|
||||
|
||||
// Limpiar vista de log
|
||||
function clearLogView() {
|
||||
const logContainer = document.getElementById('events-log');
|
||||
logContainer.innerHTML = `
|
||||
<div class="log-entry log-info">
|
||||
<div class="log-header">
|
||||
<span>🧹 System</span>
|
||||
<span class="log-timestamp">${new Date().toLocaleString('es-ES')}</span>
|
||||
</div>
|
||||
<div class="log-message">Log view cleared. Click refresh to reload events.</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const logStats = document.getElementById('log-stats');
|
||||
logStats.textContent = 'Log view cleared';
|
||||
}
|
||||
|
||||
// Inicializar listeners para eventos
|
||||
function initEventListeners() {
|
||||
// Botones de control de log para el tab de events
|
||||
const refreshBtn = document.getElementById('refresh-events-btn');
|
||||
const clearBtn = document.getElementById('clear-events-btn');
|
||||
|
||||
if (refreshBtn) {
|
||||
refreshBtn.addEventListener('click', loadEvents);
|
||||
}
|
||||
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener('click', clearEventsView);
|
||||
}
|
||||
}
|
||||
|
||||
// Función para cargar eventos en el tab de events
|
||||
window.loadEvents = function () {
|
||||
fetch('/api/events?limit=50')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const eventsContainer = document.getElementById('events-container');
|
||||
const eventsCount = document.getElementById('events-count');
|
||||
|
||||
// Verificar que los elementos existan
|
||||
if (!eventsContainer) {
|
||||
console.warn('Events container not found in loadEvents');
|
||||
return;
|
||||
}
|
||||
|
||||
// Limpiar contenedor
|
||||
eventsContainer.innerHTML = '';
|
||||
|
||||
// Actualizar contador
|
||||
if (eventsCount) {
|
||||
eventsCount.textContent = data.showing || 0;
|
||||
}
|
||||
|
||||
// Añadir eventos (orden inverso para mostrar primero los más nuevos)
|
||||
const events = data.events.reverse();
|
||||
|
||||
if (events.length === 0) {
|
||||
eventsContainer.innerHTML = `
|
||||
<div class="log-entry log-info">
|
||||
<div class="log-header">
|
||||
<span>📋 System</span>
|
||||
<span class="log-timestamp">${new Date().toLocaleString('es-ES')}</span>
|
||||
</div>
|
||||
<div class="log-message">No events found</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
events.forEach(event => {
|
||||
eventsContainer.appendChild(createLogEntry(event));
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-scroll al inicio para mostrar eventos más nuevos
|
||||
eventsContainer.scrollTop = 0;
|
||||
} else {
|
||||
console.error('Error loading events:', data.error);
|
||||
showMessage('Error loading events log', 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching events:', error);
|
||||
showMessage('Error fetching events log', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Función para limpiar vista de eventos
|
||||
function clearEventsView() {
|
||||
const eventsContainer = document.getElementById('events-container');
|
||||
const eventsCount = document.getElementById('events-count');
|
||||
|
||||
eventsContainer.innerHTML = `
|
||||
<div class="log-entry log-info">
|
||||
<div class="log-header">
|
||||
<span>🧹 System</span>
|
||||
<span class="log-timestamp">${new Date().toLocaleString('es-ES')}</span>
|
||||
</div>
|
||||
<div class="log-message">Events view cleared. Click refresh to reload events.</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
eventsCount.textContent = '0';
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
/**
|
||||
* Archivo principal que inicializa todos los componentes
|
||||
*/
|
||||
|
||||
// Inicializar la aplicación al cargar el documento
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Inicializar tema
|
||||
loadTheme();
|
||||
|
||||
// Iniciar streaming de estado automáticamente
|
||||
startStatusStreaming();
|
||||
|
||||
// Cargar datos iniciales
|
||||
loadDatasets();
|
||||
updateStatus();
|
||||
loadCsvConfig();
|
||||
refreshEventLog();
|
||||
|
||||
// Inicializar listeners de eventos
|
||||
initPlcListeners();
|
||||
initDatasetListeners();
|
||||
initVariableEditListeners();
|
||||
initStreamingListeners();
|
||||
initCsvListeners();
|
||||
initEventListeners();
|
||||
|
||||
// 🔑 NUEVO: Inicializar plotManager si existe
|
||||
if (typeof PlotManager !== 'undefined' && !window.plotManager) {
|
||||
window.plotManager = new PlotManager();
|
||||
}
|
||||
|
||||
// Configurar actualizaciones periódicas como respaldo
|
||||
setInterval(updateStatus, 30000); // Cada 30 segundos como respaldo
|
||||
setInterval(refreshEventLog, 10000); // Cada 10 segundos
|
||||
|
||||
// Inicializar visibilidad de campos en formularios
|
||||
toggleFields();
|
||||
});
|
||||
|
||||
// Limpiar conexiones SSE cuando se descarga la página
|
||||
window.addEventListener('beforeunload', function () {
|
||||
if (variableEventSource) {
|
||||
variableEventSource.close();
|
||||
}
|
||||
if (statusEventSource) {
|
||||
statusEventSource.close();
|
||||
}
|
||||
});
|
|
@ -1,79 +0,0 @@
|
|||
/**
|
||||
* Gestión de la conexión con el PLC y configuración relacionada
|
||||
*/
|
||||
|
||||
// Inicializar listeners de eventos para PLC
|
||||
function initPlcListeners() {
|
||||
// Configuración del PLC
|
||||
document.getElementById('plc-config-form').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
const data = {
|
||||
ip: document.getElementById('plc-ip').value,
|
||||
rack: parseInt(document.getElementById('plc-rack').value),
|
||||
slot: parseInt(document.getElementById('plc-slot').value)
|
||||
};
|
||||
|
||||
fetch('/api/plc/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// Configuración UDP
|
||||
document.getElementById('udp-config-form').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
const data = {
|
||||
host: document.getElementById('udp-host').value,
|
||||
port: parseInt(document.getElementById('udp-port').value)
|
||||
};
|
||||
|
||||
fetch('/api/udp/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// Botón de conexión PLC
|
||||
document.getElementById('connect-btn').addEventListener('click', function () {
|
||||
fetch('/api/plc/connect', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
});
|
||||
});
|
||||
|
||||
// Botón de desconexión PLC
|
||||
document.getElementById('disconnect-btn').addEventListener('click', function () {
|
||||
fetch('/api/plc/disconnect', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
});
|
||||
});
|
||||
|
||||
// Botón de actualización de intervalo
|
||||
document.getElementById('update-sampling-btn').addEventListener('click', function () {
|
||||
const interval = parseFloat(document.getElementById('sampling-interval').value);
|
||||
fetch('/api/sampling', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ interval: interval })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
});
|
||||
});
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,299 +0,0 @@
|
|||
/**
|
||||
* Gestión del estado del sistema y actualizaciones en tiempo real
|
||||
*/
|
||||
|
||||
// Variables para el streaming de estado
|
||||
let statusEventSource = null;
|
||||
let isStreamingStatus = false;
|
||||
|
||||
// Actualizar el estado del sistema
|
||||
function updateStatus() {
|
||||
fetch('/api/status')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const plcStatus = document.getElementById('plc-status');
|
||||
const streamStatus = document.getElementById('stream-status');
|
||||
const csvStatus = document.getElementById('csv-status');
|
||||
const diskSpaceStatus = document.getElementById('disk-space');
|
||||
|
||||
// Actualizar estado de conexión PLC con información de reconexión
|
||||
if (data.plc_connected) {
|
||||
const reconnectionInfo = data.plc_reconnection || {};
|
||||
let reconnectionStatus = '';
|
||||
|
||||
if (reconnectionInfo.enabled) {
|
||||
reconnectionStatus = '<div style="font-size: 0.8em; color: #666;">🔄 Auto-reconnection: enabled</div>';
|
||||
}
|
||||
|
||||
plcStatus.innerHTML = `🔌 PLC: Connected ${reconnectionStatus}<div style="margin-top: 8px;"><button type="button" id="status-disconnect-btn">❌ Disconnect</button></div>`;
|
||||
plcStatus.className = 'status-item status-connected';
|
||||
|
||||
// Añadir event listener al nuevo botón de desconexión
|
||||
document.getElementById('status-disconnect-btn').addEventListener('click', function () {
|
||||
fetch('/api/plc/disconnect', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const reconnectionInfo = data.plc_reconnection || {};
|
||||
const connectionInfo = data.plc_connection_info || {};
|
||||
|
||||
let statusText = '🔌 PLC: Disconnected';
|
||||
let statusClass = 'status-item status-disconnected';
|
||||
let reconnectionDetails = '';
|
||||
|
||||
// Mostrar información de reconexión si está habilitada
|
||||
if (reconnectionInfo.enabled) {
|
||||
if (reconnectionInfo.active) {
|
||||
statusText = '🔌 PLC: Reconnecting...';
|
||||
statusClass = 'status-item status-reconnecting';
|
||||
|
||||
const nextDelay = reconnectionInfo.next_delay_seconds || 0;
|
||||
const failures = reconnectionInfo.consecutive_failures || 0;
|
||||
|
||||
if (nextDelay > 0) {
|
||||
reconnectionDetails = `<div style="font-size: 0.8em; color: #ff9800;">🔄 Next attempt in ${nextDelay}s (failure #${failures})</div>`;
|
||||
} else {
|
||||
reconnectionDetails = '<div style="font-size: 0.8em; color: #ff9800;">🔄 Attempting reconnection...</div>';
|
||||
}
|
||||
} else {
|
||||
if (reconnectionInfo.consecutive_failures > 0) {
|
||||
reconnectionDetails = `<div style="font-size: 0.8em; color: #666;">🔄 Auto-reconnection: enabled (${reconnectionInfo.consecutive_failures} failures)</div>`;
|
||||
} else {
|
||||
reconnectionDetails = '<div style="font-size: 0.8em; color: #666;">🔄 Auto-reconnection: enabled</div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
plcStatus.innerHTML = `${statusText} ${reconnectionDetails}<div style="margin-top: 8px;"><button type="button" id="status-connect-btn">🔗 Connect</button></div>`;
|
||||
plcStatus.className = statusClass;
|
||||
|
||||
// Añadir event listener al botón de conexión
|
||||
document.getElementById('status-connect-btn').addEventListener('click', function () {
|
||||
fetch('/api/plc/connect', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const msg = data.error ? `${data.message}: ${data.error}` : data.message;
|
||||
showMessage(msg, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
})
|
||||
.catch(err => {
|
||||
showMessage(`Error connecting to PLC: ${err}`, 'error');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Actualizar estado de streaming UDP
|
||||
if (data.streaming) {
|
||||
streamStatus.innerHTML = '📡 UDP Streaming: Active <div style="margin-top: 8px;"><button type="button" id="status-streaming-btn">⏹️ Stop</button></div>';
|
||||
streamStatus.className = 'status-item status-streaming';
|
||||
|
||||
// Añadir event listener al botón de parar streaming UDP
|
||||
document.getElementById('status-streaming-btn').addEventListener('click', function () {
|
||||
fetch('/api/udp/streaming/stop', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
streamStatus.innerHTML = '📡 UDP Streaming: Inactive <div style="margin-top: 8px;"><button type="button" id="status-start-btn">▶️ Start</button></div>';
|
||||
streamStatus.className = 'status-item status-idle';
|
||||
|
||||
// Añadir event listener al botón de iniciar streaming UDP
|
||||
document.getElementById('status-start-btn').addEventListener('click', function () {
|
||||
fetch('/api/udp/streaming/start', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Actualizar estado de grabación CSV
|
||||
if (data.csv_recording) {
|
||||
csvStatus.textContent = `💾 CSV: Recording`;
|
||||
csvStatus.className = 'status-item status-streaming';
|
||||
} else {
|
||||
csvStatus.textContent = `💾 CSV: Inactive`;
|
||||
csvStatus.className = 'status-item status-idle';
|
||||
}
|
||||
|
||||
// Actualizar estado de espacio en disco
|
||||
if (data.disk_space_info) {
|
||||
diskSpaceStatus.innerHTML = `💽 Disk: ${data.disk_space_info.free_space} free<br>
|
||||
⏱️ ~${data.disk_space_info.recording_time_left}`;
|
||||
diskSpaceStatus.className = 'status-item status-idle';
|
||||
} else {
|
||||
diskSpaceStatus.textContent = '💽 Disk Space: Calculating...';
|
||||
diskSpaceStatus.className = 'status-item status-idle';
|
||||
}
|
||||
})
|
||||
.catch(error => console.error('Error updating status:', error));
|
||||
}
|
||||
|
||||
// Iniciar streaming de estado en tiempo real
|
||||
function startStatusStreaming() {
|
||||
if (isStreamingStatus) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cerrar conexión existente si hay alguna
|
||||
if (statusEventSource) {
|
||||
statusEventSource.close();
|
||||
}
|
||||
|
||||
// Crear nueva conexión EventSource
|
||||
statusEventSource = new EventSource('/api/stream/status?interval=2.0');
|
||||
|
||||
statusEventSource.onopen = function (event) {
|
||||
isStreamingStatus = true;
|
||||
};
|
||||
|
||||
statusEventSource.onmessage = function (event) {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
switch (data.type) {
|
||||
case 'connected':
|
||||
break;
|
||||
|
||||
case 'status':
|
||||
// Actualizar estado en tiempo real
|
||||
updateStatusFromStream(data.status);
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
console.error('Status stream error:', data.message);
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing status SSE data:', error);
|
||||
}
|
||||
};
|
||||
|
||||
statusEventSource.onerror = function (event) {
|
||||
console.error('Status stream error:', event);
|
||||
isStreamingStatus = false;
|
||||
|
||||
// Intentar reconectar después de un retraso
|
||||
setTimeout(() => {
|
||||
startStatusStreaming();
|
||||
}, 10000);
|
||||
};
|
||||
}
|
||||
|
||||
// Detener streaming de estado en tiempo real
|
||||
function stopStatusStreaming() {
|
||||
if (statusEventSource) {
|
||||
statusEventSource.close();
|
||||
statusEventSource = null;
|
||||
}
|
||||
isStreamingStatus = false;
|
||||
}
|
||||
|
||||
// Actualizar estado desde datos de streaming
|
||||
function updateStatusFromStream(status) {
|
||||
const plcStatus = document.getElementById('plc-status');
|
||||
const streamStatus = document.getElementById('stream-status');
|
||||
const csvStatus = document.getElementById('csv-status');
|
||||
const diskSpaceStatus = document.getElementById('disk-space');
|
||||
|
||||
// Actualizar estado de conexión PLC
|
||||
if (status.plc_connected) {
|
||||
plcStatus.innerHTML = '🔌 PLC: Connected <div style="margin-top: 8px;"><button type="button" id="status-disconnect-btn">❌ Disconnect</button></div>';
|
||||
plcStatus.className = 'status-item status-connected';
|
||||
|
||||
// Añadir event listener al nuevo botón de desconexión
|
||||
const disconnectBtn = document.getElementById('status-disconnect-btn');
|
||||
if (disconnectBtn) {
|
||||
disconnectBtn.addEventListener('click', function () {
|
||||
fetch('/api/plc/disconnect', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
plcStatus.innerHTML = '🔌 PLC: Disconnected <div style="margin-top: 8px;"><button type="button" id="status-connect-btn">🔗 Connect</button></div>';
|
||||
plcStatus.className = 'status-item status-disconnected';
|
||||
|
||||
// Añadir event listener al botón de conexión
|
||||
const connectBtn = document.getElementById('status-connect-btn');
|
||||
if (connectBtn) {
|
||||
connectBtn.addEventListener('click', function () {
|
||||
fetch('/api/plc/connect', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const msg = data.error ? `${data.message}: ${data.error}` : data.message;
|
||||
showMessage(msg, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
})
|
||||
.catch(err => {
|
||||
showMessage(`Error connecting to PLC: ${err}`, 'error');
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Actualizar estado de streaming UDP
|
||||
if (status.streaming) {
|
||||
streamStatus.innerHTML = '📡 UDP Streaming: Active <div style="margin-top: 8px;"><button type="button" id="status-streaming-btn">⏹️ Stop</button></div>';
|
||||
streamStatus.className = 'status-item status-streaming';
|
||||
|
||||
// Añadir event listener al botón de parar streaming UDP
|
||||
const stopBtn = document.getElementById('status-streaming-btn');
|
||||
if (stopBtn) {
|
||||
stopBtn.addEventListener('click', function () {
|
||||
fetch('/api/udp/streaming/stop', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
streamStatus.innerHTML = '📡 UDP Streaming: Inactive <div style="margin-top: 8px;"><button type="button" id="status-start-btn">▶️ Start</button></div>';
|
||||
streamStatus.className = 'status-item status-idle';
|
||||
|
||||
// Añadir event listener al botón de iniciar streaming UDP
|
||||
const startBtn = document.getElementById('status-start-btn');
|
||||
if (startBtn) {
|
||||
startBtn.addEventListener('click', function () {
|
||||
fetch('/api/udp/streaming/start', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Actualizar estado de grabación CSV
|
||||
if (status.csv_recording) {
|
||||
csvStatus.textContent = `💾 CSV: Recording`;
|
||||
csvStatus.className = 'status-item status-streaming';
|
||||
} else {
|
||||
csvStatus.textContent = `💾 CSV: Inactive`;
|
||||
csvStatus.className = 'status-item status-idle';
|
||||
}
|
||||
|
||||
// Actualizar estado de espacio en disco
|
||||
if (status.disk_space_info) {
|
||||
diskSpaceStatus.innerHTML = `💽 Disk: ${status.disk_space_info.free_space} free<br>
|
||||
⏱️ ~${status.disk_space_info.recording_time_left}`;
|
||||
diskSpaceStatus.className = 'status-item status-idle';
|
||||
} else {
|
||||
diskSpaceStatus.textContent = '💽 Disk Space: Calculating...';
|
||||
diskSpaceStatus.className = 'status-item status-idle';
|
||||
}
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
/**
|
||||
* Gestión del streaming UDP a PlotJuggler (independiente del recording CSV)
|
||||
*/
|
||||
|
||||
// Inicializar listeners para el control de streaming UDP
|
||||
function initStreamingListeners() {
|
||||
// Iniciar streaming UDP
|
||||
document.getElementById('start-streaming-btn').addEventListener('click', function () {
|
||||
fetch('/api/udp/streaming/start', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error starting UDP streaming:', error);
|
||||
showMessage('Error starting UDP streaming', 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// Detener streaming UDP
|
||||
document.getElementById('stop-streaming-btn').addEventListener('click', function () {
|
||||
fetch('/api/udp/streaming/stop', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error stopping UDP streaming:', error);
|
||||
showMessage('Error stopping UDP streaming', 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// Cargar estado de streaming de variables
|
||||
loadStreamingStatus();
|
||||
}
|
||||
|
||||
// Cargar estado de variables en streaming
|
||||
function loadStreamingStatus() {
|
||||
fetch('/api/variables/streaming')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
data.streaming_variables.forEach(varName => {
|
||||
const checkbox = document.getElementById(`stream-${varName}`);
|
||||
if (checkbox) {
|
||||
checkbox.checked = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => console.error('Error loading streaming status:', error));
|
||||
}
|
|
@ -1,319 +0,0 @@
|
|||
/**
|
||||
* Tab System Management
|
||||
* Maneja la navegación entre tabs en la aplicación
|
||||
*/
|
||||
|
||||
class TabManager {
|
||||
constructor() {
|
||||
this.currentTab = 'datasets';
|
||||
this.plotTabs = new Set(); // Track dynamic plot tabs
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Event listeners para los botones de tab estáticos
|
||||
this.bindStaticTabs();
|
||||
|
||||
// Inicializar con el tab activo por defecto
|
||||
this.switchTab(this.currentTab);
|
||||
}
|
||||
|
||||
bindStaticTabs() {
|
||||
// Solo bindear tabs estáticos, los dinámicos se bindean al crearlos
|
||||
document.querySelectorAll('.tab-btn:not([data-plot-id])').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const tabName = e.target.dataset.tab;
|
||||
this.switchTab(tabName);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
switchTab(tabName) {
|
||||
// Remover clase active de todos los tabs
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
|
||||
document.querySelectorAll('.tab-content').forEach(content => {
|
||||
content.classList.remove('active');
|
||||
});
|
||||
|
||||
// Activar el tab seleccionado
|
||||
const activeBtn = document.querySelector(`[data-tab="${tabName}"]`);
|
||||
const activeContent = document.getElementById(`${tabName}-tab`);
|
||||
|
||||
if (activeBtn && activeContent) {
|
||||
activeBtn.classList.add('active');
|
||||
activeContent.classList.add('active');
|
||||
this.currentTab = tabName;
|
||||
|
||||
// Eventos específicos por tab
|
||||
this.handleTabSpecificEvents(tabName);
|
||||
}
|
||||
}
|
||||
|
||||
createPlotTab(sessionId, plotName) {
|
||||
// Crear botón de sub-tab dinámico
|
||||
const subTabBtn = document.createElement('button');
|
||||
subTabBtn.className = 'sub-tab-btn plot-sub-tab';
|
||||
subTabBtn.dataset.subTab = `plot-${sessionId}`;
|
||||
subTabBtn.dataset.plotId = sessionId;
|
||||
subTabBtn.innerHTML = `
|
||||
📈 ${plotName}
|
||||
<span class="sub-tab-close" data-session-id="${sessionId}">×</span>
|
||||
`;
|
||||
|
||||
// Crear contenido del sub-tab
|
||||
const subTabContent = document.createElement('div');
|
||||
subTabContent.className = 'sub-tab-content plot-sub-tab-content';
|
||||
subTabContent.id = `plot-${sessionId}-sub-tab`;
|
||||
subTabContent.innerHTML = `
|
||||
<article>
|
||||
<header>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>📈 ${plotName}</span>
|
||||
<div>
|
||||
<button type="button" class="outline" onclick="window.editPlotSession('${sessionId}')">
|
||||
✏️ Edit Plot
|
||||
</button>
|
||||
<button type="button" class="secondary" onclick="window.removePlotSession('${sessionId}')">
|
||||
🗑️ Remove Plot
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="plot-session" id="plot-session-${sessionId}">
|
||||
<div class="plot-header">
|
||||
<h4>📈 ${plotName}</h4>
|
||||
<div class="plot-controls">
|
||||
<button class="btn btn-sm" onclick="plotManager.controlPlot('${sessionId}', 'start')" title="Start">
|
||||
▶️ Start
|
||||
</button>
|
||||
<button class="btn btn-sm" onclick="plotManager.controlPlot('${sessionId}', 'pause')" title="Pause">
|
||||
⏸️ Pause
|
||||
</button>
|
||||
<button class="btn btn-sm" onclick="plotManager.controlPlot('${sessionId}', 'clear')" title="Clear">
|
||||
🗑️ Clear
|
||||
</button>
|
||||
<button class="btn btn-sm" onclick="plotManager.controlPlot('${sessionId}', 'stop')" title="Stop">
|
||||
⏹️ Stop
|
||||
</button>
|
||||
<div class="refresh-rate-control">
|
||||
<label for="refresh-rate-tab-${sessionId}" title="Chart Refresh Rate (ms)">⏱️</label>
|
||||
<input type="number" id="refresh-rate-tab-${sessionId}" class="refresh-rate-input"
|
||||
value="1000" min="100" max="60000" step="100"
|
||||
onchange="plotManager.updateRefreshRate('${sessionId}', this.value)"
|
||||
onkeypress="if(event.key==='Enter') plotManager.updateRefreshRate('${sessionId}', this.value)"
|
||||
title="Refresh rate in milliseconds (100-60000)">
|
||||
<span class="refresh-rate-unit">ms</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="plot-info">
|
||||
<span class="plot-stats" id="plot-stats-${sessionId}">
|
||||
Loading plot information...
|
||||
</span>
|
||||
</div>
|
||||
<div class="plot-canvas">
|
||||
<div style="display:flex; justify-content: space-between; align-items:center; gap: 0.5rem; margin-bottom: 0.5rem;">
|
||||
<div class="refresh-rate-control">
|
||||
<label for="refresh-rate-tab-${sessionId}" title="Chart Refresh Rate (ms)">⏱️</label>
|
||||
<input type="number" id="refresh-rate-tab-${sessionId}" class="refresh-rate-input"
|
||||
value="1000" min="100" max="60000" step="100"
|
||||
onchange="plotManager.updateRefreshRate('${sessionId}', this.value)"
|
||||
onkeypress="if(event.key==='Enter') plotManager.updateRefreshRate('${sessionId}', this.value)"
|
||||
title="Refresh rate in milliseconds (100-60000)">
|
||||
<span class="refresh-rate-unit">ms</span>
|
||||
</div>
|
||||
<div class="time-window-control">
|
||||
<label for="time-window-tab-${sessionId}" title="Time Window (seconds)">🕒</label>
|
||||
<input type="number" id="time-window-tab-${sessionId}" class="time-window-input"
|
||||
value="60" min="5" max="3600" step="5"
|
||||
onchange="plotManager.onTimeWindowChange('${sessionId}', this.value)"
|
||||
onkeypress="if(event.key==='Enter') plotManager.onTimeWindowChange('${sessionId}', this.value)"
|
||||
title="Visible time window in seconds (5-3600)">
|
||||
<span class="time-window-unit">s</span>
|
||||
</div>
|
||||
<div class="zoom-controls">
|
||||
<button type="button" class="outline" title="Reset Zoom" onclick="(function(){ try { const s=plotManager.sessions.get('${sessionId}'); if(s&&s.chart&&typeof s.chart.resetZoom==='function'){ s.chart.resetZoom('none'); } } catch(e){ console.warn(e);} })()">🔍 Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
<canvas id="chart-${sessionId}"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
|
||||
// Mostrar sub-tabs si no están visibles
|
||||
const subTabs = document.getElementById('plot-sub-tabs');
|
||||
const plotSessionsContainer = document.getElementById('plot-sessions-container');
|
||||
const plotSubContent = document.getElementById('plot-sub-content');
|
||||
|
||||
if (subTabs.style.display === 'none') {
|
||||
subTabs.style.display = 'flex';
|
||||
plotSessionsContainer.style.display = 'none';
|
||||
plotSubContent.style.display = 'block';
|
||||
}
|
||||
|
||||
// Agregar sub-tab al contenedor de sub-tabs
|
||||
subTabs.appendChild(subTabBtn);
|
||||
|
||||
// Agregar contenido del sub-tab
|
||||
plotSubContent.appendChild(subTabContent);
|
||||
|
||||
// Bind events
|
||||
subTabBtn.addEventListener('click', (e) => {
|
||||
if (!e.target.classList.contains('sub-tab-close')) {
|
||||
this.switchSubTab(`plot-${sessionId}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Close button event
|
||||
subTabBtn.querySelector('.sub-tab-close').addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
// Llamar a la función que elimina el plot del backend Y del frontend
|
||||
if (typeof window.removePlotSession === 'function') {
|
||||
window.removePlotSession(sessionId);
|
||||
} else {
|
||||
console.error('removePlotSession function not available');
|
||||
// Fallback: solo remover del frontend
|
||||
this.removePlotTab(sessionId);
|
||||
}
|
||||
});
|
||||
|
||||
this.plotTabs.add(sessionId);
|
||||
|
||||
return subTabBtn;
|
||||
}
|
||||
|
||||
switchSubTab(subTabName) {
|
||||
// Remover clase active de todos los sub-tabs
|
||||
document.querySelectorAll('.sub-tab-btn').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
|
||||
document.querySelectorAll('.sub-tab-content').forEach(content => {
|
||||
content.classList.remove('active');
|
||||
});
|
||||
|
||||
// Activar el sub-tab seleccionado
|
||||
const activeBtn = document.querySelector(`[data-sub-tab="${subTabName}"]`);
|
||||
const activeContent = document.getElementById(`${subTabName}-sub-tab`);
|
||||
|
||||
if (activeBtn && activeContent) {
|
||||
activeBtn.classList.add('active');
|
||||
activeContent.classList.add('active');
|
||||
|
||||
// Eventos específicos por sub-tab
|
||||
this.handleSubTabSpecificEvents(subTabName);
|
||||
}
|
||||
}
|
||||
|
||||
handleSubTabSpecificEvents(subTabName) {
|
||||
if (subTabName.startsWith('plot-')) {
|
||||
// Sub-tab de plot individual - cargar datos específicos
|
||||
const sessionId = subTabName.replace('plot-', '');
|
||||
if (typeof plotManager !== 'undefined') {
|
||||
plotManager.updateSessionData(sessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removePlotTab(sessionId) {
|
||||
// Remover sub-tab button
|
||||
const subTabBtn = document.querySelector(`[data-plot-id="${sessionId}"]`);
|
||||
if (subTabBtn) {
|
||||
subTabBtn.remove();
|
||||
}
|
||||
|
||||
// Remover sub-tab content
|
||||
const subTabContent = document.getElementById(`plot-${sessionId}-sub-tab`);
|
||||
if (subTabContent) {
|
||||
subTabContent.remove();
|
||||
}
|
||||
|
||||
this.plotTabs.delete(sessionId);
|
||||
|
||||
// Si no quedan sub-tabs, mostrar vista inicial
|
||||
const subTabs = document.getElementById('plot-sub-tabs');
|
||||
const plotSessionsContainer = document.getElementById('plot-sessions-container');
|
||||
const plotSubContent = document.getElementById('plot-sub-content');
|
||||
|
||||
if (subTabs.children.length === 0) {
|
||||
subTabs.style.display = 'none';
|
||||
plotSessionsContainer.style.display = 'block';
|
||||
plotSubContent.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
updatePlotTabName(sessionId, newName) {
|
||||
const subTabBtn = document.querySelector(`[data-plot-id="${sessionId}"]`);
|
||||
if (subTabBtn) {
|
||||
subTabBtn.innerHTML = `
|
||||
📈 ${newName}
|
||||
<span class="sub-tab-close" data-session-id="${sessionId}">×</span>
|
||||
`;
|
||||
|
||||
// Re-bind close event
|
||||
subTabBtn.querySelector('.sub-tab-close').addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
// Llamar a la función que elimina el plot del backend Y del frontend
|
||||
if (typeof window.removePlotSession === 'function') {
|
||||
window.removePlotSession(sessionId);
|
||||
} else {
|
||||
console.error('removePlotSession function not available');
|
||||
// Fallback: solo remover del frontend
|
||||
this.removePlotTab(sessionId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Actualizar header del contenido
|
||||
const header = document.querySelector(`#plot-${sessionId}-sub-tab h4`);
|
||||
if (header) {
|
||||
header.textContent = `📈 ${newName}`;
|
||||
}
|
||||
|
||||
const articleHeader = document.querySelector(`#plot-${sessionId}-sub-tab header span`);
|
||||
if (articleHeader) {
|
||||
articleHeader.textContent = `📈 ${newName}`;
|
||||
}
|
||||
}
|
||||
|
||||
handleTabSpecificEvents(tabName) {
|
||||
switch (tabName) {
|
||||
case 'plotting':
|
||||
// Inicializar plotting si no está inicializado
|
||||
if (typeof plotManager !== 'undefined' && !plotManager.isInitialized) {
|
||||
plotManager.init();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'events':
|
||||
// Cargar eventos si no están cargados
|
||||
if (typeof loadEvents === 'function') {
|
||||
loadEvents();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'datasets':
|
||||
// Actualizar datasets si es necesario
|
||||
if (typeof loadDatasets === 'function') {
|
||||
loadDatasets();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
getCurrentTab() {
|
||||
return this.currentTab;
|
||||
}
|
||||
}
|
||||
|
||||
// Inicialización
|
||||
let tabManager = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
tabManager = new TabManager();
|
||||
});
|
|
@ -1,32 +0,0 @@
|
|||
/**
|
||||
* Gestión del tema de la aplicación (claro/oscuro/auto)
|
||||
*/
|
||||
|
||||
// Establecer el tema
|
||||
function setTheme(theme) {
|
||||
const html = document.documentElement;
|
||||
const buttons = document.querySelectorAll('.theme-selector button');
|
||||
|
||||
// Eliminar clase active de todos los botones
|
||||
buttons.forEach(btn => btn.classList.remove('active'));
|
||||
|
||||
// Establecer tema
|
||||
html.setAttribute('data-theme', theme);
|
||||
|
||||
// Añadir clase active al botón seleccionado
|
||||
document.getElementById(`theme-${theme}`).classList.add('active');
|
||||
|
||||
// Guardar preferencia en localStorage
|
||||
localStorage.setItem('theme', theme);
|
||||
}
|
||||
|
||||
// Cargar tema guardado al cargar la página
|
||||
function loadTheme() {
|
||||
const savedTheme = localStorage.getItem('theme') || 'light';
|
||||
setTheme(savedTheme);
|
||||
}
|
||||
|
||||
// Inicializar tema al cargar la página
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
loadTheme();
|
||||
});
|
|
@ -1,64 +0,0 @@
|
|||
/**
|
||||
* Funciones de utilidad general para la aplicación
|
||||
*/
|
||||
|
||||
// Función para mostrar mensajes en la interfaz
|
||||
function showMessage(message, type = 'success') {
|
||||
const messagesDiv = document.getElementById('messages');
|
||||
let alertClass;
|
||||
|
||||
switch (type) {
|
||||
case 'success':
|
||||
alertClass = 'alert-success';
|
||||
break;
|
||||
case 'warning':
|
||||
alertClass = 'alert-warning';
|
||||
break;
|
||||
case 'info':
|
||||
alertClass = 'alert-info';
|
||||
break;
|
||||
case 'error':
|
||||
default:
|
||||
alertClass = 'alert-error';
|
||||
break;
|
||||
}
|
||||
|
||||
messagesDiv.innerHTML = `<div class="alert ${alertClass}">${message}</div>`;
|
||||
setTimeout(() => {
|
||||
messagesDiv.innerHTML = '';
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Formatear timestamp para los logs
|
||||
function formatTimestamp(isoString) {
|
||||
const date = new Date(isoString);
|
||||
return date.toLocaleString('es-ES', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
// Obtener icono para tipo de evento
|
||||
function getEventIcon(eventType) {
|
||||
const icons = {
|
||||
'plc_connection': '🔗',
|
||||
'plc_connection_failed': '❌',
|
||||
'plc_disconnection': '🔌',
|
||||
'plc_disconnection_error': '⚠️',
|
||||
'streaming_started': '▶️',
|
||||
'streaming_stopped': '⏹️',
|
||||
'streaming_error': '❌',
|
||||
'csv_started': '💾',
|
||||
'csv_stopped': '📁',
|
||||
'csv_error': '❌',
|
||||
'config_change': '⚙️',
|
||||
'variable_added': '➕',
|
||||
'variable_removed': '➖',
|
||||
'application_started': '🚀'
|
||||
};
|
||||
return icons[eventType] || '📋';
|
||||
}
|
|
@ -1,319 +0,0 @@
|
|||
/**
|
||||
* Gestión de variables y streaming de valores en tiempo real
|
||||
*/
|
||||
|
||||
// Variables para el streaming de variables
|
||||
let variableEventSource = null;
|
||||
let isStreamingVariables = false;
|
||||
|
||||
// Toggle de campos de variables según el área de memoria
|
||||
function toggleFields() {
|
||||
const area = document.getElementById('var-area').value;
|
||||
const dbField = document.getElementById('db-field');
|
||||
const dbInput = document.getElementById('var-db');
|
||||
const bitField = document.getElementById('bit-field');
|
||||
const typeSelect = document.getElementById('var-type');
|
||||
|
||||
// Manejar campo DB
|
||||
if (area === 'db') {
|
||||
dbField.style.display = 'block';
|
||||
dbInput.required = true;
|
||||
} else {
|
||||
dbField.style.display = 'none';
|
||||
dbInput.required = false;
|
||||
dbInput.value = 1; // Valor por defecto para áreas no DB
|
||||
}
|
||||
|
||||
// Manejar campo Bit y restricciones de tipo de datos
|
||||
if (area === 'e' || area === 'a' || area === 'mb') {
|
||||
bitField.style.display = 'block';
|
||||
// Para áreas de bit, forzar tipo de dato a bool
|
||||
typeSelect.value = 'bool';
|
||||
// Deshabilitar otros tipos de datos para áreas de bit
|
||||
Array.from(typeSelect.options).forEach(option => {
|
||||
option.disabled = (option.value !== 'bool');
|
||||
});
|
||||
} else {
|
||||
bitField.style.display = 'none';
|
||||
// Re-habilitar todos los tipos de datos para áreas no-bit
|
||||
Array.from(typeSelect.options).forEach(option => {
|
||||
option.disabled = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle de campos de edición de variables
|
||||
function toggleEditFields() {
|
||||
const area = document.getElementById('edit-var-area').value;
|
||||
const dbField = document.getElementById('edit-db-field');
|
||||
const dbInput = document.getElementById('edit-var-db');
|
||||
const bitField = document.getElementById('edit-bit-field');
|
||||
const typeSelect = document.getElementById('edit-var-type');
|
||||
|
||||
// Manejar campo DB
|
||||
if (area === 'db') {
|
||||
dbField.style.display = 'block';
|
||||
dbInput.required = true;
|
||||
} else {
|
||||
dbField.style.display = 'none';
|
||||
dbInput.required = false;
|
||||
dbInput.value = 1; // Valor por defecto para áreas no DB
|
||||
}
|
||||
|
||||
// Manejar campo Bit y restricciones de tipo de datos
|
||||
if (area === 'e' || area === 'a' || area === 'mb') {
|
||||
bitField.style.display = 'block';
|
||||
// Para áreas de bit, forzar tipo de dato a bool
|
||||
typeSelect.value = 'bool';
|
||||
// Deshabilitar otros tipos de datos para áreas de bit
|
||||
Array.from(typeSelect.options).forEach(option => {
|
||||
option.disabled = (option.value !== 'bool');
|
||||
});
|
||||
} else {
|
||||
bitField.style.display = 'none';
|
||||
// Re-habilitar todos los tipos de datos para áreas no-bit
|
||||
Array.from(typeSelect.options).forEach(option => {
|
||||
option.disabled = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Actualizar streaming para una variable
|
||||
function toggleStreaming(varName, enabled) {
|
||||
if (!currentDatasetId) {
|
||||
showMessage('No dataset selected', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/api/datasets/${currentDatasetId}/variables/${varName}/streaming`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ enabled: enabled })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus(); // Actualizar contador de variables de streaming
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error toggling streaming:', error);
|
||||
showMessage('Error updating streaming setting', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-start live display when dataset changes (if PLC is connected)
|
||||
function autoStartLiveDisplay() {
|
||||
if (currentDatasetId) {
|
||||
// Check if PLC is connected by fetching status
|
||||
fetch('/api/status')
|
||||
.then(response => response.json())
|
||||
.then(status => {
|
||||
if (status.plc_connected && !isStreamingVariables) {
|
||||
startVariableStreaming();
|
||||
showMessage('Live display started automatically for active dataset', 'info');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error checking PLC status:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Limpiar todos los valores de variables y establecer mensaje de estado
|
||||
function clearVariableValues(statusMessage = '--') {
|
||||
// Encontrar todas las celdas de valor y limpiarlas
|
||||
const valueCells = document.querySelectorAll('[id^="value-"]');
|
||||
valueCells.forEach(cell => {
|
||||
cell.textContent = statusMessage;
|
||||
cell.style.color = 'var(--pico-muted-color)';
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Iniciar streaming de variables en tiempo real
|
||||
function startVariableStreaming() {
|
||||
if (!currentDatasetId || isStreamingVariables) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cerrar conexión existente si hay alguna
|
||||
if (variableEventSource) {
|
||||
variableEventSource.close();
|
||||
}
|
||||
|
||||
// Crear nueva conexión EventSource
|
||||
variableEventSource = new EventSource(`/api/stream/variables?dataset_id=${currentDatasetId}&interval=1.0`);
|
||||
|
||||
variableEventSource.onopen = function (event) {
|
||||
isStreamingVariables = true;
|
||||
updateStreamingIndicator(true);
|
||||
};
|
||||
|
||||
variableEventSource.onmessage = function (event) {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
switch (data.type) {
|
||||
case 'connected':
|
||||
break;
|
||||
|
||||
case 'values':
|
||||
// Actualizar valores de variables en tiempo real desde caché
|
||||
updateVariableValuesFromStream(data);
|
||||
break;
|
||||
|
||||
case 'cache_error':
|
||||
console.error('Cache error in variable stream:', data.message);
|
||||
showMessage(`Cache error: ${data.message}`, 'error');
|
||||
clearVariableValues('CACHE ERROR');
|
||||
break;
|
||||
|
||||
case 'plc_disconnected':
|
||||
clearVariableValues('PLC OFFLINE');
|
||||
showMessage('PLC disconnected - cache not being populated', 'warning');
|
||||
break;
|
||||
|
||||
case 'dataset_inactive':
|
||||
clearVariableValues('DATASET INACTIVE');
|
||||
showMessage('Dataset is not active - activate to populate cache', 'warning');
|
||||
break;
|
||||
|
||||
case 'no_variables':
|
||||
clearVariableValues('NO VARIABLES');
|
||||
showMessage('No variables defined in this dataset', 'info');
|
||||
break;
|
||||
|
||||
case 'no_cache':
|
||||
clearVariableValues('READING...');
|
||||
const samplingInfo = data.sampling_interval ? ` (every ${data.sampling_interval}s)` : '';
|
||||
showMessage(`Waiting for cache to be populated${samplingInfo}`, 'info');
|
||||
break;
|
||||
|
||||
case 'stream_error':
|
||||
console.error('SSE stream error:', data.message);
|
||||
showMessage(`Streaming error: ${data.message}`, 'error');
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing SSE data:', error);
|
||||
}
|
||||
};
|
||||
|
||||
variableEventSource.onerror = function (event) {
|
||||
console.error('Variable stream error:', event);
|
||||
isStreamingVariables = false;
|
||||
updateStreamingIndicator(false);
|
||||
|
||||
// Intentar reconectar después de un retraso
|
||||
setTimeout(() => {
|
||||
if (currentDatasetId) {
|
||||
startVariableStreaming();
|
||||
}
|
||||
}, 5000);
|
||||
};
|
||||
}
|
||||
|
||||
// Detener streaming de variables en tiempo real
|
||||
function stopVariableStreaming() {
|
||||
if (variableEventSource) {
|
||||
variableEventSource.close();
|
||||
variableEventSource = null;
|
||||
}
|
||||
isStreamingVariables = false;
|
||||
updateStreamingIndicator(false);
|
||||
}
|
||||
|
||||
// Actualizar valores de variables desde datos de streaming
|
||||
function updateVariableValuesFromStream(data) {
|
||||
const values = data.values;
|
||||
const timestamp = data.timestamp;
|
||||
const source = data.source;
|
||||
const stats = data.stats;
|
||||
|
||||
// Actualizar cada valor de variable
|
||||
Object.keys(values).forEach(varName => {
|
||||
const valueCell = document.getElementById(`value-${varName}`);
|
||||
if (valueCell) {
|
||||
const value = values[varName];
|
||||
valueCell.textContent = value;
|
||||
|
||||
// Código de color basado en el estado del valor
|
||||
if (value === 'ERROR' || value === 'FORMAT_ERROR') {
|
||||
valueCell.style.color = 'var(--pico-color-red-500)';
|
||||
valueCell.style.fontWeight = 'bold';
|
||||
} else {
|
||||
valueCell.style.color = 'var(--pico-color-green-600)';
|
||||
valueCell.style.fontWeight = 'bold';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Actualizar timestamp e información de origen
|
||||
const lastRefreshTime = document.getElementById('last-refresh-time');
|
||||
if (lastRefreshTime) {
|
||||
const sourceIcon = source === 'cache' ? '📊' : '🔗';
|
||||
const sourceText = source === 'cache' ? 'streaming cache' : 'direct PLC';
|
||||
|
||||
if (stats && stats.failed > 0) {
|
||||
lastRefreshTime.innerHTML = `
|
||||
<span style="color: var(--pico-color-green-600);">🔄 Live streaming</span><br/>
|
||||
<small style="color: var(--pico-color-amber-600);">
|
||||
⚠️ ${stats.success}/${stats.total} variables (${stats.failed} failed)
|
||||
</small><br/>
|
||||
<small style="color: var(--pico-muted-color);">
|
||||
${sourceIcon} ${sourceText} • ${new Date(timestamp).toLocaleTimeString()}
|
||||
</small>
|
||||
`;
|
||||
} else {
|
||||
lastRefreshTime.innerHTML = `
|
||||
<span style="color: var(--pico-color-green-600);">🔄 Live streaming</span><br/>
|
||||
<small style="color: var(--pico-color-green-600);">
|
||||
✅ All ${stats ? stats.success : 'N/A'} variables OK
|
||||
</small><br/>
|
||||
<small style="color: var(--pico-muted-color);">
|
||||
${sourceIcon} ${sourceText} • ${new Date(timestamp).toLocaleTimeString()}
|
||||
</small>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Actualizar indicador de streaming
|
||||
function updateStreamingIndicator(isStreaming) {
|
||||
const toggleBtn = document.getElementById('toggle-streaming-btn');
|
||||
if (toggleBtn) {
|
||||
if (isStreaming) {
|
||||
toggleBtn.innerHTML = '⏹️ Stop Live Display';
|
||||
toggleBtn.title = 'Stop live variable display';
|
||||
} else {
|
||||
toggleBtn.innerHTML = '▶️ Start Live Display';
|
||||
toggleBtn.title = 'Start live variable display';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Alternar streaming en tiempo real
|
||||
function toggleRealTimeStreaming() {
|
||||
if (isStreamingVariables) {
|
||||
stopVariableStreaming();
|
||||
showMessage('Real-time streaming stopped', 'info');
|
||||
} else {
|
||||
startVariableStreaming();
|
||||
showMessage('Real-time streaming started', 'success');
|
||||
}
|
||||
|
||||
// Actualizar texto del botón
|
||||
const toggleBtn = document.getElementById('toggle-streaming-btn');
|
||||
if (toggleBtn) {
|
||||
if (isStreamingVariables) {
|
||||
toggleBtn.innerHTML = '⏹️ Stop Live Streaming';
|
||||
} else {
|
||||
toggleBtn.innerHTML = '▶️ Start Live Streaming';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
{
|
||||
"last_state": {
|
||||
"should_connect": true,
|
||||
"should_stream": false,
|
||||
"should_stream": true,
|
||||
"active_datasets": [
|
||||
"Test",
|
||||
"Fast",
|
||||
"Test",
|
||||
"DAR"
|
||||
]
|
||||
},
|
||||
"auto_recovery_enabled": true,
|
||||
"last_update": "2025-08-14T13:31:23.906177"
|
||||
"last_update": "2025-08-14T14:47:13.218027"
|
||||
}
|
|
@ -1,825 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>PLC S7-315 Streamer & Logger</title>
|
||||
|
||||
<!-- Pico.css offline -->
|
||||
<link rel="stylesheet" href="/static/css/pico.min.css">
|
||||
|
||||
<!-- Custom styles -->
|
||||
<link rel="stylesheet" href="/static/css/styles.css">
|
||||
<!-- JSONEditor (tree view) CSS -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jsoneditor@9.10.2/dist/jsoneditor.min.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Theme Selector -->
|
||||
<div class="theme-selector">
|
||||
<button id="theme-light" class="active" onclick="setTheme('light')">☀️ Light</button>
|
||||
<button id="theme-dark" onclick="setTheme('dark')">🌙 Dark</button>
|
||||
<button id="theme-auto" onclick="setTheme('auto')">🔄 Auto</button>
|
||||
</div>
|
||||
|
||||
<main class="container">
|
||||
<div class="info-section" style="margin: 1rem 0;">
|
||||
<p><strong>Notice:</strong> Esta UI ha sido reemplazada por la SPA React. Accede a <a href="/app">/app</a>.
|
||||
Esta página no se usa en producción.</p>
|
||||
</div>
|
||||
<!-- Header -->
|
||||
<header class="header">
|
||||
<h1>
|
||||
<img src="/images/SIDEL.png" alt="SIDEL Logo" class="header-logo">
|
||||
-- PLC S7-31x Streamer & Logger
|
||||
</h1>
|
||||
<p>Real-time monitoring and streaming system</p>
|
||||
</header>
|
||||
|
||||
<!-- Status Bar -->
|
||||
<section class="status-grid">
|
||||
<div class="status-item" id="plc-status">
|
||||
🔌 PLC: Disconnected
|
||||
<div style="margin-top: 8px;">
|
||||
<button type="button" id="status-connect-btn">🔗 Connect</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-item" id="stream-status">
|
||||
📡 UDP Streaming: Inactive
|
||||
<div style="margin-top: 8px;">
|
||||
<button type="button" id="status-streaming-btn">⏹️ Stop</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-item status-idle">
|
||||
📊 Datasets: {{ status.datasets_count }} ({{ status.active_datasets_count }} active)
|
||||
</div>
|
||||
<div class="status-item status-idle">
|
||||
📋 Variables: {{ status.total_variables }}
|
||||
</div>
|
||||
<div class="status-item status-idle">
|
||||
⏱️ Interval: {{ status.sampling_interval }}s
|
||||
</div>
|
||||
<div class="status-item" id="csv-status">
|
||||
💾 CSV: Inactive
|
||||
</div>
|
||||
<div class="status-item" id="disk-space">
|
||||
💽 Disk Space: Calculating...
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Status messages -->
|
||||
<div id="messages"></div>
|
||||
|
||||
<!-- Configuration Grid -->
|
||||
<div class="config-grid">
|
||||
<!-- PLC Configuration -->
|
||||
<article>
|
||||
<header>⚙️ PLC S7-315 Configuration</header>
|
||||
<form id="plc-config-form">
|
||||
<div class="form-row">
|
||||
<label>
|
||||
PLC IP Address:
|
||||
<input type="text" id="plc-ip" value="{{ status.plc_config.ip }}"
|
||||
placeholder="192.168.1.100">
|
||||
</label>
|
||||
<label>
|
||||
Rack:
|
||||
<input type="number" id="plc-rack" value="{{ status.plc_config.rack }}" min="0" max="7">
|
||||
</label>
|
||||
<label>
|
||||
Slot:
|
||||
<input type="number" id="plc-slot" value="{{ status.plc_config.slot }}" min="0" max="31">
|
||||
</label>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<button type="submit">💾 Save Configuration</button>
|
||||
<button type="button" id="connect-btn">🔗 Connect PLC</button>
|
||||
<button type="button" id="disconnect-btn" class="secondary">❌ Disconnect PLC</button>
|
||||
</div>
|
||||
</form>
|
||||
</article>
|
||||
|
||||
<!-- UDP Configuration -->
|
||||
<article>
|
||||
<header>🌐 UDP Gateway Configuration (PlotJuggler)</header>
|
||||
<form id="udp-config-form">
|
||||
<div class="form-row">
|
||||
<label>
|
||||
UDP Host:
|
||||
<input type="text" id="udp-host" value="{{ status.udp_config.host }}"
|
||||
placeholder="127.0.0.1">
|
||||
</label>
|
||||
<label>
|
||||
UDP Port:
|
||||
<input type="number" id="udp-port" value="{{ status.udp_config.port }}" min="1" max="65535">
|
||||
</label>
|
||||
<label>
|
||||
Sampling Interval (s):
|
||||
<input type="number" id="sampling-interval" value="{{ status.sampling_interval }}"
|
||||
min="0.01" max="10" step="0.01">
|
||||
</label>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<button type="submit">💾 Save Configuration</button>
|
||||
<button type="button" id="update-sampling-btn" class="secondary">⏱️ Update Interval</button>
|
||||
</div>
|
||||
</form>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<!-- Tab Navigation -->
|
||||
<nav class="tabs">
|
||||
<button class="tab-btn active" data-tab="datasets">📊 Datasets & Variables</button>
|
||||
<button class="tab-btn" data-tab="plotting">📈 Real-Time Plotting</button>
|
||||
<button class="tab-btn" data-tab="config-editor">🧩 Config Editor</button>
|
||||
<button class="tab-btn" data-tab="events">📋 Events & Logs</button>
|
||||
</nav>
|
||||
|
||||
<!-- Tab Content -->
|
||||
<div class="tab-content active" id="datasets-tab">
|
||||
<!-- Integrated Dataset & Variables Management -->
|
||||
<article>
|
||||
<header>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>📊 Dataset & Variables Management</span>
|
||||
<div style="display: flex; gap: 0.5rem; align-items: center;">
|
||||
<select id="dataset-selector" style="min-width: 200px;">
|
||||
<option value="">Select a dataset...</option>
|
||||
</select>
|
||||
<button type="button" id="new-dataset-btn" class="outline">➕ New</button>
|
||||
<button type="button" id="delete-dataset-btn" class="secondary">🗑️ Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Dataset Status Bar -->
|
||||
<div id="dataset-status-bar" style="display: none; margin-bottom: 1rem;">
|
||||
<div class="info-section" style="margin-bottom: 0;">
|
||||
<div
|
||||
style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem;">
|
||||
<div style="display: flex; gap: 2rem; flex-wrap: wrap;">
|
||||
<span><strong>📂 Name:</strong> <span id="dataset-name"></span></span>
|
||||
<span><strong>🏷️ Prefix:</strong> <span id="dataset-prefix"></span></span>
|
||||
<span><strong>⏱️ Sampling:</strong> <span id="dataset-sampling"></span></span>
|
||||
<span><strong>📊 Variables:</strong> <span id="dataset-var-count"></span></span>
|
||||
<span><strong>📡 Streaming:</strong> <span id="dataset-stream-count"></span></span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.5rem; align-items: center;">
|
||||
<span id="dataset-status" class="status-item"></span>
|
||||
<button type="button" id="activate-dataset-btn" class="outline">▶️ Activate</button>
|
||||
<button type="button" id="deactivate-dataset-btn" class="secondary">⏹️
|
||||
Deactivate</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Variables Management Section -->
|
||||
<div id="variables-management" style="display: none;">
|
||||
<!-- Real-time Variable Monitoring Info -->
|
||||
<div
|
||||
style="margin-bottom: 1rem; padding: 1rem; background: var(--pico-card-background-color); border-radius: var(--pico-border-radius); border: var(--pico-border-width) solid var(--pico-border-color);">
|
||||
<div
|
||||
style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem;">
|
||||
<div>
|
||||
<strong>📊 Automatic Variable Monitoring</strong>
|
||||
<br>
|
||||
<small style="color: var(--pico-muted-color);">
|
||||
Variables are automatically monitored and recorded when PLC is connected and dataset
|
||||
is
|
||||
active
|
||||
</small>
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.5rem; align-items: center;">
|
||||
<button type="button" id="toggle-streaming-btn" class="outline"
|
||||
onclick="toggleRealTimeStreaming()">
|
||||
▶️ Start Live Display
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="last-refresh-time"
|
||||
style="margin-top: 0.5rem; font-size: 0.9em; color: var(--pico-muted-color);">
|
||||
Live display shows cached values from automatic monitoring
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Variable Form -->
|
||||
<form id="variable-form" style="margin-bottom: 1.5rem;">
|
||||
<div class="form-row">
|
||||
<label>
|
||||
Variable Name:
|
||||
<input type="text" id="var-name" placeholder="temperature" required>
|
||||
</label>
|
||||
<label>
|
||||
Memory Area:
|
||||
<select id="var-area" required onchange="toggleFields()">
|
||||
<option value="db">DB (Data Block)</option>
|
||||
<option value="mw">MW (Memory Words)</option>
|
||||
<option value="pew">PEW (Process Input Words)</option>
|
||||
<option value="paw">PAW (Process Output Words)</option>
|
||||
<option value="e">E (Input Bits)</option>
|
||||
<option value="a">A (Output Bits)</option>
|
||||
<option value="mb">MB (Memory Bits)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label id="db-field">
|
||||
Data Block (DB):
|
||||
<input type="number" id="var-db" min="1" max="9999" value="1" required>
|
||||
</label>
|
||||
<label>
|
||||
Offset:
|
||||
<input type="number" id="var-offset" min="0" max="8192" value="0" required>
|
||||
</label>
|
||||
<label id="bit-field" style="display: none;">
|
||||
Bit Position:
|
||||
<select id="var-bit">
|
||||
<option value="0">0</option>
|
||||
<option value="1">1</option>
|
||||
<option value="2">2</option>
|
||||
<option value="3">3</option>
|
||||
<option value="4">4</option>
|
||||
<option value="5">5</option>
|
||||
<option value="6">6</option>
|
||||
<option value="7">7</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Data Type:
|
||||
<select id="var-type" required>
|
||||
<option value="real">REAL (Float 32-bit)</option>
|
||||
<option value="int">INT (16-bit Signed)</option>
|
||||
<option value="uint">UINT (16-bit Unsigned)</option>
|
||||
<option value="dint">DINT (32-bit Signed)</option>
|
||||
<option value="udint">UDINT (32-bit Unsigned)</option>
|
||||
<option value="word">WORD (16-bit)</option>
|
||||
<option value="byte">BYTE (8-bit)</option>
|
||||
<option value="sint">SINT (8-bit Signed)</option>
|
||||
<option value="usint">USINT (8-bit Unsigned)</option>
|
||||
<option value="bool">BOOL</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit">➕ Add Variable</button>
|
||||
</form>
|
||||
|
||||
<!-- Variables Table -->
|
||||
<div
|
||||
style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
||||
<h4 style="margin: 0;">📊 Variables in Dataset</h4>
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;">
|
||||
<div style="margin-left: 1rem;">
|
||||
<span id="last-refresh-time"
|
||||
style="color: var(--pico-muted-color); font-size: 0.9rem;"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<table class="variables-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Memory Area</th>
|
||||
<th>Offset</th>
|
||||
<th>Type</th>
|
||||
<th>Current Value</th>
|
||||
<th>Stream to PlotJuggler</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="variables-tbody">
|
||||
{% for name, var in variables.items() %}
|
||||
<tr>
|
||||
<td>{{ name }}</td>
|
||||
<td>
|
||||
{% if var.area == 'db' or var.get('db') %}
|
||||
DB{{ var.get('db', 'N/A') }}.{{ var.offset }}
|
||||
{% elif var.area == 'mw' or var.area == 'm' %}
|
||||
MW{{ var.offset }}
|
||||
{% elif var.area == 'pew' or var.area == 'pe' %}
|
||||
PEW{{ var.offset }}
|
||||
{% elif var.area == 'paw' or var.area == 'pa' %}
|
||||
PAW{{ var.offset }}
|
||||
{% elif var.area == 'e' %}
|
||||
E{{ var.offset }}.{{ var.bit }}
|
||||
{% elif var.area == 'a' %}
|
||||
A{{ var.offset }}.{{ var.bit }}
|
||||
{% elif var.area == 'mb' %}
|
||||
M{{ var.offset }}.{{ var.bit }}
|
||||
{% else %}
|
||||
DB{{ var.get('db', 'N/A') }}.{{ var.offset }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ var.offset }}</td>
|
||||
<td>{{ var.type.upper() }}</td>
|
||||
<td id="value-{{ name }}"
|
||||
style="font-family: monospace; font-weight: bold; color: var(--pico-muted-color);">
|
||||
--
|
||||
</td>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" id="stream-{{ name }}" role="switch"
|
||||
onchange="toggleStreaming('{{ name }}', this.checked)">
|
||||
Enable
|
||||
</label>
|
||||
</td>
|
||||
<td>
|
||||
<button class="outline" onclick="editVariable('{{ name }}')">✏️ Edit</button>
|
||||
<button class="secondary" onclick="removeVariable('{{ name }}')">🗑️ Remove</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- No Dataset Selected Message -->
|
||||
<div id="no-dataset-message" style="text-align: center; padding: 2rem; color: var(--pico-muted-color);">
|
||||
<p>📊 Please select a dataset to manage its variables</p>
|
||||
<p>Or create a new dataset to get started</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<!-- PlotJuggler Streaming Control -->
|
||||
<article>
|
||||
<header>📡 PlotJuggler UDP Streaming Control</header>
|
||||
<div class="info-section">
|
||||
<p><strong>📡 UDP Streaming:</strong> Sends only variables marked for streaming to PlotJuggler via
|
||||
UDP
|
||||
</p>
|
||||
<p><strong>💾 Automatic Recording:</strong> When PLC is connected, all datasets with variables
|
||||
automatically record to CSV files</p>
|
||||
<p><strong>📁 File Organization:</strong> records/[dd-mm-yyyy]/[prefix]_[hour].csv (e.g.,
|
||||
temp_14.csv,
|
||||
pressure_14.csv)</p>
|
||||
<p><strong>⏱️ Independent Operation:</strong> CSV recording works independently of UDP streaming -
|
||||
always active when PLC is connected</p>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<button id="start-streaming-btn">📡 Start UDP Streaming</button>
|
||||
<button class="secondary" id="stop-streaming-btn">⏹️ Stop UDP Streaming</button>
|
||||
<button class="outline" onclick="location.reload()">🔄 Refresh Status</button>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<!-- CSV Recording Configuration -->
|
||||
<article>
|
||||
<header>📁 CSV Recording Configuration</header>
|
||||
<div class="info-section">
|
||||
<p><strong>📂 Directory Management:</strong> Configure where CSV files are saved and manage file
|
||||
rotation</p>
|
||||
<p><strong>🔄 Automatic Cleanup:</strong> Set limits by size, days, or hours to automatically delete
|
||||
old
|
||||
files</p>
|
||||
<p><strong>💿 Disk Space:</strong> Monitor available space and estimated recording time remaining
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Current Configuration Display -->
|
||||
<div class="csv-config-display" id="csv-config-display">
|
||||
<div class="config-grid">
|
||||
<div class="config-item">
|
||||
<strong>📁 Records Directory:</strong>
|
||||
<span id="csv-directory-path">Loading...</span>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<strong>🔄 Rotation Enabled:</strong>
|
||||
<span id="csv-rotation-enabled">Loading...</span>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<strong>📊 Max Size:</strong>
|
||||
<span id="csv-max-size">Loading...</span>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<strong>📅 Max Days:</strong>
|
||||
<span id="csv-max-days">Loading...</span>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<strong>⏰ Max Hours:</strong>
|
||||
<span id="csv-max-hours">Loading...</span>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<strong>🧹 Cleanup Interval:</strong>
|
||||
<span id="csv-cleanup-interval">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Directory Information -->
|
||||
<div class="csv-directory-info" id="csv-directory-info" style="margin-top: 1rem;">
|
||||
<details>
|
||||
<summary><strong>📊 Directory Information</strong></summary>
|
||||
<div class="directory-stats" id="directory-stats">
|
||||
<p>Loading directory information...</p>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<!-- Configuration Form -->
|
||||
<details style="margin-top: 1rem;">
|
||||
<summary><strong>⚙️ Modify Configuration</strong></summary>
|
||||
<form id="csv-config-form" style="margin-top: 1rem;">
|
||||
<div class="grid">
|
||||
<div>
|
||||
<label for="records-directory">📁 Records Directory:</label>
|
||||
<input type="text" id="records-directory" name="records_directory" placeholder="records"
|
||||
required>
|
||||
<small>Base directory where CSV files will be saved</small>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
<input type="checkbox" id="rotation-enabled" name="rotation_enabled">
|
||||
🔄 Enable File Rotation
|
||||
</label>
|
||||
<small>Automatically delete old files based on limits below</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div>
|
||||
<label for="max-size-mb">📊 Max Total Size (MB):</label>
|
||||
<input type="number" id="max-size-mb" name="max_size_mb" min="1" step="1"
|
||||
placeholder="1000">
|
||||
<small>Maximum total size of all CSV files in MB (leave empty for no limit)</small>
|
||||
</div>
|
||||
<div>
|
||||
<label for="max-days">📅 Max Days to Keep:</label>
|
||||
<input type="number" id="max-days" name="max_days" min="1" step="1" placeholder="30">
|
||||
<small>Delete files older than this many days (leave empty for no limit)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div>
|
||||
<label for="max-hours">⏰ Max Hours to Keep:</label>
|
||||
<input type="number" id="max-hours" name="max_hours" min="1" step="1" placeholder="">
|
||||
<small>Delete files older than this many hours (overrides days setting)</small>
|
||||
</div>
|
||||
<div>
|
||||
<label for="cleanup-interval">🧹 Cleanup Interval (hours):</label>
|
||||
<input type="number" id="cleanup-interval" name="cleanup_interval_hours" min="1"
|
||||
step="1" value="24" required>
|
||||
<small>How often to run automatic cleanup</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button type="submit">💾 Save Configuration</button>
|
||||
<button type="button" class="secondary" onclick="loadCsvConfig()">🔄 Reload</button>
|
||||
<button type="button" class="outline" onclick="triggerManualCleanup()">🧹 Manual
|
||||
Cleanup</button>
|
||||
</div>
|
||||
</form>
|
||||
</details>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<!-- 🧩 CONFIG EDITOR TAB -->
|
||||
<div class="tab-content" id="config-editor-tab">
|
||||
<article>
|
||||
<header>🧩 Dynamic JSON Config Editor</header>
|
||||
<div class="info-section">
|
||||
<p><strong>Esquemas:</strong> Selecciona un esquema y edita PLC, Datasets o Plot Sessions con
|
||||
formularios dinámicos.</p>
|
||||
<p><strong>Import/Export:</strong> Puedes importar desde un archivo JSON o exportar el actual.</p>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div>
|
||||
<label>Schema</label>
|
||||
<select id="schema-selector"></select>
|
||||
</div>
|
||||
<div class="controls" style="align-self: end; display: flex; gap: .5rem;">
|
||||
<button id="btn-export-config" class="outline">⬇️ Export</button>
|
||||
<label class="outline" style="margin: 0;">
|
||||
⬆️ Import <input type="file" id="import-file" accept="application/json"
|
||||
style="display:none;">
|
||||
</label>
|
||||
<button id="btn-save-config">💾 Save</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="config-form-container" style="margin-top: 1rem;"></div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<!-- 📈 PLOTTING TAB -->
|
||||
<div class="tab-content" id="plotting-tab">
|
||||
<article>
|
||||
<header>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>📈 Real-Time Plotting</span>
|
||||
<button type="button" id="toggle-plot-form-btn" class="outline">➕ New Plot</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
|
||||
<!-- Plot Creation/Edit Form (Collapsible) -->
|
||||
<div id="plot-form-container" class="collapsible-section" style="display: none;">
|
||||
<article class="plot-form-article">
|
||||
<header>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span id="plot-form-title">🆕 Create New Plot</span>
|
||||
<button type="button" id="close-plot-form-btn" class="close-btn">×</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<form id="plot-form">
|
||||
<div class="form-group">
|
||||
<label for="plot-form-name">Plot Name:</label>
|
||||
<input type="text" id="plot-form-name" placeholder="Temperature Monitoring" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Variables to Plot:</label>
|
||||
<div class="variables-selection">
|
||||
<div id="selected-variables-display" class="selected-variables">
|
||||
<p class="no-variables">No variables selected</p>
|
||||
</div>
|
||||
<button type="button" id="select-variables-btn" class="outline">
|
||||
🎨 Select Variables & Colors
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="plot-form-time-window">Time Window (seconds):</label>
|
||||
<input type="number" id="plot-form-time-window" value="60" min="10" max="3600" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Y-Axis Range (optional):</label>
|
||||
<div class="range-inputs">
|
||||
<input type="number" id="plot-form-y-min" placeholder="Auto Min" step="any">
|
||||
<span>to</span>
|
||||
<input type="number" id="plot-form-y-max" placeholder="Auto Max" step="any">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" id="plot-form-trigger-enabled"
|
||||
onchange="togglePlotFormTriggerConfig()">
|
||||
Enable Trigger System
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="plot-form-trigger-config" style="display: none;">
|
||||
<div class="form-group">
|
||||
<label for="plot-form-trigger-variable">Trigger Variable:</label>
|
||||
<select id="plot-form-trigger-variable">
|
||||
<option value="">No trigger</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" id="plot-form-trigger-on-true" checked>
|
||||
Trigger on True (uncheck for False)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary" id="plot-form-submit">Create Plot</button>
|
||||
<button type="button" class="btn btn-secondary" id="cancel-plot-form">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<!-- Sub-tabs para plots -->
|
||||
<nav class="sub-tabs" id="plot-sub-tabs" style="display: none;">
|
||||
<!-- Los sub-tabs se crearán dinámicamente aquí -->
|
||||
</nav>
|
||||
|
||||
<!-- Contenido de sub-tabs -->
|
||||
<div id="plot-sub-content">
|
||||
<!-- Contenido de plots se mostrará aquí -->
|
||||
</div>
|
||||
|
||||
<!-- Plot Sessions Container (vista inicial) -->
|
||||
<div id="plot-sessions-container">
|
||||
<div style="text-align: center; padding: 2rem; color: var(--pico-muted-color);">
|
||||
<p>📈 No plot sessions created yet</p>
|
||||
<p>Click "New Plot" to create your first real-time chart</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<!-- 📋 EVENTS TAB -->
|
||||
<div class="tab-content" id="events-tab">
|
||||
<article>
|
||||
<header>📋 Events & System Logs</header>
|
||||
<div class="info-section">
|
||||
<p><strong>📋 Event Logging:</strong> Monitor system events, errors, and operational status</p>
|
||||
<p><strong>🔍 Real-time Updates:</strong> Events are automatically updated as they occur</p>
|
||||
<p><strong>📊 Filtering:</strong> Filter events by type and time range</p>
|
||||
</div>
|
||||
|
||||
<div class="log-controls">
|
||||
<button id="refresh-events-btn">🔄 Refresh Events</button>
|
||||
<button id="clear-events-btn" class="secondary">🗑️ Clear Events</button>
|
||||
<div class="log-stats">
|
||||
<span id="events-count">Loading...</span> events
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="log-container" id="events-container">
|
||||
<p>Loading events...</p>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- Edit Variable Modal -->
|
||||
<div id="edit-modal" class="modal" style="display: none;">
|
||||
<article>
|
||||
<header>
|
||||
<h3>✏️ Edit Variable</h3>
|
||||
<button class="close" onclick="closeEditModal()" aria-label="Close">×</button>
|
||||
</header>
|
||||
<form id="edit-variable-form">
|
||||
<div class="form-row">
|
||||
<label>
|
||||
Variable Name:
|
||||
<input type="text" id="edit-var-name" required>
|
||||
</label>
|
||||
<label>
|
||||
Memory Area:
|
||||
<select id="edit-var-area" required onchange="toggleEditFields()">
|
||||
<option value="db">DB (Data Block)</option>
|
||||
<option value="mw">MW (Memory Words)</option>
|
||||
<option value="pew">PEW (Process Input Words)</option>
|
||||
<option value="paw">PAW (Process Output Words)</option>
|
||||
<option value="e">E (Input Bits)</option>
|
||||
<option value="a">A (Output Bits)</option>
|
||||
<option value="mb">MB (Memory Bits)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label id="edit-db-field">
|
||||
Data Block (DB):
|
||||
<input type="number" id="edit-var-db" min="1" max="9999" value="1" required>
|
||||
</label>
|
||||
<label>
|
||||
Offset:
|
||||
<input type="number" id="edit-var-offset" min="0" max="8192" value="0" required>
|
||||
</label>
|
||||
<label id="edit-bit-field" style="display: none;">
|
||||
Bit Position:
|
||||
<select id="edit-var-bit">
|
||||
<option value="0">0</option>
|
||||
<option value="1">1</option>
|
||||
<option value="2">2</option>
|
||||
<option value="3">3</option>
|
||||
<option value="4">4</option>
|
||||
<option value="5">5</option>
|
||||
<option value="6">6</option>
|
||||
<option value="7">7</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Data Type:
|
||||
<select id="edit-var-type" required>
|
||||
<option value="real">REAL (Float 32-bit)</option>
|
||||
<option value="int">INT (16-bit Signed)</option>
|
||||
<option value="uint">UINT (16-bit Unsigned)</option>
|
||||
<option value="dint">DINT (32-bit Signed)</option>
|
||||
<option value="udint">UDINT (32-bit Unsigned)</option>
|
||||
<option value="word">WORD (16-bit)</option>
|
||||
<option value="byte">BYTE (8-bit)</option>
|
||||
<option value="sint">SINT (8-bit Signed)</option>
|
||||
<option value="usint">USINT (8-bit Unsigned)</option>
|
||||
<option value="bool">BOOL (Boolean)</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<footer>
|
||||
<button type="button" class="secondary" onclick="closeEditModal()">❌ Cancel</button>
|
||||
<button type="submit">💾 Update Variable</button>
|
||||
</footer>
|
||||
</form>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Variable Selection Modal (NEW) -->
|
||||
<div id="variable-selection-modal" class="modal" style="display: none;">
|
||||
<div class="modal-content variable-modal">
|
||||
<header class="modal-header">
|
||||
<h3>🎨 Select Variables & Colors</h3>
|
||||
<button type="button" class="close-btn" id="close-variable-modal">×</button>
|
||||
</header>
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="variable-selection-container">
|
||||
<div class="datasets-sidebar">
|
||||
<h4>📊 Datasets</h4>
|
||||
<div id="datasets-list" class="datasets-list">
|
||||
<p>Loading datasets...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="variables-main">
|
||||
<div class="variables-header">
|
||||
<h4>🔧 Variables</h4>
|
||||
<div class="selection-controls">
|
||||
<button type="button" id="select-all-variables" class="btn btn-sm outline">Select
|
||||
All</button>
|
||||
<button type="button" id="deselect-all-variables" class="btn btn-sm outline">Deselect
|
||||
All</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="variables-list" class="variables-list">
|
||||
<p class="no-dataset-message">Select a dataset to see its variables</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="selected-summary">
|
||||
<h4>📝 Selected Variables Summary</h4>
|
||||
<div id="selected-variables-summary" class="selected-summary-list">
|
||||
<p>No variables selected</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" id="cancel-variable-selection">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="confirm-variable-selection">Confirm Selection</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New Dataset Modal -->
|
||||
<div id="dataset-modal" class="modal" style="display: none;">
|
||||
<article>
|
||||
<header>
|
||||
<h3>Create New Dataset</h3>
|
||||
<button class="close" id="close-dataset-modal" aria-label="Close">×</button>
|
||||
</header>
|
||||
<form id="dataset-form">
|
||||
<label>
|
||||
Dataset ID:
|
||||
<input type="text" id="dataset-id" placeholder="e.g., temperature_sensors" required>
|
||||
<small>Used internally, no spaces or special characters</small>
|
||||
</label>
|
||||
<label>
|
||||
Dataset Name:
|
||||
<input type="text" id="dataset-name-input" placeholder="e.g., Temperature Sensors" required>
|
||||
</label>
|
||||
<label>
|
||||
CSV Prefix:
|
||||
<input type="text" id="dataset-prefix-input" placeholder="e.g., temp" required>
|
||||
<small>Files will be named: prefix_hour.csv (e.g., temp_14.csv)</small>
|
||||
</label>
|
||||
<label>
|
||||
Sampling Interval (seconds):
|
||||
<input type="number" id="dataset-sampling-input" placeholder="Leave empty to use global interval"
|
||||
min="0.01" step="0.01">
|
||||
<small>Leave empty to use global sampling interval</small>
|
||||
</label>
|
||||
<footer>
|
||||
<button type="button" class="secondary" id="cancel-dataset-btn">Cancel</button>
|
||||
<button type="submit">Create Dataset</button>
|
||||
</footer>
|
||||
</form>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<!-- Chart.js Libraries - Cargar en orden estricto (versiones compatibles) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/luxon@2"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-luxon@1.3.1"></script>
|
||||
<script src="https://unpkg.com/chartjs-plugin-zoom@1.2.1/dist/chartjs-plugin-zoom.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-streaming@2.0.0"></script>
|
||||
<!-- JSONEditor (tree view) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/jsoneditor@9.10.2/dist/jsoneditor.min.js"></script>
|
||||
<!-- JSONForm deps (jQuery + Underscore) and JSONForm -->
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.13.6/underscore-min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/gh/jsonform/jsonform@2.5.1/lib/deps/opt/jsv.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/gh/jsonform/jsonform@2.5.1/lib/jsonform.js"></script>
|
||||
|
||||
<!-- JavaScript Modules -->
|
||||
<script src="/static/js/utils.js"></script>
|
||||
<script src="/static/js/theme.js"></script>
|
||||
<script src="/static/js/status.js"></script>
|
||||
<script src="/static/js/plc.js"></script>
|
||||
<script src="/static/js/variables.js"></script>
|
||||
<script src="/static/js/datasets.js"></script>
|
||||
<script src="/static/js/streaming.js"></script>
|
||||
<script src="/static/js/csv.js"></script>
|
||||
<script src="/static/js/events.js"></script>
|
||||
<script src="/static/js/tabs.js"></script>
|
||||
<script src="/static/js/plotting.js"></script>
|
||||
<script src="/static/js/config_editor.js"></script>
|
||||
<script src="/static/js/main.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
File diff suppressed because one or more lines are too long
Binary file not shown.
Before Width: | Height: | Size: 81 KiB |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,378 +0,0 @@
|
|||
/**
|
||||
* 📈 Chart.js Plugin Streaming Integration
|
||||
* Integra chartjs-plugin-streaming para plotting en tiempo real
|
||||
*
|
||||
* Combinación de módulos:
|
||||
* - helpers.streaming.js
|
||||
* - scale.realtime.js
|
||||
* - plugin.streaming.js
|
||||
* - plugin.zoom.js (integración con zoom)
|
||||
*/
|
||||
|
||||
(function (global, factory) {
|
||||
if (typeof exports === 'object' && typeof module !== 'undefined') {
|
||||
factory(exports, require('chart.js'));
|
||||
} else if (typeof define === 'function' && define.amd) {
|
||||
define(['exports', 'chart.js'], factory);
|
||||
} else {
|
||||
global = global || self;
|
||||
factory(global.ChartStreaming = {}, global.Chart);
|
||||
}
|
||||
})(this, function (exports, Chart) {
|
||||
'use strict';
|
||||
|
||||
// ============= HELPERS.STREAMING.JS =============
|
||||
function clamp(value, min, max) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
function resolveOption(scale, key) {
|
||||
const realtimeOptions = scale.options.realtime || {};
|
||||
const scaleOptions = scale.options;
|
||||
|
||||
if (realtimeOptions[key] !== undefined) {
|
||||
return realtimeOptions[key];
|
||||
}
|
||||
if (scaleOptions[key] !== undefined) {
|
||||
return scaleOptions[key];
|
||||
}
|
||||
|
||||
// Valores por defecto
|
||||
const defaults = {
|
||||
duration: 10000,
|
||||
delay: 0,
|
||||
refresh: 1000,
|
||||
frameRate: 30,
|
||||
pause: false,
|
||||
ttl: undefined,
|
||||
onRefresh: null
|
||||
};
|
||||
|
||||
return defaults[key];
|
||||
}
|
||||
|
||||
function getAxisMap(element, keys, meta) {
|
||||
const axis = meta.vAxisID || 'y';
|
||||
return keys[axis] || [];
|
||||
}
|
||||
|
||||
// ============= SCALE.REALTIME.JS (Simplificado) =============
|
||||
class RealTimeScale extends Chart.Scale {
|
||||
constructor(cfg) {
|
||||
super(cfg);
|
||||
this.type = 'realtime';
|
||||
}
|
||||
|
||||
init(scaleOptions, scaleContext) {
|
||||
super.init(scaleOptions, scaleContext);
|
||||
|
||||
const me = this;
|
||||
const chart = me.chart;
|
||||
const streaming = chart.$streaming = chart.$streaming || {};
|
||||
|
||||
// Inicializar opciones de tiempo real
|
||||
const realtimeOptions = me.options.realtime || {};
|
||||
me.realtime = {
|
||||
duration: resolveOption(me, 'duration'),
|
||||
delay: resolveOption(me, 'delay'),
|
||||
refresh: resolveOption(me, 'refresh'),
|
||||
frameRate: resolveOption(me, 'frameRate'),
|
||||
pause: resolveOption(me, 'pause'),
|
||||
ttl: resolveOption(me, 'ttl'),
|
||||
onRefresh: resolveOption(me, 'onRefresh')
|
||||
};
|
||||
|
||||
// Configurar intervalo de actualización
|
||||
if (!streaming.intervalId && me.realtime.refresh > 0) {
|
||||
streaming.intervalId = setInterval(() => {
|
||||
if (!me.realtime.pause && typeof me.realtime.onRefresh === 'function') {
|
||||
me.realtime.onRefresh(chart);
|
||||
}
|
||||
me.update();
|
||||
chart.update('quiet');
|
||||
}, me.realtime.refresh);
|
||||
}
|
||||
}
|
||||
|
||||
update(args) {
|
||||
const me = this;
|
||||
const chart = me.chart;
|
||||
|
||||
if (!chart.data || !chart.data.datasets) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const duration = me.realtime.duration;
|
||||
const delay = me.realtime.delay;
|
||||
const ttl = me.realtime.ttl;
|
||||
|
||||
// Calcular ventana de tiempo
|
||||
me.max = now - delay;
|
||||
me.min = me.max - duration;
|
||||
|
||||
// Limpiar datos antiguos si TTL está configurado
|
||||
if (ttl) {
|
||||
const cutoff = now - ttl;
|
||||
chart.data.datasets.forEach(dataset => {
|
||||
if (dataset.data) {
|
||||
dataset.data = dataset.data.filter(point => {
|
||||
return point.x > cutoff;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
super.update(args);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
const me = this;
|
||||
const chart = me.chart;
|
||||
const streaming = chart.$streaming;
|
||||
|
||||
if (streaming && streaming.intervalId) {
|
||||
clearInterval(streaming.intervalId);
|
||||
delete streaming.intervalId;
|
||||
}
|
||||
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
static id = 'realtime';
|
||||
static defaults = {
|
||||
realtime: {
|
||||
duration: 10000,
|
||||
delay: 0,
|
||||
refresh: 1000,
|
||||
frameRate: 30,
|
||||
pause: false,
|
||||
ttl: undefined,
|
||||
onRefresh: null
|
||||
},
|
||||
time: {
|
||||
unit: 'second',
|
||||
displayFormats: {
|
||||
second: 'HH:mm:ss'
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ============= PLUGIN.STREAMING.JS (Simplificado) =============
|
||||
const streamingPlugin = {
|
||||
id: 'streaming',
|
||||
|
||||
beforeInit(chart) {
|
||||
const streaming = chart.$streaming = chart.$streaming || {};
|
||||
streaming.enabled = false;
|
||||
|
||||
// Detectar si hay escalas realtime
|
||||
const scales = chart.options.scales || {};
|
||||
Object.keys(scales).forEach(scaleId => {
|
||||
if (scales[scaleId].type === 'realtime') {
|
||||
streaming.enabled = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
afterInit(chart) {
|
||||
const streaming = chart.$streaming;
|
||||
if (streaming && streaming.enabled) {
|
||||
// Configurar actualización automática
|
||||
const update = chart.update;
|
||||
chart.update = function (mode) {
|
||||
if (mode === 'quiet') {
|
||||
// Actualización silenciosa para streaming
|
||||
Chart.prototype.update.call(this, mode);
|
||||
} else {
|
||||
update.call(this, mode);
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
beforeUpdate(chart) {
|
||||
const streaming = chart.$streaming;
|
||||
if (!streaming || !streaming.enabled) return;
|
||||
|
||||
// Permitir que las líneas Bézier se extiendan fuera del área del gráfico
|
||||
const elements = chart.options.elements || {};
|
||||
if (elements.line) {
|
||||
elements.line.capBezierPoints = false;
|
||||
}
|
||||
},
|
||||
|
||||
destroy(chart) {
|
||||
const streaming = chart.$streaming;
|
||||
if (streaming && streaming.intervalId) {
|
||||
clearInterval(streaming.intervalId);
|
||||
delete streaming.intervalId;
|
||||
}
|
||||
delete chart.$streaming;
|
||||
}
|
||||
};
|
||||
|
||||
// ============= REGISTRO DE COMPONENTES =============
|
||||
|
||||
// Registrar escala realtime
|
||||
Chart.register(RealTimeScale);
|
||||
|
||||
// Registrar plugin de streaming
|
||||
Chart.register(streamingPlugin);
|
||||
|
||||
// ============= UTILIDADES PARA LA APLICACIÓN =============
|
||||
|
||||
/**
|
||||
* Crea una configuración de Chart.js optimizada para streaming
|
||||
*/
|
||||
function createStreamingChartConfig(options = {}) {
|
||||
const config = {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: []
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: false, // Desactivar animaciones para mejor performance
|
||||
|
||||
scales: {
|
||||
x: {
|
||||
type: 'realtime',
|
||||
realtime: {
|
||||
duration: options.duration || 60000, // 60 segundos por defecto
|
||||
delay: options.delay || 0,
|
||||
refresh: options.refresh || 1000, // 1 segundo
|
||||
frameRate: options.frameRate || 30,
|
||||
pause: options.pause || false,
|
||||
ttl: options.ttl || undefined,
|
||||
onRefresh: options.onRefresh || null
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Tiempo'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Valor'
|
||||
},
|
||||
min: options.yMin,
|
||||
max: options.yMax
|
||||
}
|
||||
},
|
||||
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'top'
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
}
|
||||
},
|
||||
|
||||
interaction: {
|
||||
mode: 'nearest',
|
||||
axis: 'x',
|
||||
intersect: false
|
||||
},
|
||||
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0, // Sin puntos para mejor performance
|
||||
hoverRadius: 3
|
||||
},
|
||||
line: {
|
||||
tension: 0.1,
|
||||
borderWidth: 2
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: ['streaming']
|
||||
};
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Agrega datos a un dataset de streaming
|
||||
*/
|
||||
function addStreamingData(chart, datasetIndex, data) {
|
||||
if (!chart || !chart.data || !chart.data.datasets[datasetIndex]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dataset = chart.data.datasets[datasetIndex];
|
||||
if (!dataset.data) {
|
||||
dataset.data = [];
|
||||
}
|
||||
|
||||
// Agregar nuevo punto con timestamp
|
||||
const timestamp = data.x || Date.now();
|
||||
dataset.data.push({
|
||||
x: timestamp,
|
||||
y: data.y
|
||||
});
|
||||
|
||||
// Chart.js se encarga automáticamente de eliminar datos antiguos
|
||||
// basado en la configuración de TTL y duration de la escala realtime
|
||||
}
|
||||
|
||||
/**
|
||||
* Controla la pausa/reanudación del streaming
|
||||
*/
|
||||
function setStreamingPause(chart, paused) {
|
||||
if (!chart || !chart.$streaming) return;
|
||||
|
||||
const scales = chart.scales;
|
||||
Object.keys(scales).forEach(scaleId => {
|
||||
const scale = scales[scaleId];
|
||||
if (scale instanceof RealTimeScale) {
|
||||
scale.realtime.pause = paused;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpia todos los datos de streaming
|
||||
*/
|
||||
function clearStreamingData(chart) {
|
||||
if (!chart || !chart.data) return;
|
||||
|
||||
chart.data.datasets.forEach(dataset => {
|
||||
if (dataset.data) {
|
||||
dataset.data.length = 0;
|
||||
}
|
||||
});
|
||||
|
||||
chart.update('quiet');
|
||||
}
|
||||
|
||||
// ============= EXPORTS =============
|
||||
|
||||
// Exportar para uso en la aplicación
|
||||
exports.RealTimeScale = RealTimeScale;
|
||||
exports.streamingPlugin = streamingPlugin;
|
||||
exports.createStreamingChartConfig = createStreamingChartConfig;
|
||||
exports.addStreamingData = addStreamingData;
|
||||
exports.setStreamingPause = setStreamingPause;
|
||||
exports.clearStreamingData = clearStreamingData;
|
||||
|
||||
// Hacer disponible globalmente
|
||||
if (typeof window !== 'undefined') {
|
||||
window.ChartStreaming = {
|
||||
createStreamingChartConfig,
|
||||
addStreamingData,
|
||||
setStreamingPause,
|
||||
clearStreamingData,
|
||||
RealTimeScale,
|
||||
streamingPlugin
|
||||
};
|
||||
}
|
||||
|
||||
console.log('📈 Chart.js Streaming Plugin loaded successfully');
|
||||
});
|
|
@ -1,170 +0,0 @@
|
|||
/**
|
||||
* Gestión de la configuración CSV y operaciones relacionadas
|
||||
*/
|
||||
|
||||
// Cargar configuración CSV
|
||||
function loadCsvConfig() {
|
||||
fetch('/api/csv/config')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const config = data.config;
|
||||
|
||||
// Actualizar elementos de visualización
|
||||
document.getElementById('csv-directory-path').textContent = config.current_directory || 'N/A';
|
||||
document.getElementById('csv-rotation-enabled').textContent = config.rotation_enabled ? '✅ Yes' : '❌ No';
|
||||
document.getElementById('csv-max-size').textContent = config.max_size_mb ? `${config.max_size_mb} MB` : 'No limit';
|
||||
document.getElementById('csv-max-days').textContent = config.max_days ? `${config.max_days} days` : 'No limit';
|
||||
document.getElementById('csv-max-hours').textContent = config.max_hours ? `${config.max_hours} hours` : 'No limit';
|
||||
document.getElementById('csv-cleanup-interval').textContent = `${config.cleanup_interval_hours} hours`;
|
||||
|
||||
// Actualizar campos del formulario
|
||||
document.getElementById('records-directory').value = config.records_directory || '';
|
||||
document.getElementById('rotation-enabled').checked = config.rotation_enabled || false;
|
||||
document.getElementById('max-size-mb').value = config.max_size_mb || '';
|
||||
document.getElementById('max-days').value = config.max_days || '';
|
||||
document.getElementById('max-hours').value = config.max_hours || '';
|
||||
document.getElementById('cleanup-interval').value = config.cleanup_interval_hours || 24;
|
||||
|
||||
// Cargar información del directorio
|
||||
loadCsvDirectoryInfo();
|
||||
} else {
|
||||
showMessage('Error loading CSV configuration: ' + data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showMessage('Error loading CSV configuration', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Cargar información del directorio CSV
|
||||
function loadCsvDirectoryInfo() {
|
||||
fetch('/api/csv/directory/info')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const info = data.info;
|
||||
const statsDiv = document.getElementById('directory-stats');
|
||||
|
||||
let html = `
|
||||
<div class="stat-item">
|
||||
<strong>📁 Directory:</strong>
|
||||
<span>${info.base_directory}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<strong>📊 Total Files:</strong>
|
||||
<span>${info.total_files}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<strong>💾 Total Size:</strong>
|
||||
<span>${info.total_size_mb} MB</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (info.oldest_file) {
|
||||
html += `
|
||||
<div class="stat-item">
|
||||
<strong>📅 Oldest File:</strong>
|
||||
<span>${new Date(info.oldest_file).toLocaleString()}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (info.newest_file) {
|
||||
html += `
|
||||
<div class="stat-item">
|
||||
<strong>🆕 Newest File:</strong>
|
||||
<span>${new Date(info.newest_file).toLocaleString()}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (info.day_folders && info.day_folders.length > 0) {
|
||||
html += '<h4>📂 Day Folders:</h4>';
|
||||
info.day_folders.forEach(folder => {
|
||||
html += `
|
||||
<div class="day-folder-item">
|
||||
<span><strong>${folder.name}</strong></span>
|
||||
<span>${folder.files} files, ${folder.size_mb} MB</span>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
statsDiv.innerHTML = html;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
document.getElementById('directory-stats').innerHTML = '<p>Error loading directory information</p>';
|
||||
});
|
||||
}
|
||||
|
||||
// Ejecutar limpieza manual
|
||||
function triggerManualCleanup() {
|
||||
if (!confirm('¿Estás seguro de que quieres ejecutar la limpieza manual? Esto eliminará archivos antiguos según la configuración actual.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('/api/csv/cleanup', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showMessage('Limpieza ejecutada correctamente', 'success');
|
||||
loadCsvDirectoryInfo(); // Recargar información del directorio
|
||||
} else {
|
||||
showMessage('Error en la limpieza: ' + data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showMessage('Error ejecutando la limpieza', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Inicializar listeners para la configuración CSV
|
||||
function initCsvListeners() {
|
||||
// Manejar envío del formulario de configuración CSV
|
||||
document.getElementById('csv-config-form').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(e.target);
|
||||
const configData = {};
|
||||
|
||||
// Convertir datos del formulario a objeto, manejando valores vacíos
|
||||
for (let [key, value] of formData.entries()) {
|
||||
if (key === 'rotation_enabled') {
|
||||
configData[key] = document.getElementById('rotation-enabled').checked;
|
||||
} else if (value.trim() === '') {
|
||||
configData[key] = null;
|
||||
} else if (key.includes('max_') || key.includes('cleanup_interval')) {
|
||||
configData[key] = parseFloat(value) || null;
|
||||
} else {
|
||||
configData[key] = value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
fetch('/api/csv/config', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(configData)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showMessage('Configuración CSV actualizada correctamente', 'success');
|
||||
loadCsvConfig(); // Recargar para mostrar valores actualizados
|
||||
} else {
|
||||
showMessage('Error actualizando configuración CSV: ' + data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showMessage('Error actualizando configuración CSV', 'error');
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,519 +0,0 @@
|
|||
/**
|
||||
* Gestión de datasets y variables asociadas
|
||||
*/
|
||||
|
||||
// Variables de gestión de datasets
|
||||
let currentDatasets = {};
|
||||
let currentDatasetId = null;
|
||||
|
||||
// Cargar todos los datasets desde API
|
||||
window.loadDatasets = function () {
|
||||
fetch('/api/datasets')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
currentDatasets = data.datasets;
|
||||
currentDatasetId = data.current_dataset_id;
|
||||
updateDatasetSelector();
|
||||
updateDatasetInfo();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading datasets:', error);
|
||||
showMessage('Error loading datasets', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Actualizar el selector de datasets
|
||||
function updateDatasetSelector() {
|
||||
const selector = document.getElementById('dataset-selector');
|
||||
selector.innerHTML = '<option value="">Select a dataset...</option>';
|
||||
|
||||
Object.keys(currentDatasets).forEach(datasetId => {
|
||||
const dataset = currentDatasets[datasetId];
|
||||
const option = document.createElement('option');
|
||||
option.value = datasetId;
|
||||
option.textContent = `${dataset.name} (${dataset.prefix})`;
|
||||
if (datasetId === currentDatasetId) {
|
||||
option.selected = true;
|
||||
}
|
||||
selector.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
// Actualizar información del dataset
|
||||
function updateDatasetInfo() {
|
||||
const statusBar = document.getElementById('dataset-status-bar');
|
||||
const variablesManagement = document.getElementById('variables-management');
|
||||
const noDatasetMessage = document.getElementById('no-dataset-message');
|
||||
|
||||
if (currentDatasetId && currentDatasets[currentDatasetId]) {
|
||||
const dataset = currentDatasets[currentDatasetId];
|
||||
|
||||
// Mostrar info del dataset en la barra de estado
|
||||
document.getElementById('dataset-name').textContent = dataset.name;
|
||||
document.getElementById('dataset-prefix').textContent = dataset.prefix;
|
||||
document.getElementById('dataset-sampling').textContent =
|
||||
dataset.sampling_interval ? `${dataset.sampling_interval}s` : 'Global interval';
|
||||
document.getElementById('dataset-var-count').textContent = Object.keys(dataset.variables).length;
|
||||
document.getElementById('dataset-stream-count').textContent = dataset.streaming_variables.length;
|
||||
|
||||
// Actualizar estado del dataset
|
||||
const statusSpan = document.getElementById('dataset-status');
|
||||
const isActive = dataset.enabled;
|
||||
statusSpan.textContent = isActive ? '🟢 Active' : '⭕ Inactive';
|
||||
statusSpan.className = `status-item ${isActive ? 'status-active' : 'status-inactive'}`;
|
||||
|
||||
// Actualizar botones de acción
|
||||
document.getElementById('activate-dataset-btn').style.display = isActive ? 'none' : 'inline-block';
|
||||
document.getElementById('deactivate-dataset-btn').style.display = isActive ? 'inline-block' : 'none';
|
||||
|
||||
// Mostrar secciones
|
||||
statusBar.style.display = 'block';
|
||||
variablesManagement.style.display = 'block';
|
||||
noDatasetMessage.style.display = 'none';
|
||||
|
||||
// Cargar variables para este dataset
|
||||
loadDatasetVariables(currentDatasetId);
|
||||
} else {
|
||||
statusBar.style.display = 'none';
|
||||
variablesManagement.style.display = 'none';
|
||||
noDatasetMessage.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// Cargar variables para un dataset específico
|
||||
function loadDatasetVariables(datasetId) {
|
||||
if (!datasetId || !currentDatasets[datasetId]) {
|
||||
// Limpiar la tabla si no hay dataset válido
|
||||
document.getElementById('variables-tbody').innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const dataset = currentDatasets[datasetId];
|
||||
const variables = dataset.variables || {};
|
||||
const streamingVars = dataset.streaming_variables || [];
|
||||
const tbody = document.getElementById('variables-tbody');
|
||||
|
||||
// Limpiar filas existentes
|
||||
tbody.innerHTML = '';
|
||||
|
||||
// Añadir una fila para cada variable
|
||||
Object.keys(variables).forEach(varName => {
|
||||
const variable = variables[varName];
|
||||
const row = document.createElement('tr');
|
||||
|
||||
// Formatear visualización del área de memoria
|
||||
let memoryAreaDisplay = '';
|
||||
if (variable.area === 'db') {
|
||||
memoryAreaDisplay = `DB${variable.db || 'N/A'}.${variable.offset}`;
|
||||
} else if (variable.area === 'mw' || variable.area === 'm') {
|
||||
memoryAreaDisplay = `MW${variable.offset}`;
|
||||
} else if (variable.area === 'pew' || variable.area === 'pe') {
|
||||
memoryAreaDisplay = `PEW${variable.offset}`;
|
||||
} else if (variable.area === 'paw' || variable.area === 'pa') {
|
||||
memoryAreaDisplay = `PAW${variable.offset}`;
|
||||
} else if (variable.area === 'e') {
|
||||
memoryAreaDisplay = `E${variable.offset}.${variable.bit}`;
|
||||
} else if (variable.area === 'a') {
|
||||
memoryAreaDisplay = `A${variable.offset}.${variable.bit}`;
|
||||
} else if (variable.area === 'mb') {
|
||||
memoryAreaDisplay = `M${variable.offset}.${variable.bit}`;
|
||||
} else {
|
||||
memoryAreaDisplay = `${variable.area.toUpperCase()}${variable.offset}`;
|
||||
}
|
||||
|
||||
// Comprobar si la variable está en la lista de streaming
|
||||
const isStreaming = streamingVars.includes(varName);
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${varName}</td>
|
||||
<td>${memoryAreaDisplay}</td>
|
||||
<td>${variable.offset}</td>
|
||||
<td>${variable.type.toUpperCase()}</td>
|
||||
<td id="value-${varName}" style="font-family: monospace; font-weight: bold; color: var(--pico-muted-color);">
|
||||
--
|
||||
</td>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" id="stream-${varName}" role="switch" ${isStreaming ? 'checked' : ''}
|
||||
onchange="toggleStreaming('${varName}', this.checked)">
|
||||
Enable
|
||||
</label>
|
||||
</td>
|
||||
<td>
|
||||
<button class="outline" onclick="editVariable('${varName}')">✏️ Edit</button>
|
||||
<button class="secondary" onclick="removeVariable('${varName}')">🗑️ Remove</button>
|
||||
</td>
|
||||
`;
|
||||
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
// Inicializar listeners de eventos para datasets
|
||||
function initDatasetListeners() {
|
||||
// Cambio de selector de dataset
|
||||
document.getElementById('dataset-selector').addEventListener('change', function () {
|
||||
const selectedDatasetId = this.value;
|
||||
if (selectedDatasetId) {
|
||||
// Detener streaming de variables actual si está activo
|
||||
if (isStreamingVariables) {
|
||||
stopVariableStreaming();
|
||||
}
|
||||
|
||||
// Establecer como dataset actual
|
||||
fetch('/api/datasets/current', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ dataset_id: selectedDatasetId })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
currentDatasetId = selectedDatasetId;
|
||||
// Recargar datasets para obtener datos frescos, luego actualizar info
|
||||
loadDatasets();
|
||||
|
||||
// Actualizar texto del botón de streaming
|
||||
const toggleBtn = document.getElementById('toggle-streaming-btn');
|
||||
if (toggleBtn) {
|
||||
toggleBtn.innerHTML = '▶️ Start Live Streaming';
|
||||
}
|
||||
|
||||
// Auto-refrescar valores para el nuevo dataset
|
||||
autoStartLiveDisplay();
|
||||
} else {
|
||||
showMessage(data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showMessage('Error setting current dataset', 'error');
|
||||
});
|
||||
} else {
|
||||
// Detener streaming de variables si está activo
|
||||
if (isStreamingVariables) {
|
||||
stopVariableStreaming();
|
||||
}
|
||||
|
||||
currentDatasetId = null;
|
||||
updateDatasetInfo();
|
||||
// Limpiar valores cuando no hay dataset seleccionado
|
||||
clearVariableValues();
|
||||
}
|
||||
});
|
||||
|
||||
// Botón de nuevo dataset
|
||||
document.getElementById('new-dataset-btn').addEventListener('click', function () {
|
||||
document.getElementById('dataset-modal').style.display = 'block';
|
||||
});
|
||||
|
||||
// Cerrar modal de dataset
|
||||
document.getElementById('close-dataset-modal').addEventListener('click', function () {
|
||||
document.getElementById('dataset-modal').style.display = 'none';
|
||||
});
|
||||
|
||||
document.getElementById('cancel-dataset-btn').addEventListener('click', function () {
|
||||
document.getElementById('dataset-modal').style.display = 'none';
|
||||
});
|
||||
|
||||
// Crear nuevo dataset
|
||||
document.getElementById('dataset-form').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const data = {
|
||||
dataset_id: document.getElementById('dataset-id').value.trim(),
|
||||
name: document.getElementById('dataset-name-input').value.trim(),
|
||||
prefix: document.getElementById('dataset-prefix-input').value.trim(),
|
||||
sampling_interval: document.getElementById('dataset-sampling-input').value || null
|
||||
};
|
||||
|
||||
if (data.sampling_interval) {
|
||||
data.sampling_interval = parseFloat(data.sampling_interval);
|
||||
}
|
||||
|
||||
fetch('/api/datasets', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showMessage(data.message, 'success');
|
||||
document.getElementById('dataset-modal').style.display = 'none';
|
||||
document.getElementById('dataset-form').reset();
|
||||
loadDatasets();
|
||||
} else {
|
||||
showMessage(data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showMessage('Error creating dataset', 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// Botón de eliminar dataset
|
||||
document.getElementById('delete-dataset-btn').addEventListener('click', function () {
|
||||
if (!currentDatasetId) {
|
||||
showMessage('No dataset selected', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const dataset = currentDatasets[currentDatasetId];
|
||||
if (confirm(`Are you sure you want to delete dataset "${dataset.name}"?`)) {
|
||||
fetch(`/api/datasets/${currentDatasetId}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showMessage(data.message, 'success');
|
||||
loadDatasets();
|
||||
} else {
|
||||
showMessage(data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showMessage('Error deleting dataset', 'error');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Botón de activar dataset
|
||||
document.getElementById('activate-dataset-btn').addEventListener('click', function () {
|
||||
if (!currentDatasetId) return;
|
||||
|
||||
fetch(`/api/datasets/${currentDatasetId}/activate`, {
|
||||
method: 'POST'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showMessage(data.message, 'success');
|
||||
loadDatasets();
|
||||
} else {
|
||||
showMessage(data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showMessage('Error activating dataset', 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// Botón de desactivar dataset
|
||||
document.getElementById('deactivate-dataset-btn').addEventListener('click', function () {
|
||||
if (!currentDatasetId) return;
|
||||
|
||||
fetch(`/api/datasets/${currentDatasetId}/deactivate`, {
|
||||
method: 'POST'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showMessage(data.message, 'success');
|
||||
loadDatasets();
|
||||
} else {
|
||||
showMessage(data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showMessage('Error deactivating dataset', 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// Formulario de variables
|
||||
document.getElementById('variable-form').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!currentDatasetId) {
|
||||
showMessage('No dataset selected. Please select a dataset first.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const area = document.getElementById('var-area').value;
|
||||
const data = {
|
||||
name: document.getElementById('var-name').value,
|
||||
area: area,
|
||||
db: area === 'db' ? parseInt(document.getElementById('var-db').value) : 1,
|
||||
offset: parseInt(document.getElementById('var-offset').value),
|
||||
type: document.getElementById('var-type').value,
|
||||
streaming: false // Default to not streaming
|
||||
};
|
||||
|
||||
// Añadir parámetro bit para áreas de bit
|
||||
if (area === 'e' || area === 'a' || area === 'mb') {
|
||||
data.bit = parseInt(document.getElementById('var-bit').value);
|
||||
}
|
||||
|
||||
fetch(`/api/datasets/${currentDatasetId}/variables`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
if (data.success) {
|
||||
document.getElementById('variable-form').reset();
|
||||
loadDatasets(); // Recargar para actualizar conteos
|
||||
updateStatus();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Eliminar variable del dataset actual
|
||||
function removeVariable(name) {
|
||||
if (!currentDatasetId) {
|
||||
showMessage('No dataset selected', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (confirm(`Are you sure you want to remove the variable "${name}" from this dataset?`)) {
|
||||
fetch(`/api/datasets/${currentDatasetId}/variables/${name}`, { method: 'DELETE' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
if (data.success) {
|
||||
loadDatasets(); // Recargar para actualizar conteos
|
||||
updateStatus();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Variables para edición de variables
|
||||
let currentEditingVariable = null;
|
||||
|
||||
// Editar variable
|
||||
function editVariable(name) {
|
||||
if (!currentDatasetId) {
|
||||
showMessage('No dataset selected', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
currentEditingVariable = name;
|
||||
|
||||
// Obtener datos de la variable del dataset actual
|
||||
const dataset = currentDatasets[currentDatasetId];
|
||||
if (dataset && dataset.variables && dataset.variables[name]) {
|
||||
const variable = dataset.variables[name];
|
||||
const streamingVars = dataset.streaming_variables || [];
|
||||
|
||||
// Crear objeto de variable con la misma estructura que la API
|
||||
const variableData = {
|
||||
name: name,
|
||||
area: variable.area,
|
||||
db: variable.db,
|
||||
offset: variable.offset,
|
||||
type: variable.type,
|
||||
bit: variable.bit,
|
||||
streaming: streamingVars.includes(name)
|
||||
};
|
||||
|
||||
populateEditForm(variableData);
|
||||
document.getElementById('edit-modal').style.display = 'block';
|
||||
} else {
|
||||
showMessage('Variable not found in current dataset', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Rellenar formulario de edición
|
||||
function populateEditForm(variable) {
|
||||
document.getElementById('edit-var-name').value = variable.name;
|
||||
document.getElementById('edit-var-area').value = variable.area;
|
||||
document.getElementById('edit-var-offset').value = variable.offset;
|
||||
document.getElementById('edit-var-type').value = variable.type;
|
||||
|
||||
if (variable.db) {
|
||||
document.getElementById('edit-var-db').value = variable.db;
|
||||
}
|
||||
|
||||
if (variable.bit !== undefined) {
|
||||
document.getElementById('edit-var-bit').value = variable.bit;
|
||||
}
|
||||
|
||||
// Actualizar visibilidad de campos según el área
|
||||
toggleEditFields();
|
||||
}
|
||||
|
||||
// Cerrar modal de edición
|
||||
function closeEditModal() {
|
||||
document.getElementById('edit-modal').style.display = 'none';
|
||||
currentEditingVariable = null;
|
||||
}
|
||||
|
||||
// Inicializar listeners para edición de variables
|
||||
function initVariableEditListeners() {
|
||||
// Manejar envío del formulario de edición
|
||||
document.getElementById('edit-variable-form').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!currentEditingVariable || !currentDatasetId) {
|
||||
showMessage('No variable or dataset selected for editing', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const area = document.getElementById('edit-var-area').value;
|
||||
const newName = document.getElementById('edit-var-name').value;
|
||||
|
||||
// Primero eliminar la variable antigua
|
||||
fetch(`/api/datasets/${currentDatasetId}/variables/${currentEditingVariable}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(deleteResult => {
|
||||
if (deleteResult.success) {
|
||||
// Luego añadir la variable actualizada
|
||||
const data = {
|
||||
name: newName,
|
||||
area: area,
|
||||
db: area === 'db' ? parseInt(document.getElementById('edit-var-db').value) : 1,
|
||||
offset: parseInt(document.getElementById('edit-var-offset').value),
|
||||
type: document.getElementById('edit-var-type').value,
|
||||
streaming: false // Se restaurará abajo si estaba habilitado
|
||||
};
|
||||
|
||||
// Añadir parámetro bit para áreas de bit
|
||||
if (area === 'e' || area === 'a' || area === 'mb') {
|
||||
data.bit = parseInt(document.getElementById('edit-var-bit').value);
|
||||
}
|
||||
|
||||
return fetch(`/api/datasets/${currentDatasetId}/variables`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
} else {
|
||||
throw new Error(deleteResult.message);
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showMessage('Variable updated successfully', 'success');
|
||||
closeEditModal();
|
||||
loadDatasets();
|
||||
updateStatus();
|
||||
} else {
|
||||
showMessage(data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showMessage(`Error updating variable: ${error}`, 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// Cerrar modal al hacer clic fuera de él
|
||||
window.onclick = function (event) {
|
||||
const editModal = document.getElementById('edit-modal');
|
||||
const datasetModal = document.getElementById('dataset-modal');
|
||||
if (event.target === editModal) {
|
||||
closeEditModal();
|
||||
}
|
||||
if (event.target === datasetModal) {
|
||||
datasetModal.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,168 +0,0 @@
|
|||
/**
|
||||
* Gestión de eventos de la aplicación y log de eventos
|
||||
*/
|
||||
|
||||
// Refrescar log de eventos
|
||||
function refreshEventLog() {
|
||||
const limit = document.getElementById('log-limit').value;
|
||||
|
||||
fetch(`/api/events?limit=${limit}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const logContainer = document.getElementById('events-log');
|
||||
const logStats = document.getElementById('log-stats');
|
||||
|
||||
// Limpiar entradas existentes
|
||||
logContainer.innerHTML = '';
|
||||
|
||||
// Actualizar estadísticas
|
||||
logStats.textContent = `Showing ${data.showing} of ${data.total_events} events`;
|
||||
|
||||
// Añadir eventos (orden inverso para mostrar primero los más nuevos)
|
||||
const events = data.events.reverse();
|
||||
|
||||
if (events.length === 0) {
|
||||
logContainer.innerHTML = `
|
||||
<div class="log-entry log-info">
|
||||
<div class="log-header">
|
||||
<span>📋 System</span>
|
||||
<span class="log-timestamp">${new Date().toLocaleString('es-ES')}</span>
|
||||
</div>
|
||||
<div class="log-message">No events found</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
events.forEach(event => {
|
||||
logContainer.appendChild(createLogEntry(event));
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-scroll al inicio para mostrar eventos más nuevos
|
||||
logContainer.scrollTop = 0;
|
||||
} else {
|
||||
console.error('Error loading events:', data.error);
|
||||
showMessage('Error loading events log', 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching events:', error);
|
||||
showMessage('Error fetching events log', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Crear entrada de log
|
||||
function createLogEntry(event) {
|
||||
const logEntry = document.createElement('div');
|
||||
logEntry.className = `log-entry log-${event.level}`;
|
||||
|
||||
const hasDetails = event.details && Object.keys(event.details).length > 0;
|
||||
|
||||
logEntry.innerHTML = `
|
||||
<div class="log-header">
|
||||
<span>${getEventIcon(event.event_type)} ${event.event_type.replace(/_/g, ' ').toUpperCase()}</span>
|
||||
<span class="log-timestamp">${formatTimestamp(event.timestamp)}</span>
|
||||
</div>
|
||||
<div class="log-message">${event.message}</div>
|
||||
${hasDetails ? `<div class="log-details">${JSON.stringify(event.details, null, 2)}</div>` : ''}
|
||||
`;
|
||||
|
||||
return logEntry;
|
||||
}
|
||||
|
||||
// Limpiar vista de log
|
||||
function clearLogView() {
|
||||
const logContainer = document.getElementById('events-log');
|
||||
logContainer.innerHTML = `
|
||||
<div class="log-entry log-info">
|
||||
<div class="log-header">
|
||||
<span>🧹 System</span>
|
||||
<span class="log-timestamp">${new Date().toLocaleString('es-ES')}</span>
|
||||
</div>
|
||||
<div class="log-message">Log view cleared. Click refresh to reload events.</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const logStats = document.getElementById('log-stats');
|
||||
logStats.textContent = 'Log view cleared';
|
||||
}
|
||||
|
||||
// Inicializar listeners para eventos
|
||||
function initEventListeners() {
|
||||
// Botones de control de log para el tab de events
|
||||
const refreshBtn = document.getElementById('refresh-events-btn');
|
||||
const clearBtn = document.getElementById('clear-events-btn');
|
||||
|
||||
if (refreshBtn) {
|
||||
refreshBtn.addEventListener('click', loadEvents);
|
||||
}
|
||||
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener('click', clearEventsView);
|
||||
}
|
||||
}
|
||||
|
||||
// Función para cargar eventos en el tab de events
|
||||
window.loadEvents = function() {
|
||||
fetch('/api/events?limit=50')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const eventsContainer = document.getElementById('events-container');
|
||||
const eventsCount = document.getElementById('events-count');
|
||||
|
||||
// Limpiar contenedor
|
||||
eventsContainer.innerHTML = '';
|
||||
|
||||
// Actualizar contador
|
||||
eventsCount.textContent = data.showing || 0;
|
||||
|
||||
// Añadir eventos (orden inverso para mostrar primero los más nuevos)
|
||||
const events = data.events.reverse();
|
||||
|
||||
if (events.length === 0) {
|
||||
eventsContainer.innerHTML = `
|
||||
<div class="log-entry log-info">
|
||||
<div class="log-header">
|
||||
<span>📋 System</span>
|
||||
<span class="log-timestamp">${new Date().toLocaleString('es-ES')}</span>
|
||||
</div>
|
||||
<div class="log-message">No events found</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
events.forEach(event => {
|
||||
eventsContainer.appendChild(createLogEntry(event));
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-scroll al inicio para mostrar eventos más nuevos
|
||||
eventsContainer.scrollTop = 0;
|
||||
} else {
|
||||
console.error('Error loading events:', data.error);
|
||||
showMessage('Error loading events log', 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching events:', error);
|
||||
showMessage('Error fetching events log', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Función para limpiar vista de eventos
|
||||
function clearEventsView() {
|
||||
const eventsContainer = document.getElementById('events-container');
|
||||
const eventsCount = document.getElementById('events-count');
|
||||
|
||||
eventsContainer.innerHTML = `
|
||||
<div class="log-entry log-info">
|
||||
<div class="log-header">
|
||||
<span>🧹 System</span>
|
||||
<span class="log-timestamp">${new Date().toLocaleString('es-ES')}</span>
|
||||
</div>
|
||||
<div class="log-message">Events view cleared. Click refresh to reload events.</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
eventsCount.textContent = '0';
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
/**
|
||||
* Archivo principal que inicializa todos los componentes
|
||||
*/
|
||||
|
||||
// Inicializar la aplicación al cargar el documento
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Inicializar tema
|
||||
loadTheme();
|
||||
|
||||
// Iniciar streaming de estado automáticamente
|
||||
startStatusStreaming();
|
||||
|
||||
// Cargar datos iniciales
|
||||
loadDatasets();
|
||||
updateStatus();
|
||||
loadCsvConfig();
|
||||
refreshEventLog();
|
||||
|
||||
// Inicializar listeners de eventos
|
||||
initPlcListeners();
|
||||
initDatasetListeners();
|
||||
initVariableEditListeners();
|
||||
initStreamingListeners();
|
||||
initCsvListeners();
|
||||
initEventListeners();
|
||||
|
||||
// 🔑 NUEVO: Inicializar plotManager si existe
|
||||
if (typeof PlotManager !== 'undefined' && !window.plotManager) {
|
||||
window.plotManager = new PlotManager();
|
||||
console.log('📈 PlotManager initialized from main.js');
|
||||
}
|
||||
|
||||
// Configurar actualizaciones periódicas como respaldo
|
||||
setInterval(updateStatus, 30000); // Cada 30 segundos como respaldo
|
||||
setInterval(refreshEventLog, 10000); // Cada 10 segundos
|
||||
|
||||
// Inicializar visibilidad de campos en formularios
|
||||
toggleFields();
|
||||
});
|
||||
|
||||
// Limpiar conexiones SSE cuando se descarga la página
|
||||
window.addEventListener('beforeunload', function () {
|
||||
if (variableEventSource) {
|
||||
variableEventSource.close();
|
||||
}
|
||||
if (statusEventSource) {
|
||||
statusEventSource.close();
|
||||
}
|
||||
});
|
File diff suppressed because one or more lines are too long
|
@ -1,79 +0,0 @@
|
|||
/**
|
||||
* Gestión de la conexión con el PLC y configuración relacionada
|
||||
*/
|
||||
|
||||
// Inicializar listeners de eventos para PLC
|
||||
function initPlcListeners() {
|
||||
// Configuración del PLC
|
||||
document.getElementById('plc-config-form').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
const data = {
|
||||
ip: document.getElementById('plc-ip').value,
|
||||
rack: parseInt(document.getElementById('plc-rack').value),
|
||||
slot: parseInt(document.getElementById('plc-slot').value)
|
||||
};
|
||||
|
||||
fetch('/api/plc/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// Configuración UDP
|
||||
document.getElementById('udp-config-form').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
const data = {
|
||||
host: document.getElementById('udp-host').value,
|
||||
port: parseInt(document.getElementById('udp-port').value)
|
||||
};
|
||||
|
||||
fetch('/api/udp/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// Botón de conexión PLC
|
||||
document.getElementById('connect-btn').addEventListener('click', function () {
|
||||
fetch('/api/plc/connect', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
});
|
||||
});
|
||||
|
||||
// Botón de desconexión PLC
|
||||
document.getElementById('disconnect-btn').addEventListener('click', function () {
|
||||
fetch('/api/plc/disconnect', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
});
|
||||
});
|
||||
|
||||
// Botón de actualización de intervalo
|
||||
document.getElementById('update-sampling-btn').addEventListener('click', function () {
|
||||
const interval = parseFloat(document.getElementById('sampling-interval').value);
|
||||
fetch('/api/sampling', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ interval: interval })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
});
|
||||
});
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,256 +0,0 @@
|
|||
/**
|
||||
* Gestión del estado del sistema y actualizaciones en tiempo real
|
||||
*/
|
||||
|
||||
// Variables para el streaming de estado
|
||||
let statusEventSource = null;
|
||||
let isStreamingStatus = false;
|
||||
|
||||
// Actualizar el estado del sistema
|
||||
function updateStatus() {
|
||||
fetch('/api/status')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const plcStatus = document.getElementById('plc-status');
|
||||
const streamStatus = document.getElementById('stream-status');
|
||||
const csvStatus = document.getElementById('csv-status');
|
||||
const diskSpaceStatus = document.getElementById('disk-space');
|
||||
|
||||
// Actualizar estado de conexión PLC
|
||||
if (data.plc_connected) {
|
||||
plcStatus.innerHTML = '🔌 PLC: Connected <div style="margin-top: 8px;"><button type="button" id="status-disconnect-btn">❌ Disconnect</button></div>';
|
||||
plcStatus.className = 'status-item status-connected';
|
||||
|
||||
// Añadir event listener al nuevo botón de desconexión
|
||||
document.getElementById('status-disconnect-btn').addEventListener('click', function () {
|
||||
fetch('/api/plc/disconnect', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
plcStatus.innerHTML = '🔌 PLC: Disconnected <div style="margin-top: 8px;"><button type="button" id="status-connect-btn">🔗 Connect</button></div>';
|
||||
plcStatus.className = 'status-item status-disconnected';
|
||||
|
||||
// Añadir event listener al botón de conexión
|
||||
document.getElementById('status-connect-btn').addEventListener('click', function () {
|
||||
fetch('/api/plc/connect', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Actualizar estado de streaming UDP
|
||||
if (data.streaming) {
|
||||
streamStatus.innerHTML = '📡 UDP Streaming: Active <div style="margin-top: 8px;"><button type="button" id="status-streaming-btn">⏹️ Stop</button></div>';
|
||||
streamStatus.className = 'status-item status-streaming';
|
||||
|
||||
// Añadir event listener al botón de parar streaming UDP
|
||||
document.getElementById('status-streaming-btn').addEventListener('click', function () {
|
||||
fetch('/api/udp/streaming/stop', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
streamStatus.innerHTML = '📡 UDP Streaming: Inactive <div style="margin-top: 8px;"><button type="button" id="status-start-btn">▶️ Start</button></div>';
|
||||
streamStatus.className = 'status-item status-idle';
|
||||
|
||||
// Añadir event listener al botón de iniciar streaming UDP
|
||||
document.getElementById('status-start-btn').addEventListener('click', function () {
|
||||
fetch('/api/udp/streaming/start', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Actualizar estado de grabación CSV
|
||||
if (data.csv_recording) {
|
||||
csvStatus.textContent = `💾 CSV: Recording`;
|
||||
csvStatus.className = 'status-item status-streaming';
|
||||
} else {
|
||||
csvStatus.textContent = `💾 CSV: Inactive`;
|
||||
csvStatus.className = 'status-item status-idle';
|
||||
}
|
||||
|
||||
// Actualizar estado de espacio en disco
|
||||
if (data.disk_space_info) {
|
||||
diskSpaceStatus.innerHTML = `💽 Disk: ${data.disk_space_info.free_space} free<br>
|
||||
⏱️ ~${data.disk_space_info.recording_time_left}`;
|
||||
diskSpaceStatus.className = 'status-item status-idle';
|
||||
} else {
|
||||
diskSpaceStatus.textContent = '💽 Disk Space: Calculating...';
|
||||
diskSpaceStatus.className = 'status-item status-idle';
|
||||
}
|
||||
})
|
||||
.catch(error => console.error('Error updating status:', error));
|
||||
}
|
||||
|
||||
// Iniciar streaming de estado en tiempo real
|
||||
function startStatusStreaming() {
|
||||
if (isStreamingStatus) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cerrar conexión existente si hay alguna
|
||||
if (statusEventSource) {
|
||||
statusEventSource.close();
|
||||
}
|
||||
|
||||
// Crear nueva conexión EventSource
|
||||
statusEventSource = new EventSource('/api/stream/status?interval=2.0');
|
||||
|
||||
statusEventSource.onopen = function (event) {
|
||||
console.log('Status streaming connected');
|
||||
isStreamingStatus = true;
|
||||
};
|
||||
|
||||
statusEventSource.onmessage = function (event) {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
switch (data.type) {
|
||||
case 'connected':
|
||||
console.log('Status stream connected:', data.message);
|
||||
break;
|
||||
|
||||
case 'status':
|
||||
// Actualizar estado en tiempo real
|
||||
updateStatusFromStream(data.status);
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
console.error('Status stream error:', data.message);
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing status SSE data:', error);
|
||||
}
|
||||
};
|
||||
|
||||
statusEventSource.onerror = function (event) {
|
||||
console.error('Status stream error:', event);
|
||||
isStreamingStatus = false;
|
||||
|
||||
// Intentar reconectar después de un retraso
|
||||
setTimeout(() => {
|
||||
startStatusStreaming();
|
||||
}, 10000);
|
||||
};
|
||||
}
|
||||
|
||||
// Detener streaming de estado en tiempo real
|
||||
function stopStatusStreaming() {
|
||||
if (statusEventSource) {
|
||||
statusEventSource.close();
|
||||
statusEventSource = null;
|
||||
}
|
||||
isStreamingStatus = false;
|
||||
}
|
||||
|
||||
// Actualizar estado desde datos de streaming
|
||||
function updateStatusFromStream(status) {
|
||||
const plcStatus = document.getElementById('plc-status');
|
||||
const streamStatus = document.getElementById('stream-status');
|
||||
const csvStatus = document.getElementById('csv-status');
|
||||
const diskSpaceStatus = document.getElementById('disk-space');
|
||||
|
||||
// Actualizar estado de conexión PLC
|
||||
if (status.plc_connected) {
|
||||
plcStatus.innerHTML = '🔌 PLC: Connected <div style="margin-top: 8px;"><button type="button" id="status-disconnect-btn">❌ Disconnect</button></div>';
|
||||
plcStatus.className = 'status-item status-connected';
|
||||
|
||||
// Añadir event listener al nuevo botón de desconexión
|
||||
const disconnectBtn = document.getElementById('status-disconnect-btn');
|
||||
if (disconnectBtn) {
|
||||
disconnectBtn.addEventListener('click', function () {
|
||||
fetch('/api/plc/disconnect', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
plcStatus.innerHTML = '🔌 PLC: Disconnected <div style="margin-top: 8px;"><button type="button" id="status-connect-btn">🔗 Connect</button></div>';
|
||||
plcStatus.className = 'status-item status-disconnected';
|
||||
|
||||
// Añadir event listener al botón de conexión
|
||||
const connectBtn = document.getElementById('status-connect-btn');
|
||||
if (connectBtn) {
|
||||
connectBtn.addEventListener('click', function () {
|
||||
fetch('/api/plc/connect', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Actualizar estado de streaming UDP
|
||||
if (status.streaming) {
|
||||
streamStatus.innerHTML = '📡 UDP Streaming: Active <div style="margin-top: 8px;"><button type="button" id="status-streaming-btn">⏹️ Stop</button></div>';
|
||||
streamStatus.className = 'status-item status-streaming';
|
||||
|
||||
// Añadir event listener al botón de parar streaming UDP
|
||||
const stopBtn = document.getElementById('status-streaming-btn');
|
||||
if (stopBtn) {
|
||||
stopBtn.addEventListener('click', function () {
|
||||
fetch('/api/udp/streaming/stop', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
streamStatus.innerHTML = '📡 UDP Streaming: Inactive <div style="margin-top: 8px;"><button type="button" id="status-start-btn">▶️ Start</button></div>';
|
||||
streamStatus.className = 'status-item status-idle';
|
||||
|
||||
// Añadir event listener al botón de iniciar streaming UDP
|
||||
const startBtn = document.getElementById('status-start-btn');
|
||||
if (startBtn) {
|
||||
startBtn.addEventListener('click', function () {
|
||||
fetch('/api/udp/streaming/start', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Actualizar estado de grabación CSV
|
||||
if (status.csv_recording) {
|
||||
csvStatus.textContent = `💾 CSV: Recording`;
|
||||
csvStatus.className = 'status-item status-streaming';
|
||||
} else {
|
||||
csvStatus.textContent = `💾 CSV: Inactive`;
|
||||
csvStatus.className = 'status-item status-idle';
|
||||
}
|
||||
|
||||
// Actualizar estado de espacio en disco
|
||||
if (status.disk_space_info) {
|
||||
diskSpaceStatus.innerHTML = `💽 Disk: ${status.disk_space_info.free_space} free<br>
|
||||
⏱️ ~${status.disk_space_info.recording_time_left}`;
|
||||
diskSpaceStatus.className = 'status-item status-idle';
|
||||
} else {
|
||||
diskSpaceStatus.textContent = '💽 Disk Space: Calculating...';
|
||||
diskSpaceStatus.className = 'status-item status-idle';
|
||||
}
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
/**
|
||||
* Gestión del streaming UDP a PlotJuggler (independiente del recording CSV)
|
||||
*/
|
||||
|
||||
// Inicializar listeners para el control de streaming UDP
|
||||
function initStreamingListeners() {
|
||||
// Iniciar streaming UDP
|
||||
document.getElementById('start-streaming-btn').addEventListener('click', function () {
|
||||
fetch('/api/udp/streaming/start', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error starting UDP streaming:', error);
|
||||
showMessage('Error starting UDP streaming', 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// Detener streaming UDP
|
||||
document.getElementById('stop-streaming-btn').addEventListener('click', function () {
|
||||
fetch('/api/udp/streaming/stop', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error stopping UDP streaming:', error);
|
||||
showMessage('Error stopping UDP streaming', 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// Cargar estado de streaming de variables
|
||||
loadStreamingStatus();
|
||||
}
|
||||
|
||||
// Cargar estado de variables en streaming
|
||||
function loadStreamingStatus() {
|
||||
fetch('/api/variables/streaming')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
data.streaming_variables.forEach(varName => {
|
||||
const checkbox = document.getElementById(`stream-${varName}`);
|
||||
if (checkbox) {
|
||||
checkbox.checked = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => console.error('Error loading streaming status:', error));
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,296 +0,0 @@
|
|||
/**
|
||||
* Tab System Management
|
||||
* Maneja la navegación entre tabs en la aplicación
|
||||
*/
|
||||
|
||||
class TabManager {
|
||||
constructor() {
|
||||
this.currentTab = 'datasets';
|
||||
this.plotTabs = new Set(); // Track dynamic plot tabs
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Event listeners para los botones de tab estáticos
|
||||
this.bindStaticTabs();
|
||||
|
||||
// Inicializar con el tab activo por defecto
|
||||
this.switchTab(this.currentTab);
|
||||
|
||||
console.log('📑 Tab Manager initialized');
|
||||
}
|
||||
|
||||
bindStaticTabs() {
|
||||
// Solo bindear tabs estáticos, los dinámicos se bindean al crearlos
|
||||
document.querySelectorAll('.tab-btn:not([data-plot-id])').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const tabName = e.target.dataset.tab;
|
||||
this.switchTab(tabName);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
switchTab(tabName) {
|
||||
// Remover clase active de todos los tabs
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
|
||||
document.querySelectorAll('.tab-content').forEach(content => {
|
||||
content.classList.remove('active');
|
||||
});
|
||||
|
||||
// Activar el tab seleccionado
|
||||
const activeBtn = document.querySelector(`[data-tab="${tabName}"]`);
|
||||
const activeContent = document.getElementById(`${tabName}-tab`);
|
||||
|
||||
if (activeBtn && activeContent) {
|
||||
activeBtn.classList.add('active');
|
||||
activeContent.classList.add('active');
|
||||
this.currentTab = tabName;
|
||||
|
||||
// Eventos específicos por tab
|
||||
this.handleTabSpecificEvents(tabName);
|
||||
|
||||
console.log(`📑 Switched to tab: ${tabName}`);
|
||||
}
|
||||
}
|
||||
|
||||
createPlotTab(sessionId, plotName) {
|
||||
// Crear botón de sub-tab dinámico
|
||||
const subTabBtn = document.createElement('button');
|
||||
subTabBtn.className = 'sub-tab-btn plot-sub-tab';
|
||||
subTabBtn.dataset.subTab = `plot-${sessionId}`;
|
||||
subTabBtn.dataset.plotId = sessionId;
|
||||
subTabBtn.innerHTML = `
|
||||
📈 ${plotName}
|
||||
<span class="sub-tab-close" data-session-id="${sessionId}">×</span>
|
||||
`;
|
||||
|
||||
// Crear contenido del sub-tab
|
||||
const subTabContent = document.createElement('div');
|
||||
subTabContent.className = 'sub-tab-content plot-sub-tab-content';
|
||||
subTabContent.id = `plot-${sessionId}-sub-tab`;
|
||||
subTabContent.innerHTML = `
|
||||
<article>
|
||||
<header>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>📈 ${plotName}</span>
|
||||
<div>
|
||||
<button type="button" class="outline" onclick="window.editPlotSession('${sessionId}')">
|
||||
✏️ Edit Plot
|
||||
</button>
|
||||
<button type="button" class="secondary" onclick="window.removePlotSession('${sessionId}')">
|
||||
🗑️ Remove Plot
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="plot-session" id="plot-session-${sessionId}">
|
||||
<div class="plot-header">
|
||||
<h4>📈 ${plotName}</h4>
|
||||
<div class="plot-controls">
|
||||
<button class="btn btn-sm" onclick="plotManager.controlPlot('${sessionId}', 'start')" title="Start">
|
||||
▶️ Start
|
||||
</button>
|
||||
<button class="btn btn-sm" onclick="plotManager.controlPlot('${sessionId}', 'pause')" title="Pause">
|
||||
⏸️ Pause
|
||||
</button>
|
||||
<button class="btn btn-sm" onclick="plotManager.controlPlot('${sessionId}', 'clear')" title="Clear">
|
||||
🗑️ Clear
|
||||
</button>
|
||||
<button class="btn btn-sm" onclick="plotManager.controlPlot('${sessionId}', 'stop')" title="Stop">
|
||||
⏹️ Stop
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="plot-info">
|
||||
<span class="plot-stats" id="plot-stats-${sessionId}">
|
||||
Loading plot information...
|
||||
</span>
|
||||
</div>
|
||||
<div class="plot-canvas">
|
||||
<canvas id="chart-${sessionId}"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
|
||||
// Mostrar sub-tabs si no están visibles
|
||||
const subTabs = document.getElementById('plot-sub-tabs');
|
||||
const plotSessionsContainer = document.getElementById('plot-sessions-container');
|
||||
const plotSubContent = document.getElementById('plot-sub-content');
|
||||
|
||||
if (subTabs.style.display === 'none') {
|
||||
subTabs.style.display = 'flex';
|
||||
plotSessionsContainer.style.display = 'none';
|
||||
plotSubContent.style.display = 'block';
|
||||
}
|
||||
|
||||
// Agregar sub-tab al contenedor de sub-tabs
|
||||
subTabs.appendChild(subTabBtn);
|
||||
|
||||
// Agregar contenido del sub-tab
|
||||
plotSubContent.appendChild(subTabContent);
|
||||
|
||||
// Bind events
|
||||
subTabBtn.addEventListener('click', (e) => {
|
||||
if (!e.target.classList.contains('sub-tab-close')) {
|
||||
this.switchSubTab(`plot-${sessionId}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Close button event
|
||||
subTabBtn.querySelector('.sub-tab-close').addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
// Llamar a la función que elimina el plot del backend Y del frontend
|
||||
if (typeof window.removePlotSession === 'function') {
|
||||
window.removePlotSession(sessionId);
|
||||
} else {
|
||||
console.error('removePlotSession function not available');
|
||||
// Fallback: solo remover del frontend
|
||||
this.removePlotTab(sessionId);
|
||||
}
|
||||
});
|
||||
|
||||
this.plotTabs.add(sessionId);
|
||||
|
||||
console.log(`📑 Created plot sub-tab for session: ${sessionId}`);
|
||||
return subTabBtn;
|
||||
}
|
||||
|
||||
switchSubTab(subTabName) {
|
||||
// Remover clase active de todos los sub-tabs
|
||||
document.querySelectorAll('.sub-tab-btn').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
|
||||
document.querySelectorAll('.sub-tab-content').forEach(content => {
|
||||
content.classList.remove('active');
|
||||
});
|
||||
|
||||
// Activar el sub-tab seleccionado
|
||||
const activeBtn = document.querySelector(`[data-sub-tab="${subTabName}"]`);
|
||||
const activeContent = document.getElementById(`${subTabName}-sub-tab`);
|
||||
|
||||
if (activeBtn && activeContent) {
|
||||
activeBtn.classList.add('active');
|
||||
activeContent.classList.add('active');
|
||||
|
||||
// Eventos específicos por sub-tab
|
||||
this.handleSubTabSpecificEvents(subTabName);
|
||||
|
||||
console.log(`📑 Switched to sub-tab: ${subTabName}`);
|
||||
}
|
||||
}
|
||||
|
||||
handleSubTabSpecificEvents(subTabName) {
|
||||
if (subTabName.startsWith('plot-')) {
|
||||
// Sub-tab de plot individual - cargar datos específicos
|
||||
const sessionId = subTabName.replace('plot-', '');
|
||||
if (typeof plotManager !== 'undefined') {
|
||||
plotManager.updateSessionData(sessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removePlotTab(sessionId) {
|
||||
// Remover sub-tab button
|
||||
const subTabBtn = document.querySelector(`[data-plot-id="${sessionId}"]`);
|
||||
if (subTabBtn) {
|
||||
subTabBtn.remove();
|
||||
}
|
||||
|
||||
// Remover sub-tab content
|
||||
const subTabContent = document.getElementById(`plot-${sessionId}-sub-tab`);
|
||||
if (subTabContent) {
|
||||
subTabContent.remove();
|
||||
}
|
||||
|
||||
this.plotTabs.delete(sessionId);
|
||||
|
||||
// Si no quedan sub-tabs, mostrar vista inicial
|
||||
const subTabs = document.getElementById('plot-sub-tabs');
|
||||
const plotSessionsContainer = document.getElementById('plot-sessions-container');
|
||||
const plotSubContent = document.getElementById('plot-sub-content');
|
||||
|
||||
if (subTabs.children.length === 0) {
|
||||
subTabs.style.display = 'none';
|
||||
plotSessionsContainer.style.display = 'block';
|
||||
plotSubContent.style.display = 'none';
|
||||
}
|
||||
|
||||
console.log(`📑 Removed plot sub-tab for session: ${sessionId}`);
|
||||
}
|
||||
|
||||
updatePlotTabName(sessionId, newName) {
|
||||
const subTabBtn = document.querySelector(`[data-plot-id="${sessionId}"]`);
|
||||
if (subTabBtn) {
|
||||
subTabBtn.innerHTML = `
|
||||
📈 ${newName}
|
||||
<span class="sub-tab-close" data-session-id="${sessionId}">×</span>
|
||||
`;
|
||||
|
||||
// Re-bind close event
|
||||
subTabBtn.querySelector('.sub-tab-close').addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
// Llamar a la función que elimina el plot del backend Y del frontend
|
||||
if (typeof window.removePlotSession === 'function') {
|
||||
window.removePlotSession(sessionId);
|
||||
} else {
|
||||
console.error('removePlotSession function not available');
|
||||
// Fallback: solo remover del frontend
|
||||
this.removePlotTab(sessionId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Actualizar header del contenido
|
||||
const header = document.querySelector(`#plot-${sessionId}-sub-tab h4`);
|
||||
if (header) {
|
||||
header.textContent = `📈 ${newName}`;
|
||||
}
|
||||
|
||||
const articleHeader = document.querySelector(`#plot-${sessionId}-sub-tab header span`);
|
||||
if (articleHeader) {
|
||||
articleHeader.textContent = `📈 ${newName}`;
|
||||
}
|
||||
}
|
||||
|
||||
handleTabSpecificEvents(tabName) {
|
||||
switch (tabName) {
|
||||
case 'plotting':
|
||||
// Inicializar plotting si no está inicializado
|
||||
if (typeof plotManager !== 'undefined' && !plotManager.isInitialized) {
|
||||
plotManager.init();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'events':
|
||||
// Cargar eventos si no están cargados
|
||||
if (typeof loadEvents === 'function') {
|
||||
loadEvents();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'datasets':
|
||||
// Actualizar datasets si es necesario
|
||||
if (typeof loadDatasets === 'function') {
|
||||
loadDatasets();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
getCurrentTab() {
|
||||
return this.currentTab;
|
||||
}
|
||||
}
|
||||
|
||||
// Inicialización
|
||||
let tabManager = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
tabManager = new TabManager();
|
||||
});
|
|
@ -1,32 +0,0 @@
|
|||
/**
|
||||
* Gestión del tema de la aplicación (claro/oscuro/auto)
|
||||
*/
|
||||
|
||||
// Establecer el tema
|
||||
function setTheme(theme) {
|
||||
const html = document.documentElement;
|
||||
const buttons = document.querySelectorAll('.theme-selector button');
|
||||
|
||||
// Eliminar clase active de todos los botones
|
||||
buttons.forEach(btn => btn.classList.remove('active'));
|
||||
|
||||
// Establecer tema
|
||||
html.setAttribute('data-theme', theme);
|
||||
|
||||
// Añadir clase active al botón seleccionado
|
||||
document.getElementById(`theme-${theme}`).classList.add('active');
|
||||
|
||||
// Guardar preferencia en localStorage
|
||||
localStorage.setItem('theme', theme);
|
||||
}
|
||||
|
||||
// Cargar tema guardado al cargar la página
|
||||
function loadTheme() {
|
||||
const savedTheme = localStorage.getItem('theme') || 'light';
|
||||
setTheme(savedTheme);
|
||||
}
|
||||
|
||||
// Inicializar tema al cargar la página
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
loadTheme();
|
||||
});
|
|
@ -1,64 +0,0 @@
|
|||
/**
|
||||
* Funciones de utilidad general para la aplicación
|
||||
*/
|
||||
|
||||
// Función para mostrar mensajes en la interfaz
|
||||
function showMessage(message, type = 'success') {
|
||||
const messagesDiv = document.getElementById('messages');
|
||||
let alertClass;
|
||||
|
||||
switch (type) {
|
||||
case 'success':
|
||||
alertClass = 'alert-success';
|
||||
break;
|
||||
case 'warning':
|
||||
alertClass = 'alert-warning';
|
||||
break;
|
||||
case 'info':
|
||||
alertClass = 'alert-info';
|
||||
break;
|
||||
case 'error':
|
||||
default:
|
||||
alertClass = 'alert-error';
|
||||
break;
|
||||
}
|
||||
|
||||
messagesDiv.innerHTML = `<div class="alert ${alertClass}">${message}</div>`;
|
||||
setTimeout(() => {
|
||||
messagesDiv.innerHTML = '';
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Formatear timestamp para los logs
|
||||
function formatTimestamp(isoString) {
|
||||
const date = new Date(isoString);
|
||||
return date.toLocaleString('es-ES', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
// Obtener icono para tipo de evento
|
||||
function getEventIcon(eventType) {
|
||||
const icons = {
|
||||
'plc_connection': '🔗',
|
||||
'plc_connection_failed': '❌',
|
||||
'plc_disconnection': '🔌',
|
||||
'plc_disconnection_error': '⚠️',
|
||||
'streaming_started': '▶️',
|
||||
'streaming_stopped': '⏹️',
|
||||
'streaming_error': '❌',
|
||||
'csv_started': '💾',
|
||||
'csv_stopped': '📁',
|
||||
'csv_error': '❌',
|
||||
'config_change': '⚙️',
|
||||
'variable_added': '➕',
|
||||
'variable_removed': '➖',
|
||||
'application_started': '🚀'
|
||||
};
|
||||
return icons[eventType] || '📋';
|
||||
}
|
|
@ -1,322 +0,0 @@
|
|||
/**
|
||||
* Gestión de variables y streaming de valores en tiempo real
|
||||
*/
|
||||
|
||||
// Variables para el streaming de variables
|
||||
let variableEventSource = null;
|
||||
let isStreamingVariables = false;
|
||||
|
||||
// Toggle de campos de variables según el área de memoria
|
||||
function toggleFields() {
|
||||
const area = document.getElementById('var-area').value;
|
||||
const dbField = document.getElementById('db-field');
|
||||
const dbInput = document.getElementById('var-db');
|
||||
const bitField = document.getElementById('bit-field');
|
||||
const typeSelect = document.getElementById('var-type');
|
||||
|
||||
// Manejar campo DB
|
||||
if (area === 'db') {
|
||||
dbField.style.display = 'block';
|
||||
dbInput.required = true;
|
||||
} else {
|
||||
dbField.style.display = 'none';
|
||||
dbInput.required = false;
|
||||
dbInput.value = 1; // Valor por defecto para áreas no DB
|
||||
}
|
||||
|
||||
// Manejar campo Bit y restricciones de tipo de datos
|
||||
if (area === 'e' || area === 'a' || area === 'mb') {
|
||||
bitField.style.display = 'block';
|
||||
// Para áreas de bit, forzar tipo de dato a bool
|
||||
typeSelect.value = 'bool';
|
||||
// Deshabilitar otros tipos de datos para áreas de bit
|
||||
Array.from(typeSelect.options).forEach(option => {
|
||||
option.disabled = (option.value !== 'bool');
|
||||
});
|
||||
} else {
|
||||
bitField.style.display = 'none';
|
||||
// Re-habilitar todos los tipos de datos para áreas no-bit
|
||||
Array.from(typeSelect.options).forEach(option => {
|
||||
option.disabled = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle de campos de edición de variables
|
||||
function toggleEditFields() {
|
||||
const area = document.getElementById('edit-var-area').value;
|
||||
const dbField = document.getElementById('edit-db-field');
|
||||
const dbInput = document.getElementById('edit-var-db');
|
||||
const bitField = document.getElementById('edit-bit-field');
|
||||
const typeSelect = document.getElementById('edit-var-type');
|
||||
|
||||
// Manejar campo DB
|
||||
if (area === 'db') {
|
||||
dbField.style.display = 'block';
|
||||
dbInput.required = true;
|
||||
} else {
|
||||
dbField.style.display = 'none';
|
||||
dbInput.required = false;
|
||||
dbInput.value = 1; // Valor por defecto para áreas no DB
|
||||
}
|
||||
|
||||
// Manejar campo Bit y restricciones de tipo de datos
|
||||
if (area === 'e' || area === 'a' || area === 'mb') {
|
||||
bitField.style.display = 'block';
|
||||
// Para áreas de bit, forzar tipo de dato a bool
|
||||
typeSelect.value = 'bool';
|
||||
// Deshabilitar otros tipos de datos para áreas de bit
|
||||
Array.from(typeSelect.options).forEach(option => {
|
||||
option.disabled = (option.value !== 'bool');
|
||||
});
|
||||
} else {
|
||||
bitField.style.display = 'none';
|
||||
// Re-habilitar todos los tipos de datos para áreas no-bit
|
||||
Array.from(typeSelect.options).forEach(option => {
|
||||
option.disabled = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Actualizar streaming para una variable
|
||||
function toggleStreaming(varName, enabled) {
|
||||
if (!currentDatasetId) {
|
||||
showMessage('No dataset selected', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/api/datasets/${currentDatasetId}/variables/${varName}/streaming`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ enabled: enabled })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus(); // Actualizar contador de variables de streaming
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error toggling streaming:', error);
|
||||
showMessage('Error updating streaming setting', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-start live display when dataset changes (if PLC is connected)
|
||||
function autoStartLiveDisplay() {
|
||||
if (currentDatasetId) {
|
||||
// Check if PLC is connected by fetching status
|
||||
fetch('/api/status')
|
||||
.then(response => response.json())
|
||||
.then(status => {
|
||||
if (status.plc_connected && !isStreamingVariables) {
|
||||
startVariableStreaming();
|
||||
showMessage('Live display started automatically for active dataset', 'info');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error checking PLC status:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Limpiar todos los valores de variables y establecer mensaje de estado
|
||||
function clearVariableValues(statusMessage = '--') {
|
||||
// Encontrar todas las celdas de valor y limpiarlas
|
||||
const valueCells = document.querySelectorAll('[id^="value-"]');
|
||||
valueCells.forEach(cell => {
|
||||
cell.textContent = statusMessage;
|
||||
cell.style.color = 'var(--pico-muted-color)';
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Iniciar streaming de variables en tiempo real
|
||||
function startVariableStreaming() {
|
||||
if (!currentDatasetId || isStreamingVariables) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cerrar conexión existente si hay alguna
|
||||
if (variableEventSource) {
|
||||
variableEventSource.close();
|
||||
}
|
||||
|
||||
// Crear nueva conexión EventSource
|
||||
variableEventSource = new EventSource(`/api/stream/variables?dataset_id=${currentDatasetId}&interval=1.0`);
|
||||
|
||||
variableEventSource.onopen = function (event) {
|
||||
console.log('Variable streaming connected');
|
||||
isStreamingVariables = true;
|
||||
updateStreamingIndicator(true);
|
||||
};
|
||||
|
||||
variableEventSource.onmessage = function (event) {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
switch (data.type) {
|
||||
case 'connected':
|
||||
console.log('Variable stream connected:', data.message);
|
||||
break;
|
||||
|
||||
case 'values':
|
||||
// Actualizar valores de variables en tiempo real desde caché
|
||||
updateVariableValuesFromStream(data);
|
||||
break;
|
||||
|
||||
case 'cache_error':
|
||||
console.error('Cache error in variable stream:', data.message);
|
||||
showMessage(`Cache error: ${data.message}`, 'error');
|
||||
clearVariableValues('CACHE ERROR');
|
||||
break;
|
||||
|
||||
case 'plc_disconnected':
|
||||
clearVariableValues('PLC OFFLINE');
|
||||
showMessage('PLC disconnected - cache not being populated', 'warning');
|
||||
break;
|
||||
|
||||
case 'dataset_inactive':
|
||||
clearVariableValues('DATASET INACTIVE');
|
||||
showMessage('Dataset is not active - activate to populate cache', 'warning');
|
||||
break;
|
||||
|
||||
case 'no_variables':
|
||||
clearVariableValues('NO VARIABLES');
|
||||
showMessage('No variables defined in this dataset', 'info');
|
||||
break;
|
||||
|
||||
case 'no_cache':
|
||||
clearVariableValues('READING...');
|
||||
const samplingInfo = data.sampling_interval ? ` (every ${data.sampling_interval}s)` : '';
|
||||
showMessage(`Waiting for cache to be populated${samplingInfo}`, 'info');
|
||||
break;
|
||||
|
||||
case 'stream_error':
|
||||
console.error('SSE stream error:', data.message);
|
||||
showMessage(`Streaming error: ${data.message}`, 'error');
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn('Unknown SSE message type:', data.type);
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing SSE data:', error);
|
||||
}
|
||||
};
|
||||
|
||||
variableEventSource.onerror = function (event) {
|
||||
console.error('Variable stream error:', event);
|
||||
isStreamingVariables = false;
|
||||
updateStreamingIndicator(false);
|
||||
|
||||
// Intentar reconectar después de un retraso
|
||||
setTimeout(() => {
|
||||
if (currentDatasetId) {
|
||||
startVariableStreaming();
|
||||
}
|
||||
}, 5000);
|
||||
};
|
||||
}
|
||||
|
||||
// Detener streaming de variables en tiempo real
|
||||
function stopVariableStreaming() {
|
||||
if (variableEventSource) {
|
||||
variableEventSource.close();
|
||||
variableEventSource = null;
|
||||
}
|
||||
isStreamingVariables = false;
|
||||
updateStreamingIndicator(false);
|
||||
}
|
||||
|
||||
// Actualizar valores de variables desde datos de streaming
|
||||
function updateVariableValuesFromStream(data) {
|
||||
const values = data.values;
|
||||
const timestamp = data.timestamp;
|
||||
const source = data.source;
|
||||
const stats = data.stats;
|
||||
|
||||
// Actualizar cada valor de variable
|
||||
Object.keys(values).forEach(varName => {
|
||||
const valueCell = document.getElementById(`value-${varName}`);
|
||||
if (valueCell) {
|
||||
const value = values[varName];
|
||||
valueCell.textContent = value;
|
||||
|
||||
// Código de color basado en el estado del valor
|
||||
if (value === 'ERROR' || value === 'FORMAT_ERROR') {
|
||||
valueCell.style.color = 'var(--pico-color-red-500)';
|
||||
valueCell.style.fontWeight = 'bold';
|
||||
} else {
|
||||
valueCell.style.color = 'var(--pico-color-green-600)';
|
||||
valueCell.style.fontWeight = 'bold';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Actualizar timestamp e información de origen
|
||||
const lastRefreshTime = document.getElementById('last-refresh-time');
|
||||
if (lastRefreshTime) {
|
||||
const sourceIcon = source === 'cache' ? '📊' : '🔗';
|
||||
const sourceText = source === 'cache' ? 'streaming cache' : 'direct PLC';
|
||||
|
||||
if (stats && stats.failed > 0) {
|
||||
lastRefreshTime.innerHTML = `
|
||||
<span style="color: var(--pico-color-green-600);">🔄 Live streaming</span><br/>
|
||||
<small style="color: var(--pico-color-amber-600);">
|
||||
⚠️ ${stats.success}/${stats.total} variables (${stats.failed} failed)
|
||||
</small><br/>
|
||||
<small style="color: var(--pico-muted-color);">
|
||||
${sourceIcon} ${sourceText} • ${new Date(timestamp).toLocaleTimeString()}
|
||||
</small>
|
||||
`;
|
||||
} else {
|
||||
lastRefreshTime.innerHTML = `
|
||||
<span style="color: var(--pico-color-green-600);">🔄 Live streaming</span><br/>
|
||||
<small style="color: var(--pico-color-green-600);">
|
||||
✅ All ${stats ? stats.success : 'N/A'} variables OK
|
||||
</small><br/>
|
||||
<small style="color: var(--pico-muted-color);">
|
||||
${sourceIcon} ${sourceText} • ${new Date(timestamp).toLocaleTimeString()}
|
||||
</small>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Actualizar indicador de streaming
|
||||
function updateStreamingIndicator(isStreaming) {
|
||||
const toggleBtn = document.getElementById('toggle-streaming-btn');
|
||||
if (toggleBtn) {
|
||||
if (isStreaming) {
|
||||
toggleBtn.innerHTML = '⏹️ Stop Live Display';
|
||||
toggleBtn.title = 'Stop live variable display';
|
||||
} else {
|
||||
toggleBtn.innerHTML = '▶️ Start Live Display';
|
||||
toggleBtn.title = 'Start live variable display';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Alternar streaming en tiempo real
|
||||
function toggleRealTimeStreaming() {
|
||||
if (isStreamingVariables) {
|
||||
stopVariableStreaming();
|
||||
showMessage('Real-time streaming stopped', 'info');
|
||||
} else {
|
||||
startVariableStreaming();
|
||||
showMessage('Real-time streaming started', 'success');
|
||||
}
|
||||
|
||||
// Actualizar texto del botón
|
||||
const toggleBtn = document.getElementById('toggle-streaming-btn');
|
||||
if (toggleBtn) {
|
||||
if (isStreamingVariables) {
|
||||
toggleBtn.innerHTML = '⏹️ Stop Live Streaming';
|
||||
} else {
|
||||
toggleBtn.innerHTML = '▶️ Start Live Streaming';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,856 +0,0 @@
|
|||
plotting.js:20 📈 Plot plot_16: Updating streaming chart with 1 datasets
|
||||
plotting.js:20 - Variable 1: UR29_Brix (31 points)
|
||||
plotting.js:20 📈 Plot plot_16: Added 0 new points to dataset 0
|
||||
debug-streaming.js:8 🔧 DIAGNÓSTICO DETALLADO DE STREAMING
|
||||
debug-streaming.js:9 ============================================================
|
||||
debug-streaming.js:23
|
||||
1️⃣ VERIFICANDO LIBRERÍAS...
|
||||
debug-streaming.js:30 Chart.js: ✅
|
||||
debug-streaming.js:31 ChartStreaming: ✅
|
||||
debug-streaming.js:32 PlotManager: ✅
|
||||
debug-streaming.js:41
|
||||
2️⃣ VERIFICANDO PLOT MANAGER...
|
||||
debug-streaming.js:44 ✅ PlotManager inicializado
|
||||
debug-streaming.js:45 📊 Sesiones activas: 1
|
||||
debug-streaming.js:59
|
||||
3️⃣ ANALIZANDO SESIÓN DE PLOT...
|
||||
debug-streaming.js:65 📈 Sesión encontrada: plot_16
|
||||
debug-streaming.js:78 Chart Config: {hasChart: true, chartType: 'line', scaleType: 'realtime', scaleConstructor: 'RealTimeScale', streamingEnabled: true, …}
|
||||
debug-streaming.js:85 ✅ Escala realtime configurada correctamente
|
||||
debug-streaming.js:93 ✅ Streaming habilitado en el chart
|
||||
debug-streaming.js:98
|
||||
4️⃣ VERIFICANDO DATOS DEL BACKEND...
|
||||
plotting.js:9 📈 Plot debugging enabled. Check console for detailed logs.
|
||||
debug-streaming.js:278 ⚡ TEST RÁPIDO DE STREAMING
|
||||
debug-streaming.js:279 ------------------------------
|
||||
debug-streaming.js:288 plotManager: ✅ true
|
||||
debug-streaming.js:288 sessions: ✅ 1
|
||||
debug-streaming.js:288 chartStreaming: ✅ true
|
||||
debug-streaming.js:293
|
||||
🧪 Probando sesión: plot_16
|
||||
debug-streaming.js:8 🔧 DIAGNÓSTICO DETALLADO DE STREAMING
|
||||
debug-streaming.js:9 ============================================================
|
||||
debug-streaming.js:23
|
||||
1️⃣ VERIFICANDO LIBRERÍAS...
|
||||
debug-streaming.js:30 Chart.js: ✅
|
||||
debug-streaming.js:31 ChartStreaming: ✅
|
||||
debug-streaming.js:32 PlotManager: ✅
|
||||
debug-streaming.js:41
|
||||
2️⃣ VERIFICANDO PLOT MANAGER...
|
||||
debug-streaming.js:44 ✅ PlotManager inicializado
|
||||
debug-streaming.js:45 📊 Sesiones activas: 1
|
||||
debug-streaming.js:59
|
||||
3️⃣ ANALIZANDO SESIÓN DE PLOT...
|
||||
debug-streaming.js:65 📈 Sesión encontrada: plot_16
|
||||
debug-streaming.js:78 Chart Config: {hasChart: true, chartType: 'line', scaleType: 'realtime', scaleConstructor: 'RealTimeScale', streamingEnabled: true, …}
|
||||
debug-streaming.js:85 ✅ Escala realtime configurada correctamente
|
||||
debug-streaming.js:93 ✅ Streaming habilitado en el chart
|
||||
debug-streaming.js:98
|
||||
4️⃣ VERIFICANDO DATOS DEL BACKEND...
|
||||
chartjs-plugin-streaming.js:75 📈 RealTimeScale DEBUG - scaleOptions: Proxy(Object) {_cacheable: false, _proxy: Proxy(Object), _context: {…}, _subProxy: undefined, _stack: Set(0), …}
|
||||
chartjs-plugin-streaming.js:76 📈 RealTimeScale DEBUG - me.options: Proxy(Object) {_cacheable: false, _proxy: Proxy(Object), _context: {…}, _subProxy: undefined, _stack: Set(0), …}
|
||||
chartjs-plugin-streaming.js:77 📈 RealTimeScale DEBUG - me.options.realtime: Proxy(Object) {_cacheable: false, _proxy: Proxy(Object), _context: {…}, _subProxy: undefined, _stack: Set(0), …}
|
||||
plotting.js:20 📈 Plot plot_16: Fetching data from backend...
|
||||
chartjs-plugin-streaming.js:81 📈 RealTimeScale DEBUG - onRefresh resolved: null object
|
||||
chartjs-plugin-streaming.js:93 📈 RealTimeScale initialized: {duration: 60000, refresh: 500, pause: false, hasOnRefresh: false}
|
||||
debug-streaming.js:8 🔧 DIAGNÓSTICO DETALLADO DE STREAMING
|
||||
debug-streaming.js:9 ============================================================
|
||||
debug-streaming.js:23
|
||||
1️⃣ VERIFICANDO LIBRERÍAS...
|
||||
debug-streaming.js:30 Chart.js: ✅
|
||||
debug-streaming.js:31 ChartStreaming: ✅
|
||||
debug-streaming.js:32 PlotManager: ✅
|
||||
debug-streaming.js:41
|
||||
2️⃣ VERIFICANDO PLOT MANAGER...
|
||||
debug-streaming.js:44 ✅ PlotManager inicializado
|
||||
debug-streaming.js:45 📊 Sesiones activas: 1
|
||||
|
||||
3️⃣ ANALIZANDO SESIÓN DE PLOT...
|
||||
debug-streaming.js:65 📈 Sesión encontrada: plot_16
|
||||
debug-streaming.js:78 Chart Config: {hasChart: true, chartType: 'line', scaleType: 'realtime', scaleConstructor: 'RealTimeScale', streamingEnabled: true, …}
|
||||
debug-streaming.js:85 ✅ Escala realtime configurada correctamente
|
||||
debug-streaming.js:93 ✅ Streaming habilitado en el chart
|
||||
debug-streaming.js:98
|
||||
4️⃣ VERIFICANDO DATOS DEL BACKEND...
|
||||
plotting.js:9 📈 Plot debugging enabled. Check console for detailed logs.
|
||||
debug-streaming.js:278 ⚡ TEST RÁPIDO DE STREAMING
|
||||
debug-streaming.js:279 ------------------------------
|
||||
debug-streaming.js:288 plotManager: ✅ true
|
||||
debug-streaming.js:288 sessions: ✅ 1
|
||||
debug-streaming.js:288 chartStreaming: ✅ true
|
||||
debug-streaming.js:293
|
||||
🧪 Probando sesión: plot_16
|
||||
debug-streaming.js:8 🔧 DIAGNÓSTICO DETALLADO DE STREAMING
|
||||
debug-streaming.js:9 ============================================================
|
||||
debug-streaming.js:23
|
||||
1️⃣ VERIFICANDO LIBRERÍAS...
|
||||
debug-streaming.js:30 Chart.js: ✅
|
||||
debug-streaming.js:31 ChartStreaming: ✅
|
||||
debug-streaming.js:32 PlotManager: ✅
|
||||
debug-streaming.js:41
|
||||
2️⃣ VERIFICANDO PLOT MANAGER...
|
||||
debug-streaming.js:44 ✅ PlotManager inicializado
|
||||
debug-streaming.js:45 📊 Sesiones activas: 1
|
||||
debug-streaming.js:59
|
||||
3️⃣ ANALIZANDO SESIÓN DE PLOT...
|
||||
debug-streaming.js:65 📈 Sesión encontrada: plot_16
|
||||
debug-streaming.js:78 Chart Config: {hasChart: true, chartType: 'line', scaleType: 'realtime', scaleConstructor: 'RealTimeScale', streamingEnabled: true, …}
|
||||
debug-streaming.js:85 ✅ Escala realtime configurada correctamente
|
||||
debug-streaming.js:93 ✅ Streaming habilitado en el chart
|
||||
debug-streaming.js:98
|
||||
4️⃣ VERIFICANDO DATOS DEL BACKEND...
|
||||
debug-streaming.js:103 📊 Respuesta del backend: {success: true, datasets: 1, totalPoints: 32, isActive: true, isPaused: false}
|
||||
debug-streaming.js:112 ✅ Backend devuelve datos
|
||||
debug-streaming.js:116 📈 Primer dataset: {label: 'UR29_Brix', dataPoints: 32, samplePoint: {…}}
|
||||
debug-streaming.js:123 ✅ Dataset tiene puntos de datos
|
||||
debug-streaming.js:130 ⏰ Análisis de timestamps: {firstPointTime: 1753115880879.299, currentTime: 1753115887307, differenceMs: 6427.700927734375, differenceSec: 6, isRecent: true}
|
||||
debug-streaming.js:156
|
||||
5️⃣ PROBANDO FUNCIONALIDAD STREAMING...
|
||||
chartjs-plugin-streaming.js:345 📈 Added point to dataset 0 (UR29_Brix): x=1753115887307, y=67.77492245225156
|
||||
debug-streaming.js:165 🧪 Test de addStreamingData: ✅
|
||||
debug-streaming.js:170 📊 Puntos después del test: 38
|
||||
debug-streaming.js:182
|
||||
6️⃣ RESUMEN DEL DIAGNÓSTICO
|
||||
debug-streaming.js:183 ========================================
|
||||
debug-streaming.js:186 🎉 No se encontraron errores graves
|
||||
debug-streaming.js:199
|
||||
7️⃣ PRÓXIMOS PASOS RECOMENDADOS:
|
||||
debug-streaming.js:213 🔧 4. Si persiste: plotManager.controlPlot("plot_16", "stop") y luego "start"
|
||||
plotting.js:20 📈 Plot plot_16: Streaming resumed
|
||||
🔧 DIAGNÓSTICO DETALLADO DE STREAMING
|
||||
============================================================
|
||||
|
||||
1️⃣ VERIFICANDO LIBRERÍAS...
|
||||
Chart.js: ✅
|
||||
ChartStreaming: ✅
|
||||
PlotManager: ✅
|
||||
|
||||
2️⃣ VERIFICANDO PLOT MANAGER...
|
||||
✅ PlotManager inicializado
|
||||
📊 Sesiones activas: 1
|
||||
|
||||
3️⃣ ANALIZANDO SESIÓN DE PLOT...
|
||||
📈 Sesión encontrada: plot_16
|
||||
Chart Config: {hasChart: true, chartType: 'line', scaleType: 'realtime', scaleConstructor: 'RealTimeScale', streamingEnabled: true, …}
|
||||
✅ Escala realtime configurada correctamente
|
||||
✅ Streaming habilitado en el chart
|
||||
|
||||
4️⃣ VERIFICANDO DATOS DEL BACKEND...
|
||||
plotting.js:9 📈 Plot debugging enabled. Check console for detailed logs.
|
||||
debug-streaming.js:278 ⚡ TEST RÁPIDO DE STREAMING
|
||||
debug-streaming.js:279 ------------------------------
|
||||
debug-streaming.js:288 plotManager: ✅ true
|
||||
debug-streaming.js:288 sessions: ✅ 1
|
||||
debug-streaming.js:288 chartStreaming: ✅ true
|
||||
debug-streaming.js:293
|
||||
🧪 Probando sesión: plot_16
|
||||
debug-streaming.js:8 🔧 DIAGNÓSTICO DETALLADO DE STREAMING
|
||||
debug-streaming.js:9 ============================================================
|
||||
debug-streaming.js:23
|
||||
1️⃣ VERIFICANDO LIBRERÍAS...
|
||||
debug-streaming.js:30 Chart.js: ✅
|
||||
debug-streaming.js:31 ChartStreaming: ✅
|
||||
debug-streaming.js:32 PlotManager: ✅
|
||||
debug-streaming.js:41
|
||||
2️⃣ VERIFICANDO PLOT MANAGER...
|
||||
debug-streaming.js:44 ✅ PlotManager inicializado
|
||||
debug-streaming.js:45 📊 Sesiones activas: 1
|
||||
debug-streaming.js:59
|
||||
3️⃣ ANALIZANDO SESIÓN DE PLOT...
|
||||
debug-streaming.js:65 📈 Sesión encontrada: plot_16
|
||||
debug-streaming.js:78 Chart Config: {hasChart: true, chartType: 'line', scaleType: 'realtime', scaleConstructor: 'RealTimeScale', streamingEnabled: true, …}
|
||||
debug-streaming.js:85 ✅ Escala realtime configurada correctamente
|
||||
debug-streaming.js:93 ✅ Streaming habilitado en el chart
|
||||
debug-streaming.js:98
|
||||
4️⃣ VERIFICANDO DATOS DEL BACKEND...
|
||||
chartjs-plugin-streaming.js:75 📈 RealTimeScale DEBUG - scaleOptions: Proxy(Object) {_cacheable: false, _proxy: Proxy(Object), _context: {…}, _subProxy: undefined, _stack: Set(0), …}
|
||||
chartjs-plugin-streaming.js:76 📈 RealTimeScale DEBUG - me.options: Proxy(Object) {_cacheable: false, _proxy: Proxy(Object), _context: {…}, _subProxy: undefined, _stack: Set(0), …}
|
||||
chartjs-plugin-streaming.js:77 📈 RealTimeScale DEBUG - me.options.realtime: Proxy(Object) {_cacheable: false, _proxy: Proxy(Object), _context: {…}, _subProxy: undefined, _stack: Set(0), …}
|
||||
chartjs-plugin-streaming.js:81 📈 RealTimeScale DEBUG - onRefresh resolved: null object
|
||||
chartjs-plugin-streaming.js:93 📈 RealTimeScale initialized: {duration: 60000, refresh: 500, pause: false, hasOnRefresh: false}
|
||||
debug-streaming.js:103 📊 Respuesta del backend: {success: true, datasets: 1, totalPoints: 34, isActive: true, isPaused: false}
|
||||
debug-streaming.js:112 ✅ Backend devuelve datos
|
||||
debug-streaming.js:116 📈 Primer dataset: {label: 'UR29_Brix', dataPoints: 34, samplePoint: {…}}
|
||||
debug-streaming.js:123 ✅ Dataset tiene puntos de datos
|
||||
debug-streaming.js:130 ⏰ Análisis de timestamps: {firstPointTime: 1753115880879.299, currentTime: 1753115887617, differenceMs: 6737.700927734375, differenceSec: 7, isRecent: true}
|
||||
debug-streaming.js:156
|
||||
5️⃣ PROBANDO FUNCIONALIDAD STREAMING...
|
||||
chartjs-plugin-streaming.js:345 📈 Added point to dataset 0 (UR29_Brix): x=1753115887617, y=11.743880180299548
|
||||
debug-streaming.js:165 🧪 Test de addStreamingData: ✅
|
||||
debug-streaming.js:170 📊 Puntos después del test: 39
|
||||
debug-streaming.js:182
|
||||
6️⃣ RESUMEN DEL DIAGNÓSTICO
|
||||
debug-streaming.js:183 ========================================
|
||||
debug-streaming.js:186 🎉 No se encontraron errores graves
|
||||
debug-streaming.js:199
|
||||
7️⃣ PRÓXIMOS PASOS RECOMENDADOS:
|
||||
debug-streaming.js:213 🔧 4. Si persiste: plotManager.controlPlot("plot_16", "stop") y luego "start"
|
||||
debug-streaming.js:8 🔧 DIAGNÓSTICO DETALLADO DE STREAMING
|
||||
debug-streaming.js:9 ============================================================
|
||||
debug-streaming.js:23
|
||||
1️⃣ VERIFICANDO LIBRERÍAS...
|
||||
debug-streaming.js:30 Chart.js: ✅
|
||||
debug-streaming.js:31 ChartStreaming: ✅
|
||||
debug-streaming.js:32 PlotManager: ✅
|
||||
debug-streaming.js:41
|
||||
2️⃣ VERIFICANDO PLOT MANAGER...
|
||||
debug-streaming.js:44 ✅ PlotManager inicializado
|
||||
debug-streaming.js:45 📊 Sesiones activas: 1
|
||||
debug-streaming.js:59
|
||||
3️⃣ ANALIZANDO SESIÓN DE PLOT...
|
||||
debug-streaming.js:65 📈 Sesión encontrada: plot_16
|
||||
debug-streaming.js:78 Chart Config: {hasChart: true, chartType: 'line', scaleType: 'realtime', scaleConstructor: 'RealTimeScale', streamingEnabled: true, …}
|
||||
debug-streaming.js:85 ✅ Escala realtime configurada correctamente
|
||||
debug-streaming.js:93 ✅ Streaming habilitado en el chart
|
||||
debug-streaming.js:98
|
||||
4️⃣ VERIFICANDO DATOS DEL BACKEND...
|
||||
📈 Plot debugging enabled. Check console for detailed logs.
|
||||
⚡ TEST RÁPIDO DE STREAMING
|
||||
debug-streaming.js:279 ------------------------------
|
||||
debug-streaming.js:288 plotManager: ✅ true
|
||||
debug-streaming.js:288 sessions: ✅ 1
|
||||
debug-streaming.js:288 chartStreaming: ✅ true
|
||||
debug-streaming.js:293
|
||||
🧪 Probando sesión: plot_16
|
||||
debug-streaming.js:8 🔧 DIAGNÓSTICO DETALLADO DE STREAMING
|
||||
debug-streaming.js:9 ============================================================
|
||||
debug-streaming.js:23
|
||||
1️⃣ VERIFICANDO LIBRERÍAS...
|
||||
debug-streaming.js:30 Chart.js: ✅
|
||||
debug-streaming.js:31 ChartStreaming: ✅
|
||||
debug-streaming.js:32 PlotManager: ✅
|
||||
debug-streaming.js:41
|
||||
2️⃣ VERIFICANDO PLOT MANAGER...
|
||||
debug-streaming.js:44 ✅ PlotManager inicializado
|
||||
debug-streaming.js:45 📊 Sesiones activas: 1
|
||||
debug-streaming.js:59
|
||||
3️⃣ ANALIZANDO SESIÓN DE PLOT...
|
||||
debug-streaming.js:65 📈 Sesión encontrada: plot_16
|
||||
debug-streaming.js:78 Chart Config: {hasChart: true, chartType: 'line', scaleType: 'realtime', scaleConstructor: 'RealTimeScale', streamingEnabled: true, …}
|
||||
debug-streaming.js:85 ✅ Escala realtime configurada correctamente
|
||||
debug-streaming.js:93 ✅ Streaming habilitado en el chart
|
||||
debug-streaming.js:98
|
||||
4️⃣ VERIFICANDO DATOS DEL BACKEND...
|
||||
plotting.js:20 📈 Plot plot_16: Received data: {data_points_count: 35, datasets: Array(1), is_active: true, is_paused: false, last_update: 1753115887.7399745, …}
|
||||
plotting.js:20 📈 Plot plot_16: Processing 1 datasets for streaming
|
||||
plotting.js:20 📈 Plot plot_16: Adding 17 new points for UR29_Brix
|
||||
chartjs-plugin-streaming.js:345 📈 Added point to dataset 0 (UR29_Brix): x=1753115884519.0737, y=44.65407943725586
|
||||
chartjs-plugin-streaming.js:345 📈 Added point to dataset 0 (UR29_Brix): x=1753115884719.9353, y=46.29325866699219
|
||||
chartjs-plugin-streaming.js:345 📈 Added point to dataset 0 (UR29_Brix): x=1753115884920.8208, y=47.510704040527344
|
||||
chartjs-plugin-streaming.js:345 📈 Added point to dataset 0 (UR29_Brix): x=1753115885121.4167, y=47.90060806274414
|
||||
chartjs-plugin-streaming.js:345 📈 Added point to dataset 0 (UR29_Brix): x=1753115885323.425, y=47.90060806274414
|
||||
chartjs-plugin-streaming.js:345 📈 Added point to dataset 0 (UR29_Brix): x=1753115885522.0952, y=49.372684478759766
|
||||
chartjs-plugin-streaming.js:345 📈 Added point to dataset 0 (UR29_Brix): x=1753115885724.864, y=50.64583206176758
|
||||
chartjs-plugin-streaming.js:345 📈 Added point to dataset 0 (UR29_Brix): x=1753115885925.8164, y=50.68561935424805
|
||||
chartjs-plugin-streaming.js:345 📈 Added point to dataset 0 (UR29_Brix): x=1753115886127.653, y=51.82349395751953
|
||||
chartjs-plugin-streaming.js:345 📈 Added point to dataset 0 (UR29_Brix): x=1753115886327.9204, y=51.831451416015625
|
||||
chartjs-plugin-streaming.js:345 📈 Added point to dataset 0 (UR29_Brix): x=1753115886529.75, y=52.46802520751953
|
||||
chartjs-plugin-streaming.js:345 📈 Added point to dataset 0 (UR29_Brix): x=1753115886731.3887, y=52.810184478759766
|
||||
chartjs-plugin-streaming.js:345 📈 Added point to dataset 0 (UR29_Brix): x=1753115886934.3843, y=53.60590362548828
|
||||
chartjs-plugin-streaming.js:345 📈 Added point to dataset 0 (UR29_Brix): x=1753115887135.6711, y=53.60590362548828
|
||||
chartjs-plugin-streaming.js:345 📈 Added point to dataset 0 (UR29_Brix): x=1753115887337.3464, y=54.2265625
|
||||
chartjs-plugin-streaming.js:345 📈 Added point to dataset 0 (UR29_Brix): x=1753115887538.3455, y=54.2265625
|
||||
chartjs-plugin-streaming.js:345 📈 Added point to dataset 0 (UR29_Brix): x=1753115887739.9746, y=54.65625
|
||||
plotting.js:20 📈 Plot plot_16: Updated last timestamp for UR29_Brix to 1753115887739.9746
|
||||
chartjs-plugin-streaming.js:75 📈 RealTimeScale DEBUG - scaleOptions: Proxy(Object) {_cacheable: false, _proxy: Proxy(Object), _context: {…}, _subProxy: undefined, _stack: Set(0), …}
|
||||
chartjs-plugin-streaming.js:76 📈 RealTimeScale DEBUG - me.options: Proxy(Object) {_cacheable: false, _proxy: Proxy(Object), _context: {…}, _subProxy: undefined, _stack: Set(0), …}
|
||||
chartjs-plugin-streaming.js:77 📈 RealTimeScale DEBUG - me.options.realtime: Proxy(Object) {_cacheable: false, _proxy: Proxy(Object), _context: {…}, _subProxy: undefined, _stack: Set(0), …}
|
||||
plotting.js:20 📈 Plot plot_16: Fetching data from backend...
|
||||
chartjs-plugin-streaming.js:81 📈 RealTimeScale DEBUG - onRefresh resolved: null object
|
||||
chartjs-plugin-streaming.js:93 📈 RealTimeScale initialized: {duration: 60000, refresh: 500, pause: false, hasOnRefresh: false}
|
||||
debug-streaming.js:8 🔧 DIAGNÓSTICO DETALLADO DE STREAMING
|
||||
debug-streaming.js:9 ============================================================
|
||||
debug-streaming.js:23
|
||||
1️⃣ VERIFICANDO LIBRERÍAS...
|
||||
debug-streaming.js:30 Chart.js: ✅
|
||||
debug-streaming.js:31 ChartStreaming: ✅
|
||||
debug-streaming.js:32 PlotManager: ✅
|
||||
debug-streaming.js:41
|
||||
2️⃣ VERIFICANDO PLOT MANAGER...
|
||||
debug-streaming.js:44 ✅ PlotManager inicializado
|
||||
debug-streaming.js:45 📊 Sesiones activas: 1
|
||||
debug-streaming.js:59
|
||||
3️⃣ ANALIZANDO SESIÓN DE PLOT...
|
||||
debug-streaming.js:65 📈 Sesión encontrada: plot_16
|
||||
debug-streaming.js:78 Chart Config: {hasChart: true, chartType: 'line', scaleType: 'realtime', scaleConstructor: 'RealTimeScale', streamingEnabled: true, …}
|
||||
debug-streaming.js:85 ✅ Escala realtime configurada correctamente
|
||||
debug-streaming.js:93 ✅ Streaming habilitado en el chart
|
||||
debug-streaming.js:98
|
||||
4️⃣ VERIFICANDO DATOS DEL BACKEND...
|
||||
plotting.js:9 📈 Plot debugging enabled. Check console for detailed logs.
|
||||
debug-streaming.js:278 ⚡ TEST RÁPIDO DE STREAMING
|
||||
debug-streaming.js:279 ------------------------------
|
||||
debug-streaming.js:288 plotManager: ✅ true
|
||||
debug-streaming.js:288 sessions: ✅ 1
|
||||
debug-streaming.js:288 chartStreaming: ✅ true
|
||||
debug-streaming.js:293
|
||||
🧪 Probando sesión: plot_16
|
||||
debug-streaming.js:8 🔧 DIAGNÓSTICO DETALLADO DE STREAMING
|
||||
debug-streaming.js:9 ============================================================
|
||||
debug-streaming.js:23
|
||||
1️⃣ VERIFICANDO LIBRERÍAS...
|
||||
debug-streaming.js:30 Chart.js: ✅
|
||||
debug-streaming.js:31 ChartStreaming: ✅
|
||||
debug-streaming.js:32 PlotManager: ✅
|
||||
debug-streaming.js:41
|
||||
2️⃣ VERIFICANDO PLOT MANAGER...
|
||||
debug-streaming.js:44 ✅ PlotManager inicializado
|
||||
debug-streaming.js:45 📊 Sesiones activas: 1
|
||||
debug-streaming.js:59
|
||||
3️⃣ ANALIZANDO SESIÓN DE PLOT...
|
||||
debug-streaming.js:65 📈 Sesión encontrada: plot_16
|
||||
debug-streaming.js:78 Chart Config: {hasChart: true, chartType: 'line', scaleType: 'realtime', scaleConstructor: 'RealTimeScale', streamingEnabled: true, …}
|
||||
debug-streaming.js:85 ✅ Escala realtime configurada correctamente
|
||||
debug-streaming.js:93 ✅ Streaming habilitado en el chart
|
||||
debug-streaming.js:98
|
||||
4️⃣ VERIFICANDO DATOS DEL BACKEND...
|
||||
debug-streaming.js:103 📊 Respuesta del backend: {success: true, datasets: 1, totalPoints: 37, isActive: true, isPaused: false}
|
||||
debug-streaming.js:112 ✅ Backend devuelve datos
|
||||
debug-streaming.js:116 📈 Primer dataset: {label: 'UR29_Brix', dataPoints: 37, samplePoint: {…}}
|
||||
debug-streaming.js:123 ✅ Dataset tiene puntos de datos
|
||||
debug-streaming.js:130 ⏰ Análisis de timestamps: {firstPointTime: 1753115880879.299, currentTime: 1753115888236, differenceMs: 7356.700927734375, differenceSec: 7, isRecent: true}
|
||||
debug-streaming.js:156
|
||||
5️⃣ PROBANDO FUNCIONALIDAD STREAMING...
|
||||
chartjs-plugin-streaming.js:345 📈 Added point to dataset 0 (UR29_Brix): x=1753115888237, y=23.50854350922291
|
||||
debug-streaming.js:165 🧪 Test de addStreamingData: ✅
|
||||
debug-streaming.js:170 📊 Puntos después del test: 57
|
||||
debug-streaming.js:182
|
||||
6️⃣ RESUMEN DEL DIAGNÓSTICO
|
||||
debug-streaming.js:183 ========================================
|
||||
debug-streaming.js:186 🎉 No se encontraron errores graves
|
||||
debug-streaming.js:199
|
||||
7️⃣ PRÓXIMOS PASOS RECOMENDADOS:
|
||||
debug-streaming.js:213 🔧 4. Si persiste: plotManager.controlPlot("plot_16", "stop") y luego "start"
|
||||
debug-streaming.js:8 🔧 DIAGNÓSTICO DETALLADO DE STREAMING
|
||||
debug-streaming.js:9 ============================================================
|
||||
debug-streaming.js:23
|
||||
1️⃣ VERIFICANDO LIBRERÍAS...
|
||||
debug-streaming.js:30 Chart.js: ✅
|
||||
debug-streaming.js:31 ChartStreaming: ✅
|
||||
debug-streaming.js:32 PlotManager: ✅
|
||||
debug-streaming.js:41
|
||||
2️⃣ VERIFICANDO PLOT MANAGER...
|
||||
debug-streaming.js:44 ✅ PlotManager inicializado
|
||||
debug-streaming.js:45 📊 Sesiones activas: 1
|
||||
debug-streaming.js:59
|
||||
3️⃣ ANALIZANDO SESIÓN DE PLOT...
|
||||
debug-streaming.js:65 📈 Sesión encontrada: plot_16
|
||||
debug-streaming.js:78 Chart Config: {hasChart: true, chartType: 'line', scaleType: 'realtime', scaleConstructor: 'RealTimeScale', streamingEnabled: true, …}
|
||||
debug-streaming.js:85 ✅ Escala realtime configurada correctamente
|
||||
debug-streaming.js:93 ✅ Streaming habilitado en el chart
|
||||
debug-streaming.js:98
|
||||
4️⃣ VERIFICANDO DATOS DEL BACKEND...
|
||||
plotting.js:9 📈 Plot debugging enabled. Check console for detailed logs.
|
||||
debug-streaming.js:278 ⚡ TEST RÁPIDO DE STREAMING
|
||||
debug-streaming.js:279 ------------------------------
|
||||
debug-streaming.js:288 plotManager: ✅ true
|
||||
debug-streaming.js:288 sessions: ✅ 1
|
||||
debug-streaming.js:288 chartStreaming: ✅ true
|
||||
debug-streaming.js:293
|
||||
🧪 Probando sesión: plot_16
|
||||
debug-streaming.js:8 🔧 DIAGNÓSTICO DETALLADO DE STREAMING
|
||||
debug-streaming.js:9 ============================================================
|
||||
debug-streaming.js:23
|
||||
1️⃣ VERIFICANDO LIBRERÍAS...
|
||||
debug-streaming.js:30 Chart.js: ✅
|
||||
debug-streaming.js:31 ChartStreaming: ✅
|
||||
debug-streaming.js:32 PlotManager: ✅
|
||||
debug-streaming.js:41
|
||||
2️⃣ VERIFICANDO PLOT MANAGER...
|
||||
debug-streaming.js:44 ✅ PlotManager inicializado
|
||||
debug-streaming.js:45 📊 Sesiones activas: 1
|
||||
debug-streaming.js:59
|
||||
3️⃣ ANALIZANDO SESIÓN DE PLOT...
|
||||
debug-streaming.js:65 📈 Sesión encontrada: plot_16
|
||||
debug-streaming.js:78 Chart Config: {hasChart: true, chartType: 'line', scaleType: 'realtime', scaleConstructor: 'RealTimeScale', streamingEnabled: true, …}
|
||||
debug-streaming.js:85 ✅ Escala realtime configurada correctamente
|
||||
debug-streaming.js:93 ✅ Streaming habilitado en el chart
|
||||
debug-streaming.js:98
|
||||
4️⃣ VERIFICANDO DATOS DEL BACKEND...
|
||||
debug-streaming.js:103 📊 Respuesta del backend: {success: true, datasets: 1, totalPoints: 38, isActive: true, isPaused: false}
|
||||
debug-streaming.js:112 ✅ Backend devuelve datos
|
||||
debug-streaming.js:116 📈 Primer dataset: {label: 'UR29_Brix', dataPoints: 38, samplePoint: {…}}
|
||||
debug-streaming.js:123 ✅ Dataset tiene puntos de datos
|
||||
debug-streaming.js:130 ⏰ Análisis de timestamps: {firstPointTime: 1753115880879.299, currentTime: 1753115888548, differenceMs: 7668.700927734375, differenceSec: 8, isRecent: true}
|
||||
debug-streaming.js:156
|
||||
5️⃣ PROBANDO FUNCIONALIDAD STREAMING...
|
||||
chartjs-plugin-streaming.js:345 📈 Added point to dataset 0 (UR29_Brix): x=1753115888548, y=4.850139391135455
|
||||
debug-streaming.js:165 🧪 Test de addStreamingData: ✅
|
||||
debug-streaming.js:170 📊 Puntos después del test: 58
|
||||
debug-streaming.js:182
|
||||
6️⃣ RESUMEN DEL DIAGNÓSTICO
|
||||
debug-streaming.js:183 ========================================
|
||||
debug-streaming.js:186 🎉 No se encontraron errores graves
|
||||
debug-streaming.js:199
|
||||
7️⃣ PRÓXIMOS PASOS RECOMENDADOS:
|
||||
debug-streaming.js:213 🔧 4. Si persiste: plotManager.controlPlot("plot_16", "stop") y luego "start"
|
||||
chartjs-plugin-streaming.js:75 📈 RealTimeScale DEBUG - scaleOptions: Proxy(Object) {_cacheable: false, _proxy: Proxy(Object), _context: {…}, _subProxy: undefined, _stack: Set(0), …}
|
||||
chartjs-plugin-streaming.js:76 📈 RealTimeScale DEBUG - me.options: Proxy(Object) {_cacheable: false, _proxy: Proxy(Object), _context: {…}, _subProxy: undefined, _stack: Set(0), …}
|
||||
chartjs-plugin-streaming.js:77 📈 RealTimeScale DEBUG - me.options.realtime: Proxy(Object) {_cacheable: false, _proxy: Proxy(Object), _context: {…}, _subProxy: undefined, _stack: Set(0), …}
|
||||
chartjs-plugin-streaming.js:81 📈 RealTimeScale DEBUG - onRefresh resolved: null object
|
||||
chartjs-plugin-streaming.js:93 📈 RealTimeScale initialized: {duration: 60000, refresh: 500, pause: false, hasOnRefresh: false}
|
||||
debug-streaming.js:8 🔧 DIAGNÓSTICO DETALLADO DE STREAMING
|
||||
debug-streaming.js:9 ============================================================
|
||||
debug-streaming.js:23
|
||||
1️⃣ VERIFICANDO LIBRERÍAS...
|
||||
debug-streaming.js:30 Chart.js: ✅
|
||||
debug-streaming.js:31 ChartStreaming: ✅
|
||||
debug-streaming.js:32 PlotManager: ✅
|
||||
debug-streaming.js:41
|
||||
2️⃣ VERIFICANDO PLOT MANAGER...
|
||||
debug-streaming.js:44 ✅ PlotManager inicializado
|
||||
debug-streaming.js:45 📊 Sesiones activas: 1
|
||||
debug-streaming.js:59
|
||||
3️⃣ ANALIZANDO SESIÓN DE PLOT...
|
||||
debug-streaming.js:65 📈 Sesión encontrada: plot_16
|
||||
debug-streaming.js:78 Chart Config: {hasChart: true, chartType: 'line', scaleType: 'realtime', scaleConstructor: 'RealTimeScale', streamingEnabled: true, …}
|
||||
debug-streaming.js:85 ✅ Escala realtime configurada correctamente
|
||||
debug-streaming.js:93 ✅ Streaming habilitado en el chart
|
||||
debug-streaming.js:98
|
||||
4️⃣ VERIFICANDO DATOS DEL BACKEND...
|
||||
plotting.js:9 📈 Plot debugging enabled. Check console for detailed logs.
|
||||
debug-streaming.js:278 ⚡ TEST RÁPIDO DE STREAMING
|
||||
debug-streaming.js:279 ------------------------------
|
||||
debug-streaming.js:288 plotManager: ✅ true
|
||||
debug-streaming.js:288 sessions: ✅ 1
|
||||
debug-streaming.js:288 chartStreaming: ✅ true
|
||||
debug-streaming.js:293
|
||||
🧪 Probando sesión: plot_16
|
||||
debug-streaming.js:8 🔧 DIAGNÓSTICO DETALLADO DE STREAMING
|
||||
debug-streaming.js:9 ============================================================
|
||||
debug-streaming.js:23
|
||||
1️⃣ VERIFICANDO LIBRERÍAS...
|
||||
debug-streaming.js:30 Chart.js: ✅
|
||||
debug-streaming.js:31 ChartStreaming: ✅
|
||||
debug-streaming.js:32 PlotManager: ✅
|
||||
debug-streaming.js:41
|
||||
2️⃣ VERIFICANDO PLOT MANAGER...
|
||||
debug-streaming.js:44 ✅ PlotManager inicializado
|
||||
debug-streaming.js:45 📊 Sesiones activas: 1
|
||||
debug-streaming.js:59
|
||||
3️⃣ ANALIZANDO SESIÓN DE PLOT...
|
||||
debug-streaming.js:65 📈 Sesión encontrada: plot_16
|
||||
debug-streaming.js:78 Chart Config: {hasChart: true, chartType: 'line', scaleType: 'realtime', scaleConstructor: 'RealTimeScale', streamingEnabled: true, …}
|
||||
debug-streaming.js:85 ✅ Escala realtime configurada correctamente
|
||||
debug-streaming.js:93 ✅ Streaming habilitado en el chart
|
||||
debug-streaming.js:98
|
||||
4️⃣ VERIFICANDO DATOS DEL BACKEND...
|
||||
plotting.js:20 📈 Plot plot_16: Updating streaming chart with 1 datasets
|
||||
plotting.js:20 - Variable 1: UR29_Brix (40 points)
|
||||
plotting.js:20 📈 Plot plot_16: Added 0 new points to dataset 0
|
||||
debug-streaming.js:8 🔧 DIAGNÓSTICO DETALLADO DE STREAMING
|
||||
debug-streaming.js:9 ============================================================
|
||||
debug-streaming.js:23
|
||||
1️⃣ VERIFICANDO LIBRERÍAS...
|
||||
debug-streaming.js:30 Chart.js: ✅
|
||||
debug-streaming.js:31 ChartStreaming: ✅
|
||||
debug-streaming.js:32 PlotManager: ✅
|
||||
debug-streaming.js:41
|
||||
2️⃣ VERIFICANDO PLOT MANAGER...
|
||||
debug-streaming.js:44 ✅ PlotManager inicializado
|
||||
debug-streaming.js:45 📊 Sesiones activas: 1
|
||||
debug-streaming.js:59
|
||||
3️⃣ ANALIZANDO SESIÓN DE PLOT...
|
||||
debug-streaming.js:65 📈 Sesión encontrada: plot_16
|
||||
debug-streaming.js:78 Chart Config: {hasChart: true, chartType: 'line', scaleType: 'realtime', scaleConstructor: 'RealTimeScale', streamingEnabled: true, …}
|
||||
debug-streaming.js:85 ✅ Escala realtime configurada correctamente
|
||||
debug-streaming.js:93 ✅ Streaming habilitado en el chart
|
||||
debug-streaming.js:98
|
||||
4️⃣ VERIFICANDO DATOS DEL BACKEND...
|
||||
plotting.js:9 📈 Plot debugging enabled. Check console for detailed logs.
|
||||
debug-streaming.js:278 ⚡ TEST RÁPIDO DE STREAMING
|
||||
debug-streaming.js:279 ------------------------------
|
||||
debug-streaming.js:288 plotManager: ✅ true
|
||||
debug-streaming.js:288 sessions: ✅ 1
|
||||
debug-streaming.js:288 chartStreaming: ✅ true
|
||||
debug-streaming.js:293
|
||||
🧪 Probando sesión: plot_16
|
||||
debug-streaming.js:8 🔧 DIAGNÓSTICO DETALLADO DE STREAMING
|
||||
debug-streaming.js:9 ============================================================
|
||||
debug-streaming.js:23
|
||||
1️⃣ VERIFICANDO LIBRERÍAS...
|
||||
debug-streaming.js:30 Chart.js: ✅
|
||||
debug-streaming.js:31 ChartStreaming: ✅
|
||||
debug-streaming.js:32 PlotManager: ✅
|
||||
debug-streaming.js:41
|
||||
2️⃣ VERIFICANDO PLOT MANAGER...
|
||||
debug-streaming.js:44 ✅ PlotManager inicializado
|
||||
debug-streaming.js:45 📊 Sesiones activas: 1
|
||||
debug-streaming.js:59
|
||||
3️⃣ ANALIZANDO SESIÓN DE PLOT...
|
||||
debug-streaming.js:65 📈 Sesión encontrada: plot_16
|
||||
debug-streaming.js:78 Chart Config: {hasChart: true, chartType: 'line', scaleType: 'realtime', scaleConstructor: 'RealTimeScale', streamingEnabled: true, …}
|
||||
debug-streaming.js:85 ✅ Escala realtime configurada correctamente
|
||||
debug-streaming.js:93 ✅ Streaming habilitado en el chart
|
||||
debug-streaming.js:98
|
||||
4️⃣ VERIFICANDO DATOS DEL BACKEND...
|
||||
chartjs-plugin-streaming.js:75 📈 RealTimeScale DEBUG - scaleOptions: Proxy(Object) {_cacheable: false, _proxy: Proxy(Object), _context: {…}, _subProxy: undefined, _stack: Set(0), …}
|
||||
chartjs-plugin-streaming.js:76 📈 RealTimeScale DEBUG - me.options: Proxy(Object) {_cacheable: false, _proxy: Proxy(Object), _context: {…}, _subProxy: undefined, _stack: Set(0), …}
|
||||
chartjs-plugin-streaming.js:77 📈 RealTimeScale DEBUG - me.options.realtime: Proxy(Object) {_cacheable: false, _proxy: Proxy(Object), _context: {…}, _subProxy: undefined, _stack: Set(0), …}
|
||||
plotting.js:20 📈 Plot plot_16: Fetching data from backend...
|
||||
chartjs-plugin-streaming.js:81 📈 RealTimeScale DEBUG - onRefresh resolved: null object
|
||||
chartjs-plugin-streaming.js:93 📈 RealTimeScale initialized: {duration: 60000, refresh: 500, pause: false, hasOnRefresh: false}
|
||||
📊 Respuesta del backend: {success: true, datasets: 1, totalPoints: 42, isActive: true, isPaused: false}
|
||||
✅ Backend devuelve datos
|
||||
📈 Primer dataset: {label: 'UR29_Brix', dataPoints: 42, samplePoint: {…}}
|
||||
✅ Dataset tiene puntos de datos
|
||||
⏰ Análisis de timestamps: {firstPointTime: 1753115880879.299, currentTime: 1753115889176, differenceMs: 8296.700927734375, differenceSec: 8, isRecent: true}
|
||||
|
||||
5️⃣ PROBANDO FUNCIONALIDAD STREAMING...
|
||||
📈 Added point to dataset 0 (UR29_Brix): x=1753115889176, y=42.83092459112214
|
||||
🧪 Test de addStreamingData: ✅
|
||||
📊 Puntos después del test: 59
|
||||
|
||||
6️⃣ RESUMEN DEL DIAGNÓSTICO
|
||||
debug-streaming.js:183 ========================================
|
||||
debug-streaming.js:186 🎉 No se encontraron errores graves
|
||||
debug-streaming.js:199
|
||||
7️⃣ PRÓXIMOS PASOS RECOMENDADOS:
|
||||
debug-streaming.js:213 🔧 4. Si persiste: plotManager.controlPlot("plot_16", "stop") y luego "start"
|
||||
debug-streaming.js:8 🔧 DIAGNÓSTICO DETALLADO DE STREAMING
|
||||
debug-streaming.js:9 ============================================================
|
||||
debug-streaming.js:23
|
||||
1️⃣ VERIFICANDO LIBRERÍAS...
|
||||
debug-streaming.js:30 Chart.js: ✅
|
||||
debug-streaming.js:31 ChartStreaming: ✅
|
||||
debug-streaming.js:32 PlotManager: ✅
|
||||
debug-streaming.js:41
|
||||
2️⃣ VERIFICANDO PLOT MANAGER...
|
||||
debug-streaming.js:44 ✅ PlotManager inicializado
|
||||
debug-streaming.js:45 📊 Sesiones activas: 1
|
||||
debug-streaming.js:59
|
||||
3️⃣ ANALIZANDO SESIÓN DE PLOT...
|
||||
debug-streaming.js:65 📈 Sesión encontrada: plot_16
|
||||
debug-streaming.js:78 Chart Config: {hasChart: true, chartType: 'line', scaleType: 'realtime', scaleConstructor: 'RealTimeScale', streamingEnabled: true, …}
|
||||
debug-streaming.js:85 ✅ Escala realtime configurada correctamente
|
||||
debug-streaming.js:93 ✅ Streaming habilitado en el chart
|
||||
debug-streaming.js:98
|
||||
4️⃣ VERIFICANDO DATOS DEL BACKEND...
|
||||
plotting.js:9 📈 Plot debugging enabled. Check console for detailed logs.
|
||||
debug-streaming.js:278 ⚡ TEST RÁPIDO DE STREAMING
|
||||
debug-streaming.js:279 ------------------------------
|
||||
debug-streaming.js:288 plotManager: ✅ true
|
||||
debug-streaming.js:288 sessions: ✅ 1
|
||||
debug-streaming.js:288 chartStreaming: ✅ true
|
||||
debug-streaming.js:293
|
||||
🧪 Probando sesión: plot_16
|
||||
debug-streaming.js:8 🔧 DIAGNÓSTICO DETALLADO DE STREAMING
|
||||
debug-streaming.js:9 ============================================================
|
||||
debug-streaming.js:23
|
||||
1️⃣ VERIFICANDO LIBRERÍAS...
|
||||
debug-streaming.js:30 Chart.js: ✅
|
||||
debug-streaming.js:31 ChartStreaming: ✅
|
||||
debug-streaming.js:32 PlotManager: ✅
|
||||
debug-streaming.js:41
|
||||
2️⃣ VERIFICANDO PLOT MANAGER...
|
||||
debug-streaming.js:44 ✅ PlotManager inicializado
|
||||
debug-streaming.js:45 📊 Sesiones activas: 1
|
||||
debug-streaming.js:59
|
||||
3️⃣ ANALIZANDO SESIÓN DE PLOT...
|
||||
debug-streaming.js:65 📈 Sesión encontrada: plot_16
|
||||
debug-streaming.js:78 Chart Config: {hasChart: true, chartType: 'line', scaleType: 'realtime', scaleConstructor: 'RealTimeScale', streamingEnabled: true, …}
|
||||
debug-streaming.js:85 ✅ Escala realtime configurada correctamente
|
||||
debug-streaming.js:93 ✅ Streaming habilitado en el chart
|
||||
debug-streaming.js:98
|
||||
4️⃣ VERIFICANDO DATOS DEL BACKEND...
|
||||
debug-streaming.js:8 🔧 DIAGNÓSTICO DETALLADO DE STREAMING
|
||||
debug-streaming.js:9 ============================================================
|
||||
debug-streaming.js:23
|
||||
1️⃣ VERIFICANDO LIBRERÍAS...
|
||||
debug-streaming.js:30 Chart.js: ✅
|
||||
debug-streaming.js:31 ChartStreaming: ✅
|
||||
debug-streaming.js:32 PlotManager: ✅
|
||||
debug-streaming.js:41
|
||||
2️⃣ VERIFICANDO PLOT MANAGER...
|
||||
debug-streaming.js:44 ✅ PlotManager inicializado
|
||||
debug-streaming.js:45 📊 Sesiones activas: 1
|
||||
debug-streaming.js:59
|
||||
3️⃣ ANALIZANDO SESIÓN DE PLOT...
|
||||
debug-streaming.js:65 📈 Sesión encontrada: plot_16
|
||||
debug-streaming.js:78 Chart Config: {hasChart: true, chartType: 'line', scaleType: 'realtime', scaleConstructor: 'RealTimeScale', streamingEnabled: true, …}
|
||||
debug-streaming.js:85 ✅ Escala realtime configurada correctamente
|
||||
debug-streaming.js:93 ✅ Streaming habilitado en el chart
|
||||
debug-streaming.js:98
|
||||
4️⃣ VERIFICANDO DATOS DEL BACKEND...
|
||||
plotting.js:9 📈 Plot debugging enabled. Check console for detailed logs.
|
||||
debug-streaming.js:278 ⚡ TEST RÁPIDO DE STREAMING
|
||||
debug-streaming.js:279 ------------------------------
|
||||
debug-streaming.js:288 plotManager: ✅ true
|
||||
debug-streaming.js:288 sessions: ✅ 1
|
||||
debug-streaming.js:288 chartStreaming: ✅ true
|
||||
debug-streaming.js:293
|
||||
🧪 Probando sesión: plot_16
|
||||
debug-streaming.js:8 🔧 DIAGNÓSTICO DETALLADO DE STREAMING
|
||||
debug-streaming.js:9 ============================================================
|
||||
debug-streaming.js:23
|
||||
1️⃣ VERIFICANDO LIBRERÍAS...
|
||||
debug-streaming.js:30 Chart.js: ✅
|
||||
debug-streaming.js:31 ChartStreaming: ✅
|
||||
debug-streaming.js:32 PlotManager: ✅
|
||||
debug-streaming.js:41
|
||||
2️⃣ VERIFICANDO PLOT MANAGER...
|
||||
✅ PlotManager inicializado
|
||||
📊 Sesiones activas: 1
|
||||
|
||||
3️⃣ ANALIZANDO SESIÓN DE PLOT...
|
||||
📈 Sesión encontrada: plot_16
|
||||
Chart Config: {hasChart: true, chartType: 'line', scaleType: 'realtime', scaleConstructor: 'RealTimeScale', streamingEnabled: true, …}
|
||||
✅ Escala realtime configurada correctamente
|
||||
✅ Streaming habilitado en el chart
|
||||
|
||||
4️⃣ VERIFICANDO DATOS DEL BACKEND...
|
||||
📊 Respuesta del backend: {success: true, datasets: 1, totalPoints: 43, isActive: true, isPaused: false}
|
||||
✅ Backend devuelve datos
|
||||
📈 Primer dataset: {label: 'UR29_Brix', dataPoints: 43, samplePoint: {…}}
|
||||
✅ Dataset tiene puntos de datos
|
||||
⏰ Análisis de timestamps: {firstPointTime: 1753115880879.299, currentTime: 1753115889485, differenceMs: 8605.700927734375, differenceSec: 9, isRecent: true}
|
||||
|
||||
5️⃣ PROBANDO FUNCIONALIDAD STREAMING...
|
||||
📈 Added point to dataset 0 (UR29_Brix): x=1753115889485, y=0.23291564278824506
|
||||
🧪 Test de addStreamingData: ✅
|
||||
📊 Puntos después del test: 60
|
||||
|
||||
6️⃣ RESUMEN DEL DIAGNÓSTICO
|
||||
========================================
|
||||
🎉 No se encontraron errores graves
|
||||
|
||||
7️⃣ PRÓXIMOS PASOS RECOMENDADOS:
|
||||
🔧 4. Si persiste: plotManager.controlPlot("plot_16", "stop") y luego "start"
|
||||
📈 Plot plot_16: Streaming resumed
|
||||
📈 RealTimeScale DEBUG - scaleOptions: Proxy(Object) {_cacheable: false, _proxy: Proxy(Object), _context: {…}, _subProxy: undefined, _stack: Set(0), …}
|
||||
📈 RealTimeScale DEBUG - me.options: Proxy(Object) {_cacheable: false, _proxy: Proxy(Object), _context: {…}, _subProxy: undefined, _stack: Set(0), …}
|
||||
📈 RealTimeScale DEBUG - me.options.realtime: Proxy(Object) {_cacheable: false, _proxy: Proxy(Object), _context: {…}, _subProxy: undefined, _stack: Set(0), …}
|
||||
📈 RealTimeScale DEBUG - onRefresh resolved: null object
|
||||
📈 RealTimeScale initialized: {duration: 60000, refresh: 500, pause: false, hasOnRefresh: false}
|
||||
🔧 DIAGNÓSTICO DETALLADO DE STREAMING
|
||||
============================================================
|
||||
|
||||
1️⃣ VERIFICANDO LIBRERÍAS...
|
||||
Chart.js: ✅
|
||||
ChartStreaming: ✅
|
||||
PlotManager: ✅
|
||||
|
||||
2️⃣ VERIFICANDO PLOT MANAGER...
|
||||
✅ PlotManager inicializado
|
||||
📊 Sesiones activas: 1
|
||||
|
||||
3️⃣ ANALIZANDO SESIÓN DE PLOT...
|
||||
📈 Sesión encontrada: plot_16
|
||||
Chart Config: {hasChart: true, chartType: 'line', scaleType: 'realtime', scaleConstructor: 'RealTimeScale', streamingEnabled: true, …}
|
||||
✅ Escala realtime configurada correctamente
|
||||
✅ Streaming habilitado en el chart
|
||||
|
||||
4️⃣ VERIFICANDO DATOS DEL BACKEND...
|
||||
📈 Plot debugging enabled. Check console for detailed logs.
|
||||
⚡ TEST RÁPIDO DE STREAMING
|
||||
------------------------------
|
||||
plotManager: ✅ true
|
||||
sessions: ✅ 1
|
||||
chartStreaming: ✅ true
|
||||
|
||||
🧪 Probando sesión: plot_16
|
||||
🔧 DIAGNÓSTICO DETALLADO DE STREAMING
|
||||
============================================================
|
||||
|
||||
1️⃣ VERIFICANDO LIBRERÍAS...
|
||||
Chart.js: ✅
|
||||
ChartStreaming: ✅
|
||||
PlotManager: ✅
|
||||
|
||||
2️⃣ VERIFICANDO PLOT MANAGER...
|
||||
✅ PlotManager inicializado
|
||||
📊 Sesiones activas: 1
|
||||
|
||||
3️⃣ ANALIZANDO SESIÓN DE PLOT...
|
||||
📈 Sesión encontrada: plot_16
|
||||
Chart Config: {hasChart: true, chartType: 'line', scaleType: 'realtime', scaleConstructor: 'RealTimeScale', streamingEnabled: true, …}
|
||||
✅ Escala realtime configurada correctamente
|
||||
✅ Streaming habilitado en el chart
|
||||
|
||||
4️⃣ VERIFICANDO DATOS DEL BACKEND...
|
||||
📈 Plot plot_16: Updating streaming chart with 1 datasets
|
||||
- Variable 1: UR29_Brix (45 points)
|
||||
📈 Plot plot_16: Added 0 new points to dataset 0
|
||||
🔧 DIAGNÓSTICO DETALLADO DE STREAMING
|
||||
============================================================
|
||||
|
||||
1️⃣ VERIFICANDO LIBRERÍAS...
|
||||
Chart.js: ✅
|
||||
ChartStreaming: ✅
|
||||
PlotManager: ✅
|
||||
|
||||
2️⃣ VERIFICANDO PLOT MANAGER...
|
||||
✅ PlotManager inicializado
|
||||
📊 Sesiones activas: 1
|
||||
|
||||
3️⃣ ANALIZANDO SESIÓN DE PLOT...
|
||||
📈 Sesión encontrada: plot_16
|
||||
Chart Config: {hasChart: true, chartType: 'line', scaleType: 'realtime', scaleConstructor: 'RealTimeScale', streamingEnabled: true, …}
|
||||
✅ Escala realtime configurada correctamente
|
||||
✅ Streaming habilitado en el chart
|
||||
|
||||
4️⃣ VERIFICANDO DATOS DEL BACKEND...
|
||||
📈 Plot debugging enabled. Check console for detailed logs.
|
||||
⚡ TEST RÁPIDO DE STREAMING
|
||||
------------------------------
|
||||
plotManager: ✅ true
|
||||
sessions: ✅ 1
|
||||
chartStreaming: ✅ true
|
||||
|
||||
🧪 Probando sesión: plot_16
|
||||
🔧 DIAGNÓSTICO DETALLADO DE STREAMING
|
||||
============================================================
|
||||
|
||||
1️⃣ VERIFICANDO LIBRERÍAS...
|
||||
Chart.js: ✅
|
||||
ChartStreaming: ✅
|
||||
PlotManager: ✅
|
||||
|
||||
2️⃣ VERIFICANDO PLOT MANAGER...
|
||||
✅ PlotManager inicializado
|
||||
📊 Sesiones activas: 1
|
||||
|
||||
3️⃣ ANALIZANDO SESIÓN DE PLOT...
|
||||
📈 Sesión encontrada: plot_16
|
||||
Chart Config: {hasChart: true, chartType: 'line', scaleType: 'realtime', scaleConstructor: 'RealTimeScale', streamingEnabled: true, …}
|
||||
✅ Escala realtime configurada correctamente
|
||||
✅ Streaming habilitado en el chart
|
||||
|
||||
4️⃣ VERIFICANDO DATOS DEL BACKEND...
|
||||
📈 RealTimeScale DEBUG - scaleOptions: Proxy(Object) {_cacheable: false, _proxy: Proxy(Object), _context: {…}, _subProxy: undefined, _stack: Set(0), …}
|
||||
📈 RealTimeScale DEBUG - me.options: Proxy(Object) {_cacheable: false, _proxy: Proxy(Object), _context: {…}, _subProxy: undefined, _stack: Set(0), …}
|
||||
📈 RealTimeScale DEBUG - me.options.realtime: Proxy(Object) {_cacheable: false, _proxy: Proxy(Object), _context: {…}, _subProxy: undefined, _stack: Set(0), …}
|
||||
📈 Plot plot_16: Fetching data from backend...
|
||||
📈 RealTimeScale DEBUG - onRefresh resolved: null object
|
||||
📈 RealTimeScale initialized: {duration: 60000, refresh: 500, pause: false, hasOnRefresh: false}
|
||||
📊 Respuesta del backend: {success: true, datasets: 1, totalPoints: 46, isActive: true, isPaused: false}
|
||||
✅ Backend devuelve datos
|
||||
📈 Primer dataset: {label: 'UR29_Brix', dataPoints: 46, samplePoint: {…}}
|
||||
✅ Dataset tiene puntos de datos
|
||||
⏰ Análisis de timestamps: {firstPointTime: 1753115880879.299, currentTime: 1753115890109, differenceMs: 9229.700927734375, differenceSec: 9, isRecent: true}
|
||||
|
||||
5️⃣ PROBANDO FUNCIONALIDAD STREAMING...
|
||||
📈 Added point to dataset 0 (UR29_Brix): x=1753115890109, y=15.728858368162268
|
||||
🧪 Test de addStreamingData: ✅
|
||||
📊 Puntos después del test: 61
|
||||
|
||||
6️⃣ RESUMEN DEL DIAGNÓSTICO
|
||||
========================================
|
||||
🎉 No se encontraron errores graves
|
||||
|
||||
7️⃣ PRÓXIMOS PASOS RECOMENDADOS:
|
||||
🔧 4. Si persiste: plotManager.controlPlot("plot_16", "stop") y luego "start"
|
||||
🔧 DIAGNÓSTICO DETALLADO DE STREAMING
|
||||
============================================================
|
||||
|
||||
1️⃣ VERIFICANDO LIBRERÍAS...
|
||||
Chart.js: ✅
|
||||
ChartStreaming: ✅
|
||||
PlotManager: ✅
|
||||
|
||||
2️⃣ VERIFICANDO PLOT MANAGER...
|
||||
✅ PlotManager inicializado
|
||||
📊 Sesiones activas: 1
|
||||
|
||||
3️⃣ ANALIZANDO SESIÓN DE PLOT...
|
||||
📈 Sesión encontrada: plot_16
|
||||
Chart Config: {hasChart: true, chartType: 'line', scaleType: 'realtime', scaleConstructor: 'RealTimeScale', streamingEnabled: true, …}
|
||||
✅ Escala realtime configurada correctamente
|
||||
✅ Streaming habilitado en el chart
|
||||
|
||||
4️⃣ VERIFICANDO DATOS DEL BACKEND...
|
||||
📈 Plot debugging enabled. Check console for detailed logs.
|
||||
⚡ TEST RÁPIDO DE STREAMING
|
||||
------------------------------
|
||||
plotManager: ✅ true
|
||||
sessions: ✅ 1
|
||||
chartStreaming: ✅ true
|
||||
|
||||
🧪 Probando sesión: plot_16
|
||||
🔧 DIAGNÓSTICO DETALLADO DE STREAMING
|
||||
============================================================
|
||||
|
||||
1️⃣ VERIFICANDO LIBRERÍAS...
|
||||
Chart.js: ✅
|
||||
ChartStreaming: ✅
|
||||
PlotManager: ✅
|
||||
|
||||
2️⃣ VERIFICANDO PLOT MANAGER...
|
||||
✅ PlotManager inicializado
|
||||
📊 Sesiones activas: 1
|
||||
|
||||
3️⃣ ANALIZANDO SESIÓN DE PLOT...
|
||||
📈 Sesión encontrada: plot_16
|
||||
Chart Config: {hasChart: true, chartType: 'line', scaleType: 'realtime', scaleConstructor: 'RealTimeScale', streamingEnabled: true, …}
|
||||
✅ Escala realtime configurada correctamente
|
||||
✅ Streaming habilitado en el chart
|
||||
|
||||
4️⃣ VERIFICANDO DATOS DEL BACKEND...
|
||||
📊 Respuesta del backend: {success: true, datasets: 1, totalPoints: 48, isActive: true, isPaused: false}
|
||||
✅ Backend devuelve datos
|
||||
📈 Primer dataset: {label: 'UR29_Brix', dataPoints: 48, samplePoint: {…}}
|
||||
✅ Dataset tiene puntos de datos
|
||||
⏰ Análisis de timestamps: {firstPointTime: 1753115880879.299, currentTime: 1753115890413, differenceMs: 9533.700927734375, differenceSec: 10, isRecent: true}
|
||||
|
||||
5️⃣ PROBANDO FUNCIONALIDAD STREAMING...
|
||||
📈 Added point to dataset 0 (UR29_Brix): x=1753115890413, y=93.73804066063487
|
||||
🧪 Test de addStreamingData: ✅
|
||||
📊 Puntos después del test: 62
|
||||
|
||||
6️⃣ RESUMEN DEL DIAGNÓSTICO
|
||||
========================================
|
||||
🎉 No se encontraron errores graves
|
||||
|
||||
7️⃣ PRÓXIMOS PASOS RECOMENDADOS:
|
||||
🔧 4. Si persiste: plotManager.controlPlot("plot_16", "stop") y luego "start"
|
||||
🔧 DIAGNÓSTICO DETALLADO DE STREAMING
|
||||
============================================================
|
||||
|
||||
1️⃣ VERIFICANDO LIBRERÍAS...
|
||||
Chart.js: ✅
|
||||
ChartStreaming: ✅
|
||||
PlotManager: ✅
|
||||
|
||||
2️⃣ VERIFICANDO PLOT MANAGER...
|
||||
✅ PlotManager inicializado
|
||||
📊 Sesiones activas: 1
|
||||
|
||||
3️⃣ ANALIZANDO SESIÓN DE PLOT...
|
||||
📈 Sesión encontrada: plot_16
|
||||
Chart Config: {hasChart: true, chartType: 'line', scaleType: 'realtime', scaleConstructor: 'RealTimeScale', streamingEnabled: true, …}
|
||||
✅ Escala realtime configurada correctamente
|
||||
✅ Streaming habilitado en el chart
|
||||
|
||||
4️⃣ VERIFICANDO DATOS DEL BACKEND...
|
||||
📈 Plot debugging enabled. Check console for detailed logs.
|
||||
⚡ TEST RÁPIDO DE STREAMING
|
||||
------------------------------
|
||||
plotManager: ✅ true
|
||||
sessions: ✅ 1
|
||||
chartStreaming: ✅ true
|
||||
|
||||
🧪 Probando sesión: plot_16
|
||||
🔧 DIAGNÓSTICO DETALLADO DE STREAMING
|
||||
============================================================
|
||||
|
||||
1️⃣ VERIFICANDO LIBRERÍAS...
|
||||
Chart.js: ✅
|
||||
ChartStreaming: ✅
|
||||
PlotManager: ✅
|
||||
|
||||
2️⃣ VERIFICANDO PLOT MANAGER...
|
||||
✅ PlotManager inicializado
|
||||
📊 Sesiones activas: 1
|
||||
|
||||
3️⃣ ANALIZANDO SESIÓN DE PLOT...
|
||||
📈 Sesión encontrada: plot_16
|
||||
Chart Config: {hasChart: true, chartType: 'line', scaleType: 'realtime', scaleConstructor: 'RealTimeScale', streamingEnabled: true, …}
|
||||
✅ Escala realtime configurada correctamente
|
||||
✅ Streaming habilitado en el chart
|
||||
|
||||
4️⃣ VERIFICANDO DATOS DEL BACKEND...
|
||||
📈 RealTimeScale DEBUG - scaleOptions: Proxy(Object) {_cacheable: false, _proxy: Proxy(Object), _context: {…}, _subProxy: undefined, _stack: Set(0), …}
|
||||
📈 RealTimeScale DEBUG - me.options: Proxy(Object) {_cacheable: false, _proxy: Proxy(Object), _context: {…}, _subProxy: undefined, _stack: Set(0), …}
|
||||
📈 RealTimeScale DEBUG - me.options.realtime: Proxy(Object) {_cacheable: false, _proxy: Proxy(Object), _context: {…}, _subProxy: undefined, _stack: Set(0), …}
|
||||
📈 RealTimeScale DEBUG - onRefresh resolved: null object
|
||||
📈 RealTimeScale initialized: {duration: 60000, refresh: 500, pause: false, hasOnRefresh: false}
|
Loading…
Reference in New Issue