import { useEffect, useRef, useState, type FormEvent, type ReactNode } from 'react'; import { Activity, Bell, ChevronDown, ChevronRight, CircleHelp, ExternalLink, Copy, KeyRound, Lock, LogOut, MoreHorizontal, Plus, Play, RotateCcw, Search, Settings2, ShieldCheck, SlidersHorizontal, Trash2 } from 'lucide-react'; import { alerts, authApiEndpoints, appBundles, appDeploymentJobs, appMigrationJobs, appReleasePipelines, appSourceConfigs, apps, containerBlueprints, k3sWorkloads, agentDevices, gatewayConnections, launcherCommonSettings, launcherVersions, licensePlans, metrics, navGroups, notices, tailscaleNodes, tailscaleSettings, runtimeCommands, userLicenseAssignments } from './data/mock'; import { Badge, Button, Card, CardHeader, Progress } from './components/ui'; import { useToast } from './components/toast'; import { DataTableCard, type DataTableColumn, type DataTableFilter } from './components/data-table'; import { cn } from './lib/utils'; import { getStoredSelectedAppId, setStoredSelectedAppId } from './lib/selected-app'; import { BuildManagementPage } from './pages/applications/BuildManagementPage'; import { DeploymentManagementPage } from './pages/applications/DeploymentManagementPage'; import { MigrationManagementPage } from './pages/applications/MigrationManagementPage'; import { ReleaseManagementPage } from './pages/applications/ReleaseManagementPage'; import { RuntimeConfigPage as RuntimeConfigApiPage } from './pages/runtime/RuntimeConfigPage'; import { RuntimeLogsPage as RuntimeLogsApiPage } from './pages/runtime/RuntimeLogsPage'; type ViewKey = | 'dashboard' | 'launcherVersions' | 'launcherApps' | 'launcherCommonSettings' | 'notices' | 'appManagement' | 'appSources' | 'appBuilds' | 'appReleases' | 'appDeployments' | 'appMigrations' | 'gatewayStatus' | 'gatewayAgents' | 'gatewayDevices' | 'tailscaleStatus' | 'tailscaleRoutes' | 'tailscaleAcl' | 'tailscaleDiagnostics' | 'agentRegistration' | 'agentInstallStatus' | 'agentSshKeys' | 'agentWorkspace' | 'agentCommands' | 'runtimeConfig' | 'runtimeLogs' | 'runtimeDeviceControl' | 'adminManagement' | 'userSessions' | 'licenses'; const viewByLabel: Record = { 대시보드: 'dashboard', '버전 관리': 'launcherVersions', '앱 연결': 'launcherApps', '공통 설정': 'launcherCommonSettings', 공지사항: 'notices', '앱 관리': 'appManagement', '소스 관리': 'appSources', '빌드 관리': 'appBuilds', '릴리즈 관리': 'appReleases', '배포 관리': 'appDeployments', 마이그레이션: 'appMigrations', '연결 상태': 'gatewayStatus', 'Agent 관리': 'gatewayAgents', '장비 동기화': 'gatewayDevices', '노드 현황': 'tailscaleStatus', 라우트: 'tailscaleRoutes', ACL: 'tailscaleAcl', '연결 진단': 'tailscaleDiagnostics', '장비 등록': 'agentRegistration', '설치 상태': 'agentInstallStatus', 'SSH 키 배포': 'agentSshKeys', 'Workspace 설정': 'agentWorkspace', '원격 명령': 'agentCommands', '런타임 구성': 'runtimeConfig', '로그/이벤트': 'runtimeLogs', '장비 제어': 'runtimeDeviceControl', '관리자 관리': 'adminManagement', '사용자/세션': 'userSessions', 라이센스: 'licenses' }; const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:4500/api'; const giteaBaseUrl = import.meta.env.VITE_GITEA_BASE_URL ?? 'http://localhost:3000'; const giteaSshBaseUrl = import.meta.env.VITE_GITEA_SSH_BASE_URL ?? 'ssh://git@localhost:2222'; type GiteaRepo = { id: number; owner: string; name: string; fullName: string; description?: string; private: boolean; htmlUrl: string; cloneUrl: string; sshUrl: string; defaultBranch: string; updatedAt: string; language?: string; branchCount?: number; releaseCount?: number; }; type ApiAppComponent = { id: string; appId: string; name: string; type: string; repository?: string | null; branch?: string | null; buildTarget?: string | null; app?: ApiApp; }; type ApiApp = { id: string; key: string; name: string; description?: string | null; namespace?: string | null; status: string; components?: ApiAppComponent[]; }; type ApiLauncherVersion = { id: string; version: string; minimumVersion?: string | null; repository?: string | null; branch?: string | null; commitSha?: string | null; artifact: string; downloadUrl: string; releaseNotes?: string | null; isActive: boolean; createdAt: string; updatedAt: string; }; type UserSessionRow = { id: string; no: number; sessionId: string | null; userId: string; user: string; email: string; role: string; session: string; createdAt: string | null; expiresAt: string | null; lastSeen: string | null; state: string; }; type AdminRow = { id: string; email: string; name: string; role: 'OWNER' | 'ADMIN' | 'OPERATOR' | 'VIEWER'; isActive: boolean; lastLoginAt: string | null; createdAt: string; updatedAt: string; sessions?: Array<{ id: string; expiresAt: string | null; revokedAt: string | null; createdAt: string; }>; sshKeys?: Array<{ id: string; label?: string | null; fingerprint?: string | null; }>; }; const statusTone = { 활성: 'green', '실행 중': 'green', 게시됨: 'green', 카나리: 'blue', 예약됨: 'blue', 점검: 'amber', 주의: 'amber', 드레이닝: 'amber', 준비중: 'amber', 배포됨: 'green', 초안: 'neutral', 대기: 'neutral', 만료: 'neutral', 종료: 'neutral', '세션 없음': 'neutral', 비활성: 'neutral', '지원 종료': 'red', 잠김: 'red', 구성완료: 'green', 검토필요: 'amber', 완료: 'green' } as const; function getTone(status: string) { return statusTone[status as keyof typeof statusTone] ?? 'neutral'; } type NavGroup = (typeof navGroups)[number]; type NavItem = NavGroup['items'][number]; function Sidebar({ activeView, menuSearch, onViewChange }: { activeView: ViewKey; menuSearch: string; onViewChange: (view: ViewKey) => void; }) { const [openMenus, setOpenMenus] = useState>({}); const normalizedMenuSearch = menuSearch.trim().toLowerCase(); const visibleGroups = normalizedMenuSearch ? navGroups .map((group) => { const groupMatches = group.label.toLowerCase().includes(normalizedMenuSearch); const items = groupMatches ? group.items : group.items.flatMap((item) => { const itemMatches = item.label.toLowerCase().includes(normalizedMenuSearch); if ('children' in item && item.children) { const matchedChildren = item.children.filter((child) => child.label.toLowerCase().includes(normalizedMenuSearch)); if (itemMatches) return [item]; if (matchedChildren.length) return [{ ...item, children: matchedChildren } as NavItem]; return []; } return itemMatches ? [item] : []; }); return { ...group, items }; }) .filter((group) => group.items.length > 0) : navGroups; return ( ); } function PageTitle({ eyebrow, title, description, actions }: { eyebrow: string; title: string; description: string; actions?: ReactNode; }) { return (

