Version basica con Forms pero no dan una mejora a la aplicacion
This commit is contained in:
parent
10df4e94bd
commit
5581e26d10
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>PLC S7-31x Streamer & Logger - React</title>
|
||||
<link rel="icon" href="/images/SIDEL.png" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import React from 'react'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="container py-3">
|
||||
<header className="mb-3">
|
||||
<h1 className="h3 d-flex align-items-center gap-2">
|
||||
<img src="/images/SIDEL.png" alt="SIDEL" style={{height: 28}} />
|
||||
PLC S7-31x Streamer & Logger (React)
|
||||
</h1>
|
||||
<p className="text-muted mb-0">Base de React + Bootstrap lista. Empezaremos a migrar vistas.</p>
|
||||
<a className="btn btn-sm btn-outline-secondary mt-2" href="/legacy">Ir al modo legado</a>
|
||||
</header>
|
||||
|
||||
<div className="alert alert-info">
|
||||
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.
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h2 className="h5">Acciones rápidas</h2>
|
||||
<div className="d-flex flex-wrap gap-2">
|
||||
<a className="btn btn-primary" href="/app">Reload SPA</a>
|
||||
<a className="btn btn-outline-primary" href="/api/status" target="_blank" rel="noreferrer">Ver /api/status</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
|
|
@ -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(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
)
|
||||
|
|
@ -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'
|
||||
}
|
||||
})
|
||||
|
45
main.py
45
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/<path:filename>")
|
||||
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(
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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
|
||||
jsonschema==4.22.0
|
||||
Flask-Cors==4.0.0
|
|
@ -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": [
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 = '';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,5 +8,5 @@
|
|||
]
|
||||
},
|
||||
"auto_recovery_enabled": true,
|
||||
"last_update": "2025-08-10T01:12:19.169231"
|
||||
"last_update": "2025-08-10T01:45:12.574799"
|
||||
}
|
|
@ -12,6 +12,8 @@
|
|||
|
||||
<!-- Custom styles -->
|
||||
<link rel="stylesheet" href="/static/css/styles.css">
|
||||
<!-- JSONEditor (tree view) CSS -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jsoneditor@9.10.2/dist/jsoneditor.min.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -23,6 +25,9 @@
|
|||
</div>
|
||||
|
||||
<main class="container">
|
||||
<div class="info-section" style="margin: 1rem 0;">
|
||||
<p><strong>New UI available:</strong> La nueva interfaz React está disponible en <a href="/app">/app</a>. Esta página permanecerá como modo legado temporalmente.</p>
|
||||
</div>
|
||||
<!-- Header -->
|
||||
<header class="header">
|
||||
<h1>
|
||||
|
@ -790,6 +795,13 @@
|
|||
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-luxon@1.3.1"></script>
|
||||
<script src="https://unpkg.com/chartjs-plugin-zoom@1.2.1/dist/chartjs-plugin-zoom.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-streaming@2.0.0"></script>
|
||||
<!-- JSONEditor (tree view) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/jsoneditor@9.10.2/dist/jsoneditor.min.js"></script>
|
||||
<!-- JSONForm deps (jQuery + Underscore) and JSONForm -->
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.13.6/underscore-min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/gh/jsonform/jsonform@2.5.1/lib/deps/opt/jsv.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/gh/jsonform/jsonform@2.5.1/lib/jsonform.js"></script>
|
||||
|
||||
<!-- JavaScript Modules -->
|
||||
<script src="/static/js/utils.js"></script>
|
||||
|
|
Loading…
Reference in New Issue