diff --git a/apps/web-dashboard/src/app/chaos/page.tsx b/apps/web-dashboard/src/app/chaos/page.tsx new file mode 100644 index 0000000..f1675a3 --- /dev/null +++ b/apps/web-dashboard/src/app/chaos/page.tsx @@ -0,0 +1,146 @@ +"use client"; + +import { useState } from "react"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Shield, Zap, Clock, AlertTriangle, Server, Database, Wifi } from "lucide-react"; + +interface Experiment { + id: string; + name: string; + target: string; + type: string; + icon: typeof Server; + description: string; + status: "idle" | "running" | "completed" | "failed"; + lastRun?: string; + duration?: string; +} + +const initialExperiments: Experiment[] = [ + { + id: "exp_001", name: "API Latency Injection", target: "API Server", type: "latency", + icon: Server, description: "Inject 500ms latency on all API endpoints to test client timeout handling.", + status: "idle", lastRun: "2026-04-13 15:00", duration: "5m", + }, + { + id: "exp_002", name: "Database Connection Kill", target: "PostgreSQL", type: "kill", + icon: Database, description: "Terminate database connections to verify reconnection and circuit breaker logic.", + status: "idle", lastRun: "2026-04-12 10:30", duration: "2m", + }, + { + id: "exp_003", name: "Charging Engine Crash", target: "Charging Engine", type: "crash", + icon: Zap, description: "Simulate Rust charging engine crash to validate Go fallback and graceful degradation.", + status: "idle", lastRun: "2026-04-11 09:00", duration: "3m", + }, + { + id: "exp_004", name: "Network Partition", target: "AMF Gateway", type: "partition", + icon: Wifi, description: "Simulate network partition between API server and AMF to test session continuity.", + status: "idle", duration: "10m", + }, + { + id: "exp_005", name: "Redis Cache Flush", target: "Redis", type: "flush", + icon: Database, description: "Flush all Redis keys to test cache miss handling and cold-start performance.", + status: "idle", lastRun: "2026-04-10 14:00", duration: "1m", + }, + { + id: "exp_006", name: "High Load Simulation", target: "API Server", type: "load", + icon: AlertTriangle, description: "Send 10,000 concurrent requests to test rate limiting and autoscaling.", + status: "idle", duration: "15m", + }, +]; + +const statusVariant = (s: string) => + s === "running" ? "warning" : s === "completed" ? "success" : s === "failed" ? "destructive" : "secondary"; + +export default function ChaosPage() { + const [experiments, setExperiments] = useState(initialExperiments); + + const runExperiment = (id: string) => { + setExperiments((prev) => + prev.map((e) => + e.id === id ? { ...e, status: "running" as const } : e + ) + ); + // Simulate completion after a delay + setTimeout(() => { + setExperiments((prev) => + prev.map((e) => + e.id === id + ? { ...e, status: (Math.random() > 0.2 ? "completed" : "failed") as Experiment["status"], lastRun: new Date().toISOString().slice(0, 16).replace("T", " ") } + : e + ) + ); + }, 3000); + }; + + return ( +
+
+ +
+

Chaos Engineering

+

Run controlled failure experiments to validate system resilience.

+
+
+ +
+ + +
{experiments.filter((e) => e.status === "completed").length}
+

Experiments Passed

+
+
+ + +
{experiments.filter((e) => e.status === "failed").length}
+

Experiments Failed

+
+
+ + +
{experiments.filter((e) => e.status === "running").length}
+

Currently Running

+
+
+
+ +
+ {experiments.map((exp) => ( + + +
+
+ +
+
+ {exp.name} + Target: {exp.target} +
+
+ {exp.status} +
+ +

{exp.description}

+
+
+ {exp.duration} + {exp.lastRun && Last: {exp.lastRun}} +
+ +
+
+
+ ))} +
+
+ ); +} diff --git a/apps/web-dashboard/src/app/esim/page.tsx b/apps/web-dashboard/src/app/esim/page.tsx new file mode 100644 index 0000000..dbc7275 --- /dev/null +++ b/apps/web-dashboard/src/app/esim/page.tsx @@ -0,0 +1,70 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Radio, Download, RefreshCw } from "lucide-react"; + +const profiles = [ + { id: "prof_001", imsi: "208930000000001", euiccId: "89049032004008882600001", status: "active", subscriber: "Alice Martin", activatedAt: "2026-03-15" }, + { id: "prof_002", imsi: "208930000000002", euiccId: "89049032004008882600002", status: "active", subscriber: "Bob Dupont", activatedAt: "2026-03-20" }, + { id: "prof_003", imsi: "208930000000003", euiccId: "89049032004008882600003", status: "downloading", subscriber: "Claire Moreau", activatedAt: "-" }, + { id: "prof_004", imsi: "208930000000005", euiccId: "89049032004008882600005", status: "active", subscriber: "Emma Petit", activatedAt: "2026-02-10" }, + { id: "prof_005", imsi: "208930000000008", euiccId: "89049032004008882600008", status: "inactive", subscriber: "Henri Faure", activatedAt: "-" }, +]; + +const statusVariant = (s: string) => + s === "active" ? "success" : s === "downloading" ? "secondary" : s === "failed" ? "destructive" : "warning"; + +export default function ESIMPage() { + return ( +
+
+
+ +
+

eSIM Profiles

+

Manage eSIM profile provisioning via SM-DP+ (ES2+).

+
+
+ +
+ + + + All Profiles + + +
+ + + + + + + + + + + + + + {profiles.map((p) => ( + + + + + + + + + + ))} + +
Profile IDSubscriberIMSIeUICC IDStatusActivatedActions
{p.id}{p.subscriber}{p.imsi}{p.euiccId}{p.status}{p.activatedAt} + +
+
+
+
+
+ ); +} diff --git a/apps/web-dashboard/src/app/health/page.tsx b/apps/web-dashboard/src/app/health/page.tsx new file mode 100644 index 0000000..10acf43 --- /dev/null +++ b/apps/web-dashboard/src/app/health/page.tsx @@ -0,0 +1,62 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { HeartPulse, Server, Database, Cpu, HardDrive, Wifi } from "lucide-react"; + +const services = [ + { name: "API Server", icon: Server, status: "healthy", latency: "12ms", uptime: "99.99%", cpu: "23%", memory: "512 MB" }, + { name: "PostgreSQL", icon: Database, status: "healthy", latency: "3ms", uptime: "99.97%", cpu: "15%", memory: "2.1 GB" }, + { name: "Redis Cache", icon: Database, status: "healthy", latency: "1ms", uptime: "99.99%", cpu: "5%", memory: "256 MB" }, + { name: "Charging Engine (Rust)", icon: Cpu, status: "healthy", latency: "2ms", uptime: "99.98%", cpu: "8%", memory: "64 MB" }, + { name: "AMF Gateway", icon: Wifi, status: "healthy", latency: "45ms", uptime: "99.95%", cpu: "12%", memory: "128 MB" }, + { name: "ES2+ SM-DP+", icon: HardDrive, status: "degraded", latency: "230ms", uptime: "98.50%", cpu: "N/A", memory: "N/A" }, +]; + +export default function HealthPage() { + return ( +
+
+ +
+

System Health

+

Monitor service health, latency, and resource usage.

+
+
+ +
+ {services.map((svc) => ( + + +
+ + {svc.name} +
+ + {svc.status} + +
+ +
+
+

Latency

+

{svc.latency}

+
+
+

Uptime

+

{svc.uptime}

+
+
+

CPU

+

{svc.cpu}

+
+
+

Memory

+

{svc.memory}

+
+
+
+
+ ))} +
+
+ ); +} diff --git a/apps/web-dashboard/src/app/layout.tsx b/apps/web-dashboard/src/app/layout.tsx index e159121..24fb2f7 100644 --- a/apps/web-dashboard/src/app/layout.tsx +++ b/apps/web-dashboard/src/app/layout.tsx @@ -2,8 +2,9 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono, Inter } from "next/font/google"; import "./globals.css"; import { cn } from "@/lib/utils"; +import { Sidebar } from "@/components/sidebar"; -const inter = Inter({subsets:['latin'],variable:'--font-sans'}); +const inter = Inter({ subsets: ["latin"], variable: "--font-sans" }); const geistSans = Geist({ variable: "--font-geist-sans", @@ -16,8 +17,8 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "Telecom Admin Dashboard", + description: "Sovereign Telecom-as-a-Service Platform Admin Dashboard", }; export default function RootLayout({ @@ -30,7 +31,10 @@ export default function RootLayout({ lang="en" className={cn("h-full", "antialiased", geistSans.variable, geistMono.variable, "font-sans", inter.variable)} > - {children} + + +
{children}
+ ); } diff --git a/apps/web-dashboard/src/app/page.tsx b/apps/web-dashboard/src/app/page.tsx index 0ad7bac..a2b286c 100644 --- a/apps/web-dashboard/src/app/page.tsx +++ b/apps/web-dashboard/src/app/page.tsx @@ -1,27 +1,108 @@ -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { StatCard } from "@/components/stat-card"; +import { Users, Activity, AlertTriangle, Server, Wifi, Database } from "lucide-react"; -export default function Home() { +export default function DashboardPage() { return ( -
-
- +
+
+

Dashboard

+

Telecom platform overview and real-time metrics.

+
+ +
+ + + + +
+ +
+ - Telecom Dashboard - - Sovereign Telecom-as-a-Service Platform - + Recent Subscribers - - - + +
+ {[ + { name: "Alice Martin", msisdn: "+33612345678", status: "active" as const }, + { name: "Bob Dupont", msisdn: "+33698765432", status: "active" as const }, + { name: "Claire Moreau", msisdn: "+33678901234", status: "provisioning" as const }, + { name: "David Leroy", msisdn: "+33667890123", status: "suspended" as const }, + { name: "Emma Petit", msisdn: "+33656789012", status: "active" as const }, + ].map((sub) => ( +
+
+

{sub.name}

+

{sub.msisdn}

+
+ + {sub.status} + +
+ ))} +
- -
-

Powered by Next.js and shadcn/ui

-
-
+ + + + System Status + + +
+ {[ + { label: "API Server", icon: Server, status: "Healthy", ok: true }, + { label: "Database", icon: Database, status: "Connected", ok: true }, + { label: "Redis Cache", icon: Database, status: "Connected", ok: true }, + { label: "Charging Engine", icon: Activity, status: "Running", ok: true }, + { label: "AMF Gateway", icon: Wifi, status: "Connected", ok: true }, + ].map((svc) => ( +
+
+ + {svc.label} +
+
+ + {svc.status} +
+
+ ))} +
+
+
+
); } diff --git a/apps/web-dashboard/src/app/payments/page.tsx b/apps/web-dashboard/src/app/payments/page.tsx new file mode 100644 index 0000000..97ff507 --- /dev/null +++ b/apps/web-dashboard/src/app/payments/page.tsx @@ -0,0 +1,73 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { StatCard } from "@/components/stat-card"; +import { CreditCard, ArrowUpRight, ArrowDownRight, Clock } from "lucide-react"; + +const transactions = [ + { id: "txn_001", subscriber: "Alice Martin", type: "TOP_UP", amount: 50.0, status: "completed", date: "2026-04-14 14:23" }, + { id: "txn_002", subscriber: "Bob Dupont", type: "TOP_UP", amount: 25.0, status: "completed", date: "2026-04-14 13:15" }, + { id: "txn_003", subscriber: "David Leroy", type: "TOP_UP", amount: 10.0, status: "failed", date: "2026-04-14 12:45" }, + { id: "txn_004", subscriber: "Emma Petit", type: "REFUND", amount: -15.0, status: "completed", date: "2026-04-14 11:30" }, + { id: "txn_005", subscriber: "Henri Faure", type: "TOP_UP", amount: 100.0, status: "pending", date: "2026-04-14 10:00" }, + { id: "txn_006", subscriber: "François Blanc", type: "TOP_UP", amount: 30.0, status: "completed", date: "2026-04-13 22:15" }, +]; + +export default function PaymentsPage() { + return ( +
+
+

Payments

+

Transaction history and payment management.

+
+ +
+ + + + +
+ + + + Recent Transactions + + +
+ + + + + + + + + + + + + {transactions.map((txn) => ( + + + + + + + + + ))} + +
Transaction IDSubscriberTypeAmountStatusDate
{txn.id}{txn.subscriber} + {txn.type} + + €{Math.abs(txn.amount).toFixed(2)} + + + {txn.status} + + {txn.date}
+
+
+
+
+ ); +} diff --git a/apps/web-dashboard/src/app/settings/page.tsx b/apps/web-dashboard/src/app/settings/page.tsx new file mode 100644 index 0000000..0a6d3fd --- /dev/null +++ b/apps/web-dashboard/src/app/settings/page.tsx @@ -0,0 +1,75 @@ +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Settings } from "lucide-react"; + +export default function SettingsPage() { + return ( +
+
+ +
+

Settings

+

Platform configuration and preferences.

+
+
+ +
+ + + API Configuration + Configure the API server connection. + + +
+ + +
+
+ + +
+ +
+
+ + + + IMSI Configuration + Mobile Country Code and Mobile Network Code settings. + + +
+
+ + +
+
+ + +
+
+ +
+
+ + + + ES2+ SM-DP+ Configuration + eSIM profile server connection settings. + + +
+ + +
+
+ + +
+ +
+
+
+
+ ); +} diff --git a/apps/web-dashboard/src/app/subscribers/page.tsx b/apps/web-dashboard/src/app/subscribers/page.tsx new file mode 100644 index 0000000..63ba8a2 --- /dev/null +++ b/apps/web-dashboard/src/app/subscribers/page.tsx @@ -0,0 +1,94 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { UserPlus, Search, Download } from "lucide-react"; + +const subscribers = [ + { id: 1, name: "Alice Martin", msisdn: "+33612345678", imsi: "208930000000001", email: "alice@example.com", status: "active" as const, balance: 45.0, plan: "Premium 5G" }, + { id: 2, name: "Bob Dupont", msisdn: "+33698765432", imsi: "208930000000002", email: "bob@example.com", status: "active" as const, balance: 12.5, plan: "Basic LTE" }, + { id: 3, name: "Claire Moreau", msisdn: "+33678901234", imsi: "208930000000003", email: "claire@example.com", status: "provisioning" as const, balance: 0.0, plan: "Premium 5G" }, + { id: 4, name: "David Leroy", msisdn: "+33667890123", imsi: "208930000000004", email: "david@example.com", status: "suspended" as const, balance: -2.3, plan: "Standard" }, + { id: 5, name: "Emma Petit", msisdn: "+33656789012", imsi: "208930000000005", email: "emma@example.com", status: "active" as const, balance: 78.9, plan: "Premium 5G" }, + { id: 6, name: "François Blanc", msisdn: "+33645678901", imsi: "208930000000006", email: "francois@example.com", status: "active" as const, balance: 25.0, plan: "Standard" }, + { id: 7, name: "Gabrielle Roux", msisdn: "+33634567890", imsi: "208930000000007", email: "gabrielle@example.com", status: "terminated" as const, balance: 0.0, plan: "Basic LTE" }, + { id: 8, name: "Henri Faure", msisdn: "+33623456789", imsi: "208930000000008", email: "henri@example.com", status: "active" as const, balance: 156.2, plan: "Enterprise" }, +]; + +const statusVariant = (s: string) => + s === "active" ? "success" : s === "suspended" ? "warning" : s === "terminated" ? "destructive" : "secondary"; + +export default function SubscribersPage() { + return ( +
+
+
+

Subscribers

+

Manage subscriber accounts and profiles.

+
+
+ + +
+
+ + + + All Subscribers +
+
+ + +
+
+
+ +
+ + + + + + + + + + + + + + {subscribers.map((sub) => ( + + + + + + + + + + ))} + +
NameMSISDNIMSIPlanBalanceStatusActions
+
+

{sub.name}

+

{sub.email}

+
+
{sub.msisdn}{sub.imsi}{sub.plan} + + €{sub.balance.toFixed(2)} + + + {sub.status} + + +
+
+
+
+
+ ); +} diff --git a/apps/web-dashboard/src/app/usage/page.tsx b/apps/web-dashboard/src/app/usage/page.tsx new file mode 100644 index 0000000..3b52bd0 --- /dev/null +++ b/apps/web-dashboard/src/app/usage/page.tsx @@ -0,0 +1,78 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { StatCard } from "@/components/stat-card"; +import { Activity, HardDrive, Phone, MessageSquare } from "lucide-react"; + +const usageEvents = [ + { id: 1, imsi: "208930000000001", type: "DATA", volume: "1.2 GB", cost: "€2.40", time: "2 min ago" }, + { id: 2, imsi: "208930000000002", type: "VOICE", volume: "15 min", cost: "€0.75", time: "5 min ago" }, + { id: 3, imsi: "208930000000005", type: "SMS", volume: "3 msgs", cost: "€0.30", time: "12 min ago" }, + { id: 4, imsi: "208930000000001", type: "DATA", volume: "450 MB", cost: "€0.90", time: "18 min ago" }, + { id: 5, imsi: "208930000000008", type: "DATA", volume: "2.8 GB", cost: "€5.60", time: "25 min ago" }, + { id: 6, imsi: "208930000000006", type: "VOICE", volume: "42 min", cost: "€2.10", time: "30 min ago" }, + { id: 7, imsi: "208930000000002", type: "DATA", volume: "780 MB", cost: "€1.56", time: "45 min ago" }, + { id: 8, imsi: "208930000000005", type: "SMS", volume: "1 msg", cost: "€0.10", time: "1 hr ago" }, +]; + +const typeIcon = (t: string) => { + if (t === "DATA") return HardDrive; + if (t === "VOICE") return Phone; + return MessageSquare; +}; + +export default function UsagePage() { + return ( +
+
+

Usage & Billing

+

Monitor real-time usage and billing metrics.

+
+ +
+ + + + +
+ + + + Recent Usage Events + + +
+ + + + + + + + + + + + {usageEvents.map((evt) => { + const Icon = typeIcon(evt.type); + return ( + + + + + + + + ); + })} + +
TypeIMSIVolumeCostTime
+
+ + {evt.type} +
+
{evt.imsi}{evt.volume}{evt.cost}{evt.time}
+
+
+
+
+ ); +} diff --git a/apps/web-dashboard/src/components/sidebar.tsx b/apps/web-dashboard/src/components/sidebar.tsx new file mode 100644 index 0000000..892de9c --- /dev/null +++ b/apps/web-dashboard/src/components/sidebar.tsx @@ -0,0 +1,62 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { + Users, + LayoutDashboard, + Activity, + CreditCard, + Shield, + Settings, + Radio, + HeartPulse, +} from "lucide-react"; +import { cn } from "@/lib/utils"; + +const navItems = [ + { href: "/", label: "Dashboard", icon: LayoutDashboard }, + { href: "/subscribers", label: "Subscribers", icon: Users }, + { href: "/usage", label: "Usage & Billing", icon: Activity }, + { href: "/payments", label: "Payments", icon: CreditCard }, + { href: "/esim", label: "eSIM Profiles", icon: Radio }, + { href: "/health", label: "System Health", icon: HeartPulse }, + { href: "/chaos", label: "Chaos Engineering", icon: Shield }, + { href: "/settings", label: "Settings", icon: Settings }, +]; + +export function Sidebar() { + const pathname = usePathname(); + + return ( + + ); +} diff --git a/apps/web-dashboard/src/components/stat-card.tsx b/apps/web-dashboard/src/components/stat-card.tsx new file mode 100644 index 0000000..5e60301 --- /dev/null +++ b/apps/web-dashboard/src/components/stat-card.tsx @@ -0,0 +1,36 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { type LucideIcon } from "lucide-react"; +import { cn } from "@/lib/utils"; + +interface StatCardProps { + title: string; + value: string | number; + description?: string; + icon: LucideIcon; + trend?: { value: number; positive: boolean }; + className?: string; +} + +export function StatCard({ title, value, description, icon: Icon, trend, className }: StatCardProps) { + return ( + + + {title} + + + +
{value}
+ {(description || trend) && ( +

+ {trend && ( + + {trend.positive ? "+" : ""}{trend.value}%{" "} + + )} + {description} +

+ )} +
+
+ ); +} diff --git a/apps/web-dashboard/src/components/ui/badge.tsx b/apps/web-dashboard/src/components/ui/badge.tsx new file mode 100644 index 0000000..777db9f --- /dev/null +++ b/apps/web-dashboard/src/components/ui/badge.tsx @@ -0,0 +1,32 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-medium transition-colors", + { + variants: { + variant: { + default: "border-transparent bg-primary text-primary-foreground", + secondary: "border-transparent bg-secondary text-secondary-foreground", + destructive: "border-transparent bg-destructive/10 text-destructive", + outline: "text-foreground", + success: "border-transparent bg-emerald-500/10 text-emerald-600 dark:text-emerald-400", + warning: "border-transparent bg-amber-500/10 text-amber-600 dark:text-amber-400", + }, + }, + defaultVariants: { variant: "default" }, + } +) + +function Badge({ + className, + variant, + ...props +}: React.ComponentProps<"span"> & VariantProps) { + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/apps/web-dashboard/src/lib/api.ts b/apps/web-dashboard/src/lib/api.ts new file mode 100644 index 0000000..bd8a072 --- /dev/null +++ b/apps/web-dashboard/src/lib/api.ts @@ -0,0 +1,69 @@ +const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"; + +async function apiFetch(path: string, init?: RequestInit): Promise { + const res = await fetch(`${API_BASE}${path}`, { + headers: { "Content-Type": "application/json", ...init?.headers }, + ...init, + }); + if (!res.ok) { + const body = await res.json().catch(() => ({})); + throw new Error(body.message || res.statusText); + } + return res.json(); +} + +export interface Subscriber { + id: number; + imsi: string; + msisdn: string; + first_name: string; + last_name: string; + email: string; + organization_id: string; + status: "active" | "inactive" | "suspended" | "terminated" | "provisioning"; + plan_id: number; + balance: number; + profile_status: string; + created_at: string; + updated_at: string; +} + +export interface SystemStats { + active_sessions: number; + total_accounts: number; + blocked_users: number; + low_balance_alerts: number; + uptime: number; +} + +export interface HealthStatus { + redis_connected: boolean; + active_sync: boolean; + last_sync: string; + memory_usage: number; +} + +export interface PaginatedResponse { + data: T[]; + total: number; + page: number; + page_size: number; +} + +export const api = { + subscribers: { + list: (page = 1, pageSize = 20) => + apiFetch>(`/api/subscribers?page=${page}&pageSize=${pageSize}`), + get: (id: number) => apiFetch(`/api/subscribers/${id}`), + create: (data: Partial) => + apiFetch("/api/subscribers", { method: "POST", body: JSON.stringify(data) }), + update: (id: number, data: Partial) => + apiFetch(`/api/subscribers/${id}`, { method: "PUT", body: JSON.stringify(data) }), + delete: (id: number) => + apiFetch(`/api/subscribers/${id}`, { method: "DELETE" }), + }, + system: { + stats: () => apiFetch("/api/system/stats"), + health: () => apiFetch("/api/system/health"), + }, +};