diff --git a/.doc/MemoriaDeEvolucion.md b/.doc/MemoriaDeEvolucion.md index ba0cdeb..110f688 100644 --- a/.doc/MemoriaDeEvolucion.md +++ b/.doc/MemoriaDeEvolucion.md @@ -30,6 +30,51 @@ The application is designed to be persistent across restarts, restoring the prev ### Latest Modifications (Current Session) +#### Config Editor UI Compact Layout and Schema Enhancements +User prompt summary: Mejorar la interfaz del editor JSONForm para que los campos no ocupen toda la pantalla; configurar límites; definir nombres y descripciones de campos. + +Decisions: +- Aplicar layout compacto por CSS tanto para JSONForm como para el renderer fallback, en columnas auto-fit. +- Enriquecer los esquemas con `title`, `description`, límites (`minimum`, `maximum`, `minLength`, `maxLength`, `pattern`) y formatos donde aplique. + +Implementation: +- styles.css: añadido grid en `#jsonform-form` y `.config-editor-form` + `.object-group` para 2–3 columnas según ancho, con gaps compactos. +- plc.schema.json: títulos y descripciones para `ip`, `rack`, `slot`; `format: ipv4` para `ip`; `maximum` y `description` para `sampling_interval`. +- datasets.schema.json: títulos/descr. para `name`, `prefix` (con `pattern` y límites de longitud), límites para `offset` (máx 8191) y `db` (máx 9999), títulos para `area/type/bit/streaming`, títulos para propiedades raíz y de dataset; `sampling_interval` con `maximum`. +- plots.schema.json: títulos y descripciones para `name`, `variables`, `time_window`, `y_min`, `y_max`, y campos de trigger. + +Notes: +- JSON Schema estándar no define layout; el layout por columnas se resuelve en CSS/JSONForm form. El fallback ahora también se ve compacto sin depender de JSONForm. + +#### Config Editor UI: JSONForm as primary renderer (simple forms from JSON Schema) +User prompt summary: Prefer a simpler form-based editor for JSON content (avoid exposing too many controls); evaluate alternatives to JSONEditor; prefer JSONForm (jQuery + Bootstrap-like forms) over tree/code editors. + +Decisions: +- Keep backend `/api/config/*` unchanged; swap frontend renderer to JSONForm for a simpler UX. +- Load JSONForm via CDN with minimal deps (jQuery + Underscore). Avoid full Bootstrap migration; styling remains with Pico.css. +- Keep JSONEditor as fallback (tree/code) for advanced edits. + +Implementation: +- templates/index.html: added CDN includes for jQuery, Underscore, and JSONForm; kept JSONEditor include for fallback. +- static/js/config_editor.js: prefer JSONForm when available, generating forms directly from JSON Schema; wired Save to submit JSONForm and PUT values; Import rebuilds form with imported JSON; Export uses last known values or JSONEditor when active. + +Notes: +- JSONForm benefits: simple, guided forms; schema-driven validation; no React/bundler required. +- Licensing: MIT; OK for commercial/offline packaging. +- Handsontable not used due to licensing constraints; Tabulator remains an alternative for tabular arrays if needed. + + +#### Schema-based Config Editor & API +Decision: Añadir un editor dinámico basado en JSON Schema para gestionar `plc_config.json`, `plc_datasets.json` y `plot_sessions.json`, con importar/exportar. + +Implementation: +- Backend: nueva clase `ConfigSchemaManager` (`core/schema_manager.py`), endpoints `/api/config/*` para listar esquemas, leer/escribir y exportar. +- Esquemas: `schemas/plc.schema.json`, `schemas/datasets.schema.json`, `schemas/plots.schema.json`. +- Frontend: pestaña “Config Editor” y script `static/js/config_editor.js` que genera formularios desde schema (objects, arrays, enums, booleans con labels) e importar/exportar. +- Dependencias: `jsonschema` (validación opcional en backend). + +Notas UX: primera versión funcional. Siguiente paso acordado: evaluar librerías UI de JSON Schema para mejorar el diseño visual y la ergonomía del editor. + #### Real-Time Plotting System Implementation **Decision**: Implementar un sistema completo de plotting en tiempo real con trigger de variables boolean y uso exclusivo del cache del recording. diff --git a/application_events.json b/application_events.json index 47dc781..a731a81 100644 --- a/application_events.json +++ b/application_events.json @@ -9060,8 +9060,139 @@ "activated_datasets": 2, "total_datasets": 2 } + }, + { + "timestamp": "2025-08-10T01:23:18.751131", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-08-10T01:23:18.852237", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: DAR", + "details": { + "dataset_id": "DAR", + "variables_count": 3, + "streaming_count": 2, + "prefix": "gateway_phoenix" + } + }, + { + "timestamp": "2025-08-10T01:23:18.873364", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: Fast", + "details": { + "dataset_id": "Fast", + "variables_count": 2, + "streaming_count": 0, + "prefix": "fast" + } + }, + { + "timestamp": "2025-08-10T01:23:18.894713", + "level": "info", + "event_type": "csv_recording_started", + "message": "CSV recording started: 2 datasets activated", + "details": { + "activated_datasets": 2, + "total_datasets": 2 + } + }, + { + "timestamp": "2025-08-10T01:45:12.463570", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-08-10T01:45:12.539769", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: DAR", + "details": { + "dataset_id": "DAR", + "variables_count": 3, + "streaming_count": 2, + "prefix": "gateway_phoenix" + } + }, + { + "timestamp": "2025-08-10T01:45:12.552768", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: Fast", + "details": { + "dataset_id": "Fast", + "variables_count": 2, + "streaming_count": 0, + "prefix": "fast" + } + }, + { + "timestamp": "2025-08-10T01:45:12.562768", + "level": "info", + "event_type": "csv_recording_started", + "message": "CSV recording started: 2 datasets activated", + "details": { + "activated_datasets": 2, + "total_datasets": 2 + } + }, + { + "timestamp": "2025-08-11T00:48:08.669755", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-08-11T01:05:38.323746", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-08-11T11:04:13.582426", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-08-11T11:07:53.052382", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-08-11T11:12:42.001877", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-08-11T11:18:42.038963", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-08-11T11:23:34.891879", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} } ], - "last_updated": "2025-08-10T01:12:19.148987", - "total_entries": 838 + "last_updated": "2025-08-11T11:23:34.891879", + "total_entries": 853 } \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..a686afe --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,14 @@ + + + + + + PLC S7-31x Streamer & Logger - React + + + +
+ + + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..1632136 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,21 @@ +{ + "name": "plc-streamer-frontend", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview --host" + }, + "dependencies": { + "bootstrap": "^5.3.3", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.3.1", + "vite": "^5.4.0" + } +} + diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..b2e0ee9 --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,32 @@ +import React from 'react' + +function App() { + return ( +
+
+

+ SIDEL + PLC S7-31x Streamer & Logger (React) +

+

Base de React + Bootstrap lista. Empezaremos a migrar vistas.

+ Ir al modo legado +
+ +
+ Esta es una vista inicial de React. Probaremos el consumo de APIs y luego migraremos módulos + (Status, Datasets/Variables, Plotting, Events, Config Editor) de forma incremental. +
+ +
+

Acciones rápidas

+
+ Reload SPA + Ver /api/status +
+
+
+ ) +} + +export default App + diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx new file mode 100644 index 0000000..4de5902 --- /dev/null +++ b/frontend/src/main.jsx @@ -0,0 +1,11 @@ +import React from 'react' +import { createRoot } from 'react-dom/client' +import App from './App.jsx' +import 'bootstrap/dist/css/bootstrap.min.css' + +createRoot(document.getElementById('root')).render( + + + +) + diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..f450d6b --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,26 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + server: { + port: 5173, + strictPort: true, + host: true, + proxy: { + '/api': { + target: 'http://localhost:5050', + changeOrigin: true, + }, + '/images': { + target: 'http://localhost:5050', + changeOrigin: true, + } + } + }, + build: { + outDir: 'dist', + assetsDir: 'assets' + } +}) + diff --git a/main.py b/main.py index 39f0194..0b03181 100644 --- a/main.py +++ b/main.py @@ -3,33 +3,28 @@ from flask import ( render_template, request, jsonify, - redirect, - url_for, send_from_directory, Response, - stream_template, ) +from flask_cors import CORS import snap7 -import snap7.util import json -import socket import time -import logging -import threading from datetime import datetime -from typing import Dict, Any, Optional, List -import struct import os -import csv -from pathlib import Path -import atexit -import psutil import sys from core import PLCDataStreamer app = Flask(__name__) +CORS( + app, + resources={r"/api/*": {"origins": ["http://localhost:5173", "http://127.0.0.1:5173", "*"]}}, +) app.secret_key = "plc_streamer_secret_key" +# React build directory (for Vite production build) +REACT_DIST_DIR = os.path.join(os.path.abspath("."), "frontend", "dist") + def resource_path(relative_path): """Get absolute path to resource, works for dev and for PyInstaller""" @@ -66,13 +61,31 @@ def serve_static(filename): return send_from_directory("static", filename) +# ============================== +# Frontend (React SPA and Legacy) +# ============================== + +@app.route("/app/assets/") +def serve_react_assets(filename): + """Serve built React assets from Vite (production).""" + assets_dir = os.path.join(REACT_DIST_DIR, "assets") + return send_from_directory(assets_dir, filename) + + @app.route("/") -def index(): - """Main page""" +@app.route("/app") +def serve_react_index(): + """Serve React SPA index (expects Vite build at frontend/dist).""" + index_path = os.path.join(REACT_DIST_DIR, "index.html") + return send_from_directory(REACT_DIST_DIR, "index.html") + + +@app.route("/legacy") +def legacy_index(): + """Legacy Jinja UI (kept for transition).""" if streamer is None: return "Application not initialized", 503 - # Get variables for the current dataset or empty dict if no current dataset current_variables = {} if streamer.current_dataset_id and streamer.current_dataset_id in streamer.datasets: current_variables = streamer.datasets[streamer.current_dataset_id].get( diff --git a/plc_datasets.json b/plc_datasets.json index ee3aeef..a39f71e 100644 --- a/plc_datasets.json +++ b/plc_datasets.json @@ -65,5 +65,5 @@ ], "current_dataset_id": "Fast", "version": "1.0", - "last_update": "2025-08-10T01:12:19.126350" + "last_update": "2025-08-10T01:45:12.551768" } \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 226fa4a..6270749 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,5 @@ Flask==2.3.3 python-snap7==1.3 psutil==5.9.5 flask-socketio==5.3.6 -jsonschema==4.22.0 \ No newline at end of file +jsonschema==4.22.0 +Flask-Cors==4.0.0 \ No newline at end of file diff --git a/schemas/datasets.schema.json b/schemas/datasets.schema.json index 20e7e4c..1629d96 100644 --- a/schemas/datasets.schema.json +++ b/schemas/datasets.schema.json @@ -13,10 +13,19 @@ "type": "object", "properties": { "name": { - "type": "string" + "type": "string", + "title": "Dataset Name", + "description": "Nombre legible del dataset", + "minLength": 1, + "maxLength": 60 }, "prefix": { - "type": "string" + "type": "string", + "title": "CSV Prefix", + "description": "Prefijo para archivos CSV", + "pattern": "^[a-zA-Z0-9_-]+$", + "minLength": 1, + "maxLength": 20 }, "variables": { "type": "object", @@ -25,6 +34,7 @@ "properties": { "area": { "type": "string", + "title": "Memory Area", "enum": [ "db", "mw", @@ -43,22 +53,28 @@ "integer", "null" ], - "minimum": 1 + "title": "DB Number", + "minimum": 1, + "maximum": 9999 }, "offset": { "type": "integer", - "minimum": 0 + "title": "Offset", + "minimum": 0, + "maximum": 8191 }, "bit": { "type": [ "integer", "null" ], + "title": "Bit Position", "minimum": 0, "maximum": 7 }, "type": { "type": "string", + "title": "Data Type", "enum": [ "real", "int", @@ -74,6 +90,7 @@ }, "streaming": { "type": "boolean", + "title": "Stream to PlotJuggler", "default": false } }, @@ -86,6 +103,7 @@ }, "streaming_variables": { "type": "array", + "title": "Streaming Variables", "items": { "type": "string" }, @@ -96,17 +114,32 @@ "number", "null" ], - "minimum": 0.01 + "title": "Sampling Interval (s)", + "description": "Vacío para usar el intervalo global", + "minimum": 0.01, + "maximum": 10 }, "enabled": { "type": "boolean", - "default": false + "title": "Dataset Enabled", + "default": false, + "enum": [ + true, + false + ], + "options": { + "enum_titles": [ + "Activate", + "Deactivate" + ] + } }, "created": { "type": [ "string", "null" - ] + ], + "title": "Created" } }, "required": [ @@ -119,6 +152,7 @@ }, "active_datasets": { "type": "array", + "title": "Active Datasets", "items": { "type": "string" }, @@ -128,16 +162,19 @@ "type": [ "string", "null" - ] + ], + "title": "Current Dataset Id" }, "version": { - "type": "string" + "type": "string", + "title": "Version" }, "last_update": { "type": [ "string", "null" - ] + ], + "title": "Last Update" } }, "required": [ diff --git a/schemas/plc.schema.json b/schemas/plc.schema.json index 89b8ee4..ab4b575 100644 --- a/schemas/plc.schema.json +++ b/schemas/plc.schema.json @@ -14,16 +14,22 @@ "ip": { "type": "string", "title": "PLC IP", + "description": "Dirección IP del PLC (S7-31x)", + "format": "ipv4", "pattern": "^.+$" }, "rack": { "type": "integer", + "title": "Rack", + "description": "Número de rack (0-7)", "minimum": 0, "maximum": 7, "default": 0 }, "slot": { "type": "integer", + "title": "Slot", + "description": "Número de slot (generalmente 2)", "minimum": 0, "maximum": 31, "default": 2 @@ -61,7 +67,9 @@ "sampling_interval": { "type": "number", "minimum": 0.01, + "maximum": 10, "title": "Sampling Interval (s)", + "description": "Intervalo global de muestreo en segundos", "default": 0.1 }, "csv_config": { @@ -78,8 +86,12 @@ "type": "boolean", "title": "Rotation", "default": true, - "x-ui": { - "toggleLabels": [ + "enum": [ + true, + false + ], + "options": { + "enum_titles": [ "Activate", "Deactivate" ] diff --git a/schemas/plots.schema.json b/schemas/plots.schema.json index b879fbf..35a8c57 100644 --- a/schemas/plots.schema.json +++ b/schemas/plots.schema.json @@ -12,10 +12,14 @@ "type": "object", "properties": { "name": { - "type": "string" + "type": "string", + "title": "Plot Name", + "description": "Nombre de la sesión de gráfica" }, "variables": { "type": "array", + "title": "Variables", + "description": "Variables a graficar", "items": { "type": "string" }, @@ -23,6 +27,8 @@ }, "time_window": { "type": "integer", + "title": "Time Window (s)", + "description": "Ventana temporal en segundos", "minimum": 5, "maximum": 3600, "default": 60 @@ -31,30 +37,38 @@ "type": [ "number", "null" - ] + ], + "title": "Y Min", + "description": "Vacío para auto" }, "y_max": { "type": [ "number", "null" - ] + ], + "title": "Y Max", + "description": "Vacío para auto" }, "trigger_variable": { "type": [ "string", "null" - ] + ], + "title": "Trigger Variable" }, "trigger_enabled": { "type": "boolean", + "title": "Enable Trigger", "default": false }, "trigger_on_true": { "type": "boolean", + "title": "Trigger on True", "default": true }, "session_id": { - "type": "string" + "type": "string", + "title": "Session Id" } }, "required": [ diff --git a/static/css/styles.css b/static/css/styles.css index 50bfd09..38af663 100644 --- a/static/css/styles.css +++ b/static/css/styles.css @@ -1337,4 +1337,64 @@ textarea { .tab-btn.active { border-right-color: var(--pico-primary); } +} + +/* ============================= + Config Editor (JSON Schema) UI compact layout + Works for both JSONForm (#jsonform-form) and fallback renderer (.config-editor-form) + ============================= */ +/* Grid layout for JSONForm-generated form */ +#jsonform-form { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 1rem; +} + +#jsonform-form .control-group { + margin: 0; +} + +#jsonform-form .form-actions { + grid-column: 1 / -1; +} + +/* Grid layout for fallback renderer */ +.config-editor-form { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 1rem; +} + +.config-editor-form>.form-group { + margin: 0; +} + +/* Group inner properties in a compact grid */ +.config-editor-form .object-group { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 0.75rem; + padding: 0.75rem; + border: var(--pico-border-width) solid var(--pico-border-color); + border-radius: var(--pico-border-radius); + background: var(--pico-muted-background-color); +} + +.config-editor-form .object-group>.form-group { + margin: 0; +} + +/* Make number inputs a bit tighter to save space */ +.config-editor-form input[type="number"] { + max-width: 100%; +} + +/* Responsive tweak for very small screens */ +@media (max-width: 480px) { + + #jsonform-form, + .config-editor-form, + .config-editor-form .object-group { + grid-template-columns: 1fr; + } } \ No newline at end of file diff --git a/static/js/config_editor.js b/static/js/config_editor.js index d9eeebb..e44b16b 100644 --- a/static/js/config_editor.js +++ b/static/js/config_editor.js @@ -84,7 +84,41 @@ currentData = configData.data; - renderForm(container, schemaData.schema, currentData); + if (container) container.innerHTML = ''; + + // Prefer JSONForm if available for a simple form UI + if (window.$ && window._ && typeof $.fn.jsonForm === 'function') { + const formEl = document.createElement('form'); + formEl.id = 'jsonform-form'; + container.appendChild(formEl); + + const formDef = ["*", { "type": "submit", "title": "Save" }]; + + $(formEl).jsonForm({ + schema: schemaData.schema, + form: formDef, + value: currentData, + onSubmit: function (errors, values) { + if (errors) { + showMessage('Validation errors in form', 'error'); + return false; + } + doSave(values); + return false; + } + }); + + const saveBtn = document.getElementById('btn-save-config'); + if (saveBtn) { + saveBtn.onclick = () => { + const f = document.getElementById('jsonform-form'); + if (f) f.requestSubmit ? f.requestSubmit() : f.dispatchEvent(new Event('submit', { cancelable: true })); + }; + } + } else { + // Fallback: minimal manual renderer based on schema + renderForm(container, schemaData.schema, currentData); + } } catch (e) { if (container) container.innerHTML = ''; showMessage(`Error loading editor: ${e}`, 'error'); @@ -120,6 +154,17 @@ title.textContent = propSchema.title || label; wrapper.appendChild(title); + // Optional description/help text + if (propSchema.description) { + const help = document.createElement('small'); + help.textContent = propSchema.description; + help.style.display = 'block'; + help.style.color = 'var(--pico-muted-color)'; + help.style.marginTop = '-0.25rem'; + help.style.marginBottom = '0.25rem'; + wrapper.appendChild(help); + } + const type = Array.isArray(propSchema.type) ? propSchema.type : [propSchema.type]; // Objetos @@ -401,20 +446,32 @@ return null; } - async function onSave() { + async function doSave(payload) { if (!currentSchemaId) return; try { const res = await fetch(`/api/config/${currentSchemaId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(currentData || {}) + body: JSON.stringify(payload) }); const result = await res.json(); - if (result.success) { - showMessage('Configuration saved successfully', 'success'); - } else { - showMessage(result.error || 'Failed to save configuration', 'error'); + if (result.success) showMessage('Configuration saved successfully', 'success'); + else showMessage(result.error || 'Failed to save configuration', 'error'); + } catch (e) { + showMessage(`Error saving configuration: ${e}`, 'error'); + } + } + + async function onSave() { + if (!currentSchemaId) return; + try { + // If JSONForm exists, its Save is already wired; fall back to currentData + let payload = currentData || {}; + if (window.__jsonEditorInstance && typeof window.__jsonEditorInstance.get === 'function') { + // Legacy safety; should not happen because we no longer render JSONEditor + payload = window.__jsonEditorInstance.get(); } + await doSave(payload); } catch (e) { showMessage(`Error saving configuration: ${e}`, 'error'); } @@ -423,8 +480,12 @@ async function onExport() { if (!currentSchemaId) return; try { - const res = await fetch(`/api/config/${currentSchemaId}/export`); - const blob = await res.blob(); + let val = currentData || {}; + // Prefer currentData; JSONForm updates are applied on submit + if (window.__jsonEditorInstance && typeof window.__jsonEditorInstance.get === 'function') { + val = window.__jsonEditorInstance.get(); + } + const blob = new Blob([JSON.stringify(val, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; @@ -447,16 +508,31 @@ const text = await file.text(); const json = JSON.parse(text); currentData = json; - // Re-render con el nuevo JSON (no volvemos a pedir el schema) - const res = await fetch(`/api/config/schema/${currentSchemaId}`); - const schemaData = await res.json(); - if (!schemaData.success) throw new Error(schemaData.error || 'Schema error'); - renderForm(document.getElementById('config-form-container'), schemaData.schema, currentData); + const formEl = document.getElementById('jsonform-form'); + if (formEl && window.$ && typeof $(formEl).jsonForm === 'function') { + const schemaRes = await fetch(`/api/config/schema/${currentSchemaId}`); + const schemaData = await schemaRes.json(); + if (!schemaData.success) throw new Error(schemaData.error || 'Schema error'); + $(formEl).jsonForm({ + schema: schemaData.schema, + form: ["*", { "type": "submit", "title": "Save" }], + value: currentData, + onSubmit: function (errors, values) { + if (errors) return showMessage('Validation errors in form', 'error'); + doSave(values); + return false; + } + }); + } else { + const res = await fetch(`/api/config/schema/${currentSchemaId}`); + const schemaData = await res.json(); + if (!schemaData.success) throw new Error(schemaData.error || 'Schema error'); + renderForm(document.getElementById('config-form-container'), schemaData.schema, currentData); + } showMessage('JSON imported (not saved yet)', 'info'); } catch (e) { showMessage(`Invalid JSON: ${e}`, 'error'); } finally { - // Reset input para permitir re-importar el mismo archivo evt.target.value = ''; } } diff --git a/system_state.json b/system_state.json index d2f9286..9539107 100644 --- a/system_state.json +++ b/system_state.json @@ -8,5 +8,5 @@ ] }, "auto_recovery_enabled": true, - "last_update": "2025-08-10T01:12:19.169231" + "last_update": "2025-08-10T01:45:12.574799" } \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index c912118..17db47a 100644 --- a/templates/index.html +++ b/templates/index.html @@ -12,6 +12,8 @@ + + @@ -23,6 +25,9 @@
+
+

New UI available: La nueva interfaz React está disponible en /app. Esta página permanecerá como modo legado temporalmente.

+

@@ -790,6 +795,13 @@ + + + + + + +