// ════════════════════════════════════════════════════════════════ // Symbiosis.DataHub — app entry // ════════════════════════════════════════════════════════════════ const { useState, useEffect } = React; const DEFAULT_TWEAKS = /*EDITMODE-BEGIN*/{ "density": "cozy", "accent": "bronze", "sparkW": 42, "focus": false, "actAsUserId": 1 }/*EDITMODE-END*/; function App() { const [route, setRoute] = useState(() => { const h = window.location.hash.replace('#', ''); return h || 'corpus'; }); const [theme, setTheme] = useState('light'); const [tweaks, setTweaks] = useState(DEFAULT_TWEAKS); const [tweaksOpen, setTweaksOpen] = useState(false); const [megaOpen, setMegaOpen] = useState(false); const [activeCluster, setActiveCluster] = useState(null); const [activeStartup, setActiveStartup] = useState(null); const [activeSynthesis, setActiveSynthesis] = useState(null); const [authState, setAuthState] = useState('in'); // 'in' | 'out' | 'invite' const [inviteToken, setInviteToken] = useState(null); // API hydration state (только если SDH_USE_API=true) const [hydrateState, setHydrateState] = useState(window.SDH_HYDRATE ? window.SDH_HYDRATE.state : 'ready'); const [hydrateTick, setHydrateTick] = useState(0); // force re-render при notify // ── API hydration startup (если SDH_USE_API=true) ─────────── useEffect(() => { if (!window.SDH_USE_API || !window.SDH_HYDRATE) return; // Subscribe на state changes hydrator'а const unsubscribe = window.SDH_HYDRATE.subscribe((newState) => { setHydrateState(newState); setHydrateTick(t => t + 1); if (newState === 'unauthenticated') setAuthState('out'); else if (newState === 'ready' && authState === 'out') setAuthState('in'); }); // Запустить hydration window.SDH_HYDRATE.runAll(); return unsubscribe; }, []); // eslint-disable-line // Current user from tweaks (acts as) ИЛИ из API const currentUser = React.useMemo(() => { if (window.SDH_USE_API && window.SDH_HYDRATE && window.SDH_HYDRATE.currentUser) { // В API mode currentUser приходит из /api/auth/me, role switcher отключён const apiUser = window.SDH_HYDRATE.currentUser; // Найти в users массиве чтобы добавить недостающие поля для UI const fullUser = (window.SDH_ADMIN.users || []).find(u => u.id === apiUser.id); return fullUser || { id: apiUser.id, email: apiUser.email, name: apiUser.name, role: apiUser.role, status: apiUser.status || 'active', token_label: apiUser.token_expires_at ? 'TTL' : 'бессрочный', }; } // Mock mode: role switcher из Tweaks const id = tweaks.actAsUserId || 1; return window.SDH_ADMIN.users.find(u => u.id === id) || window.SDH_ADMIN.users[0]; }, [tweaks.actAsUserId, hydrateTick]); // close mega on route change useEffect(() => { setMegaOpen(false); setActiveCluster(null); setActiveSynthesis(null); }, [route]); // sync theme to useEffect(() => { document.documentElement.setAttribute('data-theme', theme); }, [theme]); // sync route to hash useEffect(() => { window.location.hash = route; }, [route]); // Tweaks protocol useEffect(() => { function onMsg(e) { if (!e.data || typeof e.data !== 'object') return; if (e.data.type === '__activate_edit_mode') setTweaksOpen(true); if (e.data.type === '__deactivate_edit_mode') setTweaksOpen(false); } window.addEventListener('message', onMsg); window.parent.postMessage({ type: '__edit_mode_available' }, '*'); return () => window.removeEventListener('message', onMsg); }, []); function setTweak(key, value) { setTweaks(prev => { const next = { ...prev, [key]: value }; try { window.parent.postMessage({ type: '__edit_mode_set_keys', edits: { [key]: value } }, '*'); } catch (e) {} return next; }); } function closeTweaks() { setTweaksOpen(false); try { window.parent.postMessage({ type: '__edit_mode_dismissed' }, '*'); } catch (e) {} } // apply accent → CSS custom property useEffect(() => { const map = { bronze: { base: '#B8855D', hover: '#A6764F', deep: '#8E6440', subtle: '#EDD9C2' }, emerald: { base: '#6B8E5A', hover: '#5C7A4C', deep: '#4D673F', subtle: '#E3ECDD' }, rose: { base: '#B85A4A', hover: '#A04F40', deep: '#83422F', subtle: '#F4DBD5' }, }; const c = map[tweaks.accent] || map.bronze; const r = document.documentElement.style; r.setProperty('--brand-bronze', c.base); r.setProperty('--brand-bronze-hover', c.hover); r.setProperty('--brand-bronze-deep', c.deep); r.setProperty('--brand-bronze-subtle', c.subtle); }, [tweaks.accent]); useEffect(() => { document.documentElement.dataset.density = tweaks.density; }, [tweaks.density]); // ── Logout handler (API-aware) ────────────────────────────── async function handleLogout() { if (window.SDH_USE_API && window.SDH_HYDRATE) { try { await window.SDH_HYDRATE.logout(); } catch (_) {} } setAuthState('out'); } // ── API hydration loading state (только в API mode) ──────── if (window.SDH_USE_API && hydrateState === 'loading') { return (