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

This commit is contained in:
Yun Chan 2026-08-20 11:12:05 +09:00
parent 5cd1de6859
commit 708e20f747
406 changed files with 42464 additions and 6199 deletions

View file

@ -1,40 +1,138 @@
'use client'
// apps/admin/src/components/admin-sidebar.tsx
// D3RO Console 사이드바
// D3RO Voice Admin CRM — "Midnight Glass v2" Floating Navigation Island
import { usePathname, useRouter } from 'next/navigation'
import { Box } from '@mui/material'
import { C, FONT } from '@/lib/console-theme'
import { Box, Typography } from '@mui/material'
import { C, FONT_SANS, FONT_MONO } from '@/lib/console-theme'
import { getSupabaseBrowserClient } from '@/lib/supabase-browser'
interface NavItem {
key: string
path: string
label: string
icon: React.ReactElement
interface NavGroup {
title: string
items: Array<{
key: string
path: string
label: string
badge?: string
badgeColor?: 'blue' | 'purple' | 'green' | 'orange'
icon: React.ReactElement
}>
}
const NAV_ITEMS: NavItem[] = [
const NAV_GROUPS: NavGroup[] = [
{
key: 'overview', path: '/', label: 'Overview',
icon: <svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="square" strokeWidth="2" d="M4 4h6v6H4zM14 4h6v6h-6zM4 14h6v6H4zM14 14h6v6h-6z" /></svg>,
title: 'Core Platform',
items: [
{
key: 'overview',
path: '/',
label: 'Dashboard Overview',
icon: (
<svg width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
),
},
{
key: 'pipelines',
path: '/pipelines',
label: 'AI & Voice Pipelines',
badge: 'v0.2',
badgeColor: 'blue',
icon: (
<svg width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" 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>
),
},
{
key: 'models',
path: '/models',
label: 'Service Models',
icon: (
<svg width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
</svg>
),
},
],
},
{
key: 'users', path: '/users', label: 'Users',
icon: <svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="square" strokeWidth="2" d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" /></svg>,
title: 'Customer & Revenue',
items: [
{
key: 'users',
path: '/users',
label: 'User Directory',
badge: '4.5k',
badgeColor: 'purple',
icon: (
<svg width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" 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>
),
},
{
key: 'subscriptions',
path: '/subscriptions',
label: 'Subscriptions & ARR',
icon: (
<svg width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
</svg>
),
},
{
key: 'ads',
path: '/ads',
label: 'Ad Monetization',
badge: '$4.6k',
badgeColor: 'blue',
icon: (
<svg width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
),
},
{
key: 'support',
path: '/support',
label: 'Customer Support (CA)',
badge: '4 Live',
badgeColor: 'orange',
icon: (
<svg width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M18.364 5.636l-3.536 3.536m0 5.656l3.536 3.536M9.172 9.172L5.636 5.636m3.536 9.192l-3.536 3.536M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-5 0a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
),
},
],
},
{
key: 'subscriptions', path: '/subscriptions', label: 'Subscriptions',
icon: <svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="square" strokeWidth="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" /></svg>,
},
{
key: 'usage', path: '/usage', label: 'Usage Data',
icon: <svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="square" strokeWidth="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /></svg>,
},
{
key: 'audit-log', path: '/audit-log', label: 'Audit Log',
icon: <svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="square" strokeWidth="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>,
title: 'Intelligence & Security',
items: [
{
key: 'usage',
path: '/usage',
label: 'Usage & Token Costs',
icon: (
<svg width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
),
},
{
key: 'audit-log',
path: '/audit-log',
label: 'Security Audit Log',
icon: (
<svg width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
),
},
],
},
]
@ -43,218 +141,367 @@ export function AdminSidebar(): React.ReactElement {
const router = useRouter()
const handleLogout = async (): Promise<void> => {
const supabase = getSupabaseBrowserClient()
await supabase.auth.signOut()
try {
await fetch('/api/auth/logout', { method: 'POST' })
} catch {
// ignore
}
try {
const supabase = getSupabaseBrowserClient()
await supabase.auth.signOut().catch(() => {})
} catch {
// ignore
}
router.replace('/login')
router.refresh()
}
const isActive = (path: string): boolean =>
path === '/' ? pathname === '/' : pathname.startsWith(path)
return (
<Box sx={{
width: 280,
height: '100%',
display: 'flex',
flexDirection: 'column',
bgcolor: C.panel,
border: `1px solid ${C.border}`,
borderRadius: '16px',
flexShrink: 0,
position: 'relative',
overflow: 'hidden',
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.6)',
}}>
{/* Grid pattern overlay */}
<Box className="bg-grid" sx={{ position: 'absolute', inset: 0, opacity: 0.03, pointerEvents: 'none' }} />
{/* Header */}
<Box sx={{
px: 3, py: 4,
borderBottom: `1px solid ${C.border}`,
<Box
sx={{
width: { xs: 260, md: 290 },
height: '100%',
display: 'flex',
flexDirection: 'column',
bgcolor: 'rgba(11, 16, 31, 0.82)',
backdropFilter: 'blur(28px)',
border: `1px solid ${C.border}`,
borderRadius: '22px',
flexShrink: 0,
position: 'relative',
}}>
{/* Top accent line */}
<Box sx={{
position: 'absolute', top: 0, left: 0, width: '100%', height: '1px',
background: `linear-gradient(to right, transparent, ${C.accent}, transparent)`,
opacity: 0.5,
}} />
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 0.5 }}>
<Box component="span" sx={{
fontFamily: FONT, fontSize: '14px', fontWeight: 700,
letterSpacing: '0.2em', textTransform: 'uppercase',
color: C.bright,
}}>
D3RO // ADMIN
</Box>
{/* Status dot */}
<Box sx={{ position: 'relative', width: 8, height: 8 }}>
<Box sx={{
position: 'absolute', inset: 0, borderRadius: '50%',
bgcolor: C.green, opacity: 0.75,
animation: 'pulse-ring 1.5s ease-out infinite',
'@keyframes pulse-ring': {
'0%': { transform: 'scale(0.8)', opacity: 1 },
'100%': { transform: 'scale(2.5)', opacity: 0 },
},
}} />
<Box sx={{ position: 'relative', width: 8, height: 8, borderRadius: '50%', bgcolor: C.green }} />
</Box>
</Box>
<Box component="span" sx={{
fontFamily: FONT, fontSize: '10px', fontWeight: 500,
letterSpacing: '0.15em', textTransform: 'uppercase',
color: C.dim,
}}>
SYS.CONSOLE.v2
</Box>
</Box>
overflow: 'hidden',
boxShadow: '0 24px 60px rgba(3, 7, 18, 0.7), inset 0 1px 0 rgba(148, 180, 255, 0.1)',
}}
>
{/* Top Ambient Sheen */}
<Box
sx={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: 120,
background: 'radial-gradient(ellipse 80% 60% at 50% -10%, rgba(59, 130, 246, 0.18) 0%, transparent 80%)',
pointerEvents: 'none',
}}
/>
{/* Nav */}
<Box component="nav" sx={{
flex: 1, overflow: 'auto', py: 2, px: 1.5,
display: 'flex', flexDirection: 'column', gap: 0.5,
position: 'relative', zIndex: 1,
}}>
{NAV_ITEMS.map((item) => {
const active = isActive(item.path)
return (
{/* Brand Header */}
<Box
sx={{
px: 3,
pt: 3.5,
pb: 2.5,
borderBottom: `1px solid ${C.border}`,
position: 'relative',
zIndex: 1,
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
{/* Logo Mark */}
<Box
key={item.key}
onClick={() => router.push(item.path)}
sx={{
display: 'flex', alignItems: 'center',
px: 2, py: 1.5,
borderRadius: '8px',
cursor: 'pointer',
position: 'relative',
border: `1px solid ${active ? C.borderHl : 'transparent'}`,
bgcolor: active ? `${C.borderHl}80` : 'transparent',
color: active ? C.bright : C.text,
transition: 'all 0.15s',
'&:hover': {
bgcolor: active ? `${C.borderHl}80` : C.panelHover,
borderColor: active ? C.borderHl : C.border,
color: C.bright,
'& svg': { color: active ? C.accent : C.bright },
},
width: 34,
height: 34,
borderRadius: '10px',
background: 'linear-gradient(135deg, #06b6d4 0%, #3b82f6 50%, #8b5cf6 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 0 20px rgba(59, 130, 246, 0.45)',
}}
>
{/* Active indicator bar */}
{active && (
<Box sx={{
position: 'absolute', left: 0, top: '50%', transform: 'translateY(-50%)',
width: 3, height: '50%', bgcolor: C.accent, borderRadius: '0 4px 4px 0',
}} />
)}
<Box sx={{
mr: 2, display: 'flex', alignItems: 'center',
color: active ? C.accent : C.dim,
transition: 'color 0.15s',
}}>
{item.icon}
</Box>
<Box component="span" sx={{
fontFamily: FONT, fontSize: '12px', fontWeight: 500,
letterSpacing: '0.1em', textTransform: 'uppercase',
}}>
{item.label}
</Box>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#ffffff" strokeWidth="2.2">
<path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" />
<path d="M19 10v2a7 7 0 0 1-14 0v-2" />
<line x1="12" y1="19" x2="12" y2="22" />
</svg>
</Box>
)
})}
{/* External Links Section */}
<Box sx={{ mt: 4, mb: 1, px: 2 }}>
<Box sx={{ height: '1px', width: '100%', bgcolor: C.border, mb: 2 }} />
<Box component="span" sx={{
fontFamily: FONT, fontSize: '9px', fontWeight: 500,
letterSpacing: '0.2em', textTransform: 'uppercase',
color: C.dim,
}}>
External Links
<Box>
<Typography
sx={{
fontFamily: FONT_SANS,
fontSize: '15px',
fontWeight: 700,
letterSpacing: '-0.02em',
color: C.bright,
lineHeight: 1.2,
}}
>
D3RO Voice
</Typography>
<Typography
sx={{
fontFamily: FONT_MONO,
fontSize: '10px',
fontWeight: 600,
color: C.cyan,
letterSpacing: '0.04em',
}}
>
INTELLIGENCE CRM
</Typography>
</Box>
</Box>
</Box>
<Box
component="a"
href="/admin-swagger/"
target="_blank"
rel="noopener noreferrer"
sx={{
display: 'flex', alignItems: 'center',
px: 2, py: 1.5, borderRadius: '8px',
cursor: 'pointer', textDecoration: 'none',
border: '1px solid transparent',
color: C.text,
transition: 'all 0.15s',
'&:hover': {
bgcolor: C.panelHover,
borderColor: C.border,
color: C.bright,
'& svg': { color: C.bright },
},
}}
>
<Box sx={{ mr: 2, display: 'flex', alignItems: 'center', color: C.dim, transition: 'color 0.15s' }}>
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="square" strokeWidth="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" /></svg>
</Box>
<Box component="span" sx={{
fontFamily: FONT, fontSize: '12px', fontWeight: 500,
letterSpacing: '0.1em', textTransform: 'uppercase',
}}>
API Docs
{/* Live Node Pulse */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 0.75,
px: 1,
py: 0.35,
borderRadius: '999px',
bgcolor: 'rgba(16, 185, 129, 0.12)',
border: '1px solid rgba(16, 185, 129, 0.25)',
}}
>
<Box
sx={{
width: 6,
height: 6,
borderRadius: '50%',
bgcolor: '#10b981',
boxShadow: '0 0 8px #10b981',
animation: 'pulse-ring 2s infinite',
}}
/>
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '9px', fontWeight: 600, color: '#34d399' }}>
LIVE
</Typography>
</Box>
</Box>
</Box>
{/* User card footer */}
<Box sx={{
p: 2,
bgcolor: `${C.base}80`,
borderTop: `1px solid ${C.border}`,
backdropFilter: 'blur(8px)',
position: 'relative', zIndex: 1,
}}>
{/* Nav Scroll Area */}
<Box
component="nav"
sx={{
flex: 1,
overflowY: 'auto',
px: 2,
py: 2,
display: 'flex',
flexDirection: 'column',
gap: 2.5,
position: 'relative',
zIndex: 1,
}}
>
{NAV_GROUPS.map((group) => (
<Box key={group.title}>
<Typography
sx={{
px: 1.5,
mb: 0.75,
fontFamily: FONT_SANS,
fontSize: '11px',
fontWeight: 600,
color: C.dim,
textTransform: 'uppercase',
letterSpacing: '0.08em',
}}
>
{group.title}
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
{group.items.map((item) => {
const active = isActive(item.path)
return (
<Box
key={item.key}
onClick={() => router.push(item.path)}
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
px: 1.75,
py: 1.1,
borderRadius: '12px',
cursor: 'pointer',
position: 'relative',
border: `1px solid ${active ? 'rgba(59, 130, 246, 0.35)' : 'transparent'}`,
bgcolor: active ? 'rgba(59, 130, 246, 0.14)' : 'transparent',
color: active ? C.bright : C.text,
boxShadow: active ? '0 0 20px rgba(59, 130, 246, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.1)' : 'none',
transition: 'all 0.18s cubic-bezier(0.16, 1, 0.3, 1)',
'&:hover': {
bgcolor: active ? 'rgba(59, 130, 246, 0.2)' : 'rgba(26, 38, 68, 0.5)',
borderColor: active ? 'rgba(96, 165, 250, 0.5)' : C.border,
color: C.bright,
transform: 'translateX(3px)',
},
'&:active': {
transform: 'scale(0.98)',
},
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
color: active ? C.accentLight : C.dim,
transition: 'color 0.15s ease',
}}
>
{item.icon}
</Box>
<Typography
sx={{
fontFamily: FONT_SANS,
fontSize: '13px',
fontWeight: active ? 600 : 500,
letterSpacing: '-0.01em',
}}
>
{item.label}
</Typography>
</Box>
{item.badge && (
<Box
sx={{
px: 0.9,
py: 0.2,
borderRadius: '999px',
fontSize: '10px',
fontFamily: FONT_MONO,
fontWeight: 700,
bgcolor: item.badgeColor === 'blue' ? 'rgba(59, 130, 246, 0.2)' : 'rgba(139, 92, 246, 0.2)',
color: item.badgeColor === 'blue' ? C.cyanLight : C.purple400,
border: `1px solid ${item.badgeColor === 'blue' ? 'rgba(59, 130, 246, 0.3)' : 'rgba(139, 92, 246, 0.3)'}`,
}}
>
{item.badge}
</Box>
)}
</Box>
)
})}
</Box>
</Box>
))}
{/* Live Service Matrix Mini-Widget */}
<Box
onClick={() => void handleLogout()}
sx={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
p: 1.5, borderRadius: '8px',
mt: 1,
p: 2,
borderRadius: '14px',
bgcolor: 'rgba(13, 21, 38, 0.7)',
border: `1px solid ${C.border}`,
display: 'flex',
flexDirection: 'column',
gap: 1.2,
}}
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '11px', fontWeight: 600, color: C.dim, textTransform: 'uppercase', letterSpacing: '0.06em' }}>
Service Telemetry
</Typography>
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '10px', color: '#10b981' }}>
99.9% Up
</Typography>
</Box>
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 1 }}>
<Box sx={{ p: 1, borderRadius: '8px', bgcolor: 'rgba(17, 26, 48, 0.6)', border: `1px solid ${C.border}` }}>
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '9px', color: C.dim }}>STT LATENCY</Typography>
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '12px', fontWeight: 700, color: C.cyanLight }}>142ms</Typography>
</Box>
<Box sx={{ p: 1, borderRadius: '8px', bgcolor: 'rgba(17, 26, 48, 0.6)', border: `1px solid ${C.border}` }}>
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '9px', color: C.dim }}>OLLAMA VRAM</Typography>
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '12px', fontWeight: 700, color: C.purple400 }}>4.6 GB</Typography>
</Box>
</Box>
</Box>
</Box>
{/* User Footer */}
<Box
sx={{
p: 2,
bgcolor: 'rgba(10, 14, 28, 0.95)',
borderTop: `1px solid ${C.border}`,
position: 'relative',
zIndex: 1,
}}
>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
p: 1.25,
borderRadius: '12px',
border: `1px solid ${C.borderHl}`,
bgcolor: C.panel,
cursor: 'pointer',
transition: 'border-color 0.15s',
bgcolor: 'rgba(17, 26, 48, 0.5)',
transition: 'border-color 0.15s ease',
'&:hover': {
borderColor: C.dim,
'& .logout-icon': { color: C.accent },
borderColor: C.accentLight,
},
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
<Box sx={{
width: 32, height: 32, borderRadius: '4px',
bgcolor: C.borderHl,
display: 'flex', alignItems: 'center', justifyContent: 'center',
border: `1px solid ${C.dim}4D`,
}}>
<Box component="span" sx={{ fontFamily: FONT, fontSize: '12px', fontWeight: 700, color: C.bright }}>A</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.25 }}>
<Box
sx={{
width: 32,
height: 32,
borderRadius: '8px',
background: 'linear-gradient(135deg, #1d4ed8 0%, #3b82f6 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 700,
fontSize: '13px',
color: '#ffffff',
boxShadow: '0 0 12px rgba(59, 130, 246, 0.4)',
}}
>
A
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
<Box component="span" sx={{ fontFamily: FONT, fontSize: '11px', fontWeight: 500, letterSpacing: '0.05em', color: C.bright }}>
<Box>
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '12px', fontWeight: 600, color: C.bright, lineHeight: 1.2 }}>
Admin User
</Box>
<Box component="span" sx={{ fontFamily: FONT, fontSize: '9px', letterSpacing: '0.15em', textTransform: 'uppercase', color: C.dim }}>
</Typography>
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '9px', fontWeight: 600, color: C.purple400, letterSpacing: '0.04em' }}>
SUPER_ADMIN
</Box>
</Typography>
</Box>
</Box>
<Box className="logout-icon" sx={{ color: C.dim, display: 'flex', transition: 'color 0.15s' }}>
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="square" strokeWidth="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" /></svg>
<Box
onClick={() => void handleLogout()}
title="Sign Out"
sx={{
p: 0.75,
borderRadius: '8px',
color: C.dim,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'all 0.15s ease',
'&:hover': {
color: C.red400,
bgcolor: 'rgba(239, 68, 68, 0.12)',
},
}}
>
<svg width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
</Box>
</Box>
</Box>
</Box>
)
}

