// App shell: sidebar + topbar + content area const NAV = [ { id: "screener", label: "Home", icon: "home", group: "Analisi" }, { id: "watchlist", label: "Watchlist", icon: "star", group: "Analisi" }, { id: "dashboard", label: "Dashboard", icon: "candles", group: "Analisi" }, { id: "calendar", label: "Calendario",icon: "calendar", group: "Analisi" }, { id: "news", label: "News", icon: "bell", group: "Analisi" }, { id: "sectors", label: "Settori", icon: "trending", group: "Analisi" }, { id: "premarket", label: "Pre-market",icon: "clock", group: "Analisi" }, { id: "picks", label: "Top Picks", icon: "spark", group: "Analisi" }, { id: "chain", label: "Chain", icon: "chain", group: "Analisi" }, { id: "report", label: "Report", icon: "fileText", group: "Strumenti" }, { id: "accuracy", label: "Accuratezza", icon: "check", group: "Strumenti" }, { id: "simulator", label: "Simulatore", icon: "chart", group: "Strumenti" }, { id: "sleeves", label: "Sleeves", icon: "chart", group: "Strumenti" }, { id: "evaluate", label: "Evaluate", icon: "beaker", group: "Strumenti" }, { id: "config", label: "Configurazione", icon: "settings", group: "Sistema" }, { id: "logs", label: "Log", icon: "terminal", group: "Sistema" }, { id: "costs", label: "Costi", icon: "coins", group: "Sistema", adminOnly: true }, { id: "admin", label: "Utenti", icon: "users", group: "Sistema", adminOnly: true }, ]; const TAB_TITLES = { dashboard: { t: "Dashboard", s: "Watchlist · candele 5m · forecast multi-tool" }, screener: { t: "Home", s: "Panoramica segnali su tutto il watchlist" }, watchlist: { t: "Watchlist", s: "Le tue liste personali · prezzo e variazione giornaliera in tempo reale" }, calendar: { t: "Calendario", s: "Earnings, eventi Fed, macro" }, news: { t: "News", s: "Agenda manuale dei catalizzatori · lanci, prodotti, IPO · digest Telegram del lunedì" }, sectors: { t: "Settori", s: "SPDR sector ETFs · momentum · alert Telegram" }, premarket: { t: "Pre-market", s: "Gap-and-Crap detector · futures · score 0-100" }, picks: { t: "Top Picks", s: "Stock Discovery · 5 factor quant + catalyst AI · crescita attesa prossimo mese (~30gg)" }, chain: { t: "Chain", s: "Lead-lag · quando A sale, i follower seguono 1-5gg · corr returns + Granger" }, report: { t: "Report", s: "Report di sintesi più recenti" }, accuracy: { t: "Accuratezza", s: "Previsioni verificate vs reale · dir-accuracy, bias, salute dati" }, simulator: { t: "Simulatore", s: "Paper-trading settimanale · denaro fittizio · vs buy&hold/SPY" }, sleeves: { t: "Sleeves", s: "Forward-test pre-registrato · 4 strategie meccaniche · 12 settimane vs €90/sett" }, evaluate: { t: "Evaluate", s: "Valuta i CSV esportati dei tre tool" }, config: { t: "Configurazione", s: "Tool attivi, visibilità, JSON editor" }, logs: { t: "Log", s: "Output del daemon · refresh ogni 30s" }, costs: { t: "Costi", s: "Costi API a pagamento (Anthropic + Perplexity) · per provider, mese, tool" }, admin: { t: "Utenti", s: "Gestione account e 2FA Telegram" }, }; // ── Risk Banner (Tool0) — striscia di stato rischio sempre visibile ─────────── const _RISK_REG = { neutral: { cls: "neutral", label: "NEUTRALE" }, elevated: { cls: "elevated", label: "RISCHIO ELEVATO" }, risk_off: { cls: "risk-off", label: "RISK-OFF" }, }; function RiskBanner({ dataLoaded }) { const [risk, setRisk] = useState(null); const [open, setOpen] = useState(false); useEffect(() => { if (!dataLoaded) return; const load = () => fetch("/api/tool0-risk").then(r => r.json()).then(setRisk).catch(() => {}); load(); const t = setInterval(load, 5 * 60 * 1000); return () => clearInterval(t); }, [dataLoaded]); if (!risk || !risk.regime) return null; const r = _RISK_REG[risk.regime] || _RISK_REG.neutral; const triggers = risk.triggers || []; return (
{open && (
{triggers.length === 0 ? Nessun segnale di rischio attivo. : triggers.map((t, i) => (
{t.label} {t.value != null && {String(t.value)}} {t.note && {t.note}}
))} {risk.portfolio && (
Portafoglio {risk.portfolio.top_sector_pct}% {risk.portfolio.top_sector} {risk.portfolio.mean_corr_1d != null && corr {risk.portfolio.mean_corr_1d}} {(risk.portfolio.notes || []).join(" · ")}
)}
)}
); } function AppShell({ user, tab, setTab, onLogout, sidebarCollapsed, setSidebarCollapsed, dataVersions }) { const [mobileOpen, setMobileOpen] = React.useState(false); const [activeSym, setActiveSym] = useState(null); const dataLoaded = (dataVersions?.dashboard || 0) > 0; const filteredNav = NAV.filter(n => !n.adminOnly || user.is_admin); const grouped = filteredNav.reduce((acc, n) => { (acc[n.group] = acc[n.group] || []).push(n); return acc; }, {}); const title = TAB_TITLES[tab] || { t: "BVF", s: "" }; return (
{/* Mobile backdrop */} {mobileOpen && (
setMobileOpen(false)}/> )} {/* SIDEBAR */} {/* MAIN */}

{title.t}

{title.s}
{tab === "dashboard" && } {tab === "screener" && { setActiveSym(sym); setTab("dashboard"); }}/>} {tab === "watchlist" && } {tab === "calendar" && } {tab === "sectors" && } {tab === "premarket" && } {tab === "picks" && } {tab === "chain" && } {tab === "report" && } {tab === "accuracy" && } {tab === "simulator" && } {tab === "sleeves" && } {tab === "news" && } {tab === "evaluate" && } {tab === "config" && } {tab === "logs" && } {tab === "costs" && user.is_admin && } {tab === "admin" && }
{/* Mobile bottom nav */}
); } // ── Alert Bell ──────────────────────────────────────────────────────────────── const _SIG_COLOR = { BUY: { color: "var(--green)", bg: "var(--green-soft)" }, STRONG_BUY: { color: "var(--green)", bg: "var(--green-soft)" }, SELL: { color: "var(--red)", bg: "var(--red-soft)" }, STRONG_SELL: { color: "var(--red)", bg: "var(--red-soft)" }, HOLD: { color: "var(--amber)", bg: "var(--amber-soft)" }, }; const _TOOL_LABEL = { tool2: "AI", tool3: "TA", tool4: "News" }; function AlertBell({ dataLoaded }) { const [alerts, setAlerts] = useState([]); const [unread, setUnread] = useState(0); const [open, setOpen] = useState(false); const ref = useRef(null); const fetchAlerts = () => { if (!dataLoaded) return; fetch("/api/alerts") .then(r => r.json()) .then(d => { setAlerts(d.alerts || []); setUnread(d.unread || 0); }) .catch(() => {}); }; useEffect(() => { fetchAlerts(); const t = setInterval(fetchAlerts, 5 * 60 * 1000); return () => clearInterval(t); }, [dataLoaded]); useEffect(() => { const close = e => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }; document.addEventListener("mousedown", close); return () => document.removeEventListener("mousedown", close); }, []); const handleToggle = () => { const next = !open; setOpen(next); if (next && unread > 0) { fetch("/api/alerts/mark-read", { method: "POST" }) .then(() => setUnread(0)) .catch(() => {}); } }; return (
{open && (
Segnali ultime 24h {unread > 0 && {unread} nuovi}
{alerts.length === 0 ?
Nessun segnale recente (esclusi HOLD).
: alerts.map((a, i) => { const sc = _SIG_COLOR[a.signal] || { color:"var(--text-muted)", bg:"var(--surface)" }; return (
{a.symbol} {(a.signal || "").replace(/_/g, " ")}
{_TOOL_LABEL[a.tool] || a.tool} {(a.timestamp || "").slice(11, 16)}
); }) }
)}
); } // ── Daemon Status ───────────────────────────────────────────────────────────── function DaemonStatus({ collapsed }) { const [running, setRunning] = useState(null); // null = caricamento const [onLinux, setOnLinux] = useState(false); const [busy, setBusy] = useState(false); const fetchStatus = () => { fetch("/api/status") .then(r => r.json()) .then(d => { setRunning(d.running); setOnLinux(d.on_linux); }) .catch(() => {}); }; useEffect(() => { fetchStatus(); const t = setInterval(fetchStatus, 30000); return () => clearInterval(t); }, []); const doAction = async (action) => { if (busy) return; setBusy(true); try { await fetch(`/api/${action}`, { method: "POST" }); await new Promise(r => setTimeout(r, 1500)); fetchStatus(); } finally { setBusy(false); } }; if (running === null) return null; return (
{running ? "In esecuzione" : "Fermo"} {onLinux && (
{running ? ( ) : ( )}
)}
); } function MarketPill() { const [now, setNow] = useState(new Date()); useEffect(() => { const t = setInterval(()=>setNow(new Date()), 1000); return () => clearInterval(t); }, []); // ET market hours (very rough) const etHour = (now.getUTCHours() - 4 + 24) % 24; const open = etHour >= 9 && etHour < 16 && now.getDay() >= 1 && now.getDay() <= 5; return (
{open ? "Mercato aperto" : "Chiuso"} {now.toLocaleTimeString("it-IT")}
); } // Badge freschezza dati per la tab attiva (legge window.HEALTH da /api/health). // "Mostra sempre con badge età" — non nasconde mai i dati, segnala solo l'età. const _TAB_TOOL = { dashboard: "tool1", screener: "tool1", sectors: "tool7", premarket: "tool8", picks: "tool9", chain: "tool10", calendar: "tool5", }; function TabFreshness({ tab }) { const key = _TAB_TOOL[tab]; const h = key && (window.HEALTH || {})[key]; if (!h) return null; const age = h.age_min; const label = age == null ? "n/d" : age < 90 ? `${age}min fa` : `${(age / 60).toFixed(1)}h fa`; const hasAnom = h.anomalies && h.anomalies.length > 0; const color = h.ok ? "var(--green)" : (h.fresh ? "var(--amber)" : "var(--red)"); const title = hasAnom ? h.anomalies.join("; ") : `Ultimo aggiornamento dati: ${label}`; return (
dati {label}
); } window.AppShell = AppShell; window.TAB_TITLES = TAB_TITLES; // ────────── Shared filter components (available to all views) ────────── // Custom dropdown — options as { value, label } array function FilterSelect({ value, onChange, options, placeholder = "Tutti", width }) { const [open, setOpen] = useState(false); const ref = useRef(null); useEffect(() => { const close = e => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }; document.addEventListener("mousedown", close); return () => document.removeEventListener("mousedown", close); }, []); const selected = options.find(o => o.value === value); const label = selected ? selected.label : placeholder; const isAll = !selected || selected.label === placeholder || value === "all"; return (
{open && (
{options.map(o => ( ))}
)}
); } // Mini-calendar date picker — mostra un calendario mensile stilizzato // availableDates: array di "YYYY-MM-DD" selezionabili (null = tutte le date disponibili) // showClear: mostra il pulsante "Tutte le date" per resettare function FilterCalendar({ value, onChange, availableDates, showClear = false, placeholder = "Tutte le date" }) { const isAll = !value || value === "all"; const initMonth = () => { const d = !isAll ? new Date(value + "T12:00:00") : (availableDates?.length ? new Date(availableDates[0] + "T12:00:00") : new Date()); return { y: d.getFullYear(), m: d.getMonth() }; }; const [open, setOpen] = useState(false); const [view, setView] = useState(initMonth); const ref = useRef(null); const availSet = new Set(availableDates || []); // Aggiorna il mese visualizzato quando cambia il valore selezionato useEffect(() => { if (!isAll) { const d = new Date(value + "T12:00:00"); setView({ y: d.getFullYear(), m: d.getMonth() }); } }, [value]); useEffect(() => { const close = e => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }; document.addEventListener("mousedown", close); return () => document.removeEventListener("mousedown", close); }, []); const fmtBtn = d => { if (!d || d === "all") return placeholder; return new Date(d + "T12:00:00").toLocaleDateString("it-IT", { weekday:"short", day:"2-digit", month:"short" }); }; const firstDay = new Date(view.y, view.m, 1); const lastDay = new Date(view.y, view.m + 1, 0); const startWd = (firstDay.getDay() + 6) % 7; // Lun=0 const monthLabel = firstDay.toLocaleDateString("it-IT", { month: "long", year: "numeric" }); const cells = []; for (let i = 0; i < startWd; i++) cells.push(null); for (let d = 1; d <= lastDay.getDate(); d++) cells.push(d); while (cells.length % 7 !== 0) cells.push(null); const makeKey = d => `${view.y}-${String(view.m+1).padStart(2,"0")}-${String(d).padStart(2,"0")}`; const today = new Date(); const todayKey = `${today.getFullYear()}-${String(today.getMonth()+1).padStart(2,"0")}-${String(today.getDate()).padStart(2,"0")}`; const prevMonth = () => { const d = new Date(view.y, view.m - 1, 1); setView({ y: d.getFullYear(), m: d.getMonth() }); }; const nextMonth = () => { const d = new Date(view.y, view.m + 1, 1); setView({ y: d.getFullYear(), m: d.getMonth() }); }; const pickDay = (d) => { const key = makeKey(d); if (availableDates && !availSet.has(key)) return; onChange(key); setOpen(false); }; return (
{open && (
{/* Intestazione mese */}
{monthLabel}
{/* Pulsante "Tutte le date" */} {showClear && ( )} {/* Griglia */}
{["L","M","M","G","V","S","D"].map((d, i) => ( {d} ))} {cells.map((d, i) => { if (!d) return ; const key = makeKey(d); const avail = !availableDates || availSet.has(key); const selected = key === value; const isToday = key === todayKey; return ( ); })}
)}
); } // Alias per retrocompatibilità (non più usato ma evita errori se rimane riferimento) const FilterDateChips = FilterCalendar; // Styled date input — opens native date picker from a custom button function FilterDateInput({ value, onChange }) { const ref = useRef(null); return (
); }