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 (
+
+
+
+
+ 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.
+
+
+
+
+ )
+}
+
+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.
+