/* Home dashboard — real API data */ const GREETINGS = { morning: ['Доброе утро, Шеф.', 'Good morning, Chef.', 'Guten Morgen, Chef.', 'Bonjour, Chef.'], afternoon: ['Добрый день, Шеф.', 'Good afternoon, Chef.', 'Servus, Chef.', 'Bonjour, Chef.'], evening: ['Добрый вечер, Шеф.', 'Good evening, Chef.', 'Guten Abend, Chef.', 'Bonsoir, Chef.'], night: ['Доброй ночи, Шеф.', 'Good night, Chef.', 'Gute Nacht, Chef.', 'Bonne nuit, Chef.'], }; function timeOfDay(h) { if (h >= 6 && h < 12) return 'morning'; if (h >= 12 && h < 17) return 'afternoon'; if (h >= 17 && h < 23) return 'evening'; return 'night'; } function GreetingPreloader({ phase, tod }) { const reelRef = React.useRef(null); const contentRef = React.useRef(null); // Reel: scroll through the 4 phrases, ending on RU. React.useEffect(() => { const reel = reelRef.current; if (!reel) return; reel.style.transition = 'none'; reel.style.transform = 'translateY(-90px)'; // 3 lines × 30px → last phrase visible requestAnimationFrame(() => { requestAnimationFrame(() => { reel.style.transition = 'transform 3.6s cubic-bezier(0.16, 1, 0.3, 1)'; reel.style.transform = 'translateY(0)'; }); }); }, []); // FLIP: when settling, translate the reel container to the live greeting position. React.useEffect(() => { if (phase !== 'settle') return; const c = contentRef.current; const target = document.querySelector('.hero-greeting .hg-title .greet'); if (!c || !target) return; const cRect = c.getBoundingClientRect(); const tRect = target.getBoundingClientRect(); const dx = tRect.left - cRect.left; const dy = tRect.top - cRect.top; requestAnimationFrame(() => { c.style.transition = 'transform 0.6s cubic-bezier(0.4, 0, 0.2, 1)'; c.style.transform = `translate(${dx}px, ${dy}px)`; }); }, [phase]); const phrases = GREETINGS[tod]; return (
{phrases.map((p, i) => (
{p}
))}
); } function HomeMode({ setMode, setInboxState, setChatState }) { const statusQ = useApi(() => API.status(), []); const inboxQ = useApi(() => API.inbox(), []); const [now, setNow] = React.useState(() => new Date()); const [preloadPhase, setPreloadPhase] = React.useState(() => { try { return sessionStorage.getItem('home_preload_done') ? 'gone' : 'reel'; } catch (e) { return 'reel'; } }); const greetRef = React.useRef(null); // FLIP target React.useEffect(() => { const t = setInterval(() => setNow(new Date()), 1000); return () => clearInterval(t); }, []); // Preloader phase machine: reel (4s) → settle → gone. React.useEffect(() => { if (preloadPhase === 'reel') { const t = setTimeout(() => setPreloadPhase('settle'), 4000); return () => clearTimeout(t); } if (preloadPhase === 'settle') { const t = setTimeout(() => { try { sessionStorage.setItem('home_preload_done', '1'); } catch (e) {} setPreloadPhase('gone'); }, 650); return () => clearTimeout(t); } }, [preloadPhase]); const hh = String(now.getHours()).padStart(2, '0'); const mm = String(now.getMinutes()).padStart(2, '0'); const tod = timeOfDay(now.getHours()); const greeting = GREETINGS[tod][0]; // RU only — no rotation in normal mode. const projects = statusQ.data?.projects || []; const health = statusQ.data?.health || {}; const ultimate = statusQ.data?.ultimate_priority; const inboxItems = inboxQ.data?.items || []; const inboxCount = inboxQ.data?.count || 0; const activeProjects = projects.filter(p => p.status === 'active' && !p.archived); const pausedCount = projects.filter(p => p.status === 'paused').length; // Highest-priority active project = focus const focusProj = activeProjects.slice().sort((a, b) => b.priority - a.priority)[0] || null; // Fetch focus project detail (arcs) const focusQ = useApi(() => focusProj ? API.project(focusProj.name) : Promise.resolve(null), [focusProj?.name]); const focusArcs = focusQ.data?.arcs || []; const activeArc = focusArcs.find(a => a.status === 'active' || a.status === 'awaiting_user'); const doneArcs = focusArcs.filter(a => a.status === 'done').length; const totalArcs = focusArcs.length; const arcPct = totalArcs > 0 ? Math.round(doneArcs / totalArcs * 100) : 0; const dotSt = (ok) => ok ? 'var(--ag-ok)' : 'var(--ag-fail)'; const allOk = health.M1 && health.router && health.embedder; const anyOk = health.M1 || health.router || health.embedder; const healthLabel = allOk ? 'all systems nominal' : anyOk ? '1+ service down' : 'all services down'; // Dot color hint for inbox items by type const dotHint = (type) => { if (type?.includes('consilium')) return 'var(--ag-info)'; if (type?.includes('todo')) return 'var(--ag-warn)'; if (type?.includes('linter')) return 'var(--ag-fail)'; return 'var(--ag-warn)'; }; const goInboxItem = (id) => { setInboxState({ view: 'detail', id }); setMode('inbox'); }; const loading = statusQ.loading && inboxQ.loading; return (
{preloadPhase !== 'gone' && }
{/* Ultimate banner */} {ultimate && (
ULTIMATE PRIORITY
{ultimate.type === 'arc' ? `Фокус: ${ultimate.project} / ${ultimate.name}` : `Фокус: ${ultimate.name}`}
)} {/* Greeting */}
{hh}:{mm} · TODAY
{greeting}
{loading ? 'Загрузка...' : inboxCount > 0 ? `${inboxCount} ${inboxCount === 1 ? 'уведомление ждёт' : 'уведомлений ждут'} ответа. ${allOk ? 'Backend в норме.' : 'Есть проблемы с сервисами.'}` : `Inbox пуст. ${allOk ? 'Backend в норме.' : 'Есть проблемы с сервисами.'}`}
{/* Stat row */}
{inboxCount}
Inbox
{inboxCount > 0 ? 'pending' : 'пусто'}
{activeProjects.length}
Active proj
{pausedCount > 0 ? `${pausedCount} paused` : 'all active'}
{focusProj && activeArc ? <>
{arcPct}%
{focusProj.name.slice(0, 10)}
{doneArcs}/{totalArcs} arcs
: <>
No focus
нет проектов
}
{/* Current focus card */} {focusProj && (
Сейчас в работе
setMode('status')}>все проекты →
{focusQ.loading ?
:
{ setChatState({ view: 'chat', projectId: focusProj.name }); setMode('chat'); }} style={{ cursor: 'pointer' }}>
FOCUS · {focusProj.name}
{activeArc ? <>
{activeArc.title} {activeArc.status}
Arc {doneArcs + 1}/{totalArcs}
{doneArcs} done · {totalArcs - doneArcs} left open chat →
: <>
Нет активных арок
{focusProj.mode} · {focusProj.status}
начать чат →
}
}
)} {!focusProj && !statusQ.loading && (
Проекты
Нет активных проектов. Создайте проект через /newproject в боте.
)} {/* Quick actions */}
Быстрые действия
{/* Inbox preview */} {inboxCount > 0 && (
Свежее в inbox
setMode('inbox')}>open inbox →
{inboxItems.slice(0, 3).map(it => (
goInboxItem(it.id)}>
{it.project} · {it.label || it.type}
{it.type}{it.scope_entity_type ? ` · ${it.scope_entity_type}` : ''}
{relTime(it.created_at)}
))}
)} {/* Health pulse */}
Сервисы
setMode('status')}>connections →
M1
router
embedder
{statusQ.loading ? '...' : healthLabel}
); } window.HomeMode = HomeMode;