diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c12ee24..f0c30a5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -23,6 +23,12 @@ const DrConsole = lazy(() => import('./pages/DrConsole')) const NetworkConsole = lazy(() => import('./pages/NetworkConsole')) const CsapConsole = lazy(() => import('./pages/CsapConsole')) const ScrapingManager = lazy(() => import('./pages/ScrapingManager')) +// ── GUARDiA 확장 v3 ── +const KpiDashboard = lazy(() => import('./pages/KpiDashboard')) +const BiAnalytics = lazy(() => import('./pages/BiAnalytics')) +const BillingManage = lazy(() => import('./pages/BillingManage')) +const IntegrationHub = lazy(() => import('./pages/IntegrationHub')) +const AiPlatform = lazy(() => import('./pages/AiPlatform')) function Loading() { return ( @@ -61,6 +67,12 @@ export default function App() { } /> } /> } /> + {/* GUARDiA 확장 v3 */} + } /> + } /> + } /> + } /> + } /> } /> diff --git a/frontend/src/components/layout/AppLayout.tsx b/frontend/src/components/layout/AppLayout.tsx index d9a3346..3ce3f9d 100644 --- a/frontend/src/components/layout/AppLayout.tsx +++ b/frontend/src/components/layout/AppLayout.tsx @@ -21,6 +21,12 @@ const PAGE_TITLES: Record = { '/dr': 'DR 재해복구 관제', '/network': '네트워크 장비 관제', '/csap': 'CSAP 보안 점검', + // GUARDiA 확장 v3 + '/kpi': 'KPI 대시보드', + '/bi': 'BI 대시보드', + '/billing': '구독 · 과금 관리', + '/integrations': '외부 연동 허브', + '/ai-platform': 'AI 플랫폼', } export function AppLayout() { diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index 1f11bd4..7730287 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -35,6 +35,14 @@ const NAV: NavItem[] = [ { label: '네트워크 장비', icon: '', path: '/network' }, { label: 'CSAP 점검', icon: '', path: '/csap' }, ]}, + // ── GUARDiA 확장 v3 ── + { label: '분석 · KPI', icon: '📈', children: [ + { label: 'KPI 대시보드', icon: '', path: '/kpi' }, + { label: 'BI 대시보드', icon: '', path: '/bi' }, + ]}, + { label: 'AI 플랫폼', icon: '🧠', path: '/ai-platform' }, + { label: '외부 연동', icon: '🔗', path: '/integrations' }, + { label: '구독 · 과금', icon: '💰', path: '/billing' }, ] /* Variant 스타일 색상 상수 */ diff --git a/frontend/src/pages/AiPlatform.tsx b/frontend/src/pages/AiPlatform.tsx new file mode 100644 index 0000000..d0fd799 --- /dev/null +++ b/frontend/src/pages/AiPlatform.tsx @@ -0,0 +1,199 @@ +import { useEffect, useState } from 'react' +import { guardiaApi } from '../api/clients' + +type Tab = 'insights' | 'learning' | 'predict' | 'benchmark' + +export default function AiPlatform() { + const [tab, setTab] = useState('insights') + const tabs: { id: Tab; label: string; icon: string }[] = [ + { id: 'insights', label: 'AI 인사이트', icon: '🧠' }, + { id: 'learning', label: 'Learning Loop', icon: '🔄' }, + { id: 'predict', label: '예측 분석', icon: '🔮' }, + { id: 'benchmark', label: '벤치마킹', icon: '📊' }, + ] + return ( +
+

🧠 AI 플랫폼

