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
|
|
@ -8,6 +8,18 @@ import { StatRing, TactileBadge, DoubleBezelCard } from '@d3ro/ui/components/ds'
|
|||
|
||||
export default async function PipelinesPage(): Promise<React.ReactElement> {
|
||||
const stats = await fetchServerStats()
|
||||
if (!stats.pipelines) {
|
||||
return (
|
||||
<DoubleBezelCard bezelPadding="6px" innerPadding="24px">
|
||||
<Typography component="h1" sx={{ fontFamily: FONT_SANS, fontSize: '20px', fontWeight: 500, color: C.bright, mb: 1 }}>
|
||||
Pipeline telemetry unavailable
|
||||
</Typography>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '12px', color: C.dim, lineHeight: 1.7 }}>
|
||||
The backend does not currently expose measured pipeline telemetry. No simulated engine, latency, accuracy, or capacity values are shown.
|
||||
</Typography>
|
||||
</DoubleBezelCard>
|
||||
)
|
||||
}
|
||||
const { whisper, ollama, realtimeVoice, ragVector, meetingIntelligence } = stats.pipelines
|
||||
|
||||
return (
|
||||
|
|
@ -42,7 +54,7 @@ export default async function PipelinesPage(): Promise<React.ReactElement> {
|
|||
sx={{
|
||||
fontFamily: FONT_SANS,
|
||||
fontSize: '18px',
|
||||
fontWeight: 700,
|
||||
fontWeight: 500,
|
||||
color: C.bright,
|
||||
letterSpacing: '-0.02em',
|
||||
m: 0,
|
||||
|
|
@ -63,7 +75,7 @@ export default async function PipelinesPage(): Promise<React.ReactElement> {
|
|||
mt: 0.25,
|
||||
}}
|
||||
>
|
||||
LOCAL WHISPER • OLLAMA V0.32.1 • GPT-REALTIME 2.1 • VECTOR RAG • DIARIZATION
|
||||
Local Whisper, Ollama v0.32.1, GPT-Realtime 2.1, Vector RAG, Diarization
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
@ -103,7 +115,7 @@ export default async function PipelinesPage(): Promise<React.ReactElement> {
|
|||
</svg>
|
||||
</StatRing>
|
||||
<Box>
|
||||
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '16px', fontWeight: 700, color: C.bright }}>
|
||||
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '16px', fontWeight: 500, color: C.bright }}>
|
||||
Faster-Whisper STT Sidecar
|
||||
</Typography>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '11px', color: C.cyanLight }}>
|
||||
|
|
@ -119,19 +131,19 @@ export default async function PipelinesPage(): Promise<React.ReactElement> {
|
|||
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 1.5, mb: 2.5 }}>
|
||||
<Box sx={{ p: 1.5, borderRadius: '10px', bgcolor: 'rgba(10, 17, 31, 0.7)', border: `1px solid ${C.border}` }}>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '10px', color: C.dim }}>AVG LATENCY</Typography>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '16px', fontWeight: 700, color: C.bright }}>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '16px', fontWeight: 500, color: C.bright }}>
|
||||
{whisper.avgLatencyMs}ms
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ p: 1.5, borderRadius: '10px', bgcolor: 'rgba(10, 17, 31, 0.7)', border: `1px solid ${C.border}` }}>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '10px', color: C.dim }}>SPEEDUP FACTOR</Typography>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '16px', fontWeight: 700, color: C.green400 }}>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '16px', fontWeight: 500, color: C.green400 }}>
|
||||
{whisper.speedupFactor}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ p: 1.5, borderRadius: '10px', bgcolor: 'rgba(10, 17, 31, 0.7)', border: `1px solid ${C.border}` }}>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '10px', color: C.dim }}>GPU VRAM</Typography>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '16px', fontWeight: 700, color: C.cyanLight }}>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '16px', fontWeight: 500, color: C.cyanLight }}>
|
||||
{whisper.gpuVramUsage}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
|
@ -156,7 +168,7 @@ export default async function PipelinesPage(): Promise<React.ReactElement> {
|
|||
</svg>
|
||||
</StatRing>
|
||||
<Box>
|
||||
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '16px', fontWeight: 700, color: C.bright }}>
|
||||
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '16px', fontWeight: 500, color: C.bright }}>
|
||||
Bundled Ollama Runtime
|
||||
</Typography>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '11px', color: C.purple400 }}>
|
||||
|
|
@ -172,19 +184,19 @@ export default async function PipelinesPage(): Promise<React.ReactElement> {
|
|||
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 1.5, mb: 2.5 }}>
|
||||
<Box sx={{ p: 1.5, borderRadius: '10px', bgcolor: 'rgba(10, 17, 31, 0.7)', border: `1px solid ${C.border}` }}>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '10px', color: C.dim }}>THROUGHPUT</Typography>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '16px', fontWeight: 700, color: C.bright }}>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '16px', fontWeight: 500, color: C.bright }}>
|
||||
{ollama.tokensPerSecond} tok/s
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ p: 1.5, borderRadius: '10px', bgcolor: 'rgba(10, 17, 31, 0.7)', border: `1px solid ${C.border}` }}>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '10px', color: C.dim }}>CONTEXT LIMIT</Typography>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '16px', fontWeight: 700, color: C.cyanLight }}>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '16px', fontWeight: 500, color: C.cyanLight }}>
|
||||
{ollama.activeContextLimit}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ p: 1.5, borderRadius: '10px', bgcolor: 'rgba(10, 17, 31, 0.7)', border: `1px solid ${C.border}` }}>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '10px', color: C.dim }}>VRAM OCCUPANCY</Typography>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '16px', fontWeight: 700, color: C.purple400 }}>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '16px', fontWeight: 500, color: C.purple400 }}>
|
||||
{ollama.vramAllocated}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
|
@ -212,7 +224,7 @@ export default async function PipelinesPage(): Promise<React.ReactElement> {
|
|||
</svg>
|
||||
</StatRing>
|
||||
<Box>
|
||||
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '16px', fontWeight: 700, color: C.bright }}>
|
||||
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '16px', fontWeight: 500, color: C.bright }}>
|
||||
GPT-Realtime 2.1 Live Engine
|
||||
</Typography>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '11px', color: C.orange400 }}>
|
||||
|
|
@ -228,19 +240,19 @@ export default async function PipelinesPage(): Promise<React.ReactElement> {
|
|||
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 1.5, mb: 2.5 }}>
|
||||
<Box sx={{ p: 1.5, borderRadius: '10px', bgcolor: 'rgba(10, 17, 31, 0.7)', border: `1px solid ${C.border}` }}>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '10px', color: C.dim }}>LIVE STREAMS</Typography>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '16px', fontWeight: 700, color: C.orange400 }}>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '16px', fontWeight: 500, color: C.orange400 }}>
|
||||
{realtimeVoice.activeStreams} Active
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ p: 1.5, borderRadius: '10px', bgcolor: 'rgba(10, 17, 31, 0.7)', border: `1px solid ${C.border}` }}>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '10px', color: C.dim }}>AUDIO RTT</Typography>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '16px', fontWeight: 700, color: C.bright }}>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '16px', fontWeight: 500, color: C.bright }}>
|
||||
{realtimeVoice.avgAudioRttMs}ms
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ p: 1.5, borderRadius: '10px', bgcolor: 'rgba(10, 17, 31, 0.7)', border: `1px solid ${C.border}` }}>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '10px', color: C.dim }}>LOCAL FALLBACK</Typography>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '16px', fontWeight: 700, color: C.green400 }}>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '16px', fontWeight: 500, color: C.green400 }}>
|
||||
{realtimeVoice.localFallbackRate}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
|
@ -265,7 +277,7 @@ export default async function PipelinesPage(): Promise<React.ReactElement> {
|
|||
</svg>
|
||||
</StatRing>
|
||||
<Box>
|
||||
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '16px', fontWeight: 700, color: C.bright }}>
|
||||
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '16px', fontWeight: 500, color: C.bright }}>
|
||||
SQLite Vector RAG Engine
|
||||
</Typography>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '11px', color: C.green400 }}>
|
||||
|
|
@ -281,19 +293,19 @@ export default async function PipelinesPage(): Promise<React.ReactElement> {
|
|||
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 1.5, mb: 2.5 }}>
|
||||
<Box sx={{ p: 1.5, borderRadius: '10px', bgcolor: 'rgba(10, 17, 31, 0.7)', border: `1px solid ${C.border}` }}>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '10px', color: C.dim }}>INDEXED DOCS</Typography>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '16px', fontWeight: 700, color: C.bright }}>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '16px', fontWeight: 500, color: C.bright }}>
|
||||
{ragVector.indexedDocuments.toLocaleString()}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ p: 1.5, borderRadius: '10px', bgcolor: 'rgba(10, 17, 31, 0.7)', border: `1px solid ${C.border}` }}>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '10px', color: C.dim }}>VECTOR CHUNKS</Typography>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '16px', fontWeight: 700, color: C.cyanLight }}>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '16px', fontWeight: 500, color: C.cyanLight }}>
|
||||
{ragVector.totalVectorChunks.toLocaleString()}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ p: 1.5, borderRadius: '10px', bgcolor: 'rgba(10, 17, 31, 0.7)', border: `1px solid ${C.border}` }}>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '10px', color: C.dim }}>SEARCH HIT RATE</Typography>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '16px', fontWeight: 700, color: C.green400 }}>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '16px', fontWeight: 500, color: C.green400 }}>
|
||||
{ragVector.topHitRatePercent}%
|
||||
</Typography>
|
||||
</Box>
|
||||
|
|
@ -319,7 +331,7 @@ export default async function PipelinesPage(): Promise<React.ReactElement> {
|
|||
</svg>
|
||||
</StatRing>
|
||||
<Box>
|
||||
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '18px', fontWeight: 700, color: C.bright }}>
|
||||
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '18px', fontWeight: 500, color: C.bright }}>
|
||||
Meeting Intelligence & Speaker Diarization (Phase 14~15.5)
|
||||
</Typography>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '12px', color: C.accentLight }}>
|
||||
|
|
@ -335,7 +347,7 @@ export default async function PipelinesPage(): Promise<React.ReactElement> {
|
|||
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', sm: 'repeat(2, 1fr)', md: 'repeat(4, 1fr)' }, gap: 2, mb: 3 }}>
|
||||
<Box sx={{ p: 2, borderRadius: '12px', bgcolor: 'rgba(10, 17, 31, 0.7)', border: `1px solid ${C.border}` }}>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '10px', color: C.dim }}>SPEAKER ACCURACY</Typography>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '20px', fontWeight: 700, color: C.green400 }}>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '20px', fontWeight: 500, color: C.green400 }}>
|
||||
{meetingIntelligence.speakerAccuracyPercent}%
|
||||
</Typography>
|
||||
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '11px', color: C.dim, mt: 0.5 }}>
|
||||
|
|
@ -344,7 +356,7 @@ export default async function PipelinesPage(): Promise<React.ReactElement> {
|
|||
</Box>
|
||||
<Box sx={{ p: 2, borderRadius: '12px', bgcolor: 'rgba(10, 17, 31, 0.7)', border: `1px solid ${C.border}` }}>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '10px', color: C.dim }}>ACTIVE MEETINGS</Typography>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '20px', fontWeight: 700, color: C.cyanLight }}>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '20px', fontWeight: 500, color: C.cyanLight }}>
|
||||
{meetingIntelligence.activeMeetingSessions} Live
|
||||
</Typography>
|
||||
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '11px', color: C.dim, mt: 0.5 }}>
|
||||
|
|
@ -353,7 +365,7 @@ export default async function PipelinesPage(): Promise<React.ReactElement> {
|
|||
</Box>
|
||||
<Box sx={{ p: 2, borderRadius: '12px', bgcolor: 'rgba(10, 17, 31, 0.7)', border: `1px solid ${C.border}` }}>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '10px', color: C.dim }}>TEMPLATES TODAY</Typography>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '20px', fontWeight: 700, color: C.purple400 }}>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '20px', fontWeight: 500, color: C.purple400 }}>
|
||||
{meetingIntelligence.templatesGeneratedToday} Docs
|
||||
</Typography>
|
||||
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '11px', color: C.dim, mt: 0.5 }}>
|
||||
|
|
@ -362,7 +374,7 @@ export default async function PipelinesPage(): Promise<React.ReactElement> {
|
|||
</Box>
|
||||
<Box sx={{ p: 2, borderRadius: '12px', bgcolor: 'rgba(10, 17, 31, 0.7)', border: `1px solid ${C.border}` }}>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '10px', color: C.dim }}>MINDMAP EXPORTS</Typography>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '20px', fontWeight: 700, color: C.orange400 }}>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '20px', fontWeight: 500, color: C.orange400 }}>
|
||||
{meetingIntelligence.mindmapsExported} Maps
|
||||
</Typography>
|
||||
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '11px', color: C.dim, mt: 0.5 }}>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue