// 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 (