fix(dashboard): JSON config renderizzato come HTML colorato custom (no Prism/hljs)

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('<pre class="raw-block">') 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) <noreply@anthropic.com>
This commit is contained in:
2026-05-11 22:52:50 +02:00
parent 171f554916
commit 6f6fbb30a0
+79 -6
View File
@@ -18,6 +18,7 @@ Palette "Neon Trading Dashboard" (ispirata screenshot 2026-05-11):
from __future__ import annotations from __future__ import annotations
import html
import json import json
import os import os
from pathlib import Path 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; }} .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;
}}
</style> </style>
""" """
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 '<span class="cb-punct">{}</span>'
items = []
for k, v in obj.items():
key = f'<span class="cb-key">"{html.escape(str(k))}"</span>'
val = _json_to_html(v, indent + 1)
items.append(f"{inner_pad}{key}<span class=\"cb-punct\">:</span> {val}")
return ('<span class="cb-punct">{</span>\n'
+ '<span class="cb-punct">,</span>\n'.join(items)
+ f'\n{pad}<span class="cb-punct">}}</span>')
if isinstance(obj, list):
if not obj:
return '<span class="cb-punct">[]</span>'
items = [_json_to_html(x, indent + 1) for x in obj]
return ('<span class="cb-punct">[</span>\n'
+ '<span class="cb-punct">,</span>\n'.join(inner_pad + i for i in items)
+ f'\n{pad}<span class="cb-punct">]</span>')
if isinstance(obj, bool):
return f'<span class="cb-bool">{str(obj).lower()}</span>'
if obj is None:
return '<span class="cb-null">null</span>'
if isinstance(obj, (int, float)):
return f'<span class="cb-number">{obj}</span>'
return f'<span class="cb-string">"{html.escape(str(obj))}"</span>'
def _apply_theme() -> None: def _apply_theme() -> None:
ui.add_head_html(_CUSTOM_CSS) ui.add_head_html(_CUSTOM_CSS)
ui.dark_mode().enable() ui.dark_mode().enable()
@@ -486,7 +555,7 @@ def index() -> None:
completed_lbl = ui.label("Completed: —") completed_lbl = ui.label("Completed: —")
ui.separator() ui.separator()
ui.label("Config").classes("text-subtitle1") ui.label("Config").classes("text-subtitle1")
cfg_code = ui.code("", language="json").classes("w-full") cfg_code = ui.html('<pre class="config-block"></pre>').classes("w-full")
def refresh() -> None: def refresh() -> None:
run_id = select.value run_id = select.value
@@ -518,7 +587,7 @@ def index() -> None:
started_lbl.text = f"Started: {s['started_at']}" started_lbl.text = f"Started: {s['started_at']}"
completed_lbl.text = f"Completed: {s['completed_at']}" completed_lbl.text = f"Completed: {s['completed_at']}"
cfg_code.content = json.dumps(s["config"], indent=2) cfg_code.content = f'<pre class="config-block">{_json_to_html(s["config"])}</pre>'
def on_select_change() -> None: def on_select_change() -> None:
state["run_id"] = select.value state["run_id"] = select.value
@@ -688,10 +757,10 @@ def genomes() -> None:
style_lbl = ui.label("").classes("text-h4") style_lbl = ui.label("").classes("text-h4")
ui.label("System prompt").classes("text-subtitle1 q-mt-md") ui.label("System prompt").classes("text-subtitle1 q-mt-md")
prompt_code = ui.code("", language="text").classes("w-full") prompt_code = ui.html('<pre class="raw-block">—</pre>').classes("w-full")
ui.label("Raw LLM output").classes("text-subtitle1 q-mt-md") ui.label("Raw LLM output").classes("text-subtitle1 q-mt-md")
raw_code = ui.code("", language="text").classes("w-full") raw_code = ui.html('<pre class="raw-block">—</pre>').classes("w-full")
parse_error_lbl = ui.label("").classes("q-mt-sm").style( parse_error_lbl = ui.label("").classes("q-mt-sm").style(
"color: #FF6B6B; font-weight: 600;" "color: #FF6B6B; font-weight: 600;"
@@ -705,8 +774,12 @@ def genomes() -> None:
dd_lbl.text = f"{float(row.get('max_dd', 0)):.3f}" dd_lbl.text = f"{float(row.get('max_dd', 0)):.3f}"
trades_lbl.text = str(int(row.get("n_trades", 0))) trades_lbl.text = str(int(row.get("n_trades", 0)))
style_lbl.text = str(row.get("cognitive_style", "")) style_lbl.text = str(row.get("cognitive_style", ""))
prompt_code.content = str(row.get("system_prompt", "")) prompt_code.content = (
raw_code.content = str(row.get("raw_text", "") or "") f'<pre class="raw-block">{html.escape(str(row.get("system_prompt", "")))}</pre>'
)
raw_code.content = (
f'<pre class="raw-block">{html.escape(str(row.get("raw_text", "") or ""))}</pre>'
)
pe = row.get("parse_error") pe = row.get("parse_error")
parse_error_lbl.text = f"❌ Parse error: {pe}" if pe else "" parse_error_lbl.text = f"❌ Parse error: {pe}" if pe else ""