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:
parent
a9c9a1ca6e
commit
5a34f66981
66 changed files with 4471 additions and 3501 deletions
|
|
@ -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 : [] }
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue