import { useState, useRef, useCallback, useEffect } from "react"; import { useIsMobile } from "@/hooks/use-mobile"; import { Button } from "@/components/button"; import { Card } from "@/components/card"; import { Separator } from "@/components/separator"; import { Activity, Database, GripHorizontal, GripVertical, KeyRound, LayoutDashboard, Network, Plus, Server, Settings, Terminal, Trash2, User, Zap, } from "lucide-react"; import { Kbd } from "@/components/kbd"; import { DASHBOARD_CARDS } from "@/lib/theme"; import type { DashboardCardId, TabType, Host } from "@/types/ui-types"; import { getSSHHosts, getUptime, getVersionInfo, getDatabaseHealth, getRecentActivity, getTunnelStatuses, getCredentials, resetRecentActivity, getAllServerStatuses, getServerMetricsById, registerMetricsViewer, sendMetricsHeartbeat, getUserInfo, } from "@/main-axios"; import type { RecentActivityItem, SSHHostWithStatus } from "@/main-axios"; import { useTranslation } from "react-i18next"; import { NetworkGraphCard } from "@/dashboard/cards/NetworkGraphCard"; function sshHostToHost(h: SSHHostWithStatus): Host { return { id: String(h.id), name: h.name, username: h.username, ip: h.ip, port: h.port, folder: h.folder ?? "", online: h.status === "online", cpu: 0, ram: 0, lastAccess: "", tags: h.tags ?? [], authType: h.authType, password: h.password, key: typeof h.key === "string" ? h.key : undefined, keyPassword: h.keyPassword, keyType: h.keyType, credentialId: h.credentialId != null ? String(h.credentialId) : undefined, notes: h.notes, pin: h.pin ?? false, macAddress: h.macAddress, enableTerminal: h.enableTerminal ?? true, enableTunnel: h.enableTunnel ?? false, enableFileManager: h.enableFileManager ?? false, enableDocker: h.enableDocker ?? false, enableSsh: h.connectionType === "ssh" || !h.connectionType, enableRdp: h.connectionType === "rdp", enableVnc: h.connectionType === "vnc", enableTelnet: h.connectionType === "telnet", sshPort: h.port, rdpPort: 3389, vncPort: 5900, telnetPort: 23, quickActions: (h.quickActions ?? []).map((a) => ({ name: a.name, snippetId: String(a.snippetId), })), serverTunnels: [], defaultPath: h.defaultPath, terminalConfig: h.terminalConfig as Host["terminalConfig"], useSocks5: h.useSocks5, socks5Host: h.socks5Host, socks5Port: h.socks5Port, socks5Username: h.socks5Username, socks5Password: h.socks5Password, }; } // ─── Types ──────────────────────────────────────────────────────────────────── type PanelId = "main" | "side"; type CardSlot = { id: DashboardCardId; panel: PanelId; order: number; height: number | null; }; type DragState = { id: DashboardCardId; sourcePanel: PanelId; sourceOrder: number; } | null; // ─── Default layout ─────────────────────────────────────────────────────────── const DEFAULT_SLOTS: CardSlot[] = [ { id: "stats_bar", panel: "main", order: 0, height: 96 }, { id: "counters_bar", panel: "main", order: 1, height: 48 }, { id: "quick_actions", panel: "main", order: 2, height: 160 }, { id: "host_status", panel: "main", order: 3, height: null }, { id: "recent_activity", panel: "side", order: 0, height: null }, ]; // ─── useColumnResize ────────────────────────────────────────────────────────── function useColumnResize( containerRef: React.RefObject, mainWidthPct: number, setMainWidthPct: (v: number) => void, ) { const dragging = useRef(false); const startX = useRef(0); const startPct = useRef(0); const onMouseDown = useCallback( (e: React.MouseEvent) => { e.preventDefault(); dragging.current = true; startX.current = e.clientX; startPct.current = mainWidthPct; const onMove = (ev: MouseEvent) => { if (!dragging.current || !containerRef.current) return; const totalW = containerRef.current.getBoundingClientRect().width; const delta = ev.clientX - startX.current; const newPct = Math.min( 85, Math.max(30, startPct.current + (delta / totalW) * 100), ); setMainWidthPct(newPct); }; const onUp = () => { dragging.current = false; window.removeEventListener("mousemove", onMove); window.removeEventListener("mouseup", onUp); }; window.addEventListener("mousemove", onMove); window.addEventListener("mouseup", onUp); }, [containerRef, mainWidthPct, setMainWidthPct], ); return onMouseDown; } // ─── Card components ────────────────────────────────────────────────────────── function StatsBarCard({ hosts, uptimeFormatted, versionText, versionStatus, dbHealth, }: { hosts: Host[]; uptimeFormatted: string; versionText: string; versionStatus: "up_to_date" | "requires_update" | "beta"; dbHealth: "healthy" | "error"; }) { const { t } = useTranslation(); const online = hosts.filter((h) => h.online).length; const statusLabel = versionStatus === "beta" ? t("dashboard.beta").toUpperCase() : versionStatus === "requires_update" ? t("dashboard.updateAvailable").toUpperCase() : t("dashboardTab.stable"); const statusColor = versionStatus === "beta" ? "bg-blue-500/20 text-blue-400" : versionStatus === "requires_update" ? "bg-yellow-500/20 text-yellow-400" : "bg-accent-brand/20 text-accent-brand"; return (
{t("dashboard.version")} {versionText || "—"} {statusLabel}
{t("dashboard.uptime")} {uptimeFormatted || "—"}
{t("dashboard.database")} {dbHealth === "healthy" ? t("dashboard.healthy") : t("dashboard.error")}
{t("dashboardTab.hostsOnline")}
{online} /{hosts.length}
); } function CountersBarCard({ hosts, credentialCount, activeTunnelCount, }: { hosts: Host[]; credentialCount: number; activeTunnelCount: number; }) { const { t } = useTranslation(); return (
{hosts.length} {t("dashboard.totalHosts")}
{credentialCount} {t("dashboard.totalCredentials")}
{activeTunnelCount} {t("dashboardTab.activeTunnels")}
); } function QuickActionsCard({ onOpenSingletonTab, hosts, onOpenTab, isAdmin, }: { onOpenSingletonTab: (type: TabType, pendingEvent?: string) => void; hosts: Host[]; onOpenTab: (host: Host, type: TabType) => void; isAdmin: boolean; }) { const { t } = useTranslation(); const pinnedHosts = hosts.filter((h) => h.pin); return (
{t("dashboard.quickActions")}
{pinnedHosts.length > 0 ? (
{pinnedHosts.slice(0, 4).map((host) => ( ))}
) : ( <> {isAdmin && ( )} )}
); } function HostStatusCard({ hosts, hostMetrics, onOpenTab, }: { hosts: Host[]; hostMetrics: Map; onOpenTab: (host: Host, type: TabType) => void; }) { const { t } = useTranslation(); const online = hosts.filter((h) => h.online).length; return (
{t("dashboardTab.hostStatus")}
{online}/{hosts.length} {t("dashboardTab.onlineLower")}
{hosts.length === 0 && (
{t("dashboardTab.noHostsConfigured")}
)} {hosts.map((host, i) => { const metrics = hostMetrics.get(host.id); const cpu = metrics?.cpu ?? null; const ram = metrics?.ram ?? null; const hasMetrics = cpu !== null || ram !== null; return (
onOpenTab(host, "stats")} className="flex items-center justify-between px-4 py-2.5 border-b border-border last:border-0 hover:bg-muted/50 cursor-pointer" >
{host.name} {host.ip}
{host.online && hasMetrics ? (
{cpu !== null && (
{t("dashboard.cpu")} {cpu.toFixed(0)}%
)} {ram !== null && (
{t("dashboard.ram")} {ram.toFixed(0)}%
)}
) : (
)} {host.online ? t("dashboardTab.online") : t("dashboardTab.offline")}
); })}
); } function RecentActivityCard({ activity, hosts, onOpenTab, onClear, }: { activity: RecentActivityItem[]; hosts: Host[]; onOpenTab: (host: Host, type: TabType) => void; onClear: () => void; }) { const { t } = useTranslation(); const typeIcon: Record = { terminal: , file_manager: , server_stats: , tunnel: , docker: , rdp: , vnc: , telnet: , }; const typeToTab: Record = { terminal: "terminal", file_manager: "files", server_stats: "stats", tunnel: "tunnel", docker: "docker", rdp: "rdp", vnc: "vnc", telnet: "telnet", }; const typeLabel: Record = { terminal: t("networkGraph.terminal"), file_manager: t("networkGraph.fileManager"), server_stats: t("networkGraph.serverStats"), tunnel: t("networkGraph.tunnel"), docker: t("networkGraph.docker"), rdp: "RDP", vnc: "VNC", telnet: "Telnet", }; function formatTime(ts: string) { const diffMs = Date.now() - new Date(ts).getTime(); if (diffMs < 0) return t("dashboard.justNow"); const diff = Math.floor(diffMs / 1000); if (diff < 60) return t("dashboard.justNow"); if (diff < 3600) return `${Math.floor(diff / 60)}m`; if (diff < 86400) return `${Math.floor(diff / 3600)}h`; return `${Math.floor(diff / 86400)}d`; } return (
{t("dashboard.recentActivity")}
{activity.length === 0 && (
{t("dashboard.noRecentActivity")}
)} {activity.map((item) => { const host = hosts.find((h) => h.id === String(item.hostId)); return (
{ if (host) onOpenTab(host, typeToTab[item.type]); }} className="flex items-center justify-between px-4 py-2 border-b border-border last:border-0 hover:bg-muted/50 cursor-pointer" >
{item.hostName}
{typeIcon[item.type]} {typeLabel[item.type]}
{formatTime(item.timestamp)}
); })}
); } // ─── CardItem ───────────────────────────────────────────────────────────────── function CardItem({ slot, editMode, isDragging, onDragStart, onDrop, onDragOver, onRemove, onHeightChange, onOpenSingletonTab, onOpenTab, hosts, hostMetrics, uptimeFormatted, versionText, versionStatus, dbHealth, credentialCount, activeTunnelCount, activity, onClearActivity, isAdmin, }: { slot: CardSlot; editMode: boolean; isDragging: boolean; onDragStart: () => void; onDrop: () => void; onDragOver: (e: React.DragEvent) => void; onRemove: () => void; onHeightChange: (id: DashboardCardId, h: number) => void; onOpenSingletonTab: (type: TabType, pendingEvent?: string) => void; onOpenTab: (host: Host, type: TabType) => void; hosts: Host[]; hostMetrics: Map; uptimeFormatted: string; versionText: string; versionStatus: "up_to_date" | "requires_update" | "beta"; dbHealth: "healthy" | "error"; credentialCount: number; activeTunnelCount: number; activity: RecentActivityItem[]; onClearActivity: () => void; isAdmin: boolean; }) { const cardRef = useRef(null); const onResizeMouseDown = useCallback( (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); const startY = e.clientY; const startH = cardRef.current?.getBoundingClientRect().height ?? 100; const onMove = (ev: MouseEvent) => { onHeightChange(slot.id, Math.max(50, startH + (ev.clientY - startY))); }; const onUp = () => { window.removeEventListener("mousemove", onMove); window.removeEventListener("mouseup", onUp); }; window.addEventListener("mousemove", onMove); window.addEventListener("mouseup", onUp); }, [slot.id, onHeightChange], ); const isFlex = slot.height === null; return (
{editMode && (
)} {editMode && (
)}
{slot.id === "stats_bar" && ( )} {slot.id === "counters_bar" && ( )} {slot.id === "quick_actions" && ( )} {slot.id === "host_status" && ( )} {slot.id === "recent_activity" && ( )} {slot.id === "network_graph" && ( onOpenSingletonTab("network_graph")} /> )}
{editMode && !isFlex && (
)}
); } // ─── DropZone ───────────────────────────────────────────────────────────────── function DropZone({ panel, order, onDrop, onDragOver, active, }: { panel: PanelId; order: number; onDrop: (panel: PanelId, order: number) => void; onDragOver: (e: React.DragEvent) => void; active: boolean; }) { const [over, setOver] = useState(false); if (!active) return null; return (
{ onDragOver(e); setOver(true); }} onDragLeave={() => setOver(false)} onDrop={() => { setOver(false); onDrop(panel, order); }} /> ); } // ─── AddCardTray ────────────────────────────────────────────────────────────── function AddCardTray({ activeIds, onAdd, cardLabels, }: { activeIds: DashboardCardId[]; onAdd: (id: DashboardCardId) => void; cardLabels: Record; }) { const { t } = useTranslation(); const available = DASHBOARD_CARDS.filter((c) => !activeIds.includes(c.id)); if (available.length === 0) return null; return (
{t("dashboardTab.add")} {available.map((card) => ( ))}
); } // ─── PanelColumn ───────────────────────────────────────────────────────────── type PanelColumnProps = { panel: PanelId; slots: CardSlot[]; editMode: boolean; dragState: DragState; onDragStart: (slot: CardSlot) => void; onDrop: (targetPanel: PanelId, targetOrder: number) => void; onDragOver: (e: React.DragEvent) => void; onRemove: (id: DashboardCardId) => void; onAdd: (id: DashboardCardId, panel: PanelId) => void; onHeightChange: (id: DashboardCardId, h: number) => void; onOpenSingletonTab: (type: TabType, pendingEvent?: string) => void; onOpenTab: (host: Host, type: TabType) => void; hosts: Host[]; hostMetrics: Map; uptimeFormatted: string; versionText: string; versionStatus: "up_to_date" | "requires_update" | "beta"; dbHealth: "healthy" | "error"; credentialCount: number; activeTunnelCount: number; activity: RecentActivityItem[]; onClearActivity: () => void; cardLabels: Record; isAdmin: boolean; }; function PanelColumn({ panel, slots, editMode, dragState, onDragStart, onDrop, onDragOver, onRemove, onAdd, onHeightChange, onOpenSingletonTab, onOpenTab, hosts, hostMetrics, uptimeFormatted, versionText, versionStatus, dbHealth, credentialCount, activeTunnelCount, activity, onClearActivity, cardLabels, isAdmin, }: PanelColumnProps) { const { t } = useTranslation(); const sorted = [...slots].sort((a, b) => a.order - b.order); const allIds = slots.map((s) => s.id); return (
{sorted.map((slot, idx) => (
{idx > 0 && (
)} onDragStart(slot)} onDrop={() => onDrop(slot.panel, slot.order)} onDragOver={onDragOver} onRemove={() => onRemove(slot.id)} onHeightChange={onHeightChange} onOpenSingletonTab={onOpenSingletonTab} onOpenTab={onOpenTab} hosts={hosts} hostMetrics={hostMetrics} uptimeFormatted={uptimeFormatted} versionText={versionText} versionStatus={versionStatus} dbHealth={dbHealth} credentialCount={credentialCount} activeTunnelCount={activeTunnelCount} activity={activity} onClearActivity={onClearActivity} isAdmin={isAdmin} />
))} {editMode && ( onAdd(id, panel)} cardLabels={cardLabels} /> )} {sorted.length === 0 && !editMode && (
{t("dashboardTab.empty")}
)}
); } function ColumnDivider({ onMouseDown, }: { onMouseDown: (e: React.MouseEvent) => void; }) { return (
); } // ─── DashboardTab ───────────────────────────────────────────────────────────── export function DashboardTab({ onOpenSingletonTab, onOpenTab, }: { onOpenSingletonTab: (type: TabType, pendingEvent?: string) => void; onOpenTab: (host: Host, type: TabType) => void; }) { const { t, i18n } = useTranslation(); const [slots, setSlots] = useState(() => { try { const saved = localStorage.getItem("dashboardTab.slots"); if (saved) return JSON.parse(saved) as CardSlot[]; } catch { /* ignore */ } return DEFAULT_SLOTS; }); const [editMode, setEditMode] = useState(false); const [dragState, setDragState] = useState(null); const [mainWidthPct, setMainWidthPct] = useState(() => { try { const saved = localStorage.getItem("dashboardTab.mainWidthPct"); if (saved) return Number(saved); } catch { /* ignore */ } return 68; }); useEffect(() => { try { localStorage.setItem("dashboardTab.slots", JSON.stringify(slots)); } catch { /* ignore */ } }, [slots]); useEffect(() => { try { localStorage.setItem("dashboardTab.mainWidthPct", String(mainWidthPct)); } catch { /* ignore */ } }, [mainWidthPct]); const [hosts, setHosts] = useState([]); const [isAdmin, setIsAdmin] = useState(false); const [uptimeFormatted, setUptimeFormatted] = useState(""); const [versionText, setVersionText] = useState(""); const [versionStatus, setVersionStatus] = useState< "up_to_date" | "requires_update" | "beta" >("up_to_date"); const [dbHealth, setDbHealth] = useState<"healthy" | "error">("healthy"); const [credentialCount, setCredentialCount] = useState(0); const [activeTunnelCount, setActiveTunnelCount] = useState(0); const [activity, setActivity] = useState([]); const [hostMetrics, setHostMetrics] = useState< Map >(new Map()); const viewerSessionsRef = useRef>(new Map()); const fetchMetrics = useCallback(async (hostList: Host[]) => { let statuses: Record = {}; try { statuses = (await getAllServerStatuses()) as Record< number, { status?: string } >; } catch { /* best-effort */ } const newSessions = new Map(viewerSessionsRef.current); const results = await Promise.all( hostList.map(async (host) => { const hostId = Number(host.id); const knownStatus = statuses?.[hostId]?.status; if (knownStatus === "offline") return null; if (host.authType === "none" || host.authType === "opkssh") return null; try { const existing = newSessions.get(hostId); if (!existing) { const reg = await registerMetricsViewer(hostId); if (reg.skipped) return null; if (reg.success && reg.viewerSessionId) { newSessions.set(hostId, reg.viewerSessionId); } } const metrics = await getServerMetricsById(hostId); if (!metrics) return null; return { id: host.id, cpu: metrics.cpu?.percent ?? null, ram: metrics.memory?.percent ?? null, }; } catch { return null; } }), ); viewerSessionsRef.current = newSessions; const map = new Map(); for (const r of results) { if (r) map.set(r.id, { cpu: r.cpu, ram: r.ram }); } setHostMetrics(map); }, []); useEffect(() => { let mounted = true; const load = async () => { const raw = await getSSHHosts().catch(() => []); const mapped = raw.map(sshHostToHost); if (mounted) setHosts(mapped); fetchMetrics(mapped).catch(() => {}); }; load(); getUserInfo() .then((info) => setIsAdmin(!!info.is_admin)) .catch(() => {}); getUptime() .then((u) => setUptimeFormatted(u.formatted)) .catch(() => {}); getVersionInfo() .then((info) => { setVersionText(info.localVersion ?? ""); setVersionStatus(info.status ?? "up_to_date"); }) .catch(() => {}); getDatabaseHealth() .then((health) => { setDbHealth( health.status === "ok" || health.status === "healthy" ? "healthy" : "error", ); }) .catch(() => { setDbHealth("error"); }); getRecentActivity(50) .then(setActivity) .catch(() => {}); getCredentials() .then((res: any) => setCredentialCount( Array.isArray(res?.credentials) ? res.credentials.length : 0, ), ) .catch(() => {}); getTunnelStatuses() .then((statuses) => { const active = Object.values(statuses ?? {}).filter( (s: any) => s?.status === "CONNECTED", ).length; setActiveTunnelCount(active); }) .catch(() => {}); const metricsInterval = setInterval(async () => { const raw = await getSSHHosts().catch(() => []); const mapped = raw.map(sshHostToHost); if (mounted) setHosts(mapped); fetchMetrics(mapped).catch(() => {}); }, 30000); return () => { mounted = false; clearInterval(metricsInterval); }; }, [fetchMetrics]); useEffect(() => { if (viewerSessionsRef.current.size === 0) return; const heartbeat = setInterval(async () => { for (const [, sessionId] of viewerSessionsRef.current) { sendMetricsHeartbeat(sessionId).catch(() => {}); } }, 30000); return () => clearInterval(heartbeat); }, [hostMetrics]); const handleClearActivity = async () => { try { await resetRecentActivity(); setActivity([]); } catch { /* ignore */ } }; const todayLabel = new Date().toLocaleDateString(i18n.language, { weekday: "long", month: "long", day: "numeric", year: "numeric", }); const bodyRef = useRef(null); const mainSlots = slots .filter((s) => s.panel === "main") .sort((a, b) => a.order - b.order); const sideSlots = slots .filter((s) => s.panel === "side") .sort((a, b) => a.order - b.order); const hasSide = sideSlots.length > 0; const cardLabels: Record = { stats_bar: t("dashboard.serverOverview"), counters_bar: t("dashboard.serverStats"), quick_actions: t("dashboard.quickActions"), host_status: t("dashboardTab.hostStatus"), recent_activity: t("dashboard.recentActivity"), network_graph: t("dashboard.networkGraph"), }; const onColumnDividerMouseDown = useCallback( (e: React.MouseEvent) => { e.preventDefault(); const startX = e.clientX; const startPct = mainWidthPct; const onMove = (ev: MouseEvent) => { if (!bodyRef.current) return; const totalW = bodyRef.current.getBoundingClientRect().width; setMainWidthPct( Math.min( 85, Math.max(25, startPct + ((ev.clientX - startX) / totalW) * 100), ), ); }; const onUp = () => { window.removeEventListener("mousemove", onMove); window.removeEventListener("mouseup", onUp); }; window.addEventListener("mousemove", onMove); window.addEventListener("mouseup", onUp); }, [mainWidthPct], ); const handleDragStart = (slot: CardSlot) => setDragState({ id: slot.id, sourcePanel: slot.panel, sourceOrder: slot.order, }); const handleDragOver = (e: React.DragEvent) => e.preventDefault(); const handleDrop = (targetPanel: PanelId, targetOrder: number) => { if (!dragState) return; setSlots((prev) => { const without = prev.filter((s) => s.id !== dragState.id); const panelSlots = without .filter((s) => s.panel === targetPanel) .sort((a, b) => a.order - b.order); const others = without.filter((s) => s.panel !== targetPanel); const insertIdx = panelSlots.findIndex((s) => s.order > targetOrder); const insertAt = insertIdx === -1 ? panelSlots.length : insertIdx; const newPanelSlots = [ ...panelSlots.slice(0, insertAt), { id: dragState.id, panel: targetPanel, order: 0, height: prev.find((s) => s.id === dragState.id)?.height ?? null, }, ...panelSlots.slice(insertAt), ].map((s, i) => ({ ...s, order: i })); return [...others, ...newPanelSlots]; }); setDragState(null); }; const handleRemove = (id: DashboardCardId) => setSlots((prev) => prev.filter((s) => s.id !== id)); const handleAdd = (id: DashboardCardId, panel: PanelId) => { setSlots((prev) => { const panelSlots = prev.filter((s) => s.panel === panel); const maxOrder = panelSlots.length > 0 ? Math.max(...panelSlots.map((s) => s.order)) + 1 : 0; const defaultHeight: number | null = id === "network_graph" ? 350 : id === "host_status" || id === "recent_activity" ? null : 150; return [...prev, { id, panel, order: maxOrder, height: defaultHeight }]; }); }; const handleHeightChange = (id: DashboardCardId, h: number) => setSlots((prev) => prev.map((s) => (s.id === id ? { ...s, height: h } : s)), ); const handleReset = () => { setSlots(DEFAULT_SLOTS); setMainWidthPct(68); setEditMode(false); try { localStorage.removeItem("dashboardTab.slots"); localStorage.removeItem("dashboardTab.mainWidthPct"); } catch { /* ignore */ } }; const columnProps = { hosts, hostMetrics, uptimeFormatted, versionText, versionStatus, dbHealth, credentialCount, activeTunnelCount, activity, onClearActivity: handleClearActivity, onOpenSingletonTab, onOpenTab, cardLabels, isAdmin, }; const isMobile = useIsMobile(); if (isMobile) { const allSlots = [...mainSlots, ...sideSlots]; return (

{t("dashboard.title")}

{todayLabel}

{allSlots.map((slot) => (
{slot.id === "stats_bar" && ( )} {slot.id === "counters_bar" && ( )} {slot.id === "quick_actions" && ( )} {slot.id === "host_status" && ( )} {slot.id === "recent_activity" && ( )} {slot.id === "network_graph" && ( onOpenSingletonTab("network_graph")} /> )}
))}
); } return (

{t("dashboard.title")}

{todayLabel}

{t("dashboardTab.commandPalette")}
Shift + Shift
{editMode ? ( <> ) : ( )}
{editMode && (
{t("dashboardTab.editModeInstructions")}
)}
{(hasSide || editMode) && (editMode ? ( ) : (
))} {(hasSide || editMode) && (
)}
); }