// Top-level App: auth check on mount + data loading + shell routing const TWEAK_DEFAULS = /*EDITMODE-BEGIN*/{ "startScreen": "Home", "density": "Comoda", "accentPalette": ["#6ea8fe","#74cfd8","#ffaa66"], "showConvergence": true, "sidebarCollapsed": false }/*EDITMODE-END*/; // ── Data loading ────────────────────────────────────────────────────────────── // Core (veloce): dashboard + calendario — caricati subito al login/refresh async function _loadCoreData() { try { const [dd, ce, hh] = await Promise.all([ fetch("/api/dashboard-data").then(r => r.json()), fetch("/api/calendar-events").then(r => r.json()), fetch("/api/health").then(r => r.json()).catch(() => ({})), ]); if (dd.all_data) { window.ALL_DATA = dd.all_data; window.WATCHLIST = dd.watchlist || []; window.DASH_SHOW = dd.show || {}; // visibilità tool nei grafici } if (ce.events) { window.CALENDAR_EVENTS = ce.events; } // Salute dati per badge freschezza in topbar: mappa per-tool window.HEALTH = (hh.health || []).reduce((m, h) => { m[h.key] = h; return m; }, {}); } catch(e) { console.warn("[app] loadCoreData:", e); } } // Lazy: report — caricato solo alla prima visita della tab Report async function _loadReportData() { try { const rd = await fetch("/api/report-data").then(r => r.json()); if (rd.by_date) window.REPORT_DATA_REAL = rd.by_date; } catch(e) { console.warn("[app] loadReportData:", e); } } // Alias per retrocompatibilità (login usa questo) const _loadAppData = _loadCoreData; // ── App ─────────────────────────────────────────────────────────────────────── function App() { const [tweaks, setTweak] = useTweaks(TWEAK_DEFAULS); // Apply tweaks → CSS vars useEffect(() => { const root = document.documentElement; const accent = tweaks.accentPalette?.[0] || "#6ea8fe"; const teal = tweaks.accentPalette?.[1] || "#74cfd8"; root.style.setProperty("--accent", accent); root.style.setProperty("--accent-soft", hexA(accent, 0.16)); root.style.setProperty("--teal", teal); root.style.setProperty("--teal-soft", hexA(teal, 0.16)); if (tweaks.density === "Compatta") root.style.fontSize = "12.5px"; else if (tweaks.density === "Ampia") root.style.fontSize = "14.5px"; else root.style.fontSize = ""; document.body.dataset.hideConvergence = tweaks.showConvergence ? "false" : "true"; }, [tweaks.accentPalette, tweaks.density, tweaks.showConvergence]); // Auth state — resolved via cookie check on mount const [user, setUser] = useState(null); const [dataReady, setDataReady] = useState(false); const [authChecked, setAuthChecked] = useState(false); // hide everything until first check done // Check existing session on mount (httpOnly cookie → /api/user) useEffect(() => { fetch("/api/user") .then(r => r.ok ? r.json() : null) .then(async (data) => { if (data?.user) { setUser(data.user); await _loadAppData(); setDataReady(true); } }) .catch(() => {}) .finally(() => setAuthChecked(true)); }, []); // Tab persisted in URL hash const initialTab = (() => { try { const h = window.location.hash.replace("#", ""); if (h && TAB_TITLES[h]) return h; } catch {} return tweaks.startScreen === "Dashboard" ? "dashboard" : "screener"; })(); const [tab, setTabRaw] = useState(initialTab); const setTab = (id) => { setTabRaw(id); try { window.history.replaceState(null, "", "#" + id); } catch {} }; const [sidebarCollapsed, setSidebarCollapsed] = useState(!!tweaks.sidebarCollapsed); useEffect(() => { setSidebarCollapsed(!!tweaks.sidebarCollapsed); }, [tweaks.sidebarCollapsed]); // Lazy load: report viene caricato solo alla prima visita della tab. // dataVersions forza il re-render delle view dopo che i dati arrivano. const [dataVersions, setDataVersions] = useState({ dashboard: 0, report: 0 }); const lazyLoaded = useRef({ report: false }); useEffect(() => { if (!dataReady) return; if (tab === "report" && !lazyLoaded.current.report) { lazyLoaded.current.report = true; _loadReportData().then(() => setDataVersions(v => ({ ...v, report: v.report + 1 }))); } }, [tab, dataReady]); // Dashboard refresh: esposto su window per il bottone "Aggiorna" + auto-refresh ogni 1 min const bumpDashboard = React.useCallback(() => _loadCoreData().then(() => setDataVersions(v => ({ ...v, dashboard: v.dashboard + 1 }))) , []); useEffect(() => { if (!dataReady) return; window.refreshDashboard = bumpDashboard; const id = setInterval(bumpDashboard, 60 * 1000); // auto-refresh ogni 1 min return () => { clearInterval(id); window.refreshDashboard = null; }; }, [dataReady, bumpDashboard]); const handleLogin = async (u) => { // u viene dalla risposta API (già autenticato, cookie impostato dal server) await _loadAppData(); setUser(u); setDataReady(true); // al login manuale parti SEMPRE dalla schermata iniziale scelta (default Home), // ignorando un eventuale #hash stantio rimasto nell'URL da una sessione precedente. setTab(tweaks.startScreen === "Dashboard" ? "dashboard" : "screener"); }; const handleLogout = () => { fetch("/api/logout", { method: "POST" }).catch(() => {}); setUser(null); setDataReady(false); }; // Attendi il check iniziale per evitare flash di login if (!authChecked) { return (
Caricamento…
); } return ( <> {!user ? ( ) : !dataReady ? (
Caricamento dati…
) : ( )} {window.TweaksPanel && ( setTweak("startScreen", v)}/> setTweak("sidebarCollapsed", v)}/> setTweak("density", v)}/> setTweak("accentPalette", v)}/> setTweak("showConvergence", v)}/> {user ? :
Non loggato.
}
)} ); } // Helpers: hex → rgba string (for var-soft variants) function hexA(hex, a) { if (!hex || !hex.startsWith("#")) return hex; let h = hex.slice(1); if (h.length === 3) h = h.split("").map(c => c+c).join(""); const r = parseInt(h.slice(0,2),16), g = parseInt(h.slice(2,4),16), b = parseInt(h.slice(4,6),16); return `rgba(${r}, ${g}, ${b}, ${a})`; } // Inject a CSS hook for hiding the convergence card const _styleHook = document.createElement("style"); _styleHook.textContent = ` body[data-hide-convergence="true"] .dash-rail > .fc-card:last-child { display: none !important; } `; document.head.appendChild(_styleHook); const root = ReactDOM.createRoot(document.getElementById("root")); root.render();