feat(admin): 예전/최신 어드민 통합 — 실데이터 복원 + 인증 아키텍처 정리

예전 배포본(HEAD)의 상세 기능을 새 아키텍처(인증=.NET 백엔드, 데이터=Supabase)
위에 실데이터로 복원. 예전 HEAD는 서명 쿠키를 거부하고 위조 쿠키는 통과시키는
인증 결함이 있었고, 삭제된 페이지 다수는 백엔드 호출 0인 하드코딩 목업이었음.

인증/세션
- 로그인 이메일 전용화(username 폐지), 에러 키별 안내 메시지
- ADMIN_COOKIE_SECURE 옵션: TLS 없는 LAN HTTP 배포에서 Secure 쿠키 유실로
  로그인이 유지되지 않던 문제 해결 (login/logout route, admin-session, compose, .env.example)
- Supabase 미설정 시 우아한 저하: isSupabaseAdminConfigured + UnavailableAdminPanel

기능 복원 (실데이터)
- Release Hub: Forgejo API 실데이터(다운로드 수/SHA-256 체크섬/릴리스 이력)
- Ad Monetization: 데스크톱 미디에이션 10개 어댑터 로스터(fail-closed) + ad_reward_claims 통계
- License Issuer: 서버사이드 Ed25519 서명(/api/admin/license, super_admin 전용),
  개인키는 ADMIN_LICENSE_PRIVATE_KEY env로만, 발급 감사를 .NET AdminAuditEntries에 기록
- Service Models: STT 7종/LLM 5종 프리셋 드롭다운 + 자동채움
- 대시보드 ARR/MRR KPI: Supabase 구독 실집계(티어 월단가 기반)
- 사용자 상세 티어별 기능 배지(pro_plus 조건부)

.NET
- SuperAdminOnly 정책 추가, /api/admin/license-audit 엔드포인트, LicenseAuditDto
This commit is contained in:
Yun Chan 2026-08-23 23:38:08 +09:00
parent a9c9a1ca6e
commit 5a34f66981
66 changed files with 4471 additions and 3501 deletions

View file

@ -1,7 +1,6 @@
// apps/admin/src/lib/api-server.ts
// Helper library for connecting Next.js apps/admin to C# .NET API Backend & High-Fidelity D3RO Telemetry
import 'server-only'
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000'
import { fetchAdminBackend } from './backend-session'
export interface SystemNodeHealth {
id: string
@ -16,44 +15,24 @@ export interface SystemNodeHealth {
}
export interface PipelineStats {
whisper: {
engine: string
activeModel: string
avgLatencyMs: number
speedupFactor: string
partialStreamingFps: number
totalTranscriptionsToday: number
gpuVramUsage: string
}
ollama: {
version: string
loadedModels: string[]
activeContextLimit: number
tokensPerSecond: number
vramAllocated: string
activeSessions: number
}
realtimeVoice: {
backend: string
activeStreams: number
streamUptime: number
localFallbackRate: string
avgAudioRttMs: number
}
ragVector: {
embeddingModel: string
indexedDocuments: number
totalVectorChunks: number
avgSearchLatencyMs: number
topHitRatePercent: number
}
meetingIntelligence: {
diarizationEngine: string
speakerAccuracyPercent: number
activeMeetingSessions: number
templatesGeneratedToday: number
mindmapsExported: number
}
whisper: { engine: string; activeModel: string; avgLatencyMs: number; speedupFactor: string; partialStreamingFps: number; totalTranscriptionsToday: number; gpuVramUsage: string }
ollama: { version: string; loadedModels: string[]; activeContextLimit: number; tokensPerSecond: number; vramAllocated: string; activeSessions: number }
realtimeVoice: { backend: string; activeStreams: number; streamUptime: number; localFallbackRate: string; avgAudioRttMs: number }
ragVector: { embeddingModel: string; indexedDocuments: number; totalVectorChunks: number; avgSearchLatencyMs: number; topHitRatePercent: number }
meetingIntelligence: { diarizationEngine: string; speakerAccuracyPercent: number; activeMeetingSessions: number; templatesGeneratedToday: number; mindmapsExported: number }
}
export interface FeatureUsageBreakdown {
featureId: string
featureName: string
category: string
totalCalls?: number
callCount?: number
percentage?: number
tokensUsed: number
totalCost: number
estimatedCostUsd?: number
avgLatencyMs: number
}
export interface ServerStats {
@ -63,44 +42,27 @@ export interface ServerStats {
totalCost: number
serverUptimeSeconds: number
errorCount: number
arrUsd: number
mrrUsd: number
tierDistribution: {
free: number
pro: number
pro_plus: number
}
arrUsd: number | null
mrrUsd: number | null
tierDistribution: { free: number; pro: number; pro_plus: number } | null
nodes: SystemNodeHealth[]
pipelines: PipelineStats
pipelines: PipelineStats | null
featureBreakdown: FeatureUsageBreakdown[]
recentErrors: Array<{
id: number
errorType: string
message: string
endpoint: string | null
createdAt: string
}>
recentErrors: Array<{ id: number; errorType: string; message: string; endpoint: string | null; createdAt: string }>
}
export interface UserItem {
id: number
id: string
uid: string
email: string
name: string
role: 'user' | 'manager' | 'admin' | 'super_admin'
tier: 'free' | 'pro' | 'pro_plus'
tier: 'free' | 'pro' | 'pro_plus' | null
createdAt: string
lastLoginAt: string | null
lastActiveDevice: string
lastActiveDevice: string | null
isActive: boolean
dailyUsage: {
dictations: number
dictationsMax: number
llmCalls: number
llmCallsMax: number
ragQueries: number
ragQueriesMax: number
}
dailyUsage: { dictations: number; dictationsMax: number | null; llmCalls: number; llmCallsMax: number | null; ragQueries: number; ragQueriesMax: number | null } | null
}
export interface ModelEndpoint {
@ -112,21 +74,13 @@ export interface ModelEndpoint {
apiKey: string
costPer1kPromptTokens: number
costPer1kCompletionTokens: number
latencyMs: number
latencyMs?: number
isActive: boolean
isDefault: boolean
isDefault?: boolean
createdAt: string
}
export type STTProviderCategory =
| 'groq'
| 'openai'
| 'deepgram'
| 'google'
| 'assemblyai'
| 'azure'
| 'custom'
| 'local-sidecar'
export type STTProviderCategory = 'groq' | 'openai' | 'deepgram' | 'google' | 'assemblyai' | 'azure' | 'custom' | 'local-sidecar'
export interface SttProviderEndpoint {
id: number
@ -166,25 +120,10 @@ export interface CreateSttEndpointDto {
isActive?: boolean
fallbackPriority?: number
extraHeadersJson?: string
memo: string
}
export interface UpdateSttEndpointDto {
name: string
providerType: STTProviderCategory
endpointUrl: string
apiKey?: string
modelId: string
method: string
language?: string
prompt?: string
temperature?: number
costPerMinute: number
costPerSecond?: number
isDefault?: boolean
isActive?: boolean
fallbackPriority?: number
extraHeadersJson?: string
}
export type UpdateSttEndpointDto = CreateSttEndpointDto
export interface SttTestResult {
success: boolean
@ -200,34 +139,8 @@ export interface SttUsageReport {
totalAudioMinutes: number
totalCost: number
avgLatencyMs: number
providerSummaries: Array<{
provider: string
modelId: string
totalRequests: number
totalAudioMinutes: number
totalCost: number
avgLatencyMs: number
}>
userSummaries: Array<{
userId: number
email: string
totalRequests: number
totalAudioMinutes: number
totalCost: number
}>
}
export interface FeatureUsageBreakdown {
featureId: string
featureName: string
category: string
totalCalls?: number
callCount?: number
percentage?: number
tokensUsed: number
totalCost: number
estimatedCostUsd?: number
avgLatencyMs: number
providerSummaries: Array<{ provider: string; modelId: string; totalRequests: number; totalAudioMinutes: number; totalCost: number; avgLatencyMs: number }>
userSummaries: Array<{ userId: number; email: string; totalRequests: number; totalAudioMinutes: number; totalCost: number }>
}
export interface UsageReport {
@ -235,712 +148,92 @@ export interface UsageReport {
totalPromptTokens: number
totalCompletionTokens: number
totalCost: number
timeline: Array<{
date: string
dictations: number
meetingSummaries: number
aiChat: number
ragSearch: number
voiceRealtime: number
totalCost: number
}>
timeline: Array<{ date: string; dictations: number; meetingSummaries: number; aiChat: number; ragSearch: number; voiceRealtime: number; totalCost: number }>
features: FeatureUsageBreakdown[]
userSummaries: Array<{
userId: number
email: string
name: string
tier: string
totalRequests: number
totalTokens: number
totalCost: number
}>
modelSummaries: Array<{
modelId: string
modelName: string
provider: string
totalRequests: number
totalTokens: number
totalCost: number
}>
userSummaries: Array<{ userId: number; email: string; name?: string; tier?: string; totalRequests: number; totalTokens: number; totalCost: number }>
modelSummaries: Array<{ modelId: string; modelName: string; provider?: string; totalRequests: number; totalTokens: number; totalCost: number }>
}
// ── Realistic Mock Fallbacks (D3RO Voice v0.2.1 / Phase 15.5 SSOT) ─────────
const MOCK_NODES: SystemNodeHealth[] = [
{
id: 'whisper-sidecar',
name: 'Faster-Whisper STT Engine',
category: 'stt',
status: 'operational',
latencyMs: 142,
uptimePercent: 99.92,
versionOrModel: 'large-v3-turbo (PyInstaller)',
vramOrMemory: '3.2 GB / 8.0 GB',
details: 'Dual-condition parallel buffer flush • 6x speedup active',
},
{
id: 'ollama-local',
name: 'Bundled Ollama Runtime',
category: 'llm',
status: 'operational',
latencyMs: 48,
uptimePercent: 99.85,
versionOrModel: 'Ollama v0.32.1 (gemma4:e4b)',
vramOrMemory: '4.6 GB / 8.0 GB',
details: 'Pruned slim 119MB runtime • NDJSON streaming active',
},
{
id: 'realtime-voice',
name: 'GPT-Realtime 2.1 Live Engine',
category: 'voice_realtime',
status: 'operational',
latencyMs: 185,
uptimePercent: 99.78,
versionOrModel: 'gpt-realtime-2.1 (Premium WebSocket)',
vramOrMemory: 'Cloud Managed',
details: 'Dual audio loopback • Local pipeline auto-fallback ready',
},
{
id: 'rag-sqlite',
name: 'Vector RAG & Embeddings',
category: 'rag_vector',
status: 'operational',
latencyMs: 18,
uptimePercent: 99.98,
versionOrModel: 'nomic-embed-text-v1.5',
vramOrMemory: '512 MB SQLite Vector',
details: 'Cosine similarity • 4,820 documents indexed',
},
{
id: 'diarization-pyannote',
name: 'Speaker Diarization Engine',
category: 'diarization',
status: 'operational',
latencyMs: 210,
uptimePercent: 99.64,
versionOrModel: 'Pyannote 3.1 + LLM Attribution',
vramOrMemory: '1.4 GB VRAM',
details: 'Multi-speaker voiceprint clustering (Phase 15.5)',
},
{
id: 'csharp-gateway',
name: 'C# .NET Core Gateway API',
category: 'backend_api',
status: 'operational',
latencyMs: 32,
uptimePercent: 99.99,
versionOrModel: '.NET 9.0 WebAPI',
vramOrMemory: '320 MB RAM',
details: 'Telemetry & token cost accounting active',
},
]
const MOCK_PIPELINES: PipelineStats = {
whisper: {
engine: 'faster-whisper (Python 3.11 sidecar)',
activeModel: 'large-v3-turbo (default)',
avgLatencyMs: 142,
speedupFactor: '6.2x vs base',
partialStreamingFps: 10,
totalTranscriptionsToday: 4890,
gpuVramUsage: '3.2 GB',
},
ollama: {
version: 'v0.32.1 (Bundled)',
loadedModels: ['gemma4:e4b', 'qwen2.5:7b', 'llama3:8b'],
activeContextLimit: 8192,
tokensPerSecond: 44.5,
vramAllocated: '4.6 GB',
activeSessions: 8,
},
realtimeVoice: {
backend: 'OpenAI GPT-Realtime 2.1 Audio WS',
activeStreams: 18,
streamUptime: 99.8,
localFallbackRate: '1.8%',
avgAudioRttMs: 185,
},
ragVector: {
embeddingModel: 'nomic-embed-text (SQLite Vector DB)',
indexedDocuments: 4820,
totalVectorChunks: 42900,
avgSearchLatencyMs: 18.4,
topHitRatePercent: 94.6,
},
meetingIntelligence: {
diarizationEngine: 'pyannote 3.1 + LLM speaker fallback',
speakerAccuracyPercent: 96.4,
activeMeetingSessions: 14,
templatesGeneratedToday: 86,
mindmapsExported: 42,
},
async function readJson<T>(path: string): Promise<T> {
const response = await fetchAdminBackend(path)
if (!response.ok) throw new Error(`Admin backend request failed (${response.status})`)
try {
return (await response.json()) as T
} catch {
throw new Error('Admin backend returned invalid JSON')
}
}
const MOCK_USERS: UserItem[] = [
{
id: 1,
uid: 'usr_d3ro_001',
email: 'admin@d3ro.voice',
name: 'D3RO System Architect',
role: 'super_admin',
tier: 'pro_plus',
createdAt: '2026-01-15T09:00:00Z',
lastLoginAt: '2026-08-19T02:45:00Z',
lastActiveDevice: 'Windows 11 x64 (Build 26100)',
isActive: true,
dailyUsage: { dictations: 42, dictationsMax: 9999, llmCalls: 128, llmCallsMax: 9999, ragQueries: 35, ragQueriesMax: 9999 },
},
{
id: 2,
uid: 'usr_d3ro_002',
email: 'sarah.kim@techcorp.io',
name: 'Sarah Kim',
role: 'admin',
tier: 'pro_plus',
createdAt: '2026-03-10T14:20:00Z',
lastLoginAt: '2026-08-19T01:30:00Z',
lastActiveDevice: 'macOS 15.4 arm64 (Apple M3 Max)',
isActive: true,
dailyUsage: { dictations: 184, dictationsMax: 9999, llmCalls: 86, llmCallsMax: 9999, ragQueries: 18, ragQueriesMax: 9999 },
},
{
id: 3,
uid: 'usr_d3ro_003',
email: 'minho.park@innovate.kr',
name: 'Minho Park',
role: 'user',
tier: 'pro_plus',
createdAt: '2026-04-02T11:15:00Z',
lastLoginAt: '2026-08-18T22:10:00Z',
lastActiveDevice: 'Windows 11 x64',
isActive: true,
dailyUsage: { dictations: 92, dictationsMax: 9999, llmCalls: 45, llmCallsMax: 9999, ragQueries: 12, ragQueriesMax: 9999 },
},
{
id: 4,
uid: 'usr_d3ro_004',
email: 'alex.chen@globalai.dev',
name: 'Alex Chen',
role: 'user',
tier: 'pro',
createdAt: '2026-05-18T16:40:00Z',
lastLoginAt: '2026-08-18T19:55:00Z',
lastActiveDevice: 'macOS 15.3 arm64 (Apple M2)',
isActive: true,
dailyUsage: { dictations: 64, dictationsMax: 9999, llmCalls: 142, llmCallsMax: 200, ragQueries: 5, ragQueriesMax: 10 },
},
{
id: 5,
uid: 'usr_d3ro_005',
email: 'jisoo.lee@creator.studio',
name: 'Jisoo Lee',
role: 'user',
tier: 'pro',
createdAt: '2026-06-01T08:12:00Z',
lastLoginAt: '2026-08-19T00:15:00Z',
lastActiveDevice: 'Windows 11 x64',
isActive: true,
dailyUsage: { dictations: 48, dictationsMax: 9999, llmCalls: 78, llmCallsMax: 200, ragQueries: 4, ragQueriesMax: 10 },
},
{
id: 6,
uid: 'usr_d3ro_006',
email: 'david.wilson@voicepod.com',
name: 'David Wilson',
role: 'manager',
tier: 'pro_plus',
createdAt: '2026-06-20T10:00:00Z',
lastLoginAt: '2026-08-18T15:22:00Z',
lastActiveDevice: 'macOS 15.4 arm64',
isActive: true,
dailyUsage: { dictations: 120, dictationsMax: 9999, llmCalls: 95, llmCallsMax: 9999, ragQueries: 28, ragQueriesMax: 9999 },
},
{
id: 7,
uid: 'usr_d3ro_007',
email: 'hyunjin.choi@startup.io',
name: 'Hyunjin Choi',
role: 'user',
tier: 'free',
createdAt: '2026-07-11T13:45:00Z',
lastLoginAt: '2026-08-19T02:10:00Z',
lastActiveDevice: 'Windows 10 x64',
isActive: true,
dailyUsage: { dictations: 18, dictationsMax: 20, llmCalls: 9, llmCallsMax: 10, ragQueries: 0, ragQueriesMax: 0 },
},
{
id: 8,
uid: 'usr_d3ro_008',
email: 'elena.rostova@designlab.eu',
name: 'Elena Rostova',
role: 'user',
tier: 'free',
createdAt: '2026-08-01T17:30:00Z',
lastLoginAt: '2026-08-17T12:00:00Z',
lastActiveDevice: 'macOS 15.2 arm64',
isActive: true,
dailyUsage: { dictations: 8, dictationsMax: 20, llmCalls: 3, llmCallsMax: 10, ragQueries: 0, ragQueriesMax: 0 },
},
]
const MOCK_ENDPOINTS: ModelEndpoint[] = [
{
id: 1,
modelId: 'whisper-large-v3-turbo',
modelName: 'Faster-Whisper Large-v3 Turbo (Local)',
provider: 'Local Sidecar',
endpointUrl: 'http://localhost:8971/stt/transcribe',
apiKey: 'internal-sidecar-token',
costPer1kPromptTokens: 0.000000,
costPer1kCompletionTokens: 0.000000,
latencyMs: 142,
isActive: true,
isDefault: true,
createdAt: '2026-01-15T09:00:00Z',
},
{
id: 2,
modelId: 'ollama-gemma4-e4b',
modelName: 'Ollama Gemma-4 E4B (Bundled Local)',
provider: 'Ollama Local',
endpointUrl: 'http://localhost:11434/api/generate',
apiKey: '',
costPer1kPromptTokens: 0.000000,
costPer1kCompletionTokens: 0.000000,
latencyMs: 48,
isActive: true,
isDefault: true,
createdAt: '2026-02-01T10:00:00Z',
},
{
id: 3,
modelId: 'gpt-realtime-2.1',
modelName: 'OpenAI GPT-Realtime 2.1 (Live Voice)',
provider: 'OpenAI',
endpointUrl: 'wss://api.openai.com/v1/realtime',
apiKey: 'sk-proj-rt-••••••••',
costPer1kPromptTokens: 0.005000,
costPer1kCompletionTokens: 0.020000,
latencyMs: 185,
isActive: true,
isDefault: false,
createdAt: '2026-05-10T12:00:00Z',
},
{
id: 4,
modelId: 'gpt-4o-mini',
modelName: 'GPT-4o Mini (Cloud Synthesis & Meeting)',
provider: 'OpenAI',
endpointUrl: 'https://api.openai.com/v1/chat/completions',
apiKey: 'sk-proj-••••••••',
costPer1kPromptTokens: 0.000150,
costPer1kCompletionTokens: 0.000600,
latencyMs: 240,
isActive: true,
isDefault: false,
createdAt: '2026-04-12T08:00:00Z',
},
{
id: 5,
modelId: 'claude-3-5-sonnet',
modelName: 'Claude 3.5 Sonnet (Complex Action Planning)',
provider: 'Anthropic',
endpointUrl: 'https://api.anthropic.com/v1/messages',
apiKey: 'sk-ant-••••••••',
costPer1kPromptTokens: 0.003000,
costPer1kCompletionTokens: 0.015000,
latencyMs: 380,
isActive: true,
isDefault: false,
createdAt: '2026-06-01T14:00:00Z',
},
{
id: 6,
modelId: 'nomic-embed-text',
modelName: 'Nomic Embed Text v1.5 (RAG Embeddings)',
provider: 'Ollama Local',
endpointUrl: 'http://localhost:11434/api/embeddings',
apiKey: '',
costPer1kPromptTokens: 0.000000,
costPer1kCompletionTokens: 0.000000,
latencyMs: 18,
isActive: true,
isDefault: true,
createdAt: '2026-03-20T11:00:00Z',
},
]
const MOCK_USAGE_REPORT: UsageReport = {
totalRequests: 142890,
totalPromptTokens: 28450120,
totalCompletionTokens: 14210980,
totalCost: 24.8912,
timeline: [
{ date: '2026-08-13', dictations: 1420, meetingSummaries: 38, aiChat: 310, ragSearch: 180, voiceRealtime: 42, totalCost: 2.841 },
{ date: '2026-08-14', dictations: 1680, meetingSummaries: 45, aiChat: 345, ragSearch: 210, voiceRealtime: 58, totalCost: 3.290 },
{ date: '2026-08-15', dictations: 1890, meetingSummaries: 52, aiChat: 410, ragSearch: 260, voiceRealtime: 64, totalCost: 3.840 },
{ date: '2026-08-16', dictations: 1250, meetingSummaries: 28, aiChat: 280, ragSearch: 140, voiceRealtime: 35, totalCost: 2.120 },
{ date: '2026-08-17', dictations: 1120, meetingSummaries: 22, aiChat: 240, ragSearch: 110, voiceRealtime: 30, totalCost: 1.940 },
{ date: '2026-08-18', dictations: 2140, meetingSummaries: 74, aiChat: 520, ragSearch: 380, voiceRealtime: 88, totalCost: 5.120 },
{ date: '2026-08-19', dictations: 2480, meetingSummaries: 86, aiChat: 610, ragSearch: 420, voiceRealtime: 104, totalCost: 5.740 },
],
features: [
{ featureId: 'dictation', featureName: 'Realtime Dictation (Whisper Turbo)', category: 'STT', callCount: 88420, tokensUsed: 12400000, totalCost: 0.00, avgLatencyMs: 142 },
{ featureId: 'meeting_mode', featureName: 'Meeting Mode + Multi-Doc Generator', category: 'Intelligence', callCount: 12840, tokensUsed: 8920000, totalCost: 6.42, avgLatencyMs: 680 },
{ featureId: 'speaker_diarization', featureName: 'Speaker Diarization (pyannote + LLM)', category: 'Audio', callCount: 14200, tokensUsed: 4200000, totalCost: 2.10, avgLatencyMs: 210 },
{ featureId: 'voice_realtime', featureName: 'Live Voice Conversation (Realtime)', category: 'Voice Mode', callCount: 6890, tokensUsed: 5410000, totalCost: 11.24, avgLatencyMs: 185 },
{ featureId: 'rag_search', featureName: 'Vector RAG Document Semantic Search', category: 'Knowledge', callCount: 12540, tokensUsed: 1240000, totalCost: 0.89, avgLatencyMs: 18 },
{ featureId: 'auto_polish', featureName: 'Auto Polish & Grammar Refinement', category: 'LLM', callCount: 8000, tokensUsed: 1091100, totalCost: 4.24, avgLatencyMs: 48 },
],
userSummaries: [
{ userId: 2, email: 'sarah.kim@techcorp.io', name: 'Sarah Kim', tier: 'pro_plus', totalRequests: 18420, totalTokens: 6420000, totalCost: 5.842 },
{ userId: 1, email: 'admin@d3ro.voice', name: 'D3RO Admin', tier: 'pro_plus', totalRequests: 14200, totalTokens: 4890000, totalCost: 4.120 },
{ userId: 6, email: 'david.wilson@voicepod.com', name: 'David Wilson', tier: 'pro_plus', totalRequests: 12400, totalTokens: 3820000, totalCost: 3.450 },
{ userId: 3, email: 'minho.park@innovate.kr', name: 'Minho Park', tier: 'pro_plus', totalRequests: 9840, totalTokens: 2940000, totalCost: 2.640 },
{ userId: 4, email: 'alex.chen@globalai.dev', name: 'Alex Chen', tier: 'pro', totalRequests: 8200, totalTokens: 2410000, totalCost: 1.820 },
{ userId: 5, email: 'jisoo.lee@creator.studio', name: 'Jisoo Lee', tier: 'pro', totalRequests: 6400, totalTokens: 1890000, totalCost: 1.420 },
],
modelSummaries: [
{ modelId: 'whisper-large-v3-turbo', modelName: 'Faster-Whisper Large-v3 Turbo', provider: 'Local Sidecar', totalRequests: 88420, totalTokens: 12400000, totalCost: 0.00 },
{ modelId: 'ollama-gemma4-e4b', modelName: 'Ollama Gemma-4 E4B', provider: 'Ollama Local', totalRequests: 32400, totalTokens: 14820000, totalCost: 0.00 },
{ modelId: 'gpt-realtime-2.1', modelName: 'OpenAI GPT-Realtime 2.1', provider: 'OpenAI', totalRequests: 6890, totalTokens: 5410000, totalCost: 11.24 },
{ modelId: 'gpt-4o-mini', modelName: 'GPT-4o Mini', provider: 'OpenAI', totalRequests: 12840, totalTokens: 8920000, totalCost: 6.42 },
{ modelId: 'claude-3-5-sonnet', modelName: 'Claude 3.5 Sonnet', provider: 'Anthropic', totalRequests: 2340, totalTokens: 1111100, totalCost: 7.23 },
],
function normalizeRole(value: unknown): UserItem['role'] {
const normalized = typeof value === 'string' ? value.replace(/[_-]/g, '').toLowerCase() : ''
if (normalized === 'superadmin') return 'super_admin'
if (normalized === 'admin') return 'admin'
if (normalized === 'manager') return 'manager'
return 'user'
}
const MOCK_FEATURE_BREAKDOWN: FeatureUsageBreakdown[] = [
{ featureId: 'dictation', featureName: 'Realtime Dictation (Whisper Turbo)', category: 'STT', totalCalls: 88420, percentage: 61.8, tokensUsed: 12400000, totalCost: 0.00, estimatedCostUsd: 0.00, avgLatencyMs: 142 },
{ featureId: 'meeting_mode', featureName: 'Meeting Mode + Multi-Doc Generator', category: 'Intelligence', totalCalls: 12840, percentage: 9.0, tokensUsed: 8920000, totalCost: 6.42, estimatedCostUsd: 6.42, avgLatencyMs: 680 },
{ featureId: 'speaker_diarization', featureName: 'Speaker Diarization (Pyannote + LLM)', category: 'Audio', totalCalls: 14200, percentage: 9.9, tokensUsed: 4200000, totalCost: 2.10, estimatedCostUsd: 2.10, avgLatencyMs: 210 },
{ featureId: 'voice_realtime', featureName: 'Live Voice Conversation (Realtime)', category: 'Voice Mode', totalCalls: 6890, percentage: 4.8, tokensUsed: 5410000, totalCost: 11.24, estimatedCostUsd: 11.24, avgLatencyMs: 185 },
{ featureId: 'rag_search', featureName: 'Vector RAG Document Semantic Search', category: 'Knowledge', totalCalls: 12540, percentage: 8.8, tokensUsed: 1240000, totalCost: 0.89, estimatedCostUsd: 0.89, avgLatencyMs: 18 },
{ featureId: 'auto_polish', featureName: 'Auto Polish & Grammar Refinement', category: 'LLM', totalCalls: 8000, percentage: 5.7, tokensUsed: 1091100, totalCost: 4.24, estimatedCostUsd: 4.24, avgLatencyMs: 48 },
]
// ── API Fetch Functions ───────────────────────────────────────────────────
export async function fetchServerStats(): Promise<ServerStats> {
try {
const res = await fetch(`${API_BASE}/api/admin/stats`, { cache: 'no-store' })
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data = await res.json()
return {
totalUsers: data.totalUsers ?? 4580,
activeUsersToday: data.activeUsersToday ?? 1240,
totalRequests: data.totalRequests ?? 142890,
totalCost: data.totalCost ?? 24.8912,
serverUptimeSeconds: data.serverUptimeSeconds ?? 864200,
errorCount: data.errorCount ?? 0,
arrUsd: 231480,
mrrUsd: 19290,
tierDistribution: { free: 3420, pro: 842, pro_plus: 318 },
nodes: MOCK_NODES,
pipelines: MOCK_PIPELINES,
featureBreakdown: MOCK_FEATURE_BREAKDOWN,
recentErrors: data.recentErrors ?? [],
}
} catch {
return {
totalUsers: 4580,
activeUsersToday: 1240,
totalRequests: 142890,
totalCost: 24.8912,
serverUptimeSeconds: 864200,
errorCount: 0,
arrUsd: 231480,
mrrUsd: 19290,
tierDistribution: { free: 3420, pro: 842, pro_plus: 318 },
nodes: MOCK_NODES,
pipelines: MOCK_PIPELINES,
featureBreakdown: MOCK_FEATURE_BREAKDOWN,
recentErrors: [],
}
const data = await readJson<Partial<ServerStats>>('/stats')
if (typeof data.totalUsers !== 'number' || typeof data.activeUsersToday !== 'number' || typeof data.totalRequests !== 'number' || typeof data.totalCost !== 'number' || typeof data.serverUptimeSeconds !== 'number' || typeof data.errorCount !== 'number') {
throw new Error('Admin stats response is incomplete')
}
return {
totalUsers: data.totalUsers,
activeUsersToday: data.activeUsersToday,
totalRequests: data.totalRequests,
totalCost: data.totalCost,
serverUptimeSeconds: data.serverUptimeSeconds,
errorCount: data.errorCount,
arrUsd: typeof data.arrUsd === 'number' ? data.arrUsd : null,
mrrUsd: typeof data.mrrUsd === 'number' ? data.mrrUsd : null,
tierDistribution: data.tierDistribution ?? null,
nodes: Array.isArray(data.nodes) ? data.nodes : [],
pipelines: data.pipelines ?? null,
featureBreakdown: Array.isArray(data.featureBreakdown) ? data.featureBreakdown : [],
recentErrors: Array.isArray(data.recentErrors) ? data.recentErrors : []
}
}
export async function fetchUsers(): Promise<UserItem[]> {
try {
const res = await fetch(`${API_BASE}/api/admin/users`, { cache: 'no-store' })
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data = await res.json()
return Array.isArray(data) && data.length > 0 ? data : MOCK_USERS
} catch {
return MOCK_USERS
}
const data = await readJson<Array<Record<string, unknown>>>('/users')
if (!Array.isArray(data)) throw new Error('Admin users response is invalid')
return data.map((item) => {
if (typeof item.id !== 'number' || typeof item.email !== 'string' || typeof item.createdAt !== 'string') {
throw new Error('Admin users response contains an invalid row')
}
return {
id: String(item.id),
uid: String(item.id),
email: item.email,
name: item.email,
role: normalizeRole(item.role),
tier: null,
createdAt: item.createdAt,
lastLoginAt: typeof item.lastLoginAt === 'string' ? item.lastLoginAt : null,
lastActiveDevice: null,
isActive: item.isActive === true,
dailyUsage: null
}
})
}
export async function fetchModelEndpoints(): Promise<ModelEndpoint[]> {
try {
const res = await fetch(`${API_BASE}/api/admin/endpoints`, { cache: 'no-store' })
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data = await res.json()
return Array.isArray(data) && data.length > 0 ? data : MOCK_ENDPOINTS
} catch {
return MOCK_ENDPOINTS
}
}
export async function createModelEndpoint(dto: {
modelId: string
modelName: string
provider: string
endpointUrl: string
apiKey: string
costPer1kPromptTokens: number
costPer1kCompletionTokens: number
}): Promise<ModelEndpoint> {
const res = await fetch(`${API_BASE}/api/admin/endpoints`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(dto),
})
if (!res.ok) throw new Error(`Failed to create model endpoint: ${res.statusText}`)
return res.json()
}
export async function deleteModelEndpoint(id: number): Promise<boolean> {
const res = await fetch(`${API_BASE}/api/admin/endpoints/${id}`, { method: 'DELETE' })
return res.ok
}
// ── STT Provider API Fetch Functions ───────────────────────────────────────
export const MOCK_STT_ENDPOINTS: SttProviderEndpoint[] = [
{
id: 1,
name: 'Groq Whisper LPU Turbo (Ultra Fast)',
providerType: 'groq',
endpointUrl: 'https://api.groq.com/openai/v1/audio/transcriptions',
apiKey: '••••••••',
modelId: 'whisper-large-v3-turbo',
method: 'multipart',
language: 'ko',
prompt: null,
temperature: 0.0,
costPerMinute: 0.0005,
costPerSecond: 0.000008,
isDefault: true,
isActive: true,
fallbackPriority: 1,
extraHeadersJson: null,
latencyMs: 140,
createdAt: '2026-01-15T09:00:00Z',
updatedAt: null,
},
{
id: 2,
name: 'OpenAI Whisper Official',
providerType: 'openai',
endpointUrl: 'https://api.openai.com/v1/audio/transcriptions',
apiKey: '••••••••',
modelId: 'whisper-1',
method: 'multipart',
language: 'ko',
prompt: null,
temperature: 0.0,
costPerMinute: 0.006,
costPerSecond: 0.0001,
isDefault: false,
isActive: true,
fallbackPriority: 2,
extraHeadersJson: null,
latencyMs: 380,
createdAt: '2026-02-01T10:00:00Z',
updatedAt: null,
},
{
id: 3,
name: 'Deepgram Nova-3 Industry Standard',
providerType: 'deepgram',
endpointUrl: 'https://api.deepgram.com/v1/listen',
apiKey: '••••••••',
modelId: 'nova-3',
method: 'binary-stream',
language: 'ko',
prompt: null,
temperature: 0.0,
costPerMinute: 0.0043,
costPerSecond: 0.000072,
isDefault: false,
isActive: true,
fallbackPriority: 3,
extraHeadersJson: null,
latencyMs: 195,
createdAt: '2026-03-10T12:00:00Z',
updatedAt: null,
},
{
id: 4,
name: 'Google Gemini 2.0 Flash / Cloud STT',
providerType: 'google',
endpointUrl: 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent',
apiKey: '••••••••',
modelId: 'gemini-2.0-flash',
method: 'json-base64',
language: 'ko',
prompt: null,
temperature: 0.0,
costPerMinute: 0.001,
costPerSecond: 0.000017,
isDefault: false,
isActive: true,
fallbackPriority: 4,
extraHeadersJson: null,
latencyMs: 260,
createdAt: '2026-04-12T08:00:00Z',
updatedAt: null,
},
{
id: 5,
name: 'AssemblyAI Universal-2',
providerType: 'assemblyai',
endpointUrl: 'https://api.assemblyai.com/v2/transcript',
apiKey: '••••••••',
modelId: 'best',
method: 'multipart',
language: 'ko',
prompt: null,
temperature: 0.0,
costPerMinute: 0.0025,
costPerSecond: 0.000042,
isDefault: false,
isActive: true,
fallbackPriority: 5,
extraHeadersJson: null,
latencyMs: 520,
createdAt: '2026-05-18T14:00:00Z',
updatedAt: null,
},
{
id: 6,
name: 'Local Sidecar (Offline Faster-Whisper)',
providerType: 'local-sidecar',
endpointUrl: 'http://localhost:8971/stt/transcribe',
apiKey: '',
modelId: 'whisper-large-v3-turbo',
method: 'multipart',
language: 'ko',
prompt: null,
temperature: 0.0,
costPerMinute: 0.0,
costPerSecond: 0.0,
isDefault: false,
isActive: true,
fallbackPriority: 6,
extraHeadersJson: null,
latencyMs: 142,
createdAt: '2026-01-15T09:00:00Z',
updatedAt: null,
},
]
export const MOCK_STT_USAGE_REPORT: SttUsageReport = {
totalTranscriptions: 88420,
totalAudioMinutes: 14820.5,
totalCost: 7.41,
avgLatencyMs: 165.4,
providerSummaries: [
{ provider: 'groq', modelId: 'whisper-large-v3-turbo', totalRequests: 74200, totalAudioMinutes: 12400.0, totalCost: 6.20, avgLatencyMs: 142.0 },
{ provider: 'openai', modelId: 'whisper-1', totalRequests: 8400, totalAudioMinutes: 1420.5, totalCost: 8.52, avgLatencyMs: 380.0 },
{ provider: 'deepgram', modelId: 'nova-3', totalRequests: 5820, totalAudioMinutes: 1000.0, totalCost: 4.30, avgLatencyMs: 195.0 },
],
userSummaries: [
{ userId: 1, email: 'admin@d3ro.voice', totalRequests: 14200, totalAudioMinutes: 2480.0, totalCost: 1.24 },
{ userId: 2, email: 'sarah.kim@techcorp.io', totalRequests: 18420, totalAudioMinutes: 3200.0, totalCost: 1.60 },
],
const data = await readJson<ModelEndpoint[]>('/endpoints')
if (!Array.isArray(data)) throw new Error('Model endpoints response is invalid')
return data
}
export async function fetchSttEndpoints(): Promise<SttProviderEndpoint[]> {
try {
const res = await fetch(`${API_BASE}/api/admin/stt-endpoints`, { cache: 'no-store' })
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data = await res.json()
return Array.isArray(data) && data.length > 0 ? data : MOCK_STT_ENDPOINTS
} catch {
return MOCK_STT_ENDPOINTS
}
}
export async function createSttEndpoint(dto: CreateSttEndpointDto): Promise<SttProviderEndpoint> {
const res = await fetch(`${API_BASE}/api/admin/stt-endpoints`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(dto),
})
if (!res.ok) throw new Error(`Failed to create STT endpoint: ${res.statusText}`)
return res.json()
}
export async function updateSttEndpoint(id: number, dto: UpdateSttEndpointDto): Promise<SttProviderEndpoint> {
const res = await fetch(`${API_BASE}/api/admin/stt-endpoints/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(dto),
})
if (!res.ok) throw new Error(`Failed to update STT endpoint: ${res.statusText}`)
return res.json()
}
export async function deleteSttEndpoint(id: number): Promise<boolean> {
const res = await fetch(`${API_BASE}/api/admin/stt-endpoints/${id}`, { method: 'DELETE' })
return res.ok
}
export async function setDefaultSttEndpoint(id: number): Promise<boolean> {
const res = await fetch(`${API_BASE}/api/admin/stt-endpoints/${id}/set-default`, { method: 'POST' })
return res.ok
}
export async function testSttEndpoint(id: number, apiKey?: string, endpointUrl?: string): Promise<SttTestResult> {
try {
let url = `${API_BASE}/api/admin/stt-endpoints/${id}/test`
if (id === 0 && endpointUrl) {
url = `${API_BASE}/api/admin/stt-endpoints/test-direct?endpointUrl=${encodeURIComponent(endpointUrl)}&apiKey=${encodeURIComponent(apiKey || '')}`
}
const res = await fetch(url, { method: 'POST' })
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return await res.json()
} catch (err) {
return {
success: false,
message: err instanceof Error ? err.message : 'Connection test failed',
latencyMs: 0,
transcriptPreview: null,
provider: null,
modelId: null,
}
}
const data = await readJson<SttProviderEndpoint[]>('/stt-endpoints')
if (!Array.isArray(data)) throw new Error('STT endpoints response is invalid')
return data
}
export async function fetchSttUsageReport(): Promise<SttUsageReport> {
try {
const res = await fetch(`${API_BASE}/api/admin/stt-usage`, { cache: 'no-store' })
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return await res.json()
} catch {
return MOCK_STT_USAGE_REPORT
}
return readJson<SttUsageReport>('/stt-usage')
}
export async function fetchUsageReport(): Promise<UsageReport> {
try {
const res = await fetch(`${API_BASE}/api/admin/usage`, { cache: 'no-store' })
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data = await res.json()
return {
totalRequests: data.totalRequests ?? MOCK_USAGE_REPORT.totalRequests,
totalPromptTokens: data.totalPromptTokens ?? MOCK_USAGE_REPORT.totalPromptTokens,
totalCompletionTokens: data.totalCompletionTokens ?? MOCK_USAGE_REPORT.totalCompletionTokens,
totalCost: data.totalCost ?? MOCK_USAGE_REPORT.totalCost,
timeline: MOCK_USAGE_REPORT.timeline,
features: MOCK_USAGE_REPORT.features,
userSummaries: data.userSummaries && data.userSummaries.length > 0 ? data.userSummaries : MOCK_USAGE_REPORT.userSummaries,
modelSummaries: data.modelSummaries && data.modelSummaries.length > 0 ? data.modelSummaries : MOCK_USAGE_REPORT.modelSummaries,
}
} catch {
return MOCK_USAGE_REPORT
}
const data = await readJson<Omit<UsageReport, 'timeline' | 'features'> & Partial<Pick<UsageReport, 'timeline' | 'features'>>>('/usage')
return { ...data, timeline: Array.isArray(data.timeline) ? data.timeline : [], features: Array.isArray(data.features) ? data.features : [] }
}