import { useState, useEffect, useCallback, useMemo, lazy, Suspense } from 'react';
import { PasswordGate } from './components/PasswordGate';
import ErrorBoundary from './components/ErrorBoundary';
import Sidebar from './components/Sidebar';
import DateFilter from './components/DateFilter';
import { CommandPalette } from './components/CommandPalette';
import { FocusChip } from './components/FocusChip';
import { EmptyState, Notice, PageHeader, Tabs } from './components/ui';
import { resolveChrome, surfaceById } from './components/nav-config';
import { useRoute } from './lib/useRoute';
import { useShortcuts } from './lib/useShortcuts';
import { useSync } from './lib/useSync';
import { useTheme } from './lib/useTheme';
import {
fetchRangeSessions,
fetchAllSessions,
buildDailyFromSessions,
fetchCostSummary,
fillMissingDays,
computeOverviewStats,
getModelBreakdown,
invalidateRealSessions,
isDemoData,
} from './lib/queries';
const AUTO_REFRESH_MS = 5 * 60 * 1000;
// One chunk per reachable location, keyed by the same `surface/tab` path the
// router produces. Adding a tab is one entry here plus one in nav-config.
const ROUTE_LOADERS = {
'today/summary': () => import('./pages/Overview'),
'today/sessions': () => import('./pages/Sessions'),
'today/runs': () => import('./pages/AgentVisualizer'),
'today/swarm': () => import('./pages/Swarm'),
'today/storage': () => import('./pages/Storage'),
'review/inbox': () => import('./pages/LoopReview'),
'review/projects':() => import('./pages/ProjectControl'),
'review/map': () => import('./pages/LoopOps'),
ledger: () => import('./pages/CostTracker'),
sanctum: () => import('./pages/ScryingSanctum'),
learn: () => import('./pages/LearningQuest'),
capacity: () => import('./pages/CapacityUsage'),
};
const Overview = lazy(ROUTE_LOADERS['today/summary']);
const Sessions = lazy(ROUTE_LOADERS['today/sessions']);
const AgentVisualizer = lazy(ROUTE_LOADERS['today/runs']);
const Swarm = lazy(ROUTE_LOADERS['today/swarm']);
const Storage = lazy(ROUTE_LOADERS['today/storage']);
const LoopReview = lazy(ROUTE_LOADERS['review/inbox']);
const ProjectControl = lazy(ROUTE_LOADERS['review/projects']);
const LoopOps = lazy(ROUTE_LOADERS['review/map']);
const CostTracker = lazy(ROUTE_LOADERS.ledger);
const ScryingSanctum = lazy(ROUTE_LOADERS.sanctum);
const LearningQuest = lazy(ROUTE_LOADERS.learn);
const CapacityUsage = lazy(ROUTE_LOADERS.capacity);
// Locations that read their own data and ship their own instructional empty
// states, so the session-data splash must not cover them.
const SELF_LOADING = new Set([
'review/inbox',
'review/map',
'review/projects',
'learn',
'capacity',
'today/storage',
'today/swarm',
]);
function PageLoader() {
return (
Loading…
);
}
function withSuspense(node) {
return }>{node};
}
function NoDataScreen() {
return (
);
}
export default function App() {
const { route, navigate, setTab } = useRoute();
const path = route.tab ? `${route.surface}/${route.tab}` : route.surface;
const [paletteOpen, setPaletteOpen] = useState(false);
const { theme, toggle: toggleTheme } = useTheme();
const [dateRange, setDateRange] = useState(() => {
try {
const raw = localStorage.getItem('meow-ops-date-range');
if (raw === '1h' || raw === '24h' || raw === 'all') return raw;
const num = Number.parseInt(raw, 10);
if (num === 7 || num === 30 || num === 90) return num;
} catch { /* localStorage blocked */ }
return 30;
});
useEffect(() => {
try { localStorage.setItem('meow-ops-date-range', String(dateRange)); } catch { /* quota */ }
}, [dateRange]);
// Two session arrays:
// sessions — date-filtered, used for display (table, charts, per-page views)
// allSessions — newest bounded compatibility preview with no date filter;
// complete all-time metrics come from costSummary rollups
const [sessions, setSessions] = useState([]);
const [allSessions, setAllSessions] = useState([]);
const [dailyData, setDailyData] = useState([]);
const [costSummary, setCostSummary] = useState(null);
const [loading, setLoading] = useState(true);
const [noData, setNoData] = useState(false);
const [reloadKey, setReloadKey] = useState(0);
const [scopeNow, setScopeNow] = useState(Date.now);
const [hasLoaded, setHasLoaded] = useState(false);
const [rangeCompleteness, setRangeCompleteness] = useState('preview');
const [rangeError, setRangeError] = useState(null);
const [tokenBudget, setTokenBudget] = useState(() => {
try {
return JSON.parse(localStorage.getItem('meow-ops-token-budget')) ||
{ claude: { week: 0, month: 0 }, codex: { week: 0, month: 0 } };
} catch { return { claude: { week: 0, month: 0 }, codex: { week: 0, month: 0 } }; }
});
const saveBudget = useCallback((next) => {
setTokenBudget(next);
localStorage.setItem('meow-ops-token-budget', JSON.stringify(next));
}, []);
const reloadData = useCallback(() => {
invalidateRealSessions();
setScopeNow(Date.now());
setReloadKey((k) => k + 1);
}, []);
const sync = useSync(reloadData);
useEffect(() => {
let cancelled = false;
async function load() {
setLoading(true);
const [range, all, summary] = await Promise.all([
fetchRangeSessions(dateRange, scopeNow),
fetchAllSessions(),
fetchCostSummary(),
]);
if (cancelled) return;
setNoData(all.length === 0 && range.items.length === 0);
setSessions(range.items);
setAllSessions(all);
setCostSummary(summary);
setRangeCompleteness(range.completeness);
setRangeError(range.error);
setDailyData(fillMissingDays(buildDailyFromSessions(range.items), dateRange, scopeNow));
setHasLoaded(true);
setLoading(false);
}
load();
return () => { cancelled = true; };
}, [dateRange, reloadKey, scopeNow]);
// Start the active chunk while session data loads so one Loading… covers
// both, instead of flashing a second loader.
useEffect(() => { ROUTE_LOADERS[path]?.(); }, [path]);
useEffect(() => {
const id = setInterval(reloadData, AUTO_REFRESH_MS);
return () => clearInterval(id);
}, [reloadData]);
const openPalette = useCallback(() => setPaletteOpen(true), []);
const closePalette = useCallback(() => setPaletteOpen(false), []);
const goToSurface = useCallback((surface, tab) => navigate(surface, tab), [navigate]);
useShortcuts({ onOpenPalette: openPalette, onNavigate: goToSurface, paletteOpen });
const modelData = costSummary?.bySourceModel
? costSummary.bySourceModel
: costSummary?.byModel
? costSummary.byModel.map((row) => ({ ...row, key: row.key, model: row.key }))
: getModelBreakdown(allSessions);
const stats = computeOverviewStats(sessions, dateRange);
// Per-source rollup for the budget panel. Computed from ALL sessions so the
// week/month figures do not move when the date filter changes.
const sourceStats = useMemo(() => {
const now = new Date();
const day = now.getDay();
const weekStart = new Date(now);
weekStart.setDate(now.getDate() - (day === 0 ? 6 : day - 1));
weekStart.setHours(0, 0, 0, 0);
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
const blank = () => ({
sessions: 0, cost: 0, tokens: 0,
weekTokens: 0, monthTokens: 0, weekSessions: 0, monthSessions: 0,
});
const acc = { claude: blank(), codex: blank() };
allSessions.forEach((s) => {
const src = s.source || 'claude';
if (!acc[src]) acc[src] = blank();
const tok = s.total_tokens || 0;
acc[src].sessions++;
acc[src].cost += s.estimated_cost_usd || 0;
acc[src].tokens += tok;
const d = new Date(s.started_at);
if (d >= weekStart) { acc[src].weekTokens += tok; acc[src].weekSessions++; }
if (d >= monthStart) { acc[src].monthTokens += tok; acc[src].monthSessions++; }
});
for (const [src, complete] of Object.entries(costSummary?.bySourceAllTime || {})) {
if (!acc[src]) acc[src] = blank();
acc[src].sessions = complete.sessions || 0;
acc[src].cost = complete.cost || 0;
acc[src].tokens = complete.tokens || 0;
}
return acc;
}, [allSessions, costSummary]);
const chrome = resolveChrome(route.surface, route.tab);
const surface = surfaceById(route.surface);
function renderLocation() {
if (!SELF_LOADING.has(path) && noData && costSummary?.snapshot?.state === 'unavailable') {
return Retry data} />;
}
if (!SELF_LOADING.has(path) && noData) return ;
switch (path) {
case 'today/summary':
return withSuspense(
,
);
case 'today/sessions':
return withSuspense();
case 'today/runs':
return withSuspense();
case 'today/swarm':
return withSuspense();
case 'today/storage':
return withSuspense();
case 'review/inbox':
return withSuspense();
case 'review/map':
return withSuspense();
case 'review/projects':
return withSuspense();
case 'ledger':
return withSuspense(
,
);
case 'sanctum':
return withSuspense();
case 'learn':
return withSuspense();
case 'capacity':
return withSuspense();
default:
return ;
}
}
// Keep the active scene and guide mounted during background metric refreshes.
const showLoader = loading && !hasLoaded && !SELF_LOADING.has(path)
&& !(path === 'sanctum' && allSessions.length > 0);
const header = (
<>
{chrome.usesDateFilter ? : null}
)}
/>
{surface?.tabs && (
)}
{isDemoData(allSessions, costSummary) && path !== 'today/swarm' && (
Open local dashboard}>
Sample data. These figures are examples, not your local sessions.
)}
{rangeError && chrome.usesDateFilter && !isDemoData(allSessions, costSummary) && (
{rangeError}
)}
{costSummary?.snapshot?.warning && (chrome.usesDateFilter || route.surface === 'sanctum' || path === 'today/runs') && !isDemoData(allSessions, costSummary) && (
{costSummary.snapshot.warning}
)}
{chrome.needsHelper && !sync.helperOnline && (
This view cannot connect to the local helper. Open the local dashboard and check the helper.
)}
>
);
const body = (
{showLoader ? : renderLocation()}
);
return (
{chrome.fullBleed ? (
<>
{header}
{body}
>
) : (
<>
{header}
{body}
>
)}
{ void sync.run(); }}
onSetDateRange={setDateRange}
onToggleTheme={toggleTheme}
theme={theme}
/>
);
}