"use client"; /** * Phase 8 — org ANALYTICS tab (read-only usage/health analytics). * * Data: `GET /orgs/:orgId/admin/analytics` (server-side `assertOrgAdmin`, * org-scoped — see apps/api `analytics.service.ts`). The Overview tab is the * billing-adjacent counts + storage/audit trend; this tab is the usage/health * picture: content mix by page kind, growth (7d/30d), comment + file volume, * top audit actions, and the four 30-day trends. Rendering reuses the * Overview tab's house chrome (Section from settings-shell, CSS Sparkline, * Stat card, formatBytes) — no new dependencies. */ import { useQuery } from "@tanstack/react-query"; import { Activity, Database, FileText, MessageSquare, NotebookText, Users, type LucideIcon } from "lucide-react"; import { orgAdminApi } from "@/lib/org-admin"; import { Section } from "@/components/settings/settings-shell"; function formatBytes(n: number): string { if (n < 1024) return `${n} B`; const units = ["KB", "MB", "GB", "TB"]; let v = n / 1024; let i = 0; while (v >= 1024 && i < units.length - 1) { v /= 1024; i += 1; } return `${v.toFixed(1)} ${units[i]}`; } function Sparkline({ points, label, fmt }: { points: { day: string; value: number }[]; label: string; fmt: (n: number) => string }) { const max = Math.max(1, ...points.map((p) => p.value)); return (
{points.map((p) => (
))}
{points[0]?.day ?? ""} {points[points.length - 1]?.day ?? ""}
); } function Stat({ icon: Icon, label, value }: { icon: LucideIcon; label: string; value: string | number }) { return (
{label}
{value}
); } export function AdminAnalyticsTab({ orgId }: { orgId: string }) { const q = useQuery({ queryKey: ["orgs", orgId, "admin", "analytics"], queryFn: () => orgAdminApi.analytics(orgId), retry: false, }); if (q.isLoading) { return

Loading analytics…

; } if (q.isError) { return

Could not load analytics.

; } const a = q.data!.data; return (
{a.pagesTotal === 0 ? (

No pages yet.

) : (
{Object.entries(a.pages).map(([kind, count]) => (
{kind}
{count}
))}
)}
New pages (7d)
{a.newPages.last7d}
New pages (30d)
{a.newPages.last30d}
New comments (30d)
{a.comments.last30d}
Files (30d)
{formatBytes(a.files.last30dBytes)}
Total storage
{formatBytes(a.files.storageBytes)}
Archived workspaces
{a.archivedWorkspaces}
{a.audit.topActions.length === 0 ? (

No audit events in the last 30 days.

) : (
    {a.audit.topActions.map((t) => (
  • {t.action} {t.count}
  • ))}
)}
({ day: p.day, value: p.count }))} label="New pages trend over 30 days" fmt={(n) => String(n)} />
({ day: p.day, value: p.bytes }))} label="Storage growth trend over 30 days" fmt={formatBytes} />
({ day: p.day, value: p.count }))} label="Member sign-up trend over 30 days" fmt={(n) => String(n)} />
({ day: p.day, value: p.count }))} label="Audit events trend over 30 days" fmt={(n) => String(n)} />
); }