import type { MetricsTimeBucket, MetricsView } from "@batch-cooking/shared"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis, } from "recharts"; import { adminApiClient } from "../../api/client"; import "../admin-page.scss"; import "./dashboard-page.scss"; import { formatCount, kpiTiles, seriesTotal, shortDay } from "./dashboard"; /** Load state for `GET /admin/metrics` — same discriminated-union shape as apps/web's page states. */ type DashboardState = | { status: "loading" } | { status: "loaded"; metrics: MetricsView } | { status: "error" }; const RANGE_DAYS = 30; /** * Usage-metrics dashboard. KPI tiles from the snapshot, then a small line * chart per instrumented time series over the last {@link RANGE_DAYS} days. * Fed by `GET /admin/metrics`; read-only, refetched only on mount. */ export function DashboardPage() { const { t } = useTranslation(); const [state, setState] = useState({ status: "loading" }); useEffect(() => { let cancelled = false; setState({ status: "loading" }); adminApiClient .getMetrics(RANGE_DAYS) .then((metrics) => { if (!cancelled) setState({ status: "loaded", metrics }); }) .catch(() => { if (!cancelled) setState({ status: "error" }); }); return () => { cancelled = true; }; }, []); return (

{t("admin.dashboard.title")}

{t("admin.dashboard.lead")}

{state.status === "loading" && (

{t("admin.common.loading")}

)} {state.status === "error" && (

{t("admin.common.loadError")}

)} {state.status === "loaded" && }
); } function DashboardBody({ metrics }: { metrics: MetricsView }) { const { t } = useTranslation(); const { snapshot, series } = metrics; const charts: { key: string; buckets: MetricsTimeBucket[] }[] = [ { key: "signups", buckets: series.signups }, { key: "recipesCreated", buckets: series.recipesCreated }, { key: "planningItemsAdded", buckets: series.planningItemsAdded }, { key: "correctionsSubmitted", buckets: series.correctionsSubmitted }, { key: "trainingSuggestions", buckets: series.trainingSuggestions }, ]; return ( <>
{kpiTiles(snapshot).map((tile) => (
{formatCount(tile.value)} {t(`admin.dashboard.kpi.${tile.labelKey}`)}
))}
{charts.map(({ key, buckets }) => (

{t(`admin.dashboard.series.${key}`)}

{t("admin.dashboard.windowTotal", { n: seriesTotal(buckets) })}
))}

{t("admin.dashboard.recipesBySource")}

{snapshot.recipesBySource.length === 0 ? (

{t("admin.dashboard.noImports")}

) : ( )}
{metrics.events.length > 0 && (

{t("admin.dashboard.events")}

)} ); } /** A compact 30-day line chart for one metrics series. */ function TrendChart({ buckets }: { buckets: MetricsTimeBucket[] }) { const data = buckets.map((bucket) => ({ day: shortDay(bucket.date), count: bucket.count })); return (
); }