View file

@ -0,0 +1,301 @@
'use client'
// apps/admin/src/components/dashboard-simulator.tsx
// D3RO Voice — Interactive Realtime Audio & Intelligence Simulator Sandbox
import React, { useState } from 'react'
import { Box, Typography, Button, TextField } from '@mui/material'
import { C, FONT_SANS, FONT_MONO } from '@/lib/console-theme'
import { DoubleBezelCard, TactileBadge, StatRing } from '@d3ro/ui/components/ds'
export function DashboardSimulator(): React.ReactElement {
const [mode, setMode] = useState<'dictation' | 'meeting' | 'rag'>('dictation')
const [isRunning, setIsRunning] = useState(false)
const [step, setStep] = useState<number>(0)
const [interimText, setInterimText] = useState('')
const [finalResult, setFinalResult] = useState<Record<string, unknown> | null>(null)
const [searchQuery, setSearchQuery] = useState('프로젝트 출시 일정 및 마일스톤')
const steps = [
{ label: 'Audio Capture', desc: '16kHz Mono PCM Buffer' },
{ label: 'Whisper STT', desc: 'Faster-Whisper large-v3-turbo' },
{ label: 'LLM Orchestrator', desc: 'Ollama gemma4 / GPT-Realtime' },
{ label: 'Context / Export', desc: 'SQLite RAG / Multi-Doc' },
]
const runSimulation = () => {
setIsRunning(true)
setStep(1)
setInterimText('')
setFinalResult(null)
// Step 1: Audio buffer
setTimeout(() => {
setStep(2)
if (mode === 'dictation') {
setInterimText('오늘 회의에서 논의된 새로운 음성 인식 모델 성능에 대해서...')
} else if (mode === 'meeting') {
setInterimText('[화자 1]: 다음 주 스프린트 목표를 검토합시다. [화자 2]: STT 지연 시간을 140ms 이하로 줄였습니다.')
} else {
setInterimText('Query vector generated via nomic-embed-text (512-dim)...')
}
}, 600)
// Step 2: STT + interim stream
setTimeout(() => {
setStep(3)
if (mode === 'dictation') {
setInterimText('오늘 회의에서 논의된 새로운 음성 인식 모델 성능에 대해서 요약 및 문서화를 요청드립니다.')
}
}, 1300)
// Step 3: LLM & Final Result
setTimeout(() => {
setStep(4)
setIsRunning(false)
if (mode === 'dictation') {
setFinalResult({
status: 'success',
engine: 'Whisper large-v3-turbo + Ollama gemma4:e4b',
latencyMs: 142,
speedup: '6.2x',
originalText: '오늘 회의에서 논의된 새로운 음성 인식 모델 성능에 대해서 요약 및 문서화를 요청드립니다.',
polishedText: '금일 회의에서 논의된 신규 음성 인식 모델의 성능에 대한 요약 및 문서 작성을 요청드립니다.',
tokensUsed: 48,
costUsd: 0.0,
})
} else if (mode === 'meeting') {
setFinalResult({
status: 'success',
meetingTitle: 'D3RO Voice v0.2.1 릴리스 및 파이프라인 최적화 회의',
diarization: {
speaker1: '팀장 (45% 발화율)',
speaker2: 'ML 엔지니어 (55% 발화율)',
},
summary: 'Faster-Whisper turbo 사이드카 도입으로 지연 시간을 142ms로 6배 단축하였으며, Pyannote 화자 분리 정확도 96.4%를 달성함.',
actionItems: [
'1. Windows 및 macOS 배포 패키지 무결성 검증 완료',
'2. SQLite 벡터 RAG 인덱스 4.8k 문서 동기화',
],
generatedDocs: ['Executive Summary', 'Action Item Checklist', 'Mindmap Diagram'],
})
} else {
setFinalResult({
status: 'success',
query: searchQuery,
embeddingLatencyMs: 18.4,
vectorMatches: [
{ docId: 'DOC_4821', title: '2026 Q3 D3RO Voice 로드맵.md', similarity: 0.942, excerpt: 'Phase 15.5 화자 분리 및 실시간 회의 모드 8월 말 정식 출시...' },
{ docId: 'DOC_3102', title: 'Whisper_Turbo_사이드카_아키텍처.md', similarity: 0.887, excerpt: 'dual-condition parallel flush 패턴을 적용하여 버퍼 지연 최소화...' },
],
})
}
}, 2100)
}
return (
<DoubleBezelCard bezelPadding="6px" innerPadding="24px">
<Box sx={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', alignItems: 'center', mb: 3, gap: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<StatRing color="blue" size={44}>
<svg width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</StatRing>
<Box>
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '16px', fontWeight: 700, color: C.bright }}>
Live Voice & AI Intelligence Sandbox
</Typography>
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '11px', color: C.dim }}>
SIMULATE VOICE CAPTURE • INTERIM STT • AUTO-POLISH • VECTOR RAG
</Typography>
</Box>
</Box>
{/* Mode Selector Tabs */}
<Box sx={{ display: 'flex', gap: 1, bgcolor: 'rgba(10, 17, 31, 0.8)', p: 0.5, borderRadius: '999px', border: `1px solid ${C.border}` }}>
{(['dictation', 'meeting', 'rag'] as const).map((m) => (
<Button
key={m}
size="small"
onClick={() => { setMode(m); setFinalResult(null); setInterimText('') }}
sx={{
borderRadius: '999px',
px: 2,
py: 0.4,
fontSize: '11px',
fontFamily: FONT_SANS,
fontWeight: mode === m ? 700 : 500,
textTransform: 'uppercase',
letterSpacing: '0.04em',
bgcolor: mode === m ? 'rgba(59, 130, 246, 0.25)' : 'transparent',
color: mode === m ? C.bright : C.dim,
border: mode === m ? '1px solid rgba(96, 165, 250, 0.4)' : '1px solid transparent',
'&:hover': { bgcolor: 'rgba(59, 130, 246, 0.15)', color: C.bright },
}}
>
{m === 'dictation' ? '🎤 Dictation' : m === 'meeting' ? '👥 Meeting Mode' : '🔍 Vector RAG'}
</Button>
))}
</Box>
</Box>
{/* Pipeline Progress Stages */}
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 1.5, mb: 3 }}>
{steps.map((s, idx) => {
const stepNum = idx + 1
const isActive = step === stepNum
const isDone = step > stepNum
return (
<Box
key={s.label}
sx={{
p: 1.5,
borderRadius: '12px',
bgcolor: isActive ? 'rgba(59, 130, 246, 0.18)' : isDone ? 'rgba(16, 185, 129, 0.12)' : 'rgba(10, 17, 31, 0.6)',
border: `1px solid ${isActive ? C.accentLight : isDone ? 'rgba(16, 185, 129, 0.3)' : C.border}`,
transition: 'all 0.2s ease',
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 0.5 }}>
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '10px', color: isActive ? C.cyanLight : isDone ? C.green400 : C.dim }}>
STAGE 0{stepNum}
</Typography>
{isDone ? (
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '10px', color: C.green400 }}>✓ DONE</Typography>
) : isActive ? (
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '10px', color: C.cyanLight, animation: 'pulse-ring 1s infinite' }}>● ACTIVE</Typography>
) : (
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '10px', color: C.muted }}>READY</Typography>
)}
</Box>
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '12px', fontWeight: 600, color: C.bright }}>
{s.label}
</Typography>
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '10px', color: C.dim }}>
{s.desc}
</Typography>
</Box>
)
})}
</Box>
{/* Interactive Trigger Bar */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}>
{mode === 'rag' ? (
<TextField
fullWidth
size="small"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search vectorized knowledge documents..."
sx={{
'& .MuiOutlinedInput-root': {
bgcolor: 'rgba(10, 17, 31, 0.7)',
color: C.bright,
borderRadius: '10px',
fontFamily: FONT_SANS,
fontSize: '13px',
'& fieldset': { borderColor: C.border },
'&:hover fieldset': { borderColor: C.borderHl },
'&.Mui-focused fieldset': { borderColor: C.accentLight },
},
}}
/>
) : (
<Box
sx={{
flex: 1,
p: 1.5,
borderRadius: '10px',
bgcolor: 'rgba(10, 17, 31, 0.7)',
border: `1px solid ${C.border}`,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '12px', color: interimText ? C.cyanLight : C.dim }}>
{interimText || 'Waiting for voice audio input stream...'}
</Typography>
{isRunning && (
<Box sx={{ display: 'flex', gap: 0.5, alignItems: 'center' }}>
{[12, 24, 18, 28, 14, 20, 32, 16].map((h, i) => (
<Box
key={i}
sx={{
width: 3,
height: h,
bgcolor: C.cyanLight,
borderRadius: '2px',
animation: `pulse-ring ${0.4 + i * 0.1}s ease-in-out infinite alternate`,
}}
/>
))}
</Box>
)}
</Box>
)}
<Button
variant="contained"
disabled={isRunning}
onClick={runSimulation}
sx={{
background: 'linear-gradient(135deg, #1d4ed8 0%, #3b82f6 55%, #60a5fa 100%)',
color: '#ffffff',
px: 3,
py: 1.1,
borderRadius: '10px',
fontFamily: FONT_SANS,
fontSize: '13px',
fontWeight: 700,
textTransform: 'none',
flexShrink: 0,
boxShadow: '0 0 20px rgba(59, 130, 246, 0.4)',
'&:hover': { filter: 'brightness(1.15)' },
}}
>
{isRunning ? 'Processing...' : '▶ Run Live Test'}
</Button>
</Box>
{/* Output Results Box */}
{finalResult && (
<Box
sx={{
p: 2.5,
borderRadius: '14px',
bgcolor: 'rgba(10, 17, 31, 0.85)',
border: `1px solid rgba(59, 130, 246, 0.3)`,
boxShadow: 'inset 0 2px 6px rgba(3, 7, 18, 0.7)',
}}
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1.5 }}>
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '11px', fontWeight: 600, color: C.green400 }}>
PIPELINE EXECUTION TELEMETRY RESULT
</Typography>
<TactileBadge tone="success" mono>
SUCCESS (200 OK)
</TactileBadge>
</Box>
<Box
component="pre"
sx={{
m: 0,
fontFamily: FONT_MONO,
fontSize: '12px',
color: C.bright,
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
lineHeight: 1.6,
}}
>
{JSON.stringify(finalResult, null, 2)}
</Box>
</Box>
)}
</DoubleBezelCard>
)
}

