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');
-});
\ No newline at end of file
diff --git a/web_test/PLC S7-315 Streamer & Logger_files/csv.js.descargar b/web_test/PLC S7-315 Streamer & Logger_files/csv.js.descargar
deleted file mode 100644
index 50e1314..0000000
--- a/web_test/PLC S7-315 Streamer & Logger_files/csv.js.descargar
+++ /dev/null
@@ -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 = `
-
- 📁 Directory:
- ${info.base_directory}
-
-
- 📊 Total Files:
- ${info.total_files}
-
-
- 💾 Total Size:
- ${info.total_size_mb} MB
-
- `;
-
- if (info.oldest_file) {
- html += `
-
- 📅 Oldest File:
- ${new Date(info.oldest_file).toLocaleString()}
-
- `;
- }
-
- if (info.newest_file) {
- html += `
-
- 🆕 Newest File:
- ${new Date(info.newest_file).toLocaleString()}
-
- `;
- }
-
- if (info.day_folders && info.day_folders.length > 0) {
- html += '📂 Day Folders:
';
- info.day_folders.forEach(folder => {
- html += `
-
- ${folder.name}
- ${folder.files} files, ${folder.size_mb} MB
-
- `;
- });
- }
-
- statsDiv.innerHTML = html;
- }
- })
- .catch(error => {
- document.getElementById('directory-stats').innerHTML = 'Error loading directory information
';
- });
-}
-
-// 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');
- });
- });
-}
\ No newline at end of file
diff --git a/web_test/PLC S7-315 Streamer & Logger_files/datasets.js.descargar b/web_test/PLC S7-315 Streamer & Logger_files/datasets.js.descargar
deleted file mode 100644
index bdf5d98..0000000
--- a/web_test/PLC S7-315 Streamer & Logger_files/datasets.js.descargar
+++ /dev/null
@@ -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 = '';
-
- 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 = `
- ${varName} |
- ${memoryAreaDisplay} |
- ${variable.offset} |
- ${variable.type.toUpperCase()} |
-
- --
- |
-
-
- |
-
-
-
- |
- `;
-
- 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';
- }
- }
-}
\ No newline at end of file
diff --git a/web_test/PLC S7-315 Streamer & Logger_files/events.js.descargar b/web_test/PLC S7-315 Streamer & Logger_files/events.js.descargar
deleted file mode 100644
index 2b2a410..0000000
--- a/web_test/PLC S7-315 Streamer & Logger_files/events.js.descargar
+++ /dev/null
@@ -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 = `
-
- `;
- } 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 = `
-
- ${event.message}
- ${hasDetails ? `${JSON.stringify(event.details, null, 2)}
` : ''}
- `;
-
- return logEntry;
-}
-
-// Limpiar vista de log
-function clearLogView() {
- const logContainer = document.getElementById('events-log');
- logContainer.innerHTML = `
-
-
-
Log view cleared. Click refresh to reload events.
-
- `;
-
- 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 = `
-
- `;
- } 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 = `
-
-
-
Events view cleared. Click refresh to reload events.
-
- `;
-
- eventsCount.textContent = '0';
-}
\ No newline at end of file
diff --git a/web_test/PLC S7-315 Streamer & Logger_files/main.js.descargar b/web_test/PLC S7-315 Streamer & Logger_files/main.js.descargar
deleted file mode 100644
index 8f23ce6..0000000
--- a/web_test/PLC S7-315 Streamer & Logger_files/main.js.descargar
+++ /dev/null
@@ -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();
- }
-});
\ No newline at end of file
diff --git a/web_test/PLC S7-315 Streamer & Logger_files/pico.min.css b/web_test/PLC S7-315 Streamer & Logger_files/pico.min.css
deleted file mode 100644
index e10ec26..0000000
--- a/web_test/PLC S7-315 Streamer & Logger_files/pico.min.css
+++ /dev/null
@@ -1,4 +0,0 @@
-@charset "UTF-8";/*!
- * Pico CSS ✨ v2.1.1 (https://picocss.com)
- * Copyright 2019-2025 - Licensed under MIT
- */:host,:root{--pico-font-family-emoji:"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--pico-font-family-sans-serif:system-ui,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,Helvetica,Arial,"Helvetica Neue",sans-serif,var(--pico-font-family-emoji);--pico-font-family-monospace:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace,var(--pico-font-family-emoji);--pico-font-family:var(--pico-font-family-sans-serif);--pico-line-height:1.5;--pico-font-weight:400;--pico-font-size:100%;--pico-text-underline-offset:0.1rem;--pico-border-radius:0.25rem;--pico-border-width:0.0625rem;--pico-outline-width:0.125rem;--pico-transition:0.2s ease-in-out;--pico-spacing:1rem;--pico-typography-spacing-vertical:1rem;--pico-block-spacing-vertical:var(--pico-spacing);--pico-block-spacing-horizontal:var(--pico-spacing);--pico-grid-column-gap:var(--pico-spacing);--pico-grid-row-gap:var(--pico-spacing);--pico-form-element-spacing-vertical:0.75rem;--pico-form-element-spacing-horizontal:1rem;--pico-group-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-group-box-shadow-focus-with-button:0 0 0 var(--pico-outline-width) var(--pico-primary-focus);--pico-group-box-shadow-focus-with-input:0 0 0 0.0625rem var(--pico-form-element-border-color);--pico-modal-overlay-backdrop-filter:blur(0.375rem);--pico-nav-element-spacing-vertical:1rem;--pico-nav-element-spacing-horizontal:0.5rem;--pico-nav-link-spacing-vertical:0.5rem;--pico-nav-link-spacing-horizontal:0.5rem;--pico-nav-breadcrumb-divider:">";--pico-icon-checkbox:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-minus:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E");--pico-icon-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-date:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E");--pico-icon-time:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-search:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E");--pico-icon-close:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E");--pico-icon-loading:url("data:image/svg+xml,%3Csvg fill='none' height='24' width='24' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' %3E%3Cstyle%3E g %7B animation: rotate 2s linear infinite; transform-origin: center center; %7D circle %7B stroke-dasharray: 75,100; stroke-dashoffset: -5; animation: dash 1.5s ease-in-out infinite; stroke-linecap: round; %7D @keyframes rotate %7B 0%25 %7B transform: rotate(0deg); %7D 100%25 %7B transform: rotate(360deg); %7D %7D @keyframes dash %7B 0%25 %7B stroke-dasharray: 1,100; stroke-dashoffset: 0; %7D 50%25 %7B stroke-dasharray: 44.5,100; stroke-dashoffset: -17.5; %7D 100%25 %7B stroke-dasharray: 44.5,100; stroke-dashoffset: -62; %7D %7D %3C/style%3E%3Cg%3E%3Ccircle cx='12' cy='12' r='10' fill='none' stroke='rgb(136, 145, 164)' stroke-width='4' /%3E%3C/g%3E%3C/svg%3E")}@media (min-width:576px){:host,:root{--pico-font-size:106.25%}}@media (min-width:768px){:host,:root{--pico-font-size:112.5%}}@media (min-width:1024px){:host,:root{--pico-font-size:118.75%}}@media (min-width:1280px){:host,:root{--pico-font-size:125%}}@media (min-width:1536px){:host,:root{--pico-font-size:131.25%}}a{--pico-text-decoration:underline}a.contrast,a.secondary{--pico-text-decoration:underline}small{--pico-font-size:0.875em}h1,h2,h3,h4,h5,h6{--pico-font-weight:700}h1{--pico-font-size:2rem;--pico-line-height:1.125;--pico-typography-spacing-top:3rem}h2{--pico-font-size:1.75rem;--pico-line-height:1.15;--pico-typography-spacing-top:2.625rem}h3{--pico-font-size:1.5rem;--pico-line-height:1.175;--pico-typography-spacing-top:2.25rem}h4{--pico-font-size:1.25rem;--pico-line-height:1.2;--pico-typography-spacing-top:1.874rem}h5{--pico-font-size:1.125rem;--pico-line-height:1.225;--pico-typography-spacing-top:1.6875rem}h6{--pico-font-size:1rem;--pico-line-height:1.25;--pico-typography-spacing-top:1.5rem}tfoot td,tfoot th,thead td,thead th{--pico-font-weight:600;--pico-border-width:0.1875rem}code,kbd,pre,samp{--pico-font-family:var(--pico-font-family-monospace)}kbd{--pico-font-weight:bolder}:where(select,textarea),input:not([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]){--pico-outline-width:0.0625rem}[type=search]{--pico-border-radius:5rem}[type=checkbox],[type=radio]{--pico-border-width:0.125rem}[type=checkbox][role=switch]{--pico-border-width:0.1875rem}details.dropdown summary:not([role=button]){--pico-outline-width:0.0625rem}nav details.dropdown summary:focus-visible{--pico-outline-width:0.125rem}[role=search]{--pico-border-radius:5rem}[role=group]:has(button.secondary:focus,[type=submit].secondary:focus,[type=button].secondary:focus,[role=button].secondary:focus),[role=search]:has(button.secondary:focus,[type=submit].secondary:focus,[type=button].secondary:focus,[role=button].secondary:focus){--pico-group-box-shadow-focus-with-button:0 0 0 var(--pico-outline-width) var(--pico-secondary-focus)}[role=group]:has(button.contrast:focus,[type=submit].contrast:focus,[type=button].contrast:focus,[role=button].contrast:focus),[role=search]:has(button.contrast:focus,[type=submit].contrast:focus,[type=button].contrast:focus,[role=button].contrast:focus){--pico-group-box-shadow-focus-with-button:0 0 0 var(--pico-outline-width) var(--pico-contrast-focus)}[role=group] [role=button],[role=group] [type=button],[role=group] [type=submit],[role=group] button,[role=search] [role=button],[role=search] [type=button],[role=search] [type=submit],[role=search] button{--pico-form-element-spacing-horizontal:2rem}details summary[role=button]:not(.outline)::after{filter:brightness(0) invert(1)}[aria-busy=true]:not(input,select,textarea):is(button,[type=submit],[type=button],[type=reset],[role=button]):not(.outline)::before{filter:brightness(0) invert(1)}:host(:not([data-theme=dark])),:root:not([data-theme=dark]),[data-theme=light]{color-scheme:light;--pico-background-color:#fff;--pico-color:#373c44;--pico-text-selection-color:rgba(2, 154, 232, 0.25);--pico-muted-color:#646b79;--pico-muted-border-color:rgb(231, 234, 239.5);--pico-primary:#0172ad;--pico-primary-background:#0172ad;--pico-primary-border:var(--pico-primary-background);--pico-primary-underline:rgba(1, 114, 173, 0.5);--pico-primary-hover:#015887;--pico-primary-hover-background:#02659a;--pico-primary-hover-border:var(--pico-primary-hover-background);--pico-primary-hover-underline:var(--pico-primary-hover);--pico-primary-focus:rgba(2, 154, 232, 0.5);--pico-primary-inverse:#fff;--pico-secondary:#5d6b89;--pico-secondary-background:#525f7a;--pico-secondary-border:var(--pico-secondary-background);--pico-secondary-underline:rgba(93, 107, 137, 0.5);--pico-secondary-hover:#48536b;--pico-secondary-hover-background:#48536b;--pico-secondary-hover-border:var(--pico-secondary-hover-background);--pico-secondary-hover-underline:var(--pico-secondary-hover);--pico-secondary-focus:rgba(93, 107, 137, 0.25);--pico-secondary-inverse:#fff;--pico-contrast:#181c25;--pico-contrast-background:#181c25;--pico-contrast-border:var(--pico-contrast-background);--pico-contrast-underline:rgba(24, 28, 37, 0.5);--pico-contrast-hover:#000;--pico-contrast-hover-background:#000;--pico-contrast-hover-border:var(--pico-contrast-hover-background);--pico-contrast-hover-underline:var(--pico-secondary-hover);--pico-contrast-focus:rgba(93, 107, 137, 0.25);--pico-contrast-inverse:#fff;--pico-box-shadow:0.0145rem 0.029rem 0.174rem rgba(129, 145, 181, 0.01698),0.0335rem 0.067rem 0.402rem rgba(129, 145, 181, 0.024),0.0625rem 0.125rem 0.75rem rgba(129, 145, 181, 0.03),0.1125rem 0.225rem 1.35rem rgba(129, 145, 181, 0.036),0.2085rem 0.417rem 2.502rem rgba(129, 145, 181, 0.04302),0.5rem 1rem 6rem rgba(129, 145, 181, 0.06),0 0 0 0.0625rem rgba(129, 145, 181, 0.015);--pico-h1-color:#2d3138;--pico-h2-color:#373c44;--pico-h3-color:#424751;--pico-h4-color:#4d535e;--pico-h5-color:#5c6370;--pico-h6-color:#646b79;--pico-mark-background-color:rgb(252.5, 230.5, 191.5);--pico-mark-color:#0f1114;--pico-ins-color:rgb(28.5, 105.5, 84);--pico-del-color:rgb(136, 56.5, 53);--pico-blockquote-border-color:var(--pico-muted-border-color);--pico-blockquote-footer-color:var(--pico-muted-color);--pico-button-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-button-hover-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-table-border-color:var(--pico-muted-border-color);--pico-table-row-stripped-background-color:rgba(111, 120, 135, 0.0375);--pico-code-background-color:rgb(243, 244.5, 246.75);--pico-code-color:#646b79;--pico-code-kbd-background-color:var(--pico-color);--pico-code-kbd-color:var(--pico-background-color);--pico-form-element-background-color:rgb(251, 251.5, 252.25);--pico-form-element-selected-background-color:#dfe3eb;--pico-form-element-border-color:#cfd5e2;--pico-form-element-color:#23262c;--pico-form-element-placeholder-color:var(--pico-muted-color);--pico-form-element-active-background-color:#fff;--pico-form-element-active-border-color:var(--pico-primary-border);--pico-form-element-focus-color:var(--pico-primary-border);--pico-form-element-disabled-opacity:0.5;--pico-form-element-invalid-border-color:rgb(183.5, 105.5, 106.5);--pico-form-element-invalid-active-border-color:rgb(200.25, 79.25, 72.25);--pico-form-element-invalid-focus-color:var(--pico-form-element-invalid-active-border-color);--pico-form-element-valid-border-color:rgb(76, 154.5, 137.5);--pico-form-element-valid-active-border-color:rgb(39, 152.75, 118.75);--pico-form-element-valid-focus-color:var(--pico-form-element-valid-active-border-color);--pico-switch-background-color:#bfc7d9;--pico-switch-checked-background-color:var(--pico-primary-background);--pico-switch-color:#fff;--pico-switch-thumb-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-range-border-color:#dfe3eb;--pico-range-active-border-color:#bfc7d9;--pico-range-thumb-border-color:var(--pico-background-color);--pico-range-thumb-color:var(--pico-secondary-background);--pico-range-thumb-active-color:var(--pico-primary-background);--pico-accordion-border-color:var(--pico-muted-border-color);--pico-accordion-active-summary-color:var(--pico-primary-hover);--pico-accordion-close-summary-color:var(--pico-color);--pico-accordion-open-summary-color:var(--pico-muted-color);--pico-card-background-color:var(--pico-background-color);--pico-card-border-color:var(--pico-muted-border-color);--pico-card-box-shadow:var(--pico-box-shadow);--pico-card-sectioning-background-color:rgb(251, 251.5, 252.25);--pico-dropdown-background-color:#fff;--pico-dropdown-border-color:#eff1f4;--pico-dropdown-box-shadow:var(--pico-box-shadow);--pico-dropdown-color:var(--pico-color);--pico-dropdown-hover-background-color:#eff1f4;--pico-loading-spinner-opacity:0.5;--pico-modal-overlay-background-color:rgba(232, 234, 237, 0.75);--pico-progress-background-color:#dfe3eb;--pico-progress-color:var(--pico-primary-background);--pico-tooltip-background-color:var(--pico-contrast-background);--pico-tooltip-color:var(--pico-contrast-inverse);--pico-icon-valid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(76, 154.5, 137.5)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-invalid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(200.25, 79.25, 72.25)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E")}:host(:not([data-theme=dark])) input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]),:root:not([data-theme=dark]) input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]),[data-theme=light] input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]){--pico-form-element-focus-color:var(--pico-primary-focus)}@media only screen and (prefers-color-scheme:dark){:host(:not([data-theme])),:root:not([data-theme]){color-scheme:dark;--pico-background-color:rgb(19, 22.5, 30.5);--pico-color:#c2c7d0;--pico-text-selection-color:rgba(1, 170, 255, 0.1875);--pico-muted-color:#7b8495;--pico-muted-border-color:#202632;--pico-primary:#01aaff;--pico-primary-background:#0172ad;--pico-primary-border:var(--pico-primary-background);--pico-primary-underline:rgba(1, 170, 255, 0.5);--pico-primary-hover:#79c0ff;--pico-primary-hover-background:#017fc0;--pico-primary-hover-border:var(--pico-primary-hover-background);--pico-primary-hover-underline:var(--pico-primary-hover);--pico-primary-focus:rgba(1, 170, 255, 0.375);--pico-primary-inverse:#fff;--pico-secondary:#969eaf;--pico-secondary-background:#525f7a;--pico-secondary-border:var(--pico-secondary-background);--pico-secondary-underline:rgba(150, 158, 175, 0.5);--pico-secondary-hover:#b3b9c5;--pico-secondary-hover-background:#5d6b89;--pico-secondary-hover-border:var(--pico-secondary-hover-background);--pico-secondary-hover-underline:var(--pico-secondary-hover);--pico-secondary-focus:rgba(144, 158, 190, 0.25);--pico-secondary-inverse:#fff;--pico-contrast:#dfe3eb;--pico-contrast-background:#eff1f4;--pico-contrast-border:var(--pico-contrast-background);--pico-contrast-underline:rgba(223, 227, 235, 0.5);--pico-contrast-hover:#fff;--pico-contrast-hover-background:#fff;--pico-contrast-hover-border:var(--pico-contrast-hover-background);--pico-contrast-hover-underline:var(--pico-contrast-hover);--pico-contrast-focus:rgba(207, 213, 226, 0.25);--pico-contrast-inverse:#000;--pico-box-shadow:0.0145rem 0.029rem 0.174rem rgba(7, 8.5, 12, 0.01698),0.0335rem 0.067rem 0.402rem rgba(7, 8.5, 12, 0.024),0.0625rem 0.125rem 0.75rem rgba(7, 8.5, 12, 0.03),0.1125rem 0.225rem 1.35rem rgba(7, 8.5, 12, 0.036),0.2085rem 0.417rem 2.502rem rgba(7, 8.5, 12, 0.04302),0.5rem 1rem 6rem rgba(7, 8.5, 12, 0.06),0 0 0 0.0625rem rgba(7, 8.5, 12, 0.015);--pico-h1-color:#f0f1f3;--pico-h2-color:#e0e3e7;--pico-h3-color:#c2c7d0;--pico-h4-color:#b3b9c5;--pico-h5-color:#a4acba;--pico-h6-color:#8891a4;--pico-mark-background-color:#014063;--pico-mark-color:#fff;--pico-ins-color:#62af9a;--pico-del-color:rgb(205.5, 126, 123);--pico-blockquote-border-color:var(--pico-muted-border-color);--pico-blockquote-footer-color:var(--pico-muted-color);--pico-button-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-button-hover-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-table-border-color:var(--pico-muted-border-color);--pico-table-row-stripped-background-color:rgba(111, 120, 135, 0.0375);--pico-code-background-color:rgb(26, 30.5, 40.25);--pico-code-color:#8891a4;--pico-code-kbd-background-color:var(--pico-color);--pico-code-kbd-color:var(--pico-background-color);--pico-form-element-background-color:rgb(28, 33, 43.5);--pico-form-element-selected-background-color:#2a3140;--pico-form-element-border-color:#2a3140;--pico-form-element-color:#e0e3e7;--pico-form-element-placeholder-color:#8891a4;--pico-form-element-active-background-color:rgb(26, 30.5, 40.25);--pico-form-element-active-border-color:var(--pico-primary-border);--pico-form-element-focus-color:var(--pico-primary-border);--pico-form-element-disabled-opacity:0.5;--pico-form-element-invalid-border-color:rgb(149.5, 74, 80);--pico-form-element-invalid-active-border-color:rgb(183.25, 63.5, 59);--pico-form-element-invalid-focus-color:var(--pico-form-element-invalid-active-border-color);--pico-form-element-valid-border-color:#2a7b6f;--pico-form-element-valid-active-border-color:rgb(22, 137, 105.5);--pico-form-element-valid-focus-color:var(--pico-form-element-valid-active-border-color);--pico-switch-background-color:#333c4e;--pico-switch-checked-background-color:var(--pico-primary-background);--pico-switch-color:#fff;--pico-switch-thumb-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-range-border-color:#202632;--pico-range-active-border-color:#2a3140;--pico-range-thumb-border-color:var(--pico-background-color);--pico-range-thumb-color:var(--pico-secondary-background);--pico-range-thumb-active-color:var(--pico-primary-background);--pico-accordion-border-color:var(--pico-muted-border-color);--pico-accordion-active-summary-color:var(--pico-primary-hover);--pico-accordion-close-summary-color:var(--pico-color);--pico-accordion-open-summary-color:var(--pico-muted-color);--pico-card-background-color:#181c25;--pico-card-border-color:var(--pico-card-background-color);--pico-card-box-shadow:var(--pico-box-shadow);--pico-card-sectioning-background-color:rgb(26, 30.5, 40.25);--pico-dropdown-background-color:#181c25;--pico-dropdown-border-color:#202632;--pico-dropdown-box-shadow:var(--pico-box-shadow);--pico-dropdown-color:var(--pico-color);--pico-dropdown-hover-background-color:#202632;--pico-loading-spinner-opacity:0.5;--pico-modal-overlay-background-color:rgba(7.5, 8.5, 10, 0.75);--pico-progress-background-color:#202632;--pico-progress-color:var(--pico-primary-background);--pico-tooltip-background-color:var(--pico-contrast-background);--pico-tooltip-color:var(--pico-contrast-inverse);--pico-icon-valid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(42, 123, 111)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-invalid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(149.5, 74, 80)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E")}:host(:not([data-theme])) input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]),:root:not([data-theme]) input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]){--pico-form-element-focus-color:var(--pico-primary-focus)}:host(:not([data-theme])) details summary[role=button].contrast:not(.outline)::after,:root:not([data-theme]) details summary[role=button].contrast:not(.outline)::after{filter:brightness(0)}:host(:not([data-theme])) [aria-busy=true]:not(input,select,textarea).contrast:is(button,[type=submit],[type=button],[type=reset],[role=button]):not(.outline)::before,:root:not([data-theme]) [aria-busy=true]:not(input,select,textarea).contrast:is(button,[type=submit],[type=button],[type=reset],[role=button]):not(.outline)::before{filter:brightness(0)}}[data-theme=dark]{color-scheme:dark;--pico-background-color:rgb(19, 22.5, 30.5);--pico-color:#c2c7d0;--pico-text-selection-color:rgba(1, 170, 255, 0.1875);--pico-muted-color:#7b8495;--pico-muted-border-color:#202632;--pico-primary:#01aaff;--pico-primary-background:#0172ad;--pico-primary-border:var(--pico-primary-background);--pico-primary-underline:rgba(1, 170, 255, 0.5);--pico-primary-hover:#79c0ff;--pico-primary-hover-background:#017fc0;--pico-primary-hover-border:var(--pico-primary-hover-background);--pico-primary-hover-underline:var(--pico-primary-hover);--pico-primary-focus:rgba(1, 170, 255, 0.375);--pico-primary-inverse:#fff;--pico-secondary:#969eaf;--pico-secondary-background:#525f7a;--pico-secondary-border:var(--pico-secondary-background);--pico-secondary-underline:rgba(150, 158, 175, 0.5);--pico-secondary-hover:#b3b9c5;--pico-secondary-hover-background:#5d6b89;--pico-secondary-hover-border:var(--pico-secondary-hover-background);--pico-secondary-hover-underline:var(--pico-secondary-hover);--pico-secondary-focus:rgba(144, 158, 190, 0.25);--pico-secondary-inverse:#fff;--pico-contrast:#dfe3eb;--pico-contrast-background:#eff1f4;--pico-contrast-border:var(--pico-contrast-background);--pico-contrast-underline:rgba(223, 227, 235, 0.5);--pico-contrast-hover:#fff;--pico-contrast-hover-background:#fff;--pico-contrast-hover-border:var(--pico-contrast-hover-background);--pico-contrast-hover-underline:var(--pico-contrast-hover);--pico-contrast-focus:rgba(207, 213, 226, 0.25);--pico-contrast-inverse:#000;--pico-box-shadow:0.0145rem 0.029rem 0.174rem rgba(7, 8.5, 12, 0.01698),0.0335rem 0.067rem 0.402rem rgba(7, 8.5, 12, 0.024),0.0625rem 0.125rem 0.75rem rgba(7, 8.5, 12, 0.03),0.1125rem 0.225rem 1.35rem rgba(7, 8.5, 12, 0.036),0.2085rem 0.417rem 2.502rem rgba(7, 8.5, 12, 0.04302),0.5rem 1rem 6rem rgba(7, 8.5, 12, 0.06),0 0 0 0.0625rem rgba(7, 8.5, 12, 0.015);--pico-h1-color:#f0f1f3;--pico-h2-color:#e0e3e7;--pico-h3-color:#c2c7d0;--pico-h4-color:#b3b9c5;--pico-h5-color:#a4acba;--pico-h6-color:#8891a4;--pico-mark-background-color:#014063;--pico-mark-color:#fff;--pico-ins-color:#62af9a;--pico-del-color:rgb(205.5, 126, 123);--pico-blockquote-border-color:var(--pico-muted-border-color);--pico-blockquote-footer-color:var(--pico-muted-color);--pico-button-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-button-hover-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-table-border-color:var(--pico-muted-border-color);--pico-table-row-stripped-background-color:rgba(111, 120, 135, 0.0375);--pico-code-background-color:rgb(26, 30.5, 40.25);--pico-code-color:#8891a4;--pico-code-kbd-background-color:var(--pico-color);--pico-code-kbd-color:var(--pico-background-color);--pico-form-element-background-color:rgb(28, 33, 43.5);--pico-form-element-selected-background-color:#2a3140;--pico-form-element-border-color:#2a3140;--pico-form-element-color:#e0e3e7;--pico-form-element-placeholder-color:#8891a4;--pico-form-element-active-background-color:rgb(26, 30.5, 40.25);--pico-form-element-active-border-color:var(--pico-primary-border);--pico-form-element-focus-color:var(--pico-primary-border);--pico-form-element-disabled-opacity:0.5;--pico-form-element-invalid-border-color:rgb(149.5, 74, 80);--pico-form-element-invalid-active-border-color:rgb(183.25, 63.5, 59);--pico-form-element-invalid-focus-color:var(--pico-form-element-invalid-active-border-color);--pico-form-element-valid-border-color:#2a7b6f;--pico-form-element-valid-active-border-color:rgb(22, 137, 105.5);--pico-form-element-valid-focus-color:var(--pico-form-element-valid-active-border-color);--pico-switch-background-color:#333c4e;--pico-switch-checked-background-color:var(--pico-primary-background);--pico-switch-color:#fff;--pico-switch-thumb-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-range-border-color:#202632;--pico-range-active-border-color:#2a3140;--pico-range-thumb-border-color:var(--pico-background-color);--pico-range-thumb-color:var(--pico-secondary-background);--pico-range-thumb-active-color:var(--pico-primary-background);--pico-accordion-border-color:var(--pico-muted-border-color);--pico-accordion-active-summary-color:var(--pico-primary-hover);--pico-accordion-close-summary-color:var(--pico-color);--pico-accordion-open-summary-color:var(--pico-muted-color);--pico-card-background-color:#181c25;--pico-card-border-color:var(--pico-card-background-color);--pico-card-box-shadow:var(--pico-box-shadow);--pico-card-sectioning-background-color:rgb(26, 30.5, 40.25);--pico-dropdown-background-color:#181c25;--pico-dropdown-border-color:#202632;--pico-dropdown-box-shadow:var(--pico-box-shadow);--pico-dropdown-color:var(--pico-color);--pico-dropdown-hover-background-color:#202632;--pico-loading-spinner-opacity:0.5;--pico-modal-overlay-background-color:rgba(7.5, 8.5, 10, 0.75);--pico-progress-background-color:#202632;--pico-progress-color:var(--pico-primary-background);--pico-tooltip-background-color:var(--pico-contrast-background);--pico-tooltip-color:var(--pico-contrast-inverse);--pico-icon-valid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(42, 123, 111)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-invalid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(149.5, 74, 80)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E")}[data-theme=dark] input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]){--pico-form-element-focus-color:var(--pico-primary-focus)}[data-theme=dark] details summary[role=button].contrast:not(.outline)::after{filter:brightness(0)}[data-theme=dark] [aria-busy=true]:not(input,select,textarea).contrast:is(button,[type=submit],[type=button],[type=reset],[role=button]):not(.outline)::before{filter:brightness(0)}[type=checkbox],[type=radio],[type=range],progress{accent-color:var(--pico-primary)}*,::after,::before{box-sizing:border-box;background-repeat:no-repeat}::after,::before{text-decoration:inherit;vertical-align:inherit}:where(:host),:where(:root){-webkit-tap-highlight-color:transparent;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%;background-color:var(--pico-background-color);color:var(--pico-color);font-weight:var(--pico-font-weight);font-size:var(--pico-font-size);line-height:var(--pico-line-height);font-family:var(--pico-font-family);text-underline-offset:var(--pico-text-underline-offset);text-rendering:optimizeLegibility;overflow-wrap:break-word;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{width:100%;margin:0}main{display:block}body>footer,body>header,body>main{padding-block:var(--pico-block-spacing-vertical)}section{margin-bottom:var(--pico-block-spacing-vertical)}.container,.container-fluid{width:100%;margin-right:auto;margin-left:auto;padding-right:var(--pico-spacing);padding-left:var(--pico-spacing)}@media (min-width:576px){.container{max-width:510px;padding-right:0;padding-left:0}}@media (min-width:768px){.container{max-width:700px}}@media (min-width:1024px){.container{max-width:950px}}@media (min-width:1280px){.container{max-width:1200px}}@media (min-width:1536px){.container{max-width:1450px}}.grid{grid-column-gap:var(--pico-grid-column-gap);grid-row-gap:var(--pico-grid-row-gap);display:grid;grid-template-columns:1fr}@media (min-width:768px){.grid{grid-template-columns:repeat(auto-fit,minmax(0%,1fr))}}.grid>*{min-width:0}.overflow-auto{overflow:auto}b,strong{font-weight:bolder}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}address,blockquote,dl,ol,p,pre,table,ul{margin-top:0;margin-bottom:var(--pico-typography-spacing-vertical);color:var(--pico-color);font-style:normal;font-weight:var(--pico-font-weight)}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:var(--pico-typography-spacing-vertical);color:var(--pico-color);font-weight:var(--pico-font-weight);font-size:var(--pico-font-size);line-height:var(--pico-line-height);font-family:var(--pico-font-family)}h1{--pico-color:var(--pico-h1-color)}h2{--pico-color:var(--pico-h2-color)}h3{--pico-color:var(--pico-h3-color)}h4{--pico-color:var(--pico-h4-color)}h5{--pico-color:var(--pico-h5-color)}h6{--pico-color:var(--pico-h6-color)}:where(article,address,blockquote,dl,figure,form,ol,p,pre,table,ul)~:is(h1,h2,h3,h4,h5,h6){margin-top:var(--pico-typography-spacing-top)}p{margin-bottom:var(--pico-typography-spacing-vertical)}hgroup{margin-bottom:var(--pico-typography-spacing-vertical)}hgroup>*{margin-top:0;margin-bottom:0}hgroup>:not(:first-child):last-child{--pico-color:var(--pico-muted-color);--pico-font-weight:unset;font-size:1rem}:where(ol,ul) li{margin-bottom:calc(var(--pico-typography-spacing-vertical) * .25)}:where(dl,ol,ul) :where(dl,ol,ul){margin:0;margin-top:calc(var(--pico-typography-spacing-vertical) * .25)}ul li{list-style:square}mark{padding:.125rem .25rem;background-color:var(--pico-mark-background-color);color:var(--pico-mark-color);vertical-align:baseline}blockquote{display:block;margin:var(--pico-typography-spacing-vertical) 0;padding:var(--pico-spacing);border-right:none;border-left:.25rem solid var(--pico-blockquote-border-color);border-inline-start:0.25rem solid var(--pico-blockquote-border-color);border-inline-end:none}blockquote footer{margin-top:calc(var(--pico-typography-spacing-vertical) * .5);color:var(--pico-blockquote-footer-color)}abbr[title]{border-bottom:1px dotted;text-decoration:none;cursor:help}ins{color:var(--pico-ins-color);text-decoration:none}del{color:var(--pico-del-color)}::-moz-selection{background-color:var(--pico-text-selection-color)}::selection{background-color:var(--pico-text-selection-color)}:where(a:not([role=button])),[role=link]{--pico-color:var(--pico-primary);--pico-background-color:transparent;--pico-underline:var(--pico-primary-underline);outline:0;background-color:var(--pico-background-color);color:var(--pico-color);-webkit-text-decoration:var(--pico-text-decoration);text-decoration:var(--pico-text-decoration);text-decoration-color:var(--pico-underline);text-underline-offset:0.125em;transition:background-color var(--pico-transition),color var(--pico-transition),box-shadow var(--pico-transition),-webkit-text-decoration var(--pico-transition);transition:background-color var(--pico-transition),color var(--pico-transition),text-decoration var(--pico-transition),box-shadow var(--pico-transition);transition:background-color var(--pico-transition),color var(--pico-transition),text-decoration var(--pico-transition),box-shadow var(--pico-transition),-webkit-text-decoration var(--pico-transition)}:where(a:not([role=button])):is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[role=link]:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-color:var(--pico-primary-hover);--pico-underline:var(--pico-primary-hover-underline);--pico-text-decoration:underline}:where(a:not([role=button])):focus-visible,[role=link]:focus-visible{box-shadow:0 0 0 var(--pico-outline-width) var(--pico-primary-focus)}:where(a:not([role=button])).secondary,[role=link].secondary{--pico-color:var(--pico-secondary);--pico-underline:var(--pico-secondary-underline)}:where(a:not([role=button])).secondary:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[role=link].secondary:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-color:var(--pico-secondary-hover);--pico-underline:var(--pico-secondary-hover-underline)}:where(a:not([role=button])).contrast,[role=link].contrast{--pico-color:var(--pico-contrast);--pico-underline:var(--pico-contrast-underline)}:where(a:not([role=button])).contrast:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[role=link].contrast:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-color:var(--pico-contrast-hover);--pico-underline:var(--pico-contrast-hover-underline)}a[role=button]{display:inline-block}button{margin:0;overflow:visible;font-family:inherit;text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[role=button],[type=button],[type=file]::file-selector-button,[type=reset],[type=submit],button{--pico-background-color:var(--pico-primary-background);--pico-border-color:var(--pico-primary-border);--pico-color:var(--pico-primary-inverse);--pico-box-shadow:var(--pico-button-box-shadow, 0 0 0 rgba(0, 0, 0, 0));padding:var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal);border:var(--pico-border-width) solid var(--pico-border-color);border-radius:var(--pico-border-radius);outline:0;background-color:var(--pico-background-color);box-shadow:var(--pico-box-shadow);color:var(--pico-color);font-weight:var(--pico-font-weight);font-size:1rem;line-height:var(--pico-line-height);text-align:center;text-decoration:none;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;transition:background-color var(--pico-transition),border-color var(--pico-transition),color var(--pico-transition),box-shadow var(--pico-transition)}[role=button]:is(:hover,:active,:focus),[role=button]:is([aria-current]:not([aria-current=false])),[type=button]:is(:hover,:active,:focus),[type=button]:is([aria-current]:not([aria-current=false])),[type=file]::file-selector-button:is(:hover,:active,:focus),[type=file]::file-selector-button:is([aria-current]:not([aria-current=false])),[type=reset]:is(:hover,:active,:focus),[type=reset]:is([aria-current]:not([aria-current=false])),[type=submit]:is(:hover,:active,:focus),[type=submit]:is([aria-current]:not([aria-current=false])),button:is(:hover,:active,:focus),button:is([aria-current]:not([aria-current=false])){--pico-background-color:var(--pico-primary-hover-background);--pico-border-color:var(--pico-primary-hover-border);--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0));--pico-color:var(--pico-primary-inverse)}[role=button]:focus,[role=button]:is([aria-current]:not([aria-current=false])):focus,[type=button]:focus,[type=button]:is([aria-current]:not([aria-current=false])):focus,[type=file]::file-selector-button:focus,[type=file]::file-selector-button:is([aria-current]:not([aria-current=false])):focus,[type=reset]:focus,[type=reset]:is([aria-current]:not([aria-current=false])):focus,[type=submit]:focus,[type=submit]:is([aria-current]:not([aria-current=false])):focus,button:focus,button:is([aria-current]:not([aria-current=false])):focus{--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--pico-outline-width) var(--pico-primary-focus)}[type=button],[type=reset],[type=submit]{margin-bottom:var(--pico-spacing)}:is(button,[type=submit],[type=button],[role=button]).secondary,[type=file]::file-selector-button,[type=reset]{--pico-background-color:var(--pico-secondary-background);--pico-border-color:var(--pico-secondary-border);--pico-color:var(--pico-secondary-inverse);cursor:pointer}:is(button,[type=submit],[type=button],[role=button]).secondary:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[type=file]::file-selector-button:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[type=reset]:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-background-color:var(--pico-secondary-hover-background);--pico-border-color:var(--pico-secondary-hover-border);--pico-color:var(--pico-secondary-inverse)}:is(button,[type=submit],[type=button],[role=button]).secondary:focus,:is(button,[type=submit],[type=button],[role=button]).secondary:is([aria-current]:not([aria-current=false])):focus,[type=file]::file-selector-button:focus,[type=file]::file-selector-button:is([aria-current]:not([aria-current=false])):focus,[type=reset]:focus,[type=reset]:is([aria-current]:not([aria-current=false])):focus{--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--pico-outline-width) var(--pico-secondary-focus)}:is(button,[type=submit],[type=button],[role=button]).contrast{--pico-background-color:var(--pico-contrast-background);--pico-border-color:var(--pico-contrast-border);--pico-color:var(--pico-contrast-inverse)}:is(button,[type=submit],[type=button],[role=button]).contrast:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-background-color:var(--pico-contrast-hover-background);--pico-border-color:var(--pico-contrast-hover-border);--pico-color:var(--pico-contrast-inverse)}:is(button,[type=submit],[type=button],[role=button]).contrast:focus,:is(button,[type=submit],[type=button],[role=button]).contrast:is([aria-current]:not([aria-current=false])):focus{--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--pico-outline-width) var(--pico-contrast-focus)}:is(button,[type=submit],[type=button],[role=button]).outline,[type=reset].outline{--pico-background-color:transparent;--pico-color:var(--pico-primary);--pico-border-color:var(--pico-primary)}:is(button,[type=submit],[type=button],[role=button]).outline:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[type=reset].outline:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-background-color:transparent;--pico-color:var(--pico-primary-hover);--pico-border-color:var(--pico-primary-hover)}:is(button,[type=submit],[type=button],[role=button]).outline.secondary,[type=reset].outline{--pico-color:var(--pico-secondary);--pico-border-color:var(--pico-secondary)}:is(button,[type=submit],[type=button],[role=button]).outline.secondary:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[type=reset].outline:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-color:var(--pico-secondary-hover);--pico-border-color:var(--pico-secondary-hover)}:is(button,[type=submit],[type=button],[role=button]).outline.contrast{--pico-color:var(--pico-contrast);--pico-border-color:var(--pico-contrast)}:is(button,[type=submit],[type=button],[role=button]).outline.contrast:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-color:var(--pico-contrast-hover);--pico-border-color:var(--pico-contrast-hover)}:where(button,[type=submit],[type=reset],[type=button],[role=button])[disabled],:where(fieldset[disabled]) :is(button,[type=submit],[type=button],[type=reset],[role=button]){opacity:.5;pointer-events:none}:where(table){width:100%;border-collapse:collapse;border-spacing:0;text-indent:0}td,th{padding:calc(var(--pico-spacing)/ 2) var(--pico-spacing);border-bottom:var(--pico-border-width) solid var(--pico-table-border-color);background-color:var(--pico-background-color);color:var(--pico-color);font-weight:var(--pico-font-weight);text-align:left;text-align:start}tfoot td,tfoot th{border-top:var(--pico-border-width) solid var(--pico-table-border-color);border-bottom:0}table.striped tbody tr:nth-child(odd) td,table.striped tbody tr:nth-child(odd) th{background-color:var(--pico-table-row-stripped-background-color)}:where(audio,canvas,iframe,img,svg,video){vertical-align:middle}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}:where(iframe){border-style:none}img{max-width:100%;height:auto;border-style:none}:where(svg:not([fill])){fill:currentColor}svg:not(:host),svg:not(:root){overflow:hidden}code,kbd,pre,samp{font-size:.875em;font-family:var(--pico-font-family)}pre code,pre samp{font-size:inherit;font-family:inherit}pre{-ms-overflow-style:scrollbar;overflow:auto}code,kbd,pre,samp{border-radius:var(--pico-border-radius);background:var(--pico-code-background-color);color:var(--pico-code-color);font-weight:var(--pico-font-weight);line-height:initial}code,kbd,samp{display:inline-block;padding:.375rem}pre{display:block;margin-bottom:var(--pico-spacing);overflow-x:auto}pre>code,pre>samp{display:block;padding:var(--pico-spacing);background:0 0;line-height:var(--pico-line-height)}kbd{background-color:var(--pico-code-kbd-background-color);color:var(--pico-code-kbd-color);vertical-align:baseline}figure{display:block;margin:0;padding:0}figure figcaption{padding:calc(var(--pico-spacing) * .5) 0;color:var(--pico-muted-color)}hr{height:0;margin:var(--pico-typography-spacing-vertical) 0;border:0;border-top:1px solid var(--pico-muted-border-color);color:inherit}[hidden],template{display:none!important}canvas{display:inline-block}input,optgroup,select,textarea{margin:0;font-size:1rem;line-height:var(--pico-line-height);font-family:inherit;letter-spacing:inherit}input{overflow:visible}select{text-transform:none}legend{max-width:100%;padding:0;color:inherit;white-space:normal}textarea{overflow:auto}[type=checkbox],[type=radio]{padding:0}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}::-moz-focus-inner{padding:0;border-style:none}:-moz-focusring{outline:0}:-moz-ui-invalid{box-shadow:none}::-ms-expand{display:none}[type=file],[type=range]{padding:0;border-width:0}input:not([type=checkbox],[type=radio],[type=range]){height:calc(1rem * var(--pico-line-height) + var(--pico-form-element-spacing-vertical) * 2 + var(--pico-border-width) * 2)}fieldset{width:100%;margin:0;margin-bottom:var(--pico-spacing);padding:0;border:0}fieldset legend,label{display:block;margin-bottom:calc(var(--pico-spacing) * .375);color:var(--pico-color);font-weight:var(--pico-form-label-font-weight,var(--pico-font-weight))}fieldset legend{margin-bottom:calc(var(--pico-spacing) * .5)}button[type=submit],input:not([type=checkbox],[type=radio]),select,textarea{width:100%}input:not([type=checkbox],[type=radio],[type=range],[type=file]),select,textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal)}input,select,textarea{--pico-background-color:var(--pico-form-element-background-color);--pico-border-color:var(--pico-form-element-border-color);--pico-color:var(--pico-form-element-color);--pico-box-shadow:none;border:var(--pico-border-width) solid var(--pico-border-color);border-radius:var(--pico-border-radius);outline:0;background-color:var(--pico-background-color);box-shadow:var(--pico-box-shadow);color:var(--pico-color);font-weight:var(--pico-font-weight);transition:background-color var(--pico-transition),border-color var(--pico-transition),color var(--pico-transition),box-shadow var(--pico-transition)}:where(select,textarea):not([readonly]):is(:active,:focus),input:not([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[readonly]):is(:active,:focus){--pico-background-color:var(--pico-form-element-active-background-color)}:where(select,textarea):not([readonly]):is(:active,:focus),input:not([type=submit],[type=button],[type=reset],[role=switch],[readonly]):is(:active,:focus){--pico-border-color:var(--pico-form-element-active-border-color)}:where(select,textarea):not([readonly]):focus,input:not([type=submit],[type=button],[type=reset],[type=range],[type=file],[readonly]):focus{--pico-box-shadow:0 0 0 var(--pico-outline-width) var(--pico-form-element-focus-color)}:where(fieldset[disabled]) :is(input:not([type=submit],[type=button],[type=reset]),select,textarea),input:not([type=submit],[type=button],[type=reset])[disabled],label[aria-disabled=true],select[disabled],textarea[disabled]{opacity:var(--pico-form-element-disabled-opacity);pointer-events:none}label[aria-disabled=true] input[disabled]{opacity:1}:where(input,select,textarea):not([type=checkbox],[type=radio],[type=date],[type=datetime-local],[type=month],[type=time],[type=week],[type=range])[aria-invalid]{padding-right:calc(var(--pico-form-element-spacing-horizontal) + 1.5rem)!important;padding-left:var(--pico-form-element-spacing-horizontal);padding-inline-start:var(--pico-form-element-spacing-horizontal)!important;padding-inline-end:calc(var(--pico-form-element-spacing-horizontal) + 1.5rem)!important;background-position:center right .75rem;background-size:1rem auto;background-repeat:no-repeat}:where(input,select,textarea):not([type=checkbox],[type=radio],[type=date],[type=datetime-local],[type=month],[type=time],[type=week],[type=range])[aria-invalid=false]:not(select){background-image:var(--pico-icon-valid)}:where(input,select,textarea):not([type=checkbox],[type=radio],[type=date],[type=datetime-local],[type=month],[type=time],[type=week],[type=range])[aria-invalid=true]:not(select){background-image:var(--pico-icon-invalid)}:where(input,select,textarea)[aria-invalid=false]{--pico-border-color:var(--pico-form-element-valid-border-color)}:where(input,select,textarea)[aria-invalid=false]:is(:active,:focus){--pico-border-color:var(--pico-form-element-valid-active-border-color)!important}:where(input,select,textarea)[aria-invalid=false]:is(:active,:focus):not([type=checkbox],[type=radio]){--pico-box-shadow:0 0 0 var(--pico-outline-width) var(--pico-form-element-valid-focus-color)!important}:where(input,select,textarea)[aria-invalid=true]{--pico-border-color:var(--pico-form-element-invalid-border-color)}:where(input,select,textarea)[aria-invalid=true]:is(:active,:focus){--pico-border-color:var(--pico-form-element-invalid-active-border-color)!important}:where(input,select,textarea)[aria-invalid=true]:is(:active,:focus):not([type=checkbox],[type=radio]){--pico-box-shadow:0 0 0 var(--pico-outline-width) var(--pico-form-element-invalid-focus-color)!important}[dir=rtl] :where(input,select,textarea):not([type=checkbox],[type=radio]):is([aria-invalid],[aria-invalid=true],[aria-invalid=false]){background-position:center left .75rem}input::-webkit-input-placeholder,input::placeholder,select:invalid,textarea::-webkit-input-placeholder,textarea::placeholder{color:var(--pico-form-element-placeholder-color);opacity:1}input:not([type=checkbox],[type=radio]),select,textarea{margin-bottom:var(--pico-spacing)}select::-ms-expand{border:0;background-color:transparent}select:not([multiple],[size]){padding-right:calc(var(--pico-form-element-spacing-horizontal) + 1.5rem);padding-left:var(--pico-form-element-spacing-horizontal);padding-inline-start:var(--pico-form-element-spacing-horizontal);padding-inline-end:calc(var(--pico-form-element-spacing-horizontal) + 1.5rem);background-image:var(--pico-icon-chevron);background-position:center right .75rem;background-size:1rem auto;background-repeat:no-repeat}select[multiple] option:checked{background:var(--pico-form-element-selected-background-color);color:var(--pico-form-element-color)}[dir=rtl] select:not([multiple],[size]){background-position:center left .75rem}textarea{display:block;resize:vertical}textarea[aria-invalid]{--pico-icon-height:calc(1rem * var(--pico-line-height) + var(--pico-form-element-spacing-vertical) * 2 + var(--pico-border-width) * 2);background-position:top right .75rem!important;background-size:1rem var(--pico-icon-height)!important}:where(input,select,textarea,fieldset,.grid)+small{display:block;width:100%;margin-top:calc(var(--pico-spacing) * -.75);margin-bottom:var(--pico-spacing);color:var(--pico-muted-color)}:where(input,select,textarea,fieldset,.grid)[aria-invalid=false]+small{color:var(--pico-ins-color)}:where(input,select,textarea,fieldset,.grid)[aria-invalid=true]+small{color:var(--pico-del-color)}label>:where(input,select,textarea){margin-top:calc(var(--pico-spacing) * .25)}label:has([type=checkbox],[type=radio]){width:-moz-fit-content;width:fit-content;cursor:pointer}[type=checkbox],[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;width:1.25em;height:1.25em;margin-top:-.125em;margin-inline-end:.5em;border-width:var(--pico-border-width);vertical-align:middle;cursor:pointer}[type=checkbox]::-ms-check,[type=radio]::-ms-check{display:none}[type=checkbox]:checked,[type=checkbox]:checked:active,[type=checkbox]:checked:focus,[type=radio]:checked,[type=radio]:checked:active,[type=radio]:checked:focus{--pico-background-color:var(--pico-primary-background);--pico-border-color:var(--pico-primary-border);background-image:var(--pico-icon-checkbox);background-position:center;background-size:.75em auto;background-repeat:no-repeat}[type=checkbox]~label,[type=radio]~label{display:inline-block;margin-bottom:0;cursor:pointer}[type=checkbox]~label:not(:last-of-type),[type=radio]~label:not(:last-of-type){margin-inline-end:1em}[type=checkbox]:indeterminate{--pico-background-color:var(--pico-primary-background);--pico-border-color:var(--pico-primary-border);background-image:var(--pico-icon-minus);background-position:center;background-size:.75em auto;background-repeat:no-repeat}[type=radio]{border-radius:50%}[type=radio]:checked,[type=radio]:checked:active,[type=radio]:checked:focus{--pico-background-color:var(--pico-primary-inverse);border-width:.35em;background-image:none}[type=checkbox][role=switch]{--pico-background-color:var(--pico-switch-background-color);--pico-color:var(--pico-switch-color);width:2.25em;height:1.25em;border:var(--pico-border-width) solid var(--pico-border-color);border-radius:1.25em;background-color:var(--pico-background-color);line-height:1.25em}[type=checkbox][role=switch]:not([aria-invalid]){--pico-border-color:var(--pico-switch-background-color)}[type=checkbox][role=switch]:before{display:block;aspect-ratio:1;height:100%;border-radius:50%;background-color:var(--pico-color);box-shadow:var(--pico-switch-thumb-box-shadow);content:"";transition:margin .1s ease-in-out}[type=checkbox][role=switch]:focus{--pico-background-color:var(--pico-switch-background-color);--pico-border-color:var(--pico-switch-background-color)}[type=checkbox][role=switch]:checked{--pico-background-color:var(--pico-switch-checked-background-color);--pico-border-color:var(--pico-switch-checked-background-color);background-image:none}[type=checkbox][role=switch]:checked::before{margin-inline-start:calc(2.25em - 1.25em)}[type=checkbox][role=switch][disabled]{--pico-background-color:var(--pico-border-color)}[type=checkbox][aria-invalid=false]:checked,[type=checkbox][aria-invalid=false]:checked:active,[type=checkbox][aria-invalid=false]:checked:focus,[type=checkbox][role=switch][aria-invalid=false]:checked,[type=checkbox][role=switch][aria-invalid=false]:checked:active,[type=checkbox][role=switch][aria-invalid=false]:checked:focus{--pico-background-color:var(--pico-form-element-valid-border-color)}[type=checkbox]:checked:active[aria-invalid=true],[type=checkbox]:checked:focus[aria-invalid=true],[type=checkbox]:checked[aria-invalid=true],[type=checkbox][role=switch]:checked:active[aria-invalid=true],[type=checkbox][role=switch]:checked:focus[aria-invalid=true],[type=checkbox][role=switch]:checked[aria-invalid=true]{--pico-background-color:var(--pico-form-element-invalid-border-color)}[type=checkbox][aria-invalid=false]:checked,[type=checkbox][aria-invalid=false]:checked:active,[type=checkbox][aria-invalid=false]:checked:focus,[type=checkbox][role=switch][aria-invalid=false]:checked,[type=checkbox][role=switch][aria-invalid=false]:checked:active,[type=checkbox][role=switch][aria-invalid=false]:checked:focus,[type=radio][aria-invalid=false]:checked,[type=radio][aria-invalid=false]:checked:active,[type=radio][aria-invalid=false]:checked:focus{--pico-border-color:var(--pico-form-element-valid-border-color)}[type=checkbox]:checked:active[aria-invalid=true],[type=checkbox]:checked:focus[aria-invalid=true],[type=checkbox]:checked[aria-invalid=true],[type=checkbox][role=switch]:checked:active[aria-invalid=true],[type=checkbox][role=switch]:checked:focus[aria-invalid=true],[type=checkbox][role=switch]:checked[aria-invalid=true],[type=radio]:checked:active[aria-invalid=true],[type=radio]:checked:focus[aria-invalid=true],[type=radio]:checked[aria-invalid=true]{--pico-border-color:var(--pico-form-element-invalid-border-color)}[type=color]::-webkit-color-swatch-wrapper{padding:0}[type=color]::-moz-focus-inner{padding:0}[type=color]::-webkit-color-swatch{border:0;border-radius:calc(var(--pico-border-radius) * .5)}[type=color]::-moz-color-swatch{border:0;border-radius:calc(var(--pico-border-radius) * .5)}input:not([type=checkbox],[type=radio],[type=range],[type=file]):is([type=date],[type=datetime-local],[type=month],[type=time],[type=week]){--pico-icon-position:0.75rem;--pico-icon-width:1rem;padding-right:calc(var(--pico-icon-width) + var(--pico-icon-position));background-image:var(--pico-icon-date);background-position:center right var(--pico-icon-position);background-size:var(--pico-icon-width) auto;background-repeat:no-repeat}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=time]{background-image:var(--pico-icon-time)}[type=date]::-webkit-calendar-picker-indicator,[type=datetime-local]::-webkit-calendar-picker-indicator,[type=month]::-webkit-calendar-picker-indicator,[type=time]::-webkit-calendar-picker-indicator,[type=week]::-webkit-calendar-picker-indicator{width:var(--pico-icon-width);margin-right:calc(var(--pico-icon-width) * -1);margin-left:var(--pico-icon-position);opacity:0}@-moz-document url-prefix(){[type=date],[type=datetime-local],[type=month],[type=time],[type=week]{padding-right:var(--pico-form-element-spacing-horizontal)!important;background-image:none!important}}[dir=rtl] :is([type=date],[type=datetime-local],[type=month],[type=time],[type=week]){text-align:right}[type=file]{--pico-color:var(--pico-muted-color);margin-left:calc(var(--pico-outline-width) * -1);padding:calc(var(--pico-form-element-spacing-vertical) * .5) 0;padding-left:var(--pico-outline-width);border:0;border-radius:0;background:0 0}[type=file]::file-selector-button{margin-right:calc(var(--pico-spacing)/ 2);padding:calc(var(--pico-form-element-spacing-vertical) * .5) var(--pico-form-element-spacing-horizontal)}[type=file]:is(:hover,:active,:focus)::file-selector-button{--pico-background-color:var(--pico-secondary-hover-background);--pico-border-color:var(--pico-secondary-hover-border)}[type=file]:focus::file-selector-button{--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--pico-outline-width) var(--pico-secondary-focus)}[type=range]{-webkit-appearance:none;-moz-appearance:none;appearance:none;width:100%;height:1.25rem;background:0 0}[type=range]::-webkit-slider-runnable-track{width:100%;height:.375rem;border-radius:var(--pico-border-radius);background-color:var(--pico-range-border-color);-webkit-transition:background-color var(--pico-transition),box-shadow var(--pico-transition);transition:background-color var(--pico-transition),box-shadow var(--pico-transition)}[type=range]::-moz-range-track{width:100%;height:.375rem;border-radius:var(--pico-border-radius);background-color:var(--pico-range-border-color);-moz-transition:background-color var(--pico-transition),box-shadow var(--pico-transition);transition:background-color var(--pico-transition),box-shadow var(--pico-transition)}[type=range]::-ms-track{width:100%;height:.375rem;border-radius:var(--pico-border-radius);background-color:var(--pico-range-border-color);-ms-transition:background-color var(--pico-transition),box-shadow var(--pico-transition);transition:background-color var(--pico-transition),box-shadow var(--pico-transition)}[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:1.25rem;height:1.25rem;margin-top:-.4375rem;border:2px solid var(--pico-range-thumb-border-color);border-radius:50%;background-color:var(--pico-range-thumb-color);cursor:pointer;-webkit-transition:background-color var(--pico-transition),transform var(--pico-transition);transition:background-color var(--pico-transition),transform var(--pico-transition)}[type=range]::-moz-range-thumb{-webkit-appearance:none;width:1.25rem;height:1.25rem;margin-top:-.4375rem;border:2px solid var(--pico-range-thumb-border-color);border-radius:50%;background-color:var(--pico-range-thumb-color);cursor:pointer;-moz-transition:background-color var(--pico-transition),transform var(--pico-transition);transition:background-color var(--pico-transition),transform var(--pico-transition)}[type=range]::-ms-thumb{-webkit-appearance:none;width:1.25rem;height:1.25rem;margin-top:-.4375rem;border:2px solid var(--pico-range-thumb-border-color);border-radius:50%;background-color:var(--pico-range-thumb-color);cursor:pointer;-ms-transition:background-color var(--pico-transition),transform var(--pico-transition);transition:background-color var(--pico-transition),transform var(--pico-transition)}[type=range]:active,[type=range]:focus-within{--pico-range-border-color:var(--pico-range-active-border-color);--pico-range-thumb-color:var(--pico-range-thumb-active-color)}[type=range]:active::-webkit-slider-thumb{transform:scale(1.25)}[type=range]:active::-moz-range-thumb{transform:scale(1.25)}[type=range]:active::-ms-thumb{transform:scale(1.25)}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search]{padding-inline-start:calc(var(--pico-form-element-spacing-horizontal) + 1.75rem);background-image:var(--pico-icon-search);background-position:center left calc(var(--pico-form-element-spacing-horizontal) + .125rem);background-size:1rem auto;background-repeat:no-repeat}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid]{padding-inline-start:calc(var(--pico-form-element-spacing-horizontal) + 1.75rem)!important;background-position:center left 1.125rem,center right .75rem}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid=false]{background-image:var(--pico-icon-search),var(--pico-icon-valid)}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid=true]{background-image:var(--pico-icon-search),var(--pico-icon-invalid)}[dir=rtl] :where(input):not([type=checkbox],[type=radio],[type=range],[type=file])[type=search]{background-position:center right 1.125rem}[dir=rtl] :where(input):not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid]{background-position:center right 1.125rem,center left .75rem}details{display:block;margin-bottom:var(--pico-spacing)}details summary{line-height:1rem;list-style-type:none;cursor:pointer;transition:color var(--pico-transition)}details summary:not([role]){color:var(--pico-accordion-close-summary-color)}details summary::-webkit-details-marker{display:none}details summary::marker{display:none}details summary::-moz-list-bullet{list-style-type:none}details summary::after{display:block;width:1rem;height:1rem;margin-inline-start:calc(var(--pico-spacing,1rem) * .5);float:right;transform:rotate(-90deg);background-image:var(--pico-icon-chevron);background-position:right center;background-size:1rem auto;background-repeat:no-repeat;content:"";transition:transform var(--pico-transition)}details summary:focus{outline:0}details summary:focus:not([role]){color:var(--pico-accordion-active-summary-color)}details summary:focus-visible:not([role]){outline:var(--pico-outline-width) solid var(--pico-primary-focus);outline-offset:calc(var(--pico-spacing,1rem) * 0.5);color:var(--pico-primary)}details summary[role=button]{width:100%;text-align:left}details summary[role=button]::after{height:calc(1rem * var(--pico-line-height,1.5))}details[open]>summary{margin-bottom:var(--pico-spacing)}details[open]>summary:not([role]):not(:focus){color:var(--pico-accordion-open-summary-color)}details[open]>summary::after{transform:rotate(0)}[dir=rtl] details summary{text-align:right}[dir=rtl] details summary::after{float:left;background-position:left center}article{margin-bottom:var(--pico-block-spacing-vertical);padding:var(--pico-block-spacing-vertical) var(--pico-block-spacing-horizontal);border-radius:var(--pico-border-radius);background:var(--pico-card-background-color);box-shadow:var(--pico-card-box-shadow)}article>footer,article>header{margin-right:calc(var(--pico-block-spacing-horizontal) * -1);margin-left:calc(var(--pico-block-spacing-horizontal) * -1);padding:calc(var(--pico-block-spacing-vertical) * .66) var(--pico-block-spacing-horizontal);background-color:var(--pico-card-sectioning-background-color)}article>header{margin-top:calc(var(--pico-block-spacing-vertical) * -1);margin-bottom:var(--pico-block-spacing-vertical);border-bottom:var(--pico-border-width) solid var(--pico-card-border-color);border-top-right-radius:var(--pico-border-radius);border-top-left-radius:var(--pico-border-radius)}article>footer{margin-top:var(--pico-block-spacing-vertical);margin-bottom:calc(var(--pico-block-spacing-vertical) * -1);border-top:var(--pico-border-width) solid var(--pico-card-border-color);border-bottom-right-radius:var(--pico-border-radius);border-bottom-left-radius:var(--pico-border-radius)}details.dropdown{position:relative;border-bottom:none}details.dropdown>a::after,details.dropdown>button::after,details.dropdown>summary::after{display:block;width:1rem;height:calc(1rem * var(--pico-line-height,1.5));margin-inline-start:.25rem;float:right;transform:rotate(0) translateX(.2rem);background-image:var(--pico-icon-chevron);background-position:right center;background-size:1rem auto;background-repeat:no-repeat;content:""}nav details.dropdown{margin-bottom:0}details.dropdown>summary:not([role]){height:calc(1rem * var(--pico-line-height) + var(--pico-form-element-spacing-vertical) * 2 + var(--pico-border-width) * 2);padding:var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal);border:var(--pico-border-width) solid var(--pico-form-element-border-color);border-radius:var(--pico-border-radius);background-color:var(--pico-form-element-background-color);color:var(--pico-form-element-placeholder-color);line-height:inherit;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;transition:background-color var(--pico-transition),border-color var(--pico-transition),color var(--pico-transition),box-shadow var(--pico-transition)}details.dropdown>summary:not([role]):active,details.dropdown>summary:not([role]):focus{border-color:var(--pico-form-element-active-border-color);background-color:var(--pico-form-element-active-background-color)}details.dropdown>summary:not([role]):focus{box-shadow:0 0 0 var(--pico-outline-width) var(--pico-form-element-focus-color)}details.dropdown>summary:not([role]):focus-visible{outline:0}details.dropdown>summary:not([role])[aria-invalid=false]{--pico-form-element-border-color:var(--pico-form-element-valid-border-color);--pico-form-element-active-border-color:var(--pico-form-element-valid-focus-color);--pico-form-element-focus-color:var(--pico-form-element-valid-focus-color)}details.dropdown>summary:not([role])[aria-invalid=true]{--pico-form-element-border-color:var(--pico-form-element-invalid-border-color);--pico-form-element-active-border-color:var(--pico-form-element-invalid-focus-color);--pico-form-element-focus-color:var(--pico-form-element-invalid-focus-color)}nav details.dropdown{display:inline;margin:calc(var(--pico-nav-element-spacing-vertical) * -1) 0}nav details.dropdown>summary::after{transform:rotate(0) translateX(0)}nav details.dropdown>summary:not([role]){height:calc(1rem * var(--pico-line-height) + var(--pico-nav-link-spacing-vertical) * 2);padding:calc(var(--pico-nav-link-spacing-vertical) - var(--pico-border-width) * 2) var(--pico-nav-link-spacing-horizontal)}nav details.dropdown>summary:not([role]):focus-visible{box-shadow:0 0 0 var(--pico-outline-width) var(--pico-primary-focus)}details.dropdown>summary+ul{display:flex;z-index:99;position:absolute;left:0;flex-direction:column;width:100%;min-width:-moz-fit-content;min-width:fit-content;margin:0;margin-top:var(--pico-outline-width);padding:0;border:var(--pico-border-width) solid var(--pico-dropdown-border-color);border-radius:var(--pico-border-radius);background-color:var(--pico-dropdown-background-color);box-shadow:var(--pico-dropdown-box-shadow);color:var(--pico-dropdown-color);white-space:nowrap;opacity:0;transition:opacity var(--pico-transition),transform 0s ease-in-out 1s}details.dropdown>summary+ul[dir=rtl]{right:0;left:auto}details.dropdown>summary+ul li{width:100%;margin-bottom:0;padding:calc(var(--pico-form-element-spacing-vertical) * .5) var(--pico-form-element-spacing-horizontal);list-style:none}details.dropdown>summary+ul li:first-of-type{margin-top:calc(var(--pico-form-element-spacing-vertical) * .5)}details.dropdown>summary+ul li:last-of-type{margin-bottom:calc(var(--pico-form-element-spacing-vertical) * .5)}details.dropdown>summary+ul li a{display:block;margin:calc(var(--pico-form-element-spacing-vertical) * -.5) calc(var(--pico-form-element-spacing-horizontal) * -1);padding:calc(var(--pico-form-element-spacing-vertical) * .5) var(--pico-form-element-spacing-horizontal);overflow:hidden;border-radius:0;color:var(--pico-dropdown-color);text-decoration:none;text-overflow:ellipsis}details.dropdown>summary+ul li a:active,details.dropdown>summary+ul li a:focus,details.dropdown>summary+ul li a:focus-visible,details.dropdown>summary+ul li a:hover,details.dropdown>summary+ul li a[aria-current]:not([aria-current=false]){background-color:var(--pico-dropdown-hover-background-color)}details.dropdown>summary+ul li label{width:100%}details.dropdown>summary+ul li:has(label):hover{background-color:var(--pico-dropdown-hover-background-color)}details.dropdown[open]>summary{margin-bottom:0}details.dropdown[open]>summary+ul{transform:scaleY(1);opacity:1;transition:opacity var(--pico-transition),transform 0s ease-in-out 0s}details.dropdown[open]>summary::before{display:block;z-index:1;position:fixed;width:100vw;height:100vh;inset:0;background:0 0;content:"";cursor:default}label>details.dropdown{margin-top:calc(var(--pico-spacing) * .25)}[role=group],[role=search]{display:inline-flex;position:relative;width:100%;margin-bottom:var(--pico-spacing);border-radius:var(--pico-border-radius);box-shadow:var(--pico-group-box-shadow,0 0 0 transparent);vertical-align:middle;transition:box-shadow var(--pico-transition)}[role=group] input:not([type=checkbox],[type=radio]),[role=group] select,[role=group]>*,[role=search] input:not([type=checkbox],[type=radio]),[role=search] select,[role=search]>*{position:relative;flex:1 1 auto;margin-bottom:0}[role=group] input:not([type=checkbox],[type=radio]):not(:first-child),[role=group] select:not(:first-child),[role=group]>:not(:first-child),[role=search] input:not([type=checkbox],[type=radio]):not(:first-child),[role=search] select:not(:first-child),[role=search]>:not(:first-child){margin-left:0;border-top-left-radius:0;border-bottom-left-radius:0}[role=group] input:not([type=checkbox],[type=radio]):not(:last-child),[role=group] select:not(:last-child),[role=group]>:not(:last-child),[role=search] input:not([type=checkbox],[type=radio]):not(:last-child),[role=search] select:not(:last-child),[role=search]>:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}[role=group] input:not([type=checkbox],[type=radio]):focus,[role=group] select:focus,[role=group]>:focus,[role=search] input:not([type=checkbox],[type=radio]):focus,[role=search] select:focus,[role=search]>:focus{z-index:2}[role=group] [role=button]:not(:first-child),[role=group] [type=button]:not(:first-child),[role=group] [type=reset]:not(:first-child),[role=group] [type=submit]:not(:first-child),[role=group] button:not(:first-child),[role=group] input:not([type=checkbox],[type=radio]):not(:first-child),[role=group] select:not(:first-child),[role=search] [role=button]:not(:first-child),[role=search] [type=button]:not(:first-child),[role=search] [type=reset]:not(:first-child),[role=search] [type=submit]:not(:first-child),[role=search] button:not(:first-child),[role=search] input:not([type=checkbox],[type=radio]):not(:first-child),[role=search] select:not(:first-child){margin-left:calc(var(--pico-border-width) * -1)}[role=group] [role=button],[role=group] [type=button],[role=group] [type=reset],[role=group] [type=submit],[role=group] button,[role=search] [role=button],[role=search] [type=button],[role=search] [type=reset],[role=search] [type=submit],[role=search] button{width:auto}@supports selector(:has(*)){[role=group]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus),[role=search]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus){--pico-group-box-shadow:var(--pico-group-box-shadow-focus-with-button)}[role=group]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus) input:not([type=checkbox],[type=radio]),[role=group]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus) select,[role=search]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus) input:not([type=checkbox],[type=radio]),[role=search]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus) select{border-color:transparent}[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus),[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus){--pico-group-box-shadow:var(--pico-group-box-shadow-focus-with-input)}[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus) [role=button],[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus) [type=button],[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus) [type=submit],[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus) button,[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus) [role=button],[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus) [type=button],[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus) [type=submit],[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus) button{--pico-button-box-shadow:0 0 0 var(--pico-border-width) var(--pico-primary-border);--pico-button-hover-box-shadow:0 0 0 var(--pico-border-width) var(--pico-primary-hover-border)}[role=group] [role=button]:focus,[role=group] [type=button]:focus,[role=group] [type=reset]:focus,[role=group] [type=submit]:focus,[role=group] button:focus,[role=search] [role=button]:focus,[role=search] [type=button]:focus,[role=search] [type=reset]:focus,[role=search] [type=submit]:focus,[role=search] button:focus{box-shadow:none}}[role=search]>:first-child{border-top-left-radius:5rem;border-bottom-left-radius:5rem}[role=search]>:last-child{border-top-right-radius:5rem;border-bottom-right-radius:5rem}[aria-busy=true]:not(input,select,textarea,html,form){white-space:nowrap}[aria-busy=true]:not(input,select,textarea,html,form)::before{display:inline-block;width:1em;height:1em;background-image:var(--pico-icon-loading);background-size:1em auto;background-repeat:no-repeat;content:"";vertical-align:-.125em}[aria-busy=true]:not(input,select,textarea,html,form):not(:empty)::before{margin-inline-end:calc(var(--pico-spacing) * .5)}[aria-busy=true]:not(input,select,textarea,html,form):empty{text-align:center}[role=button][aria-busy=true],[type=button][aria-busy=true],[type=reset][aria-busy=true],[type=submit][aria-busy=true],a[aria-busy=true],button[aria-busy=true]{pointer-events:none}:host,:root{--pico-scrollbar-width:0px}dialog{display:flex;z-index:999;position:fixed;top:0;right:0;bottom:0;left:0;align-items:center;justify-content:center;width:inherit;min-width:100%;height:inherit;min-height:100%;padding:0;border:0;-webkit-backdrop-filter:var(--pico-modal-overlay-backdrop-filter);backdrop-filter:var(--pico-modal-overlay-backdrop-filter);background-color:var(--pico-modal-overlay-background-color);color:var(--pico-color)}dialog>article{width:100%;max-height:calc(100vh - var(--pico-spacing) * 2);margin:var(--pico-spacing);overflow:auto}@media (min-width:576px){dialog>article{max-width:510px}}@media (min-width:768px){dialog>article{max-width:700px}}dialog>article>header>*{margin-bottom:0}dialog>article>header .close,dialog>article>header :is(a,button)[rel=prev]{margin:0;margin-left:var(--pico-spacing);padding:0;float:right}dialog>article>footer{text-align:right}dialog>article>footer [role=button],dialog>article>footer button{margin-bottom:0}dialog>article>footer [role=button]:not(:first-of-type),dialog>article>footer button:not(:first-of-type){margin-left:calc(var(--pico-spacing) * .5)}dialog>article .close,dialog>article :is(a,button)[rel=prev]{display:block;width:1rem;height:1rem;margin-top:calc(var(--pico-spacing) * -1);margin-bottom:var(--pico-spacing);margin-left:auto;border:none;background-image:var(--pico-icon-close);background-position:center;background-size:auto 1rem;background-repeat:no-repeat;background-color:transparent;opacity:.5;transition:opacity var(--pico-transition)}dialog>article .close:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),dialog>article :is(a,button)[rel=prev]:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){opacity:1}dialog:not([open]),dialog[open=false]{display:none}.modal-is-open{padding-right:var(--pico-scrollbar-width,0);overflow:hidden;pointer-events:none;touch-action:none}.modal-is-open dialog{pointer-events:auto;touch-action:auto}:where(.modal-is-opening,.modal-is-closing) dialog,:where(.modal-is-opening,.modal-is-closing) dialog>article{animation-duration:.2s;animation-timing-function:ease-in-out;animation-fill-mode:both}:where(.modal-is-opening,.modal-is-closing) dialog{animation-duration:.8s;animation-name:modal-overlay}:where(.modal-is-opening,.modal-is-closing) dialog>article{animation-delay:.2s;animation-name:modal}.modal-is-closing dialog,.modal-is-closing dialog>article{animation-delay:0s;animation-direction:reverse}@keyframes modal-overlay{from{-webkit-backdrop-filter:none;backdrop-filter:none;background-color:transparent}}@keyframes modal{from{transform:translateY(-100%);opacity:0}}:where(nav li)::before{float:left;content:""}nav,nav ul{display:flex}nav{justify-content:space-between;overflow:visible}nav ol,nav ul{align-items:center;margin-bottom:0;padding:0;list-style:none}nav ol:first-of-type,nav ul:first-of-type{margin-left:calc(var(--pico-nav-element-spacing-horizontal) * -1)}nav ol:last-of-type,nav ul:last-of-type{margin-right:calc(var(--pico-nav-element-spacing-horizontal) * -1)}nav li{display:inline-block;margin:0;padding:var(--pico-nav-element-spacing-vertical) var(--pico-nav-element-spacing-horizontal)}nav li :where(a,[role=link]){display:inline-block;margin:calc(var(--pico-nav-link-spacing-vertical) * -1) calc(var(--pico-nav-link-spacing-horizontal) * -1);padding:var(--pico-nav-link-spacing-vertical) var(--pico-nav-link-spacing-horizontal);border-radius:var(--pico-border-radius)}nav li :where(a,[role=link]):not(:hover){text-decoration:none}nav li [role=button],nav li [type=button],nav li button,nav li input:not([type=checkbox],[type=radio],[type=range],[type=file]),nav li select{height:auto;margin-right:inherit;margin-bottom:0;margin-left:inherit;padding:calc(var(--pico-nav-link-spacing-vertical) - var(--pico-border-width) * 2) var(--pico-nav-link-spacing-horizontal)}nav[aria-label=breadcrumb]{align-items:center;justify-content:start}nav[aria-label=breadcrumb] ul li:not(:first-child){margin-inline-start:var(--pico-nav-link-spacing-horizontal)}nav[aria-label=breadcrumb] ul li a{margin:calc(var(--pico-nav-link-spacing-vertical) * -1) 0;margin-inline-start:calc(var(--pico-nav-link-spacing-horizontal) * -1)}nav[aria-label=breadcrumb] ul li:not(:last-child)::after{display:inline-block;position:absolute;width:calc(var(--pico-nav-link-spacing-horizontal) * 4);margin:0 calc(var(--pico-nav-link-spacing-horizontal) * -1);content:var(--pico-nav-breadcrumb-divider);color:var(--pico-muted-color);text-align:center;text-decoration:none;white-space:nowrap}nav[aria-label=breadcrumb] a[aria-current]:not([aria-current=false]){background-color:transparent;color:inherit;text-decoration:none;pointer-events:none}aside li,aside nav,aside ol,aside ul{display:block}aside li{padding:calc(var(--pico-nav-element-spacing-vertical) * .5) var(--pico-nav-element-spacing-horizontal)}aside li a{display:block}aside li [role=button]{margin:inherit}[dir=rtl] nav[aria-label=breadcrumb] ul li:not(:last-child) ::after{content:"\\"}progress{display:inline-block;vertical-align:baseline}progress{-webkit-appearance:none;-moz-appearance:none;display:inline-block;appearance:none;width:100%;height:.5rem;margin-bottom:calc(var(--pico-spacing) * .5);overflow:hidden;border:0;border-radius:var(--pico-border-radius);background-color:var(--pico-progress-background-color);color:var(--pico-progress-color)}progress::-webkit-progress-bar{border-radius:var(--pico-border-radius);background:0 0}progress[value]::-webkit-progress-value{background-color:var(--pico-progress-color);-webkit-transition:inline-size var(--pico-transition);transition:inline-size var(--pico-transition)}progress::-moz-progress-bar{background-color:var(--pico-progress-color)}@media (prefers-reduced-motion:no-preference){progress:indeterminate{background:var(--pico-progress-background-color) linear-gradient(to right,var(--pico-progress-color) 30%,var(--pico-progress-background-color) 30%) top left/150% 150% no-repeat;animation:progress-indeterminate 1s linear infinite}progress:indeterminate[value]::-webkit-progress-value{background-color:transparent}progress:indeterminate::-moz-progress-bar{background-color:transparent}}@media (prefers-reduced-motion:no-preference){[dir=rtl] progress:indeterminate{animation-direction:reverse}}@keyframes progress-indeterminate{0%{background-position:200% 0}100%{background-position:-200% 0}}[data-tooltip]{position:relative}[data-tooltip]:not(a,button,input,[role=button]){border-bottom:1px dotted;text-decoration:none;cursor:help}[data-tooltip]::after,[data-tooltip]::before,[data-tooltip][data-placement=top]::after,[data-tooltip][data-placement=top]::before{display:block;z-index:99;position:absolute;bottom:100%;left:50%;padding:.25rem .5rem;overflow:hidden;transform:translate(-50%,-.25rem);border-radius:var(--pico-border-radius);background:var(--pico-tooltip-background-color);content:attr(data-tooltip);color:var(--pico-tooltip-color);font-style:normal;font-weight:var(--pico-font-weight);font-size:.875rem;text-decoration:none;text-overflow:ellipsis;white-space:nowrap;opacity:0;pointer-events:none}[data-tooltip]::after,[data-tooltip][data-placement=top]::after{padding:0;transform:translate(-50%,0);border-top:.3rem solid;border-right:.3rem solid transparent;border-left:.3rem solid transparent;border-radius:0;background-color:transparent;content:"";color:var(--pico-tooltip-background-color)}[data-tooltip][data-placement=bottom]::after,[data-tooltip][data-placement=bottom]::before{top:100%;bottom:auto;transform:translate(-50%,.25rem)}[data-tooltip][data-placement=bottom]:after{transform:translate(-50%,-.3rem);border:.3rem solid transparent;border-bottom:.3rem solid}[data-tooltip][data-placement=left]::after,[data-tooltip][data-placement=left]::before{top:50%;right:100%;bottom:auto;left:auto;transform:translate(-.25rem,-50%)}[data-tooltip][data-placement=left]:after{transform:translate(.3rem,-50%);border:.3rem solid transparent;border-left:.3rem solid}[data-tooltip][data-placement=right]::after,[data-tooltip][data-placement=right]::before{top:50%;right:auto;bottom:auto;left:100%;transform:translate(.25rem,-50%)}[data-tooltip][data-placement=right]:after{transform:translate(-.3rem,-50%);border:.3rem solid transparent;border-right:.3rem solid}[data-tooltip]:focus::after,[data-tooltip]:focus::before,[data-tooltip]:hover::after,[data-tooltip]:hover::before{opacity:1}@media (hover:hover) and (pointer:fine){[data-tooltip]:focus::after,[data-tooltip]:focus::before,[data-tooltip]:hover::after,[data-tooltip]:hover::before{--pico-tooltip-slide-to:translate(-50%, -0.25rem);transform:translate(-50%,.75rem);animation-duration:.2s;animation-fill-mode:forwards;animation-name:tooltip-slide;opacity:0}[data-tooltip]:focus::after,[data-tooltip]:hover::after{--pico-tooltip-caret-slide-to:translate(-50%, 0rem);transform:translate(-50%,-.25rem);animation-name:tooltip-caret-slide}[data-tooltip][data-placement=bottom]:focus::after,[data-tooltip][data-placement=bottom]:focus::before,[data-tooltip][data-placement=bottom]:hover::after,[data-tooltip][data-placement=bottom]:hover::before{--pico-tooltip-slide-to:translate(-50%, 0.25rem);transform:translate(-50%,-.75rem);animation-name:tooltip-slide}[data-tooltip][data-placement=bottom]:focus::after,[data-tooltip][data-placement=bottom]:hover::after{--pico-tooltip-caret-slide-to:translate(-50%, -0.3rem);transform:translate(-50%,-.5rem);animation-name:tooltip-caret-slide}[data-tooltip][data-placement=left]:focus::after,[data-tooltip][data-placement=left]:focus::before,[data-tooltip][data-placement=left]:hover::after,[data-tooltip][data-placement=left]:hover::before{--pico-tooltip-slide-to:translate(-0.25rem, -50%);transform:translate(.75rem,-50%);animation-name:tooltip-slide}[data-tooltip][data-placement=left]:focus::after,[data-tooltip][data-placement=left]:hover::after{--pico-tooltip-caret-slide-to:translate(0.3rem, -50%);transform:translate(.05rem,-50%);animation-name:tooltip-caret-slide}[data-tooltip][data-placement=right]:focus::after,[data-tooltip][data-placement=right]:focus::before,[data-tooltip][data-placement=right]:hover::after,[data-tooltip][data-placement=right]:hover::before{--pico-tooltip-slide-to:translate(0.25rem, -50%);transform:translate(-.75rem,-50%);animation-name:tooltip-slide}[data-tooltip][data-placement=right]:focus::after,[data-tooltip][data-placement=right]:hover::after{--pico-tooltip-caret-slide-to:translate(-0.3rem, -50%);transform:translate(-.05rem,-50%);animation-name:tooltip-caret-slide}}@keyframes tooltip-slide{to{transform:var(--pico-tooltip-slide-to);opacity:1}}@keyframes tooltip-caret-slide{50%{opacity:0}to{transform:var(--pico-tooltip-caret-slide-to);opacity:1}}[aria-controls]{cursor:pointer}[aria-disabled=true],[disabled]{cursor:not-allowed}[aria-hidden=false][hidden]{display:initial}[aria-hidden=false][hidden]:not(:focus){clip:rect(0,0,0,0);position:absolute}[tabindex],a,area,button,input,label,select,summary,textarea{-ms-touch-action:manipulation}[dir=rtl]{direction:rtl}@media (prefers-reduced-motion:reduce){:not([aria-busy=true]),:not([aria-busy=true])::after,:not([aria-busy=true])::before{background-attachment:initial!important;animation-duration:1ms!important;animation-delay:-1ms!important;animation-iteration-count:1!important;scroll-behavior:auto!important;transition-delay:0s!important;transition-duration:0s!important}}
\ No newline at end of file
diff --git a/web_test/PLC S7-315 Streamer & Logger_files/plc.js.descargar b/web_test/PLC S7-315 Streamer & Logger_files/plc.js.descargar
deleted file mode 100644
index 9f3fd1d..0000000
--- a/web_test/PLC S7-315 Streamer & Logger_files/plc.js.descargar
+++ /dev/null
@@ -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');
- });
- });
-}
\ No newline at end of file
diff --git a/web_test/PLC S7-315 Streamer & Logger_files/plotting.js.descargar b/web_test/PLC S7-315 Streamer & Logger_files/plotting.js.descargar
deleted file mode 100644
index 6eb91b2..0000000
--- a/web_test/PLC S7-315 Streamer & Logger_files/plotting.js.descargar
+++ /dev/null
@@ -1,1461 +0,0 @@
-/**
- * 📈 Real-Time Plotting System
- * Maneja sesiones de plotting con Chart.js y comunicación con el backend
- */
-
-// 🔧 Global debugging para plots
-window.enablePlotDebug = () => {
- window.plotDebugEnabled = true;
- console.log('📈 Plot debugging enabled. Check console for detailed logs.');
-};
-
-window.disablePlotDebug = () => {
- window.plotDebugEnabled = false;
- console.log('📈 Plot debugging disabled.');
-};
-
-// Helper function para logging condicional
-function plotDebugLog(...args) {
- if (window.plotDebugEnabled) {
- console.debug(...args);
- }
-}
-
-// 🔧 Test function para verificar que todo funciona
-window.testPlotSystem = async () => {
- console.log('📈 Testing plot system...');
-
- try {
- // Test 1: Verificar disponibilidad de variables
- const varsResponse = await fetch('/api/plots/variables');
- const varsData = await varsResponse.json();
- console.log('✅ Available variables:', varsData.available_variables.length);
- console.log('✅ Boolean variables for triggers:', varsData.boolean_variables.length);
-
- // Test 2: Verificar plots existentes
- const plotsResponse = await fetch('/api/plots');
- const plotsData = await plotsResponse.json();
- console.log('✅ Existing plot sessions:', plotsData.sessions.length);
-
- // Test 3: Si hay plots, verificar datos
- if (plotsData.sessions.length > 0) {
- const sessionId = plotsData.sessions[0].session_id;
- const dataResponse = await fetch(`/api/plots/${sessionId}/data`);
- const dataResult = await dataResponse.json();
- console.log(`✅ Plot ${sessionId} has ${dataResult.datasets?.length || 0} datasets with ${dataResult.data_points_count || 0} points`);
- }
-
- console.log('📈 Plot system test completed. Enable debug with enablePlotDebug() for detailed logs.');
-
- } catch (error) {
- console.error('❌ Plot system test failed:', error);
- }
-};
-
-class PlotManager {
- constructor() {
- this.sessions = new Map(); // session_id -> Chart instance
- this.updateInterval = null;
- this.statusUpdateInterval = null; // 🔑 NUEVO: Para updates de status
- this.isInitialized = false;
-
- // Colores para las variables
- this.colors = [
- '#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF',
- '#FF9F40', '#8A2BE2', '#FF1493', '#00CED1', '#32CD32',
- '#FFB347', '#DA70D6', '#40E0D0', '#EE82EE', '#90EE90'
- ];
-
- // Variables seleccionadas para el formulario
- this.selectedVariables = new Map(); // variable_name -> { color, dataset }
- this.availableDatasets = [];
- this.currentEditingSession = null;
-
- this.init();
- }
-
- init() {
- // Cargar sesiones existentes al inicializar
- this.loadExistingSessions();
-
- // Inicializar formulario colapsable
- this.initCollapsibleForm();
-
- // Inicializar modal de selección de variables
- this.initVariableModal();
-
- // Iniciar actualización automática
- this.startAutoUpdate();
-
- this.isInitialized = true;
- console.log('📈 Plot Manager initialized');
- }
-
- async loadExistingSessions() {
- try {
- const response = await fetch('/api/plots');
- const data = await response.json();
-
- if (data.sessions) {
- for (const session of data.sessions) {
- // 🔑 NUEVO: Obtener configuración completa para cada sesión
- try {
- const configResponse = await fetch(`/api/plots/${session.session_id}/config`);
- const configData = await configResponse.json();
-
- let sessionInfo = session;
-
- // Si tenemos la configuración completa, usar esa información
- if (configData.success && configData.config) {
- sessionInfo = {
- ...session,
- is_active: configData.config.is_active,
- is_paused: configData.config.is_paused,
- variables_count: configData.config.variables ? configData.config.variables.length : session.variables_count,
- trigger_enabled: configData.config.trigger_enabled,
- trigger_variable: configData.config.trigger_variable,
- trigger_on_true: configData.config.trigger_on_true
- };
- }
-
- await this.createPlotSessionTab(session.session_id, sessionInfo);
- } catch (configError) {
- console.error(`Error loading config for session ${session.session_id}:`, configError);
- // Usar información básica si falla la carga de configuración
- await this.createPlotSessionTab(session.session_id, session);
- }
- }
- }
- } catch (error) {
- console.error('Error loading existing plot sessions:', error);
- }
- }
-
- startAutoUpdate() {
- // Actualizar datos cada 500ms para plots activos
- this.updateInterval = setInterval(() => {
- this.updateAllSessions();
- }, 500);
-
- // 🔑 NUEVO: Actualizar status cada 2 segundos para mantener sincronización
- this.statusUpdateInterval = setInterval(() => {
- this.updateAllSessionsStatus();
- }, 2000);
- }
-
- stopAutoUpdate() {
- if (this.updateInterval) {
- clearInterval(this.updateInterval);
- this.updateInterval = null;
- }
-
- // 🔑 NUEVO: Detener también el update de status
- if (this.statusUpdateInterval) {
- clearInterval(this.statusUpdateInterval);
- this.statusUpdateInterval = null;
- }
- }
-
- async updateAllSessions() {
- const activeSessions = Array.from(this.sessions.keys());
-
- for (const sessionId of activeSessions) {
- await this.updateSessionData(sessionId);
- }
- }
-
- // 🔑 NUEVO: Actualizar status de todas las sesiones
- async updateAllSessionsStatus() {
- const activeSessions = Array.from(this.sessions.keys());
-
- for (const sessionId of activeSessions) {
- await this.updateSessionStatus(sessionId);
- }
- }
-
- async updateSessionData(sessionId) {
- try {
- // 🚀 NUEVO: Para streaming, el refreshStreamingData maneja la actualización automática
- // Esta función ahora es principalmente para compatibilidad y casos especiales
- const sessionData = this.sessions.get(sessionId);
- if (!sessionData || !sessionData.chart) {
- return;
- }
-
- const response = await fetch(`/api/plots/${sessionId}/data`);
- const plotData = await response.json();
-
- if (plotData.datasets) {
- // 🔧 DEBUG: Log para troubleshooting
- if (plotData.datasets.length > 1) {
- plotDebugLog(`📈 Plot ${sessionId}: Manual update ${plotData.datasets.length} variables, ${plotData.data_points_count} total points`);
- }
- this.updateChart(sessionId, plotData);
- } else {
- console.warn(`📈 Plot ${sessionId}: No datasets received from API`);
- }
- } catch (error) {
- console.error(`Error updating session ${sessionId}:`, error);
- }
- }
-
- createPlotSession(sessionId, config) {
- // Crear contenedor para el plot
- const container = document.createElement('div');
- container.className = 'plot-session';
- container.id = `plot-${sessionId}`;
-
- container.innerHTML = `
-
-
-
- Variables: ${config.variables_count || 0} |
- Data Points: 0 |
- ${config.trigger_enabled ? `Trigger: ${config.trigger_variable} (${config.trigger_on_true ? 'True' : 'False'})` : 'No Trigger'}
-
-
-
-
-
- `;
-
- document.getElementById('plot-sessions-container').appendChild(container);
-
- // 🚀 NUEVO: Usar chartjs-plugin-streaming para crear chart
- const ctx = document.getElementById(`chart-${sessionId}`).getContext('2d');
-
- // Configurar streaming con la nueva librería
- const streamingConfig = window.ChartStreaming.createStreamingChartConfig({
- duration: (config.time_window || 60) * 1000, // Convertir a milisegundos
- refresh: 500, // Actualizar cada 500ms
- frameRate: 30,
- pause: false,
- yMin: config.y_min,
- yMax: config.y_max,
- onRefresh: (chart) => {
- // Esta función se llama automáticamente para obtener nuevos datos
- this.refreshStreamingData(sessionId, chart);
- }
- });
-
- const chart = new Chart(ctx, streamingConfig);
-
- this.sessions.set(sessionId, {
- chart: chart,
- config: config,
- lastDataFetch: 0,
- datasets: new Map() // Para tracking de datasets por variable
- });
-
- // Inicializar datasets para las variables del plot
- this.initializeStreamingDatasets(sessionId, config);
-
- console.log(`📈 Created streaming plot session: ${sessionId}`);
- }
-
- updateChart(sessionId, plotData) {
- const sessionData = this.sessions.get(sessionId);
- if (!sessionData || !sessionData.chart) {
- console.warn(`📈 Plot ${sessionId}: Chart not found in sessions`);
- return;
- }
-
- const chart = sessionData.chart;
-
- // 🔧 DEBUG: Verificar datos antes de actualizar
- if (plotData.datasets && plotData.datasets.length > 0) {
- plotDebugLog(`📈 Plot ${sessionId}: Updating streaming chart with ${plotData.datasets.length} datasets`);
- plotData.datasets.forEach((dataset, idx) => {
- plotDebugLog(` - Variable ${idx + 1}: ${dataset.label} (${dataset.data.length} points)`);
- });
- }
-
- // 🚀 NUEVO: Para streaming, agregamos nuevos datos en lugar de reemplazar todo
- this.updateStreamingData(sessionId, plotData);
-
- // Actualizar escalas Y - mejore el auto-scaling
- if (plotData.y_min !== undefined && plotData.y_max !== undefined) {
- chart.options.scales.y.min = plotData.y_min;
- chart.options.scales.y.max = plotData.y_max;
- } else {
- // Auto-scaling mejorado cuando no hay límites definidos
- chart.options.scales.y.min = undefined;
- chart.options.scales.y.max = undefined;
- }
-
- // Actualizar contador de puntos
- const pointsElement = document.getElementById(`points-${sessionId}`);
- if (pointsElement) {
- const totalPoints = plotData.data_points_count || 0;
- pointsElement.textContent = totalPoints;
-
- // 🔧 DEBUG: Log si el contador no coincide
- const calculatedPoints = (plotData.datasets || []).reduce((sum, dataset) => sum + (dataset.data?.length || 0), 0);
- if (totalPoints !== calculatedPoints) {
- plotDebugLog(`📈 Plot ${sessionId}: Points mismatch - reported: ${totalPoints}, calculated: ${calculatedPoints}`);
- }
- }
-
- // 🔑 NUEVO: Actualizar status visual del plot
- if (plotData.is_active !== undefined || plotData.is_paused !== undefined) {
- this.updatePlotStats(sessionId, {
- variables_count: plotData.datasets ? plotData.datasets.length : 0,
- is_active: plotData.is_active,
- is_paused: plotData.is_paused,
- trigger_enabled: plotData.trigger_enabled,
- trigger_variable: plotData.trigger_variable,
- trigger_on_true: plotData.trigger_on_true
- });
- }
-
- // El chart de streaming se actualiza automáticamente, no necesitamos llamar update manualmente
- }
-
- async controlPlot(sessionId, action) {
- try {
- // 🚀 NUEVO: Controlar streaming localmente para algunas acciones
- if (action === 'pause') {
- this.setStreamingPause(sessionId, true);
- } else if (action === 'start') {
- this.setStreamingPause(sessionId, false);
- } else if (action === 'clear') {
- this.clearStreamingData(sessionId);
- }
-
- const response = await fetch(`/api/plots/${sessionId}/control`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({ action: action })
- });
-
- const result = await response.json();
-
- if (result.success) {
- // 🔑 NUEVO: Actualizar status inmediatamente después de la acción
- await this.updateSessionStatus(sessionId);
-
- // Para stop, también pausar el streaming
- if (action === 'stop') {
- this.setStreamingPause(sessionId, true);
- }
-
- // Para start, asegurar que el streaming esté activo
- if (action === 'start') {
- this.setStreamingPause(sessionId, false);
- }
-
- showNotification(result.message, 'success');
- } else {
- showNotification(result.error, 'error');
- }
- } catch (error) {
- console.error(`Error controlling plot ${sessionId}:`, error);
- showNotification('Error controlling plot session', 'error');
- }
- }
-
- // 🔑 NUEVO: Método para actualizar solo el status del plot
- async updateSessionStatus(sessionId) {
- try {
- const response = await fetch(`/api/plots/${sessionId}/config`);
- const data = await response.json();
-
- if (data.success && data.config) {
- this.updatePlotStats(sessionId, data.config);
-
- // 🚀 NUEVO: Actualizar estado de streaming basado en el status del backend
- const sessionData = this.sessions.get(sessionId);
- if (sessionData) {
- // Actualizar configuración local
- sessionData.config = data.config;
-
- // Controlar pausa del streaming basado en el estado del plot
- const shouldPause = !data.config.is_active || data.config.is_paused;
- this.setStreamingPause(sessionId, shouldPause);
- }
- }
- } catch (error) {
- console.error(`Error updating session status ${sessionId}:`, error);
- }
- }
-
- async removePlot(sessionId) {
- try {
- const response = await fetch(`/api/plots/${sessionId}`, {
- method: 'DELETE'
- });
-
- const result = await response.json();
-
- if (result.success) {
- this.removeChart(sessionId);
- showNotification('Plot session removed', 'success');
- } else {
- showNotification(result.error, 'error');
- }
- } catch (error) {
- console.error(`Error removing plot ${sessionId}:`, error);
- showNotification('Error removing plot session', 'error');
- }
- }
-
- removeChart(sessionId) {
- // Destruir Chart.js
- const sessionData = this.sessions.get(sessionId);
- if (sessionData && sessionData.chart) {
- sessionData.chart.destroy();
- this.sessions.delete(sessionId);
- }
-
- // Remover contenedor
- const container = document.getElementById(`plot-${sessionId}`);
- if (container) {
- container.remove();
- }
- }
-
- // 🚀 NUEVAS FUNCIONES PARA STREAMING
-
- /**
- * Inicializa los datasets de streaming para las variables del plot
- */
- initializeStreamingDatasets(sessionId, config) {
- const sessionData = this.sessions.get(sessionId);
- if (!sessionData || !sessionData.chart || !config.variables) {
- return;
- }
-
- const chart = sessionData.chart;
- const datasets = [];
-
- config.variables.forEach((variable, index) => {
- const color = this.getColor(variable, index);
- const dataset = {
- label: variable,
- data: [],
- borderColor: color,
- backgroundColor: color + '20', // Color con transparencia
- borderWidth: 2,
- fill: false,
- pointRadius: 0,
- pointHoverRadius: 3,
- tension: 0.1
- };
-
- datasets.push(dataset);
- sessionData.datasets.set(variable, index);
- });
-
- chart.data.datasets = datasets;
- chart.update('quiet');
-
- plotDebugLog(`📈 Plot ${sessionId}: Initialized ${datasets.length} streaming datasets`);
- }
-
- /**
- * Función llamada automáticamente por chartjs-plugin-streaming para obtener nuevos datos
- */
- async refreshStreamingData(sessionId, chart) {
- try {
- // Evitar llamadas muy frecuentes
- const sessionData = this.sessions.get(sessionId);
- if (!sessionData) return;
-
- const now = Date.now();
- if (now - sessionData.lastDataFetch < 400) { // Mínimo 400ms entre llamadas
- return;
- }
- sessionData.lastDataFetch = now;
-
- // Obtener datos del backend
- const response = await fetch(`/api/plots/${sessionId}/data`);
- const plotData = await response.json();
-
- if (plotData && plotData.datasets) {
- // Para streaming, solo agregamos los datos más recientes
- this.addLatestDataToStreamingChart(sessionId, plotData);
- }
-
- } catch (error) {
- console.error(`Error refreshing streaming data for ${sessionId}:`, error);
- }
- }
-
- /**
- * Actualiza los datos de streaming del chart
- */
- updateStreamingData(sessionId, plotData) {
- const sessionData = this.sessions.get(sessionId);
- if (!sessionData || !sessionData.chart || !plotData.datasets) {
- return;
- }
-
- const chart = sessionData.chart;
-
- // Para cada dataset del backend, agregar los nuevos puntos
- plotData.datasets.forEach((backendDataset, index) => {
- if (chart.data.datasets[index] && backendDataset.data) {
- const chartDataset = chart.data.datasets[index];
-
- // Obtener los últimos puntos que no tengamos aún
- const existingPoints = chartDataset.data.length;
- const newPoints = backendDataset.data.slice(existingPoints);
-
- newPoints.forEach(point => {
- window.ChartStreaming.addStreamingData(chart, index, {
- x: point.x,
- y: point.y
- });
- });
-
- plotDebugLog(`📈 Plot ${sessionId}: Added ${newPoints.length} new points to dataset ${index}`);
- }
- });
- }
-
- /**
- * Agrega solo los datos más recientes al chart de streaming
- */
- addLatestDataToStreamingChart(sessionId, plotData) {
- const sessionData = this.sessions.get(sessionId);
- if (!sessionData || !sessionData.chart || !plotData.datasets) {
- return;
- }
-
- const chart = sessionData.chart;
- const now = Date.now();
-
- // Para cada variable, agregar el punto más reciente
- plotData.datasets.forEach((dataset, index) => {
- if (dataset.data && dataset.data.length > 0) {
- // Obtener el punto más reciente
- const latestPoint = dataset.data[dataset.data.length - 1];
-
- if (latestPoint && latestPoint.y !== null && latestPoint.y !== undefined) {
- window.ChartStreaming.addStreamingData(chart, index, {
- x: now, // Usar timestamp actual para streaming en tiempo real
- y: latestPoint.y
- });
- }
- }
- });
- }
-
- /**
- * Controla la pausa/reanudación del streaming
- */
- setStreamingPause(sessionId, paused) {
- const sessionData = this.sessions.get(sessionId);
- if (sessionData && sessionData.chart) {
- window.ChartStreaming.setStreamingPause(sessionData.chart, paused);
- plotDebugLog(`📈 Plot ${sessionId}: Streaming ${paused ? 'paused' : 'resumed'}`);
- }
- }
-
- /**
- * Limpia todos los datos de streaming
- */
- clearStreamingData(sessionId) {
- const sessionData = this.sessions.get(sessionId);
- if (sessionData && sessionData.chart) {
- window.ChartStreaming.clearStreamingData(sessionData.chart);
- plotDebugLog(`📈 Plot ${sessionId}: Streaming data cleared`);
- }
- }
-
- async createPlotSessionTab(sessionId, sessionInfo) {
- // Crear tab dinámico para el plot
- if (typeof tabManager !== 'undefined') {
- tabManager.createPlotTab(sessionId, sessionInfo.name || `Plot ${sessionId}`);
- }
-
- // 🚀 NUEVO: Obtener configuración completa del plot para el streaming
- let plotConfig = sessionInfo;
- try {
- const configResponse = await fetch(`/api/plots/${sessionId}/config`);
- const configData = await configResponse.json();
- if (configData.success && configData.config) {
- plotConfig = configData.config;
- }
- } catch (error) {
- console.warn(`Could not load plot config for ${sessionId}, using basic info`);
- }
-
- // Crear el Chart.js con streaming en el canvas del tab
- const ctx = document.getElementById(`chart-${sessionId}`).getContext('2d');
-
- // Configurar streaming con la nueva librería
- const streamingConfig = window.ChartStreaming.createStreamingChartConfig({
- duration: (plotConfig.time_window || 60) * 1000, // Convertir a milisegundos
- refresh: 500, // Actualizar cada 500ms
- frameRate: 30,
- pause: !plotConfig.is_active, // Pausar si no está activo
- yMin: plotConfig.y_min,
- yMax: plotConfig.y_max,
- onRefresh: (chart) => {
- // Esta función se llama automáticamente para obtener nuevos datos
- this.refreshStreamingData(sessionId, chart);
- }
- });
-
- const chart = new Chart(ctx, streamingConfig);
-
- this.sessions.set(sessionId, {
- chart: chart,
- config: plotConfig,
- lastDataFetch: 0,
- datasets: new Map() // Para tracking de datasets por variable
- });
-
- // Inicializar datasets para las variables del plot
- if (plotConfig.variables) {
- this.initializeStreamingDatasets(sessionId, plotConfig);
- }
-
- // Actualizar estadísticas del plot
- this.updatePlotStats(sessionId, sessionInfo);
-
- console.log(`📈 Created streaming plot tab and chart for session: ${sessionId}`);
- }
-
- updatePlotStats(sessionId, sessionInfo) {
- const statsElement = document.getElementById(`plot-stats-${sessionId}`);
- if (statsElement) {
- const status = sessionInfo.is_active ?
- (sessionInfo.is_paused ? 'Paused' : 'Active') :
- 'Stopped';
-
- statsElement.innerHTML = `
- Variables: ${sessionInfo.variables_count || 0} |
- Data Points: 0 |
- Status: ${status} |
- ${sessionInfo.trigger_enabled ? `Trigger: ${sessionInfo.trigger_variable} (${sessionInfo.trigger_on_true ? 'True' : 'False'})` : 'No Trigger'}
- `;
- }
- }
-
- async createNewPlot(config) {
- try {
- const response = await fetch('/api/plots', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify(config)
- });
-
- const result = await response.json();
-
- if (result.success) {
- // 🔑 NUEVO: Obtener configuración real del backend en lugar de hardcodear
- const configResponse = await fetch(`/api/plots/${result.session_id}/config`);
- const configData = await configResponse.json();
-
- let sessionConfig = {
- name: config.name,
- variables_count: config.variables.length,
- trigger_enabled: config.trigger_enabled,
- trigger_variable: config.trigger_variable,
- trigger_on_true: config.trigger_on_true,
- is_active: false, // Por defecto stopped hasta que se obtenga del backend
- is_paused: false
- };
-
- // Si tenemos la configuración real del backend, usarla
- if (configData.success && configData.config) {
- sessionConfig = {
- ...sessionConfig,
- is_active: configData.config.is_active,
- is_paused: configData.config.is_paused
- };
- }
-
- await this.createPlotSessionTab(result.session_id, sessionConfig);
-
- // Cambiar al sub-tab del nuevo plot
- if (typeof tabManager !== 'undefined') {
- tabManager.switchSubTab(`plot-${result.session_id}`);
- }
-
- // 🔑 NUEVO: Los plots se auto-inician en el backend, no necesitamos start manual aquí
- console.log(`Plot session created and auto-started: ${result.session_id}`);
-
- showNotification(result.message, 'success');
- return result.session_id;
- } else {
- showNotification(result.error, 'error');
- return null;
- }
- } catch (error) {
- console.error('Error creating new plot:', error);
- showNotification('Error creating plot session', 'error');
- return null;
- }
- }
-
- initCollapsibleForm() {
- const toggleBtn = document.getElementById('toggle-plot-form-btn');
- const formContainer = document.getElementById('plot-form-container');
- const closeBtn = document.getElementById('close-plot-form-btn');
- const cancelBtn = document.getElementById('cancel-plot-form');
- const plotForm = document.getElementById('plot-form');
-
- if (toggleBtn) {
- toggleBtn.addEventListener('click', () => {
- this.showPlotForm();
- });
- }
-
- if (closeBtn) {
- closeBtn.addEventListener('click', () => {
- this.hidePlotForm();
- });
- }
-
- if (cancelBtn) {
- cancelBtn.addEventListener('click', () => {
- this.hidePlotForm();
- });
- }
-
- if (plotForm) {
- plotForm.addEventListener('submit', (e) => {
- e.preventDefault();
- this.handleFormSubmit();
- });
- }
-
- // Botón para abrir modal de variables
- const selectVariablesBtn = document.getElementById('select-variables-btn');
- if (selectVariablesBtn) {
- selectVariablesBtn.addEventListener('click', () => {
- this.showVariableModal();
- });
- }
- }
-
- initVariableModal() {
- const modal = document.getElementById('variable-selection-modal');
- const closeBtn = document.getElementById('close-variable-modal');
- const cancelBtn = document.getElementById('cancel-variable-selection');
- const confirmBtn = document.getElementById('confirm-variable-selection');
- const selectAllBtn = document.getElementById('select-all-variables');
- const deselectAllBtn = document.getElementById('deselect-all-variables');
-
- if (closeBtn) {
- closeBtn.addEventListener('click', () => {
- this.hideVariableModal();
- });
- }
-
- if (cancelBtn) {
- cancelBtn.addEventListener('click', () => {
- this.hideVariableModal();
- });
- }
-
- if (confirmBtn) {
- confirmBtn.addEventListener('click', () => {
- this.confirmVariableSelection();
- });
- }
-
- if (selectAllBtn) {
- selectAllBtn.addEventListener('click', () => {
- this.selectAllVisibleVariables();
- });
- }
-
- if (deselectAllBtn) {
- deselectAllBtn.addEventListener('click', () => {
- this.deselectAllVisibleVariables();
- });
- }
-
- // Prevenir cierre con teclas como Ctrl+V, Escape (opcional)
- if (modal) {
- modal.addEventListener('keydown', (e) => {
- // Prevenir cierre accidental con Ctrl+V y otras combinaciones
- if (e.ctrlKey || e.metaKey) {
- e.stopPropagation();
- }
-
- // Solo permitir Escape para cerrar si se presiona intencionalmente
- if (e.key === 'Escape' && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey) {
- this.hideVariableModal();
- }
- });
-
- // Prevenir cierre al hacer clic en el modal (solo en el backdrop)
- modal.addEventListener('click', (e) => {
- if (e.target === modal) {
- this.hideVariableModal();
- }
- });
- }
- }
-
- showPlotForm(sessionId = null) {
- console.log('showPlotForm called with sessionId:', sessionId);
-
- // Asegurar que estemos en el tab de plotting
- if (typeof tabManager !== 'undefined' && tabManager.getCurrentTab() !== 'plotting') {
- console.log('Switching to plotting tab');
- tabManager.switchTab('plotting');
- }
-
- const formContainer = document.getElementById('plot-form-container');
- const formTitle = document.getElementById('plot-form-title');
- const submitBtn = document.getElementById('plot-form-submit');
-
- // Verificar que los elementos existan
- if (!formContainer) {
- console.error('plot-form-container element not found');
- showNotification('Error: Form container not found', 'error');
- return;
- }
-
- if (!formTitle) {
- console.warn('plot-form-title element not found');
- }
-
- if (!submitBtn) {
- console.warn('plot-form-submit element not found');
- }
-
- this.currentEditingSession = sessionId;
-
- if (sessionId) {
- // Modo edición
- console.log('Setting up form for editing');
- if (formTitle) formTitle.textContent = '✏️ Edit Plot';
- if (submitBtn) submitBtn.textContent = 'Update Plot';
- this.loadPlotConfigForEdit(sessionId);
- } else {
- // Modo creación
- console.log('Setting up form for creation');
- if (formTitle) formTitle.textContent = '🆕 Create New Plot';
- if (submitBtn) submitBtn.textContent = 'Create Plot';
- this.resetPlotForm();
- }
-
- console.log('Making form visible');
- formContainer.style.display = 'block';
-
- // Scroll hacia el formulario para que sea visible
- setTimeout(() => {
- formContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
- console.log('Scrolled to form');
- }, 100);
-
- // Cargar variables disponibles para triggers
- this.loadTriggerVariables();
-
- console.log('showPlotForm completed');
- }
-
- hidePlotForm() {
- const formContainer = document.getElementById('plot-form-container');
- formContainer.style.display = 'none';
- this.currentEditingSession = null;
- this.selectedVariables.clear();
- this.updateSelectedVariablesDisplay();
- }
-
- resetPlotForm() {
- const form = document.getElementById('plot-form');
- form.reset();
- document.getElementById('plot-form-time-window').value = '60';
- document.getElementById('plot-form-trigger-on-true').checked = true;
- this.selectedVariables.clear();
- this.updateSelectedVariablesDisplay();
- }
-
- async loadPlotConfigForEdit(sessionId) {
- try {
- const response = await fetch(`/api/plots/${sessionId}/config`);
- const data = await response.json();
-
- if (data.success && data.config) {
- const config = data.config;
-
- // Llenar formulario
- document.getElementById('plot-form-name').value = config.name || '';
- document.getElementById('plot-form-time-window').value = config.time_window || 60;
- document.getElementById('plot-form-y-min').value = config.y_min || '';
- document.getElementById('plot-form-y-max').value = config.y_max || '';
- document.getElementById('plot-form-trigger-enabled').checked = config.trigger_enabled || false;
- document.getElementById('plot-form-trigger-variable').value = config.trigger_variable || '';
- document.getElementById('plot-form-trigger-on-true').checked = config.trigger_on_true !== false;
-
- // Configurar trigger
- this.togglePlotFormTriggerConfig();
-
- // Cargar variables seleccionadas
- this.selectedVariables.clear();
- if (config.variables) {
- for (let i = 0; i < config.variables.length; i++) {
- const variable = config.variables[i];
- const color = this.getColor(variable, i);
- this.selectedVariables.set(variable, {
- color: color,
- dataset: 'Unknown' // Will be resolved when loading datasets
- });
- }
- }
- this.updateSelectedVariablesDisplay();
- }
- } catch (error) {
- console.error('Error loading plot config for edit:', error);
- showNotification('Error loading plot configuration', 'error');
- }
- }
-
- async showVariableModal() {
- const modal = document.getElementById('variable-selection-modal');
-
- // Cargar datasets y variables
- await this.loadDatasetsAndVariables();
-
- modal.style.display = 'block';
- }
-
- hideVariableModal() {
- const modal = document.getElementById('variable-selection-modal');
- modal.style.display = 'none';
- }
-
- async loadDatasetsAndVariables() {
- try {
- // Cargar datasets
- const response = await fetch('/api/datasets');
- const data = await response.json();
-
- if (data.success) {
- this.availableDatasets = Object.entries(data.datasets).map(([id, dataset]) => ({
- id: id,
- name: dataset.name,
- variables: dataset.variables || {},
- active: data.active_datasets.includes(id)
- }));
-
- this.updateDatasetsDisplay();
- }
- } catch (error) {
- console.error('Error loading datasets:', error);
- showNotification('Error loading datasets', 'error');
- }
- }
-
- updateDatasetsDisplay() {
- const container = document.getElementById('datasets-list');
- container.innerHTML = '';
-
- if (this.availableDatasets.length === 0) {
- container.innerHTML = 'No datasets available
';
- return;
- }
-
- for (const dataset of this.availableDatasets) {
- const item = document.createElement('div');
- item.className = 'dataset-item';
- item.dataset.datasetId = dataset.id;
-
- const variableCount = Object.keys(dataset.variables).length;
- const statusIcon = dataset.active ? '🟢' : '⚫';
-
- item.innerHTML = `
- ${statusIcon} ${dataset.name}
- ${variableCount} variables
- `;
-
- item.addEventListener('click', () => {
- this.selectDataset(dataset.id);
- });
-
- container.appendChild(item);
- }
- }
-
- selectDataset(datasetId) {
- // Actualizar UI
- document.querySelectorAll('.dataset-item').forEach(item => {
- item.classList.remove('active');
- });
- document.querySelector(`[data-dataset-id="${datasetId}"]`).classList.add('active');
-
- // Mostrar variables del dataset
- const dataset = this.availableDatasets.find(d => d.id === datasetId);
- if (dataset) {
- this.updateVariablesDisplay(dataset);
- }
- }
-
- updateVariablesDisplay(dataset) {
- const container = document.getElementById('variables-list');
- container.innerHTML = '';
-
- const variables = Object.entries(dataset.variables);
-
- if (variables.length === 0) {
- container.innerHTML = 'No variables in this dataset
';
- return;
- }
-
- for (const [varName, varConfig] of variables) {
- const item = document.createElement('div');
- item.className = 'variable-item';
- item.dataset.variableName = varName;
-
- const isSelected = this.selectedVariables.has(varName);
- const selectedData = this.selectedVariables.get(varName);
- const color = selectedData?.color || this.getNextAvailableColor();
-
- if (isSelected) {
- item.classList.add('selected');
- }
-
- // Formatear detalles de la variable
- let details = `${varConfig.type.toUpperCase()}`;
- if (varConfig.area === 'db') {
- details += ` - DB${varConfig.db}.${varConfig.offset}`;
- } else if (varConfig.area === 'mw') {
- details += ` - MW${varConfig.offset}`;
- } else if (['e', 'a', 'mb'].includes(varConfig.area)) {
- details += ` - ${varConfig.area.toUpperCase()}${varConfig.offset}.${varConfig.bit || 0}`;
- }
-
- item.innerHTML = `
-
-
${varName}
-
${details}
-
-
-
-
-
- `;
-
- container.appendChild(item);
- }
- }
-
- toggleVariableSelection(variableName, datasetId, selected) {
- if (selected) {
- const color = this.getNextAvailableColor();
- const datasetName = this.availableDatasets.find(d => d.id === datasetId)?.name || datasetId;
-
- this.selectedVariables.set(variableName, {
- color: color,
- dataset: datasetName
- });
-
- // Habilitar selector de color
- const colorSelector = document.querySelector(`[data-variable-name="${variableName}"] .color-selector`);
- if (colorSelector) {
- colorSelector.disabled = false;
- colorSelector.value = color;
- }
-
- // Marcar item como seleccionado
- const item = document.querySelector(`[data-variable-name="${variableName}"]`);
- if (item) {
- item.classList.add('selected');
- }
- } else {
- this.selectedVariables.delete(variableName);
-
- // Deshabilitar selector de color
- const colorSelector = document.querySelector(`[data-variable-name="${variableName}"] .color-selector`);
- if (colorSelector) {
- colorSelector.disabled = true;
- }
-
- // Desmarcar item
- const item = document.querySelector(`[data-variable-name="${variableName}"]`);
- if (item) {
- item.classList.remove('selected');
- }
- }
-
- this.updateSelectedSummary();
- }
-
- updateVariableColor(variableName, color) {
- if (this.selectedVariables.has(variableName)) {
- const data = this.selectedVariables.get(variableName);
- data.color = color;
- this.selectedVariables.set(variableName, data);
- this.updateSelectedSummary();
- }
- }
-
- selectAllVisibleVariables() {
- const checkboxes = document.querySelectorAll('.variables-list .variable-checkbox');
- checkboxes.forEach(checkbox => {
- if (!checkbox.checked) {
- checkbox.checked = true;
- checkbox.onchange();
- }
- });
- }
-
- deselectAllVisibleVariables() {
- const checkboxes = document.querySelectorAll('.variables-list .variable-checkbox');
- checkboxes.forEach(checkbox => {
- if (checkbox.checked) {
- checkbox.checked = false;
- checkbox.onchange();
- }
- });
- }
-
- updateSelectedSummary() {
- const container = document.getElementById('selected-variables-summary');
- container.innerHTML = '';
-
- if (this.selectedVariables.size === 0) {
- container.innerHTML = 'No variables selected
';
- return;
- }
-
- for (const [varName, data] of this.selectedVariables) {
- const item = document.createElement('div');
- item.className = 'selected-summary-item';
- item.innerHTML = `
-
- ${varName}
- (${data.dataset})
- `;
- container.appendChild(item);
- }
- }
-
- confirmVariableSelection() {
- this.updateSelectedVariablesDisplay();
- this.hideVariableModal();
- }
-
- updateSelectedVariablesDisplay() {
- const container = document.getElementById('selected-variables-display');
- container.innerHTML = '';
-
- if (this.selectedVariables.size === 0) {
- container.innerHTML = 'No variables selected
';
- return;
- }
-
- for (const [varName, data] of this.selectedVariables) {
- const chip = document.createElement('div');
- chip.className = 'variable-chip';
- chip.innerHTML = `
-
- ${varName}
-
- `;
- container.appendChild(chip);
- }
- }
-
- removeSelectedVariable(variableName) {
- this.selectedVariables.delete(variableName);
- this.updateSelectedVariablesDisplay();
- }
-
- getNextAvailableColor() {
- const usedColors = new Set([...this.selectedVariables.values()].map(v => v.color));
- return this.colors.find(color => !usedColors.has(color)) || this.colors[0];
- }
-
- getColor(variable, index = null) {
- if (index !== null) {
- return this.colors[index % this.colors.length];
- }
- const hash = this.hashCode(variable);
- return this.colors[hash % this.colors.length];
- }
-
- hashCode(str) {
- let hash = 0;
- for (let i = 0; i < str.length; i++) {
- const char = str.charCodeAt(i);
- hash = ((hash << 5) - hash) + char;
- hash = hash & hash;
- }
- return Math.abs(hash);
- }
-
- async loadTriggerVariables() {
- try {
- const response = await fetch('/api/plots/variables');
- const data = await response.json();
-
- const triggerSelect = document.getElementById('plot-form-trigger-variable');
- triggerSelect.innerHTML = '';
-
- if (data.boolean_variables) {
- for (const variable of data.boolean_variables) {
- const option = document.createElement('option');
- option.value = variable;
- option.textContent = variable;
- triggerSelect.appendChild(option);
- }
- }
- } catch (error) {
- console.error('Error loading trigger variables:', error);
- }
- }
-
- async handleFormSubmit() {
- if (this.selectedVariables.size === 0) {
- showNotification('Please select at least one variable', 'error');
- return;
- }
-
- const config = {
- name: document.getElementById('plot-form-name').value || `Plot ${Date.now()}`,
- variables: Array.from(this.selectedVariables.keys()),
- time_window: parseInt(document.getElementById('plot-form-time-window').value) || 60,
- y_min: document.getElementById('plot-form-y-min').value || null,
- y_max: document.getElementById('plot-form-y-max').value || null,
- trigger_enabled: document.getElementById('plot-form-trigger-enabled').checked,
- trigger_variable: document.getElementById('plot-form-trigger-variable').value || null,
- trigger_on_true: document.getElementById('plot-form-trigger-on-true').checked
- };
-
- // Convertir valores numéricos
- if (config.y_min) config.y_min = parseFloat(config.y_min);
- if (config.y_max) config.y_max = parseFloat(config.y_max);
-
- // 🔧 DEBUG: Log configuración del plot
- plotDebugLog('📈 Creating plot with config:', config);
-
- try {
- let response;
- let sessionId = null;
-
- if (this.currentEditingSession) {
- // Modo edición: ELIMINAR el plot existente y crear uno nuevo desde cero
- console.log(`Deleting existing plot ${this.currentEditingSession} to recreate it from scratch`);
-
- // 1. Eliminar el plot existente
- const deleteResponse = await fetch(`/api/plots/${this.currentEditingSession}`, {
- method: 'DELETE'
- });
-
- if (!deleteResponse.ok) {
- throw new Error('Failed to delete existing plot');
- }
-
- // 2. Remover de la UI
- if (typeof tabManager !== 'undefined') {
- tabManager.removePlotTab(this.currentEditingSession);
- }
-
- // 3. Remover del PlotManager
- if (this.sessions.has(this.currentEditingSession)) {
- const chart = this.sessions.get(this.currentEditingSession);
- if (chart) {
- chart.destroy();
- }
- this.sessions.delete(this.currentEditingSession);
- }
-
- // 4. Crear nuevo plot desde cero
- response = await fetch('/api/plots', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(config)
- });
-
- const deleteResult = await deleteResponse.json();
- console.log(`Old plot deleted: ${deleteResult.message || 'Success'}`);
- } else {
- // Modo creación normal
- response = await fetch('/api/plots', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(config)
- });
- }
-
- const result = await response.json();
-
- if (result.success) {
- sessionId = result.session_id;
-
- if (this.currentEditingSession) {
- showNotification(`Plot recreated successfully: ${config.name}`, 'success');
- } else {
- showNotification(result.message, 'success');
- }
-
- // Crear nuevo tab para la sesión (tanto en modo edición como creación)
- await this.createPlotSessionTab(sessionId, {
- name: config.name,
- variables_count: config.variables.length,
- trigger_enabled: config.trigger_enabled,
- trigger_variable: config.trigger_variable,
- trigger_on_true: config.trigger_on_true,
- is_active: false,
- is_paused: false
- });
-
- // Cambiar al sub-tab del nuevo plot
- if (typeof tabManager !== 'undefined') {
- tabManager.switchSubTab(`plot-${sessionId}`);
- }
-
- this.hidePlotForm();
- } else {
- showNotification(result.error, 'error');
- }
- } catch (error) {
- console.error('Error submitting plot form:', error);
- if (this.currentEditingSession) {
- showNotification('Error recreating plot. Please try again.', 'error');
- } else {
- showNotification('Error creating plot', 'error');
- }
- }
- }
-
- destroy() {
- this.stopAutoUpdate();
-
- // Destruir todos los charts
- for (const [sessionId, sessionData] of this.sessions) {
- if (sessionData && sessionData.chart) {
- sessionData.chart.destroy();
- }
- }
- this.sessions.clear();
-
- console.log('📈 Plot Manager destroyed');
- }
-}
-
-// Función global para toggle de trigger config
-window.togglePlotFormTriggerConfig = function () {
- const triggerEnabled = document.getElementById('plot-form-trigger-enabled');
- const triggerConfig = document.getElementById('plot-form-trigger-config');
-
- if (triggerConfig) {
- triggerConfig.style.display = triggerEnabled.checked ? 'block' : 'none';
- }
-}
-
-// Función global corregida para editar plots
-window.editPlotSession = function (sessionId) {
- console.log('editPlotSession called with sessionId:', sessionId);
- console.log('plotManager available:', !!plotManager);
- console.log('tabManager available:', typeof tabManager !== 'undefined');
-
- if (!sessionId || sessionId.trim() === '') {
- console.error('Invalid or empty session ID provided to editPlotSession');
- showNotification('Error: Invalid session ID', 'error');
- return;
- }
-
- if (!plotManager) {
- console.error('Plot manager not initialized');
- showNotification('Error: Plot manager not available', 'error');
- return;
- }
-
- console.log('Opening plot form for editing session:', sessionId);
- console.log('Current tab:', typeof tabManager !== 'undefined' ? tabManager.getCurrentTab() : 'tabManager not available');
-
- plotManager.showPlotForm(sessionId);
-
- console.log('Plot form should now be visible');
-}
-
-// Función global para remover sesiones de plot
-window.removePlotSession = async function (sessionId) {
- if (confirm('¿Estás seguro de que quieres eliminar este plot?')) {
- try {
- const response = await fetch(`/api/plots/${sessionId}`, {
- method: 'DELETE'
- });
-
- const result = await response.json();
-
- if (result.success) {
- // Remover tab dinámico
- if (typeof tabManager !== 'undefined') {
- tabManager.removePlotTab(sessionId);
- }
-
- // Remover de PlotManager
- if (plotManager && plotManager.sessions.has(sessionId)) {
- plotManager.sessions.delete(sessionId);
- }
-
- showNotification(result.message, 'success');
- } else {
- showNotification(result.error, 'error');
- }
- } catch (error) {
- console.error('Error removing plot:', error);
- showNotification('Error removing plot session', 'error');
- }
- }
-}
-
-// Función de utilidad para notificaciones
-window.showNotification = function (message, type = 'info') {
- console.log(`${type.toUpperCase()}: ${message}`);
-
- // Si tienes un sistema de notificaciones, úsalo aquí
- if (typeof showAlert === 'function') {
- showAlert(message, type);
- } else if (typeof showMessage === 'function') {
- showMessage(message, type);
- }
-}
-
-// Inicialización
-let plotManager = null;
-
-document.addEventListener('DOMContentLoaded', function () {
- // Inicializar Plot Manager
- plotManager = new PlotManager();
-
- // Cerrar modales con Escape (pero prevenir cierre accidental)
- document.addEventListener('keydown', function (e) {
- if (e.key === 'Escape' && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey) {
- // Cerrar formulario colapsable si está abierto
- const formContainer = document.getElementById('plot-form-container');
- if (formContainer && formContainer.style.display !== 'none') {
- plotManager.hidePlotForm();
- }
- }
- });
-});
\ No newline at end of file
diff --git a/web_test/PLC S7-315 Streamer & Logger_files/status.js.descargar b/web_test/PLC S7-315 Streamer & Logger_files/status.js.descargar
deleted file mode 100644
index e214d23..0000000
--- a/web_test/PLC S7-315 Streamer & Logger_files/status.js.descargar
+++ /dev/null
@@ -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 ';
- 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 ';
- 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 ';
- 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 ';
- 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
- ⏱️ ~${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 ';
- 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 ';
- 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 ';
- 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 ';
- 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
- ⏱️ ~${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';
- }
-}
\ No newline at end of file
diff --git a/web_test/PLC S7-315 Streamer & Logger_files/streaming.js.descargar b/web_test/PLC S7-315 Streamer & Logger_files/streaming.js.descargar
deleted file mode 100644
index 40373be..0000000
--- a/web_test/PLC S7-315 Streamer & Logger_files/streaming.js.descargar
+++ /dev/null
@@ -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));
-}
\ No newline at end of file
diff --git a/web_test/PLC S7-315 Streamer & Logger_files/styles.css b/web_test/PLC S7-315 Streamer & Logger_files/styles.css
deleted file mode 100644
index d0ed8fc..0000000
--- a/web_test/PLC S7-315 Streamer & Logger_files/styles.css
+++ /dev/null
@@ -1,1281 +0,0 @@
-/* Header with logo */
-.header {
- text-align: center;
- margin-bottom: 2rem;
-}
-
-.header h1 {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 1rem;
- flex-wrap: wrap;
-}
-
-.header-logo {
- height: 1.2em;
- width: auto;
- vertical-align: middle;
-}
-
-/* Status grid */
-.status-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
- gap: 1rem;
- margin-bottom: 2rem;
-}
-
-.status-item {
- padding: 1rem;
- border-radius: var(--pico-border-radius);
- text-align: center;
- font-weight: bold;
- background: var(--pico-card-background-color);
- border: var(--pico-border-width) solid var(--pico-border-color);
-}
-
-.status-connected {
- background: var(--pico-primary-background);
- color: var(--pico-primary-inverse);
-}
-
-.status-disconnected {
- background: var(--pico-secondary-background);
- color: var(--pico-secondary-inverse);
-}
-
-.status-streaming {
- background: var(--pico-primary-background);
- color: var(--pico-primary-inverse);
-}
-
-.status-idle {
- background: var(--pico-muted-background-color);
- color: var(--pico-muted-color);
-}
-
-/* Configuration grid */
-.config-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
- gap: 1.5rem;
- margin-bottom: 1.5rem;
-}
-
-/* Form styling */
-.form-row {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
- gap: 1rem;
-}
-
-.controls {
- display: flex;
- flex-wrap: wrap;
- gap: 0.5rem;
- margin-top: 1rem;
-}
-
-/* Variables table */
-.variables-table {
- width: 100%;
- margin-top: 1rem;
-}
-
-/* Variable value cells styling */
-.variables-table td[id^="value-"] {
- font-family: var(--pico-font-family-monospace);
- font-weight: bold;
- text-align: center;
- min-width: 100px;
-}
-
-/* Refresh button styling */
-#refresh-values-btn:disabled {
- opacity: 0.6;
- cursor: not-allowed;
-}
-
-/* Diagnose button styling */
-#diagnose-btn:disabled {
- opacity: 0.6;
- cursor: not-allowed;
-}
-
-/* Last refresh time styling */
-#last-refresh-time {
- font-style: italic;
- font-size: 0.85rem;
-}
-
-/* Error cell tooltips */
-.variables-table td[id^="value-"]:hover {
- position: relative;
-}
-
-/* Variable value status colors */
-.value-success {
- color: var(--pico-color-green-600) !important;
-}
-
-.value-error {
- color: var(--pico-color-red-500) !important;
-}
-
-.value-warning {
- color: var(--pico-color-amber-600) !important;
-}
-
-.value-offline {
- color: var(--pico-muted-color) !important;
-}
-
-/* Alert styles */
-.alert {
- padding: 1rem;
- border-radius: var(--pico-border-radius);
- margin: 1rem 0;
- font-weight: bold;
-}
-
-.alert-success {
- background-color: var(--pico-primary-background);
- color: var(--pico-primary-inverse);
-}
-
-.alert-error {
- background-color: var(--pico-color-red-400);
- color: white;
-}
-
-.alert-warning {
- background-color: var(--pico-color-amber-100);
- color: var(--pico-color-amber-800);
- border: 1px solid var(--pico-color-amber-300);
-}
-
-.alert-info {
- background-color: var(--pico-color-blue-100);
- color: var(--pico-color-blue-800);
- border: 1px solid var(--pico-color-blue-300);
-}
-
-/* Dataset controls */
-.dataset-controls {
- background: var(--pico-card-background-color);
- border: var(--pico-border-width) solid var(--pico-border-color);
- padding: 1.5rem;
- border-radius: var(--pico-border-radius);
- margin-bottom: 1.5rem;
-}
-
-/* Info section */
-.info-section {
- background: var(--pico-muted-background-color);
- border: var(--pico-border-width) solid var(--pico-muted-border-color);
- border-radius: var(--pico-border-radius);
- padding: 1.5rem;
- margin-bottom: 1.5rem;
-}
-
-.info-section p {
- margin: 0.5rem 0;
-}
-
-/* Log container */
-.log-container {
- max-height: 400px;
- overflow-y: auto;
- background: var(--pico-background-color);
- border: var(--pico-border-width) solid var(--pico-border-color);
- border-radius: var(--pico-border-radius);
- padding: 1rem;
- font-family: var(--pico-font-family-monospace);
-}
-
-.log-entry {
- display: flex;
- flex-direction: column;
- margin-bottom: 1rem;
- padding: 0.75rem;
- border-radius: var(--pico-border-radius);
- font-size: 0.875rem;
- border-left: 3px solid transparent;
-}
-
-.log-entry.log-info {
- background: var(--pico-card-background-color);
- border-left-color: var(--pico-primary);
-}
-
-.log-entry.log-warning {
- background: var(--pico-color-amber-50);
- border-left-color: var(--pico-color-amber-500);
-}
-
-.log-entry.log-error {
- background: var(--pico-color-red-50);
- border-left-color: var(--pico-color-red-500);
-}
-
-.log-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- font-weight: bold;
- margin-bottom: 0.25rem;
-}
-
-.log-timestamp {
- font-size: 0.75rem;
- opacity: 0.7;
-}
-
-.log-message {
- margin-bottom: 0.25rem;
- word-wrap: break-word;
-}
-
-.log-details {
- font-size: 0.75rem;
- opacity: 0.8;
- background: rgba(0, 0, 0, 0.1);
- padding: 0.5rem;
- border-radius: var(--pico-border-radius);
- margin-top: 0.25rem;
- white-space: pre-wrap;
-}
-
-.log-controls {
- display: flex;
- gap: 1rem;
- margin-bottom: 1rem;
- flex-wrap: wrap;
- align-items: center;
-}
-
-.log-stats {
- background: var(--pico-muted-background-color);
- border: var(--pico-border-width) solid var(--pico-muted-border-color);
- border-radius: var(--pico-border-radius);
- padding: 0.5rem 1rem;
- font-size: 0.875rem;
-}
-
-/* Utility classes */
-.status-active {
- color: var(--pico-primary);
- font-weight: bold;
-}
-
-.status-inactive {
- color: var(--pico-muted-color);
-}
-
-.status-error {
- color: var(--pico-color-red-500);
-}
-
-/* Modal improvements */
-.modal {
- position: fixed;
- z-index: 1000;
- left: 0;
- top: 0;
- width: 100%;
- height: 100%;
- background-color: rgba(0, 0, 0, 0.5);
- backdrop-filter: blur(5px);
-}
-
-.modal article {
- margin: 5% auto;
- width: 90%;
- max-width: 600px;
-}
-
-.modal header {
- display: flex;
- justify-content: space-between;
- align-items: center;
-}
-
-.close {
- font-size: 1.5rem;
- font-weight: bold;
- cursor: pointer;
- border: none;
- background: none;
- color: inherit;
-}
-
-.close:hover {
- opacity: 0.7;
-}
-
-/* Mobile responsive */
-@media (max-width: 768px) {
- .header h1 {
- flex-direction: column;
- gap: 0.5rem;
- }
-
- .header-logo {
- height: 1.2em;
- }
-
- .config-grid {
- grid-template-columns: 1fr;
- }
-
- .form-row {
- grid-template-columns: 1fr;
- }
-
- .controls {
- flex-direction: column;
- }
-
- .status-grid {
- grid-template-columns: 1fr;
- }
-}
-
-/* Theme selector */
-.theme-selector {
- position: fixed;
- top: 1rem;
- right: 1rem;
- z-index: 1000;
- background: var(--pico-card-background-color);
- border: var(--pico-border-width) solid var(--pico-border-color);
- border-radius: var(--pico-border-radius);
- padding: 0.5rem;
- display: flex;
- gap: 0.5rem;
- align-items: center;
- box-shadow: var(--pico-box-shadow);
-}
-
-.theme-selector button {
- padding: 0.25rem 0.5rem;
- font-size: 0.75rem;
- border-radius: var(--pico-border-radius);
- cursor: pointer;
- transition: all 0.2s ease;
-}
-
-.theme-selector button.active {
- background: var(--pico-primary-background);
- color: var(--pico-primary-inverse);
-}
-
-.theme-selector button:not(.active) {
- background: var(--pico-muted-background-color);
- color: var(--pico-muted-color);
-}
-
-.theme-selector button:hover:not(.active) {
- background: var(--pico-primary-hover);
- color: var(--pico-primary-inverse);
-}
-
-/* Font size reduction - 15% smaller (more balanced) */
-html {
- font-size: 85%;
- /* 15% reduction from 100% - more balanced */
-}
-
-/* Adjust specific elements that might need fine-tuning */
-.header h1 {
- font-size: 2.2rem;
- /* Adjusted for smaller base font */
-}
-
-.header p {
- font-size: 1.1rem;
-}
-
-.status-item {
- font-size: 1rem;
-}
-
-.log-entry {
- font-size: 0.9rem;
- /* Adjusted for smaller base font */
-}
-
-.log-timestamp {
- font-size: 0.8rem;
- /* Adjusted for smaller base font */
-}
-
-.log-details {
- font-size: 0.8rem;
- /* Adjusted for smaller base font */
-}
-
-.log-stats {
- font-size: 0.9rem;
- /* Adjusted for smaller base font */
-}
-
-/* Ensure buttons and inputs remain readable */
-button,
-input,
-select,
-textarea {
- font-size: 1rem;
-}
-
-/* Table adjustments */
-.variables-table th,
-.variables-table td {
- font-size: 0.95rem;
-}
-
-/* Modal adjustments */
-.modal article {
- font-size: 1rem;
-}
-
-.modal h3 {
- font-size: 1.4rem;
-}
-
-/* CSV Configuration styles */
-.csv-config-display {
- background: var(--pico-card-background-color);
- border: var(--pico-border-width) solid var(--pico-border-color);
- border-radius: var(--pico-border-radius);
- padding: 1.5rem;
- margin-bottom: 1rem;
-}
-
-.config-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
- gap: 1rem;
-}
-
-.config-item {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 0.5rem 0;
- border-bottom: 1px solid var(--pico-muted-border-color);
-}
-
-.config-item:last-child {
- border-bottom: none;
-}
-
-.config-item span {
- font-weight: normal;
- color: var(--pico-primary);
-}
-
-.directory-stats {
- padding: 1rem;
- background: var(--pico-muted-background-color);
- border-radius: var(--pico-border-radius);
- margin-top: 0.5rem;
-}
-
-.directory-stats .stat-item {
- display: flex;
- justify-content: space-between;
- margin: 0.5rem 0;
-}
-
-.day-folder-item {
- background: var(--pico-card-background-color);
- border: 1px solid var(--pico-border-color);
- border-radius: var(--pico-border-radius);
- padding: 0.75rem;
- margin: 0.5rem 0;
- display: flex;
- justify-content: space-between;
- align-items: center;
-}
-
-.cleanup-status {
- padding: 0.5rem;
- border-radius: var(--pico-border-radius);
- margin: 0.5rem 0;
- font-weight: bold;
-}
-
-.cleanup-success {
- background-color: var(--pico-color-green-100);
- color: var(--pico-color-green-800);
- border: 1px solid var(--pico-color-green-300);
-}
-
-.cleanup-error {
- background-color: var(--pico-color-red-100);
- color: var(--pico-color-red-800);
- border: 1px solid var(--pico-color-red-300);
-}
-
-/* 📈 PLOT SYSTEM STYLES */
-.plot-session {
- background: var(--pico-card-background-color);
- border: var(--pico-border-width) solid var(--pico-border-color);
- border-radius: var(--pico-border-radius);
- margin-bottom: 1rem;
- overflow: hidden;
-}
-
-.plot-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 0.75rem 1rem;
- background: var(--pico-muted-background-color);
- border-bottom: var(--pico-border-width) solid var(--pico-border-color);
-}
-
-.plot-header h4 {
- margin: 0;
- color: var(--pico-h4-color);
- font-size: 1.1rem;
-}
-
-.plot-controls {
- display: flex;
- gap: 0.5rem;
- flex-wrap: wrap;
-}
-
-.plot-controls .btn {
- padding: 0.25rem 0.5rem;
- font-size: 0.8rem;
- min-width: auto;
-}
-
-.plot-info {
- padding: 0.5rem 1rem;
- background: var(--pico-card-background-color);
- border-bottom: var(--pico-border-width) solid var(--pico-border-color);
- font-size: 0.85rem;
- color: var(--pico-muted-color);
-}
-
-.plot-stats {
- display: flex;
- gap: 1rem;
- flex-wrap: wrap;
-}
-
-.plot-canvas {
- padding: 1rem;
- height: 300px;
- position: relative;
-}
-
-.plot-canvas canvas {
- max-height: 100%;
-}
-
-/* Modal de creación de plots */
-.modal {
- display: none;
- position: fixed;
- z-index: 1000;
- left: 0;
- top: 0;
- width: 100%;
- height: 100%;
- background-color: rgba(0, 0, 0, 0.5);
-}
-
-.modal-content {
- background-color: var(--pico-card-background-color);
- margin: 5% auto;
- padding: 2rem;
- border: var(--pico-border-width) solid var(--pico-border-color);
- border-radius: var(--pico-border-radius);
- width: 90%;
- max-width: 600px;
- max-height: 80vh;
- overflow-y: auto;
-}
-
-.modal-content h4 {
- margin-top: 0;
- color: var(--pico-h4-color);
-}
-
-.form-group {
- margin-bottom: 1rem;
-}
-
-.form-group label {
- display: block;
- margin-bottom: 0.5rem;
- font-weight: bold;
- color: var(--pico-form-element-label-color);
-}
-
-.form-group input,
-.form-group select {
- width: 100%;
- padding: 0.5rem;
- border: var(--pico-border-width) solid var(--pico-border-color);
- border-radius: var(--pico-border-radius);
- background: var(--pico-form-element-background-color);
- color: var(--pico-form-element-color);
-}
-
-.form-group input:focus,
-.form-group select:focus {
- border-color: var(--pico-primary);
- outline: none;
-}
-
-.variable-checkbox {
- margin-bottom: 0.5rem;
-}
-
-.variable-checkbox label {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- font-weight: normal;
- cursor: pointer;
-}
-
-.variable-checkbox input[type="checkbox"] {
- width: auto;
- margin: 0;
-}
-
-.range-inputs {
- display: flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.range-inputs input {
- flex: 1;
-}
-
-.range-inputs span {
- color: var(--pico-muted-color);
- font-weight: bold;
-}
-
-.form-actions {
- display: flex;
- gap: 1rem;
- justify-content: flex-end;
- margin-top: 2rem;
- padding-top: 1rem;
- border-top: var(--pico-border-width) solid var(--pico-border-color);
-}
-
-.form-actions .btn {
- min-width: 100px;
-}
-
-#trigger-config {
- padding: 1rem;
- background: var(--pico-muted-background-color);
- border: var(--pico-border-width) solid var(--pico-border-color);
- border-radius: var(--pico-border-radius);
- margin-top: 0.5rem;
-}
-
-#trigger-config .form-group {
- margin-bottom: 0.5rem;
-}
-
-#trigger-config .form-group:last-child {
- margin-bottom: 0;
-}
-
-/* Responsive design para plots */
-@media (max-width: 768px) {
- .plot-header {
- flex-direction: column;
- align-items: flex-start;
- gap: 0.5rem;
- }
-
- .plot-controls {
- width: 100%;
- justify-content: flex-start;
- }
-
- .plot-stats {
- flex-direction: column;
- gap: 0.25rem;
- }
-
- .modal-content {
- margin: 10% auto;
- width: 95%;
- padding: 1rem;
- }
-
- .form-actions {
- flex-direction: column;
- }
-
- .range-inputs {
- flex-direction: column;
- align-items: stretch;
- }
-}
-
-/* TAB SYSTEM STYLES */
-.tabs {
- display: flex;
- border-bottom: var(--pico-border-width) solid var(--pico-border-color);
- margin-bottom: 1.5rem;
- background: var(--pico-card-background-color);
- border-radius: var(--pico-border-radius) var(--pico-border-radius) 0 0;
- overflow-x: auto;
-}
-
-.tab-btn {
- padding: 1rem 1.5rem;
- border: none;
- background: none;
- color: var(--pico-muted-color);
- cursor: pointer;
- font-weight: 500;
- border-bottom: 3px solid transparent;
- transition: all 0.2s ease;
- white-space: nowrap;
- flex: 1;
- text-align: center;
- min-width: 0;
- max-width: none;
-}
-
-.tab-btn:hover {
- color: var(--pico-primary);
- background: var(--pico-muted-background-color);
-}
-
-.tab-btn.active {
- color: var(--pico-primary);
- border-bottom-color: var(--pico-primary);
- background: var(--pico-card-background-color);
-}
-
-/* Plot tabs específicos */
-.tab-btn.plot-tab {
- position: relative;
- padding-right: 2.5rem;
-}
-
-.tab-close {
- position: absolute;
- right: 0.5rem;
- top: 50%;
- transform: translateY(-50%);
- background: none;
- border: none;
- color: var(--pico-muted-color);
- cursor: pointer;
- font-size: 1.2rem;
- line-height: 1;
- padding: 0.25rem;
- border-radius: 50%;
- transition: all 0.2s ease;
-}
-
-.tab-close:hover {
- background: var(--pico-muted-background-color);
- color: var(--pico-color-red-500);
-}
-
-/* SUB-TABS STYLES */
-.sub-tabs {
- display: flex;
- border-bottom: var(--pico-border-width) solid var(--pico-border-color);
- margin-bottom: 1rem;
- background: var(--pico-muted-background-color);
- border-radius: var(--pico-border-radius);
- overflow-x: auto;
-}
-
-.sub-tab-btn {
- padding: 0.75rem 1rem;
- border: none;
- background: none;
- color: var(--pico-muted-color);
- cursor: pointer;
- font-weight: 500;
- border-bottom: 2px solid transparent;
- transition: all 0.2s ease;
- white-space: nowrap;
- flex: 1;
- text-align: center;
- min-width: 0;
- font-size: 0.9rem;
-}
-
-.sub-tab-btn:hover {
- color: var(--pico-primary);
- background: var(--pico-card-background-color);
-}
-
-.sub-tab-btn.active {
- color: var(--pico-primary);
- border-bottom-color: var(--pico-primary);
- background: var(--pico-card-background-color);
-}
-
-.sub-tab-btn.plot-sub-tab {
- position: relative;
- padding-right: 2rem;
-}
-
-.sub-tab-close {
- position: absolute;
- right: 0.25rem;
- top: 50%;
- transform: translateY(-50%);
- background: none;
- border: none;
- color: var(--pico-muted-color);
- cursor: pointer;
- font-size: 1rem;
- line-height: 1;
- padding: 0.2rem;
- border-radius: 50%;
- transition: all 0.2s ease;
-}
-
-.sub-tab-close:hover {
- background: var(--pico-muted-background-color);
- color: var(--pico-color-red-500);
-}
-
-.sub-tab-content {
- display: none;
-}
-
-.sub-tab-content.active {
- display: block;
-}
-
-.tab-content {
- display: none;
-}
-
-.tab-content.active {
- display: block;
-}
-
-/* COLLAPSIBLE PLOT FORM STYLES */
-.collapsible-section {
- margin-bottom: 1.5rem;
- border: var(--pico-border-width) solid var(--pico-border-color);
- border-radius: var(--pico-border-radius);
- overflow: hidden;
- transition: all 0.3s ease;
-}
-
-.plot-form-article {
- margin: 0;
- background: var(--pico-card-background-color);
-}
-
-.plot-form-article header {
- background: var(--pico-primary-background);
- color: var(--pico-primary-inverse);
- padding: 1rem 1.5rem;
- margin: 0;
-}
-
-.close-btn {
- background: none;
- border: none;
- color: inherit;
- font-size: 1.5rem;
- cursor: pointer;
- padding: 0.25rem;
- border-radius: 50%;
- transition: all 0.2s ease;
-}
-
-.close-btn:hover {
- background: rgba(255, 255, 255, 0.2);
-}
-
-.variables-selection {
- border: var(--pico-border-width) solid var(--pico-border-color);
- border-radius: var(--pico-border-radius);
- padding: 1rem;
- background: var(--pico-muted-background-color);
-}
-
-.selected-variables {
- margin-bottom: 1rem;
- min-height: 2rem;
- padding: 0.5rem;
- border: var(--pico-border-width) solid var(--pico-border-color);
- border-radius: var(--pico-border-radius);
- background: var(--pico-card-background-color);
-}
-
-.selected-variables .no-variables {
- color: var(--pico-muted-color);
- font-style: italic;
- margin: 0;
-}
-
-.variable-chip {
- display: inline-flex;
- align-items: center;
- gap: 0.5rem;
- padding: 0.25rem 0.75rem;
- margin: 0.25rem;
- background: var(--pico-primary-background);
- color: var(--pico-primary-inverse);
- border-radius: var(--pico-border-radius);
- font-size: 0.875rem;
- font-weight: 500;
-}
-
-.variable-chip .color-indicator {
- width: 12px;
- height: 12px;
- border-radius: 50%;
- border: 1px solid rgba(255, 255, 255, 0.3);
-}
-
-.variable-chip .remove-variable {
- background: none;
- border: none;
- color: inherit;
- cursor: pointer;
- font-size: 1.1rem;
- padding: 0;
- margin-left: 0.25rem;
- opacity: 0.8;
- transition: opacity 0.2s ease;
-}
-
-.variable-chip .remove-variable:hover {
- opacity: 1;
-}
-
-/* VARIABLE SELECTION MODAL STYLES */
-.variable-modal {
- max-width: 900px;
- width: 95%;
- max-height: 85vh;
- display: flex;
- flex-direction: column;
-}
-
-.modal-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 1.5rem;
- background: var(--pico-primary-background);
- color: var(--pico-primary-inverse);
- margin: 0;
- border-radius: var(--pico-border-radius) var(--pico-border-radius) 0 0;
-}
-
-.modal-header h3 {
- margin: 0;
- color: inherit;
-}
-
-.modal-body {
- flex: 1;
- padding: 1.5rem;
- overflow-y: auto;
-}
-
-.modal-footer {
- padding: 1rem 1.5rem;
- background: var(--pico-muted-background-color);
- border-top: var(--pico-border-width) solid var(--pico-border-color);
- display: flex;
- justify-content: flex-end;
- gap: 1rem;
-}
-
-.variable-selection-container {
- display: grid;
- grid-template-columns: 200px 1fr;
- gap: 1.5rem;
- margin-bottom: 1.5rem;
-}
-
-.datasets-sidebar {
- border: var(--pico-border-width) solid var(--pico-border-color);
- border-radius: var(--pico-border-radius);
- padding: 1rem;
- background: var(--pico-muted-background-color);
- height: fit-content;
-}
-
-.datasets-sidebar h4 {
- margin: 0 0 1rem 0;
- color: var(--pico-h4-color);
- font-size: 1rem;
-}
-
-.datasets-list {
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
-}
-
-.dataset-item {
- padding: 0.75rem;
- border: var(--pico-border-width) solid var(--pico-border-color);
- border-radius: var(--pico-border-radius);
- background: var(--pico-card-background-color);
- cursor: pointer;
- transition: all 0.2s ease;
- font-size: 0.875rem;
-}
-
-.dataset-item:hover {
- background: var(--pico-primary-hover);
- color: var(--pico-primary-inverse);
-}
-
-.dataset-item.active {
- background: var(--pico-primary-background);
- color: var(--pico-primary-inverse);
-}
-
-.dataset-item .dataset-name {
- font-weight: bold;
- display: block;
-}
-
-.dataset-item .dataset-info {
- font-size: 0.75rem;
- opacity: 0.8;
- margin-top: 0.25rem;
-}
-
-.variables-main {
- border: var(--pico-border-width) solid var(--pico-border-color);
- border-radius: var(--pico-border-radius);
- background: var(--pico-card-background-color);
-}
-
-.variables-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 1rem;
- background: var(--pico-muted-background-color);
- border-bottom: var(--pico-border-width) solid var(--pico-border-color);
-}
-
-.variables-header h4 {
- margin: 0;
- color: var(--pico-h4-color);
- font-size: 1rem;
-}
-
-.selection-controls {
- display: flex;
- gap: 0.5rem;
-}
-
-.selection-controls .btn {
- padding: 0.25rem 0.75rem;
- font-size: 0.75rem;
-}
-
-.variables-list {
- padding: 1rem;
- max-height: 300px;
- overflow-y: auto;
-}
-
-.variable-item {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 0.75rem;
- margin-bottom: 0.5rem;
- border: var(--pico-border-width) solid var(--pico-border-color);
- border-radius: var(--pico-border-radius);
- background: var(--pico-muted-background-color);
- transition: all 0.2s ease;
-}
-
-.variable-item:last-child {
- margin-bottom: 0;
-}
-
-.variable-item.selected {
- background: var(--pico-primary-background);
- color: var(--pico-primary-inverse);
- border-color: var(--pico-primary);
-}
-
-.variable-info {
- flex: 1;
-}
-
-.variable-name {
- font-weight: bold;
- margin-bottom: 0.25rem;
-}
-
-.variable-details {
- font-size: 0.75rem;
- opacity: 0.8;
-}
-
-.variable-controls {
- display: flex;
- align-items: center;
- gap: 0.75rem;
-}
-
-.variable-checkbox {
- transform: scale(1.2);
-}
-
-.color-selector {
- width: 40px;
- height: 30px;
- border: none;
- border-radius: var(--pico-border-radius);
- cursor: pointer;
- transition: all 0.2s ease;
-}
-
-.color-selector:hover {
- transform: scale(1.1);
-}
-
-.color-selector:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- transform: none;
-}
-
-.selected-summary {
- border: var(--pico-border-width) solid var(--pico-border-color);
- border-radius: var(--pico-border-radius);
- padding: 1rem;
- background: var(--pico-muted-background-color);
-}
-
-.selected-summary h4 {
- margin: 0 0 1rem 0;
- color: var(--pico-h4-color);
- font-size: 1rem;
-}
-
-.selected-summary-list {
- display: flex;
- flex-wrap: wrap;
- gap: 0.5rem;
- min-height: 2rem;
-}
-
-.selected-summary-item {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- padding: 0.5rem 0.75rem;
- background: var(--pico-card-background-color);
- border: var(--pico-border-width) solid var(--pico-border-color);
- border-radius: var(--pico-border-radius);
- font-size: 0.875rem;
-}
-
-.selected-summary-item .color-indicator {
- width: 16px;
- height: 16px;
- border-radius: 50%;
- border: 1px solid var(--pico-border-color);
-}
-
-.no-dataset-message {
- text-align: center;
- color: var(--pico-muted-color);
- font-style: italic;
- padding: 2rem;
- margin: 0;
-}
-
-/* Responsive design para variable modal */
-@media (max-width: 768px) {
- .variable-modal {
- width: 98%;
- max-height: 90vh;
- }
-
- .variable-selection-container {
- grid-template-columns: 1fr;
- gap: 1rem;
- }
-
- .datasets-sidebar {
- max-height: 150px;
- overflow-y: auto;
- }
-
- .variables-header {
- flex-direction: column;
- align-items: flex-start;
- gap: 0.5rem;
- }
-
- .selection-controls {
- width: 100%;
- justify-content: flex-end;
- }
-
- .variable-item {
- flex-direction: column;
- align-items: flex-start;
- gap: 0.5rem;
- }
-
- .variable-controls {
- width: 100%;
- justify-content: flex-end;
- }
-
- .modal-footer {
- flex-direction: column;
- gap: 0.5rem;
- }
-}
-
-/* Responsive tabs */
-@media (max-width: 768px) {
- .tabs {
- flex-direction: column;
- border-bottom: none;
- border-right: var(--pico-border-width) solid var(--pico-border-color);
- border-radius: var(--pico-border-radius) 0 0 var(--pico-border-radius);
- margin-bottom: 1rem;
- }
-
- .tab-btn {
- border-bottom: none;
- border-right: 3px solid transparent;
- text-align: left;
- min-width: auto;
- }
-
- .tab-btn.active {
- border-right-color: var(--pico-primary);
- }
-}
\ No newline at end of file
diff --git a/web_test/PLC S7-315 Streamer & Logger_files/tabs.js.descargar b/web_test/PLC S7-315 Streamer & Logger_files/tabs.js.descargar
deleted file mode 100644
index d746059..0000000
--- a/web_test/PLC S7-315 Streamer & Logger_files/tabs.js.descargar
+++ /dev/null
@@ -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}
- ×
- `;
-
- // 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 = `
-
-
-
-
-
-
-
- Loading plot information...
-
-
-
-
-
-
-
- `;
-
- // 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}
- ×
- `;
-
- // 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();
-});
\ No newline at end of file
diff --git a/web_test/PLC S7-315 Streamer & Logger_files/theme.js.descargar b/web_test/PLC S7-315 Streamer & Logger_files/theme.js.descargar
deleted file mode 100644
index 3bb556c..0000000
--- a/web_test/PLC S7-315 Streamer & Logger_files/theme.js.descargar
+++ /dev/null
@@ -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();
-});
\ No newline at end of file
diff --git a/web_test/PLC S7-315 Streamer & Logger_files/utils.js.descargar b/web_test/PLC S7-315 Streamer & Logger_files/utils.js.descargar
deleted file mode 100644
index 8f9014a..0000000
--- a/web_test/PLC S7-315 Streamer & Logger_files/utils.js.descargar
+++ /dev/null
@@ -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 = `${message}
`;
- 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] || '📋';
-}
\ No newline at end of file
diff --git a/web_test/PLC S7-315 Streamer & Logger_files/variables.js.descargar b/web_test/PLC S7-315 Streamer & Logger_files/variables.js.descargar
deleted file mode 100644
index 66b99a3..0000000
--- a/web_test/PLC S7-315 Streamer & Logger_files/variables.js.descargar
+++ /dev/null
@@ -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 = `
- 🔄 Live streaming
-
- ⚠️ ${stats.success}/${stats.total} variables (${stats.failed} failed)
-
-
- ${sourceIcon} ${sourceText} • ${new Date(timestamp).toLocaleTimeString()}
-
- `;
- } else {
- lastRefreshTime.innerHTML = `
- 🔄 Live streaming
-
- ✅ All ${stats ? stats.success : 'N/A'} variables OK
-
-
- ${sourceIcon} ${sourceText} • ${new Date(timestamp).toLocaleTimeString()}
-
- `;
- }
- }
-}
-
-// 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';
- }
- }
-}
-
diff --git a/web_test/consola.txt b/web_test/consola.txt
deleted file mode 100644
index 4f5e6f1..0000000
--- a/web_test/consola.txt
+++ /dev/null
@@ -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}