Models, pipelines, users, subscriptions, usage, audit log, ads, and releases still rendered placeholder or duplicated implementations from the earlier admin split. They now read the live back-office API, share one sidebar and console theme, and the license issuer dialog uses the rotated signing key.
452 lines
17 KiB
TypeScript
452 lines
17 KiB
TypeScript
// apps/admin/src/app/(admin)/page.tsx
|
||
// D3RO Voice Admin CRM — Unified Dashboard Overview (Midnight Glass v2)
|
||
|
||
import { Box, Typography, Button } from '@mui/material'
|
||
import { fetchServerStats } from '@/lib/api-server'
|
||
import { isSupabaseAdminConfigured } from '@/lib/supabase-admin'
|
||
import { fetchSubscriptionRevenue, type SubscriptionRevenue } from '@/lib/subscription-metrics'
|
||
import { C, FONT_SANS, FONT_MONO, panelSx, tableSx, statusBadgeSx } from '@/lib/console-theme'
|
||
import { DoubleBezelCard, StatRing, TactileBadge } from '@d3ro/ui/components/ds'
|
||
import Link from 'next/link'
|
||
|
||
export default async function AdminOverviewPage(): Promise<React.ReactElement> {
|
||
const stats = await fetchServerStats()
|
||
const nonOperationalNodes = stats.nodes.filter((node) => node.status !== 'operational')
|
||
|
||
let revenue: SubscriptionRevenue | null = null
|
||
if (isSupabaseAdminConfigured()) {
|
||
try {
|
||
revenue = await fetchSubscriptionRevenue()
|
||
} catch {
|
||
revenue = null
|
||
}
|
||
}
|
||
|
||
const revenueCards = revenue
|
||
? [
|
||
{
|
||
title: 'Annual Recurring Revenue',
|
||
value: `$${revenue.arrUsd.toLocaleString()}`,
|
||
subtext: 'Active subscriptions × 12 months',
|
||
color: 'green' as const,
|
||
badge: 'ARR',
|
||
badgeColor: 'green' as const,
|
||
},
|
||
{
|
||
title: 'Monthly Recurring Revenue',
|
||
value: `$${revenue.mrrUsd.toLocaleString()}`,
|
||
subtext: `Pro ${revenue.tierBreakdown.pro.toLocaleString()} · Pro+ ${revenue.tierBreakdown.pro_plus.toLocaleString()}`,
|
||
color: 'blue' as const,
|
||
badge: 'MRR',
|
||
badgeColor: 'blue' as const,
|
||
},
|
||
{
|
||
title: 'Active Subscriptions',
|
||
value: revenue.activeCount.toLocaleString(),
|
||
subtext: 'Supabase subscriptions with status = active',
|
||
color: 'purple' as const,
|
||
badge: 'BILLING',
|
||
badgeColor: 'purple' as const,
|
||
},
|
||
]
|
||
: []
|
||
|
||
const bentoCards = [
|
||
{
|
||
title: 'Backend Uptime',
|
||
value: `${Math.floor(stats.serverUptimeSeconds / 3600).toLocaleString()}h`,
|
||
subtext: 'Measured by the active .NET API process',
|
||
color: 'purple' as const,
|
||
badge: 'RUNTIME',
|
||
badgeColor: 'purple' as const,
|
||
icon: (
|
||
<svg width="22" height="22" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||
</svg>
|
||
),
|
||
},
|
||
{
|
||
title: 'Active Admin Accounts Today',
|
||
value: `${stats.activeUsersToday.toLocaleString()} Active`,
|
||
subtext: `${stats.totalUsers.toLocaleString()} administrator accounts`,
|
||
color: 'blue' as const,
|
||
badge: 'AUTH',
|
||
badgeColor: 'blue' as const,
|
||
icon: (
|
||
<svg width="22" height="22" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 100-6 3 3 0 000 6z" />
|
||
</svg>
|
||
),
|
||
},
|
||
{
|
||
title: 'Total API Requests & Compute',
|
||
value: `${stats.totalRequests.toLocaleString()}`,
|
||
subtext: `Total Compute Cost: $${stats.totalCost.toFixed(4)}`,
|
||
color: 'green' as const,
|
||
badge: 'TELEMETRY',
|
||
badgeColor: 'green' as const,
|
||
icon: (
|
||
<svg width="22" height="22" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||
</svg>
|
||
),
|
||
},
|
||
{
|
||
title: 'Recorded Backend Errors',
|
||
value: stats.errorCount.toLocaleString(),
|
||
subtext: 'Persisted server error log entries',
|
||
color: 'orange' as const,
|
||
badge: 'ERROR LOG',
|
||
badgeColor: 'orange' as const,
|
||
icon: (
|
||
<svg width="22" height="22" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
||
</svg>
|
||
),
|
||
},
|
||
]
|
||
|
||
return (
|
||
<>
|
||
{/* Header bar */}
|
||
<Box
|
||
sx={{
|
||
...panelSx,
|
||
minHeight: 84,
|
||
px: { xs: 2.5, md: 4 },
|
||
py: 2,
|
||
display: 'flex',
|
||
flexWrap: 'wrap',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
gap: 2,
|
||
}}
|
||
>
|
||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||
<Box
|
||
sx={{
|
||
width: 4,
|
||
height: 32,
|
||
borderRadius: '999px',
|
||
background: 'linear-gradient(180deg, var(--d3-tag-cyan) 0%, var(--d3-accent-main) 50%, var(--d3-tag-purple) 100%)',
|
||
}}
|
||
/>
|
||
<Box>
|
||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||
<Typography
|
||
component="h1"
|
||
sx={{
|
||
fontFamily: FONT_SANS,
|
||
fontSize: '20px',
|
||
fontWeight: 500,
|
||
color: C.bright,
|
||
letterSpacing: '-0.02em',
|
||
m: 0,
|
||
}}
|
||
>
|
||
Unified Dashboard Overview
|
||
</Typography>
|
||
<TactileBadge ledColor="green" ledPulse tone="success" mono>
|
||
BACKEND CONNECTED
|
||
</TactileBadge>
|
||
</Box>
|
||
<Typography
|
||
sx={{
|
||
fontFamily: FONT_MONO,
|
||
fontSize: '11px',
|
||
color: C.dim,
|
||
letterSpacing: '0.04em',
|
||
mt: 0.25,
|
||
}}
|
||
>
|
||
Backend runtime counters, error ledger, reported node health
|
||
</Typography>
|
||
</Box>
|
||
</Box>
|
||
|
||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||
<Link href="/pipelines" style={{ textDecoration: 'none' }}>
|
||
<Button
|
||
variant="outlined"
|
||
size="small"
|
||
sx={{
|
||
fontFamily: FONT_SANS,
|
||
fontSize: '12px',
|
||
color: C.bright,
|
||
borderColor: C.borderHl,
|
||
bgcolor: 'var(--d3-bg-card-soft)',
|
||
borderRadius: '10px',
|
||
textTransform: 'none',
|
||
px: 2,
|
||
'&:hover': { borderColor: C.accentLight, bgcolor: 'var(--d3-bg-raised-soft)' },
|
||
}}
|
||
>
|
||
View Pipelines Console →
|
||
</Button>
|
||
</Link>
|
||
</Box>
|
||
</Box>
|
||
|
||
{/* Main Content Area */}
|
||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3.5 }}>
|
||
{/* Revenue KPI Row */}
|
||
{revenueCards.length > 0 && (
|
||
<Box
|
||
sx={{
|
||
display: 'grid',
|
||
gridTemplateColumns: { xs: '1fr', sm: 'repeat(3, 1fr)' },
|
||
gap: 2.5,
|
||
}}
|
||
>
|
||
{revenueCards.map((card) => (
|
||
<DoubleBezelCard
|
||
key={card.title}
|
||
interactive
|
||
bezelPadding="5px"
|
||
innerPadding="20px"
|
||
sx={{ height: '100%' }}
|
||
>
|
||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2 }}>
|
||
<StatRing color={card.color} size={46}>
|
||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '13px', fontWeight: 500 }}>$</Typography>
|
||
</StatRing>
|
||
<Box component="span" sx={statusBadgeSx(card.badgeColor)}>
|
||
{card.badge}
|
||
</Box>
|
||
</Box>
|
||
|
||
<Typography
|
||
sx={{
|
||
fontFamily: FONT_SANS,
|
||
fontSize: '11px',
|
||
fontWeight: 500,
|
||
color: C.dim,
|
||
letterSpacing: '0.04em',
|
||
}}
|
||
>
|
||
{card.title}
|
||
</Typography>
|
||
|
||
<Typography
|
||
sx={{
|
||
fontFamily: FONT_MONO,
|
||
fontSize: '26px',
|
||
fontWeight: 500,
|
||
color: C.bright,
|
||
my: 0.75,
|
||
}}
|
||
>
|
||
{card.value}
|
||
</Typography>
|
||
|
||
<Typography
|
||
sx={{
|
||
fontFamily: FONT_SANS,
|
||
fontSize: '11px',
|
||
color: C.text,
|
||
}}
|
||
>
|
||
{card.subtext}
|
||
</Typography>
|
||
</DoubleBezelCard>
|
||
))}
|
||
</Box>
|
||
)}
|
||
|
||
{/* Executive Bento Grid */}
|
||
<Box
|
||
sx={{
|
||
display: 'grid',
|
||
gridTemplateColumns: { xs: '1fr', sm: '1fr 1fr', lg: 'repeat(4, 1fr)' },
|
||
gap: 2.5,
|
||
}}
|
||
>
|
||
{bentoCards.map((card) => (
|
||
<DoubleBezelCard
|
||
key={card.title}
|
||
interactive
|
||
bezelPadding="5px"
|
||
innerPadding="20px"
|
||
sx={{ height: '100%' }}
|
||
>
|
||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2 }}>
|
||
<StatRing color={card.color} size={46}>
|
||
{card.icon}
|
||
</StatRing>
|
||
<Box component="span" sx={statusBadgeSx(card.badgeColor)}>
|
||
{card.badge}
|
||
</Box>
|
||
</Box>
|
||
|
||
<Typography
|
||
sx={{
|
||
fontFamily: FONT_SANS,
|
||
fontSize: '11px',
|
||
fontWeight: 500,
|
||
color: C.dim,
|
||
letterSpacing: '0.04em',
|
||
}}
|
||
>
|
||
{card.title}
|
||
</Typography>
|
||
|
||
<Typography
|
||
sx={{
|
||
fontFamily: FONT_MONO,
|
||
fontSize: '26px',
|
||
fontWeight: 500,
|
||
color: C.bright,
|
||
my: 0.75,
|
||
}}
|
||
>
|
||
{card.value}
|
||
</Typography>
|
||
|
||
<Typography
|
||
sx={{
|
||
fontFamily: FONT_SANS,
|
||
fontSize: '11px',
|
||
color: C.text,
|
||
}}
|
||
>
|
||
{card.subtext}
|
||
</Typography>
|
||
</DoubleBezelCard>
|
||
))}
|
||
</Box>
|
||
|
||
{/* Live System Nodes Grid */}
|
||
<DoubleBezelCard bezelPadding="6px" innerPadding="24px">
|
||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2.5 }}>
|
||
<Box>
|
||
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '16px', fontWeight: 500, color: C.bright }}>
|
||
System Nodes & Pipeline Topology
|
||
</Typography>
|
||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '11px', color: C.dim }}>
|
||
{stats.nodes.length === 0
|
||
? 'NODE TELEMETRY NOT REPORTED'
|
||
: `${stats.nodes.length} REPORTED • ${nonOperationalNodes.length} NON-OPERATIONAL`}
|
||
</Typography>
|
||
</Box>
|
||
<TactileBadge tone="mono" mono>
|
||
{stats.nodes.length === 0
|
||
? 'UNAVAILABLE'
|
||
: nonOperationalNodes.length === 0 ? 'ALL REPORTED OPERATIONAL' : 'ATTENTION REQUIRED'}
|
||
</TactileBadge>
|
||
</Box>
|
||
|
||
<Box
|
||
sx={{
|
||
display: 'grid',
|
||
gridTemplateColumns: { xs: '1fr', sm: 'repeat(2, 1fr)', lg: 'repeat(3, 1fr)' },
|
||
gap: 2,
|
||
}}
|
||
>
|
||
{stats.nodes.length === 0 ? (
|
||
<Typography sx={{ gridColumn: '1 / -1', py: 3, textAlign: 'center', color: C.dim, fontFamily: FONT_MONO, fontSize: '12px' }}>
|
||
No node-health telemetry has been reported by the backend.
|
||
</Typography>
|
||
) : stats.nodes.map((node) => (
|
||
<Box
|
||
key={node.id}
|
||
sx={{
|
||
p: 2,
|
||
borderRadius: '14px',
|
||
bgcolor: 'var(--d3-bg-inset-soft)',
|
||
border: `1px solid ${C.border}`,
|
||
transition: 'all 0.2s ease',
|
||
'&:hover': {
|
||
borderColor: C.accentLight,
|
||
bgcolor: 'var(--d3-bg-card-soft)',
|
||
transform: 'translateY(-2px)',
|
||
},
|
||
}}
|
||
>
|
||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 1 }}>
|
||
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '13px', fontWeight: 500, color: C.bright }}>
|
||
{node.name}
|
||
</Typography>
|
||
<Box
|
||
sx={{
|
||
width: 8,
|
||
height: 8,
|
||
borderRadius: '50%',
|
||
bgcolor: node.status === 'operational' ? 'var(--d3-status-success)' : node.status === 'degraded' ? 'var(--d3-status-warning)' : 'var(--d3-status-danger)',
|
||
boxShadow: `0 0 8px ${node.status === 'operational' ? 'var(--d3-status-success)' : node.status === 'degraded' ? 'var(--d3-status-warning)' : 'var(--d3-status-danger)'}`,
|
||
}}
|
||
/>
|
||
</Box>
|
||
|
||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '11px', color: C.cyanLight, mb: 1.5 }}>
|
||
{node.versionOrModel}
|
||
</Typography>
|
||
|
||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', pt: 1, borderTop: `1px solid ${C.border}` }}>
|
||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '10px', color: C.dim }}>
|
||
Latency: <strong style={{ color: C.bright }}>{node.latencyMs}ms</strong>
|
||
</Typography>
|
||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '10px', color: node.status === 'operational' ? C.green400 : node.status === 'degraded' ? C.orange400 : C.red400 }}>
|
||
{node.uptimePercent}% · {node.status.toUpperCase()}
|
||
</Typography>
|
||
</Box>
|
||
</Box>
|
||
))}
|
||
</Box>
|
||
</DoubleBezelCard>
|
||
|
||
{/* Server Operational Telemetry Logs */}
|
||
<DoubleBezelCard bezelPadding="6px" innerPadding="24px">
|
||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '15px', fontWeight: 500, color: C.bright }}>
|
||
Operational Telemetry & Server Logs
|
||
</Typography>
|
||
<TactileBadge tone="mono" mono>
|
||
SERVER SNAPSHOT
|
||
</TactileBadge>
|
||
</Box>
|
||
|
||
<Box sx={{ overflowX: 'auto' }}>
|
||
<Box component="table" sx={tableSx}>
|
||
<Box component="thead">
|
||
<Box component="tr">
|
||
<Box component="th">ID</Box>
|
||
<Box component="th">STATUS</Box>
|
||
<Box component="th">MESSAGE</Box>
|
||
<Box component="th">ENDPOINT</Box>
|
||
<Box component="th">TIMESTAMP</Box>
|
||
</Box>
|
||
</Box>
|
||
<Box component="tbody">
|
||
{stats.recentErrors.length === 0 ? (
|
||
<Box component="tr">
|
||
<Box component="td" colSpan={5} sx={{ textAlign: 'center', color: C.green400, py: 3 }}>
|
||
No backend errors have been recorded.
|
||
</Box>
|
||
</Box>
|
||
) : (
|
||
stats.recentErrors.map((err) => (
|
||
<Box component="tr" key={err.id}>
|
||
<Box component="td">#{err.id}</Box>
|
||
<Box component="td">
|
||
<Box component="span" sx={statusBadgeSx('red')}>
|
||
{err.errorType}
|
||
</Box>
|
||
</Box>
|
||
<Box component="td">{err.message}</Box>
|
||
<Box component="td" sx={{ fontFamily: FONT_MONO, fontSize: '11px' }}>
|
||
{err.endpoint || '-'}
|
||
</Box>
|
||
<Box component="td" sx={{ color: C.dim }}>
|
||
{new Date(err.createdAt).toLocaleString()}
|
||
</Box>
|
||
</Box>
|
||
))
|
||
)}
|
||
</Box>
|
||
</Box>
|
||
</Box>
|
||
</DoubleBezelCard>
|
||
</Box>
|
||
</>
|
||
)
|
||
}
|