"use client" import { useState, useEffect, useMemo, useCallback } from "react" import { Badge } from "./ui/badge" import { Button } from "./ui/button" import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs" import { SystemOverview } from "./system-overview" import { StorageOverview } from "./storage-overview" import { NetworkMetrics } from "./network-metrics" import { VirtualMachines } from "./virtual-machines" import Hardware from "./hardware" import { SystemLogs } from "./system-logs" import { Settings } from "./settings" import { Security } from "./security" import { OnboardingCarousel } from "./onboarding-carousel" import { HealthStatusModal } from "./health-status-modal" import { ReleaseNotesModal, useVersionCheck } from "./release-notes-modal" import { getApiUrl, fetchApi } from "../lib/api-config" import { TerminalPanel } from "./terminal-panel" import { RefreshCw, AlertTriangle, CheckCircle, XCircle, Server, Menu, LayoutDashboard, HardDrive, NetworkIcon, Box, Cpu, FileText, SettingsIcon, Terminal, ShieldCheck, Info, } from "lucide-react" import Image from "next/image" import { ThemeToggle } from "./theme-toggle" import { Sheet, SheetContent, SheetTrigger } from "./ui/sheet" interface SystemStatus { status: "healthy" | "warning" | "critical" uptime: string lastUpdate: string serverName: string nodeId: string } interface FlaskSystemData { hostname: string node_id: string uptime: string cpu_usage: number memory_usage: number temperature: number load_average: number[] } interface FlaskSystemInfo { hostname: string node_id: string uptime: string health: { status: "healthy" | "warning" | "critical" } } export function ProxmoxDashboard() { const [systemStatus, setSystemStatus] = useState({ status: "healthy", uptime: "Loading...", lastUpdate: new Date().toLocaleTimeString("en-US", { hour12: false }), serverName: "Loading...", nodeId: "Loading...", }) const [isRefreshing, setIsRefreshing] = useState(false) const [isServerConnected, setIsServerConnected] = useState(true) const [componentKey, setComponentKey] = useState(0) const [mobileMenuOpen, setMobileMenuOpen] = useState(false) const [activeTab, setActiveTab] = useState("overview") const [infoCount, setInfoCount] = useState(0) const [updateAvailable, setUpdateAvailable] = useState(false) const [showNavigation, setShowNavigation] = useState(true) const [lastScrollY, setLastScrollY] = useState(0) const [showHealthModal, setShowHealthModal] = useState(false) const { showReleaseNotes, setShowReleaseNotes } = useVersionCheck() // Category keys for health info count calculation const HEALTH_CATEGORY_KEYS = [ { key: "cpu", category: "temperature" }, { key: "memory", category: "memory" }, { key: "storage", category: "storage" }, { key: "disks", category: "disks" }, { key: "network", category: "network" }, { key: "vms", category: "vms" }, { key: "services", category: "pve_services" }, { key: "logs", category: "logs" }, { key: "updates", category: "updates" }, { key: "security", category: "security" }, ] // Fetch ProxMenux update status const fetchUpdateStatus = useCallback(async () => { try { const response = await fetchApi("/api/proxmenux/update-status") if (response?.success && response?.update_available) { const { stable, beta } = response.update_available setUpdateAvailable(stable || beta) } } catch (error) { // Silently fail - updateAvailable will remain false } }, []) // Fetch health info count independently (for initial load and refresh) const fetchHealthInfoCount = useCallback(async () => { try { const response = await fetchApi("/api/health/full") let calculatedInfoCount = 0 if (response && response.health?.details) { // Get categories that have dismissed items (these become INFO) const customCats = new Set((response.custom_suppressions || []).map((cs: { category: string }) => cs.category)) const filteredDismissed = (response.dismissed || []).filter((item: { category: string }) => !customCats.has(item.category)) const categoriesWithDismissed = new Set() filteredDismissed.forEach((item: { category: string }) => { const catMeta = HEALTH_CATEGORY_KEYS.find(c => c.category === item.category || c.key === item.category) if (catMeta) { categoriesWithDismissed.add(catMeta.key) } }) // Count effective INFO categories (original INFO + OK categories with dismissed) HEALTH_CATEGORY_KEYS.forEach(({ key }) => { const cat = response.health.details[key as keyof typeof response.health.details] if (cat) { const originalStatus = cat.status?.toUpperCase() // Count as INFO if: originally INFO OR (originally OK and has dismissed items) if (originalStatus === "INFO" || (originalStatus === "OK" && categoriesWithDismissed.has(key))) { calculatedInfoCount++ } } }) } setInfoCount(calculatedInfoCount) } catch (error) { // Silently fail - infoCount will remain at 0 } }, []) const fetchSystemData = useCallback(async () => { try { const data: FlaskSystemInfo = await fetchApi("/api/system-info") const uptimeValue = data.uptime && typeof data.uptime === "string" && data.uptime.trim() !== "" ? data.uptime : "N/A" const backendStatus = data.health?.status?.toUpperCase() || "OK" let healthStatus: "healthy" | "warning" | "critical" if (backendStatus === "CRITICAL") { healthStatus = "critical" } else if (backendStatus === "WARNING") { healthStatus = "warning" } else { healthStatus = "healthy" } setSystemStatus({ status: healthStatus, uptime: uptimeValue, lastUpdate: new Date().toLocaleTimeString("en-US", { hour12: false }), serverName: data.hostname || "Unknown", nodeId: data.node_id || "Unknown", }) setIsServerConnected(true) } catch (error) { // Expected to fail in v0 preview (no Flask server) setIsServerConnected(false) setSystemStatus((prev) => ({ ...prev, status: "critical", serverName: "Server Offline", nodeId: "Server Offline", uptime: "N/A", lastUpdate: new Date().toLocaleTimeString("en-US", { hour12: false }), })) } }, []) useEffect(() => { // Siempre fetch inicial fetchSystemData() fetchHealthInfoCount() // Fetch info count on initial load fetchUpdateStatus() // Fetch ProxMenux update status on initial load // En overview: cada 30 segundos para actualización frecuente del estado de salud // En otras tabs: cada 60 segundos para reducir carga let interval: ReturnType | null = null let healthInterval: ReturnType | null = null if (activeTab === "overview") { interval = setInterval(fetchSystemData, 30000) // 30 segundos healthInterval = setInterval(fetchHealthInfoCount, 30000) // Also refresh info count } else { interval = setInterval(fetchSystemData, 60000) // 60 segundos healthInterval = setInterval(fetchHealthInfoCount, 60000) // Also refresh info count } return () => { if (interval) clearInterval(interval) if (healthInterval) clearInterval(healthInterval) } }, [fetchSystemData, fetchHealthInfoCount, fetchUpdateStatus, activeTab]) useEffect(() => { const handleChangeTab = (event: CustomEvent) => { const { tab } = event.detail if (tab) { setActiveTab(tab) } } window.addEventListener("changeTab", handleChangeTab as EventListener) return () => { window.removeEventListener("changeTab", handleChangeTab as EventListener) } }, []) // Auto-refresh terminal on mobile devices // This fixes the issue where terminal doesn't connect properly on mobile/VPN useEffect(() => { if (activeTab === "terminal") { const isMobileDevice = window.innerWidth < 768 || ('ontouchstart' in window && navigator.maxTouchPoints > 0) if (isMobileDevice) { // Delay to allow initial connection attempt, then refresh to ensure proper connection const timeoutId = setTimeout(() => { setComponentKey(prev => prev + 1) }, 500) return () => clearTimeout(timeoutId) } } }, [activeTab]) useEffect(() => { const handleHealthStatusUpdate = (event: CustomEvent) => { const { status, infoCount: newInfoCount } = event.detail let healthStatus: "healthy" | "warning" | "critical" if (status === "CRITICAL") { healthStatus = "critical" } else if (status === "WARNING") { healthStatus = "warning" } else { healthStatus = "healthy" } setSystemStatus((prev) => ({ ...prev, status: healthStatus, })) // Update info count (INFO categories + dismissed items) if (typeof newInfoCount === "number") { setInfoCount(newInfoCount) } } window.addEventListener("healthStatusUpdated", handleHealthStatusUpdate as EventListener) return () => { window.removeEventListener("healthStatusUpdated", handleHealthStatusUpdate as EventListener) } }, []) useEffect(() => { if ( systemStatus.serverName && systemStatus.serverName !== "Loading..." && systemStatus.serverName !== "Server Offline" ) { document.title = `${systemStatus.serverName} - ProxMenux Monitor` } else { document.title = "ProxMenux Monitor" } }, [systemStatus.serverName]) useEffect(() => { let hideTimeout: ReturnType | null = null let lastPosition = window.scrollY const handleScroll = () => { const currentScrollY = window.scrollY const delta = currentScrollY - lastPosition if (currentScrollY < 50) { setShowNavigation(true) } else if (delta > 2) { if (hideTimeout) clearTimeout(hideTimeout) hideTimeout = setTimeout(() => setShowNavigation(false), 20) } else if (delta < -2) { if (hideTimeout) clearTimeout(hideTimeout) setShowNavigation(true) } lastPosition = currentScrollY } window.addEventListener("scroll", handleScroll, { passive: true }) return () => { window.removeEventListener("scroll", handleScroll) if (hideTimeout) clearTimeout(hideTimeout) } }, []) const refreshData = async () => { setIsRefreshing(true) await fetchSystemData() setComponentKey((prev) => prev + 1) await new Promise((resolve) => setTimeout(resolve, 500)) setIsRefreshing(false) } const statusIcon = useMemo(() => { switch (systemStatus.status) { case "healthy": return case "warning": return case "critical": return } }, [systemStatus.status]) const statusColor = useMemo(() => { switch (systemStatus.status) { case "healthy": return "bg-green-500/10 text-green-500 border-green-500/20" case "warning": return "bg-yellow-500/10 text-yellow-500 border-yellow-500/20" case "critical": return "bg-red-500/10 text-red-500 border-red-500/20" } }, [systemStatus.status]) const getActiveTabLabel = () => { switch (activeTab) { case "overview": return "Overview" case "storage": return "Storage" case "network": return "Network" case "vms": return "VMs & LXCs" case "hardware": return "Hardware" case "terminal": return "Terminal" case "logs": return "System Logs" case "security": return "Security" case "settings": return "Settings" default: return "Navigation Menu" } } return (
setShowReleaseNotes(false)} /> {!isServerConnected && (
ProxMenux Server Connection Failed

• Check that the monitor.service is running correctly.

• The ProxMenux server should start automatically on port 8008

• Try accessing:{" "} {getApiUrl("/api/health")}

)}
setShowHealthModal(true)} >
{/* Logo and Title */}
{/* Logo and Title */}
ProxMenux Logo { const target = e.target as HTMLImageElement target.style.display = "none" const fallback = target.parentElement?.querySelector(".fallback-icon") if (fallback) { fallback.classList.remove("hidden") } }} />

ProxMenux Monitor

Proxmox System Dashboard

Node: {systemStatus.serverName}
{/* Desktop Actions */}
Node: {systemStatus.serverName}
{statusIcon} {systemStatus.status} {systemStatus.status === "healthy" && infoCount > 0 && ( {infoCount} info )}
Uptime: {systemStatus.uptime || "N/A"}
e.stopPropagation()}>
{/* Mobile Actions */}
{statusIcon} {systemStatus.status === "healthy" && infoCount > 0 && ( {infoCount} )}
e.stopPropagation()} className="-mt-1">
{/* Mobile Server Info */}
Uptime: {systemStatus.uptime || "N/A"}
Overview Storage Network VMs & LXCs Hardware System Logs Terminal Security Settings
) }