// screens-analytics.jsx — Analytics: usage / latency / tokens / errors over // selectable periods, with a flight-style dual-month custom range picker. // Exports: window.Analytics. Props: { navigate, pushToast } // Per-screen CSS in screens-analytics.css (.an- prefix). (() => { const { useState, useEffect, useMemo, useCallback } = React; const { Icon, api, useToast, PageHeader, Card, StatCard, Button, IconButton, Tabs, AreaChart, BarChart, EmptyState, Skeleton, fmtCount, fmtMs, fmtPct, } = window; const cx = (...p) => p.filter(Boolean).join(" "); const DAY = 86400000; const PERIODS = [ { id: "today", label: "Today" }, { id: "this_week", label: "This week" }, { id: "this_month", label: "This month" }, { id: "last_month", label: "Last month" }, { id: "last_30_days", label: "30 days" }, { id: "last_6_months", label: "6 months" }, { id: "last_year", label: "1 year" }, { id: "custom", label: "Custom" }, ]; const LIVE_PERIODS = new Set(["today", "this_week", "this_month", "last_30_days"]); const METRICS = [ { id: "requests", label: "Requests", tone: "accent", fmt: (v) => fmtCount(v) }, { id: "latency", label: "Latency", tone: "info", fmt: (v) => fmtMs(v) }, { id: "tokens", label: "Tokens", tone: "success", fmt: (v) => fmtCount(v) }, { id: "errors", label: "Errors", tone: "danger", fmt: (v) => fmtCount(v) }, ]; /* ── date helpers (local time) ── */ function startOfDay(ms) { const d = new Date(ms); d.setHours(0, 0, 0, 0); return d.getTime(); } function fmtDay(ms) { return new Date(ms).toLocaleDateString(undefined, { month: "short", day: "numeric" }); } function fmtFull(ms) { return new Date(ms).toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" }); } function fmtHour(ms) { return new Date(ms).toLocaleTimeString(undefined, { hour: "numeric" }); } function fmtMonth(ms) { return new Date(ms).toLocaleDateString(undefined, { month: "short", year: "2-digit" }); } function bucketLabel(ms, bucketMs) { if (bucketMs <= 3600000) return fmtHour(ms); if (bucketMs >= 28 * DAY) return fmtMonth(ms); return fmtDay(ms); } function addMonth(y, m, delta) { const t = m + delta; return { y: y + Math.floor(t / 12), m: ((t % 12) + 12) % 12 }; } function periodLabel(period, range) { if (period === "custom" && range.from != null && range.to != null) { return fmtDay(range.from) + " – " + fmtDay(range.to); } const p = PERIODS.find((x) => x.id === period); return p ? p.label : ""; } /* ════════════════════════════════════════════════════════════════ ANALYTICS SCREEN ════════════════════════════════════════════════════════════════ */ function Analytics({ pushToast }) { const toast = useToast(); const notify = useCallback((m, t) => { if (toast) { t === "error" ? toast.error(m) : toast.info(m); } else if (pushToast) pushToast(m, t || "info"); }, [toast, pushToast]); const [period, setPeriod] = useState("today"); const [metric, setMetric] = useState("requests"); const [range, setRange] = useState({ from: null, to: null }); // custom (ms, local midnight) const [calOpen, setCalOpen] = useState(false); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const buildUrl = useCallback(() => { let url = "/ui/api/analytics?period=" + period; if (period === "custom") { if (range.from == null || range.to == null) return null; url += "&from=" + range.from + "&to=" + (range.to + DAY); // end day inclusive } return url; }, [period, range]); const load = useCallback(() => { const url = buildUrl(); if (!url) { setData(null); setLoading(false); return; } setLoading(true); api.get(url) .then((res) => { setData(res); setError(null); }) .catch((e) => { setError(e); notify("Failed to load analytics", "error"); }) .finally(() => setLoading(false)); }, [buildUrl, notify]); useEffect(() => { load(); }, [load]); // Light 60s poll for periods that include "now". useEffect(() => { if (!LIVE_PERIODS.has(period)) return undefined; const id = setInterval(() => { if (!document.hidden) load(); }, 60000); return () => clearInterval(id); }, [period, load]); const buckets = (data && data.buckets) || []; const totals = (data && data.totals) || {}; const bucketMs = (data && data.bucket_ms) || DAY; const unavailable = data && data.available === false; const series = useMemo(() => buckets.map((b) => { switch (metric) { case "latency": return b.avg_latency_ms || 0; case "tokens": return (b.tokens_in || 0) + (b.tokens_out || 0); case "errors": return b.errors || 0; default: return b.requests || 0; } }), [buckets, metric]); const labels = useMemo(() => buckets.map((b) => bucketLabel(b.t, bucketMs)), [buckets, bucketMs]); const metricConf = METRICS.find((m) => m.id === metric) || METRICS[0]; const hasData = buckets.some((b) => (b.requests || 0) > 0); const needDates = period === "custom" && (range.from == null || range.to == null); const periodTabs = PERIODS.map((p) => ({ id: p.id, label: p.id === "custom" && range.from != null && range.to != null ? periodLabel("custom", range) : p.label, })); return (
Refresh} />
{period === "custom" && ( )}
{error && (
Failed to load analytics: {error.message || String(error)}
)}
0 ? "danger" : "success"} loading={loading} />
({ id: m.id, label: m.label }))} />} > {needDates ? ( ) : loading ? ( ) : !hasData ? ( ) : ( )} {hasData && !loading && !needDates && ( ({ label: bucketLabel(b.t, bucketMs), value: b.requests || 0 }))} height={180} formatValue={(v) => fmtCount(v)} /> )} {calOpen && ( setCalOpen(false)} onApply={(r) => { setRange(r); setCalOpen(false); }} /> )}
); } /* ════════════════════════════════════════════════════════════════ RANGE CALENDAR — flight-style dual month range picker ════════════════════════════════════════════════════════════════ */ function RangeCalendar({ initial, onClose, onApply }) { const today = startOfDay(Date.now()); const [start, setStart] = useState(initial.from); const [end, setEnd] = useState(initial.to); const [view, setView] = useState(() => { const base = initial.from != null ? new Date(initial.from) : new Date(); return { y: base.getFullYear(), m: base.getMonth() }; }); useEffect(() => { const onKey = (e) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [onClose]); const pick = (ms) => { if (start == null || end != null) { setStart(ms); setEnd(null); } else if (ms < start) { setEnd(start); setStart(ms); } else { setEnd(ms); } }; const right = addMonth(view.y, view.m, 1); const shift = (delta) => setView(addMonth(view.y, view.m, delta)); return (
{ if (e.target === e.currentTarget) onClose(); }}>
shift(-1)} />
Select a date range
shift(1)} />
{start != null ? fmtFull(start) : "Start date"} {end != null ? fmtFull(end) : "End date"}
); } const DOW = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"]; function MonthGrid({ y, m, start, end, today, onPick }) { const first = new Date(y, m, 1); const monthName = first.toLocaleDateString(undefined, { month: "long", year: "numeric" }); const daysInMonth = new Date(y, m + 1, 0).getDate(); const lead = (first.getDay() + 6) % 7; // Monday-first const cells = []; for (let i = 0; i < lead; i++) cells.push(null); for (let d = 1; d <= daysInMonth; d++) cells.push(new Date(y, m, d).getTime()); return (
{monthName}
{DOW.map((d) => {d})}
{cells.map((ms, i) => { if (ms == null) return ; const future = ms > today; const isEdge = (start != null && ms === start) || (end != null && ms === end); const inRange = start != null && end != null && ms > start && ms < end; return ( ); })}
); } window.Analytics = Analytics; })();