style(dashboard): redesign moderno con palette Modern Fintech Dark

Sostituita palette Inter FC con design language fintech moderno tipo
Linear/Tremor/Plane:

Palette:
- BG #0A0E1A deep blue-black con radial gradient sottile
- Surface #131829 / surface-2 #1A2138 per hover/active
- Primary cyan #06B6D4 (highlight performance)
- Secondary purple #A78BFA (median curves)
- Accent amber #F59E0B (warnings, p90)
- Success #10B981, danger #EF4444

Typography:
- Inter da Google Fonts (peso 400-700)
- JetBrains Mono per code/numeri tabulari
- letter-spacing negativo + tnum font-feature

Component refinement:
- Card: border sottile rgba + hover cyan glow, no gradient pesante
- Header: backdrop-blur 12px + glass effect
- Nav link: underline animato bottom su active
- Brand: dot pulsante cyan con glow
- Progress bar: 8px rounded, track surface-2
- Metric card: layout left-aligned, label uppercase tracked, value 26px
  con accent class (cyan/purple/amber/green) per coding semantico
- Tabella: header uppercase tracked, row hover cyan-tinted
- Code block: surface scuro + border + JetBrains Mono

Plot Plotly:
- paper_bgcolor → surface (no più sfondo full-bleed)
- gridcolor neutral rgba(slate, 0.08)
- Legend bg surface 0.95 opacity

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-11 22:39:59 +02:00
parent 2f38562e23
commit 0486e19829
+206 -78
View File
@@ -5,11 +5,15 @@ Default port 8080. Streamlit resta su 8501 durante la migrazione.
Riusa ``dashboard.data`` (Repository helpers) senza modifiche al backend.
Palette Inter FC:
- Nero: #000000 (sfondo)
- Dark navy: #010E80 (heritage Inter, surface)
- Blu primary: #1E5BC6
- Blu secondary: #0068A8
Palette "Modern Fintech Dark":
- BG: #0A0E1A (deep blue-black)
- Surface: #131829 (card base)
- Surface elevata: #1A2138 (hover/active)
- Primary cyan: #06B6D4 (performance highlights)
- Secondary purple: #A78BFA (median, secondary curves)
- Accent amber: #F59E0B (warnings, p90)
- Success green: #10B981, Danger red: #EF4444
- Text: #F1F5F9 (primary), #94A3B8 (muted)
"""
from __future__ import annotations
@@ -34,57 +38,178 @@ from multi_swarm.dashboard.data import (
DB_PATH = os.environ.get("DB_PATH", "./runs.db")
REFRESH_INTERVAL_S = 3.0
# --- Inter FC palette ---
COLOR_BG = "#000000"
COLOR_SURFACE = "#010E80"
COLOR_PRIMARY = "#1E5BC6"
COLOR_SECONDARY = "#0068A8"
COLOR_ACCENT = "#FFD700"
COLOR_TEXT = "#E8EEFC"
COLOR_TEXT_MUTED = "#8FA3D4"
# --- Modern Fintech Dark palette ---
COLOR_BG = "#0A0E1A"
COLOR_SURFACE = "#131829"
COLOR_SURFACE_2 = "#1A2138"
COLOR_BORDER = "rgba(148, 163, 184, 0.12)"
COLOR_PRIMARY = "#06B6D4"
COLOR_SECONDARY = "#A78BFA"
COLOR_ACCENT = "#F59E0B"
COLOR_SUCCESS = "#10B981"
COLOR_DANGER = "#EF4444"
COLOR_TEXT = "#F1F5F9"
COLOR_TEXT_MUTED = "#94A3B8"
_STATUS_BADGE: dict[str, tuple[str, str]] = {
"running": ("🟢 running", "positive"),
"completed": (" completed", "positive"),
"failed": (" failed", "negative"),
"running": (" running", "positive"),
"completed": (" completed", "positive"),
"failed": (" failed", "negative"),
}
_CUSTOM_CSS = f"""
<style>
body {{ background: {COLOR_BG} !important; color: {COLOR_TEXT}; }}
.q-page {{ background: {COLOR_BG} !important; }}
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
* {{ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; letter-spacing: -0.01em; }}
code, pre, .q-code {{ font-family: 'JetBrains Mono', 'Fira Code', monospace !important; font-size: 12.5px !important; }}
body, .q-page-container, .q-page {{
background: {COLOR_BG} !important;
color: {COLOR_TEXT};
background-image: radial-gradient(ellipse at top, rgba(6, 182, 212, 0.04) 0%, transparent 50%);
}}
.q-card {{
background: linear-gradient(135deg, {COLOR_SURFACE} 0%, #000936 100%) !important;
background: {COLOR_SURFACE} !important;
color: {COLOR_TEXT} !important;
border: 1px solid {COLOR_PRIMARY}33;
box-shadow: 0 2px 8px {COLOR_PRIMARY}22;
border: 1px solid {COLOR_BORDER};
border-radius: 12px !important;
box-shadow: 0 1px 2px rgba(0,0,0,0.3), 0 4px 12px rgba(0,0,0,0.15);
transition: border-color 0.2s, transform 0.2s;
}}
.q-card:hover {{ border-color: rgba(6, 182, 212, 0.35); }}
.metric-card {{
padding: 16px;
border-radius: 10px;
text-align: center;
padding: 20px 16px;
text-align: left;
display: flex;
flex-direction: column;
gap: 6px;
min-width: 140px;
}}
.metric-card .text-h4 {{ color: {COLOR_PRIMARY} !important; font-weight: 600; }}
.metric-card .text-caption {{ color: {COLOR_TEXT_MUTED} !important; }}
.q-header {{
background: linear-gradient(90deg, {COLOR_BG} 0%, {COLOR_SURFACE} 100%) !important;
border-bottom: 2px solid {COLOR_PRIMARY};
.metric-card .text-caption {{
color: {COLOR_TEXT_MUTED} !important;
font-size: 11px !important;
font-weight: 500 !important;
text-transform: uppercase;
letter-spacing: 0.06em;
}}
.q-tab--active {{ color: {COLOR_PRIMARY} !important; }}
.q-tab__indicator {{ background: {COLOR_PRIMARY} !important; }}
.nav-link {{
.metric-card .text-h4 {{
color: {COLOR_TEXT} !important;
padding: 8px 16px;
border-radius: 6px;
text-decoration: none;
transition: background 0.2s;
font-weight: 600 !important;
font-size: 26px !important;
line-height: 1.2 !important;
font-feature-settings: 'tnum';
}}
.nav-link:hover {{ background: {COLOR_PRIMARY}33; }}
.nav-link.active {{ background: {COLOR_PRIMARY}; color: #fff !important; }}
.q-linear-progress__track {{ background: {COLOR_BG} !important; }}
.q-separator {{ background: {COLOR_PRIMARY}44 !important; }}
.q-field--outlined .q-field__control {{ color: {COLOR_TEXT} !important; }}
code, pre {{ background: #000936 !important; color: {COLOR_TEXT} !important; }}
.metric-card.accent-cyan .text-h4 {{ color: {COLOR_PRIMARY} !important; }}
.metric-card.accent-purple .text-h4 {{ color: {COLOR_SECONDARY} !important; }}
.metric-card.accent-amber .text-h4 {{ color: {COLOR_ACCENT} !important; }}
.metric-card.accent-green .text-h4 {{ color: {COLOR_SUCCESS} !important; }}
.q-header {{
background: rgba(19, 24, 41, 0.85) !important;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-bottom: 1px solid {COLOR_BORDER} !important;
box-shadow: none !important;
}}
.nav-link {{
color: {COLOR_TEXT_MUTED} !important;
padding: 8px 14px;
border-radius: 8px;
text-decoration: none;
font-size: 13.5px;
font-weight: 500;
transition: all 0.15s ease;
position: relative;
}}
.nav-link:hover {{
color: {COLOR_TEXT} !important;
background: {COLOR_SURFACE_2};
}}
.nav-link.active {{
color: {COLOR_PRIMARY} !important;
background: rgba(6, 182, 212, 0.08);
}}
.nav-link.active::after {{
content: '';
position: absolute;
bottom: -16px;
left: 14px;
right: 14px;
height: 2px;
background: {COLOR_PRIMARY};
border-radius: 2px 2px 0 0;
}}
.brand {{
color: {COLOR_TEXT};
font-weight: 700;
font-size: 15px;
display: flex;
align-items: center;
gap: 8px;
}}
.brand-dot {{
width: 8px;
height: 8px;
border-radius: 50%;
background: {COLOR_PRIMARY};
box-shadow: 0 0 12px {COLOR_PRIMARY};
}}
.q-linear-progress {{ height: 8px !important; border-radius: 6px !important; }}
.q-linear-progress__track {{ background: {COLOR_SURFACE_2} !important; }}
.q-linear-progress__model {{ border-radius: 6px !important; }}
.q-separator {{ background: {COLOR_BORDER} !important; }}
.q-field--outlined .q-field__control {{
background: {COLOR_SURFACE} !important;
border-radius: 8px !important;
}}
.q-field--outlined .q-field__control:before {{ border-color: {COLOR_BORDER} !important; }}
.q-field--outlined.q-field--focused .q-field__control:after {{ border-color: {COLOR_PRIMARY} !important; }}
.q-field__label {{ color: {COLOR_TEXT_MUTED} !important; }}
.q-field__native, .q-field__input {{ color: {COLOR_TEXT} !important; }}
.q-btn {{ border-radius: 8px !important; font-weight: 500 !important; text-transform: none !important; letter-spacing: 0 !important; }}
.q-table {{ background: transparent !important; color: {COLOR_TEXT} !important; }}
.q-table thead tr {{ background: {COLOR_SURFACE_2} !important; }}
.q-table th {{
color: {COLOR_TEXT_MUTED} !important;
font-size: 11px !important;
font-weight: 600 !important;
text-transform: uppercase;
letter-spacing: 0.06em;
}}
.q-table tbody tr {{ transition: background 0.15s; }}
.q-table tbody tr:hover {{ background: rgba(6, 182, 212, 0.05) !important; }}
.q-table tbody tr.selected {{ background: rgba(6, 182, 212, 0.12) !important; }}
.q-table td {{ border-bottom: 1px solid {COLOR_BORDER} !important; font-feature-settings: 'tnum'; }}
.text-h6 {{ font-weight: 600 !important; letter-spacing: -0.015em !important; }}
.text-subtitle1 {{
color: {COLOR_TEXT_MUTED} !important;
font-size: 11px !important;
font-weight: 600 !important;
text-transform: uppercase !important;
letter-spacing: 0.08em !important;
margin-bottom: 8px !important;
}}
code, pre {{
background: #0D1220 !important;
color: {COLOR_TEXT} !important;
border: 1px solid {COLOR_BORDER};
border-radius: 8px !important;
padding: 12px !important;
}}
.q-badge {{ border-radius: 6px !important; font-weight: 500 !important; padding: 4px 10px !important; font-size: 12px !important; }}
</style>
"""
@@ -98,29 +223,32 @@ def _apply_theme() -> None:
accent=COLOR_ACCENT,
dark=COLOR_BG,
dark_page=COLOR_BG,
positive="#00C853",
negative="#D32F2F",
info=COLOR_SECONDARY,
warning="#FFB300",
positive=COLOR_SUCCESS,
negative=COLOR_DANGER,
info=COLOR_PRIMARY,
warning=COLOR_ACCENT,
)
def _build_header(active: str) -> None:
with ui.header().classes("items-center justify-between q-px-md"):
with ui.row().classes("items-center gap-6"):
ui.label("⚫🔵 Multi-Swarm Coevolutivo").classes("text-h6").style(
f"color: {COLOR_TEXT}; font-weight: 700;"
)
for path, label in (
("/", "Overview"),
("/convergence", "GA Convergence"),
("/genomes", "Genomes"),
):
cls = "nav-link active" if active == path else "nav-link"
ui.link(label, path).classes(cls)
ui.label(f"DB: {Path(DB_PATH).resolve().name}").classes("text-caption").style(
f"color: {COLOR_TEXT_MUTED};"
)
with ui.header().classes("items-center justify-between q-px-lg q-py-md"):
with ui.row().classes("items-center gap-8"):
with ui.row().classes("items-center gap-2").classes("brand"):
ui.html('<span class="brand-dot"></span>')
ui.html('<span class="brand">Multi-Swarm <span style="color:'
+ COLOR_TEXT_MUTED + ';font-weight:400;">/ Coevolutivo</span></span>')
with ui.row().classes("items-center gap-1"):
for path, label in (
("/", "Overview"),
("/convergence", "Convergence"),
("/genomes", "Genomes"),
):
cls = "nav-link active" if active == path else "nav-link"
ui.link(label, path).classes(cls)
with ui.row().classes("items-center gap-3"):
ui.html(f'<span style="color:{COLOR_TEXT_MUTED};font-size:12px;'
f'font-family:JetBrains Mono,monospace;">'
f'{Path(DB_PATH).resolve().name}</span>')
def _runs_options() -> dict[str, str]:
@@ -206,13 +334,13 @@ def _convergence_figure(gens_df: Any) -> go.Figure:
)
fig.update_layout(
template="plotly_dark",
paper_bgcolor=COLOR_BG,
plot_bgcolor=COLOR_BG,
paper_bgcolor=COLOR_SURFACE,
plot_bgcolor=COLOR_SURFACE,
font={"color": COLOR_TEXT},
xaxis={"title": "generation", "gridcolor": "rgba(30, 91, 198, 0.2)", "dtick": 1},
yaxis={"title": "fitness", "gridcolor": "rgba(30, 91, 198, 0.2)"},
xaxis={"title": "generation", "gridcolor": "rgba(148, 163, 184, 0.08)", "dtick": 1},
yaxis={"title": "fitness", "gridcolor": "rgba(148, 163, 184, 0.08)"},
title={"text": "Fitness convergence", "font": {"color": COLOR_TEXT, "size": 18}},
legend={"bgcolor": "rgba(1, 14, 128, 0.6)", "bordercolor": COLOR_PRIMARY, "borderwidth": 1},
legend={"bgcolor": "rgba(19, 24, 41, 0.95)", "bordercolor": COLOR_PRIMARY, "borderwidth": 1},
margin={"l": 50, "r": 30, "t": 50, "b": 50},
)
return fig
@@ -237,11 +365,11 @@ def _entropy_figure(gens_df: Any) -> go.Figure:
)
fig.update_layout(
template="plotly_dark",
paper_bgcolor=COLOR_BG,
plot_bgcolor=COLOR_BG,
paper_bgcolor=COLOR_SURFACE,
plot_bgcolor=COLOR_SURFACE,
font={"color": COLOR_TEXT},
xaxis={"title": "generation", "gridcolor": "rgba(30, 91, 198, 0.2)", "dtick": 1},
yaxis={"title": "entropy", "gridcolor": "rgba(30, 91, 198, 0.2)"},
xaxis={"title": "generation", "gridcolor": "rgba(148, 163, 184, 0.08)", "dtick": 1},
yaxis={"title": "entropy", "gridcolor": "rgba(148, 163, 184, 0.08)"},
title={"text": "Diversity (fitness entropy)", "font": {"color": COLOR_TEXT, "size": 18}},
margin={"l": 50, "r": 30, "t": 50, "b": 50},
)
@@ -275,16 +403,16 @@ def index() -> None:
eval_bar = ui.linear_progress(0.0, show_value=False).props("size=20px color=accent")
with ui.row().classes("w-full gap-4"):
with ui.card().classes("flex-grow metric-card"):
with ui.card().classes("flex-grow metric-card accent-cyan"):
ui.label("Top fitness").classes("text-caption")
top_lbl = ui.label("").classes("text-h4")
with ui.card().classes("flex-grow metric-card"):
with ui.card().classes("flex-grow metric-card accent-purple"):
ui.label("Median fitness").classes("text-caption")
median_lbl = ui.label("").classes("text-h4")
with ui.card().classes("flex-grow metric-card"):
with ui.card().classes("flex-grow metric-card accent-amber"):
ui.label("Parse success").classes("text-caption")
parse_lbl = ui.label("").classes("text-h4")
with ui.card().classes("flex-grow metric-card"):
with ui.card().classes("flex-grow metric-card accent-green"):
ui.label("Cost (USD)").classes("text-caption")
cost_lbl = ui.label("").classes("text-h4")
@@ -475,19 +603,19 @@ def genomes() -> None:
).style(f"color: {COLOR_TEXT_MUTED};")
with ui.row().classes("w-full gap-4 q-mt-sm"):
with ui.card().classes("flex-grow metric-card"):
with ui.card().classes("flex-grow metric-card accent-cyan"):
ui.label("fitness").classes("text-caption")
fit_lbl = ui.label("").classes("text-h4")
with ui.card().classes("flex-grow metric-card"):
with ui.card().classes("flex-grow metric-card accent-purple"):
ui.label("DSR").classes("text-caption")
dsr_lbl = ui.label("").classes("text-h4")
with ui.card().classes("flex-grow metric-card"):
with ui.card().classes("flex-grow metric-card accent-amber"):
ui.label("Sharpe").classes("text-caption")
sharpe_lbl = ui.label("").classes("text-h4")
with ui.card().classes("flex-grow metric-card"):
ui.label("max DD").classes("text-caption")
dd_lbl = ui.label("").classes("text-h4")
with ui.card().classes("flex-grow metric-card"):
with ui.card().classes("flex-grow metric-card accent-green"):
ui.label("trades").classes("text-caption")
trades_lbl = ui.label("").classes("text-h4")
with ui.card().classes("flex-grow metric-card"):