{eyebrow}

{title}

{description}

{actions ?
{actions}
: null}
); } function LoadingModal({ open, label = '데이터를 불러오는 중입니다.' }: { open: boolean; label?: string }) { if (!open) return null; return (

{label}

); } function useMinimumLoading(loading: boolean, minimumMs = 800) { const [visible, setVisible] = useState(loading); const startedAtRef = useRef(loading ? Date.now() : null); useEffect(() => { if (loading) { startedAtRef.current = Date.now(); setVisible(true); return; } if (!visible) return; const elapsed = startedAtRef.current ? Date.now() - startedAtRef.current : minimumMs; const remaining = Math.max(minimumMs - elapsed, 0); const timer = window.setTimeout(() => { startedAtRef.current = null; setVisible(false); }, remaining); return () => window.clearTimeout(timer); }, [loading, minimumMs, visible]); return visible; } function Topbar({ menuSearch, onMenuSearchChange }: { menuSearch: string; onMenuSearchChange: (value: string) => void; }) { return (
onMenuSearchChange(event.target.value)} />
); } function LoginPage({ onLogin }: { onLogin: () => void }) { const [email, setEmail] = useState('admin@futuredrive.net'); const [password, setPassword] = useState(''); const [loginLoading, setLoginLoading] = useState(false); const [loginError, setLoginError] = useState(false); const passwordInputRef = useRef(null); const passwordMask = '•'.repeat(password.length); const submitLogin = (event: FormEvent) => { event.preventDefault(); if (!email.trim() || !password.trim()) return; setLoginLoading(true); setLoginError(false); fetch(`${apiBaseUrl}/auth/admin/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: email.trim(), password }) }) .then((response) => { if (!response.ok) throw new Error('Admin login failed'); return response.json() as Promise<{ token: string; expiresAt: string; admin: { email: string; name: string; role: string } }>; }) .then((data) => { localStorage.setItem('studio-admin-token', data.token); localStorage.setItem('studio-admin', JSON.stringify(data.admin)); setLoginLoading(false); onLogin(); }) .catch(() => { setLoginLoading(false); setLoginError(true); }); }; return (
Autonomy Studio

관리자 로그인

계정 정보를 입력해 주세요.

{loginError ? (

이메일 또는 비밀번호를 확인해 주세요.

) : null}
); } function MetricCards() { return (
{metrics.map((metric) => (

{metric.label}

{metric.delta}
{metric.value}

{metric.hint}

))}
); } function LauncherPanel() { return ( 배포 정책 } />
{launcherVersions.map((item) => (

{item.name}

{item.status}

최신 {item.latestVersion} · 최소 지원 {item.minimumSupportedVersion}

배포 파일

{item.artifact}

다운로드 URL

{item.downloadUrl}

{item.releaseNotes}

))}

운영 알림

{alerts.map((alert) => (

{alert.label}

))}
); } function SessionsPanel({ onMore }: { onMore: () => void }) { const [recentSessions, setRecentSessions] = useState([]); const [sessionsLoading, setSessionsLoading] = useState(true); useEffect(() => { loadUserSessions() .then((rows) => { setRecentSessions(rows.slice(0, 5)); setSessionsLoading(false); }) .catch(() => { setRecentSessions([]); setSessionsLoading(false); }); }, []); return ( 더보기 } />
{sessionsLoading ? (
불러오는 중
) : recentSessions.length === 0 ? (
표시할 사용자 세션이 없습니다.
) : null} {!sessionsLoading && recentSessions.map((session) => (

{session.user}

{session.state}

{session.email} · {session.role}

{formatDateTime(session.lastSeen)}
))}
); } function AppRegistryTable() { return ( 앱 등록 } />
{apps.map((app) => ( ))}
앱 상태 릴리즈 저장소/경로 워크로드 시크릿 사용량

{app.name}

{app.key} · {app.version}

{app.status} {app.release} {app.gitea} {app.containers} {app.secrets}
70 ? 'amber' : 'primary'} /> 75 ? 'red' : 'green'} />
); } function ContainerPanel() { return (
{k3sWorkloads.map((item) => (

{item.workload}

{item.status}

{item.app} · {item.node}

CPU {item.cpu}%
80 ? 'red' : item.cpu > 65 ? 'amber' : 'primary'} />
메모리 {item.memory}%
80 ? 'red' : item.memory > 65 ? 'amber' : 'green'} />
))}
); } function NoticePanel() { return ( 공지 작성 } />
{notices.map((notice) => (
{notice.scope} {notice.status}

{notice.title}

{notice.audience}

{notice.date}
))}
); } function LauncherManagementPage() { const [launcherRows, setLauncherRows] = useState([]); const [launcherRepos, setLauncherRepos] = useState([]); const [launcherLoading, setLauncherLoading] = useState(true); const launcherLoadingVisible = useMinimumLoading(launcherLoading); const [launcherError, setLauncherError] = useState(false); const [sourceModalOpen, setSourceModalOpen] = useState(false); const [sourceRepository, setSourceRepository] = useState(''); const [sourceBranch, setSourceBranch] = useState('release'); const [sourceCommitSha, setSourceCommitSha] = useState(''); const [sourceSaveStatus, setSourceSaveStatus] = useState<'idle' | 'saving' | 'error'>('idle'); const [releaseModalOpen, setReleaseModalOpen] = useState(false); const [releaseSaveStatus, setReleaseSaveStatus] = useState<'idle' | 'saving' | 'error'>('idle'); const [newLauncherRelease, setNewLauncherRelease] = useState({ version: '', minimumVersion: '', artifact: '', downloadUrl: '', releaseNotes: '', repository: '', branch: 'release', commitSha: '' }); const launcher = launcherRows[0]; const refreshLauncherVersions = () => { setLauncherLoading(true); loadLauncherVersions() .then((versions) => { setLauncherRows(versions); setLauncherError(false); setLauncherLoading(false); }) .catch(() => { setLauncherRows([]); setLauncherError(true); setLauncherLoading(false); }); }; useEffect(() => { let mounted = true; refreshLauncherVersions(); loadPlatformRepos() .then((response) => { if (!response.ok) throw new Error('Gitea repositories request failed'); return response.json() as Promise<{ repos: GiteaRepo[] }>; }) .then((data) => { if (!mounted) return; setLauncherRepos(data.repos ?? []); }) .catch(() => { if (!mounted) return; setLauncherRepos([]); }); return () => { mounted = false; }; }, []); const openSourceModal = () => { if (!launcher) return; setSourceRepository(launcher.repository ?? launcherRepos[0]?.fullName ?? ''); setSourceBranch(launcher.branch ?? 'release'); setSourceCommitSha(launcher.commitSha ?? ''); setSourceSaveStatus('idle'); setSourceModalOpen(true); }; const saveLauncherSource = () => { if (!launcher || !sourceRepository.trim()) return; setSourceSaveStatus('saving'); fetch(`${apiBaseUrl}/launcher/versions/${launcher.id}/source`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ repository: sourceRepository.trim(), branch: sourceBranch.trim() || undefined, commitSha: sourceCommitSha.trim() || undefined }) }) .then((response) => { if (!response.ok) throw new Error('Launcher source link failed'); return response.json(); }) .then(() => { setSourceModalOpen(false); setSourceSaveStatus('idle'); refreshLauncherVersions(); }) .catch(() => setSourceSaveStatus('error')); }; const openReleaseModal = () => { setNewLauncherRelease({ version: '', minimumVersion: launcher?.minimumVersion ?? '', artifact: '', downloadUrl: '', releaseNotes: '', repository: launcher?.repository ?? launcherRepos[0]?.fullName ?? '', branch: launcher?.branch ?? 'release', commitSha: '' }); setReleaseSaveStatus('idle'); setReleaseModalOpen(true); }; const createLauncherRelease = () => { if (!newLauncherRelease.version.trim() || !newLauncherRelease.artifact.trim() || !newLauncherRelease.downloadUrl.trim()) return; setReleaseSaveStatus('saving'); fetch(`${apiBaseUrl}/launcher/versions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ version: newLauncherRelease.version.trim(), minimumVersion: newLauncherRelease.minimumVersion.trim() || undefined, artifact: newLauncherRelease.artifact.trim(), downloadUrl: newLauncherRelease.downloadUrl.trim(), releaseNotes: newLauncherRelease.releaseNotes.trim() || undefined, repository: newLauncherRelease.repository || undefined, branch: newLauncherRelease.branch || undefined, commitSha: newLauncherRelease.commitSha.trim() || undefined }) }) .then((response) => { if (!response.ok) throw new Error('Launcher release create failed'); return response.json(); }) .then(() => { setReleaseModalOpen(false); setReleaseSaveStatus('idle'); refreshLauncherVersions(); }) .catch(() => setReleaseSaveStatus('error')); }; return ( <> } /> {launcherError ? (
런처 버전 데이터를 불러오지 못했습니다.
) : null} {!launcherLoading && !launcher ? ( 등록된 런처 버전이 없습니다. ) : null} {launcher ? (

Autonomy Studio Launcher

{launcher.isActive ? '활성' : '비활성'}

최신 {launcher.version} · 최소 지원 {launcher.minimumVersion || '-'}

배포 파일

{launcher.artifact}

다운로드 URL

{launcher.downloadUrl}

소스 연결

{launcher.repository ? (

Repository

{launcher.repository}

Branch

{launcher.branch || '-'}

Commit

{launcher.commitSha || '-'}

Clone URL

{getHttpCloneUrl(launcher.repository)}

) : (

연결된 Gitea 저장소가 없습니다.

)}

릴리즈 노트

{launcher.releaseNotes || '-'}

{[ ['최신 버전', launcher.version], ['최소 런처 버전', launcher.minimumVersion || '-'] ].map(([label, value]) => (
{label} {value}
))}
) : null} {sourceModalOpen && launcher ? (

런처 소스 연결

{sourceSaveStatus === 'error' ?

런처 소스 연결 저장에 실패했습니다.

: null}
) : null} {releaseModalOpen ? (

런처 릴리즈 등록