Setup Authentication
-
Create a username and password to protect your dashboard
+
Create a username and password to protect your dashboard
{error && (
@@ -179,7 +179,9 @@ export function AuthSetup({ onComplete }: AuthSetupProps) {
-
+
setUsername(e.target.value)}
- className="pl-10"
+ className="pl-10 text-base"
disabled={loading}
+ autoComplete="username"
/>
-
+
setPassword(e.target.value)}
- className="pl-10"
+ className="pl-10 text-base"
disabled={loading}
+ autoComplete="new-password"
/>
-
+
setConfirmPassword(e.target.value)}
- className="pl-10"
+ className="pl-10 text-base"
disabled={loading}
+ autoComplete="new-password"
/>
diff --git a/AppImage/components/hardware.tsx b/AppImage/components/hardware.tsx
index 53e0b2a..15db5a0 100644
--- a/AppImage/components/hardware.tsx
+++ b/AppImage/components/hardware.tsx
@@ -163,14 +163,49 @@ const groupAndSortTemperatures = (temperatures: any[]) => {
}
export default function Hardware() {
+ // Static data - load once without refresh
const {
- data: hardwareData,
- error,
- isLoading,
+ data: staticHardwareData,
+ error: staticError,
+ isLoading: staticLoading,
} = useSWR
("/api/hardware", fetcher, {
- refreshInterval: 5000,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshInterval: 0, // No auto-refresh for static data
})
+ // Dynamic data - refresh every 5 seconds for temperatures, fans, power, ups
+ const {
+ data: dynamicHardwareData,
+ error: dynamicError,
+ isLoading: dynamicLoading,
+ } = useSWR("/api/hardware", fetcher, {
+ refreshInterval: 7000,
+ })
+
+ // Merge static and dynamic data, preferring static for CPU/memory/PCI/disks
+ const hardwareData = staticHardwareData
+ ? {
+ ...dynamicHardwareData,
+ // Keep static data from initial load
+ cpu: staticHardwareData.cpu,
+ motherboard: staticHardwareData.motherboard,
+ memory_modules: staticHardwareData.memory_modules,
+ pci_devices: staticHardwareData.pci_devices,
+ storage_devices: staticHardwareData.storage_devices,
+ gpus: staticHardwareData.gpus,
+ // Use dynamic data for these
+ temperatures: dynamicHardwareData?.temperatures,
+ fans: dynamicHardwareData?.fans,
+ power_meter: dynamicHardwareData?.power_meter,
+ power_supplies: dynamicHardwareData?.power_supplies,
+ ups: dynamicHardwareData?.ups,
+ }
+ : undefined
+
+ const error = staticError || dynamicError
+ const isLoading = staticLoading
+
useEffect(() => {
if (hardwareData?.storage_devices) {
console.log("[v0] Storage devices data from backend:", hardwareData.storage_devices)
diff --git a/AppImage/components/login.tsx b/AppImage/components/login.tsx
index 38f3aac..0c6c4f9 100644
--- a/AppImage/components/login.tsx
+++ b/AppImage/components/login.tsx
@@ -2,11 +2,12 @@
import type React from "react"
-import { useState } from "react"
+import { useState, useEffect } from "react"
import { Button } from "./ui/button"
import { Input } from "./ui/input"
import { Label } from "./ui/label"
-import { Lock, User, AlertCircle, Server } from "lucide-react"
+import { Checkbox } from "./ui/checkbox"
+import { Lock, User, AlertCircle, Server, Shield } from "lucide-react"
import { getApiUrl } from "../lib/api-config"
import Image from "next/image"
@@ -17,9 +18,23 @@ interface LoginProps {
export function Login({ onLogin }: LoginProps) {
const [username, setUsername] = useState("")
const [password, setPassword] = useState("")
+ const [totpCode, setTotpCode] = useState("")
+ const [requiresTotp, setRequiresTotp] = useState(false)
+ const [rememberMe, setRememberMe] = useState(false)
const [error, setError] = useState("")
const [loading, setLoading] = useState(false)
+ useEffect(() => {
+ const savedUsername = localStorage.getItem("proxmenux-saved-username")
+ const savedPassword = localStorage.getItem("proxmenux-saved-password")
+
+ if (savedUsername && savedPassword) {
+ setUsername(savedUsername)
+ setPassword(savedPassword)
+ setRememberMe(true)
+ }
+ }, [])
+
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault()
setError("")
@@ -29,23 +44,46 @@ export function Login({ onLogin }: LoginProps) {
return
}
+ if (requiresTotp && !totpCode) {
+ setError("Please enter your 2FA code")
+ return
+ }
+
setLoading(true)
try {
const response = await fetch(getApiUrl("/api/auth/login"), {
method: "POST",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ username, password }),
+ body: JSON.stringify({
+ username,
+ password,
+ totp_token: totpCode || undefined, // Include 2FA code if provided
+ }),
})
const data = await response.json()
- if (!response.ok) {
- throw new Error(data.error || "Login failed")
+ if (data.requires_totp) {
+ setRequiresTotp(true)
+ setLoading(false)
+ return
+ }
+
+ if (!response.ok) {
+ throw new Error(data.message || "Login failed")
}
- // Save token
localStorage.setItem("proxmenux-auth-token", data.token)
+
+ if (rememberMe) {
+ localStorage.setItem("proxmenux-saved-username", username)
+ localStorage.setItem("proxmenux-saved-password", password)
+ } else {
+ localStorage.removeItem("proxmenux-saved-username")
+ localStorage.removeItem("proxmenux-saved-password")
+ }
+
onLogin()
} catch (err) {
setError(err instanceof Error ? err.message : "Login failed")
@@ -94,42 +132,107 @@ export function Login({ onLogin }: LoginProps) {
)}
-
-
-
-
- setUsername(e.target.value)}
- className="pl-10"
- disabled={loading}
- autoComplete="username"
- />
-
-
+ {!requiresTotp ? (
+ <>
+
+
+
+
+ setUsername(e.target.value)}
+ className="pl-10 text-base"
+ disabled={loading}
+ autoComplete="username"
+ />
+
+
-
-
-
-
-
setPassword(e.target.value)}
- className="pl-10"
- disabled={loading}
- autoComplete="current-password"
- />
+
+
+
+
+ setPassword(e.target.value)}
+ className="pl-10 text-base"
+ disabled={loading}
+ autoComplete="current-password"
+ />
+
+
+
+
+ setRememberMe(checked as boolean)}
+ disabled={loading}
+ />
+
+
+ >
+ ) : (
+
+
+
+
+
Two-Factor Authentication
+
Enter the 6-digit code from your authentication app
+
+
+
+
+
+
setTotpCode(e.target.value.replace(/\D/g, "").slice(0, 6))}
+ className="text-center text-lg tracking-widest font-mono text-base"
+ maxLength={6}
+ disabled={loading}
+ autoComplete="one-time-code"
+ autoFocus
+ />
+
+ You can also use a backup code (format: XXXX-XXXX)
+
+
+
+
-
+ )}
diff --git a/AppImage/components/network-metrics.tsx b/AppImage/components/network-metrics.tsx
index 0d9386a..6b6de14 100644
--- a/AppImage/components/network-metrics.tsx
+++ b/AppImage/components/network-metrics.tsx
@@ -3,7 +3,7 @@
import { useState } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
import { Badge } from "./ui/badge"
-import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./ui/dialog"
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "./ui/dialog"
import { Wifi, Activity, Network, Router, AlertCircle, Zap } from "lucide-react"
import useSWR from "swr"
import { NetworkTrafficChart } from "./network-traffic-chart"
@@ -149,7 +149,7 @@ export function NetworkMetrics() {
error,
isLoading,
} = useSWR
("/api/network", fetcher, {
- refreshInterval: 60000, // Refresh every 60 seconds
+ refreshInterval: 53000,
revalidateOnFocus: false,
revalidateOnReconnect: true,
})
@@ -161,13 +161,13 @@ export function NetworkMetrics() {
const [interfaceTotals, setInterfaceTotals] = useState<{ received: number; sent: number }>({ received: 0, sent: 0 })
const { data: modalNetworkData } = useSWR(selectedInterface ? "/api/network" : null, fetcher, {
- refreshInterval: 15000, // Refresh every 15 seconds when modal is open
+ refreshInterval: 17000,
revalidateOnFocus: false,
revalidateOnReconnect: true,
})
const { data: interfaceHistoricalData } = useSWR(`/api/node/metrics?timeframe=${timeframe}`, fetcher, {
- refreshInterval: 30000,
+ refreshInterval: 29000,
revalidateOnFocus: false,
})
@@ -688,6 +688,9 @@ export function NetworkMetrics() {
{selectedInterface?.name} - Interface Details
+
+ View detailed information and network traffic statistics for this interface
+
{selectedInterface?.status.toLowerCase() === "up" && selectedInterface?.vm_type !== "vm" && (
@@ -362,7 +369,7 @@ export function ProxmoxDashboard() {
>
@@ -539,10 +567,14 @@ export function ProxmoxDashboard() {