From 6f6fbb30a014d3e2d17fceff0bd51b890975ded9 Mon Sep 17 00:00:00 2001 From: AdrianoDev Date: Mon, 11 May 2026 22:52:50 +0200 Subject: [PATCH] fix(dashboard): JSON config renderizzato come HTML colorato custom (no Prism/hljs) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Soluzione robusta al bug "chiavi JSON invisibili": invece di tentare override CSS su Prism.js/highlight.js (che nelle build NiceGUI possono non essere attivi o variare), il JSON viene serializzato server-side con span colorati espliciti e renderizzato via ui.html. Implementato helper _json_to_html(obj) che produce HTML con classi: - .cb-key (chiavi) → cyan #00D9FF - .cb-string → green #00E676 - .cb-number → pink #FF2D87 - .cb-bool / .cb-null → amber #FFB800 - .cb-punct (graffe, virgole, due punti) → muted Stessa logica per system_prompt e raw LLM output in /genomes: sostituito ui.code(language=text) con ui.html('
') e
html.escape() per safety XSS. Background dedicato #1A1A24 e font
JetBrains Mono 13.5px, line-height 1.7, ottimo contrasto su qualsiasi
browser senza dipendere da syntax highlighter esterno.

Co-Authored-By: Claude Opus 4.7 (1M context) 
---
 src/multi_swarm/dashboard/nicegui_app.py | 85 ++++++++++++++++++++++--
 1 file changed, 79 insertions(+), 6 deletions(-)

diff --git a/src/multi_swarm/dashboard/nicegui_app.py b/src/multi_swarm/dashboard/nicegui_app.py
index 918f178..701b152 100644
--- a/src/multi_swarm/dashboard/nicegui_app.py
+++ b/src/multi_swarm/dashboard/nicegui_app.py
@@ -18,6 +18,7 @@ Palette "Neon Trading Dashboard" (ispirata screenshot 2026-05-11):
 
 from __future__ import annotations
 
+import html
 import json
 import os
 from pathlib import Path
@@ -269,10 +270,78 @@ pre[class*="language-"], code[class*="language-"] {{
 }}
 
 .q-badge {{ border-radius: 6px !important; font-weight: 500 !important; padding: 4px 10px !important; font-size: 12px !important; }}
+
+.config-block {{
+    background: #1A1A24;
+    color: {COLOR_TEXT};
+    border: 1px solid {COLOR_BORDER};
+    border-radius: 10px;
+    padding: 18px 20px;
+    font-family: 'JetBrains Mono', 'Fira Code', monospace;
+    font-size: 13.5px;
+    line-height: 1.7;
+    overflow-x: auto;
+    white-space: pre;
+    margin: 0;
+}}
+.config-block .cb-key {{ color: {COLOR_SECONDARY}; font-weight: 500; }}
+.config-block .cb-string {{ color: {COLOR_SUCCESS}; }}
+.config-block .cb-number {{ color: {COLOR_PRIMARY}; font-weight: 500; }}
+.config-block .cb-bool {{ color: {COLOR_ACCENT}; }}
+.config-block .cb-null {{ color: {COLOR_ACCENT}; font-style: italic; }}
+.config-block .cb-punct {{ color: {COLOR_TEXT_MUTED}; }}
+
+.raw-block {{
+    background: #1A1A24;
+    color: {COLOR_TEXT};
+    border: 1px solid {COLOR_BORDER};
+    border-radius: 10px;
+    padding: 18px 20px;
+    font-family: 'JetBrains Mono', 'Fira Code', monospace;
+    font-size: 13px;
+    line-height: 1.6;
+    overflow-x: auto;
+    white-space: pre-wrap;
+    word-break: break-word;
+    max-height: 400px;
+    overflow-y: auto;
+    margin: 0;
+}}
 
 """
 
 
+def _json_to_html(obj: Any, indent: int = 0) -> str:
+    """Render JSON con span colorati espliciti. Garantisce leggibilità ovunque."""
+    pad = "  " * indent
+    inner_pad = "  " * (indent + 1)
+    if isinstance(obj, dict):
+        if not obj:
+            return '{}'
+        items = []
+        for k, v in obj.items():
+            key = f'"{html.escape(str(k))}"'
+            val = _json_to_html(v, indent + 1)
+            items.append(f"{inner_pad}{key}: {val}")
+        return ('{\n'
+                + ',\n'.join(items)
+                + f'\n{pad}}}')
+    if isinstance(obj, list):
+        if not obj:
+            return '[]'
+        items = [_json_to_html(x, indent + 1) for x in obj]
+        return ('[\n'
+                + ',\n'.join(inner_pad + i for i in items)
+                + f'\n{pad}]')
+    if isinstance(obj, bool):
+        return f'{str(obj).lower()}'
+    if obj is None:
+        return 'null'
+    if isinstance(obj, (int, float)):
+        return f'{obj}'
+    return f'"{html.escape(str(obj))}"'
+
+
 def _apply_theme() -> None:
     ui.add_head_html(_CUSTOM_CSS)
     ui.dark_mode().enable()
@@ -486,7 +555,7 @@ def index() -> None:
         completed_lbl = ui.label("Completed: —")
     ui.separator()
     ui.label("Config").classes("text-subtitle1")
-    cfg_code = ui.code("", language="json").classes("w-full")
+    cfg_code = ui.html('
').classes("w-full")
 
     def refresh() -> None:
         run_id = select.value
@@ -518,7 +587,7 @@ def index() -> None:
 
         started_lbl.text = f"Started: {s['started_at']}"
         completed_lbl.text = f"Completed: {s['completed_at']}"
-        cfg_code.content = json.dumps(s["config"], indent=2)
+        cfg_code.content = f'
{_json_to_html(s["config"])}
' def on_select_change() -> None: state["run_id"] = select.value @@ -688,10 +757,10 @@ def genomes() -> None: style_lbl = ui.label("—").classes("text-h4") ui.label("System prompt").classes("text-subtitle1 q-mt-md") - prompt_code = ui.code("—", language="text").classes("w-full") + prompt_code = ui.html('
').classes("w-full") ui.label("Raw LLM output").classes("text-subtitle1 q-mt-md") - raw_code = ui.code("—", language="text").classes("w-full") + raw_code = ui.html('
').classes("w-full") parse_error_lbl = ui.label("").classes("q-mt-sm").style( "color: #FF6B6B; font-weight: 600;" @@ -705,8 +774,12 @@ def genomes() -> None: dd_lbl.text = f"{float(row.get('max_dd', 0)):.3f}" trades_lbl.text = str(int(row.get("n_trades", 0))) style_lbl.text = str(row.get("cognitive_style", "—")) - prompt_code.content = str(row.get("system_prompt", "—")) - raw_code.content = str(row.get("raw_text", "—") or "—") + prompt_code.content = ( + f'
{html.escape(str(row.get("system_prompt", "—")))}
' + ) + raw_code.content = ( + f'
{html.escape(str(row.get("raw_text", "—") or "—"))}
' + ) pe = row.get("parse_error") parse_error_lbl.text = f"❌ Parse error: {pe}" if pe else ""