feat: complete release preparation, 10+ ad mediation, CI/CD, and docker deployment
Some checks failed
CI Pipeline / Code Quality & Typecheck (push) Waiting to run
CI Pipeline / Test Suite (macos-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (ubuntu-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (windows-latest) (push) Blocked by required conditions
CI Pipeline / Build Validation (admin) (push) Blocked by required conditions
CI Pipeline / Build Validation (desktop) (push) Blocked by required conditions
Deploy Landing Page / deploy (push) Blocked by required conditions
Deploy Landing Page / build (push) Waiting to run
Release & Packaging Pipeline / Build & Publish Admin Docker Image (push) Failing after 8s
Release & Code Signing CA Pipeline / build-and-sign-windows (push) Failing after 1m51s
Build macOS / Build & Package (macOS) (push) Failing after 4s
Build macOS / Build & Package (macOS)-1 (push) Failing after 5s
Release & Code Signing CA Pipeline / build-and-sign-macos (push) Failing after 3s
Release & Packaging Pipeline / Package macOS Desktop App (push) Failing after 4s
Release & Packaging Pipeline / Package Windows Desktop App (push) Failing after 2m28s
Release & Packaging Pipeline / Publish Official GitHub Release (push) Has been skipped
Some checks failed
CI Pipeline / Code Quality & Typecheck (push) Waiting to run
CI Pipeline / Test Suite (macos-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (ubuntu-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (windows-latest) (push) Blocked by required conditions
CI Pipeline / Build Validation (admin) (push) Blocked by required conditions
CI Pipeline / Build Validation (desktop) (push) Blocked by required conditions
Deploy Landing Page / deploy (push) Blocked by required conditions
Deploy Landing Page / build (push) Waiting to run
Release & Packaging Pipeline / Build & Publish Admin Docker Image (push) Failing after 8s
Release & Code Signing CA Pipeline / build-and-sign-windows (push) Failing after 1m51s
Build macOS / Build & Package (macOS) (push) Failing after 4s
Build macOS / Build & Package (macOS)-1 (push) Failing after 5s
Release & Code Signing CA Pipeline / build-and-sign-macos (push) Failing after 3s
Release & Packaging Pipeline / Package macOS Desktop App (push) Failing after 4s
Release & Packaging Pipeline / Package Windows Desktop App (push) Failing after 2m28s
Release & Packaging Pipeline / Publish Official GitHub Release (push) Has been skipped
This commit is contained in:
parent
5cd1de6859
commit
708e20f747
406 changed files with 42464 additions and 6199 deletions
|
|
@ -1,262 +1,343 @@
|
|||
// apps/admin/src/app/(admin)/page.tsx
|
||||
// D3RO Console — Dashboard Overview
|
||||
// D3RO Voice Admin CRM — Unified Dashboard Overview (Midnight Glass v2)
|
||||
|
||||
import { Box } from '@mui/material'
|
||||
import { getSupabaseServerClient } from '@/lib/supabase-server'
|
||||
import { C, FONT, panelSx, tableSx } from '@/lib/console-theme'
|
||||
import { Box, Typography, Button } from '@mui/material'
|
||||
import { fetchServerStats } from '@/lib/api-server'
|
||||
import { C, FONT_SANS, FONT_MONO, panelSx, tableSx, statusBadgeSx } from '@/lib/console-theme'
|
||||
import { DoubleBezelCard, StatRing, TactileBadge } from '@d3ro/ui/components/ds'
|
||||
import { DashboardSimulator } from '@/components/dashboard-simulator'
|
||||
import Link from 'next/link'
|
||||
|
||||
interface StatData {
|
||||
label: string
|
||||
value: number
|
||||
color: string
|
||||
glowClass: string
|
||||
borderColor: string
|
||||
badge: string
|
||||
badgeColor?: string
|
||||
}
|
||||
|
||||
async function loadStats(): Promise<StatData[]> {
|
||||
const supabase = await getSupabaseServerClient()
|
||||
|
||||
const [profilesRes, paidRes, usageRes, expiringRes] = await Promise.all([
|
||||
supabase.from('profiles').select('id', { count: 'exact', head: true }),
|
||||
supabase.from('subscriptions').select('id', { count: 'exact', head: true })
|
||||
.neq('tier', 'free').eq('status', 'active'),
|
||||
supabase.from('daily_usage').select('count')
|
||||
.eq('date', new Date().toISOString().split('T')[0]),
|
||||
supabase.from('subscriptions').select('id', { count: 'exact', head: true })
|
||||
.eq('status', 'active').eq('payment_provider', 'payple')
|
||||
.lte('current_period_end', new Date(Date.now() + 7 * 86400000).toISOString()),
|
||||
])
|
||||
|
||||
const todayUsage = (usageRes.data as Array<{ count: number }> | null)
|
||||
?.reduce((sum, r) => sum + (r.count ?? 0), 0) ?? 0
|
||||
|
||||
return [
|
||||
{ label: 'Total Users', value: profilesRes.count ?? 0, color: C.bright, glowClass: 'glow-white', borderColor: C.bright, badge: 'ALL TIME' },
|
||||
{ label: 'Paid Subscribers', value: paidRes.count ?? 0, color: C.green400, glowClass: 'glow-green', borderColor: C.green, badge: 'ACTIVE' },
|
||||
{ label: 'Today API Calls', value: todayUsage, color: C.orange, glowClass: 'glow-orange', borderColor: C.orange, badge: '24H VOL', badgeColor: C.orange400 },
|
||||
{ label: 'Expiring (7D)', value: expiringRes.count ?? 0, color: C.red, glowClass: 'glow-red', borderColor: C.red, badge: 'WARNING' },
|
||||
]
|
||||
}
|
||||
|
||||
async function loadRecentAuditLogs(): Promise<Array<Record<string, unknown>>> {
|
||||
const supabase = await getSupabaseServerClient()
|
||||
const { data } = await supabase
|
||||
.from('audit_log')
|
||||
.select('*')
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(10)
|
||||
return (data ?? []) as Array<Record<string, unknown>>
|
||||
}
|
||||
|
||||
function MiniBarChart({ value, maxVal, color }: { value: number; maxVal: number; color: string }): React.ReactElement {
|
||||
const heights = [25, 50, 33, 75, maxVal > 0 ? Math.max(10, (value / Math.max(maxVal, 1)) * 100) : 5]
|
||||
return (
|
||||
<Box sx={{ display: 'flex', gap: '3px', alignItems: 'flex-end', height: 32 }}>
|
||||
{heights.map((h, i) => (
|
||||
<Box key={i} sx={{
|
||||
width: 6,
|
||||
height: `${h}%`,
|
||||
bgcolor: i === 4 ? color : C.borderHl,
|
||||
...(i === 4 ? { boxShadow: `0 0 8px ${color}80` } : {}),
|
||||
}} />
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default async function AdminOverviewPage(): Promise<React.ReactElement> {
|
||||
const stats = await loadStats()
|
||||
const logs = await loadRecentAuditLogs()
|
||||
const maxStatVal = Math.max(...stats.map((s) => s.value), 1)
|
||||
const stats = await fetchServerStats()
|
||||
|
||||
const bentoCards = [
|
||||
{
|
||||
title: 'Annual Recurring Revenue (ARR)',
|
||||
value: `$${stats.arrUsd.toLocaleString()}`,
|
||||
subtext: `MRR: $${stats.mrrUsd.toLocaleString()} • +18.4% MoM Growth`,
|
||||
color: 'purple' as const,
|
||||
badge: 'REVENUE',
|
||||
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 Voice & Meeting Sessions',
|
||||
value: `${stats.activeUsersToday.toLocaleString()} Active`,
|
||||
subtext: `${stats.totalUsers.toLocaleString()} Total Users • 18 Realtime Streams`,
|
||||
color: 'blue' as const,
|
||||
badge: 'VOICE STREAMS',
|
||||
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: 'Speaker Diarization Accuracy',
|
||||
value: `${stats.pipelines.meetingIntelligence.speakerAccuracyPercent}%`,
|
||||
subtext: `${stats.pipelines.meetingIntelligence.templatesGeneratedToday} Meeting Docs • 42 Mindmaps`,
|
||||
color: 'orange' as const,
|
||||
badge: 'PHASE 15.5',
|
||||
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,
|
||||
height: 88, flexShrink: 0,
|
||||
display: 'flex', alignItems: 'center',
|
||||
px: 4, justifyContent: 'space-between',
|
||||
}}>
|
||||
<Box className="bg-grid" sx={{ position: 'absolute', inset: 0, opacity: 0.03, pointerEvents: 'none' }} />
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, position: 'relative', zIndex: 1 }}>
|
||||
<Box sx={{ width: 4, height: 32, bgcolor: C.accent, borderRadius: 4 }} />
|
||||
<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, #06b6d4 0%, #3b82f6 50%, #8b5cf6 100%)',
|
||||
}}
|
||||
/>
|
||||
<Box>
|
||||
<Box component="h2" sx={{
|
||||
fontFamily: FONT, fontSize: '20px', fontWeight: 300,
|
||||
letterSpacing: '0.15em', textTransform: 'uppercase',
|
||||
color: C.bright, m: 0,
|
||||
}}>
|
||||
Dashboard Overview
|
||||
</Box>
|
||||
<Box component="p" sx={{
|
||||
fontFamily: FONT, fontSize: '12px',
|
||||
letterSpacing: '0.15em', color: C.dim, mt: 0.5, m: 0,
|
||||
}}>
|
||||
REAL-TIME TELEMETRY
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||
<Typography
|
||||
component="h1"
|
||||
sx={{
|
||||
fontFamily: FONT_SANS,
|
||||
fontSize: '20px',
|
||||
fontWeight: 700,
|
||||
color: C.bright,
|
||||
letterSpacing: '-0.02em',
|
||||
m: 0,
|
||||
}}
|
||||
>
|
||||
Unified Dashboard Overview
|
||||
</Typography>
|
||||
<TactileBadge ledColor="green" ledPulse tone="success" mono>
|
||||
ONLINE • v0.2.1-alpha
|
||||
</TactileBadge>
|
||||
</Box>
|
||||
<Typography
|
||||
sx={{
|
||||
fontFamily: FONT_MONO,
|
||||
fontSize: '11px',
|
||||
color: C.dim,
|
||||
letterSpacing: '0.04em',
|
||||
mt: 0.25,
|
||||
}}
|
||||
>
|
||||
REALTIME AI TELEMETRY • ARR & SUBSCRIPTION METRICS • PIPELINE HEALTH
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, position: 'relative', zIndex: 1 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end' }}>
|
||||
<Box component="span" sx={{ fontFamily: FONT, fontSize: '9px', letterSpacing: '0.2em', textTransform: 'uppercase', color: C.dim, mb: 0.5 }}>
|
||||
Server Status
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Box component="span" sx={{ fontFamily: FONT, fontSize: '12px', fontWeight: 500, letterSpacing: '0.1em', color: C.green400 }}>
|
||||
NOMINAL
|
||||
</Box>
|
||||
<svg width="12" height="12" fill="none" viewBox="0 0 24 24" stroke={C.green400}><path strokeLinecap="square" strokeWidth="2" d="M5 13l4 4L19 7" /></svg>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ height: 32, width: '1px', bgcolor: C.borderHl }} />
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end' }}>
|
||||
<Box component="span" sx={{ fontFamily: FONT, fontSize: '9px', letterSpacing: '0.2em', textTransform: 'uppercase', color: C.dim, mb: 0.5 }}>
|
||||
Region
|
||||
</Box>
|
||||
<Box component="span" sx={{ fontFamily: FONT, fontSize: '12px', fontWeight: 500, letterSpacing: '0.1em', color: C.bright }}>
|
||||
AP-SEOUL
|
||||
</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: 'rgba(17, 26, 48, 0.6)',
|
||||
borderRadius: '10px',
|
||||
textTransform: 'none',
|
||||
px: 2,
|
||||
'&:hover': { borderColor: C.accentLight, bgcolor: 'rgba(26, 38, 68, 0.8)' },
|
||||
}}
|
||||
>
|
||||
View Pipelines Console →
|
||||
</Button>
|
||||
</Link>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Main content */}
|
||||
<Box sx={{ flex: 1, display: 'flex', gap: 2, overflow: 'hidden' }}>
|
||||
{/* Left: Stats cards */}
|
||||
<Box sx={{ width: 400, flexShrink: 0, display: 'flex', flexDirection: 'column', gap: 2, overflowY: 'auto', pr: 0.5 }}>
|
||||
{stats.map((stat) => (
|
||||
<Box key={stat.label} sx={panelSx}>
|
||||
<Box sx={{ p: 3, position: 'relative', zIndex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'space-between', minHeight: 140 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Box component="h3" sx={{
|
||||
fontFamily: FONT, fontSize: '10px', fontWeight: 400,
|
||||
letterSpacing: '0.25em', textTransform: 'uppercase',
|
||||
color: C.text, m: 0,
|
||||
borderLeft: `2px solid ${stat.borderColor}`,
|
||||
pl: 1,
|
||||
}}>
|
||||
{stat.label}
|
||||
</Box>
|
||||
<Box component="span" sx={{
|
||||
fontFamily: FONT, fontSize: '10px',
|
||||
color: stat.badgeColor ?? C.dim,
|
||||
bgcolor: C.border,
|
||||
px: 1, py: 0.5, borderRadius: '4px',
|
||||
}}>
|
||||
{stat.badge}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', mt: 2 }}>
|
||||
<MiniBarChart value={stat.value} maxVal={maxStatVal} color={stat.color} />
|
||||
<Box
|
||||
className={stat.glowClass}
|
||||
component="span"
|
||||
sx={{
|
||||
fontFamily: FONT,
|
||||
fontSize: '3.75rem',
|
||||
fontWeight: 300,
|
||||
lineHeight: 1,
|
||||
color: stat.color,
|
||||
letterSpacing: '-0.05em',
|
||||
fontVariantNumeric: 'tabular-nums',
|
||||
}}
|
||||
>
|
||||
{stat.value}
|
||||
</Box>
|
||||
{/* Main Content Area */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3.5 }}>
|
||||
{/* 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>
|
||||
</Box>
|
||||
|
||||
<Typography
|
||||
sx={{
|
||||
fontFamily: FONT_SANS,
|
||||
fontSize: '11px',
|
||||
fontWeight: 600,
|
||||
color: C.dim,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.06em',
|
||||
}}
|
||||
>
|
||||
{card.title}
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
sx={{
|
||||
fontFamily: FONT_MONO,
|
||||
fontSize: '26px',
|
||||
fontWeight: 700,
|
||||
color: C.bright,
|
||||
my: 0.75,
|
||||
}}
|
||||
>
|
||||
{card.value}
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
sx={{
|
||||
fontFamily: FONT_SANS,
|
||||
fontSize: '11px',
|
||||
color: C.text,
|
||||
}}
|
||||
>
|
||||
{card.subtext}
|
||||
</Typography>
|
||||
</DoubleBezelCard>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{/* Right: Activity Log */}
|
||||
<Box sx={{ ...panelSx, flex: 1, display: 'flex', flexDirection: 'column', minHeight: 400 }}>
|
||||
<Box className="bg-grid" sx={{ position: 'absolute', inset: 0, opacity: 0.02, pointerEvents: 'none' }} />
|
||||
{/* 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: 700, color: C.bright }}>
|
||||
System Nodes & Pipeline Topology
|
||||
</Typography>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '11px', color: C.dim }}>
|
||||
6 NODES HEALTHY • ZERO SERVICE DEGRADATION DETECTED
|
||||
</Typography>
|
||||
</Box>
|
||||
<TactileBadge tone="success" mono>
|
||||
ALL OPERATIONAL
|
||||
</TactileBadge>
|
||||
</Box>
|
||||
|
||||
{/* Log header */}
|
||||
<Box sx={{
|
||||
px: 3, py: 2,
|
||||
borderBottom: `1px solid ${C.border}`,
|
||||
bgcolor: `${C.base}4D`,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
position: 'relative', zIndex: 1,
|
||||
}}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke={C.dim}><path strokeLinecap="square" strokeWidth="2" d="M4 6h16M4 12h16M4 18h7" /></svg>
|
||||
<Box component="span" sx={{
|
||||
fontFamily: FONT, fontSize: '12px', fontWeight: 500,
|
||||
letterSpacing: '0.2em', textTransform: 'uppercase', color: C.bright,
|
||||
}}>
|
||||
System Activity Log
|
||||
<Box
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: { xs: '1fr', sm: 'repeat(2, 1fr)', lg: 'repeat(3, 1fr)' },
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
{stats.nodes.map((node) => (
|
||||
<Box
|
||||
key={node.id}
|
||||
sx={{
|
||||
p: 2,
|
||||
borderRadius: '14px',
|
||||
bgcolor: 'rgba(10, 17, 31, 0.65)',
|
||||
border: `1px solid ${C.border}`,
|
||||
transition: 'all 0.2s ease',
|
||||
'&:hover': {
|
||||
borderColor: C.accentLight,
|
||||
bgcolor: 'rgba(17, 26, 48, 0.8)',
|
||||
transform: 'translateY(-2px)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 1 }}>
|
||||
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '13px', fontWeight: 700, color: C.bright }}>
|
||||
{node.name}
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: '50%',
|
||||
bgcolor: '#10b981',
|
||||
boxShadow: '0 0 8px #10b981',
|
||||
}}
|
||||
/>
|
||||
</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: C.green400 }}>
|
||||
{node.uptimePercent}% Up
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</DoubleBezelCard>
|
||||
|
||||
{/* Live Audio & Voice Intelligence Simulator Widget */}
|
||||
<DashboardSimulator />
|
||||
|
||||
{/* 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: 700, color: C.bright }}>
|
||||
Operational Telemetry & Server Logs
|
||||
</Typography>
|
||||
<TactileBadge tone="mono" mono>
|
||||
AUTO REFRESH (30s)
|
||||
</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 OPERATIONAL ERRORS — ALL C# .NET API NODES HEALTHY (100% SUCCESS RATE)
|
||||
</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 sx={{ display: 'flex', gap: 1 }}>
|
||||
<Link href="/audit-log" style={{ textDecoration: 'none' }}>
|
||||
<Box component="span" sx={{
|
||||
fontFamily: FONT, fontSize: '10px', fontWeight: 500,
|
||||
letterSpacing: '0.1em', textTransform: 'uppercase',
|
||||
px: 1.5, py: 0.5, borderRadius: '4px',
|
||||
bgcolor: C.borderHl, color: C.bright,
|
||||
cursor: 'pointer',
|
||||
'&:hover': { bgcolor: C.dim },
|
||||
transition: 'background 0.15s',
|
||||
}}>
|
||||
View All
|
||||
</Box>
|
||||
</Link>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Log table */}
|
||||
<Box sx={{ flex: 1, overflow: 'auto', p: 3, position: 'relative', zIndex: 1 }}>
|
||||
<Box component="table" sx={tableSx}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: 140 }}>Timestamp</th>
|
||||
<th style={{ width: 120 }}>Action</th>
|
||||
<th style={{ width: 100 }}>Target</th>
|
||||
<th>Memo</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{logs.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={4} style={{ textAlign: 'center', color: C.dim, padding: '32px 0' }}>
|
||||
No activity records yet
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
logs.map((log) => {
|
||||
const action = log.action as string
|
||||
const actionColor = action.includes('delete') ? C.red400
|
||||
: action.includes('create') ? C.green400
|
||||
: action.includes('role') ? C.purple400
|
||||
: C.orange400
|
||||
return (
|
||||
<tr key={log.id as number}>
|
||||
<td style={{ color: C.dim, whiteSpace: 'nowrap' }}>
|
||||
{new Date(log.created_at as string).toLocaleTimeString()}
|
||||
</td>
|
||||
<td style={{ color: actionColor }}>
|
||||
{action.toUpperCase().replace('.', '_')}
|
||||
</td>
|
||||
<td>{(log.target_type as string).toUpperCase()}</td>
|
||||
<td style={{ color: C.dim }}>
|
||||
{((log.memo as string) ?? '').substring(0, 60)}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</DoubleBezelCard>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue