/* 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;