mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2025-11-17 19:16:25 +00:00
Merge branch 'MacRimi:main' into main
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
||||
import useSWR from "swr"
|
||||
import { useState, useEffect } from "react"
|
||||
import { type HardwareData, type GPU, type PCIDevice, type StorageDevice, fetcher } from "../types/hardware"
|
||||
import { API_PORT } from "@/lib/api-config"
|
||||
|
||||
const parseLsblkSize = (sizeStr: string | undefined): number => {
|
||||
if (!sizeStr) return 0
|
||||
@@ -247,7 +248,7 @@ export default function Hardware() {
|
||||
|
||||
const apiUrl = isStandardPort
|
||||
? `/api/gpu/${fullSlot}/realtime`
|
||||
: `${protocol}//${hostname}:8008/api/gpu/${fullSlot}/realtime`
|
||||
: `${protocol}//${hostname}:${API_PORT}/api/gpu/${fullSlot}/realtime`
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
method: "GET",
|
||||
|
||||
@@ -92,6 +92,11 @@ export function HealthStatusModal({ open, onOpenChange, getApiUrl }: HealthStatu
|
||||
const data = await response.json()
|
||||
console.log("[v0] Health data received:", data)
|
||||
setHealthData(data)
|
||||
|
||||
const event = new CustomEvent("healthStatusUpdated", {
|
||||
detail: { status: data.overall },
|
||||
})
|
||||
window.dispatchEvent(event)
|
||||
} catch (err) {
|
||||
console.error("[v0] Error fetching health data:", err)
|
||||
setError(err instanceof Error ? err.message : "Unknown error")
|
||||
@@ -275,7 +280,7 @@ export function HealthStatusModal({ open, onOpenChange, getApiUrl }: HealthStatu
|
||||
onClick={() => handleCategoryClick(key, status)}
|
||||
className={`flex items-start gap-3 p-3 rounded-lg border transition-colors ${
|
||||
status === "OK"
|
||||
? "bg-green-500/5 border-green-500/20 hover:bg-green-500/10"
|
||||
? "bg-card border-border hover:bg-muted/30"
|
||||
: status === "WARNING"
|
||||
? "bg-yellow-500/5 border-yellow-500/20 hover:bg-yellow-500/10 cursor-pointer"
|
||||
: status === "CRITICAL"
|
||||
@@ -284,7 +289,7 @@ export function HealthStatusModal({ open, onOpenChange, getApiUrl }: HealthStatu
|
||||
}`}
|
||||
>
|
||||
<div className="mt-0.5 flex-shrink-0 flex items-center gap-2">
|
||||
<Icon className="h-4 w-4 text-muted-foreground" />
|
||||
<Icon className="h-4 w-4 text-blue-500" />
|
||||
{getStatusIcon(status)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
@@ -294,7 +299,7 @@ export function HealthStatusModal({ open, onOpenChange, getApiUrl }: HealthStatu
|
||||
variant="outline"
|
||||
className={`shrink-0 text-xs ${
|
||||
status === "OK"
|
||||
? "border-green-500 text-green-500 bg-green-500/5"
|
||||
? "border-green-500 text-green-500 bg-transparent"
|
||||
: status === "WARNING"
|
||||
? "border-yellow-500 text-yellow-500 bg-yellow-500/5"
|
||||
: status === "CRITICAL"
|
||||
@@ -321,7 +326,7 @@ export function HealthStatusModal({ open, onOpenChange, getApiUrl }: HealthStatu
|
||||
<span className="ml-1 text-muted-foreground">{detailValue.reason}</span>
|
||||
)}
|
||||
</div>
|
||||
{status !== "OK" && (
|
||||
{(status === "WARNING" || status === "CRITICAL") && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { ArrowLeft, Loader2 } from "lucide-react"
|
||||
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts"
|
||||
import { API_PORT } from "@/lib/api-config"
|
||||
|
||||
interface MetricsViewProps {
|
||||
vmid: number
|
||||
@@ -121,7 +122,7 @@ export function MetricsView({ vmid, vmName, vmType, onBack }: MetricsViewProps)
|
||||
const { protocol, hostname, port } = window.location
|
||||
const isStandardPort = port === "" || port === "80" || port === "443"
|
||||
|
||||
const baseUrl = isStandardPort ? "" : `${protocol}//${hostname}:8008`
|
||||
const baseUrl = isStandardPort ? "" : `${protocol}//${hostname}:${API_PORT}`
|
||||
|
||||
const apiUrl = `${baseUrl}/api/vms/${vmid}/metrics?timeframe=${timeframe}`
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState, useEffect } from "react"
|
||||
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts"
|
||||
import { Loader2 } from "lucide-react"
|
||||
import { API_PORT } from "@/lib/api-config"
|
||||
|
||||
interface NetworkMetricsData {
|
||||
time: string
|
||||
@@ -78,7 +79,7 @@ export function NetworkTrafficChart({
|
||||
const { protocol, hostname, port } = window.location
|
||||
const isStandardPort = port === "" || port === "80" || port === "443"
|
||||
|
||||
const baseUrl = isStandardPort ? "" : `${protocol}//${hostname}:8008`
|
||||
const baseUrl = isStandardPort ? "" : `${protocol}//${hostname}:${API_PORT}`
|
||||
|
||||
const apiUrl = interfaceName
|
||||
? `${baseUrl}/api/network/${interfaceName}/metrics?timeframe=${timeframe}`
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from ".
|
||||
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts"
|
||||
import { Loader2, TrendingUp, MemoryStick } from "lucide-react"
|
||||
import { useIsMobile } from "../hooks/use-mobile"
|
||||
import { API_PORT } from "@/lib/api-config"
|
||||
|
||||
const TIMEFRAME_OPTIONS = [
|
||||
{ value: "hour", label: "1 Hour" },
|
||||
@@ -91,7 +92,7 @@ export function NodeMetricsCharts() {
|
||||
const { protocol, hostname, port } = window.location
|
||||
const isStandardPort = port === "" || port === "80" || port === "443"
|
||||
|
||||
const baseUrl = isStandardPort ? "" : `${protocol}//${hostname}:8008`
|
||||
const baseUrl = isStandardPort ? "" : `${protocol}//${hostname}:${API_PORT}`
|
||||
|
||||
const apiUrl = `${baseUrl}/api/node/metrics?timeframe=${timeframe}`
|
||||
|
||||
|
||||
@@ -17,10 +17,6 @@ import {
|
||||
Cpu,
|
||||
FileText,
|
||||
Rocket,
|
||||
Zap,
|
||||
Shield,
|
||||
Link2,
|
||||
Gauge,
|
||||
} from "lucide-react"
|
||||
import Image from "next/image"
|
||||
import { Checkbox } from "./ui/checkbox"
|
||||
@@ -32,7 +28,6 @@ interface OnboardingSlide {
|
||||
image?: string
|
||||
icon: React.ReactNode
|
||||
gradient: string
|
||||
features?: { icon: React.ReactNode; text: string }[]
|
||||
}
|
||||
|
||||
const slides: OnboardingSlide[] = [
|
||||
@@ -46,35 +41,6 @@ const slides: OnboardingSlide[] = [
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: "What's New in This Version",
|
||||
description: "We've added exciting new features and improvements to make ProxMenux Monitor even better!",
|
||||
icon: <Zap className="h-16 w-16" />,
|
||||
gradient: "from-amber-500 via-orange-500 to-red-500",
|
||||
features: [
|
||||
{
|
||||
icon: <Link2 className="h-5 w-5" />,
|
||||
text: "Proxy Support - Access ProxMenux through reverse proxies with full functionality",
|
||||
},
|
||||
{
|
||||
icon: <Shield className="h-5 w-5" />,
|
||||
text: "Authentication System - Secure your dashboard with password protection",
|
||||
},
|
||||
{
|
||||
icon: <Gauge className="h-5 w-5" />,
|
||||
text: "PCIe Link Speed Detection - View NVMe drive connection speeds and detect performance issues",
|
||||
},
|
||||
{
|
||||
icon: <HardDrive className="h-5 w-5" />,
|
||||
text: "Enhanced Storage Display - Better formatting for disk sizes (auto-converts GB to TB when needed)",
|
||||
},
|
||||
{
|
||||
icon: <Network className="h-5 w-5" />,
|
||||
text: "SATA/SAS Information - View detailed interface information for all storage devices",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "System Overview",
|
||||
description:
|
||||
"Monitor your server's status in real-time: CPU, memory, temperature, system load and more. Everything in an intuitive and easy-to-understand dashboard.",
|
||||
@@ -83,7 +49,7 @@ const slides: OnboardingSlide[] = [
|
||||
gradient: "from-blue-500 to-cyan-500",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
id: 2,
|
||||
title: "Storage Management",
|
||||
description:
|
||||
"Visualize the status of all your disks and volumes. Detailed information on capacity, usage, SMART health, temperature and performance of each storage device.",
|
||||
@@ -92,7 +58,7 @@ const slides: OnboardingSlide[] = [
|
||||
gradient: "from-cyan-500 to-teal-500",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
id: 3,
|
||||
title: "Network Metrics",
|
||||
description:
|
||||
"Monitor network traffic in real-time. Bandwidth statistics, active interfaces, transfer speeds and historical usage graphs.",
|
||||
@@ -101,7 +67,7 @@ const slides: OnboardingSlide[] = [
|
||||
gradient: "from-teal-500 to-green-500",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
id: 4,
|
||||
title: "Virtual Machines & Containers",
|
||||
description:
|
||||
"Manage all your VMs and LXC containers from one place. Status, allocated resources, current usage and quick controls for each virtual machine.",
|
||||
@@ -110,7 +76,7 @@ const slides: OnboardingSlide[] = [
|
||||
gradient: "from-green-500 to-emerald-500",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
id: 5,
|
||||
title: "Hardware Information",
|
||||
description:
|
||||
"Complete details of your server hardware: CPU, RAM, GPU, disks, network, UPS and more. Technical specifications, models, serial numbers and status of each component.",
|
||||
@@ -119,7 +85,7 @@ const slides: OnboardingSlide[] = [
|
||||
gradient: "from-emerald-500 to-blue-500",
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
id: 6,
|
||||
title: "System Logs",
|
||||
description:
|
||||
"Access system logs in real-time. Filter by event type, search for specific errors and keep complete track of your server activity. Download the displayed logs for further analysis.",
|
||||
@@ -128,7 +94,7 @@ const slides: OnboardingSlide[] = [
|
||||
gradient: "from-blue-500 to-indigo-500",
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
id: 7,
|
||||
title: "Ready for the Future!",
|
||||
description:
|
||||
"ProxMenux Monitor is prepared to receive updates and improvements that will be added gradually, improving the user experience and being able to execute ProxMenux functions from the web panel.",
|
||||
@@ -194,7 +160,6 @@ export function OnboardingCarousel() {
|
||||
<Dialog open={open} onOpenChange={handleClose}>
|
||||
<DialogContent className="max-w-4xl p-0 gap-0 overflow-hidden border-0 bg-transparent">
|
||||
<div className="relative bg-card rounded-lg overflow-hidden shadow-2xl">
|
||||
{/* Close button */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -210,7 +175,6 @@ export function OnboardingCarousel() {
|
||||
<div className="absolute inset-0 bg-black/10" />
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_120%,rgba(255,255,255,0.1),transparent)]" />
|
||||
|
||||
{/* Icon or Image */}
|
||||
<div className="relative z-10 text-white">
|
||||
{slide.image ? (
|
||||
<div className="relative w-full h-36 md:h-48 flex items-center justify-center px-4">
|
||||
@@ -236,7 +200,6 @@ export function OnboardingCarousel() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Decorative elements */}
|
||||
<div className="absolute top-10 left-10 w-20 h-20 bg-white/10 rounded-full blur-2xl" />
|
||||
<div className="absolute bottom-10 right-10 w-32 h-32 bg-white/10 rounded-full blur-3xl" />
|
||||
</div>
|
||||
@@ -249,21 +212,6 @@ export function OnboardingCarousel() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{slide.features && (
|
||||
<div className="space-y-2 md:space-y-3 py-2">
|
||||
{slide.features.map((feature, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-start gap-2 md:gap-3 p-2 md:p-3 rounded-lg bg-muted/50 border border-border/50"
|
||||
>
|
||||
<div className="text-blue-500 mt-0.5 flex-shrink-0">{feature.icon}</div>
|
||||
<p className="text-xs md:text-sm text-foreground leading-relaxed">{feature.text}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Progress dots */}
|
||||
<div className="flex items-center justify-center gap-2 py-2 md:py-4">
|
||||
{slides.map((_, index) => (
|
||||
<button
|
||||
|
||||
@@ -13,6 +13,7 @@ import { SystemLogs } from "./system-logs"
|
||||
import { Settings } from "./settings"
|
||||
import { OnboardingCarousel } from "./onboarding-carousel"
|
||||
import { HealthStatusModal } from "./health-status-modal"
|
||||
import { ReleaseNotesModal, useVersionCheck } from "./release-notes-modal"
|
||||
import { getApiUrl } from "../lib/api-config"
|
||||
import {
|
||||
RefreshCw,
|
||||
@@ -76,6 +77,7 @@ export function ProxmoxDashboard() {
|
||||
const [showNavigation, setShowNavigation] = useState(true)
|
||||
const [lastScrollY, setLastScrollY] = useState(0)
|
||||
const [showHealthModal, setShowHealthModal] = useState(false)
|
||||
const { showReleaseNotes, setShowReleaseNotes } = useVersionCheck()
|
||||
|
||||
const fetchSystemData = useCallback(async () => {
|
||||
const apiUrl = getApiUrl("/api/system-info")
|
||||
@@ -164,6 +166,31 @@ export function ProxmoxDashboard() {
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const handleHealthStatusUpdate = (event: CustomEvent) => {
|
||||
const { status } = 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,
|
||||
}))
|
||||
}
|
||||
|
||||
window.addEventListener("healthStatusUpdated", handleHealthStatusUpdate as EventListener)
|
||||
return () => {
|
||||
window.removeEventListener("healthStatusUpdated", handleHealthStatusUpdate as EventListener)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
systemStatus.serverName &&
|
||||
@@ -258,6 +285,7 @@ export function ProxmoxDashboard() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<OnboardingCarousel />
|
||||
<ReleaseNotesModal open={showReleaseNotes} onClose={() => setShowReleaseNotes(false)} />
|
||||
|
||||
{!isServerConnected && (
|
||||
<div className="bg-red-500/10 border-b border-red-500/20 px-6 py-3">
|
||||
|
||||
203
AppImage/components/release-notes-modal.tsx
Normal file
203
AppImage/components/release-notes-modal.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Button } from "./ui/button"
|
||||
import { Dialog, DialogContent } from "./ui/dialog"
|
||||
import { X, Sparkles, Link2, Shield, Zap, HardDrive, Gauge, Wrench, Settings } from "lucide-react"
|
||||
import { Checkbox } from "./ui/checkbox"
|
||||
|
||||
const APP_VERSION = "1.0.1" // Sync with AppImage/package.json
|
||||
|
||||
interface ReleaseNote {
|
||||
date: string
|
||||
changes: {
|
||||
added?: string[]
|
||||
changed?: string[]
|
||||
fixed?: string[]
|
||||
}
|
||||
}
|
||||
|
||||
export const CHANGELOG: Record<string, ReleaseNote> = {
|
||||
"1.0.1": {
|
||||
date: "November 11, 2025",
|
||||
changes: {
|
||||
added: [
|
||||
"Proxy Support - Access ProxMenux through reverse proxies with full functionality",
|
||||
"Authentication System - Secure your dashboard with password protection",
|
||||
"PCIe Link Speed Detection - View NVMe drive connection speeds and detect performance issues",
|
||||
"Enhanced Storage Display - Better formatting for disk sizes (auto-converts GB to TB when needed)",
|
||||
"SATA/SAS Information - View detailed interface information for all storage devices",
|
||||
"Two-Factor Authentication (2FA) - Enhanced security with TOTP support",
|
||||
"Health Monitoring System - Comprehensive system health checks with dismissible warnings",
|
||||
"Release Notes Modal - Automatic notification of new features and improvements",
|
||||
],
|
||||
changed: [
|
||||
"Optimized VM & LXC page - Reduced CPU usage by 85% through intelligent caching",
|
||||
"Storage metrics now separate local and remote storage for clarity",
|
||||
"Update warnings now appear only after 365 days instead of 30 days",
|
||||
"API intervals staggered to distribute server load (23s and 37s)",
|
||||
],
|
||||
fixed: [
|
||||
"Fixed dark mode text contrast issues in various components",
|
||||
"Corrected storage calculation discrepancies between Overview and Storage pages",
|
||||
"Resolved JSON stringify error in VM control actions",
|
||||
"Improved IP address fetching for LXC containers",
|
||||
],
|
||||
},
|
||||
},
|
||||
"1.0.0": {
|
||||
date: "October 15, 2025",
|
||||
changes: {
|
||||
added: [
|
||||
"Initial release of ProxMenux Monitor",
|
||||
"Real-time system monitoring dashboard",
|
||||
"Storage management with SMART health monitoring",
|
||||
"Network metrics and bandwidth tracking",
|
||||
"VM & LXC container management",
|
||||
"Hardware information display",
|
||||
"System logs viewer with filtering",
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const CURRENT_VERSION_FEATURES = [
|
||||
{
|
||||
icon: <Link2 className="h-5 w-5" />,
|
||||
text: "Proxy Support - Access ProxMenux through reverse proxies with full functionality",
|
||||
},
|
||||
{
|
||||
icon: <Shield className="h-5 w-5" />,
|
||||
text: "Two-Factor Authentication (2FA) - Enhanced security with TOTP support for login protection",
|
||||
},
|
||||
{
|
||||
icon: <Zap className="h-5 w-5" />,
|
||||
text: "Performance Improvements - Optimized loading times and reduced CPU usage across the application",
|
||||
},
|
||||
{
|
||||
icon: <HardDrive className="h-5 w-5" />,
|
||||
text: "Storage Enhancements - Improved disk space consumption display with local and remote storage separation",
|
||||
},
|
||||
{
|
||||
icon: <Gauge className="h-5 w-5" />,
|
||||
text: "PCIe Link Speed Detection - View NVMe drive connection speeds and identify performance bottlenecks",
|
||||
},
|
||||
{
|
||||
icon: <Wrench className="h-5 w-5" />,
|
||||
text: "Hardware Page Improvements - Enhanced hardware information display with detailed PCIe and interface data",
|
||||
},
|
||||
{
|
||||
icon: <Settings className="h-5 w-5" />,
|
||||
text: "New Settings Page - Centralized configuration for authentication, optimizations, and system preferences",
|
||||
},
|
||||
]
|
||||
|
||||
interface ReleaseNotesModalProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export function ReleaseNotesModal({ open, onClose }: ReleaseNotesModalProps) {
|
||||
const [dontShowAgain, setDontShowAgain] = useState(false)
|
||||
|
||||
const handleClose = () => {
|
||||
if (dontShowAgain) {
|
||||
localStorage.setItem("proxmenux-last-seen-version", APP_VERSION)
|
||||
}
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleClose}>
|
||||
<DialogContent className="max-w-2xl max-h-[85vh] p-0 gap-0 border-0 bg-transparent">
|
||||
<div className="relative bg-card rounded-lg shadow-2xl h-full flex flex-col max-h-[85vh]">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute top-4 right-4 z-50 h-8 w-8 rounded-full bg-background/80 backdrop-blur-sm hover:bg-background"
|
||||
onClick={handleClose}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<div className="relative h-32 md:h-40 bg-gradient-to-br from-amber-500 via-orange-500 to-red-500 flex items-center justify-center overflow-hidden flex-shrink-0">
|
||||
<div className="absolute inset-0 bg-black/10" />
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_120%,rgba(255,255,255,0.1),transparent)]" />
|
||||
|
||||
<div className="relative z-10 text-white animate-pulse">
|
||||
<Sparkles className="h-12 w-12 md:h-14 md:w-14" />
|
||||
</div>
|
||||
|
||||
<div className="absolute top-10 left-10 w-20 h-20 bg-white/10 rounded-full blur-2xl" />
|
||||
<div className="absolute bottom-10 right-10 w-32 h-32 bg-white/10 rounded-full blur-3xl" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-6 md:p-8 space-y-4 md:space-y-6 min-h-0">
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-xl md:text-2xl font-bold text-foreground text-balance">
|
||||
What's New in Version {APP_VERSION}
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
We've added exciting new features and improvements to make ProxMenux Monitor even better!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{CURRENT_VERSION_FEATURES.map((feature, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-start gap-2 md:gap-3 p-3 rounded-lg bg-muted/50 border border-border/50 hover:bg-muted/70 transition-colors"
|
||||
>
|
||||
<div className="text-orange-500 mt-0.5 flex-shrink-0">{feature.icon}</div>
|
||||
<p className="text-xs md:text-sm text-foreground leading-relaxed">{feature.text}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-shrink-0 p-6 md:p-8 pt-4 border-t border-border/50 bg-card">
|
||||
<div className="flex flex-col gap-3">
|
||||
<Button
|
||||
onClick={handleClose}
|
||||
className="w-full bg-gradient-to-r from-amber-500 to-orange-500 hover:from-amber-600 hover:to-orange-600"
|
||||
>
|
||||
<Sparkles className="h-4 w-4 mr-2" />
|
||||
Got it!
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<Checkbox
|
||||
id="dont-show-version-again"
|
||||
checked={dontShowAgain}
|
||||
onCheckedChange={(checked) => setDontShowAgain(checked as boolean)}
|
||||
/>
|
||||
<label
|
||||
htmlFor="dont-show-version-again"
|
||||
className="text-xs md:text-sm text-muted-foreground hover:text-foreground transition-colors cursor-pointer select-none"
|
||||
>
|
||||
Don't show again for this version
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export function useVersionCheck() {
|
||||
const [showReleaseNotes, setShowReleaseNotes] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const lastSeenVersion = localStorage.getItem("proxmenux-last-seen-version")
|
||||
|
||||
if (lastSeenVersion !== APP_VERSION) {
|
||||
setShowReleaseNotes(true)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return { showReleaseNotes, setShowReleaseNotes }
|
||||
}
|
||||
|
||||
export { APP_VERSION }
|
||||
@@ -6,6 +6,7 @@ import { Input } from "./ui/input"
|
||||
import { Label } from "./ui/label"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card"
|
||||
import { Shield, Lock, User, AlertCircle, CheckCircle, Info, LogOut, Wrench, Package } from "lucide-react"
|
||||
import { APP_VERSION } from "./release-notes-modal"
|
||||
import { getApiUrl } from "../lib/api-config"
|
||||
import { TwoFactorSetup } from "./two-factor-setup"
|
||||
|
||||
@@ -40,6 +41,9 @@ export function Settings() {
|
||||
|
||||
const [proxmenuxTools, setProxmenuxTools] = useState<ProxMenuxTool[]>([])
|
||||
const [loadingTools, setLoadingTools] = useState(true)
|
||||
const [expandedVersions, setExpandedVersions] = useState<Record<string, boolean>>({
|
||||
[APP_VERSION]: true, // Current version expanded by default
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
checkAuthStatus()
|
||||
@@ -274,6 +278,13 @@ export function Settings() {
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
const toggleVersion = (version: string) => {
|
||||
setExpandedVersions((prev) => ({
|
||||
...prev,
|
||||
[version]: !prev[version],
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { HardDrive, Database, AlertTriangle, CheckCircle2, XCircle, Square, Thermometer } from "lucide-react"
|
||||
import { HardDrive, Database, AlertTriangle, CheckCircle2, XCircle, Square, Thermometer, Archive } from "lucide-react"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
@@ -394,10 +394,20 @@ export function StorageOverview() {
|
||||
return "[&>div]:bg-red-500"
|
||||
}
|
||||
|
||||
const getUsageColor = (percent: number): string => {
|
||||
if (percent < 70) return "text-blue-500"
|
||||
if (percent < 85) return "text-yellow-500"
|
||||
if (percent < 95) return "text-orange-500"
|
||||
return "text-red-500"
|
||||
}
|
||||
|
||||
const diskHealthBreakdown = getDiskHealthBreakdown()
|
||||
const diskTypesBreakdown = getDiskTypesBreakdown()
|
||||
|
||||
const totalProxmoxUsed =
|
||||
const localStorageTypes = ["dir", "lvmthin", "lvm", "zfspool", "btrfs"]
|
||||
const remoteStorageTypes = ["pbs", "nfs", "cifs", "smb", "glusterfs", "iscsi", "iscsidirect", "rbd", "cephfs"]
|
||||
|
||||
const totalLocalUsed =
|
||||
proxmoxStorage?.storage
|
||||
.filter(
|
||||
(storage) =>
|
||||
@@ -406,11 +416,12 @@ export function StorageOverview() {
|
||||
storage.status === "active" &&
|
||||
storage.total > 0 &&
|
||||
storage.used >= 0 &&
|
||||
storage.available >= 0,
|
||||
storage.available >= 0 &&
|
||||
localStorageTypes.includes(storage.type.toLowerCase()),
|
||||
)
|
||||
.reduce((sum, storage) => sum + storage.used, 0) || 0
|
||||
|
||||
const totalProxmoxCapacity =
|
||||
const totalLocalCapacity =
|
||||
proxmoxStorage?.storage
|
||||
.filter(
|
||||
(storage) =>
|
||||
@@ -419,11 +430,52 @@ export function StorageOverview() {
|
||||
storage.status === "active" &&
|
||||
storage.total > 0 &&
|
||||
storage.used >= 0 &&
|
||||
storage.available >= 0,
|
||||
storage.available >= 0 &&
|
||||
localStorageTypes.includes(storage.type.toLowerCase()),
|
||||
)
|
||||
.reduce((sum, storage) => sum + storage.total, 0) || 0
|
||||
|
||||
const usagePercent = totalProxmoxCapacity > 0 ? ((totalProxmoxUsed / totalProxmoxCapacity) * 100).toFixed(2) : "0.00"
|
||||
const localUsagePercent = totalLocalCapacity > 0 ? ((totalLocalUsed / totalLocalCapacity) * 100).toFixed(2) : "0.00"
|
||||
|
||||
const totalRemoteUsed =
|
||||
proxmoxStorage?.storage
|
||||
.filter(
|
||||
(storage) =>
|
||||
storage &&
|
||||
storage.name &&
|
||||
storage.status === "active" &&
|
||||
storage.total > 0 &&
|
||||
storage.used >= 0 &&
|
||||
storage.available >= 0 &&
|
||||
remoteStorageTypes.includes(storage.type.toLowerCase()),
|
||||
)
|
||||
.reduce((sum, storage) => sum + storage.used, 0) || 0
|
||||
|
||||
const totalRemoteCapacity =
|
||||
proxmoxStorage?.storage
|
||||
.filter(
|
||||
(storage) =>
|
||||
storage &&
|
||||
storage.name &&
|
||||
storage.status === "active" &&
|
||||
storage.total > 0 &&
|
||||
storage.used >= 0 &&
|
||||
storage.available >= 0 &&
|
||||
remoteStorageTypes.includes(storage.type.toLowerCase()),
|
||||
)
|
||||
.reduce((sum, storage) => sum + storage.total, 0) || 0
|
||||
|
||||
const remoteUsagePercent =
|
||||
totalRemoteCapacity > 0 ? ((totalRemoteUsed / totalRemoteCapacity) * 100).toFixed(2) : "0.00"
|
||||
|
||||
const remoteStorageCount =
|
||||
proxmoxStorage?.storage.filter(
|
||||
(storage) =>
|
||||
storage &&
|
||||
storage.name &&
|
||||
storage.status === "active" &&
|
||||
remoteStorageTypes.includes(storage.type.toLowerCase()),
|
||||
).length || 0
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -458,64 +510,81 @@ export function StorageOverview() {
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Used Storage</CardTitle>
|
||||
<CardTitle className="text-sm font-medium">Local Used</CardTitle>
|
||||
<Database className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-xl lg:text-2xl font-bold">{formatStorage(totalProxmoxUsed)}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">{usagePercent}% used</p>
|
||||
<div className="text-xl lg:text-2xl font-bold">{formatStorage(totalLocalUsed)}</div>
|
||||
<p className="text-xs mt-1">
|
||||
<span className={getUsageColor(Number.parseFloat(localUsagePercent))}>{localUsagePercent}%</span>
|
||||
<span className="text-muted-foreground"> of </span>
|
||||
<span className="text-green-500">{formatStorage(totalLocalCapacity)}</span>
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Disk Health */}
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Disk Health</CardTitle>
|
||||
<CheckCircle2 className="h-4 w-4 text-muted-foreground" />
|
||||
<CardTitle className="text-sm font-medium">Remote Used</CardTitle>
|
||||
<Archive className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-xl lg:text-2xl font-bold">{storageData.disk_count} disks</div>
|
||||
<div className="text-xl lg:text-2xl font-bold">
|
||||
{remoteStorageCount > 0 ? formatStorage(totalRemoteUsed) : "None"}
|
||||
</div>
|
||||
<p className="text-xs mt-1">
|
||||
<span className="text-green-500">{diskHealthBreakdown.normal} normal</span>
|
||||
{diskHealthBreakdown.warning > 0 && (
|
||||
{remoteStorageCount > 0 ? (
|
||||
<>
|
||||
{", "}
|
||||
<span className="text-yellow-500">{diskHealthBreakdown.warning} warning</span>
|
||||
</>
|
||||
)}
|
||||
{diskHealthBreakdown.critical > 0 && (
|
||||
<>
|
||||
{", "}
|
||||
<span className="text-red-500">{diskHealthBreakdown.critical} critical</span>
|
||||
<span className={getUsageColor(Number.parseFloat(remoteUsagePercent))}>{remoteUsagePercent}%</span>
|
||||
<span className="text-muted-foreground"> of </span>
|
||||
<span className="text-green-500">{formatStorage(totalRemoteCapacity)}</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-muted-foreground">No remote storage</span>
|
||||
)}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Disk Types */}
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Disk Types</CardTitle>
|
||||
<CardTitle className="text-sm font-medium">Physical Disks</CardTitle>
|
||||
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-xl lg:text-2xl font-bold">{storageData.disk_count} disks</div>
|
||||
<p className="text-xs mt-1">
|
||||
{diskTypesBreakdown.nvme > 0 && <span className="text-purple-500">{diskTypesBreakdown.nvme} NVMe</span>}
|
||||
{diskTypesBreakdown.ssd > 0 && (
|
||||
<>
|
||||
{diskTypesBreakdown.nvme > 0 && ", "}
|
||||
<span className="text-cyan-500">{diskTypesBreakdown.ssd} SSD</span>
|
||||
</>
|
||||
)}
|
||||
{diskTypesBreakdown.hdd > 0 && (
|
||||
<>
|
||||
{(diskTypesBreakdown.nvme > 0 || diskTypesBreakdown.ssd > 0) && ", "}
|
||||
<span className="text-blue-500">{diskTypesBreakdown.hdd} HDD</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
<div className="space-y-1 mt-1">
|
||||
<p className="text-xs">
|
||||
{diskTypesBreakdown.nvme > 0 && <span className="text-purple-500">{diskTypesBreakdown.nvme} NVMe</span>}
|
||||
{diskTypesBreakdown.ssd > 0 && (
|
||||
<>
|
||||
{diskTypesBreakdown.nvme > 0 && ", "}
|
||||
<span className="text-cyan-500">{diskTypesBreakdown.ssd} SSD</span>
|
||||
</>
|
||||
)}
|
||||
{diskTypesBreakdown.hdd > 0 && (
|
||||
<>
|
||||
{(diskTypesBreakdown.nvme > 0 || diskTypesBreakdown.ssd > 0) && ", "}
|
||||
<span className="text-blue-500">{diskTypesBreakdown.hdd} HDD</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs">
|
||||
<span className="text-green-500">{diskHealthBreakdown.normal} normal</span>
|
||||
{diskHealthBreakdown.warning > 0 && (
|
||||
<>
|
||||
{", "}
|
||||
<span className="text-yellow-500">{diskHealthBreakdown.warning} warning</span>
|
||||
</>
|
||||
)}
|
||||
{diskHealthBreakdown.critical > 0 && (
|
||||
<>
|
||||
{", "}
|
||||
<span className="text-red-500">{diskHealthBreakdown.critical} critical</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -533,11 +602,7 @@ export function StorageOverview() {
|
||||
{proxmoxStorage.storage
|
||||
.filter(
|
||||
(storage) =>
|
||||
storage &&
|
||||
storage.name &&
|
||||
storage.total > 0 &&
|
||||
storage.used >= 0 && // Ensure used is not negative
|
||||
storage.available >= 0, // Ensure available is not negative
|
||||
storage && storage.name && storage.total > 0 && storage.used >= 0 && storage.available >= 0,
|
||||
)
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((storage) => (
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
Terminal,
|
||||
} from "lucide-react"
|
||||
import { useState, useEffect, useMemo } from "react"
|
||||
import { API_PORT } from "@/lib/api-config"
|
||||
|
||||
interface Log {
|
||||
timestamp: string
|
||||
@@ -131,10 +132,10 @@ export function SystemLogs() {
|
||||
if (isStandardPort) {
|
||||
return endpoint
|
||||
} else {
|
||||
return `${protocol}//${hostname}:8008${endpoint}`
|
||||
return `${protocol}//${hostname}:${API_PORT}${endpoint}`
|
||||
}
|
||||
}
|
||||
return `http://localhost:8008${endpoint}`
|
||||
return `${protocol}//${hostname}:${API_PORT}${endpoint}`
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
|
||||
import { Badge } from "./ui/badge"
|
||||
import { Progress } from "./ui/progress"
|
||||
import { Button } from "./ui/button"
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "./ui/dialog"
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./ui/dialog"
|
||||
import {
|
||||
Server,
|
||||
Play,
|
||||
@@ -124,7 +124,6 @@ interface VMDetails extends VMData {
|
||||
gpu_passthrough?: string[]
|
||||
devices?: string[]
|
||||
}
|
||||
lxc_ip?: string
|
||||
lxc_ip_info?: {
|
||||
all_ips: string[]
|
||||
real_ips: string[]
|
||||
@@ -139,7 +138,7 @@ const fetcher = async (url: string) => {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
signal: AbortSignal.timeout(30000),
|
||||
signal: AbortSignal.timeout(60000),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -283,15 +282,22 @@ export function VirtualMachines() {
|
||||
const [editedNotes, setEditedNotes] = useState("")
|
||||
const [savingNotes, setSavingNotes] = useState(false)
|
||||
const [selectedMetric, setSelectedMetric] = useState<string | null>(null)
|
||||
const [ipsLoaded, setIpsLoaded] = useState(false)
|
||||
const [loadingIPs, setLoadingIPs] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchLXCIPs = async () => {
|
||||
if (!vmData) return
|
||||
// Only fetch if data exists, not already loaded, and not currently loading
|
||||
if (!vmData || ipsLoaded || loadingIPs) return
|
||||
|
||||
const lxcs = vmData.filter((vm) => vm.type === "lxc")
|
||||
|
||||
if (lxcs.length === 0) return
|
||||
if (lxcs.length === 0) {
|
||||
setIpsLoaded(true)
|
||||
return
|
||||
}
|
||||
|
||||
setLoadingIPs(true)
|
||||
const configs: Record<number, string> = {}
|
||||
|
||||
const batchSize = 5
|
||||
@@ -320,16 +326,20 @@ export function VirtualMachines() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`[v0] Could not fetch IP for LXC ${lxc.vmid}`)
|
||||
configs[lxc.vmid] = "N/A"
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
setVmConfigs((prev) => ({ ...prev, ...configs }))
|
||||
}
|
||||
|
||||
setLoadingIPs(false)
|
||||
setIpsLoaded(true)
|
||||
}
|
||||
|
||||
fetchLXCIPs()
|
||||
}, [vmData])
|
||||
}, [vmData, ipsLoaded, loadingIPs])
|
||||
|
||||
const handleVMClick = async (vm: VMData) => {
|
||||
setSelectedVM(vm)
|
||||
@@ -469,7 +479,7 @@ export function VirtualMachines() {
|
||||
"/api/system",
|
||||
fetcher,
|
||||
{
|
||||
refreshInterval: 23000,
|
||||
refreshInterval: 37000,
|
||||
revalidateOnFocus: false,
|
||||
},
|
||||
)
|
||||
@@ -1068,6 +1078,7 @@ export function VirtualMachines() {
|
||||
<>
|
||||
<DialogHeader className="pb-4 border-b border-border px-6 pt-6">
|
||||
<DialogTitle className="flex flex-col gap-3">
|
||||
{/* Desktop layout: Uptime now appears after status badge */}
|
||||
<div className="hidden sm:flex items-center gap-3 flex-wrap">
|
||||
<div className="flex items-center gap-2">
|
||||
<Server className="h-5 w-5 flex-shrink-0" />
|
||||
@@ -1084,15 +1095,16 @@ export function VirtualMachines() {
|
||||
<Badge variant="outline" className={`${getStatusColor(selectedVM.status)} flex-shrink-0`}>
|
||||
{selectedVM.status.toUpperCase()}
|
||||
</Badge>
|
||||
{selectedVM.status === "running" && (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Uptime: {formatUptime(selectedVM.uptime)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{selectedVM.status === "running" && (
|
||||
<span className="text-sm text-muted-foreground ml-auto">
|
||||
Uptime: {formatUptime(selectedVM.uptime)}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{/* Mobile layout unchanged */}
|
||||
<div className="sm:hidden flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Server className="h-5 w-5 flex-shrink-0" />
|
||||
@@ -1117,9 +1129,6 @@ export function VirtualMachines() {
|
||||
)}
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
View and manage configuration, resources, and status for this virtual machine
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 overflow-y-auto px-6 py-4">
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
* Handles API URL generation with automatic proxy detection
|
||||
*/
|
||||
|
||||
/**
|
||||
* API Server Port Configuration
|
||||
* Default: 8008 (production)
|
||||
* Can be changed to 8009 for beta testing
|
||||
* This can also be set via NEXT_PUBLIC_API_PORT environment variable
|
||||
*/
|
||||
export const API_PORT = process.env.NEXT_PUBLIC_API_PORT || "8008"
|
||||
|
||||
/**
|
||||
* Gets the base URL for API calls
|
||||
* Automatically detects if running behind a proxy by checking if we're on a standard port
|
||||
@@ -30,8 +38,8 @@ export function getApiBaseUrl(): string {
|
||||
console.log("[v0] getApiBaseUrl: Detected proxy access, using relative URLs")
|
||||
return ""
|
||||
} else {
|
||||
// Direct access - use explicit port 8008
|
||||
const baseUrl = `${protocol}//${hostname}:8008`
|
||||
// Direct access - use explicit API port
|
||||
const baseUrl = `${protocol}//${hostname}:${API_PORT}`
|
||||
console.log("[v0] getApiBaseUrl: Direct access detected, using:", baseUrl)
|
||||
return baseUrl
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "proxmenux-monitor",
|
||||
"name": "ProxMenux-Monitor",
|
||||
"version": "1.0.1",
|
||||
"description": "Proxmox System Monitoring Dashboard",
|
||||
"private": true,
|
||||
|
||||
@@ -64,8 +64,8 @@ class HealthMonitor:
|
||||
LOG_CHECK_INTERVAL = 300
|
||||
|
||||
# Updates Thresholds
|
||||
UPDATES_WARNING = 10
|
||||
UPDATES_CRITICAL = 30
|
||||
UPDATES_WARNING = 365 # Only warn after 1 year without updates
|
||||
UPDATES_CRITICAL = 730 # Critical after 2 years
|
||||
|
||||
# Known benign errors from Proxmox that should not trigger alerts
|
||||
BENIGN_ERROR_PATTERNS = [
|
||||
@@ -1376,7 +1376,8 @@ class HealthMonitor:
|
||||
def _check_updates(self) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Check for pending system updates with intelligence.
|
||||
Only warns for: critical security updates, kernel updates, or updates pending >30 days.
|
||||
Now only warns after 365 days without updates.
|
||||
Critical security updates and kernel updates trigger INFO status immediately.
|
||||
"""
|
||||
cache_key = 'updates_check'
|
||||
current_time = time.time()
|
||||
@@ -1386,6 +1387,17 @@ class HealthMonitor:
|
||||
return self.cached_results.get(cache_key)
|
||||
|
||||
try:
|
||||
apt_history_path = '/var/log/apt/history.log'
|
||||
last_update_days = None
|
||||
|
||||
if os.path.exists(apt_history_path):
|
||||
try:
|
||||
mtime = os.path.getmtime(apt_history_path)
|
||||
days_since_update = (current_time - mtime) / 86400
|
||||
last_update_days = int(days_since_update)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
result = subprocess.run(
|
||||
['apt-get', 'upgrade', '--dry-run'],
|
||||
capture_output=True,
|
||||
@@ -1419,8 +1431,38 @@ class HealthMonitor:
|
||||
if security_updates:
|
||||
status = 'WARNING'
|
||||
reason = f'{len(security_updates)} security update(s) available'
|
||||
# Record persistent error for security updates
|
||||
health_persistence.record_error(
|
||||
error_key='updates_security',
|
||||
category='updates',
|
||||
severity='WARNING',
|
||||
reason=reason,
|
||||
details={'count': len(security_updates), 'packages': security_updates[:5]}
|
||||
)
|
||||
elif last_update_days and last_update_days >= 730:
|
||||
# 2+ years without updates - CRITICAL
|
||||
status = 'CRITICAL'
|
||||
reason = f'System not updated in {last_update_days} days (>2 years)'
|
||||
health_persistence.record_error(
|
||||
error_key='updates_730days',
|
||||
category='updates',
|
||||
severity='CRITICAL',
|
||||
reason=reason,
|
||||
details={'days': last_update_days, 'update_count': update_count}
|
||||
)
|
||||
elif last_update_days and last_update_days >= 365:
|
||||
# 1+ year without updates - WARNING
|
||||
status = 'WARNING'
|
||||
reason = f'System not updated in {last_update_days} days (>1 year)'
|
||||
health_persistence.record_error(
|
||||
error_key='updates_365days',
|
||||
category='updates',
|
||||
severity='WARNING',
|
||||
reason=reason,
|
||||
details={'days': last_update_days, 'update_count': update_count}
|
||||
)
|
||||
elif kernel_updates:
|
||||
status = 'INFO' # Informational, not critical
|
||||
status = 'INFO'
|
||||
reason = f'{len(kernel_updates)} kernel/PVE update(s) available'
|
||||
elif update_count > 50:
|
||||
status = 'INFO'
|
||||
@@ -1435,6 +1477,8 @@ class HealthMonitor:
|
||||
}
|
||||
if reason:
|
||||
update_result['reason'] = reason
|
||||
if last_update_days:
|
||||
update_result['days_since_update'] = last_update_days
|
||||
|
||||
self.cached_results[cache_key] = update_result
|
||||
self.last_check_times[cache_key] = current_time
|
||||
|
||||
@@ -27,6 +27,7 @@ class HealthPersistence:
|
||||
VM_ERROR_RETENTION = 48 * 3600 # 48 hours
|
||||
LOG_ERROR_RETENTION = 24 * 3600 # 24 hours
|
||||
DISK_ERROR_RETENTION = 48 * 3600 # 48 hours
|
||||
UPDATES_SUPPRESSION = 180 * 24 * 3600 # 180 days (6 months)
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize persistence with database in config directory"""
|
||||
@@ -102,8 +103,15 @@ class HealthPersistence:
|
||||
resolved_dt = datetime.fromisoformat(ack_check[1])
|
||||
hours_since_ack = (datetime.now() - resolved_dt).total_seconds() / 3600
|
||||
|
||||
if hours_since_ack < 24:
|
||||
# Skip re-adding recently acknowledged errors (within 24h)
|
||||
if category == 'updates':
|
||||
# Updates: suppress for 180 days (6 months)
|
||||
suppression_hours = self.UPDATES_SUPPRESSION / 3600
|
||||
else:
|
||||
# Other errors: suppress for 24 hours
|
||||
suppression_hours = 24
|
||||
|
||||
if hours_since_ack < suppression_hours:
|
||||
# Skip re-adding recently acknowledged errors
|
||||
conn.close()
|
||||
return {'type': 'skipped_acknowledged', 'needs_notification': False}
|
||||
except Exception:
|
||||
|
||||
BIN
scripts/test/ProxMenux-1.0.1-beta2.AppImage
Executable file
BIN
scripts/test/ProxMenux-1.0.1-beta2.AppImage
Executable file
Binary file not shown.
Reference in New Issue
Block a user