View file

@ -0,0 +1,31 @@
'use client'
// apps/admin/src/components/license-issuer-button.tsx
// D3RO Voice Admin — Ed25519 라이선스 발급 트리거 버튼
import { useState } from 'react'
import { Button } from '@mui/material'
import { primaryButtonSx } from '@/lib/console-theme'
import { LicenseIssuerDialog } from './license-issuer-dialog'
export function LicenseIssuerButton(): React.ReactElement {
const [open, setOpen] = useState(false)
return (
<>
<Button
variant="contained"
size="small"
onClick={() => setOpen(true)}
sx={{
...primaryButtonSx,
background: 'linear-gradient(135deg, #a855f7 0%, #3b82f6 100%)',
boxShadow: '0 0 12px rgba(168, 85, 247, 0.4)',
}}
>
⚡ Issue Ed25519 License Key
</Button>
<LicenseIssuerDialog open={open} onClose={() => setOpen(false)} />
</>
)
}

View file

@ -0,0 +1,236 @@
'use client'
// apps/admin/src/components/license-issuer-dialog.tsx
// D3RO Voice Admin — Ed25519 비대칭 암호화 라이선스 발급 다이얼로그
import { useState } from 'react'
import {
Dialog,
DialogTitle,
DialogContent,
Box,
Typography,
IconButton,
TextField,
FormControl,
InputLabel,
Select,
MenuItem,
Button,
Alert,
} from '@mui/material'
import {
issueSignedLicenseKey,
DEFAULT_LICENSE_PRIVATE_KEY,
} from '@d3ro/core/utils/crypto-license'
import type { LicenseTier } from '@d3ro/core/types'
import { d3roPalette, d3roFontMono, d3roRadius } from '@d3ro/ui/theme'
import { C, FONT_SANS, FONT_MONO, primaryButtonSx } from '@/lib/console-theme'
interface LicenseIssuerDialogProps {
open: boolean
onClose: () => void
}
export function LicenseIssuerDialog({ open, onClose }: LicenseIssuerDialogProps): React.ReactElement {
const [customerEmail, setCustomerEmail] = useState('')
const [tier, setTier] = useState<LicenseTier>('pro_plus')
const [validity, setValidity] = useState<'30d' | '365d' | 'lifetime'>('365d')
const [machineId, setMachineId] = useState('')
const [teamId, setTeamId] = useState('')
const [generatedKey, setGeneratedKey] = useState<string | null>(null)
const [copied, setCopied] = useState(false)
const [error, setError] = useState<string | null>(null)
const handleGenerate = () => {
setError(null)
setCopied(false)
if (!customerEmail.trim()) {
setError('Customer Email is required')
return
}
try {
const now = Date.now()
let expiresAt: number | null = null
if (validity === '30d') {
expiresAt = now + 30 * 24 * 60 * 60 * 1000
} else if (validity === '365d') {
expiresAt = now + 365 * 24 * 60 * 60 * 1000
}
const payload = {
licenseId: `lic-${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 6)}`,
tier,
customerEmail: customerEmail.trim(),
issuedAt: now,
expiresAt,
machineId: machineId.trim() || null,
teamId: teamId.trim() || undefined,
maxDevices: tier === 'enterprise' ? 999 : tier === 'team' ? 25 : tier === 'pro_plus' ? 5 : 3,
}
const key = issueSignedLicenseKey(payload, DEFAULT_LICENSE_PRIVATE_KEY)
setGeneratedKey(key)
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to generate license key')
}
}
const handleCopy = () => {
if (!generatedKey) return
navigator.clipboard.writeText(generatedKey)
setCopied(true)
setTimeout(() => setCopied(false), 3000)
}
const inputSx = {
'& .MuiInputBase-root': {
fontFamily: d3roFontMono,
fontSize: 13,
color: d3roPalette.text.primary,
bgcolor: d3roPalette.bg.inset,
borderRadius: d3roRadius.button,
},
'& .MuiInputLabel-root': {
fontFamily: FONT_MONO,
fontSize: 12,
color: C.dim,
},
}
return (
<Dialog
open={open}
onClose={onClose}
maxWidth="sm"
fullWidth
PaperProps={{
sx: {
bgcolor: '#0a0d14',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '16px',
boxShadow: '0 20px 40px rgba(0, 0, 0, 0.8)',
p: 1,
},
}}
>
<DialogTitle sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', pb: 1 }}>
<Box>
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '18px', fontWeight: 700, color: C.bright }}>
Issue Cryptographic License Key
</Typography>
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '11px', color: C.dim }}>
ED25519 ASYMMETRIC SIGNED OFFLINE / ENTERPRISE TOKEN
</Typography>
</Box>
<IconButton onClick={onClose} size="small" sx={{ color: C.dim }}>
✕
</IconButton>
</DialogTitle>
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: 1 }}>
<TextField
fullWidth
label="Customer Email *"
placeholder="enterprise-client@company.com"
value={customerEmail}
onChange={(e) => setCustomerEmail(e.target.value)}
sx={inputSx}
/>
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 2 }}>
<FormControl fullWidth sx={inputSx}>
<InputLabel>Plan / Tier</InputLabel>
<Select value={tier} onChange={(e) => setTier(e.target.value as LicenseTier)} label="Plan / Tier">
<MenuItem value="pro">Pro ($9.9 / ₩12,900)</MenuItem>
<MenuItem value="pro_plus">Pro+ ($19.9 / ₩24,900)</MenuItem>
<MenuItem value="team">Team ($25 / ₩32,000)</MenuItem>
<MenuItem value="enterprise">Enterprise (Custom)</MenuItem>
</Select>
</FormControl>
<FormControl fullWidth sx={inputSx}>
<InputLabel>Validity Period</InputLabel>
<Select value={validity} onChange={(e) => setValidity(e.target.value as '30d' | '365d' | 'lifetime')} label="Validity Period">
<MenuItem value="30d">30 Days (Monthly)</MenuItem>
<MenuItem value="365d">1 Year (Annual)</MenuItem>
<MenuItem value="lifetime">Lifetime (Permanent)</MenuItem>
</Select>
</FormControl>
</Box>
{(tier === 'team' || tier === 'enterprise') && (
<TextField
fullWidth
label="Team / Org Name (Optional)"
placeholder="Engineering AI Squad"
value={teamId}
onChange={(e) => setTeamId(e.target.value)}
sx={inputSx}
/>
)}
<TextField
fullWidth
label="Target Machine ID (Optional Hardware Lock)"
placeholder="e.g. 94dbaa34... (Leave blank for any device)"
value={machineId}
onChange={(e) => setMachineId(e.target.value)}
sx={inputSx}
/>
{error && <Alert severity="error" sx={{ fontFamily: FONT_MONO, fontSize: '12px' }}>{error}</Alert>}
<Button
variant="contained"
onClick={handleGenerate}
sx={{
...primaryButtonSx,
py: 1.2,
background: 'linear-gradient(135deg, #a855f7 0%, #3b82f6 100%)',
fontWeight: 700,
}}
>
⚡ Generate Ed25519 Signed License
</Button>
{generatedKey && (
<Box sx={{ mt: 1, p: 2, bgcolor: 'rgba(0, 0, 0, 0.4)', borderRadius: '10px', border: '1px solid rgba(168, 85, 247, 0.4)' }}>
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '11px', color: '#a855f7', fontWeight: 700, mb: 0.5 }}>
SIGNED LICENSE KEY (Copy and paste into D3RO Voice Desktop App):
</Typography>
<Typography
sx={{
fontFamily: FONT_MONO,
fontSize: '11px',
color: C.bright,
p: 1.5,
bgcolor: 'rgba(255, 255, 255, 0.05)',
borderRadius: '6px',
wordBreak: 'break-all',
userSelect: 'all',
mb: 1.5,
}}
>
{generatedKey}
</Typography>
<Button
variant="outlined"
fullWidth
onClick={handleCopy}
sx={{
fontFamily: FONT_MONO,
fontSize: '12px',
borderColor: copied ? '#10b981' : '#a855f7',
color: copied ? '#10b981' : C.bright,
}}
>
{copied ? '✓ Copied to Clipboard!' : '📋 Copy License Key'}
</Button>
</Box>
)}
</DialogContent>
</Dialog>
)
}

View file

@ -18,7 +18,7 @@ import { MetalCard, PhosphorText } from '@d3ro/ui/components/ds'
import { d3roPalette, d3roFontMono } from '@d3ro/ui/theme'
import { callAdminApi } from '@/lib/admin-api'
type Tier = 'free' | 'pro' | 'pro_plus'
type Tier = 'free' | 'pro' | 'pro_plus' | 'team' | 'enterprise'
type SubStatus = 'active' | 'canceled' | 'past_due' | 'expired'
interface SubscriptionFormProps {
@ -109,6 +109,8 @@ export function SubscriptionForm({ mode, userId, initial, onSuccess }: Subscript
<MenuItem value="free">FREE</MenuItem>
<MenuItem value="pro">PRO</MenuItem>
<MenuItem value="pro_plus">PRO+</MenuItem>
<MenuItem value="team">TEAM</MenuItem>
<MenuItem value="enterprise">ENTERPRISE</MenuItem>
</Select>
</FormControl>