+
+ {tabs.map(t => ( + + ))} +
+ {tab === 'insights' && } + {tab === 'learning' && } + {tab === 'predict' && } + {tab === 'benchmark' && } +
+ ) +} + +function Card({ title, children }: { title: string; children: React.ReactNode }) { + return ( +
+

{title}

+ {children} +
+ ) +} + +function InsightsTab() { + const [weekly, setWeekly] = useState(null) + const [anomalies, setAnomalies] = useState(null) + useEffect(() => { + guardiaApi.get('/api/insights/weekly').then((r: any) => setWeekly(r.data)).catch(() => {}) + guardiaApi.get('/api/insights/anomalies').then((r: any) => setAnomalies(r.data)).catch(() => {}) + }, []) + return ( + <> + {anomalies?.anomalies?.length > 0 && ( +
+ ⚠️ 이상 감지 ({anomalies.anomalies.length}건) + {anomalies.anomalies.map((a: any, i: number) => ( +
{a.message}
+ ))} +
+ )} +
+ {weekly && [ + { label: '신규 SR', val: weekly.stats?.total || 0, color: '#003366' }, + { label: '완료율', val: `${weekly.stats?.completion_rate || 0}%`, color: '#10B981' }, + { label: '미처리', val: weekly.stats?.open || 0, color: '#EF4444' }, + ].map(s => ( +
+
{s.val}
+
{s.label}
+
+ ))} +
+ +

+ {weekly?.ai_insight || '데이터 수집 중...'} +

+
+ + ) +} + +function LearningTab() { + const [status, setStatus] = useState(null) + const [quality, setQuality] = useState(null) + const [history, setHistory] = useState([]) + useEffect(() => { + guardiaApi.get('/api/learn/status').then((r: any) => setStatus(r.data)).catch(() => {}) + guardiaApi.get('/api/learn/quality').then((r: any) => setQuality(r.data)).catch(() => {}) + guardiaApi.get('/api/learn/history?limit=10').then((r: any) => setHistory(r.data || [])).catch(() => {}) + }, []) + async function startTrain() { + await guardiaApi.post('/api/learn/train') + guardiaApi.get('/api/learn/history?limit=10').then((r: any) => setHistory(r.data || [])).catch(() => {}) + } + return ( + <> +
+ +
{status?.available_samples || 0}
+
수집 가능 샘플
+
+ RAG 피드백: {status?.high_quality_rag || 0} · SR 이력: {status?.sr_samples || 0} +
+ +
+ +
{quality?.quality_grade || 'N/A'}
+
+ 평균 {quality?.avg_rating || 0}점 · 긍정 {quality?.positive_rate || 0}% +
+
+
+ + + + {['모델', '상태', '샘플', '시작'].map(h => )} + + + {history.slice(0, 8).map((h: any) => ( + + + + + + + ))} + {!history.length && } + +
{h}
{h.model_name || '-'}{h.status}{h.samples_used || 0}{h.started_at ? new Date(h.started_at).toLocaleDateString('ko-KR') : '-'}
이력 없음
+
+ + ) +} + +function PredictTab() { + const [sla, setSla] = useState(null) + const [surge, setSurge] = useState(null) + useEffect(() => { + guardiaApi.get('/api/predict/sla-breach').then((r: any) => setSla(r.data)).catch(() => {}) + guardiaApi.get('/api/predict/sr-surge').then((r: any) => setSurge(r.data)).catch(() => {}) + }, []) + return ( +
+ +
+ {Math.round((sla?.breach_probability_7d || 0) * 100)}% +
+
현재 SLA: {sla?.current_rate || 0}% · 목표: {sla?.target || 95}%
+ {sla?.insight &&

{sla.insight}

} +
+ +
+ {surge?.surge_ratio || 1}x +
+
오늘 {surge?.today_count || 0}건 · 7일 평균 {surge?.avg_7d || 0}건
+ {surge?.insight &&

{surge.insight}

} +
+
+ ) +} + +function BenchmarkTab() { + const [comp, setComp] = useState(null) + const [rank, setRank] = useState(null) + useEffect(() => { + guardiaApi.get('/api/benchmark/comparison').then((r: any) => setComp(r.data)).catch(() => {}) + guardiaApi.get('/api/benchmark/my-rank').then((r: any) => setRank(r.data)).catch(() => {}) + }, []) + async function contribute() { await guardiaApi.post('/api/benchmark/contribute') } + return ( + <> + +
+ {(comp?.comparison || []).map((m: any) => ( +
+
{m.metric}
+
+ {m.mine}{m.unit} +
+
업계 평균: {m.industry}{m.unit}
+
+ {m.status === 'ABOVE' ? '▲ 평균 이상' : '▼ 평균 이하'} +
+
+ ))} +
+
+
+ + 기여 시 더 정확한 벤치마킹 가능 (완전 익명화) +
+ + ) +} diff --git a/frontend/src/pages/BiAnalytics.tsx b/frontend/src/pages/BiAnalytics.tsx new file mode 100644 index 0000000..d1e2e2e --- /dev/null +++ b/frontend/src/pages/BiAnalytics.tsx @@ -0,0 +1,150 @@ +import { useEffect, useState } from 'react' +import { + LineChart, Line, BarChart, Bar, PieChart, Pie, Cell, + XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, +} from 'recharts' +import { guardiaApi } from '../api/clients' + +const COLORS = ['#003366', '#00A0C8', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6'] + +export default function BiAnalytics() { + const [overview, setOverview] = useState(null) + const [srTrend, setSrTrend] = useState(null) + const [pie, setPie] = useState(null) + const [mttr, setMttr] = useState(null) + const [tab, setTab] = useState<'overview' | 'trend' | 'engineer' | 'mttr'>('overview') + + useEffect(() => { loadData() }, []) + + async function loadData() { + const [o, t, p, m] = await Promise.all([ + guardiaApi.get('/api/bi/overview').then((r: any) => r.data).catch(() => null), + guardiaApi.get('/api/bi/sr-trend?days=30').then((r: any) => r.data).catch(() => null), + guardiaApi.get('/api/bi/category-pie?days=30').then((r: any) => r.data).catch(() => null), + guardiaApi.get('/api/bi/mttr-trend?months=6').then((r: any) => r.data).catch(() => null), + ]) + setOverview(o); setSrTrend(t); setPie(p); setMttr(m) + } + + const tabs = [ + { id: 'overview', label: '개요' }, + { id: 'trend', label: 'SR 트렌드' }, + { id: 'engineer', label: '엔지니어' }, + { id: 'mttr', label: 'MTTR' }, + ] as const + + return ( +
+
+

📊 BI 대시보드

+ +
+ + {/* 탭 */} +
+ {tabs.map(t => ( + + ))} +
+ + {/* 개요 탭 */} + {tab === 'overview' && ( + <> +
+ {(overview?.cards || []).map((c: any) => ( +
+
{c.value}
+
{c.label} ({c.unit})
+
+ ))} +
+ {pie && ( +
+

SR 카테고리 분포 (30일)

+ + + `${category} ${pct}%`}> + {(pie.data || []).map((_: any, i: number) => )} + + + + +
+ )} + + )} + + {/* SR 트렌드 탭 */} + {tab === 'trend' && srTrend && ( +
+

SR 생성/완료 추이 (30일)

+ + ({ + date: d.slice(5), + 신규: srTrend.datasets[0]?.data[i] || 0, + 완료: srTrend.datasets[1]?.data[i] || 0, + }))}> + + + + + + + + + +
+ )} + + {/* MTTR 탭 */} + {tab === 'mttr' && mttr && ( +
+

MTTR 월별 추이 (시간)

+ + ({ month: r.month, mttr: r.mttr_hours }))}> + + + + `${v}시간`} /> + + + +
+ )} + + {tab === 'engineer' && } +
+ ) +} + +function EngineerLoad() { + const [data, setData] = useState(null) + useEffect(() => { + guardiaApi.get('/api/bi/engineer-load?days=30').then((r: any) => setData(r.data)).catch(() => {}) + }, []) + if (!data) return
+ const combined = (data.labels || []).map((name: string, i: number) => ({ + name, 완료: data.datasets[0]?.data[i] || 0, 진행중: data.datasets[1]?.data[i] || 0, + })) + return ( +
+

엔지니어별 SR 워크로드

+ + + + + + + + + + + +
+ ) +} diff --git a/frontend/src/pages/BillingManage.tsx b/frontend/src/pages/BillingManage.tsx new file mode 100644 index 0000000..e788de1 --- /dev/null +++ b/frontend/src/pages/BillingManage.tsx @@ -0,0 +1,118 @@ +import { useEffect, useState } from 'react' +import { guardiaApi } from '../api/clients' + +export default function BillingManage() { + const [sub, setSub] = useState(null) + const [usage, setUsage] = useState(null) + const [invoices, setInvoices] = useState([]) + const [plans, setPlans] = useState([]) + + useEffect(() => { load() }, []) + + async function load() { + const [s, u, i, p] = await Promise.all([ + guardiaApi.get('/api/billing/subscription').then((r: any) => r.data).catch(() => null), + guardiaApi.get('/api/billing/usage').then((r: any) => r.data).catch(() => null), + guardiaApi.get('/api/billing/invoices').then((r: any) => r.data).catch(() => []), + guardiaApi.get('/api/billing/plans').then((r: any) => r.data).catch(() => []), + ]) + setSub(s); setUsage(u); setInvoices(i || []); setPlans(p || []) + } + + async function generateInvoice() { + await guardiaApi.post('/api/billing/invoices/generate') + load() + } + + const pctColor = (pct: number) => pct > 90 ? '#ef4444' : pct > 70 ? '#f59e0b' : '#003366' + + return ( +
+

💰 구독 · 과금 관리

+ +
+ {/* 현재 구독 */} +
+

📋 현재 구독

+
{sub?.plan || 'COMMUNITY'}
+
+ {sub?.price ? `월 ${sub.price.toLocaleString()}원` : '무료'} · {sub?.billing_cycle || 'MONTHLY'} +
+
+ {plans.slice(0, 3).map((p: any) => ( + + ))} +
+
+ + {/* 사용량 */} +
+

📊 이번 달 사용량

+ {usage && [ + { label: '서버', used: usage.servers?.used || 0, limit: usage.servers?.limit || 20 }, + { label: '사용자', used: usage.users?.used || 0, limit: usage.users?.limit || 10 }, + ].map(item => { + const pct = item.limit > 0 ? Math.min(100, Math.round(item.used / item.limit * 100)) : 0 + return ( +
+
+ {item.label} + {item.used} / {item.limit < 0 ? '∞' : item.limit} +
+
+
+
+
+ ) + })} +
SR (이번 달): {usage?.sr_this_month || 0}건
+
+
+ + {/* 청구서 */} +
+
+

🧾 청구서 이력 ({invoices.length}건)

+ +
+ + + + {['기간', '플랜', '금액', '상태', '생성일'].map(h => ( + + ))} + + + + {invoices.length === 0 ? ( + + ) : invoices.slice(0, 12).map((inv: any) => ( + + + + + + + + ))} + +
{h}
청구서 없음
{inv.period}{inv.plan || '-'}{inv.amount ? `${inv.amount.toLocaleString()}원` : '무료'} + + {inv.status} + + + {inv.created_at ? new Date(inv.created_at).toLocaleDateString('ko-KR') : '-'} +
+
+
+ ) +} diff --git a/frontend/src/pages/IntegrationHub.tsx b/frontend/src/pages/IntegrationHub.tsx new file mode 100644 index 0000000..6107783 --- /dev/null +++ b/frontend/src/pages/IntegrationHub.tsx @@ -0,0 +1,213 @@ +import { useEffect, useState } from 'react' +import { guardiaApi } from '../api/clients' + +type Tab = 'jira' | 'slack' | 'servicenow' | 'erp' | 'kakao' | 'sso' + +export default function IntegrationHub() { + const [tab, setTab] = useState('jira') + const tabs: { id: Tab; label: string; icon: string }[] = [ + { id: 'jira', label: 'Jira', icon: '🔵' }, + { id: 'slack', label: 'Slack', icon: '💬' }, + { id: 'servicenow', label: 'ServiceNow', icon: '🔗' }, + { id: 'erp', label: 'ERP', icon: '🏢' }, + { id: 'kakao', label: '카카오', icon: '💛' }, + { id: 'sso', label: 'SSO', icon: '🔐' }, + ] + + return ( +
+

🔗 외부 연동 허브

+
+ {tabs.map(t => ( + + ))} +
+ {tab === 'jira' && } + {tab === 'slack' && } + {tab === 'servicenow' && } + {tab === 'erp' && } + {tab === 'kakao' && } + {tab === 'sso' && } +
+ ) +} + +function Card({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) { + return
{children}
+} + +function JiraTab() { + const [cfg, setCfg] = useState(null) + const [mappings, setMappings] = useState([]) + useEffect(() => { + guardiaApi.get('/api/jira/config').then((r: any) => setCfg(r.data)).catch(() => {}) + guardiaApi.get('/api/jira/mappings').then((r: any) => setMappings(r.data || [])).catch(() => {}) + }, []) + return ( +
+ +

Jira 연동 설정

+ {cfg ? ( +
+
URL: {cfg.base_url}
+
프로젝트: {cfg.project_key}
+
✅ 연동됨
+
+ ) :

설정 없음

} +
+ +

SR-Issue 매핑 ({mappings.length}건)

+ + + {['SR ID', 'Jira Key', '동기화 시간'].map(h => )} + + + {mappings.slice(0, 8).map((m: any) => ( + + + + + + ))} + {!mappings.length && } + +
{h}
SR-{m.sr_id}{m.jira_key}{m.synced_at ? new Date(m.synced_at).toLocaleDateString('ko-KR') : '-'}
매핑 없음
+
+
+ ) +} + +function SlackTab() { + const [cfg, setCfg] = useState(null) + const [webhook, setWebhook] = useState('') + const [channel, setChannel] = useState('#guardia-ops') + useEffect(() => { guardiaApi.get('/api/slack/config').then((r: any) => setCfg(r.data)).catch(() => {}) }, []) + async function save() { + await guardiaApi.post('/api/slack/config', { name: 'Slack', webhook_url: webhook, default_channel: channel }) + const r: any = await guardiaApi.get('/api/slack/config') + setCfg(r.data) + } + async function testMsg() { await guardiaApi.get('/api/slack/test') } + return ( + +

Slack 연동

+ {cfg &&
✅ 연동됨 ({cfg.default_channel})
} +
+ + setWebhook(e.target.value)} placeholder="https://hooks.slack.com/..." style={{ width: '100%', padding: '8px 12px', border: '1px solid #e2e8f0', borderRadius: 6, fontSize: 13, boxSizing: 'border-box' }} /> +
+
+ + setChannel(e.target.value)} style={{ width: '100%', padding: '8px 12px', border: '1px solid #e2e8f0', borderRadius: 6, fontSize: 13, boxSizing: 'border-box' }} /> +
+
+ + {cfg && } +
+
+ ) +} + +function ServiceNowTab() { + const [url, setUrl] = useState(''); const [user, setUser] = useState(''); const [pw, setPw] = useState('') + async function save() { await guardiaApi.post('/api/servicenow/config', { instance_url: url, username: user, password: pw }) } + return ( + +

ServiceNow 연동

+ {[['인스턴스 URL', url, setUrl, 'https://company.service-now.com'], ['사용자명', user, setUser, ''], ['비밀번호', pw, setPw, '']].map(([label, val, set, ph]: any) => ( +
+ + (set as Function)(e.target.value)} placeholder={ph as string} + style={{ width: '100%', padding: '8px 12px', border: '1px solid #e2e8f0', borderRadius: 6, fontSize: 13, boxSizing: 'border-box' }} /> +
+ ))} + +
+ ) +} + +function ERPTab() { + const [cfgs, setCfgs] = useState([]) + useEffect(() => { guardiaApi.get('/api/erp/config').then((r: any) => setCfgs(r.data || [])).catch(() => {}) }, []) + return ( +
+

등록된 ERP 연동 ({cfgs.length}개)

+ {cfgs.map((c: any) => ( + + 🏢 +
+
{c.name}
+
{c.erp_type} · {c.base_url}
+
+
+ ))} + {!cfgs.length &&

등록된 연동 없음

} +
+ ) +} + +function KakaoTab() { + const [cfg, setCfg] = useState(null) + const [history, setHistory] = useState([]) + useEffect(() => { + guardiaApi.get('/api/kakao/config').then((r: any) => setCfg(r.data)).catch(() => {}) + guardiaApi.get('/api/kakao/history').then((r: any) => setHistory(r.data || [])).catch(() => {}) + }, []) + return ( +
+ +

카카오 알림톡

+ {cfg ?
✅ 연동됨
발신: {cfg.sender}
+ :

미설정

} +
+ +

발송 이력

+ + + {['템플릿', '수신', '결과', '시간'].map(h => )} + + + {history.slice(0, 8).map((h: any) => ( + + + + + + + ))} + {!history.length && } + +
{h}
{h.template}{h.receivers}명{h.success ? '성공' : '실패'}{h.sent_at ? new Date(h.sent_at).toLocaleDateString('ko-KR') : '-'}
이력 없음
+
+
+ ) +} + +function SSOTab() { + const [configs, setConfigs] = useState([]) + useEffect(() => { guardiaApi.get('/api/sso/config').then((r: any) => setConfigs(r.data || [])).catch(() => {}) }, []) + return ( +
+

등록된 SSO IdP ({configs.length}개)

+ {configs.map((c: any) => ( + + {c.provider_type === 'SAML' ? '🏛️' : '🔑'} +
+
{c.name}
+
{c.provider_type} · {c.is_active ? '활성' : '비활성'}
+
+ 테스트 → +
+ ))} + {!configs.length &&

등록된 IdP 없음

} + +
+ ) +} diff --git a/frontend/src/pages/KpiDashboard.tsx b/frontend/src/pages/KpiDashboard.tsx new file mode 100644 index 0000000..f68e35d --- /dev/null +++ b/frontend/src/pages/KpiDashboard.tsx @@ -0,0 +1,119 @@ +import { useEffect, useState } from 'react' +import { guardiaApi } from '../api/clients' + +const STATUS_COLOR: Record = { + GREEN: '#22c55e', YELLOW: '#f59e0b', RED: '#ef4444', NO_DATA: '#94a3b8', +} +const STATUS_ICON: Record = { + GREEN: '✅', YELLOW: '⚠️', RED: '❌', NO_DATA: '❔', +} + +interface KPI { + id: number; name: string; display_name: string + unit: string; direction: string; target: number + current_value: number | null; status: string + achievement_pct: number | null; last_calculated_at: string | null +} + +interface Dashboard { kpis: KPI[]; summary: Record; overall_status: string } + +function GaugeBar({ pct }: { pct: number | null }) { + const p = Math.min(100, Math.max(0, pct ?? 0)) + const color = p >= 95 ? '#22c55e' : p >= 80 ? '#f59e0b' : '#ef4444' + return ( +
+
+
+ ) +} + +export default function KpiDashboard() { + const [data, setData] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { load() }, []) + + async function load() { + setLoading(true) + try { + const d = await guardiaApi.get('/api/kpi/dashboard').then((r: any) => r.data) + setData(d) + } catch { /* 오류 무시 */ } + setLoading(false) + } + + async function applyTemplates() { + await guardiaApi.post('/api/kpi/apply-template', { + template_names: ['MTTR', 'FCR', 'SLA_COMPLIANCE', 'SR_BACKLOG', 'DEPLOY_SUCCESS_RATE'], + }) + load() + } + + async function recalc(id: number) { + await guardiaApi.post(`/api/kpi/${id}/calculate`) + load() + } + + if (loading) return
⏳ 로딩 중...
+ + const overall = data?.overall_status || 'NO_DATA' + const summary = data?.summary || {} + + return ( +
+ {/* 헤더 */} +
+
+

+ {STATUS_ICON[overall]} KPI 대시보드 +

+

+ GREEN:{summary.GREEN||0} · YELLOW:{summary.YELLOW||0} · RED:{summary.RED||0} +

+
+
+ + {(!data?.kpis.length) && ( + + )} +
+
+ + {/* KPI 카드 그리드 */} + {data?.kpis.length === 0 ? ( +
+

KPI가 없습니다.

+ +
+ ) : ( +
+ {data?.kpis.map(k => ( +
+
+ {k.display_name} + {k.status} +
+
+ {k.current_value !== null ? k.current_value : '—'} + {k.unit} +
+
+ 목표: {k.target}{k.unit} · 달성: {k.achievement_pct !== null ? `${k.achievement_pct}%` : '—'} +
+ + +
+ ))} +
+ )} +
+ ) +} diff --git a/frontend/tsconfig.tsbuildinfo b/frontend/tsconfig.tsbuildinfo index 9c34ea3..576c062 100644 --- a/frontend/tsconfig.tsbuildinfo +++ b/frontend/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/main.tsx","./src/api/clients.ts","./src/api/types.ts","./src/components/common/btn.tsx","./src/components/common/datatable.tsx","./src/components/common/protectedroute.tsx","./src/components/common/slidepanel.tsx","./src/components/common/statcard.tsx","./src/components/common/statusbadge.tsx","./src/components/layout/applayout.tsx","./src/components/layout/gnb.tsx","./src/components/layout/sidebar.tsx","./src/config/env.ts","./src/hooks/useapi.ts","./src/hooks/useauth.ts","./src/pages/apikeys.tsx","./src/pages/auditlog.tsx","./src/pages/cmdb.tsx","./src/pages/configenv.tsx","./src/pages/confignginx.tsx","./src/pages/csapconsole.tsx","./src/pages/dashboard.tsx","./src/pages/deployments.tsx","./src/pages/drconsole.tsx","./src/pages/exportimport.tsx","./src/pages/institutions.tsx","./src/pages/llmmanager.tsx","./src/pages/licenses.tsx","./src/pages/login.tsx","./src/pages/networkconsole.tsx","./src/pages/notifications.tsx","./src/pages/repos.tsx","./src/pages/servers.tsx","./src/pages/users.tsx"],"version":"5.9.3"} \ No newline at end of file +{"root":["./src/app.tsx","./src/main.tsx","./src/api/clients.ts","./src/api/types.ts","./src/components/common/btn.tsx","./src/components/common/datatable.tsx","./src/components/common/protectedroute.tsx","./src/components/common/slidepanel.tsx","./src/components/common/statcard.tsx","./src/components/common/statusbadge.tsx","./src/components/layout/applayout.tsx","./src/components/layout/gnb.tsx","./src/components/layout/sidebar.tsx","./src/config/env.ts","./src/hooks/useapi.ts","./src/hooks/useauth.ts","./src/pages/aiplatform.tsx","./src/pages/apikeys.tsx","./src/pages/auditlog.tsx","./src/pages/bianalytics.tsx","./src/pages/billingmanage.tsx","./src/pages/cmdb.tsx","./src/pages/configenv.tsx","./src/pages/confignginx.tsx","./src/pages/csapconsole.tsx","./src/pages/dashboard.tsx","./src/pages/deployments.tsx","./src/pages/drconsole.tsx","./src/pages/exportimport.tsx","./src/pages/institutions.tsx","./src/pages/integrationhub.tsx","./src/pages/kpidashboard.tsx","./src/pages/llmmanager.tsx","./src/pages/licenses.tsx","./src/pages/login.tsx","./src/pages/networkconsole.tsx","./src/pages/notifications.tsx","./src/pages/repos.tsx","./src/pages/scrapingmanager.tsx","./src/pages/servers.tsx","./src/pages/users.tsx"],"version":"5.9.3"} \ No newline at end of file