feat: Implement Plot Manager and Plot Manager Simple components with collapsible forms for plot definitions and variables configuration

This commit is contained in:
Miguel 2025-08-14 14:48:02 +02:00
parent 3cf14df246
commit f5db758698
62 changed files with 5334 additions and 18686 deletions

2581
.examples/symbolics.txt Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = '';
}
}
})();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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}">&times;</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}">&times;</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();
});

View File

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

View File

@ -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] || '📋';
}

View File

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

View File

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

View File

@ -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">&times;</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">&times;</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">&times;</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">&times;</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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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}">&times;</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}">&times;</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();
});

View File

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

View File

@ -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] || '📋';
}

View File

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

View File

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