import { ChevronDown, ChevronUp, MoreHorizontal, Plus, RefreshCcw, Settings2, SlidersHorizontal } from 'lucide-react'; import { useEffect, useMemo, useState } from 'react'; import { DataTableCard, type DataTableColumn } from '../components/data-table'; import { Badge, Button, Card, CardHeader, Modal, Progress } from '../components/ui'; import { agentDevices, alerts, gatewayConnections, k3sWorkloads, launcherVersions, runtimeCommands, tailscaleSettings } from '../data/mock'; import { apiRequest } from '../lib/api'; import { PageTitle } from '../layout/AppLayout'; type Tone = 'neutral' | 'green' | 'amber' | 'red' | 'blue' | 'violet'; function tone(status: string): Tone { if (['활성', '완료', '게시됨', '실행 중', '배포됨'].includes(status)) return 'green'; if (['주의', '예약됨', '대기', '준비중', '카나리'].includes(status)) return 'amber'; if (['잠김', '실패', '오프라인', '비활성'].includes(status)) return 'red'; return 'neutral'; } function StatusBadge({ status, badgeTone }: { status: string; badgeTone?: Tone }) { return {status}; } type TailscaleNode = { id: string; name: string; ip: string; ips?: string[]; role: string; user: string; routes: string; lastSeen: string | null; status: string; online?: boolean; }; type TailscaleNodesResponse = { nodes: TailscaleNode[]; source?: string; fetchedAt?: string; error?: string; }; type TailscaleRoute = { id: string; nodeId: string; nodeName: string; user: string; route: string; approved: boolean; advertised: boolean; connected: boolean; lastSeenAt: string | null; }; type TailscaleRoutesResponse = { routes: TailscaleRoute[]; source?: string; fetchedAt?: string; error?: string; }; type TailscaleAcl = { id: string; action: string; sources: string[]; destinations: string[]; }; type TailscaleAclPolicy = { groups: Record; tagOwners: Record; acls: TailscaleAcl[]; }; type TailscaleAclResponse = { policy: TailscaleAclPolicy; source?: string; fetchedAt?: string; error?: string; }; function formatTailscaleLastSeen(value: string | null) { if (!value) return '-'; const time = Date.parse(value); if (!Number.isFinite(time)) return value; const diff = Date.now() - time; if (diff < 60_000) return '방금 전'; if (diff < 60 * 60_000) return `${Math.floor(diff / 60_000)}분 전`; if (diff < 24 * 60 * 60_000) return `${Math.floor(diff / (60 * 60_000))}시간 전`; return new Date(time).toLocaleString(); } export function LauncherVersionsPage() { const [sourceOpen, setSourceOpen] = useState(false); const [releaseOpen, setReleaseOpen] = useState(false); const launcher = launcherVersions[0]; return ( <> } /> {!launcher ? 등록된 런처 버전이 없습니다. : null} {launcher ? (

{launcher.name}

최신 {launcher.latestVersion} · 최소 지원 {launcher.minimumSupportedVersion || '-'}

배포 파일

{launcher.artifact}

다운로드 URL

{launcher.downloadUrl}

소스 연결

Repository

platform/launcher

Branch

release

Commit SHA

-

{launcher.releaseNotes}
) : null} setSourceOpen(false)} footer={ <> } >
setReleaseOpen(false)} footer={ <> } >
{['버전', '최소 지원 버전', '배포 파일', '다운로드 URL', 'Repository', 'Branch', 'Commit SHA'].map((label) => ( ))}