/* QUANTA · Auth + Session - useAuth: per-tab session (sessionStorage) with login/logout + profile - useSessionWatcher: detects duplicate sessions across tabs via BroadcastChannel + kick-others action - Session log: append-only event log (LOGIN / LOGOUT / DUPLICATE / KICKED), stored in localStorage so all tabs see the same history - LoginScreen, DuplicateBanner, UserMenu, UserProfilePanel components This is a CLIENT-SIDE prototype of the auth flow. Wiring to a real server: – Replace useAuth with API calls (POST /login → JWT) – Replace BroadcastChannel with server-side session table + WS push – appendLog() should hit POST /audit-log instead of localStorage */ const { useState: useStateA, useEffect: useEffectA, useRef: useRefA, useMemo: useMemoA } = React; const SESSION_KEY = "quanta-session"; const LOG_KEY = "quanta-session-log"; const PROFILE_KEY_PREFIX = "quanta-profile:"; const CHANNEL_NAME = "quanta-sessions"; const HEARTBEAT_MS = 4000; const PEER_TIMEOUT_MS = 12000; function shortId(s) { return (s || "").slice(0, 6); } function getSessionLog() { try { return JSON.parse(localStorage.getItem(LOG_KEY) || "[]"); } catch { return []; } } function appendLog(entry) { const log = getSessionLog(); log.push({ ts: Date.now(), ...entry }); while (log.length > 200) log.shift(); try { localStorage.setItem(LOG_KEY, JSON.stringify(log)); } catch {} // Cross-tab notify try { window.dispatchEvent(new CustomEvent("quanta-log-update")); } catch {} } function loadProfile(user) { try { const raw = localStorage.getItem(PROFILE_KEY_PREFIX + user); if (!raw) return { kellyDivisor: 3, alertThreshold: 0.03 }; return { kellyDivisor: 3, alertThreshold: 0.03, ...JSON.parse(raw) }; } catch { return { kellyDivisor: 3, alertThreshold: 0.03 }; } } function saveProfile(user, profile) { try { localStorage.setItem(PROFILE_KEY_PREFIX + user, JSON.stringify(profile)); } catch {} } // Helper crypto-quality local ID (replaces Math.random()) function cryptoLocalId() { if (typeof crypto !== "undefined" && crypto.randomUUID) return crypto.randomUUID(); const arr = new Uint8Array(12); if (typeof crypto !== "undefined" && crypto.getRandomValues) crypto.getRandomValues(arr); else for (let i = 0; i < arr.length; i++) arr[i] = (Math.random() * 256) | 0; return Array.from(arr, b => b.toString(16).padStart(2, "0")).join(""); } function useAuth() { const [session, setSession] = useStateA(null); const [profile, setProfile] = useStateA(null); const [bootDone, setBootDone] = useStateA(false); /* Pull the server-side notif row right after the session restores so the dashboard's slider + Kelly buttons reflect what Telegram is actually using. Single source of truth: server's user_notif. */ const _hydrateFromServer = async (username) => { try { const r = await fetch("/api/notif/me", { credentials: "include" }); if (!r.ok) return loadProfile(username); const data = await r.json(); const notif = (data && data.notif) || {}; const base = loadProfile(username); return { ...base, kellyDivisor: notif.kelly_divisor ?? base.kellyDivisor, alertThreshold: (notif.threshold != null) ? Number(notif.threshold) : base.alertThreshold, }; } catch { return loadProfile(username); } }; // Au mount : essaie de restaurer la session via cookie useEffectA(() => { let cancelled = false; fetch("/api/auth/me", { credentials: "include" }) .then(async (r) => { if (cancelled) return; if (r.ok) { const { user } = await r.json(); const sess = { user: user.username, role: user.role, sessionId: cryptoLocalId(), since: Date.now() }; setSession(sess); const p = await _hydrateFromServer(user.username); if (!cancelled) setProfile(p); appendLog({ kind: "LOGIN_RESTORED", user: user.username, sessionId: sess.sessionId }); } }) .catch(() => { /* offline ou serveur down */ }) .finally(() => { if (!cancelled) setBootDone(true); }); return () => { cancelled = true; }; }, []); const login = async (username, password) => { try { const r = await fetch("/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ username, password }), }); if (r.status === 429) { const j = await r.json().catch(() => ({})); return { ok: false, error: j.detail || "Trop de tentatives. Réessaie plus tard." }; } if (!r.ok) { return { ok: false, error: "Identifiants invalides" }; } const { user } = await r.json(); const sess = { user: user.username, role: user.role, sessionId: cryptoLocalId(), since: Date.now() }; setSession(sess); const p = await _hydrateFromServer(user.username); setProfile(p); appendLog({ kind: "LOGIN", user: user.username, sessionId: sess.sessionId }); return { ok: true }; } catch (e) { return { ok: false, error: "Serveur injoignable" }; } }; const logout = async () => { if (session) appendLog({ kind: "LOGOUT", user: session.user, sessionId: session.sessionId }); try { await fetch("/api/auth/logout", { method: "POST", credentials: "include" }); } catch {} setSession(null); setProfile(null); }; /* Debounced sync of shared fields → server's user_notif row. - kellyDivisor / alertThreshold both gate the Telegram dispatcher AND the in-app alerts panel, so they MUST be unified. - The slider can fire dozens of changes per drag; a 350ms debounce coalesces them into a single POST when the user pauses. */ const _syncTimerRef = useRefA(null); const _syncPendingRef = useRefA({}); const _flushSync = () => { const patch = _syncPendingRef.current; _syncPendingRef.current = {}; if (!patch || Object.keys(patch).length === 0) return; const body = {}; if ("kellyDivisor" in patch) body.kelly_divisor = patch.kellyDivisor; if ("alertThreshold" in patch) body.threshold = patch.alertThreshold; if (Object.keys(body).length === 0) return; fetch("/api/notif/me", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify(body), }).catch(() => { /* swallow — local state stays as the user set it */ }); }; const _queueSync = (patch) => { Object.assign(_syncPendingRef.current, patch); if (_syncTimerRef.current) clearTimeout(_syncTimerRef.current); _syncTimerRef.current = setTimeout(_flushSync, 350); }; const updateProfile = (patch) => { if (!session) return; const next = { ...profile, ...patch }; saveProfile(session.user, next); setProfile(next); // Fields that also live server-side → debounced POST. Other patch // keys (purely local prefs) skip the sync. const serverFields = {}; if ("kellyDivisor" in patch) serverFields.kellyDivisor = patch.kellyDivisor; if ("alertThreshold" in patch) serverFields.alertThreshold = patch.alertThreshold; if (Object.keys(serverFields).length) _queueSync(serverFields); }; return { session, profile, login, logout, updateProfile, isAuthed: !!session, bootDone }; } function useSessionWatcher(session, onKicked) { const [duplicate, setDuplicate] = useStateA(null); const peersRef = useRefA(new Map()); useEffectA(() => { if (!session) { setDuplicate(null); return; } let channel; try { channel = new BroadcastChannel(CHANNEL_NAME); } catch (e) { console.warn("[auth] BroadcastChannel unavailable", e); return; } const peers = peersRef.current; peers.clear(); const checkDup = () => { const now = Date.now(); for (const [sid, ts] of peers.entries()) { if (now - ts > PEER_TIMEOUT_MS) peers.delete(sid); } if (peers.size > 0) { const first = [...peers.keys()][0]; setDuplicate(d => (d && d.otherSessionId === first) ? d : { otherSessionId: first, since: now }); } else { setDuplicate(null); } }; const announce = (type) => { channel.postMessage({ type, user: session.user, sessionId: session.sessionId, ts: Date.now(), }); }; channel.onmessage = (ev) => { const msg = ev.data; if (!msg || msg.user !== session.user) return; if (msg.sessionId === session.sessionId) return; if (msg.type === "presence" || msg.type === "hello") { const wasNew = !peers.has(msg.sessionId); peers.set(msg.sessionId, msg.ts); if (wasNew) { appendLog({ kind: "DUPLICATE_DETECTED", user: session.user, sessionId: session.sessionId, info: `peer=${shortId(msg.sessionId)}`, }); announce("hello"); // tell the other tab we're here too } checkDup(); } else if (msg.type === "leave") { peers.delete(msg.sessionId); checkDup(); } else if (msg.type === "kick-all") { // Another tab issued a kick — if it's us being kicked, exit. appendLog({ kind: "KICKED", user: session.user, sessionId: session.sessionId, info: `by=${shortId(msg.sessionId)}`, }); onKicked && onKicked(); } }; announce("hello"); const id = setInterval(() => announce("presence"), HEARTBEAT_MS); const cleanup = setInterval(checkDup, 5000); const onBeforeUnload = () => { try { announce("leave"); } catch {} }; window.addEventListener("beforeunload", onBeforeUnload); return () => { try { announce("leave"); } catch {} window.removeEventListener("beforeunload", onBeforeUnload); clearInterval(id); clearInterval(cleanup); channel.close(); peers.clear(); }; }, [session, onKicked]); const kickOthers = () => { if (!session) return; try { const ch = new BroadcastChannel(CHANNEL_NAME); ch.postMessage({ type: "kick-all", user: session.user, sessionId: session.sessionId, ts: Date.now(), }); ch.close(); } catch {} appendLog({ kind: "KICKED_OTHERS", user: session.user, sessionId: session.sessionId, }); setDuplicate(null); peersRef.current.clear(); }; return { duplicate, kickOthers }; } /* ────────────────────────────────────────────────────────── LOGIN SCREEN ────────────────────────────────────────────────────────── */ function QLoginScreen({ onLogin }) { const [user, setUser] = useStateA(""); const [pwd, setPwd] = useStateA(""); const [error, setError] = useStateA(""); const [busy, setBusy] = useStateA(false); const submit = async (e) => { e && e.preventDefault(); const u = user.trim(); if (!u) { setError("Identifiant requis"); return; } if (!pwd) { setError("Mot de passe requis"); return; } setError(""); setBusy(true); const res = await onLogin(u, pwd); if (!res || !res.ok) { setError((res && res.error) || "Échec de connexion"); setBusy(false); } // sinon le parent change d'écran ; on ne reset pas busy }; return (
CONNEXION SÉCURISÉE QUANTA · ODDS · LIVE v4.0 · BUILD 2026.05
Quanta
QUANTA
SESSION · NEW
Bienvenue.
Authentifie-toi pour accéder aux cotes en direct. Tant que la session n'est pas active, aucune donnée n'est diffusée.
{error &&
{error}
}
setUser(e.target.value)} placeholder="ex: client-jean" disabled={busy} />
setPwd(e.target.value)} placeholder="••••••••" disabled={busy} />
); } /* ────────────────────────────────────────────────────────── DUPLICATE BANNER ────────────────────────────────────────────────────────── */ function QDuplicateBanner({ duplicate, onKickOthers }) { if (!duplicate) return null; return (
!
Session multiple détectée — ton compte est ouvert sur un autre onglet ou appareil. peer #{shortId(duplicate.otherSessionId)} · enregistré dans le journal
); } /* ────────────────────────────────────────────────────────── USER MENU (dropdown in header) ────────────────────────────────────────────────────────── */ function QUserMenu({ session, profile, onOpenProfile, onOpenLog, onOpenAdmin, onLogout }) { const [open, setOpen] = useStateA(false); const ref = useRefA(null); useEffectA(() => { if (!open) return; const onClick = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }; document.addEventListener("mousedown", onClick); return () => document.removeEventListener("mousedown", onClick); }, [open]); const initial = (session.user[0] || "?").toUpperCase(); return (
{open && (
{session.user}
session #{shortId(session.sessionId)} · kelly /{profile.kellyDivisor}
{session.role === "admin" && ( )}
)}
); } /* ────────────────────────────────────────────────────────── USER PROFILE PANEL (slide-in) ────────────────────────────────────────────────────────── */ function QProfilePanel({ open, onClose, session, profile, updateProfile, visibleBMs, toggleVisibleBM, bipeEnabled, setBipeEnabled }) { const closeRef = useRefA(null); useEffectA(() => { if (!open) return; const onKey = (e) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [open, onClose]); // Focus trap: transfer focus to the close button when the panel opens // so keyboard / screen-reader users land inside the dialog. useEffectA(() => { if (open && closeRef.current) closeRef.current.focus(); }, [open]); if (!session || !profile) return null; const sinceMin = Math.round((Date.now() - session.since) / 60000); return ( <>
); } /* ────────────────────────────────────────────────────────── PER-USER BOOKMAKER PREFERENCES Two distinct settings, two storage paths on purpose: * Telegram-notification filter: persisted server-side in `user_notif.bookmakers` (CSV). Survives device changes, shared across devices logged in as the same account. Edited via POST /api/notif/me. * Dashboard visibility: persisted in localStorage, keyed by username. Per-device by design — most operators sit at the same laptop and may want to hide books they never use without affecting their account-wide notification filter. A separate "Dashboard bookmakers" section in the profile panel surfaces the toggles. Both settings share the same constant ALL_BMS_LIST so the UI stays consistent — adding a new BM requires touching this single line. ────────────────────────────────────────────────────────── */ const ALL_BMS_LIST = ["Betclic", "Winamax", "Yonibet", "PMU", "Unibet"]; const ALL_SPORTS_LIST = [ { value: "tennis", label: "Tennis" }, { value: "football", label: "Football" }, ]; function _localKey(username) { return username ? `quanta-visible-bms-${username}` : null; } function useVisibleBMs(username) { const key = _localKey(username); const [visible, setVisible] = useStateA(() => { if (!key) return new Set(ALL_BMS_LIST); try { const raw = localStorage.getItem(key); if (!raw) return new Set(ALL_BMS_LIST); const arr = JSON.parse(raw); if (!Array.isArray(arr)) return new Set(ALL_BMS_LIST); const known = arr.filter(b => ALL_BMS_LIST.includes(b)); // An empty selection is almost always a mistake — restore all BMs // rather than render an empty dashboard. return new Set(known.length ? known : ALL_BMS_LIST); } catch { return new Set(ALL_BMS_LIST); } }); const toggle = (bm) => { setVisible(prev => { const next = new Set(prev); if (next.has(bm)) next.delete(bm); else next.add(bm); if (key) { try { localStorage.setItem(key, JSON.stringify([...next])); } catch {} } return next; }); }; return [visible, toggle]; } /* DashboardBookmakers — toggle list rendered inside QProfilePanel. No network calls: state is owned by useVisibleBMs at the QuantaAppAuthed level and threaded down here for editing. */ function DashboardBookmakers({ visibleBMs, toggleVisibleBM }) { return ( <>
{ALL_BMS_LIST.map(bm => { const on = visibleBMs.has(bm); return ( ); })}
Décoche les bookmakers que tu ne veux PAS voir dans les cartes / tableaux du dashboard. Préférence locale (par appareil) — n'affecte pas les notifications.
); } /* ────────────────────────────────────────────────────────── NOTIF SETTINGS — Telegram link UI inside the profile panel. Gracefully degrades when the /api/notif endpoints aren't available (the backend worker is shipping in parallel). ────────────────────────────────────────────────────────── */ function NotifSettings({ session }) { const EMPTY = { loading: true, available: true, linked: false, chat_id: null, threshold: null, sports: "", bookmakers: "", }; const [state, setState] = useStateA(EMPTY); const [linking, setLinking] = useStateA(false); const [linkPayload, setLinkPayload] = useStateA(null); // { bot_url, token } const _unavailable = () => setState({ ...EMPTY, loading: false, available: false }); // Server wraps the row in `{notif: {...}}`. Be defensive: a future // un-wrapping or partial response shouldn't blank the UI. const _absorbNotif = (rawJson) => { const notif = (rawJson && rawJson.notif) || rawJson || {}; setState({ loading: false, available: true, linked: !!notif.telegram_chat_id, chat_id: notif.telegram_chat_id || null, threshold: notif.threshold ?? null, sports: notif.sports ?? "", bookmakers: notif.bookmakers ?? "", }); }; const refresh = async () => { try { const r = await fetch("/api/notif/me", { credentials: "include" }); if (r.status === 404 || r.status === 501 || !r.ok) { _unavailable(); return; } _absorbNotif(await r.json()); } catch { _unavailable(); } }; useEffectA(() => { refresh(); }, [session && session.user]); const startLink = async () => { setLinking(true); try { const r = await fetch("/api/notif/telegram/link", { method: "POST", credentials: "include" }); if (!r.ok) { setLinking(false); return; } const data = await r.json(); setLinkPayload({ bot_url: data.bot_url, token: data.token }); } catch { /* swallow */ } setLinking(false); }; const unlink = async () => { try { await fetch("/api/notif/telegram/unlink", { method: "POST", credentials: "include" }); } catch {} setLinkPayload(null); refresh(); }; /* Persist a partial settings patch (`{bookmakers: "..."}` or `{sports: "..."}`) via POST /api/notif/me. Optimistic UI: we set local state immediately so the checkbox flips without waiting on the round-trip, then reconcile with the row the server returns. A failed POST silently leaves the optimistic value in place — next `refresh()` will correct it. */ const persist = async (patch) => { setState(prev => ({ ...prev, ...patch })); try { const r = await fetch("/api/notif/me", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify(patch), }); if (r.ok) { _absorbNotif(await r.json()); } } catch { /* keep optimistic state */ } }; const _csvToSet = (s) => new Set(String(s || "").split(",").map(x => x.trim()).filter(Boolean)); const _setToCsv = (set) => [...set].join(","); const toggleBm = (bm) => { const next = _csvToSet(state.bookmakers); if (next.has(bm)) next.delete(bm); else next.add(bm); persist({ bookmakers: _setToCsv(next) }); }; const toggleSport = (sp) => { const next = _csvToSet(state.sports); if (next.has(sp)) next.delete(sp); else next.add(sp); persist({ sports: _setToCsv(next) }); }; if (state.loading) { return
Chargement…
; } if (!state.available) { return (
Telegram pas encore configuré côté serveur.
Cette section s'activera dès que les notifs seront déployées.
); } const bmSet = _csvToSet(state.bookmakers); const spSet = _csvToSet(state.sports); /* When linked, surface the per-account filters (BMs + sports) so the user can curate which spike events the Telegram bot pushes to them. These are persisted server-side so they're shared across devices — a deliberate split from the dashboard-visibility toggle below, which is local-only. */ const filtersBlock = ( <>
Bookmakers (notifications)
{ALL_BMS_LIST.map(bm => { const on = bmSet.has(bm); return ( ); })}
Seuls les value-bets sur les bookmakers cochés déclenchent une notification.
Tout décocher = tous les bookmakers (équivalent à tout cocher).
Sports (notifications)
{ALL_SPORTS_LIST.map(sp => { const on = spSet.has(sp.value); return ( ); })}
Décoche les sports que tu ne veux pas recevoir en notifications.
Tout décocher = tous les sports (équivalent à tout cocher).
); if (state.linked) { return ( <>
✓ Notifications Telegram actives
Tu reçois les value-bets ≥ seuil défini en push.
{filtersBlock} ); } // Not linked yet return ( <>
Notifications Telegram non liées
Lie ton compte Telegram pour recevoir les value-bets en push, sans garder l'écran ouvert.
{linkPayload && linkPayload.bot_url ? ( <> {linkPayload.bot_url} {linkPayload.token && (
Code : {linkPayload.token}
)}
) : (
)} {/* Pre-pair the filters so a freshly linked friend doesn't have to dig back in — the row already exists in DB (lazy-materialised on first /api/notif/me hit) with the default 5 BMs + 2 sports. */} {filtersBlock} ); } /* ────────────────────────────────────────────────────────── SESSION LOG PANEL (slide-in, read-only) ────────────────────────────────────────────────────────── */ function QSessionLogPanel({ open, onClose }) { const [entries, setEntries] = useStateA(() => getSessionLog()); const closeRef = useRefA(null); useEffectA(() => { if (!open) return; const refresh = () => setEntries(getSessionLog()); refresh(); window.addEventListener("quanta-log-update", refresh); // Listen to storage events for cross-tab sync window.addEventListener("storage", refresh); const id = setInterval(refresh, 3000); return () => { window.removeEventListener("quanta-log-update", refresh); window.removeEventListener("storage", refresh); clearInterval(id); }; }, [open]); useEffectA(() => { if (!open) return; const onKey = (e) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [open, onClose]); // Focus trap: send focus to the close button when the panel opens. useEffectA(() => { if (open && closeRef.current) closeRef.current.focus(); }, [open]); const sorted = [...entries].reverse(); return ( <>
); } /* ────────────────────────────────────────────────────────── BOOT SPLASH — shown while /api/auth/me is in flight ────────────────────────────────────────────────────────── */ function QBootSplash() { return (
CONNECTING…
); } /* ────────────────────────────────────────────────────────── INVITE / SETUP SCREEN The friend opens /?invite=. We: 1. Resolve the token via GET /api/auth/setup-info (returns username) 2. Show "Bienvenue , choisis ton mot de passe" form 3. POST /api/auth/setup → server consumes token, sets pwd, returns user + sets the session cookie → friend is logged in immediately. QuantaApp picks this screen instead of QLoginScreen when the query string carries `?invite=` (see quanta-app.jsx). ────────────────────────────────────────────────────────── */ function QSetupScreen({ token, onLoggedIn }) { const [phase, setPhase] = useStateA("loading"); const [username, setUsername] = useStateA(""); const [expiresAt, setExpiresAt] = useStateA(null); const [pwd, setPwd] = useStateA(""); const [pwd2, setPwd2] = useStateA(""); const [error, setError] = useStateA(""); // Resolve token on mount useEffectA(() => { let cancelled = false; fetch(`/api/auth/setup-info?token=${encodeURIComponent(token)}`, { credentials: "include" }) .then(async (r) => { if (cancelled) return; if (!r.ok) { setError("Lien d'invitation invalide ou expiré."); setPhase("ready"); return; } const data = await r.json(); setUsername(data.username); setExpiresAt(data.expires_at); setPhase("ready"); }) .catch(() => { if (cancelled) return; setError("Serveur injoignable. Réessaie dans un instant."); setPhase("ready"); }); return () => { cancelled = true; }; }, [token]); const submit = async (e) => { e && e.preventDefault(); if (!pwd || pwd.length < 8) { setError("Mot de passe trop court (8 caractères minimum)."); return; } if (pwd !== pwd2) { setError("Les deux mots de passe ne correspondent pas."); return; } setError(""); setPhase("submitting"); try { const r = await fetch("/api/auth/setup", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ token, password: pwd }), }); if (r.status === 429) { const j = await r.json().catch(() => ({})); setError(j.detail || "Trop de tentatives. Réessaie plus tard."); setPhase("ready"); return; } if (!r.ok) { const j = await r.json().catch(() => ({})); setError(j.detail || "Lien d'invitation invalide ou expiré."); setPhase("ready"); return; } const { user } = await r.json(); setPhase("done"); // Drop the ?invite=... from the URL so a refresh doesn't reuse a now-dead // token; parent re-evaluates auth state (cookie is set server-side). try { const url = new URL(location.href); url.searchParams.delete("invite"); history.replaceState(null, "", url.toString()); } catch {} onLoggedIn && onLoggedIn(user); } catch (e) { setError("Serveur injoignable."); setPhase("ready"); } }; if (phase === "loading") { return (
VERIFICATION DU LIEN…
); } if (!username) { return (
LIEN INVALIDE
Quanta
QUANTA
Lien expiré ou déjà utilisé.
Demande à l'admin de générer une nouvelle invitation.
{error &&
{error}
}
); } const expDate = expiresAt ? new Date(expiresAt * 1000) : null; return (
INVITATION QUANTA {expDate && EXPIRE {expDate.toLocaleDateString()} {expDate.toLocaleTimeString([], {hour:"2-digit", minute:"2-digit"})} }
Quanta
QUANTA
SETUP
Bienvenue, {username}.
Choisis ton mot de passe — c'est celui que tu utiliseras à chaque connexion. Minimum 8 caractères, idéalement long et inhabituel.
{error &&
{error}
}
setPwd(e.target.value)} placeholder="••••••••" disabled={phase === "submitting"} />
setPwd2(e.target.value)} placeholder="••••••••" disabled={phase === "submitting"} />
); } Object.assign(window, { useAuth, useSessionWatcher, appendLog, getSessionLog, shortId, QLoginScreen, QSetupScreen, QDuplicateBanner, QUserMenu, QProfilePanel, QSessionLogPanel, QBootSplash, cryptoLocalId, useVisibleBMs, DashboardBookmakers, ALL_BMS_LIST, ALL_SPORTS_LIST, });