d3ro-voice/apps/admin/src/app/(admin)/page.tsx
Yun Chan 5aa268970a feat(admin): read live back-office data in every console view
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.
2026-09-16 23:24:35 +09:00

452 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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>
</>
)
}