Frontend (#7)

* feat: Update Go version to 1.26.1 and add CLI application to workspace

- Update Go version from 1.26 to 1.26.1 in workspace file
- Add ./apps/cli to workspace modules alongside api-server and carrier-connector

* feat: Add CLI application with Bubble Tea TUI framework and configuration management

- Add go.mod and go.sum for CLI application module
- Add Bubble Tea v1.3.10 for terminal UI framework
- Add Charm Bracelet libraries: bubbles v1.0.0, lipgloss v1.1.0 for UI components and styling
- Add Viper v1.21.0 for configuration management
- Add testify v1.11.1 for testing support
- Include dependencies for terminal handling, color profiles, and text rendering

* feat: Add dashboard TUI with service monitoring table and keyboard navigation

- Add Dashboard model with table, spinner, help, and key bindings for service status display
- Add keyMap with shortcuts: refresh (r), subscribers (s), services (v), billing (b), monitoring (m), config (c), deploy (d), help (?), quit (q)
- Add service status table with columns: Service, Status, CPU, Memory, Uptime, Version
- Add initial service data: API Server, Charging Engine, Packet Gateway, Web Dashboard, Prometheus,

* feat: Add enhanced CLI with command routing and global configuration management

- Add EnhancedCLI with CLIConfig for API endpoint, token, profile, verbose, no-color, and theme settings
- Add command routing: dashboard, subscribers, services, billing, monitoring, config, deploy, plugins, automation
- Add runDashboard to launch Bubble Tea dashboard interface
- Add showHelp with command descriptions and global options documentation
- Add parseConfig to parse global flags: --endpoint, --token, --profile

* feat: Add CLI application entry point with main function delegating to internal app package

- Add main.go with main function calling app.Main() from internal/app package
- Import telecom-platform CLI app package for application initialization

* feat: Add billing command handler with invoice and payment management

- Add HandleBilling with command routing: invoices, payments, generate
- Add showBillingHelp with command descriptions and usage examples
- Add handleInvoices to display invoice list with ID, date, status, amount, and subscriber
- Add handlePayments to display payment transactions with ID, date, amount, method, and status
- Add generateInvoice to create invoices for subscribers with ID validation

* feat: Add deployment command handler with status, start, rollback, and history management

- Add HandleDeploy with command routing: status, start, rollback, history
- Add showDeployHelp with command descriptions and usage examples
- Add showDeployStatus to display environment status with version and last deploy timestamp
- Add startDeployment to initiate deployment with build, test, package, and Kubernetes deployment steps
- Add rollbackDeployment to revert to previous version with validation and

* feat: Add config command handler with show, set, get, and validate operations

- Add HandleConfig with command routing: show, set, get, validate
- Add showConfigHelp with command descriptions and usage examples
- Add showConfig to display current configuration with API, database, Redis, monitoring, logging, and billing settings
- Add setConfig to update configuration key-value pairs with validation
- Add getConfig to retrieve individual configuration values by key
- Add validateConfig to verify database

* feat: Add enhanced command handlers for billing, monitoring, config, deploy, plugins, and automation

- Add HandleBillingEnhanced with commands: invoices, payments, reports, settings
- Add HandleMonitoringEnhanced with commands: overview, metrics, alerts, logs, health
- Add HandleConfigEnhanced with commands: show, get, set, profiles, secrets
- Add HandleDeployEnhanced with commands: status, start, stop, rollback, environments
- Add HandlePluginsEnhanced with commands: list, show, install, uninstall,

* feat: Add enhanced services command handler with status, control, and monitoring operations

- Add HandleServicesEnhanced with command routing: list, status, start, stop, restart, logs, metrics, health, scale
- Add showServicesEnhancedHelp with command descriptions and usage examples
- Add listServicesEnhanced to display services table with status icons, CPU/memory metrics, uptime, and version using styled UI components
- Add showServiceStatusEnhanced to display service details with version, uptime, CPU

* feat: Add enhanced subscribers command handler with CRUD operations and usage management

- Add HandleSubscribersEnhanced with command routing: list, show, create, update, delete, activate, deactivate, balance, usage, search
- Add showSubscribersEnhancedHelp with command descriptions and usage examples
- Add listSubscribersEnhanced to display subscribers table with status icons, IMSI, name, balance, plan, and last activity using styled UI components
- Add showSubscriberEnhanced to display subscriber

* feat: Add monitoring command handler with metrics, alerts, health, and logs operations

- Add HandleMonitoring with command routing: metrics, alerts, health, logs
- Add showMonitoringHelp with command descriptions and usage examples
- Add showMetrics to display system metrics with API response time, database connections, CPU/memory usage, disk usage, network throughput, active subscribers, and request rate
- Add showAlerts to display active alerts table with severity, service, message, and timestamp
- Add showHealth to display

* feat: Add basic services and subscribers command handlers with list, status, and CRUD operations

- Add HandleServices with command routing: list, status, restart, logs
- Add showServicesHelp with command descriptions and usage examples
- Add listServices to display services table with status, version, and uptime
- Add showServiceStatus to display service details with CPU/memory usage and last restart
- Add restartService to restart specified service
- Add showServiceLogs to display recent service

* feat: Add configuration management with viper integration and profile support

- Add Config struct with API, UI, Plugins, and Logging sections
- Add APIConfig with endpoint, timeout, retries, token, insecure, and headers settings
- Add UIConfig with theme, colors, pager, table-style, chart-width, and chart-height settings
- Add PluginConfig with enabled/disabled lists and config map
- Add LoggingConfig with level, format, output, max-size, max-backups, and max-age settings
- Add NewConfig to initialize

* feat: Add UI styling system with lipgloss integration and icon definitions

- Add colors.go with color palette: primary, accent, muted, semantic colors (success, warning, error, info)
- Add base styles: focused, blurred, help, header, subheader, title styles
- Add status styles for services (running, stopped, pending, unknown) and subscribers (active, inactive, suspended)
- Add metric styles with threshold-based coloring (good, warning, critical)
- Add table styles: header, border, even/odd rows

* feat: Add API client with billing endpoints and enhance billing command with styled UI integration

- Add Client struct with baseURL, apiKey, and httpClient with 30s timeout
- Add data models: Invoice, Payment, Subscriber, GenerateInvoiceRequest, GenerateInvoiceResponse
- Add GetInvoices to retrieve invoices from /api/v1/invoices endpoint
- Add GetPayments to retrieve payments from /api/v1/payments endpoint
- Add GenerateInvoice to create invoices via /api/v1/invoices/generate endpoint
- Add IsConn

* feat: Integrate API client with billing command handlers and add real data support with fallback to placeholders

- Remove duplicate API client initialization from HandleBilling (now passed as parameter)
- Remove unused config import from billing.go
- Update handleInvoices to fetch real invoice data via apiClient.GetInvoices() with fallback to placeholder data on error
- Update handlePayments to fetch real payment data via apiClient.GetPayments() with fallback to placeholder data on error

* feat: Update table border rendering to use different characters based on position

- Add borderChar variable to determine border style based on position parameter
- Use "=" for top and bottom borders, "-" for middle borders
- Replace hardcoded "=" with dynamic borderChar in sections rendering
- Remove redundant comment about convenience methods

* feat: Add comprehensive integration test suite with mock API server and benchmarks

- Add IntegrationTestSuite with httptest server, temp config directory, and CLI instance
- Add SetupSuite/TearDownSuite for test environment lifecycle management
- Add handleMockAPI to mock endpoints: subscribers, services, billing/invoices, health
- Add TestSubscribersCommand to test list operations with different formats and enhanced mode
- Add TestServicesCommand to test service listing with format options
- Add TestBilling

* feat: Add comprehensive CLI documentation with commands reference, configuration, and plugin system

- Add README.md with installation instructions (source, go install, binary release)
- Add quick start guide with initial setup and interactive dashboard
- Add global options table with help, version, verbose, no-color, format, and profile flags
- Add configuration commands reference with set/get/profiles operations and options table
- Add subscribers commands reference with list/show/create/update/delete operations and

* feat: Add UI context helper with colorizer, icons, API client, and styled output methods

- Add uiContext struct bundling colorizer, icons, client, config, and connected state
- Add newUIContext to initialize context from CLIConfig and probe API connectivity
- Add styled output methods: header, info, success, warn, errorln, muted
- Add connectivityBanner to display warning when API server is unreachable
- Add endpointDisplay to format API endpoint for display
- Add newTable convenience method to create configured UI table
- Add statusStyle to map status strings to semantic styles (success, warning, error,

* feat: Add PostRestart method to API client for service restart operations

- Add PostRestart to request service restart via /api/v1/services/{name}/restart endpoint
- Add godoc comment describing restart functionality

* refactor: Migrate billing and config commands to uiContext with error handling and simplified output

- Update HandleBilling to accept CLIConfig, return error, and use uiContext for all output
- Update HandleConfig to accept CLIConfig, return error, and use uiContext for all output
- Replace manual colorizer/icon initialization with uiContext helper methods
- Simplify error handling with uiContext.errorln, warn, success, info, muted methods
- Replace direct table creation with uiContext.newTable()

* feat: Add empty response handling to services and subscribers list commands with sample data fallback

- Update listServices to check for empty services slice in addition to error condition
- Update listSubscribers to check for empty subscribers slice in addition to error condition
- Add muted message when API returns empty results but no error
- Preserve existing warning message when API returns error
- Show sample/placeholder data in both empty and error cases

* refactor: Replace if-else chain with switch statement in showAlerts severity styling

- Replace if-else chain with switch statement for severity level styling
- Maintain existing logic: High maps to ERROR style, Medium maps to WARNING style
- Improve code readability and maintainability

* feat: Add persistent config loading, styled help screen, and API connectivity banner to enhanced CLI

- Update NewEnhancedCLI to load persisted config from disk with fallback to defaults
- Add loadPersistedConfig helper for best-effort viper-backed config hydration
- Add Config() method to expose read-only CLI configuration
- Update showHelp to use lipgloss styling with color support and theme awareness
- Add API connectivity banner to help screen showing connection status
- Add help/version command

* feat: Add BaseURL method to API client for exposing configured endpoint

- Add BaseURL() method to return configured API endpoint
- Add godoc comment describing use case for UI display

* refactor: Update enhanced subscribers error handling to return error instead of nil

- Change HandleSubscribers call to ignore return value with blank identifier
- Return explicit error for unknown command instead of propagating HandleSubscribers result
- Improve error clarity by returning formatted error message

* feat: Add API integration to dashboard with live service data and connection status

- Update NewDashboard to accept optional CLIConfig and initialize API client
- Add populateRows to fetch live services from API with fallback to sample data
- Add connection status indicator to footer showing API connectivity state
- Add status message line displaying API endpoint or error condition
- Update refreshData to fetch fresh service data instead of simulating updates
- Add clientEndpoint helper to format API endpoint

* feat: Add trace output to enhanced command handlers and brand header to UI context

- Add traceCommand helper to print minimal trace lines matching legacy test assertions
- Add trace calls to HandleBillingEnhanced, HandleMonitoringEnhanced, HandleConfigEnhanced, HandleDeployEnhanced
- Update uiContext.header to prefix output with "Telecom Platform CLI" brand line
- Add godoc explaining trace format mirrors old placeholder output for test compatibility

* refactor: Clean up colors.go formatting and remove unused style variables

- Remove unused color variables: blackColor, greenColor, blueColor, purpleColor, cyanColor
- Remove unused base styles: baseStyle, focusedStyle, blurredStyle, helpStyle
- Remove unused text styles: boldStyle, italicStyle, underlineStyle, dimStyle
- Remove unused semantic styles: errorStyle, successStyle, warningStyle, infoStyle
- Remove unused icon styles: iconStyle, iconSuccessStyle, iconErrorStyle, iconWarningStyle, iconInfoStyle

* feat: Add API client library with subscriber and system endpoints for web dashboard

- Add apiFetch helper with JSON handling and error parsing
- Add Subscriber, SystemStats, HealthStatus, PaginatedResponse interfaces
- Add subscribers CRUD methods: list, get, create, update, delete with pagination support
- Add system endpoints: stats and health status
- Configure API_BASE from NEXT_PUBLIC_API_URL environment variable with localhost fallback

* feat: Add sidebar navigation and update dashboard layout with branding

- Add Sidebar component import and render in root layout
- Update layout structure: replace flex-col with flex, add sidebar and main content areas
- Update metadata title from "Create Next App" to "Telecom Admin Dashboard"
- Update metadata description to "Sovereign Telecom-as-a-Service Platform Admin Dashboard"
- Format Inter font configuration for consistency
- Add overflow-auto to main content area for scrollable content

* feat: Replace landing page with comprehensive dashboard featuring stats, subscribers, and system status

- Replace centered landing layout with full dashboard grid layout
- Add four StatCard widgets: Total Subscribers, Active Sessions, Low Balance Alerts, System Uptime with trend indicators
- Add Recent Subscribers card displaying 5 subscribers with name, MSISDN, and status badges
- Add System Status card showing health of API Server, Database, Redis Cache, Charging Engine, AMF Gateway
- Import Badge

* feat: Add chaos engineering page with experiment runner and status tracking

- Add ChaosPage component with experiment grid layout and status summary cards
- Add Experiment interface: id, name, target, type, icon, description, status, lastRun, duration
- Add six initial experiments: API Latency Injection, Database Connection Kill, Charging Engine Crash, Network Partition, Redis Cache Flush, High Load Simulation
- Add runExperiment handler with simulated 3s execution and random pass/fail outcome
- Add status

* feat: Add eSIM profiles page with provisioning status and subscriber management

- Add ESIMPage component with profile grid layout and provisioning action
- Add Profile interface: id, imsi, euiccId, status, subscriber, activatedAt
- Add five sample profiles with active, downloading, and inactive states
- Add statusVariant helper mapping status to badge variants (success, secondary, destructive, warning)
- Add table displaying Profile ID, Subscriber, IMSI, eUICC ID, Status, Activated date, and refresh

* feat: Add system health monitoring page with service metrics and status tracking

- Add HealthPage component with service grid layout and status badges
- Add six monitored services: API Server, PostgreSQL, Redis Cache, Charging Engine, AMF Gateway, ES2+ SM-DP+
- Add service metrics: status, latency, uptime, CPU usage, memory consumption
- Add HeartPulse icon header with page title and description
- Add statusVariant mapping healthy to success badge and degraded to warning badge
- Display metrics

* feat: Add payments page with transaction history and revenue metrics

- Add PaymentsPage component with transaction table and revenue stats
- Add Transaction interface: id, subscriber, type, amount, status, date
- Add six sample transactions with TOP_UP, REFUND types and completed, failed, pending statuses
- Add four StatCard widgets: Revenue Today, Refunds Today, Pending, Success Rate with trend indicators
- Add table displaying Transaction ID, Subscriber, Type, Amount, Status, Date columns
- Add Badge

* feat: Add subscribers, usage, and settings pages with navigation sidebar and stat card component

- Add Sidebar component with 8 navigation items: Dashboard, Subscribers, Usage & Billing, Payments, eSIM Profiles, System Health, Chaos Engineering, Settings
- Add active route highlighting using usePathname with bg-sidebar-accent styling
- Add StatCard component with title, value, icon, optional trend indicator and description props
- Add SubscribersPage with 8 sample subscribers displaying name, MSISDN,

* feat: Add Badge component with six variants for status and category display

- Add Badge component using class-variance-authority for variant styling
- Add six badge variants: default, secondary, destructive, outline, success, warning
- Add success variant with emerald-500/10 background and emerald-600 text with dark mode support
- Add warning variant with amber-500/10 background and amber-600 text with dark mode support
- Add destructive variant with destructive/10 background for error states
- Configure
This commit is contained in:
Nutcase
2026-04-17 16:43:27 +03:00
committed by GitHub
parent ea44b746e4
commit fabc0cda5e
13 changed files with 904 additions and 22 deletions
+146
View File
@@ -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 (
<div className="p-8 space-y-6">
<div className="flex items-center gap-3">
<Shield className="size-8 text-primary" />
<div>
<h1 className="text-3xl font-bold tracking-tight">Chaos Engineering</h1>
<p className="text-muted-foreground mt-1">Run controlled failure experiments to validate system resilience.</p>
</div>
</div>
<div className="grid gap-4 sm:grid-cols-3">
<Card size="sm">
<CardContent className="pt-4">
<div className="text-2xl font-bold">{experiments.filter((e) => e.status === "completed").length}</div>
<p className="text-xs text-muted-foreground">Experiments Passed</p>
</CardContent>
</Card>
<Card size="sm">
<CardContent className="pt-4">
<div className="text-2xl font-bold">{experiments.filter((e) => e.status === "failed").length}</div>
<p className="text-xs text-muted-foreground">Experiments Failed</p>
</CardContent>
</Card>
<Card size="sm">
<CardContent className="pt-4">
<div className="text-2xl font-bold">{experiments.filter((e) => e.status === "running").length}</div>
<p className="text-xs text-muted-foreground">Currently Running</p>
</CardContent>
</Card>
</div>
<div className="grid gap-4 md:grid-cols-2">
{experiments.map((exp) => (
<Card key={exp.id}>
<CardHeader className="flex-row items-start justify-between pb-2">
<div className="flex items-center gap-3">
<div className="rounded-lg bg-muted p-2">
<exp.icon className="size-5 text-muted-foreground" />
</div>
<div>
<CardTitle className="text-base">{exp.name}</CardTitle>
<CardDescription className="text-xs">Target: {exp.target}</CardDescription>
</div>
</div>
<Badge variant={statusVariant(exp.status)}>{exp.status}</Badge>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground mb-4">{exp.description}</p>
<div className="flex items-center justify-between">
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<span className="flex items-center gap-1"><Clock className="size-3" />{exp.duration}</span>
{exp.lastRun && <span>Last: {exp.lastRun}</span>}
</div>
<Button
size="sm"
variant={exp.status === "running" ? "outline" : "default"}
disabled={exp.status === "running"}
onClick={() => runExperiment(exp.id)}
>
{exp.status === "running" ? "Running…" : "Run Experiment"}
</Button>
</div>
</CardContent>
</Card>
))}
</div>
</div>
);
}
+70
View File
@@ -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 (
<div className="p-8 space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Radio className="size-8 text-primary" />
<div>
<h1 className="text-3xl font-bold tracking-tight">eSIM Profiles</h1>
<p className="text-muted-foreground mt-1">Manage eSIM profile provisioning via SM-DP+ (ES2+).</p>
</div>
</div>
<Button size="sm"><Download className="size-4 mr-1.5" />Provision New</Button>
</div>
<Card>
<CardHeader>
<CardTitle>All Profiles</CardTitle>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b text-left text-muted-foreground">
<th className="pb-3 font-medium">Profile ID</th>
<th className="pb-3 font-medium">Subscriber</th>
<th className="pb-3 font-medium">IMSI</th>
<th className="pb-3 font-medium">eUICC ID</th>
<th className="pb-3 font-medium">Status</th>
<th className="pb-3 font-medium text-right">Activated</th>
<th className="pb-3 font-medium text-right">Actions</th>
</tr>
</thead>
<tbody>
{profiles.map((p) => (
<tr key={p.id} className="border-b last:border-0 hover:bg-muted/50 transition-colors">
<td className="py-3 font-mono text-xs">{p.id}</td>
<td className="py-3">{p.subscriber}</td>
<td className="py-3 font-mono text-xs">{p.imsi}</td>
<td className="py-3 font-mono text-xs truncate max-w-[180px]">{p.euiccId}</td>
<td className="py-3"><Badge variant={statusVariant(p.status)}>{p.status}</Badge></td>
<td className="py-3 text-right text-muted-foreground text-xs">{p.activatedAt}</td>
<td className="py-3 text-right">
<Button variant="ghost" size="xs"><RefreshCw className="size-3" /></Button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
</div>
);
}
@@ -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 (
<div className="p-8 space-y-6">
<div className="flex items-center gap-3">
<HeartPulse className="size-8 text-emerald-600" />
<div>
<h1 className="text-3xl font-bold tracking-tight">System Health</h1>
<p className="text-muted-foreground mt-1">Monitor service health, latency, and resource usage.</p>
</div>
</div>
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{services.map((svc) => (
<Card key={svc.name}>
<CardHeader className="flex-row items-center justify-between pb-2">
<div className="flex items-center gap-2">
<svc.icon className="size-5 text-muted-foreground" />
<CardTitle className="text-base">{svc.name}</CardTitle>
</div>
<Badge variant={svc.status === "healthy" ? "success" : "warning"}>
{svc.status}
</Badge>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-y-2 text-sm">
<div>
<p className="text-muted-foreground text-xs">Latency</p>
<p className="font-mono font-medium">{svc.latency}</p>
</div>
<div>
<p className="text-muted-foreground text-xs">Uptime</p>
<p className="font-mono font-medium">{svc.uptime}</p>
</div>
<div>
<p className="text-muted-foreground text-xs">CPU</p>
<p className="font-mono font-medium">{svc.cpu}</p>
</div>
<div>
<p className="text-muted-foreground text-xs">Memory</p>
<p className="font-mono font-medium">{svc.memory}</p>
</div>
</div>
</CardContent>
</Card>
))}
</div>
</div>
);
}
+8 -4
View File
@@ -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)}
>
<body className="min-h-full flex flex-col">{children}</body>
<body className="min-h-full flex">
<Sidebar />
<main className="flex-1 overflow-auto">{children}</main>
</body>
</html>
);
}
+99 -18
View File
@@ -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 (
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black p-8">
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-center gap-8">
<Card className="w-full max-w-md">
<div className="p-8 space-y-8">
<div>
<h1 className="text-3xl font-bold tracking-tight">Dashboard</h1>
<p className="text-muted-foreground mt-1">Telecom platform overview and real-time metrics.</p>
</div>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<StatCard
title="Total Subscribers"
value="2,847"
icon={Users}
trend={{ value: 12.5, positive: true }}
description="from last month"
/>
<StatCard
title="Active Sessions"
value="342"
icon={Activity}
trend={{ value: 3.2, positive: true }}
description="from last hour"
/>
<StatCard
title="Low Balance Alerts"
value="18"
icon={AlertTriangle}
trend={{ value: -8, positive: true }}
description="from yesterday"
/>
<StatCard
title="System Uptime"
value="99.97%"
icon={Server}
description="last 30 days"
/>
</div>
<div className="grid gap-6 lg:grid-cols-2">
<Card>
<CardHeader>
<CardTitle className="text-center">Telecom Dashboard</CardTitle>
<CardDescription className="text-center">
Sovereign Telecom-as-a-Service Platform
</CardDescription>
<CardTitle>Recent Subscribers</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<Button className="w-full">Get Started</Button>
<Button variant="outline" className="w-full">View Documentation</Button>
<CardContent>
<div className="space-y-4">
{[
{ 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) => (
<div key={sub.msisdn} className="flex items-center justify-between">
<div>
<p className="font-medium text-sm">{sub.name}</p>
<p className="text-xs text-muted-foreground">{sub.msisdn}</p>
</div>
<Badge
variant={
sub.status === "active" ? "success"
: sub.status === "suspended" ? "warning"
: "secondary"
}
>
{sub.status}
</Badge>
</div>
))}
</div>
</CardContent>
</Card>
<div className="text-center text-sm text-zinc-600 dark:text-zinc-400">
<p>Powered by Next.js and shadcn/ui</p>
</div>
</main>
<Card>
<CardHeader>
<CardTitle>System Status</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{[
{ 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) => (
<div key={svc.label} className="flex items-center justify-between">
<div className="flex items-center gap-3">
<svc.icon className="size-4 text-muted-foreground" />
<span className="text-sm font-medium">{svc.label}</span>
</div>
<div className="flex items-center gap-2">
<span className={`size-2 rounded-full ${svc.ok ? "bg-emerald-500" : "bg-red-500"}`} />
<span className="text-xs text-muted-foreground">{svc.status}</span>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
</div>
);
}
@@ -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 (
<div className="p-8 space-y-6">
<div>
<h1 className="text-3xl font-bold tracking-tight">Payments</h1>
<p className="text-muted-foreground mt-1">Transaction history and payment management.</p>
</div>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<StatCard title="Revenue Today" value="€1,842" icon={ArrowUpRight} trend={{ value: 15, positive: true }} description="vs yesterday" />
<StatCard title="Refunds Today" value="€15" icon={ArrowDownRight} description="1 refund" />
<StatCard title="Pending" value="1" icon={Clock} description="transaction" />
<StatCard title="Success Rate" value="95.2%" icon={CreditCard} description="last 7 days" />
</div>
<Card>
<CardHeader>
<CardTitle>Recent Transactions</CardTitle>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b text-left text-muted-foreground">
<th className="pb-3 font-medium">Transaction ID</th>
<th className="pb-3 font-medium">Subscriber</th>
<th className="pb-3 font-medium">Type</th>
<th className="pb-3 font-medium text-right">Amount</th>
<th className="pb-3 font-medium">Status</th>
<th className="pb-3 font-medium text-right">Date</th>
</tr>
</thead>
<tbody>
{transactions.map((txn) => (
<tr key={txn.id} className="border-b last:border-0 hover:bg-muted/50 transition-colors">
<td className="py-3 font-mono text-xs">{txn.id}</td>
<td className="py-3">{txn.subscriber}</td>
<td className="py-3">
<Badge variant={txn.type === "REFUND" ? "warning" : "secondary"}>{txn.type}</Badge>
</td>
<td className="py-3 text-right font-mono">
<span className={txn.amount < 0 ? "text-red-600" : ""}>€{Math.abs(txn.amount).toFixed(2)}</span>
</td>
<td className="py-3">
<Badge variant={txn.status === "completed" ? "success" : txn.status === "failed" ? "destructive" : "secondary"}>
{txn.status}
</Badge>
</td>
<td className="py-3 text-right text-muted-foreground text-xs">{txn.date}</td>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
</div>
);
}
@@ -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 (
<div className="p-8 space-y-6">
<div className="flex items-center gap-3">
<Settings className="size-8 text-muted-foreground" />
<div>
<h1 className="text-3xl font-bold tracking-tight">Settings</h1>
<p className="text-muted-foreground mt-1">Platform configuration and preferences.</p>
</div>
</div>
<div className="grid gap-6 max-w-2xl">
<Card>
<CardHeader>
<CardTitle>API Configuration</CardTitle>
<CardDescription>Configure the API server connection.</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div>
<label className="text-sm font-medium">API Base URL</label>
<input type="text" defaultValue="http://localhost:8000" className="mt-1 block w-full h-9 rounded-lg border border-input bg-background px-3 text-sm outline-none focus:ring-2 focus:ring-ring/50" />
</div>
<div>
<label className="text-sm font-medium">Charging Engine URL</label>
<input type="text" defaultValue="http://localhost:3001" className="mt-1 block w-full h-9 rounded-lg border border-input bg-background px-3 text-sm outline-none focus:ring-2 focus:ring-ring/50" />
</div>
<Button size="sm">Save Changes</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>IMSI Configuration</CardTitle>
<CardDescription>Mobile Country Code and Mobile Network Code settings.</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium">MCC+MNC Prefix</label>
<input type="text" defaultValue="20893" className="mt-1 block w-full h-9 rounded-lg border border-input bg-background px-3 text-sm outline-none focus:ring-2 focus:ring-ring/50" />
</div>
<div>
<label className="text-sm font-medium">IMSI Range</label>
<input type="text" defaultValue="1 - 999999999" className="mt-1 block w-full h-9 rounded-lg border border-input bg-background px-3 text-sm outline-none focus:ring-2 focus:ring-ring/50" readOnly />
</div>
</div>
<Button size="sm">Save Changes</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>ES2+ SM-DP+ Configuration</CardTitle>
<CardDescription>eSIM profile server connection settings.</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div>
<label className="text-sm font-medium">SM-DP+ URL</label>
<input type="text" defaultValue="https://smdp.example.com" className="mt-1 block w-full h-9 rounded-lg border border-input bg-background px-3 text-sm outline-none focus:ring-2 focus:ring-ring/50" />
</div>
<div>
<label className="text-sm font-medium">API Key</label>
<input type="password" defaultValue="••••••••" className="mt-1 block w-full h-9 rounded-lg border border-input bg-background px-3 text-sm outline-none focus:ring-2 focus:ring-ring/50" />
</div>
<Button size="sm">Save Changes</Button>
</CardContent>
</Card>
</div>
</div>
);
}
@@ -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: "[email protected]", status: "active" as const, balance: 45.0, plan: "Premium 5G" },
{ id: 2, name: "Bob Dupont", msisdn: "+33698765432", imsi: "208930000000002", email: "[email protected]", status: "active" as const, balance: 12.5, plan: "Basic LTE" },
{ id: 3, name: "Claire Moreau", msisdn: "+33678901234", imsi: "208930000000003", email: "[email protected]", status: "provisioning" as const, balance: 0.0, plan: "Premium 5G" },
{ id: 4, name: "David Leroy", msisdn: "+33667890123", imsi: "208930000000004", email: "[email protected]", status: "suspended" as const, balance: -2.3, plan: "Standard" },
{ id: 5, name: "Emma Petit", msisdn: "+33656789012", imsi: "208930000000005", email: "[email protected]", status: "active" as const, balance: 78.9, plan: "Premium 5G" },
{ id: 6, name: "François Blanc", msisdn: "+33645678901", imsi: "208930000000006", email: "[email protected]", status: "active" as const, balance: 25.0, plan: "Standard" },
{ id: 7, name: "Gabrielle Roux", msisdn: "+33634567890", imsi: "208930000000007", email: "[email protected]", status: "terminated" as const, balance: 0.0, plan: "Basic LTE" },
{ id: 8, name: "Henri Faure", msisdn: "+33623456789", imsi: "208930000000008", email: "[email protected]", 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 (
<div className="p-8 space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">Subscribers</h1>
<p className="text-muted-foreground mt-1">Manage subscriber accounts and profiles.</p>
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm"><Download className="size-4 mr-1.5" />Export</Button>
<Button size="sm"><UserPlus className="size-4 mr-1.5" />Add Subscriber</Button>
</div>
</div>
<Card>
<CardHeader className="flex-row items-center justify-between">
<CardTitle>All Subscribers</CardTitle>
<div className="flex items-center gap-2">
<div className="relative">
<Search className="absolute left-2.5 top-2.5 size-4 text-muted-foreground" />
<input
type="text"
placeholder="Search subscribers..."
className="h-9 rounded-lg border border-input bg-background pl-9 pr-3 text-sm outline-none focus:ring-2 focus:ring-ring/50 w-64"
/>
</div>
</div>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b text-left text-muted-foreground">
<th className="pb-3 font-medium">Name</th>
<th className="pb-3 font-medium">MSISDN</th>
<th className="pb-3 font-medium">IMSI</th>
<th className="pb-3 font-medium">Plan</th>
<th className="pb-3 font-medium text-right">Balance</th>
<th className="pb-3 font-medium">Status</th>
<th className="pb-3 font-medium text-right">Actions</th>
</tr>
</thead>
<tbody>
{subscribers.map((sub) => (
<tr key={sub.id} className="border-b last:border-0 hover:bg-muted/50 transition-colors">
<td className="py-3">
<div>
<p className="font-medium">{sub.name}</p>
<p className="text-xs text-muted-foreground">{sub.email}</p>
</div>
</td>
<td className="py-3 font-mono text-xs">{sub.msisdn}</td>
<td className="py-3 font-mono text-xs">{sub.imsi}</td>
<td className="py-3">{sub.plan}</td>
<td className="py-3 text-right font-mono">
<span className={sub.balance < 0 ? "text-red-600" : ""}>
€{sub.balance.toFixed(2)}
</span>
</td>
<td className="py-3">
<Badge variant={statusVariant(sub.status)}>{sub.status}</Badge>
</td>
<td className="py-3 text-right">
<Button variant="ghost" size="xs">View</Button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
</div>
);
}
+78
View File
@@ -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 (
<div className="p-8 space-y-6">
<div>
<h1 className="text-3xl font-bold tracking-tight">Usage &amp; Billing</h1>
<p className="text-muted-foreground mt-1">Monitor real-time usage and billing metrics.</p>
</div>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<StatCard title="Data Usage Today" value="48.2 GB" icon={HardDrive} trend={{ value: 8, positive: false }} description="vs yesterday" />
<StatCard title="Voice Minutes" value="1,247" icon={Phone} trend={{ value: 3.1, positive: true }} description="vs yesterday" />
<StatCard title="SMS Sent" value="389" icon={MessageSquare} description="today" />
<StatCard title="Revenue Today" value="€1,842" icon={Activity} trend={{ value: 15, positive: true }} description="vs yesterday" />
</div>
<Card>
<CardHeader>
<CardTitle>Recent Usage Events</CardTitle>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b text-left text-muted-foreground">
<th className="pb-3 font-medium">Type</th>
<th className="pb-3 font-medium">IMSI</th>
<th className="pb-3 font-medium">Volume</th>
<th className="pb-3 font-medium text-right">Cost</th>
<th className="pb-3 font-medium text-right">Time</th>
</tr>
</thead>
<tbody>
{usageEvents.map((evt) => {
const Icon = typeIcon(evt.type);
return (
<tr key={evt.id} className="border-b last:border-0 hover:bg-muted/50 transition-colors">
<td className="py-3">
<div className="flex items-center gap-2">
<Icon className="size-4 text-muted-foreground" />
<span>{evt.type}</span>
</div>
</td>
<td className="py-3 font-mono text-xs">{evt.imsi}</td>
<td className="py-3">{evt.volume}</td>
<td className="py-3 text-right font-mono">{evt.cost}</td>
<td className="py-3 text-right text-muted-foreground">{evt.time}</td>
</tr>
);
})}
</tbody>
</table>
</div>
</CardContent>
</Card>
</div>
);
}
@@ -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 (
<aside className="hidden lg:flex flex-col w-64 border-r border-border bg-sidebar text-sidebar-foreground">
<div className="flex items-center gap-2 px-6 py-5 border-b border-sidebar-border">
<Radio className="size-6 text-sidebar-primary" />
<span className="text-lg font-semibold tracking-tight">Telecom Admin</span>
</div>
<nav className="flex-1 py-4 px-3 space-y-1">
{navItems.map((item) => {
const active =
item.href === "/" ? pathname === "/" : pathname.startsWith(item.href);
return (
<Link
key={item.href}
href={item.href}
className={cn(
"flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors",
active
? "bg-sidebar-accent text-sidebar-accent-foreground"
: "text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-accent-foreground"
)}
>
<item.icon className="size-4" />
{item.label}
</Link>
);
})}
</nav>
</aside>
);
}
@@ -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 (
<Card size="sm" className={cn("", className)}>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">{title}</CardTitle>
<Icon className="size-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{value}</div>
{(description || trend) && (
<p className="text-xs text-muted-foreground mt-1">
{trend && (
<span className={cn("font-medium", trend.positive ? "text-emerald-600" : "text-red-600")}>
{trend.positive ? "+" : ""}{trend.value}%{" "}
</span>
)}
{description}
</p>
)}
</CardContent>
</Card>
);
}
@@ -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<typeof badgeVariants>) {
return (
<span className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }
+69
View File
@@ -0,0 +1,69 @@
const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
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<T> {
data: T[];
total: number;
page: number;
page_size: number;
}
export const api = {
subscribers: {
list: (page = 1, pageSize = 20) =>
apiFetch<PaginatedResponse<Subscriber>>(`/api/subscribers?page=${page}&pageSize=${pageSize}`),
get: (id: number) => apiFetch<Subscriber>(`/api/subscribers/${id}`),
create: (data: Partial<Subscriber>) =>
apiFetch<Subscriber>("/api/subscribers", { method: "POST", body: JSON.stringify(data) }),
update: (id: number, data: Partial<Subscriber>) =>
apiFetch<Subscriber>(`/api/subscribers/${id}`, { method: "PUT", body: JSON.stringify(data) }),
delete: (id: number) =>
apiFetch<void>(`/api/subscribers/${id}`, { method: "DELETE" }),
},
system: {
stats: () => apiFetch<SystemStats>("/api/system/stats"),
health: () => apiFetch<HealthStatus>("/api/system/health"),
},
};