mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2025-10-01 23:56:18 +00:00
Compare commits
111 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
013d1980a3 | ||
|
df9f4a23b4 | ||
|
c41da47a48 | ||
|
e7214ad8df | ||
|
d6671de842 | ||
|
aad218db5d | ||
|
724ba1e271 | ||
|
97d554f638 | ||
|
c5a7655d26 | ||
|
403e896e3e | ||
|
1a15f43cad | ||
|
399b460c53 | ||
|
acc0362180 | ||
|
00db93e03f | ||
|
d1997794c8 | ||
|
aa1ebe69f2 | ||
|
4e7f5f56f1 | ||
|
28cb7359ce | ||
|
91c272d21c | ||
|
3c00125e83 | ||
|
f359848a2f | ||
|
989769e5e8 | ||
|
0f2f1b6211 | ||
|
ffe8f4acc6 | ||
|
edb09777de | ||
|
5262c7863e | ||
|
54256826fe | ||
|
3d3c224b3a | ||
|
049eccb872 | ||
|
269828c79e | ||
|
b4e25ae66d | ||
|
b20dd74d23 | ||
|
bc3e2ec358 | ||
|
6133a6d6d8 | ||
|
46a16c04e6 | ||
|
8469b3b26f | ||
|
2ed04f57fe | ||
|
b19bac679a | ||
|
3c33d5982c | ||
|
5b934eeb87 | ||
|
795d96f8d5 | ||
|
a8e7119b4a | ||
|
38569ff7fc | ||
|
e404557d62 | ||
|
96cbc75a5e | ||
|
c989af6cf0 | ||
|
4eac9d03ea | ||
|
6292009b0b | ||
|
3272be967d | ||
|
1c015da440 | ||
|
0d047cc956 | ||
|
e682070b85 | ||
|
9f08694d9b | ||
|
70f0db73e5 | ||
|
9dc8f44379 | ||
|
59f7ccd723 | ||
|
0710e95a6d | ||
|
4d1b5e3919 | ||
|
0cc2cb92dd | ||
|
dba4d168f7 | ||
|
d87ac7843c | ||
|
040535b004 | ||
|
c8acd2c0b1 | ||
|
d67fecea6e | ||
|
61f80f9ee6 | ||
|
9da8f9a5d1 | ||
|
f381468d5a | ||
|
6ae97266e4 | ||
|
66060f345c | ||
|
c61f568170 | ||
|
dcd108bda3 | ||
|
9d89f98987 | ||
|
ca7b959fce | ||
|
4a30793595 | ||
|
35e2d53f0f | ||
|
503efa4572 | ||
|
b0c33d9dff | ||
|
012b156b46 | ||
|
25d0d3bf59 | ||
|
0f1babc82b | ||
|
e2b93ea785 | ||
|
b1cedfa81e | ||
|
701ee36f6a | ||
|
4e5db86434 | ||
|
f45e9e657c | ||
|
4936fcdb1e | ||
|
374e05c422 | ||
|
9c00798373 | ||
|
db82fce925 | ||
|
acaa28e476 | ||
|
f297ce5809 | ||
|
3dc3fc5f67 | ||
|
4884fc4418 | ||
|
adc17842ec | ||
|
daa48b0b7c | ||
|
17c0362df3 | ||
|
29b9a63fc9 | ||
|
2a9fae160e | ||
|
0c49a1e3bd | ||
|
e896c41be1 | ||
|
187250fa24 | ||
|
9035b18584 | ||
|
4534d78978 | ||
|
f4ab0e982c | ||
|
3e7c6629a6 | ||
|
3ea17331fe | ||
|
1057fcc271 | ||
|
5a31c36097 | ||
|
1677a69bba | ||
|
315c49165d | ||
|
aae70e7ec0 |
56
.github/workflows/build-appimage.yml
vendored
Normal file
56
.github/workflows/build-appimage.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: Build ProxMenux Monitor AppImage
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths: [ 'AppImage/**' ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
paths: [ 'AppImage/**' ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: AppImage
|
||||
run: npm install --legacy-peer-deps
|
||||
|
||||
- name: Build Next.js app
|
||||
working-directory: AppImage
|
||||
run: npm run build
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y python3 python3-pip python3-venv
|
||||
|
||||
- name: Make build script executable
|
||||
working-directory: AppImage
|
||||
run: chmod +x scripts/build_appimage.sh
|
||||
|
||||
- name: Build AppImage
|
||||
working-directory: AppImage
|
||||
run: ./scripts/build_appimage.sh
|
||||
|
||||
- name: Get version from package.json
|
||||
id: version
|
||||
working-directory: AppImage
|
||||
run: echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload AppImage artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ProxMenux-${{ steps.version.outputs.VERSION }}-AppImage
|
||||
path: AppImage/dist/*.AppImage
|
||||
retention-days: 30
|
22
AppImage/README.md
Normal file
22
AppImage/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# ProxMenux Monitor
|
||||
|
||||
A modern, responsive dashboard for monitoring Proxmox VE systems built with Next.js and React.
|
||||
|
||||
## Features
|
||||
|
||||
- **System Overview**: Real-time monitoring of CPU, memory, temperature, and active VMs/LXC containers
|
||||
- **Storage Management**: Visual representation of storage distribution and disk performance metrics
|
||||
- **Network Monitoring**: Network interface statistics and performance graphs
|
||||
- **Virtual Machines**: Comprehensive view of VMs and LXC containers with resource usage
|
||||
- **System Logs**: Real-time system log monitoring and filtering
|
||||
- **Dark/Light Theme**: Toggle between themes with Proxmox-inspired design
|
||||
- **Responsive Design**: Works seamlessly on desktop and mobile devices
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- **Frontend**: Next.js 15, React 19, TypeScript
|
||||
- **Styling**: Tailwind CSS with custom Proxmox-inspired theme
|
||||
- **Charts**: Recharts for data visualization
|
||||
- **UI Components**: Radix UI primitives with shadcn/ui
|
||||
- **Backend**: Flask server for system data collection
|
||||
- **Packaging**: AppImage for easy distribution
|
9
AppImage/app/dashboard/page.tsx
Normal file
9
AppImage/app/dashboard/page.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { ProxmoxDashboard } from "../../components/proxmox-dashboard"
|
||||
|
||||
export default function DashboardPage() {
|
||||
return (
|
||||
<main className="min-h-screen bg-background">
|
||||
<ProxmoxDashboard />
|
||||
</main>
|
||||
)
|
||||
}
|
124
AppImage/app/globals.css
Normal file
124
AppImage/app/globals.css
Normal file
@@ -0,0 +1,124 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.145 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.145 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.985 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.396 0.141 25.723);
|
||||
--destructive-foreground: oklch(0.637 0.237 25.331);
|
||||
--border: oklch(0.269 0 0);
|
||||
--input: oklch(0.269 0 0);
|
||||
--ring: oklch(0.439 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(0.269 0 0);
|
||||
--sidebar-ring: oklch(0.439 0 0);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
/* optional: --font-sans, --font-serif, --font-mono if they are applied in the layout.tsx */
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
46
AppImage/app/layout.tsx
Normal file
46
AppImage/app/layout.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import type React from "react"
|
||||
import type { Metadata } from "next"
|
||||
import { GeistSans } from "geist/font/sans"
|
||||
import { GeistMono } from "geist/font/mono"
|
||||
import { Analytics } from "@vercel/analytics/next"
|
||||
import { ThemeProvider } from "../components/theme-provider"
|
||||
import { Suspense } from "react"
|
||||
import "./globals.css"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "ProxMenux Monitor",
|
||||
description: "Proxmox System Dashboard and Monitor",
|
||||
generator: "v0.app",
|
||||
manifest: "/manifest.json",
|
||||
icons: {
|
||||
icon: [
|
||||
{ url: "/favicon-16x16.png", sizes: "16x16", type: "image/png" },
|
||||
{ url: "/favicon-32x32.png", sizes: "32x32", type: "image/png" },
|
||||
],
|
||||
apple: [{ url: "/apple-touch-icon.png", sizes: "180x180", type: "image/png" }],
|
||||
},
|
||||
viewport: "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no",
|
||||
themeColor: [
|
||||
{ media: "(prefers-color-scheme: light)", color: "#ffffff" },
|
||||
{ media: "(prefers-color-scheme: dark)", color: "#2b2f36" },
|
||||
],
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={`${GeistSans.variable} ${GeistMono.variable} antialiased bg-background text-foreground`}>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem disableTransitionOnChange>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
<Analytics />
|
||||
</Suspense>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
7
AppImage/app/page.tsx
Normal file
7
AppImage/app/page.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { ProxmoxDashboard } from "../components/proxmox-dashboard"
|
||||
|
||||
export default function Home() {
|
||||
return <ProxmoxDashboard />
|
||||
}
|
239
AppImage/components/network-metrics.tsx
Normal file
239
AppImage/components/network-metrics.tsx
Normal file
@@ -0,0 +1,239 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
|
||||
import { Badge } from "./ui/badge"
|
||||
import { Wifi, Globe, Shield, Activity, Network, Router, AlertCircle } from "lucide-react"
|
||||
|
||||
interface NetworkData {
|
||||
interfaces: NetworkInterface[]
|
||||
traffic: {
|
||||
bytes_sent: number
|
||||
bytes_recv: number
|
||||
packets_sent?: number
|
||||
packets_recv?: number
|
||||
}
|
||||
}
|
||||
|
||||
interface NetworkInterface {
|
||||
name: string
|
||||
status: string
|
||||
addresses: Array<{
|
||||
ip: string
|
||||
netmask: string
|
||||
}>
|
||||
}
|
||||
|
||||
const fetchNetworkData = async (): Promise<NetworkData | null> => {
|
||||
try {
|
||||
console.log("[v0] Fetching network data from Flask server...")
|
||||
const response = await fetch("/api/network", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
signal: AbortSignal.timeout(5000),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Flask server responded with status: ${response.status}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
console.log("[v0] Successfully fetched network data from Flask:", data)
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error("[v0] Failed to fetch network data from Flask server:", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function NetworkMetrics() {
|
||||
const [networkData, setNetworkData] = useState<NetworkData | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const result = await fetchNetworkData()
|
||||
|
||||
if (!result) {
|
||||
setError("Flask server not available. Please ensure the server is running.")
|
||||
} else {
|
||||
setNetworkData(result)
|
||||
}
|
||||
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
fetchData()
|
||||
const interval = setInterval(fetchData, 30000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center py-8">
|
||||
<div className="text-lg font-medium text-foreground mb-2">Loading network data...</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error || !networkData) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card className="bg-red-500/10 border-red-500/20">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-3 text-red-600">
|
||||
<AlertCircle className="h-6 w-6" />
|
||||
<div>
|
||||
<div className="font-semibold text-lg mb-1">Flask Server Not Available</div>
|
||||
<div className="text-sm">
|
||||
{error || "Unable to connect to the Flask server. Please ensure the server is running and try again."}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const trafficInMB = (networkData.traffic.bytes_recv / (1024 * 1024)).toFixed(1)
|
||||
const trafficOutMB = (networkData.traffic.bytes_sent / (1024 * 1024)).toFixed(1)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Network Overview Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Network Traffic</CardTitle>
|
||||
<Activity className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-foreground">{trafficInMB} MB</div>
|
||||
<div className="flex items-center space-x-2 mt-2">
|
||||
<span className="text-xs text-green-500">↓ {trafficInMB} MB</span>
|
||||
<span className="text-xs text-blue-500">↑ {trafficOutMB} MB</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">Total data transferred</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Active Interfaces</CardTitle>
|
||||
<Network className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-foreground">
|
||||
{networkData.interfaces.filter((i) => i.status === "up").length}
|
||||
</div>
|
||||
<div className="flex items-center mt-2">
|
||||
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">
|
||||
Online
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">{networkData.interfaces.length} total interfaces</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Firewall Status</CardTitle>
|
||||
<Shield className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-foreground">Active</div>
|
||||
<div className="flex items-center mt-2">
|
||||
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">
|
||||
Protected
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">System protected</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground flex items-center">
|
||||
<Globe className="h-5 w-5 mr-2" />
|
||||
Packets
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-foreground">
|
||||
{networkData.traffic.packets_recv ? (networkData.traffic.packets_recv / 1000).toFixed(0) : "N/A"}K
|
||||
</div>
|
||||
<div className="flex items-center mt-2">
|
||||
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">
|
||||
Received
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">Total packets received</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Network Interfaces */}
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground flex items-center">
|
||||
<Router className="h-5 w-5 mr-2" />
|
||||
Network Interfaces
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{networkData.interfaces.map((interface_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between p-4 rounded-lg border border-border bg-card/50"
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<Wifi className="h-5 w-5 text-muted-foreground" />
|
||||
<div>
|
||||
<div className="font-medium text-foreground">{interface_.name}</div>
|
||||
<div className="text-sm text-muted-foreground">Network Interface</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-6">
|
||||
<div className="text-center">
|
||||
<div className="text-sm text-muted-foreground">IP Address</div>
|
||||
<div className="text-sm font-medium text-foreground font-mono">
|
||||
{interface_.addresses.length > 0 ? interface_.addresses[0].ip : "N/A"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="text-sm text-muted-foreground">Netmask</div>
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
{interface_.addresses.length > 0 ? interface_.addresses[0].netmask : "N/A"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
interface_.status === "up"
|
||||
? "bg-green-500/10 text-green-500 border-green-500/20"
|
||||
: "bg-red-500/10 text-red-500 border-red-500/20"
|
||||
}
|
||||
>
|
||||
{interface_.status.toUpperCase()}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
294
AppImage/components/proxmox-dashboard.tsx
Normal file
294
AppImage/components/proxmox-dashboard.tsx
Normal file
@@ -0,0 +1,294 @@
|
||||
"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 { StorageMetrics } from "./storage-metrics"
|
||||
import { NetworkMetrics } from "./network-metrics"
|
||||
import { VirtualMachines } from "./virtual-machines"
|
||||
import { SystemLogs } from "./system-logs"
|
||||
import { RefreshCw, AlertTriangle, CheckCircle, XCircle, Server } from "lucide-react"
|
||||
import Image from "next/image"
|
||||
import { ThemeToggle } from "./theme-toggle"
|
||||
|
||||
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[]
|
||||
}
|
||||
|
||||
export function ProxmoxDashboard() {
|
||||
const [systemStatus, setSystemStatus] = useState<SystemStatus>({
|
||||
status: "healthy",
|
||||
uptime: "Loading...",
|
||||
lastUpdate: new Date().toLocaleTimeString(),
|
||||
serverName: "Loading...",
|
||||
nodeId: "Loading...",
|
||||
})
|
||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||
const [isServerConnected, setIsServerConnected] = useState(true)
|
||||
const [componentKey, setComponentKey] = useState(0)
|
||||
|
||||
const fetchSystemData = useCallback(async () => {
|
||||
console.log("[v0] Fetching system data from Flask server...")
|
||||
console.log("[v0] Current window location:", window.location.href)
|
||||
|
||||
const apiUrl = "/api/system"
|
||||
|
||||
console.log("[v0] API URL:", apiUrl)
|
||||
|
||||
try {
|
||||
const response = await fetch(apiUrl, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
console.log("[v0] Response status:", response.status)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server responded with status: ${response.status}`)
|
||||
}
|
||||
|
||||
const data: FlaskSystemData = await response.json()
|
||||
console.log("[v0] System data received:", data)
|
||||
|
||||
let status: "healthy" | "warning" | "critical" = "healthy"
|
||||
if (data.cpu_usage > 90 || data.memory_usage > 90) {
|
||||
status = "critical"
|
||||
} else if (data.cpu_usage > 75 || data.memory_usage > 75) {
|
||||
status = "warning"
|
||||
}
|
||||
|
||||
setSystemStatus({
|
||||
status,
|
||||
uptime: data.uptime,
|
||||
lastUpdate: new Date().toLocaleTimeString(),
|
||||
serverName: data.hostname,
|
||||
nodeId: data.node_id,
|
||||
})
|
||||
setIsServerConnected(true)
|
||||
} catch (error) {
|
||||
console.error("[v0] Failed to fetch system data from Flask server:", error)
|
||||
console.error("[v0] Error details:", {
|
||||
message: error instanceof Error ? error.message : "Unknown error",
|
||||
apiUrl,
|
||||
windowLocation: window.location.href,
|
||||
})
|
||||
|
||||
setIsServerConnected(false)
|
||||
setSystemStatus((prev) => ({
|
||||
...prev,
|
||||
status: "critical",
|
||||
serverName: "Server Offline",
|
||||
nodeId: "Server Offline",
|
||||
uptime: "N/A",
|
||||
lastUpdate: new Date().toLocaleTimeString(),
|
||||
}))
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchSystemData()
|
||||
const interval = setInterval(fetchSystemData, 5000)
|
||||
return () => clearInterval(interval)
|
||||
}, [fetchSystemData])
|
||||
|
||||
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 <CheckCircle className="h-4 w-4 text-green-500" />
|
||||
case "warning":
|
||||
return <AlertTriangle className="h-4 w-4 text-yellow-500" />
|
||||
case "critical":
|
||||
return <XCircle className="h-4 w-4 text-red-500" />
|
||||
}
|
||||
}, [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])
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{!isServerConnected && (
|
||||
<div className="bg-red-500/10 border-b border-red-500/20 px-6 py-3">
|
||||
<div className="container mx-auto">
|
||||
<div className="flex items-center space-x-2 text-red-500 mb-2">
|
||||
<XCircle className="h-5 w-5" />
|
||||
<span className="font-medium">Flask Server Connection Failed</span>
|
||||
</div>
|
||||
<div className="text-sm text-red-500/80 space-y-1 ml-7">
|
||||
<p>• Check that the AppImage is running correctly</p>
|
||||
<p>• The Flask server should start automatically on port 8008</p>
|
||||
<p>
|
||||
• Try accessing:{" "}
|
||||
<a
|
||||
href={`http://${typeof window !== "undefined" ? window.location.host : "localhost:8008"}/api/health`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
http://{typeof window !== "undefined" ? window.location.host : "localhost:8008"}/api/health
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<header className="border-b border-border bg-card sticky top-0 z-50 shadow-sm">
|
||||
<div className="container mx-auto px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-10 h-10 relative flex items-center justify-center bg-primary/10 overflow-hidden">
|
||||
<Image
|
||||
src="/images/proxmenux-logo.png"
|
||||
alt="ProxMenux Logo"
|
||||
width={40}
|
||||
height={40}
|
||||
className="object-contain"
|
||||
priority
|
||||
onError={(e) => {
|
||||
console.log("[v0] Logo failed to load, using fallback icon")
|
||||
const target = e.target as HTMLImageElement
|
||||
target.style.display = "none"
|
||||
const fallback = target.parentElement?.querySelector(".fallback-icon")
|
||||
if (fallback) {
|
||||
fallback.classList.remove("hidden")
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Server className="h-6 w-6 text-primary absolute fallback-icon hidden" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-foreground">ProxMenux Monitor</h1>
|
||||
<p className="text-sm text-muted-foreground">Proxmox System Dashboard</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="hidden md:flex items-center space-x-2">
|
||||
<Server className="h-4 w-4 text-muted-foreground" />
|
||||
<div className="text-sm">
|
||||
<div className="font-medium text-foreground">{systemStatus.serverName}</div>
|
||||
<div className="text-xs text-muted-foreground">{systemStatus.nodeId}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Badge variant="outline" className={statusColor}>
|
||||
{statusIcon}
|
||||
<span className="ml-1 capitalize">{systemStatus.status}</span>
|
||||
</Badge>
|
||||
|
||||
<div className="text-sm text-muted-foreground">Uptime: {systemStatus.uptime}</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={refreshData}
|
||||
disabled={isRefreshing}
|
||||
className="border-border/50 bg-transparent hover:bg-secondary"
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 mr-2 ${isRefreshing ? "animate-spin" : ""}`} />
|
||||
Refresh
|
||||
</Button>
|
||||
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="container mx-auto px-6 py-6">
|
||||
<Tabs defaultValue="overview" className="space-y-6">
|
||||
<TabsList className="grid w-full grid-cols-5 bg-card border border-border">
|
||||
<TabsTrigger
|
||||
value="overview"
|
||||
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground"
|
||||
>
|
||||
Overview
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="storage"
|
||||
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground"
|
||||
>
|
||||
Storage
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="network"
|
||||
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground"
|
||||
>
|
||||
Network
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="vms"
|
||||
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground"
|
||||
>
|
||||
Virtual Machines
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="logs"
|
||||
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground"
|
||||
>
|
||||
System Logs
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="overview" className="space-y-6">
|
||||
<SystemOverview key={`overview-${componentKey}`} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="storage" className="space-y-6">
|
||||
<StorageMetrics key={`storage-${componentKey}`} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="network" className="space-y-6">
|
||||
<NetworkMetrics key={`network-${componentKey}`} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="vms" className="space-y-6">
|
||||
<VirtualMachines key={`vms-${componentKey}`} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="logs" className="space-y-6">
|
||||
<SystemLogs key={`logs-${componentKey}`} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<footer className="mt-12 pt-6 border-t border-border text-center text-sm text-muted-foreground">
|
||||
<p>Last updated: {systemStatus.lastUpdate} • ProxMenux Monitor v1.0.0</p>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
237
AppImage/components/storage-metrics.tsx
Normal file
237
AppImage/components/storage-metrics.tsx
Normal file
@@ -0,0 +1,237 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
|
||||
import { Progress } from "./ui/progress"
|
||||
import { Badge } from "./ui/badge"
|
||||
import { HardDrive, Database, Archive, AlertTriangle, CheckCircle, Activity, AlertCircle } from "lucide-react"
|
||||
|
||||
interface StorageData {
|
||||
total: number
|
||||
used: number
|
||||
available: number
|
||||
disks: DiskInfo[]
|
||||
}
|
||||
|
||||
interface DiskInfo {
|
||||
name: string
|
||||
mountpoint: string
|
||||
fstype: string
|
||||
total: number
|
||||
used: number
|
||||
available: number
|
||||
usage_percent: number
|
||||
health: string
|
||||
temperature: number
|
||||
}
|
||||
|
||||
const fetchStorageData = async (): Promise<StorageData | null> => {
|
||||
try {
|
||||
console.log("[v0] Fetching storage data from Flask server...")
|
||||
const response = await fetch("/api/storage", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
signal: AbortSignal.timeout(5000),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Flask server responded with status: ${response.status}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
console.log("[v0] Successfully fetched storage data from Flask:", data)
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error("[v0] Failed to fetch storage data from Flask server:", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function StorageMetrics() {
|
||||
const [storageData, setStorageData] = useState<StorageData | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const result = await fetchStorageData()
|
||||
|
||||
if (!result) {
|
||||
setError("Flask server not available. Please ensure the server is running.")
|
||||
} else {
|
||||
setStorageData(result)
|
||||
}
|
||||
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
fetchData()
|
||||
const interval = setInterval(fetchData, 30000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center py-8">
|
||||
<div className="text-lg font-medium text-foreground mb-2">Loading storage data...</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error || !storageData) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card className="bg-red-500/10 border-red-500/20">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-3 text-red-600">
|
||||
<AlertCircle className="h-6 w-6" />
|
||||
<div>
|
||||
<div className="font-semibold text-lg mb-1">Flask Server Not Available</div>
|
||||
<div className="text-sm">
|
||||
{error || "Unable to connect to the Flask server. Please ensure the server is running and try again."}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const usagePercent = storageData.total > 0 ? (storageData.used / storageData.total) * 100 : 0
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Storage Overview Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Total Storage</CardTitle>
|
||||
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-foreground">{storageData.total.toFixed(1)} GB</div>
|
||||
<Progress value={usagePercent} className="mt-2" />
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
{storageData.used.toFixed(1)} GB used • {storageData.available.toFixed(1)} GB available
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Used Storage</CardTitle>
|
||||
<Database className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-foreground">{storageData.used.toFixed(1)} GB</div>
|
||||
<Progress value={usagePercent} className="mt-2" />
|
||||
<p className="text-xs text-muted-foreground mt-2">{usagePercent.toFixed(1)}% of total space</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground flex items-center">
|
||||
<Archive className="h-5 w-5 mr-2" />
|
||||
Available
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-foreground">{storageData.available.toFixed(1)} GB</div>
|
||||
<div className="flex items-center mt-2">
|
||||
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">
|
||||
{((storageData.available / storageData.total) * 100).toFixed(1)}% Free
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">Available space</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground flex items-center">
|
||||
<Activity className="h-5 w-5 mr-2" />
|
||||
Disks
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-foreground">{storageData.disks.length}</div>
|
||||
<div className="flex items-center space-x-2 mt-2">
|
||||
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">
|
||||
{storageData.disks.filter((d) => d.health === "healthy").length} Healthy
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">Storage devices</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Disk Details */}
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground flex items-center">
|
||||
<Database className="h-5 w-5 mr-2" />
|
||||
Storage Devices
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{storageData.disks.map((disk, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between p-4 rounded-lg border border-border bg-card/50"
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<HardDrive className="h-5 w-5 text-muted-foreground" />
|
||||
<div>
|
||||
<div className="font-medium text-foreground">{disk.name}</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{disk.fstype} • {disk.mountpoint}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-6">
|
||||
<div className="text-right">
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
{disk.used.toFixed(1)} GB / {disk.total.toFixed(1)} GB
|
||||
</div>
|
||||
<Progress value={disk.usage_percent} className="w-24 mt-1" />
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="text-sm text-muted-foreground">Temp</div>
|
||||
<div className="text-sm font-medium text-foreground">{disk.temperature}°C</div>
|
||||
</div>
|
||||
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
disk.health === "healthy"
|
||||
? "bg-green-500/10 text-green-500 border-green-500/20"
|
||||
: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
|
||||
}
|
||||
>
|
||||
{disk.health === "healthy" ? (
|
||||
<CheckCircle className="h-3 w-3 mr-1" />
|
||||
) : (
|
||||
<AlertTriangle className="h-3 w-3 mr-1" />
|
||||
)}
|
||||
{disk.health}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
275
AppImage/components/system-logs.tsx
Normal file
275
AppImage/components/system-logs.tsx
Normal file
@@ -0,0 +1,275 @@
|
||||
"use client"
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
|
||||
import { Badge } from "./ui/badge"
|
||||
import { Button } from "./ui/button"
|
||||
import { Input } from "./ui/input"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
|
||||
import { ScrollArea } from "./ui/scroll-area"
|
||||
import { FileText, Search, Download, AlertTriangle, Info, CheckCircle, XCircle } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
|
||||
const systemLogs = [
|
||||
{
|
||||
timestamp: "2024-01-15 14:32:15",
|
||||
level: "info",
|
||||
service: "pveproxy",
|
||||
message: "User root@pam authenticated successfully",
|
||||
source: "auth.log",
|
||||
},
|
||||
{
|
||||
timestamp: "2024-01-15 14:31:45",
|
||||
level: "warning",
|
||||
service: "pvedaemon",
|
||||
message: "VM 101 high memory usage detected (85%)",
|
||||
source: "syslog",
|
||||
},
|
||||
{
|
||||
timestamp: "2024-01-15 14:30:22",
|
||||
level: "error",
|
||||
service: "pve-cluster",
|
||||
message: "Failed to connect to cluster node pve-02",
|
||||
source: "cluster.log",
|
||||
},
|
||||
{
|
||||
timestamp: "2024-01-15 14:29:18",
|
||||
level: "info",
|
||||
service: "pvestatd",
|
||||
message: "Storage local: 1.25TB used, 750GB available",
|
||||
source: "syslog",
|
||||
},
|
||||
{
|
||||
timestamp: "2024-01-15 14:28:33",
|
||||
level: "info",
|
||||
service: "pve-firewall",
|
||||
message: "Blocked connection attempt from 192.168.1.50",
|
||||
source: "firewall.log",
|
||||
},
|
||||
{
|
||||
timestamp: "2024-01-15 14:27:45",
|
||||
level: "warning",
|
||||
service: "smartd",
|
||||
message: "SMART warning: /dev/nvme0n1 temperature high (55°C)",
|
||||
source: "smart.log",
|
||||
},
|
||||
{
|
||||
timestamp: "2024-01-15 14:26:12",
|
||||
level: "info",
|
||||
service: "pveproxy",
|
||||
message: "Started backup job for VM 100",
|
||||
source: "backup.log",
|
||||
},
|
||||
{
|
||||
timestamp: "2024-01-15 14:25:38",
|
||||
level: "error",
|
||||
service: "qemu-server",
|
||||
message: "VM 102 failed to start: insufficient memory",
|
||||
source: "qemu.log",
|
||||
},
|
||||
{
|
||||
timestamp: "2024-01-15 14:24:55",
|
||||
level: "info",
|
||||
service: "pvedaemon",
|
||||
message: "VM 103 migrated successfully to node pve-01",
|
||||
source: "migration.log",
|
||||
},
|
||||
{
|
||||
timestamp: "2024-01-15 14:23:17",
|
||||
level: "warning",
|
||||
service: "pve-ha-lrm",
|
||||
message: "Resource VM:104 state changed to error",
|
||||
source: "ha.log",
|
||||
},
|
||||
]
|
||||
|
||||
export function SystemLogs() {
|
||||
const [searchTerm, setSearchTerm] = useState("")
|
||||
const [levelFilter, setLevelFilter] = useState("all")
|
||||
const [serviceFilter, setServiceFilter] = useState("all")
|
||||
|
||||
const filteredLogs = systemLogs.filter((log) => {
|
||||
const matchesSearch =
|
||||
log.message.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
log.service.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
const matchesLevel = levelFilter === "all" || log.level === levelFilter
|
||||
const matchesService = serviceFilter === "all" || log.service === serviceFilter
|
||||
|
||||
return matchesSearch && matchesLevel && matchesService
|
||||
})
|
||||
|
||||
const getLevelColor = (level: string) => {
|
||||
switch (level) {
|
||||
case "error":
|
||||
return "bg-red-500/10 text-red-500 border-red-500/20"
|
||||
case "warning":
|
||||
return "bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
|
||||
case "info":
|
||||
return "bg-blue-500/10 text-blue-500 border-blue-500/20"
|
||||
default:
|
||||
return "bg-gray-500/10 text-gray-500 border-gray-500/20"
|
||||
}
|
||||
}
|
||||
|
||||
const getLevelIcon = (level: string) => {
|
||||
switch (level) {
|
||||
case "error":
|
||||
return <XCircle className="h-3 w-3 mr-1" />
|
||||
case "warning":
|
||||
return <AlertTriangle className="h-3 w-3 mr-1" />
|
||||
case "info":
|
||||
return <Info className="h-3 w-3 mr-1" />
|
||||
default:
|
||||
return <CheckCircle className="h-3 w-3 mr-1" />
|
||||
}
|
||||
}
|
||||
|
||||
const logCounts = {
|
||||
total: systemLogs.length,
|
||||
error: systemLogs.filter((log) => log.level === "error").length,
|
||||
warning: systemLogs.filter((log) => log.level === "warning").length,
|
||||
info: systemLogs.filter((log) => log.level === "info").length,
|
||||
}
|
||||
|
||||
const uniqueServices = [...new Set(systemLogs.map((log) => log.service))]
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Log Statistics */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Total Logs</CardTitle>
|
||||
<FileText className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-foreground">{logCounts.total}</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">Last 24 hours</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Errors</CardTitle>
|
||||
<XCircle className="h-4 w-4 text-red-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-red-500">{logCounts.error}</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">Requires attention</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Warnings</CardTitle>
|
||||
<AlertTriangle className="h-4 w-4 text-yellow-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-yellow-500">{logCounts.warning}</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">Monitor closely</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Info</CardTitle>
|
||||
<Info className="h-4 w-4 text-blue-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-blue-500">{logCounts.info}</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">Normal operations</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Log Filters and Search */}
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground flex items-center">
|
||||
<FileText className="h-5 w-5 mr-2" />
|
||||
System Logs
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col sm:flex-row gap-4 mb-6">
|
||||
<div className="flex-1">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search logs..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10 bg-background border-border"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Select value={levelFilter} onValueChange={setLevelFilter}>
|
||||
<SelectTrigger className="w-full sm:w-[180px] bg-background border-border">
|
||||
<SelectValue placeholder="Filter by level" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Levels</SelectItem>
|
||||
<SelectItem value="error">Error</SelectItem>
|
||||
<SelectItem value="warning">Warning</SelectItem>
|
||||
<SelectItem value="info">Info</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select value={serviceFilter} onValueChange={setServiceFilter}>
|
||||
<SelectTrigger className="w-full sm:w-[180px] bg-background border-border">
|
||||
<SelectValue placeholder="Filter by service" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Services</SelectItem>
|
||||
{uniqueServices.map((service) => (
|
||||
<SelectItem key={service} value={service}>
|
||||
{service}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Button variant="outline" className="border-border bg-transparent">
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
Export
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="h-[600px] w-full rounded-md border border-border">
|
||||
<div className="space-y-2 p-4">
|
||||
{filteredLogs.map((log, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-start space-x-4 p-3 rounded-lg bg-card/50 border border-border/50"
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
<Badge variant="outline" className={getLevelColor(log.level)}>
|
||||
{getLevelIcon(log.level)}
|
||||
{log.level.toUpperCase()}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="text-sm font-medium text-foreground">{log.service}</div>
|
||||
<div className="text-xs text-muted-foreground font-mono">{log.timestamp}</div>
|
||||
</div>
|
||||
<div className="text-sm text-foreground mb-1">{log.message}</div>
|
||||
<div className="text-xs text-muted-foreground">Source: {log.source}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{filteredLogs.length === 0 && (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<FileText className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
||||
<p>No logs found matching your criteria</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
485
AppImage/components/system-overview.tsx
Normal file
485
AppImage/components/system-overview.tsx
Normal file
@@ -0,0 +1,485 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
|
||||
import { Progress } from "./ui/progress"
|
||||
import { Badge } from "./ui/badge"
|
||||
import { XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, AreaChart, Area } from "recharts"
|
||||
import { Cpu, MemoryStick, Thermometer, Activity, Server, Zap, AlertCircle } from "lucide-react"
|
||||
|
||||
interface SystemData {
|
||||
cpu_usage: number
|
||||
memory_usage: number
|
||||
memory_total: number
|
||||
memory_used: number
|
||||
temperature: number
|
||||
uptime: string
|
||||
load_average: number[]
|
||||
hostname: string
|
||||
node_id: string
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
interface VMData {
|
||||
vmid: number
|
||||
name: string
|
||||
status: string
|
||||
cpu: number
|
||||
mem: number
|
||||
maxmem: number
|
||||
disk: number
|
||||
maxdisk: number
|
||||
uptime: number
|
||||
}
|
||||
|
||||
interface HistoricalData {
|
||||
timestamp: string
|
||||
cpu_usage: number
|
||||
memory_used: number
|
||||
memory_total: number
|
||||
}
|
||||
|
||||
const historicalDataStore: HistoricalData[] = []
|
||||
const MAX_HISTORICAL_POINTS = 24 // Store 24 data points for 24h view
|
||||
|
||||
const fetchSystemData = async (): Promise<SystemData | null> => {
|
||||
try {
|
||||
console.log("[v0] Fetching system data from Flask server...")
|
||||
|
||||
const baseUrl = typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : ""
|
||||
const apiUrl = `${baseUrl}/api/system`
|
||||
console.log("[v0] Fetching from URL:", apiUrl)
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
cache: "no-store",
|
||||
})
|
||||
|
||||
console.log("[v0] Response status:", response.status)
|
||||
console.log("[v0] Response ok:", response.ok)
|
||||
console.log("[v0] Response headers:", Object.fromEntries(response.headers.entries()))
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
console.error("[v0] Flask server error response:", errorText)
|
||||
throw new Error(`Flask server responded with status: ${response.status}`)
|
||||
}
|
||||
|
||||
const responseText = await response.text()
|
||||
console.log("[v0] Raw response text:", responseText)
|
||||
console.log("[v0] Response text length:", responseText.length)
|
||||
console.log("[v0] First 100 chars:", responseText.substring(0, 100))
|
||||
|
||||
// Try to parse the JSON
|
||||
let data
|
||||
try {
|
||||
data = JSON.parse(responseText)
|
||||
console.log("[v0] Successfully parsed JSON:", data)
|
||||
} catch (parseError) {
|
||||
console.error("[v0] JSON parse error:", parseError)
|
||||
console.error("[v0] Failed to parse response as JSON")
|
||||
throw new Error("Invalid JSON response from server")
|
||||
}
|
||||
|
||||
// Store historical data
|
||||
historicalDataStore.push({
|
||||
timestamp: data.timestamp,
|
||||
cpu_usage: data.cpu_usage,
|
||||
memory_used: data.memory_used,
|
||||
memory_total: data.memory_total,
|
||||
})
|
||||
|
||||
// Keep only last MAX_HISTORICAL_POINTS
|
||||
if (historicalDataStore.length > MAX_HISTORICAL_POINTS) {
|
||||
historicalDataStore.shift()
|
||||
}
|
||||
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error("[v0] Failed to fetch system data from Flask server:", error)
|
||||
console.error("[v0] Error type:", error instanceof Error ? error.constructor.name : typeof error)
|
||||
console.error("[v0] Error message:", error instanceof Error ? error.message : String(error))
|
||||
console.error("[v0] Error stack:", error instanceof Error ? error.stack : "No stack trace")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const fetchVMData = async (): Promise<VMData[]> => {
|
||||
try {
|
||||
console.log("[v0] Fetching VM data from Flask server...")
|
||||
|
||||
const baseUrl = typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : ""
|
||||
const apiUrl = `${baseUrl}/api/vms`
|
||||
console.log("[v0] Fetching from URL:", apiUrl)
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
cache: "no-store",
|
||||
})
|
||||
|
||||
console.log("[v0] VM Response status:", response.status)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
console.error("[v0] Flask server error response:", errorText)
|
||||
throw new Error(`Flask server responded with status: ${response.status}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
console.log("[v0] Successfully fetched VM data from Flask:", data)
|
||||
return Array.isArray(data) ? data : data.vms || []
|
||||
} catch (error) {
|
||||
console.error("[v0] Failed to fetch VM data from Flask server:", error)
|
||||
console.error("[v0] Error type:", error instanceof Error ? error.constructor.name : typeof error)
|
||||
console.error("[v0] Error message:", error instanceof Error ? error.message : String(error))
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
const generateChartData = () => {
|
||||
if (historicalDataStore.length === 0) {
|
||||
return { cpuData: [], memoryData: [] }
|
||||
}
|
||||
|
||||
const cpuData = historicalDataStore.map((point) => ({
|
||||
time: new Date(point.timestamp).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" }),
|
||||
value: point.cpu_usage,
|
||||
}))
|
||||
|
||||
const memoryData = historicalDataStore.map((point) => ({
|
||||
time: new Date(point.timestamp).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" }),
|
||||
used: point.memory_used,
|
||||
available: point.memory_total - point.memory_used,
|
||||
}))
|
||||
|
||||
return { cpuData, memoryData }
|
||||
}
|
||||
|
||||
export function SystemOverview() {
|
||||
const [systemData, setSystemData] = useState<SystemData | null>(null)
|
||||
const [vmData, setVmData] = useState<VMData[]>([])
|
||||
const [chartData, setChartData] = useState(generateChartData())
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
const [systemResult, vmResult] = await Promise.all([fetchSystemData(), fetchVMData()])
|
||||
|
||||
if (!systemResult) {
|
||||
setError("Flask server not available. Please ensure the server is running.")
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
setSystemData(systemResult)
|
||||
setVmData(vmResult)
|
||||
setChartData(generateChartData())
|
||||
} catch (err) {
|
||||
console.error("[v0] Error fetching data:", err)
|
||||
setError("Failed to connect to Flask server. Please check your connection.")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchData()
|
||||
|
||||
const interval = setInterval(() => {
|
||||
fetchData()
|
||||
}, 30000) // Update every 30 seconds
|
||||
|
||||
return () => {
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center py-8">
|
||||
<div className="text-lg font-medium text-foreground mb-2">Connecting to ProxMenux Monitor...</div>
|
||||
<div className="text-sm text-muted-foreground">Fetching real-time system data</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{[...Array(4)].map((_, i) => (
|
||||
<Card key={i} className="bg-card border-border animate-pulse">
|
||||
<CardContent className="p-6">
|
||||
<div className="h-4 bg-muted rounded w-1/2 mb-4"></div>
|
||||
<div className="h-8 bg-muted rounded w-3/4 mb-2"></div>
|
||||
<div className="h-2 bg-muted rounded w-full mb-2"></div>
|
||||
<div className="h-3 bg-muted rounded w-2/3"></div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error || !systemData) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card className="bg-red-500/10 border-red-500/20">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-3 text-red-600">
|
||||
<AlertCircle className="h-6 w-6" />
|
||||
<div>
|
||||
<div className="font-semibold text-lg mb-1">Flask Server Not Available</div>
|
||||
<div className="text-sm">
|
||||
{error || "Unable to connect to the Flask server. Please ensure the server is running and try again."}
|
||||
</div>
|
||||
<div className="text-sm mt-2">
|
||||
<strong>Troubleshooting:</strong>
|
||||
<ul className="list-disc list-inside mt-1 space-y-1">
|
||||
<li>Check if the Flask server is running on the correct port</li>
|
||||
<li>Verify network connectivity</li>
|
||||
<li>Check server logs for errors</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const vmStats = {
|
||||
total: vmData.length,
|
||||
running: vmData.filter((vm) => vm.status === "running").length,
|
||||
stopped: vmData.filter((vm) => vm.status === "stopped").length,
|
||||
lxc: 0,
|
||||
}
|
||||
|
||||
const getTemperatureStatus = (temp: number) => {
|
||||
if (temp === 0) return { status: "N/A", color: "bg-gray-500/10 text-gray-500 border-gray-500/20" }
|
||||
if (temp < 60) return { status: "Normal", color: "bg-green-500/10 text-green-500 border-green-500/20" }
|
||||
if (temp < 75) return { status: "Warm", color: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20" }
|
||||
return { status: "Hot", color: "bg-red-500/10 text-red-500 border-red-500/20" }
|
||||
}
|
||||
|
||||
const tempStatus = getTemperatureStatus(systemData.temperature)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Key Metrics Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">CPU Usage</CardTitle>
|
||||
<Cpu className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-foreground">{systemData.cpu_usage}%</div>
|
||||
<Progress value={systemData.cpu_usage} className="mt-2" />
|
||||
<p className="text-xs text-muted-foreground mt-2">Real-time data from Flask server</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Memory Usage</CardTitle>
|
||||
<MemoryStick className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-foreground">{systemData.memory_used.toFixed(1)} GB</div>
|
||||
<Progress value={systemData.memory_usage} className="mt-2" />
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
{systemData.memory_usage.toFixed(1)}% of {systemData.memory_total} GB
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Temperature</CardTitle>
|
||||
<Thermometer className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-foreground">
|
||||
{systemData.temperature === 0 ? "N/A" : `${systemData.temperature}°C`}
|
||||
</div>
|
||||
<div className="flex items-center mt-2">
|
||||
<Badge variant="outline" className={tempStatus.color}>
|
||||
{tempStatus.status}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
{systemData.temperature === 0 ? "No sensor available" : "Live temperature reading"}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Active VMs</CardTitle>
|
||||
<Server className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-foreground">{vmStats.running}</div>
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">
|
||||
{vmStats.running} Running
|
||||
</Badge>
|
||||
{vmStats.stopped > 0 && (
|
||||
<Badge variant="outline" className="bg-yellow-500/10 text-yellow-500 border-yellow-500/20">
|
||||
{vmStats.stopped} Stopped
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">Total: {vmStats.total} VMs configured</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Charts Section */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground flex items-center">
|
||||
<Activity className="h-5 w-5 mr-2" />
|
||||
CPU Usage (Last {historicalDataStore.length} readings)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{chartData.cpuData.length > 0 ? (
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<AreaChart data={chartData.cpuData}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
|
||||
<XAxis dataKey="time" stroke="hsl(var(--muted-foreground))" fontSize={12} />
|
||||
<YAxis stroke="hsl(var(--muted-foreground))" fontSize={12} />
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: "hsl(var(--card))",
|
||||
border: "1px solid hsl(var(--border))",
|
||||
borderRadius: "8px",
|
||||
color: "hsl(var(--foreground))",
|
||||
}}
|
||||
/>
|
||||
<Area type="monotone" dataKey="value" stroke="#3b82f6" fill="#3b82f6" fillOpacity={0.2} />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<div className="h-[300px] flex items-center justify-center text-muted-foreground">
|
||||
Collecting data... Check back in a few minutes
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground flex items-center">
|
||||
<MemoryStick className="h-5 w-5 mr-2" />
|
||||
Memory Usage (Last {historicalDataStore.length} readings)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{chartData.memoryData.length > 0 ? (
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<AreaChart data={chartData.memoryData}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
|
||||
<XAxis dataKey="time" stroke="hsl(var(--muted-foreground))" fontSize={12} />
|
||||
<YAxis stroke="hsl(var(--muted-foreground))" fontSize={12} />
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: "hsl(var(--card))",
|
||||
border: "1px solid hsl(var(--border))",
|
||||
borderRadius: "8px",
|
||||
color: "hsl(var(--foreground))",
|
||||
}}
|
||||
/>
|
||||
<Area type="monotone" dataKey="used" stackId="1" stroke="#3b82f6" fill="#3b82f6" fillOpacity={0.6} />
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="available"
|
||||
stackId="1"
|
||||
stroke="#10b981"
|
||||
fill="#10b981"
|
||||
fillOpacity={0.6}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<div className="h-[300px] flex items-center justify-center text-muted-foreground">
|
||||
Collecting data... Check back in a few minutes
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* System Information */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground flex items-center">
|
||||
<Server className="h-5 w-5 mr-2" />
|
||||
System Information
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Hostname:</span>
|
||||
<span className="text-foreground font-mono">{systemData.hostname}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Uptime:</span>
|
||||
<span className="text-foreground">{systemData.uptime}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Node ID:</span>
|
||||
<span className="text-foreground font-mono">{systemData.node_id}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Last Update:</span>
|
||||
<span className="text-foreground">{new Date(systemData.timestamp).toLocaleTimeString()}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground flex items-center">
|
||||
<Zap className="h-5 w-5 mr-2" />
|
||||
Performance Metrics
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Load Average:</span>
|
||||
<span className="text-foreground font-mono">
|
||||
{systemData.load_average.map((avg) => avg.toFixed(2)).join(", ")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Total Memory:</span>
|
||||
<span className="text-foreground">{systemData.memory_total} GB</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Available Memory:</span>
|
||||
<span className="text-foreground">
|
||||
{(systemData.memory_total - systemData.memory_used).toFixed(1)} GB
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">CPU Cores:</span>
|
||||
<span className="text-foreground">{navigator.hardwareConcurrency || "N/A"}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
7
AppImage/components/theme-provider.tsx
Normal file
7
AppImage/components/theme-provider.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
"use client"
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
||||
import type { ThemeProviderProps } from "next-themes"
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||
}
|
39
AppImage/components/theme-toggle.tsx
Normal file
39
AppImage/components/theme-toggle.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
"use client"
|
||||
import { Moon, Sun } from "lucide-react"
|
||||
import { useTheme } from "next-themes"
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
import { Button } from "./ui/button"
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { theme, setTheme } = useTheme()
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
const handleThemeToggle = () => {
|
||||
console.log("[v0] Current theme:", theme)
|
||||
const newTheme = theme === "light" ? "dark" : "light"
|
||||
console.log("[v0] Switching to theme:", newTheme)
|
||||
setTheme(newTheme)
|
||||
}
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<Button variant="outline" size="sm" className="border-border bg-transparent w-9 h-9">
|
||||
<Sun className="h-4 w-4" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button variant="outline" size="sm" onClick={handleThemeToggle} className="border-border bg-transparent w-9 h-9">
|
||||
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
28
AppImage/components/ui/badge.tsx
Normal file
28
AppImage/components/ui/badge.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import type * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return <div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
46
AppImage/components/ui/button.tsx
Normal file
46
AppImage/components/ui/button.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
|
||||
},
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
42
AppImage/components/ui/card.tsx
Normal file
42
AppImage/components/ui/card.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)} {...props} />
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
|
||||
),
|
||||
)
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<h3 ref={ref} className={cn("text-2xl font-semibold leading-none tracking-tight", className)} {...props} />
|
||||
),
|
||||
)
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
||||
),
|
||||
)
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />,
|
||||
)
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
|
||||
),
|
||||
)
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
22
AppImage/components/ui/input.tsx
Normal file
22
AppImage/components/ui/input.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
25
AppImage/components/ui/progress.tsx
Normal file
25
AppImage/components/ui/progress.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Progress = React.forwardRef<
|
||||
React.ElementRef<typeof ProgressPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
||||
>(({ className, value, ...props }, ref) => (
|
||||
<ProgressPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative h-2 w-full overflow-hidden rounded-full bg-secondary", className)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
className="h-full w-full flex-1 bg-primary transition-all"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
))
|
||||
Progress.displayName = ProgressPrimitive.Root.displayName
|
||||
|
||||
export { Progress }
|
40
AppImage/components/ui/scroll-area.tsx
Normal file
40
AppImage/components/ui/scroll-area.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root ref={ref} className={cn("relative overflow-hidden", className)} {...props}>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">{children}</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
))
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
||||
|
||||
const ScrollBar = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
>(({ className, orientation = "vertical", ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
ref={ref}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none select-none transition-colors",
|
||||
orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent p-[1px]",
|
||||
orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
))
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
144
AppImage/components/ui/select.tsx
Normal file
144
AppImage/components/ui/select.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Select = SelectPrimitive.Root
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group
|
||||
|
||||
const SelectValue = SelectPrimitive.Value
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-9 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
))
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
))
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
))
|
||||
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
))
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label ref={ref} className={cn("px-2 py-1.5 text-sm font-semibold", className)} {...props} />
|
||||
))
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
))
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
|
||||
))
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
}
|
52
AppImage/components/ui/tabs.tsx
Normal file
52
AppImage/components/ui/tabs.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import * as React from "react"
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Tabs = TabsPrimitive.Root
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsList.displayName = TabsPrimitive.List.displayName
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
267
AppImage/components/virtual-machines.tsx
Normal file
267
AppImage/components/virtual-machines.tsx
Normal file
@@ -0,0 +1,267 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
|
||||
import { Badge } from "./ui/badge"
|
||||
import { Progress } from "./ui/progress"
|
||||
import { Server, Play, Square, Monitor, Cpu, MemoryStick, AlertCircle } from "lucide-react"
|
||||
|
||||
interface VMData {
|
||||
vmid: number
|
||||
name: string
|
||||
status: string
|
||||
cpu: number
|
||||
mem: number
|
||||
maxmem: number
|
||||
disk: number
|
||||
maxdisk: number
|
||||
uptime: number
|
||||
}
|
||||
|
||||
const fetchVMData = async (): Promise<VMData[]> => {
|
||||
try {
|
||||
console.log("[v0] Fetching VM data from Flask server...")
|
||||
const response = await fetch("/api/vms", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
signal: AbortSignal.timeout(5000),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Flask server responded with status: ${response.status}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
console.log("[v0] Successfully fetched VM data from Flask:", data)
|
||||
return Array.isArray(data) ? data : []
|
||||
} catch (error) {
|
||||
console.error("[v0] Failed to fetch VM data from Flask server:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export function VirtualMachines() {
|
||||
const [vmData, setVmData] = useState<VMData[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const result = await fetchVMData()
|
||||
setVmData(result)
|
||||
} catch (err) {
|
||||
setError("Flask server not available. Please ensure the server is running.")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchData()
|
||||
const interval = setInterval(fetchData, 30000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center py-8">
|
||||
<div className="text-lg font-medium text-foreground mb-2">Loading VM data...</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card className="bg-red-500/10 border-red-500/20">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-3 text-red-600">
|
||||
<AlertCircle className="h-6 w-6" />
|
||||
<div>
|
||||
<div className="font-semibold text-lg mb-1">Flask Server Not Available</div>
|
||||
<div className="text-sm">
|
||||
{error || "Unable to connect to the Flask server. Please ensure the server is running and try again."}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const runningVMs = vmData.filter((vm) => vm.status === "running").length
|
||||
const stoppedVMs = vmData.filter((vm) => vm.status === "stopped").length
|
||||
const totalCPU = vmData.reduce((sum, vm) => sum + (vm.cpu || 0), 0)
|
||||
const totalMemory = vmData.reduce((sum, vm) => sum + (vm.maxmem || 0), 0)
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case "running":
|
||||
return "bg-green-500/10 text-green-500 border-green-500/20"
|
||||
case "stopped":
|
||||
return "bg-red-500/10 text-red-500 border-red-500/20"
|
||||
default:
|
||||
return "bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case "running":
|
||||
return <Play className="h-3 w-3 mr-1" />
|
||||
case "stopped":
|
||||
return <Square className="h-3 w-3 mr-1" />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const formatUptime = (seconds: number) => {
|
||||
const days = Math.floor(seconds / 86400)
|
||||
const hours = Math.floor((seconds % 86400) / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
return `${days}d ${hours}h ${minutes}m`
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* VM Overview Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Total VMs</CardTitle>
|
||||
<Server className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-foreground">{vmData.length}</div>
|
||||
<div className="vm-badges mt-2">
|
||||
<Badge variant="outline" className="vm-badge bg-green-500/10 text-green-500 border-green-500/20">
|
||||
{runningVMs} Running
|
||||
</Badge>
|
||||
<Badge variant="outline" className="vm-badge bg-red-500/10 text-red-500 border-red-500/20">
|
||||
{stoppedVMs} Stopped
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">Virtual machines configured</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Total CPU</CardTitle>
|
||||
<Cpu className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-foreground">{(totalCPU * 100).toFixed(0)}%</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">Allocated CPU usage</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Total Memory</CardTitle>
|
||||
<MemoryStick className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-foreground">{(totalMemory / 1024 ** 3).toFixed(1)} GB</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">Allocated RAM</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Average Load</CardTitle>
|
||||
<Monitor className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-foreground">
|
||||
{runningVMs > 0 ? ((totalCPU / runningVMs) * 100).toFixed(0) : 0}%
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">Average resource utilization</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Virtual Machines List */}
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground flex items-center">
|
||||
<Server className="h-5 w-5 mr-2" />
|
||||
Virtual Machines
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{vmData.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">No virtual machines found</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{vmData.map((vm) => {
|
||||
const cpuPercent = (vm.cpu * 100).toFixed(1)
|
||||
const memPercent = vm.maxmem > 0 ? ((vm.mem / vm.maxmem) * 100).toFixed(1) : "0"
|
||||
const memGB = (vm.mem / 1024 ** 3).toFixed(1)
|
||||
const maxMemGB = (vm.maxmem / 1024 ** 3).toFixed(1)
|
||||
|
||||
return (
|
||||
<div key={vm.vmid} className="p-6 rounded-lg border border-border bg-card/50">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Server className="h-6 w-6 text-muted-foreground" />
|
||||
<div>
|
||||
<div className="font-semibold text-foreground text-lg flex items-center">
|
||||
{vm.name}
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="ml-2 text-xs bg-purple-500/10 text-purple-500 border-purple-500/20"
|
||||
>
|
||||
VM
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">ID: {vm.vmid}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
<Badge variant="outline" className={getStatusColor(vm.status)}>
|
||||
{getStatusIcon(vm.status)}
|
||||
{vm.status.toUpperCase()}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground mb-2">CPU Usage</div>
|
||||
<div className="text-lg font-semibold text-foreground mb-1">{cpuPercent}%</div>
|
||||
<Progress value={Number.parseFloat(cpuPercent)} className="h-2" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground mb-2">Memory Usage</div>
|
||||
<div className="text-lg font-semibold text-foreground mb-1">
|
||||
{memGB} GB / {maxMemGB} GB
|
||||
</div>
|
||||
<Progress value={Number.parseFloat(memPercent)} className="h-2" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground mb-2">Uptime</div>
|
||||
<div className="text-lg font-semibold text-foreground">{formatUptime(vm.uptime)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
6
AppImage/lib/utils.ts
Normal file
6
AppImage/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
30
AppImage/next.config.mjs
Normal file
30
AppImage/next.config.mjs
Normal file
@@ -0,0 +1,30 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: 'export',
|
||||
trailingSlash: true,
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
experimental: {
|
||||
esmExternals: 'loose',
|
||||
},
|
||||
webpack: (config, { isServer }) => {
|
||||
if (!isServer) {
|
||||
config.resolve.fallback = {
|
||||
...config.resolve.fallback,
|
||||
fs: false,
|
||||
net: false,
|
||||
tls: false,
|
||||
};
|
||||
}
|
||||
return config;
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
74
AppImage/package.json
Normal file
74
AppImage/package.json
Normal file
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"name": "proxmenux-monitor",
|
||||
"version": "1.0.0",
|
||||
"description": "Proxmox System Monitoring Dashboard",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"export": "next build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.10.0",
|
||||
"@radix-ui/react-accordion": "1.2.2",
|
||||
"@radix-ui/react-alert-dialog": "1.1.4",
|
||||
"@radix-ui/react-aspect-ratio": "1.1.1",
|
||||
"@radix-ui/react-avatar": "1.1.2",
|
||||
"@radix-ui/react-checkbox": "1.1.3",
|
||||
"@radix-ui/react-collapsible": "1.1.2",
|
||||
"@radix-ui/react-context-menu": "2.2.4",
|
||||
"@radix-ui/react-dialog": "1.1.4",
|
||||
"@radix-ui/react-dropdown-menu": "2.1.4",
|
||||
"@radix-ui/react-hover-card": "1.1.4",
|
||||
"@radix-ui/react-label": "2.1.1",
|
||||
"@radix-ui/react-menubar": "1.1.4",
|
||||
"@radix-ui/react-navigation-menu": "1.2.3",
|
||||
"@radix-ui/react-popover": "1.1.4",
|
||||
"@radix-ui/react-progress": "1.1.1",
|
||||
"@radix-ui/react-radio-group": "1.2.2",
|
||||
"@radix-ui/react-scroll-area": "1.2.2",
|
||||
"@radix-ui/react-select": "2.1.4",
|
||||
"@radix-ui/react-separator": "1.1.1",
|
||||
"@radix-ui/react-slider": "1.2.2",
|
||||
"@radix-ui/react-slot": "1.1.1",
|
||||
"@radix-ui/react-switch": "1.1.2",
|
||||
"@radix-ui/react-tabs": "1.1.2",
|
||||
"@radix-ui/react-toast": "1.2.4",
|
||||
"@radix-ui/react-toggle": "1.1.1",
|
||||
"@radix-ui/react-toggle-group": "1.1.1",
|
||||
"@radix-ui/react-tooltip": "1.1.6",
|
||||
"@vercel/analytics": "1.3.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "1.0.4",
|
||||
"date-fns": "4.1.0",
|
||||
"embla-carousel-react": "8.5.1",
|
||||
"geist": "^1.3.1",
|
||||
"input-otp": "1.4.1",
|
||||
"lucide-react": "^0.454.0",
|
||||
"next": "15.1.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19",
|
||||
"react-day-picker": "9.8.0",
|
||||
"react-dom": "^19",
|
||||
"react-hook-form": "^7.60.0",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"recharts": "2.15.4",
|
||||
"sonner": "^1.7.4",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vaul": "^0.9.9",
|
||||
"zod": "3.25.67"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.5",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
9
AppImage/postcss.config.mjs
Normal file
9
AppImage/postcss.config.mjs
Normal file
@@ -0,0 +1,9 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
BIN
AppImage/public/apple-touch-icon.png
Normal file
BIN
AppImage/public/apple-touch-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.9 KiB |
BIN
AppImage/public/favicon.ico
Normal file
BIN
AppImage/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
AppImage/public/images/proxmenux-logo.png
Normal file
BIN
AppImage/public/images/proxmenux-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
16
AppImage/public/manifest.json
Normal file
16
AppImage/public/manifest.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "ProxMenux Monitor",
|
||||
"short_name": "ProxMenux",
|
||||
"description": "Proxmox System Dashboard and Monitor",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#2b2f36",
|
||||
"theme_color": "#2b2f36",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/images/proxmenux-logo.png",
|
||||
"sizes": "256x256",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
45
AppImage/scripts/AppRun
Normal file
45
AppImage/scripts/AppRun
Normal file
@@ -0,0 +1,45 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ProxMenux Monitor AppImage Entry Point
|
||||
# This script is executed when the AppImage is run
|
||||
|
||||
# Get the directory where this AppImage is mounted
|
||||
APPDIR="$(dirname "$(readlink -f "${0}")")"
|
||||
|
||||
export PATH="${APPDIR}/usr/bin:${PATH}"
|
||||
export LD_LIBRARY_PATH="${APPDIR}/usr/lib:${LD_LIBRARY_PATH}"
|
||||
export PYTHONPATH="${APPDIR}/usr/lib/python3/dist-packages:${APPDIR}/usr/lib/python3/site-packages:${PYTHONPATH}"
|
||||
|
||||
# Change to the AppImage directory
|
||||
cd "${APPDIR}"
|
||||
|
||||
# Debug: Print directory structure for troubleshooting
|
||||
echo "[v0] AppImage mounted at: ${APPDIR}"
|
||||
echo "[v0] Contents of AppImage root:"
|
||||
ls -la "${APPDIR}/" || echo "[v0] Cannot list AppImage root"
|
||||
|
||||
echo "[v0] Contents of web directory:"
|
||||
ls -la "${APPDIR}/web/" || echo "[v0] Web directory not found"
|
||||
|
||||
echo "[v0] Looking for index.html:"
|
||||
find "${APPDIR}" -name "index.html" -type f || echo "[v0] No index.html found"
|
||||
|
||||
echo "[v0] Python path: ${PYTHONPATH}"
|
||||
echo "[v0] Checking Flask installation:"
|
||||
python3 -c "import flask; print('Flask version:', flask.__version__)" 2>/dev/null || echo "[v0] Flask not found"
|
||||
|
||||
# Check for translation argument
|
||||
if [[ "$1" == "--translate" ]]; then
|
||||
echo "🌐 Starting ProxMenux Translation Service..."
|
||||
exec python3 "${APPDIR}/usr/bin/translate_cli.py" "${@:2}"
|
||||
else
|
||||
echo "🚀 Starting ProxMenux Monitor Dashboard..."
|
||||
echo "📊 Dashboard will be available at: http://localhost:8008"
|
||||
echo "🔌 API endpoints at: http://localhost:8008/api/"
|
||||
echo ""
|
||||
echo "Press Ctrl+C to stop the server"
|
||||
echo ""
|
||||
|
||||
# Start the Flask server
|
||||
exec python3 "${APPDIR}/usr/bin/flask_server.py"
|
||||
fi
|
338
AppImage/scripts/build_appimage.sh
Normal file
338
AppImage/scripts/build_appimage.sh
Normal file
@@ -0,0 +1,338 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ProxMenux Monitor AppImage Builder
|
||||
# This script creates a single AppImage with Flask server, Next.js dashboard, and translation support
|
||||
|
||||
set -e
|
||||
|
||||
WORK_DIR="/tmp/proxmenux_build"
|
||||
APP_DIR="$WORK_DIR/ProxMenux.AppDir"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
DIST_DIR="$SCRIPT_DIR/../dist"
|
||||
APPIMAGE_ROOT="$SCRIPT_DIR/.."
|
||||
|
||||
VERSION=$(node -p "require('$APPIMAGE_ROOT/package.json').version")
|
||||
APPIMAGE_NAME="ProxMenux-${VERSION}.AppImage"
|
||||
|
||||
echo "🚀 Building ProxMenux Monitor AppImage v${VERSION} with translation support..."
|
||||
|
||||
# Clean and create work directory
|
||||
rm -rf "$WORK_DIR"
|
||||
mkdir -p "$APP_DIR"
|
||||
mkdir -p "$DIST_DIR"
|
||||
|
||||
# Download appimagetool if not exists
|
||||
if [ ! -f "$WORK_DIR/appimagetool" ]; then
|
||||
echo "📥 Downloading appimagetool..."
|
||||
wget -q "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" -O "$WORK_DIR/appimagetool"
|
||||
chmod +x "$WORK_DIR/appimagetool"
|
||||
fi
|
||||
|
||||
# Create directory structure
|
||||
mkdir -p "$APP_DIR/usr/bin"
|
||||
mkdir -p "$APP_DIR/usr/lib/python3/dist-packages"
|
||||
mkdir -p "$APP_DIR/usr/share/applications"
|
||||
mkdir -p "$APP_DIR/usr/share/icons/hicolor/256x256/apps"
|
||||
mkdir -p "$APP_DIR/web"
|
||||
|
||||
echo "🔨 Building Next.js application..."
|
||||
cd "$APPIMAGE_ROOT"
|
||||
if [ ! -f "package.json" ]; then
|
||||
echo "❌ Error: package.json not found in AppImage directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Install dependencies if node_modules doesn't exist
|
||||
if [ ! -d "node_modules" ]; then
|
||||
echo "📦 Installing dependencies..."
|
||||
npm install
|
||||
fi
|
||||
|
||||
echo "🏗️ Building Next.js static export..."
|
||||
npm run export
|
||||
|
||||
echo "🔍 Checking export results..."
|
||||
if [ -d "out" ]; then
|
||||
echo "✅ Export directory found"
|
||||
echo "📁 Contents of out directory:"
|
||||
ls -la out/
|
||||
if [ -f "out/index.html" ]; then
|
||||
echo "✅ index.html found in out directory"
|
||||
else
|
||||
echo "❌ index.html NOT found in out directory"
|
||||
echo "📁 Looking for HTML files:"
|
||||
find out/ -name "*.html" -type f || echo "No HTML files found"
|
||||
fi
|
||||
else
|
||||
echo "❌ Error: Next.js export failed - out directory not found"
|
||||
echo "📁 Current directory contents:"
|
||||
ls -la
|
||||
echo "📁 Looking for any build outputs:"
|
||||
find . -name "*.html" -type f 2>/dev/null || echo "No HTML files found anywhere"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Return to script directory
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# Copy Flask server
|
||||
echo "📋 Copying Flask server..."
|
||||
cp "$SCRIPT_DIR/flask_server.py" "$APP_DIR/usr/bin/"
|
||||
|
||||
echo "📋 Adding translation support..."
|
||||
cat > "$APP_DIR/usr/bin/translate_cli.py" << 'PYEOF'
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
ProxMenux translate CLI
|
||||
stdin JSON -> {"text":"...", "dest_lang":"es", "context":"...", "cache_file":"/usr/local/share/proxmenux/cache.json"}
|
||||
stdout JSON -> {"success":true,"text":"..."} or {"success":false,"error":"..."}
|
||||
"""
|
||||
import sys, json, re
|
||||
from pathlib import Path
|
||||
|
||||
# Ensure embedded site-packages are discoverable
|
||||
HERE = Path(__file__).resolve().parents[2] # .../AppDir
|
||||
DIST = HERE / "usr" / "lib" / "python3" / "dist-packages"
|
||||
SITE = HERE / "usr" / "lib" / "python3" / "site-packages"
|
||||
for p in (str(DIST), str(SITE)):
|
||||
if p not in sys.path:
|
||||
sys.path.insert(0, p)
|
||||
|
||||
# Python 3.13 compat: inline 'cgi' shim
|
||||
try:
|
||||
import cgi
|
||||
except Exception:
|
||||
import types, html
|
||||
def _parse_header(value: str):
|
||||
value = str(value or "")
|
||||
parts = [p.strip() for p in value.split(";")]
|
||||
if not parts:
|
||||
return "", {}
|
||||
key = parts[0].lower()
|
||||
params = {}
|
||||
for item in parts[1:]:
|
||||
if not item:
|
||||
continue
|
||||
if "=" in item:
|
||||
k, v = item.split("=", 1)
|
||||
k = k.strip().lower()
|
||||
v = v.strip().strip('"').strip("'")
|
||||
params[k] = v
|
||||
else:
|
||||
params[item.strip().lower()] = ""
|
||||
return key, params
|
||||
cgi = types.SimpleNamespace(parse_header=_parse_header, escape=html.escape)
|
||||
|
||||
try:
|
||||
from googletrans import Translator
|
||||
except Exception as e:
|
||||
print(json.dumps({"success": False, "error": f"ImportError: {e}"}))
|
||||
sys.exit(0)
|
||||
|
||||
def load_json_stdin():
|
||||
try:
|
||||
return json.load(sys.stdin)
|
||||
except Exception as e:
|
||||
print(json.dumps({"success": False, "error": f"Invalid JSON input: {e}"}))
|
||||
sys.exit(0)
|
||||
|
||||
def ensure_cache(path: Path):
|
||||
try:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not path.exists():
|
||||
path.write_text("{}", encoding="utf-8")
|
||||
json.loads(path.read_text(encoding="utf-8") or "{}")
|
||||
except Exception:
|
||||
path.write_text("{}", encoding="utf-8")
|
||||
|
||||
def read_cache(path: Path):
|
||||
try:
|
||||
return json.loads(path.read_text(encoding="utf-8") or "{}")
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
def write_cache(path: Path, cache: dict):
|
||||
tmp = path.with_suffix(".tmp")
|
||||
tmp.write_text(json.dumps(cache, ensure_ascii=False), encoding="utf-8")
|
||||
tmp.replace(path)
|
||||
|
||||
def clean_translated(s: str) -> str:
|
||||
s = re.sub(r'^.*?(Translate:|Traducir:|Traduire:|Übersetzen:|Tradurre:|Traduzir:|翻译:|翻訳:)', '', s, flags=re.IGNORECASE | re.DOTALL).strip()
|
||||
s = re.sub(r'^.*?(Context:|Contexto:|Contexte:|Kontext:|Contesto:|上下文:|コンテキスト:).*?:', '', s, flags=re.IGNORECASE | re.DOTALL).strip()
|
||||
return s.strip()
|
||||
|
||||
def main():
|
||||
req = load_json_stdin()
|
||||
text = req.get("text", "")
|
||||
dest = req.get("dest_lang", "en") or "en"
|
||||
context = req.get("context", "")
|
||||
cache_file = Path(req.get("cache_file", "")) if req.get("cache_file") else None
|
||||
|
||||
if dest == "en":
|
||||
print(json.dumps({"success": True, "text": text}))
|
||||
return
|
||||
|
||||
cache = {}
|
||||
if cache_file:
|
||||
ensure_cache(cache_file)
|
||||
cache = read_cache(cache_file)
|
||||
if text in cache and (dest in cache[text] or "notranslate" in cache[text]):
|
||||
found = cache[text].get(dest) or cache[text].get("notranslate")
|
||||
print(json.dumps({"success": True, "text": found}))
|
||||
return
|
||||
|
||||
try:
|
||||
full = (context + " " + text).strip() if context else text
|
||||
tr = Translator()
|
||||
result = tr.translate(full, dest=dest).text
|
||||
result = clean_translated(result)
|
||||
|
||||
if cache_file:
|
||||
cache.setdefault(text, {})
|
||||
cache[text][dest] = result
|
||||
write_cache(cache_file, cache)
|
||||
|
||||
print(json.dumps({"success": True, "text": result}))
|
||||
except Exception as e:
|
||||
print(json.dumps({"success": False, "error": str(e)}))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
PYEOF
|
||||
|
||||
chmod +x "$APP_DIR/usr/bin/translate_cli.py"
|
||||
|
||||
# Copy Next.js build
|
||||
echo "📋 Copying web dashboard..."
|
||||
if [ -d "$APPIMAGE_ROOT/out" ]; then
|
||||
mkdir -p "$APP_DIR/web"
|
||||
echo "📁 Copying from $APPIMAGE_ROOT/out to $APP_DIR/web"
|
||||
cp -r "$APPIMAGE_ROOT/out"/* "$APP_DIR/web/"
|
||||
|
||||
if [ -f "$APP_DIR/web/index.html" ]; then
|
||||
echo "✅ index.html copied successfully to $APP_DIR/web/"
|
||||
else
|
||||
echo "❌ index.html NOT found after copying"
|
||||
echo "📁 Contents of $APP_DIR/web:"
|
||||
ls -la "$APP_DIR/web/" || echo "Directory is empty or doesn't exist"
|
||||
fi
|
||||
|
||||
if [ -d "$APPIMAGE_ROOT/public" ]; then
|
||||
cp -r "$APPIMAGE_ROOT/public"/* "$APP_DIR/web/" 2>/dev/null || true
|
||||
fi
|
||||
cp "$APPIMAGE_ROOT/package.json" "$APP_DIR/web/"
|
||||
|
||||
echo "✅ Next.js static export copied successfully"
|
||||
else
|
||||
echo "❌ Error: Next.js export not found even after building"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Copy AppRun script
|
||||
echo "📋 Copying AppRun script..."
|
||||
if [ -f "$SCRIPT_DIR/AppRun" ]; then
|
||||
cp "$SCRIPT_DIR/AppRun" "$APP_DIR/AppRun"
|
||||
chmod +x "$APP_DIR/AppRun"
|
||||
echo "✅ AppRun script copied successfully"
|
||||
else
|
||||
echo "❌ Error: AppRun script not found at $SCRIPT_DIR/AppRun"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create desktop file
|
||||
cat > "$APP_DIR/proxmenux-monitor.desktop" << EOF
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=ProxMenux Monitor
|
||||
Comment=Proxmox System Monitoring Dashboard with Translation Support
|
||||
Exec=AppRun
|
||||
Icon=proxmenux-monitor
|
||||
Categories=System;Monitor;
|
||||
Terminal=false
|
||||
StartupNotify=true
|
||||
EOF
|
||||
|
||||
# Copy desktop file to applications directory
|
||||
cp "$APP_DIR/proxmenux-monitor.desktop" "$APP_DIR/usr/share/applications/"
|
||||
|
||||
# Download and set icon
|
||||
echo "🎨 Setting up icon..."
|
||||
if [ -f "$APPIMAGE_ROOT/public/images/proxmenux-logo.png" ]; then
|
||||
cp "$APPIMAGE_ROOT/public/images/proxmenux-logo.png" "$APP_DIR/proxmenux-monitor.png"
|
||||
else
|
||||
wget -q "https://raw.githubusercontent.com/MacRimi/ProxMenux/main/images/logo.png" -O "$APP_DIR/proxmenux-monitor.png" || {
|
||||
echo "⚠️ Could not download logo, creating placeholder..."
|
||||
convert -size 256x256 xc:blue -fill white -gravity center -pointsize 24 -annotate +0+0 "PM" "$APP_DIR/proxmenux-monitor.png" 2>/dev/null || {
|
||||
echo "⚠️ ImageMagick not available, skipping icon creation"
|
||||
}
|
||||
}
|
||||
fi
|
||||
|
||||
if [ -f "$APP_DIR/proxmenux-monitor.png" ]; then
|
||||
cp "$APP_DIR/proxmenux-monitor.png" "$APP_DIR/usr/share/icons/hicolor/256x256/apps/"
|
||||
fi
|
||||
|
||||
echo "📦 Installing Python dependencies..."
|
||||
pip3 install --target "$APP_DIR/usr/lib/python3/dist-packages" \
|
||||
flask \
|
||||
flask-cors \
|
||||
psutil \
|
||||
requests \
|
||||
googletrans==4.0.0-rc1 \
|
||||
httpx==0.13.3 \
|
||||
httpcore==0.9.1 \
|
||||
beautifulsoup4
|
||||
|
||||
cat > "$APP_DIR/usr/lib/python3/dist-packages/cgi.py" << 'PYEOF'
|
||||
from typing import Tuple, Dict
|
||||
try:
|
||||
from html import escape as _html_escape
|
||||
except Exception:
|
||||
def _html_escape(s, quote=True): return s
|
||||
|
||||
__all__ = ["parse_header", "escape"]
|
||||
|
||||
def escape(s, quote=True):
|
||||
return _html_escape(s, quote=quote)
|
||||
|
||||
def parse_header(value: str) -> Tuple[str, Dict[str, str]]:
|
||||
if not isinstance(value, str):
|
||||
value = str(value or "")
|
||||
parts = [p.strip() for p in value.split(";")]
|
||||
if not parts:
|
||||
return "", {}
|
||||
key = parts[0].lower()
|
||||
params: Dict[str, str] = {}
|
||||
for item in parts[1:]:
|
||||
if not item:
|
||||
continue
|
||||
if "=" in item:
|
||||
k, v = item.split("=", 1)
|
||||
k = k.strip().lower()
|
||||
v = v.strip().strip('"').strip("'")
|
||||
params[k] = v
|
||||
else:
|
||||
params[item.strip().lower()] = ""
|
||||
return key, params
|
||||
PYEOF
|
||||
|
||||
# Build AppImage
|
||||
echo "🔨 Building unified AppImage v${VERSION}..."
|
||||
cd "$WORK_DIR"
|
||||
export NO_CLEANUP=1
|
||||
export APPIMAGE_EXTRACT_AND_RUN=1
|
||||
ARCH=x86_64 ./appimagetool --no-appstream --verbose "$APP_DIR" "$APPIMAGE_NAME"
|
||||
|
||||
# Move to dist directory
|
||||
mv "$APPIMAGE_NAME" "$DIST_DIR/"
|
||||
|
||||
echo "✅ Unified AppImage created: $DIST_DIR/$APPIMAGE_NAME"
|
||||
echo ""
|
||||
echo "📋 Usage:"
|
||||
echo " Dashboard: ./$APPIMAGE_NAME"
|
||||
echo " Translation: ./$APPIMAGE_NAME --translate"
|
||||
echo ""
|
||||
echo "🚀 Installation:"
|
||||
echo " sudo cp $DIST_DIR/$APPIMAGE_NAME /usr/local/bin/proxmenux-monitor"
|
||||
echo " sudo chmod +x /usr/local/bin/proxmenux-monitor"
|
582
AppImage/scripts/flask_server.py
Normal file
582
AppImage/scripts/flask_server.py
Normal file
@@ -0,0 +1,582 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ProxMenux Flask Server
|
||||
Provides REST API endpoints for Proxmox monitoring data
|
||||
Runs on port 8008 and serves system metrics, storage info, network stats, etc.
|
||||
Also serves the Next.js dashboard as static files
|
||||
"""
|
||||
|
||||
from flask import Flask, jsonify, request, send_from_directory, send_file
|
||||
from flask_cors import CORS
|
||||
import psutil
|
||||
import subprocess
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import socket
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app) # Enable CORS for Next.js frontend
|
||||
|
||||
@app.route('/')
|
||||
def serve_dashboard():
|
||||
"""Serve the main dashboard page from Next.js build"""
|
||||
try:
|
||||
# Detectar si estamos ejecutándose desde AppImage
|
||||
appimage_root = os.environ.get('APPDIR')
|
||||
if not appimage_root:
|
||||
# Fallback: intentar detectar desde la ubicación del script
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
appimage_root = os.path.dirname(base_dir) # Subir un nivel desde usr/bin/
|
||||
|
||||
index_paths = [
|
||||
os.path.join(appimage_root, 'web', 'index.html'), # Ruta principal para AppImage
|
||||
os.path.join(appimage_root, 'usr', 'web', 'index.html'), # Fallback con usr/
|
||||
os.path.join(appimage_root, 'web', 'out', 'index.html'), # Fallback si está en subcarpeta
|
||||
os.path.join(appimage_root, 'usr', 'web', 'out', 'index.html'), # Fallback con usr/out/
|
||||
]
|
||||
|
||||
print(f"[v0] Flask server looking for index.html in:")
|
||||
for path in index_paths:
|
||||
abs_path = os.path.abspath(path)
|
||||
exists = os.path.exists(abs_path)
|
||||
print(f"[v0] {abs_path} - {'EXISTS' if exists else 'NOT FOUND'}")
|
||||
if exists:
|
||||
print(f"[v0] Found index.html, serving from: {abs_path}")
|
||||
return send_file(abs_path)
|
||||
|
||||
# If no Next.js build found, return error message with actual paths checked
|
||||
actual_paths = [os.path.abspath(path) for path in index_paths]
|
||||
return f'''
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>ProxMenux Monitor - Build Error</title></head>
|
||||
<body style="font-family: Arial; padding: 2rem; background: #0a0a0a; color: #fff;">
|
||||
<h1>🚨 ProxMenux Monitor - Build Error</h1>
|
||||
<p>Next.js application not found. The AppImage may not have been built correctly.</p>
|
||||
<p>Expected paths checked:</p>
|
||||
<ul>{''.join([f'<li>{path}</li>' for path in actual_paths])}</ul>
|
||||
<p>API endpoints are still available:</p>
|
||||
<ul>
|
||||
<li><a href="/api/system" style="color: #4f46e5;">/api/system</a></li>
|
||||
<li><a href="/api/system-info" style="color: #4f46e5;">/api/system-info</a></li>
|
||||
<li><a href="/api/storage" style="color: #4f46e5;">/api/storage</a></li>
|
||||
<li><a href="/api/network" style="color: #4f46e5;">/api/network</a></li>
|
||||
<li><a href="/api/vms" style="color: #4f46e5;">/api/vms</a></li>
|
||||
<li><a href="/api/health" style="color: #4f46e5;">/api/health</a></li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
''', 500
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error serving dashboard: {e}")
|
||||
return jsonify({'error': f'Dashboard not available: {str(e)}'}), 500
|
||||
|
||||
@app.route('/manifest.json')
|
||||
def serve_manifest():
|
||||
"""Serve PWA manifest"""
|
||||
try:
|
||||
manifest_paths = [
|
||||
os.path.join(os.path.dirname(__file__), '..', 'web', 'public', 'manifest.json'),
|
||||
os.path.join(os.path.dirname(__file__), '..', 'public', 'manifest.json')
|
||||
]
|
||||
|
||||
for manifest_path in manifest_paths:
|
||||
if os.path.exists(manifest_path):
|
||||
return send_file(manifest_path)
|
||||
|
||||
# Return default manifest if not found
|
||||
return jsonify({
|
||||
"name": "ProxMenux Monitor",
|
||||
"short_name": "ProxMenux",
|
||||
"description": "Proxmox System Monitoring Dashboard",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#0a0a0a",
|
||||
"theme_color": "#4f46e5",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/images/proxmenux-logo.png",
|
||||
"sizes": "256x256",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"Error serving manifest: {e}")
|
||||
return jsonify({}), 404
|
||||
|
||||
@app.route('/sw.js')
|
||||
def serve_sw():
|
||||
"""Serve service worker"""
|
||||
return '''
|
||||
const CACHE_NAME = 'proxmenux-v1';
|
||||
const urlsToCache = [
|
||||
'/',
|
||||
'/api/system',
|
||||
'/api/storage',
|
||||
'/api/network',
|
||||
'/api/health'
|
||||
];
|
||||
|
||||
self.addEventListener('install', event => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME)
|
||||
.then(cache => cache.addAll(urlsToCache))
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', event => {
|
||||
event.respondWith(
|
||||
caches.match(event.request)
|
||||
.then(response => response || fetch(event.request))
|
||||
);
|
||||
});
|
||||
''', 200, {'Content-Type': 'application/javascript'}
|
||||
|
||||
@app.route('/_next/<path:filename>')
|
||||
def serve_next_static(filename):
|
||||
"""Serve Next.js static files"""
|
||||
try:
|
||||
appimage_root = os.environ.get('APPDIR')
|
||||
if not appimage_root:
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
appimage_root = os.path.dirname(base_dir)
|
||||
|
||||
static_paths = [
|
||||
os.path.join(appimage_root, 'web', '_next'), # Ruta principal
|
||||
os.path.join(appimage_root, 'usr', 'web', '_next'), # Fallback con usr/
|
||||
os.path.join(appimage_root, 'web', 'out', '_next'), # Fallback con out/
|
||||
os.path.join(appimage_root, 'usr', 'web', 'out', '_next'), # Fallback con usr/out/
|
||||
]
|
||||
|
||||
for static_dir in static_paths:
|
||||
file_path = os.path.join(static_dir, filename)
|
||||
if os.path.exists(file_path):
|
||||
return send_file(file_path)
|
||||
return '', 404
|
||||
except Exception as e:
|
||||
print(f"Error serving Next.js static file {filename}: {e}")
|
||||
return '', 404
|
||||
|
||||
@app.route('/<path:filename>')
|
||||
def serve_static_files(filename):
|
||||
"""Serve static files (icons, etc.)"""
|
||||
try:
|
||||
appimage_root = os.environ.get('APPDIR')
|
||||
if not appimage_root:
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
appimage_root = os.path.dirname(base_dir)
|
||||
|
||||
public_paths = [
|
||||
os.path.join(appimage_root, 'web'), # Raíz web para exportación estática
|
||||
os.path.join(appimage_root, 'usr', 'web'), # Fallback con usr/
|
||||
os.path.join(appimage_root, 'web', 'out'), # Fallback con out/
|
||||
os.path.join(appimage_root, 'usr', 'web', 'out'), # Fallback con usr/out/
|
||||
]
|
||||
|
||||
for public_dir in public_paths:
|
||||
file_path = os.path.join(public_dir, filename)
|
||||
if os.path.exists(file_path):
|
||||
return send_from_directory(public_dir, filename)
|
||||
|
||||
return '', 404
|
||||
except Exception as e:
|
||||
print(f"Error serving static file {filename}: {e}")
|
||||
return '', 404
|
||||
|
||||
@app.route('/images/<path:filename>')
|
||||
def serve_images(filename):
|
||||
"""Serve image files"""
|
||||
try:
|
||||
appimage_root = os.environ.get('APPDIR')
|
||||
if not appimage_root:
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
appimage_root = os.path.dirname(base_dir)
|
||||
|
||||
image_paths = [
|
||||
os.path.join(appimage_root, 'web', 'images'), # Ruta principal para exportación estática
|
||||
os.path.join(appimage_root, 'usr', 'web', 'images'), # Fallback con usr/
|
||||
os.path.join(appimage_root, 'web', 'public', 'images'), # Ruta con public/
|
||||
os.path.join(appimage_root, 'usr', 'web', 'public', 'images'), # Fallback usr/public/
|
||||
os.path.join(appimage_root, 'public', 'images'), # Ruta directa a public
|
||||
os.path.join(appimage_root, 'usr', 'public', 'images'), # Fallback usr/public
|
||||
]
|
||||
|
||||
print(f"[v0] Looking for image: {filename}")
|
||||
for image_dir in image_paths:
|
||||
file_path = os.path.join(image_dir, filename)
|
||||
abs_path = os.path.abspath(file_path)
|
||||
exists = os.path.exists(abs_path)
|
||||
print(f"[v0] Checking: {abs_path} - {'FOUND' if exists else 'NOT FOUND'}")
|
||||
if exists:
|
||||
print(f"[v0] Serving image from: {abs_path}")
|
||||
return send_from_directory(image_dir, filename)
|
||||
|
||||
print(f"[v0] Image not found: {filename}")
|
||||
return '', 404
|
||||
except Exception as e:
|
||||
print(f"Error serving image {filename}: {e}")
|
||||
return '', 404
|
||||
|
||||
def get_system_info():
|
||||
"""Get basic system information"""
|
||||
try:
|
||||
# CPU usage
|
||||
cpu_percent = psutil.cpu_percent(interval=1)
|
||||
|
||||
# Memory usage
|
||||
memory = psutil.virtual_memory()
|
||||
|
||||
temp = 0
|
||||
try:
|
||||
if hasattr(psutil, "sensors_temperatures"):
|
||||
temps = psutil.sensors_temperatures()
|
||||
if temps:
|
||||
# Priority order for temperature sensors
|
||||
sensor_priority = ['coretemp', 'cpu_thermal', 'acpi', 'thermal_zone']
|
||||
for sensor_name in sensor_priority:
|
||||
if sensor_name in temps and temps[sensor_name]:
|
||||
temp = temps[sensor_name][0].current
|
||||
break
|
||||
|
||||
# If no priority sensor found, use first available
|
||||
if temp == 0:
|
||||
for name, entries in temps.items():
|
||||
if entries:
|
||||
temp = entries[0].current
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"Error reading temperature sensors: {e}")
|
||||
temp = 0 # Use 0 to indicate no temperature available
|
||||
|
||||
# Uptime
|
||||
boot_time = psutil.boot_time()
|
||||
uptime_seconds = time.time() - boot_time
|
||||
uptime_str = str(timedelta(seconds=int(uptime_seconds)))
|
||||
|
||||
# Load average
|
||||
load_avg = os.getloadavg() if hasattr(os, 'getloadavg') else [0, 0, 0]
|
||||
|
||||
hostname = socket.gethostname()
|
||||
node_id = f"pve-{hostname}"
|
||||
|
||||
# Try to get Proxmox node info if available
|
||||
try:
|
||||
result = subprocess.run(['pvesh', 'get', '/nodes', '--output-format', 'json'],
|
||||
capture_output=True, text=True, timeout=5)
|
||||
if result.returncode == 0:
|
||||
nodes = json.loads(result.stdout)
|
||||
if nodes and len(nodes) > 0:
|
||||
node_id = nodes[0].get('node', node_id)
|
||||
except Exception as e:
|
||||
print(f"Note: pvesh not available or failed: {e}")
|
||||
pass # Use default if pvesh not available
|
||||
|
||||
return {
|
||||
'cpu_usage': round(cpu_percent, 1),
|
||||
'memory_usage': round(memory.percent, 1),
|
||||
'memory_total': round(memory.total / (1024**3), 1), # GB
|
||||
'memory_used': round(memory.used / (1024**3), 1), # GB
|
||||
'temperature': temp,
|
||||
'uptime': uptime_str,
|
||||
'load_average': list(load_avg),
|
||||
'hostname': hostname,
|
||||
'node_id': node_id,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Critical error getting system info: {e}")
|
||||
return {
|
||||
'error': f'Unable to access system information: {str(e)}',
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
def get_storage_info():
|
||||
"""Get storage and disk information"""
|
||||
try:
|
||||
storage_data = {
|
||||
'total': 0,
|
||||
'used': 0,
|
||||
'available': 0,
|
||||
'disks': []
|
||||
}
|
||||
|
||||
# Get disk usage for root partition
|
||||
disk_usage = psutil.disk_usage('/')
|
||||
storage_data['total'] = round(disk_usage.total / (1024**3), 1) # GB
|
||||
storage_data['used'] = round(disk_usage.used / (1024**3), 1) # GB
|
||||
storage_data['available'] = round(disk_usage.free / (1024**3), 1) # GB
|
||||
|
||||
# Get individual disk information
|
||||
disk_partitions = psutil.disk_partitions()
|
||||
for partition in disk_partitions:
|
||||
try:
|
||||
partition_usage = psutil.disk_usage(partition.mountpoint)
|
||||
|
||||
disk_temp = 0
|
||||
try:
|
||||
# Try to get disk temperature from sensors
|
||||
if hasattr(psutil, "sensors_temperatures"):
|
||||
temps = psutil.sensors_temperatures()
|
||||
if temps:
|
||||
for name, entries in temps.items():
|
||||
if 'disk' in name.lower() or 'hdd' in name.lower() or 'sda' in name.lower():
|
||||
if entries:
|
||||
disk_temp = entries[0].current
|
||||
break
|
||||
except:
|
||||
pass
|
||||
|
||||
disk_info = {
|
||||
'name': partition.device,
|
||||
'mountpoint': partition.mountpoint,
|
||||
'fstype': partition.fstype,
|
||||
'total': round(partition_usage.total / (1024**3), 1),
|
||||
'used': round(partition_usage.used / (1024**3), 1),
|
||||
'available': round(partition_usage.free / (1024**3), 1),
|
||||
'usage_percent': round((partition_usage.used / partition_usage.total) * 100, 1),
|
||||
'health': 'unknown', # Would need SMART data for real health
|
||||
'temperature': disk_temp
|
||||
}
|
||||
storage_data['disks'].append(disk_info)
|
||||
except PermissionError:
|
||||
print(f"Permission denied accessing {partition.mountpoint}")
|
||||
continue
|
||||
except Exception as e:
|
||||
print(f"Error accessing partition {partition.device}: {e}")
|
||||
continue
|
||||
|
||||
if not storage_data['disks'] and storage_data['total'] == 0:
|
||||
return {
|
||||
'error': 'No storage data available - unable to access disk information',
|
||||
'total': 0,
|
||||
'used': 0,
|
||||
'available': 0,
|
||||
'disks': []
|
||||
}
|
||||
|
||||
return storage_data
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting storage info: {e}")
|
||||
return {
|
||||
'error': f'Unable to access storage information: {str(e)}',
|
||||
'total': 0,
|
||||
'used': 0,
|
||||
'available': 0,
|
||||
'disks': []
|
||||
}
|
||||
|
||||
def get_network_info():
|
||||
"""Get network interface information"""
|
||||
try:
|
||||
network_data = {
|
||||
'interfaces': [],
|
||||
'traffic': {'incoming': 0, 'outgoing': 0}
|
||||
}
|
||||
|
||||
# Get network interfaces
|
||||
net_if_addrs = psutil.net_if_addrs()
|
||||
net_if_stats = psutil.net_if_stats()
|
||||
|
||||
for interface_name, interface_addresses in net_if_addrs.items():
|
||||
if interface_name == 'lo': # Skip loopback
|
||||
continue
|
||||
|
||||
interface_info = {
|
||||
'name': interface_name,
|
||||
'status': 'up' if net_if_stats[interface_name].isup else 'down',
|
||||
'addresses': []
|
||||
}
|
||||
|
||||
for address in interface_addresses:
|
||||
if address.family == 2: # IPv4
|
||||
interface_info['addresses'].append({
|
||||
'ip': address.address,
|
||||
'netmask': address.netmask
|
||||
})
|
||||
|
||||
network_data['interfaces'].append(interface_info)
|
||||
|
||||
# Get network I/O statistics
|
||||
net_io = psutil.net_io_counters()
|
||||
network_data['traffic'] = {
|
||||
'bytes_sent': net_io.bytes_sent,
|
||||
'bytes_recv': net_io.bytes_recv,
|
||||
'packets_sent': net_io.packets_sent,
|
||||
'packets_recv': net_io.packets_recv
|
||||
}
|
||||
|
||||
return network_data
|
||||
except Exception as e:
|
||||
print(f"Error getting network info: {e}")
|
||||
return {
|
||||
'error': f'Unable to access network information: {str(e)}',
|
||||
'interfaces': [],
|
||||
'traffic': {'bytes_sent': 0, 'bytes_recv': 0, 'packets_sent': 0, 'packets_recv': 0}
|
||||
}
|
||||
|
||||
def get_proxmox_vms():
|
||||
"""Get Proxmox VM information (requires pvesh command)"""
|
||||
try:
|
||||
# Try to get VM list using pvesh command
|
||||
result = subprocess.run(['pvesh', 'get', '/nodes/localhost/qemu', '--output-format', 'json'],
|
||||
capture_output=True, text=True, timeout=10)
|
||||
|
||||
if result.returncode == 0:
|
||||
vms = json.loads(result.stdout)
|
||||
return vms
|
||||
else:
|
||||
return {
|
||||
'error': 'pvesh command not available or failed - Proxmox API not accessible',
|
||||
'vms': []
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Error getting VM info: {e}")
|
||||
return {
|
||||
'error': f'Unable to access VM information: {str(e)}',
|
||||
'vms': []
|
||||
}
|
||||
|
||||
@app.route('/api/system', methods=['GET'])
|
||||
def api_system():
|
||||
"""Get system information"""
|
||||
return jsonify(get_system_info())
|
||||
|
||||
@app.route('/api/storage', methods=['GET'])
|
||||
def api_storage():
|
||||
"""Get storage information"""
|
||||
return jsonify(get_storage_info())
|
||||
|
||||
@app.route('/api/network', methods=['GET'])
|
||||
def api_network():
|
||||
"""Get network information"""
|
||||
return jsonify(get_network_info())
|
||||
|
||||
@app.route('/api/vms', methods=['GET'])
|
||||
def api_vms():
|
||||
"""Get virtual machine information"""
|
||||
return jsonify(get_proxmox_vms())
|
||||
|
||||
@app.route('/api/logs', methods=['GET'])
|
||||
def api_logs():
|
||||
"""Get system logs"""
|
||||
try:
|
||||
# Get recent system logs
|
||||
result = subprocess.run(['journalctl', '-n', '100', '--output', 'json'],
|
||||
capture_output=True, text=True, timeout=10)
|
||||
|
||||
if result.returncode == 0:
|
||||
logs = []
|
||||
for line in result.stdout.strip().split('\n'):
|
||||
if line:
|
||||
try:
|
||||
log_entry = json.loads(line)
|
||||
logs.append({
|
||||
'timestamp': log_entry.get('__REALTIME_TIMESTAMP', ''),
|
||||
'level': log_entry.get('PRIORITY', '6'),
|
||||
'service': log_entry.get('_SYSTEMD_UNIT', 'system'),
|
||||
'message': log_entry.get('MESSAGE', ''),
|
||||
'source': 'journalctl'
|
||||
})
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
return jsonify(logs)
|
||||
else:
|
||||
return jsonify({
|
||||
'error': 'journalctl not available or failed',
|
||||
'logs': []
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"Error getting logs: {e}")
|
||||
return jsonify({
|
||||
'error': f'Unable to access system logs: {str(e)}',
|
||||
'logs': []
|
||||
})
|
||||
|
||||
@app.route('/api/health', methods=['GET'])
|
||||
def api_health():
|
||||
"""Health check endpoint"""
|
||||
return jsonify({
|
||||
'status': 'healthy',
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'version': '1.0.0'
|
||||
})
|
||||
|
||||
@app.route('/api/system-info', methods=['GET'])
|
||||
def api_system_info():
|
||||
"""Get system and node information for dashboard header"""
|
||||
try:
|
||||
hostname = socket.gethostname()
|
||||
node_id = f"pve-{hostname}"
|
||||
pve_version = None
|
||||
|
||||
# Try to get Proxmox version
|
||||
try:
|
||||
result = subprocess.run(['pveversion'], capture_output=True, text=True, timeout=5)
|
||||
if result.returncode == 0:
|
||||
pve_version = result.stdout.strip().split('\n')[0]
|
||||
except:
|
||||
pass
|
||||
|
||||
# Try to get node info from Proxmox API
|
||||
try:
|
||||
result = subprocess.run(['pvesh', 'get', '/nodes', '--output-format', 'json'],
|
||||
capture_output=True, text=True, timeout=5)
|
||||
if result.returncode == 0:
|
||||
nodes = json.loads(result.stdout)
|
||||
if nodes and len(nodes) > 0:
|
||||
node_info = nodes[0]
|
||||
node_id = node_info.get('node', node_id)
|
||||
hostname = node_info.get('node', hostname)
|
||||
except:
|
||||
pass
|
||||
|
||||
response = {
|
||||
'hostname': hostname,
|
||||
'node_id': node_id,
|
||||
'status': 'online',
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
if pve_version:
|
||||
response['pve_version'] = pve_version
|
||||
else:
|
||||
response['error'] = 'Proxmox version not available - pveversion command not found'
|
||||
|
||||
return jsonify(response)
|
||||
except Exception as e:
|
||||
print(f"Error getting system info: {e}")
|
||||
return jsonify({
|
||||
'error': f'Unable to access system information: {str(e)}',
|
||||
'hostname': socket.gethostname(),
|
||||
'status': 'error',
|
||||
'timestamp': datetime.now().isoformat()
|
||||
})
|
||||
|
||||
@app.route('/api/info', methods=['GET'])
|
||||
def api_info():
|
||||
"""Root endpoint with API information"""
|
||||
return jsonify({
|
||||
'name': 'ProxMenux Monitor API',
|
||||
'version': '1.0.0',
|
||||
'endpoints': [
|
||||
'/api/system',
|
||||
'/api/system-info',
|
||||
'/api/storage',
|
||||
'/api/network',
|
||||
'/api/vms',
|
||||
'/api/logs',
|
||||
'/api/health'
|
||||
]
|
||||
})
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("Starting ProxMenux Flask Server on port 8008...")
|
||||
print("Server will be accessible on all network interfaces (0.0.0.0:8008)")
|
||||
print("API endpoints available at: /api/system, /api/storage, /api/network, /api/vms, /api/logs, /api/health")
|
||||
|
||||
app.run(host='0.0.0.0', port=8008, debug=False)
|
89
AppImage/tailwind.config.js
Normal file
89
AppImage/tailwind.config.js
Normal file
@@ -0,0 +1,89 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: ["class"],
|
||||
content: ["./pages/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}", "./app/**/*.{ts,tsx}", "./src/**/*.{ts,tsx}"],
|
||||
prefix: "",
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
chart: {
|
||||
1: "hsl(var(--chart-1))",
|
||||
2: "hsl(var(--chart-2))",
|
||||
3: "hsl(var(--chart-3))",
|
||||
4: "hsl(var(--chart-4))",
|
||||
5: "hsl(var(--chart-5))",
|
||||
},
|
||||
sidebar: {
|
||||
DEFAULT: "hsl(var(--sidebar-background))",
|
||||
foreground: "hsl(var(--sidebar-foreground))",
|
||||
primary: "hsl(var(--sidebar-primary))",
|
||||
"primary-foreground": "hsl(var(--sidebar-primary-foreground))",
|
||||
accent: "hsl(var(--sidebar-accent))",
|
||||
"accent-foreground": "hsl(var(--sidebar-accent-foreground))",
|
||||
border: "hsl(var(--sidebar-border))",
|
||||
ring: "hsl(var(--sidebar-ring))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: "0" },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
}
|
27
AppImage/tsconfig.json
Normal file
27
AppImage/tsconfig.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "es6"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
17
CHANGELOG.md
17
CHANGELOG.md
@@ -4,22 +4,23 @@
|
||||
|
||||

|
||||
|
||||
|
||||
### Added
|
||||
|
||||
- **New Menu: Mount and Share Manager**
|
||||
Introduced a comprehensive new menu for managing shared resources between Proxmox host and LXC containers:
|
||||
|
||||
**Host Configuration Options:**
|
||||
1. **Configure NFS Shared on Host** - Add, view, and remove NFS shared resources on the Proxmox server with automatic export management
|
||||
2. **Configure Samba Shared on Host** - Add, view, and remove Samba/CIFS shared resources on the Proxmox server with share configuration
|
||||
3. **Configure Local Shared on Host** - Create and manage local shared directories with proper permissions on the Proxmox host
|
||||
- **Configure NFS Shared on Host** - Add, view, and remove NFS shared resources on the Proxmox server with automatic export management
|
||||
- **Configure Samba Shared on Host** - Add, view, and remove Samba/CIFS shared resources on the Proxmox server with share configuration
|
||||
- **Configure Local Shared on Host** - Create and manage local shared directories with proper permissions on the Proxmox host
|
||||
|
||||
**LXC Integration Options:**
|
||||
4. **Configure LXC Mount Points (Host ↔ Container)** - **Core feature** that enables mounting host directories into LXC containers with automatic permission handling. Includes the ability to **view existing mount points** for each container in a clear, organized way and **remove mount points** with proper verification that the process completed successfully. Especially optimized for **unprivileged containers** where UID/GID mapping is critical.
|
||||
5. **Configure NFS Client in LXC** - Set up NFS client inside privileged containers
|
||||
6. **Configure Samba Client in LXC** - Set up Samba client inside privileged containers
|
||||
7. **Configure NFS Server in LXC** - Install NFS server inside privileged containers
|
||||
8. **Configure Samba Server in LXC** - Install Samba server inside privileged containers
|
||||
- **Configure LXC Mount Points (Host ↔ Container)** - **Core feature** that enables mounting host directories into LXC containers with automatic permission handling. Includes the ability to **view existing mount points** for each container in a clear, organized way and **remove mount points** with proper verification that the process completed successfully. Especially optimized for **unprivileged containers** where UID/GID mapping is critical.
|
||||
- **Configure NFS Client in LXC** - Set up NFS client inside privileged containers
|
||||
- **Configure Samba Client in LXC** - Set up Samba client inside privileged containers
|
||||
- **Configure NFS Server in LXC** - Install NFS server inside privileged containers
|
||||
- **Configure Samba Server in LXC** - Install Samba server inside privileged containers
|
||||
|
||||
**Documentation & Support:**
|
||||
- **Help & Info (commands)** - Comprehensive guides with step-by-step manual instructions for all sharing scenarios
|
||||
|
10
README.md
10
README.md
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
<img src="https://github.com/MacRimi/ProxMenux/blob/main/images/main.png"
|
||||
alt="ProxMenu Logo"
|
||||
alt="ProxMenux Logo"
|
||||
style="max-width: 100%; height: auto;" >
|
||||
|
||||
</div>
|
||||
@@ -70,6 +70,12 @@ Then, follow the on-screen options to manage your Proxmox server efficiently.
|
||||
## ⭐ Support the Project!
|
||||
If you find **ProxMenux** useful, consider giving it a ⭐ on GitHub to help others discover it!
|
||||
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://www.star-history.com/#MacRimi/ProxMenux&Date)
|
||||
|
||||
|
||||
<div style="display: flex; justify-content: center; align-items: center;">
|
||||
<a href="https://ko-fi.com/G2G313ECAN" target="_blank" style="display: flex; align-items: center; text-decoration: none;">
|
||||
<img src="https://raw.githubusercontent.com/MacRimi/HWEncoderX/main/images/kofi.png" alt="Support me on Ko-fi" style="width:140px; margin-right:40px;"/>
|
||||
@@ -77,5 +83,3 @@ If you find **ProxMenux** useful, consider giving it a ⭐ on GitHub to help oth
|
||||
</div>
|
||||
|
||||
Support the project on Ko-fi!
|
||||
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
@@ -101,7 +101,7 @@ check_existing_installation() {
|
||||
fi
|
||||
}
|
||||
|
||||
uninstall_proxmenu() {
|
||||
uninstall_proxmenux() {
|
||||
local install_type="$1"
|
||||
local force_clean="$2"
|
||||
|
||||
@@ -168,7 +168,7 @@ handle_installation_change() {
|
||||
if whiptail --title "Installation Type Change" \
|
||||
--yesno "Switch from Translation to Normal Version?\n\nThis will remove translation components." 10 60; then
|
||||
echo "Preparing for installation type change..."
|
||||
uninstall_proxmenu "translation" "force" >/dev/null 2>&1
|
||||
uninstall_proxmenux "translation" "force" >/dev/null 2>&1
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
|
106
json/cache.json
106
json/cache.json
@@ -202,7 +202,7 @@
|
||||
"it": "Giapponese",
|
||||
"pt": "Japonês"
|
||||
},
|
||||
"Thank you for using ProxMenu. Goodbye!": {
|
||||
"Thank you for using ProxMenux. Goodbye!": {
|
||||
"es": "Gracias por usar ProxMenu. ¡Adiós!",
|
||||
"fr": "Merci d'avoir utilisé ProxMenu. Au revoir!",
|
||||
"de": "Danke für die Nutzung von ProxMenu. Auf Wiedersehen!",
|
||||
@@ -900,7 +900,11 @@
|
||||
"it": "Trova il tuo dispositivo usando https://finds.synology.com"
|
||||
},
|
||||
"Help and Info Commands": {
|
||||
"es": "Comandos de ayuda e información"
|
||||
"es": "Comandos de ayuda e información",
|
||||
"fr": "Aide et Informations (commandes)",
|
||||
"de": "Hilfe & Informationen (Befehle)",
|
||||
"it": "Aiuto e Informazioni (comandi)",
|
||||
"pt": "Ajuda e Informações (comandos)"
|
||||
},
|
||||
"Create VM from template or script": {
|
||||
"es": "Crear VM a partir de plantilla o script",
|
||||
@@ -2566,5 +2570,103 @@
|
||||
"de": "Notfallwiederherstellung:",
|
||||
"it": "Ripristino di emergenza:",
|
||||
"pt": "Recuperação de emergência:"
|
||||
},
|
||||
"Mount and Share Manager": {
|
||||
"es": "Montajes y Recursos Compartidos",
|
||||
"fr": "Gestionnaire de Montage et Partage",
|
||||
"de": "Mount- und Share-Manager",
|
||||
"it": "Gestore di Mount e Condivisioni",
|
||||
"pt": "Gerenciador de Montagem e Compartilhamento"
|
||||
},
|
||||
"HOST": {
|
||||
"es": "HOST",
|
||||
"fr": "HÔTE",
|
||||
"de": "HOST",
|
||||
"it": "HOST",
|
||||
"pt": "HOST"
|
||||
},
|
||||
"Configure NFS shared on Host": {
|
||||
"es": "Configurar recursos NFS compartidos en el Host",
|
||||
"fr": "Configurer les partages NFS sur l'hôte",
|
||||
"de": "NFS-Freigaben auf Host konfigurieren",
|
||||
"it": "Configurare condivisioni NFS su Host",
|
||||
"pt": "Configurar compartilhamentos NFS no Host"
|
||||
},
|
||||
"Configure Samba shared on Host": {
|
||||
"es": "Configurar recursos Samba compartidos en el Host",
|
||||
"fr": "Configurer les partages Samba sur l'hôte",
|
||||
"de": "Samba-Freigaben auf Host konfigurieren",
|
||||
"it": "Configurare condivisioni Samba su Host",
|
||||
"pt": "Configurar compartilhamentos Samba no Host"
|
||||
},
|
||||
"Configure Local Shared on Host": {
|
||||
"es": "Configurar directorios locales compartidos en el Host",
|
||||
"fr": "Configurer les répertoires locaux partagés sur l'hôte",
|
||||
"de": "Lokale geteilte Verzeichnisse auf Host konfigurieren",
|
||||
"it": "Configurare directory locali condivise su Host",
|
||||
"pt": "Configurar diretórios locais compartilhados no Host"
|
||||
},
|
||||
"LXC": {
|
||||
"es": "LXC",
|
||||
"fr": "LXC",
|
||||
"de": "LXC",
|
||||
"it": "LXC",
|
||||
"pt": "LXC"
|
||||
},
|
||||
"Configure LXC Mount Points (Host ↔ Container)": {
|
||||
"es": "Configurar puntos de montaje LXC (Host ↔ LXC)",
|
||||
"fr": "Configurer les points de montage LXC (Hôte ↔ LXC)",
|
||||
"de": "LXC-Mount-Punkte konfigurieren (Host ↔ LXC)",
|
||||
"it": "Configurare punti di mount LXC (Host ↔ LXC)",
|
||||
"pt": "Configurar pontos de montagem LXC (Host ↔ LXC)"
|
||||
},
|
||||
"Configure NFS Client in LXC (only privileged)": {
|
||||
"es": "Configurar cliente NFS en LXC (solo privilegiados)",
|
||||
"fr": "Configurer le client NFS dans LXC (privilégiés uniquement)",
|
||||
"de": "NFS-Client in LXC konfigurieren (nur privilegiert)",
|
||||
"it": "Configurare client NFS in LXC (solo privilegiati)",
|
||||
"pt": "Configurar cliente NFS em LXC (apenas privilegiados)"
|
||||
},
|
||||
"Configure Samba Client in LXC (only privileged)": {
|
||||
"es": "Configurar cliente Samba en LXC (solo privilegiados)",
|
||||
"fr": "Configurer le client Samba dans LXC (privilégiés uniquement)",
|
||||
"de": "Samba-Client in LXC konfigurieren (nur privilegiert)",
|
||||
"it": "Configurare client Samba in LXC (solo privilegiati)",
|
||||
"pt": "Configurar cliente Samba em LXC (apenas privilegiados)"
|
||||
},
|
||||
"Configure NFS Server in LXC (only privileged)": {
|
||||
"es": "Configurar servidor NFS en LXC (solo privilegiados)",
|
||||
"fr": "Configurer le serveur NFS dans LXC (privilégiés uniquement)",
|
||||
"de": "NFS-Server in LXC konfigurieren (nur privilegiert)",
|
||||
"it": "Configurare server NFS in LXC (solo privilegiati)",
|
||||
"pt": "Configurar servidor NFS em LXC (apenas privilegiados)"
|
||||
},
|
||||
"configure Samba Server in LXC (only privileged)": {
|
||||
"es": "Configurar servidor Samba en LXC (solo privilegiados)",
|
||||
"fr": "Configurer le serveur Samba dans LXC (privilégiés uniquement)",
|
||||
"de": "Samba-Server in LXC konfigurieren (nur privilegiert)",
|
||||
"it": "Configurare server Samba in LXC (solo privilegiati)",
|
||||
"pt": "Configurar servidor Samba em LXC (apenas privilegiados)"
|
||||
},
|
||||
"Help & Info (commands)": {
|
||||
"es": "Comandos de ayuda e información",
|
||||
"fr": "Aide et Informations (commandes)",
|
||||
"de": "Hilfe & Informationen (Befehle)",
|
||||
"it": "Aiuto e Informazioni (comandi)",
|
||||
"pt": "Ajuda e Informações (comandos)"
|
||||
},
|
||||
"English": {
|
||||
"es": "Inglés",
|
||||
"fr": "Anglais",
|
||||
"de": "Englisch",
|
||||
"it": "Inglese",
|
||||
"pt": "Inglês"
|
||||
},
|
||||
"Language Change": {
|
||||
"es": "Cambio de Idioma",
|
||||
"fr": "Changement de Langue",
|
||||
"de": "Sprachänderung",
|
||||
"it": "Cambio Lingua",
|
||||
"pt": "Mudança de Idioma"
|
||||
}
|
||||
}
|
||||
|
@@ -27,7 +27,7 @@
|
||||
"type": "ct"
|
||||
},
|
||||
{
|
||||
"name": "Proxmox VE LXC Tag",
|
||||
"name": "PVE LXC Tag",
|
||||
"slug": "add-iptag",
|
||||
"desc": "This script automatically adds IP address as tags to LXC containers or VM's using a systemd service. The service also updates the tags if a LXC/VM IP address is changed.",
|
||||
"script": "tools/pve/add-iptag.sh",
|
||||
@@ -84,7 +84,7 @@
|
||||
5
|
||||
],
|
||||
"notes": [
|
||||
"Adguard Home can be updated via the user interface."
|
||||
"AdGuard Home can only be updated via the user interface."
|
||||
],
|
||||
"type": "ct"
|
||||
},
|
||||
@@ -635,7 +635,7 @@
|
||||
"type": "ct"
|
||||
},
|
||||
{
|
||||
"name": "Proxmox VE LXC Cleaner",
|
||||
"name": "PVE LXC Cleaner",
|
||||
"slug": "clean-lxcs",
|
||||
"desc": "This script provides options to delete logs and cache, and repopulate apt lists for Ubuntu and Debian systems.",
|
||||
"script": "tools/pve/clean-lxcs.sh",
|
||||
@@ -649,7 +649,7 @@
|
||||
"type": "pve"
|
||||
},
|
||||
{
|
||||
"name": "Proxmox Clean Orphaned LVM",
|
||||
"name": "PVE Clean Orphaned LVM",
|
||||
"slug": "clean-orphaned-lvm",
|
||||
"desc": "This script helps Proxmox users identify and remove orphaned LVM volumes that are no longer associated with any VM or LXC container. It scans all LVM volumes, detects unused ones, and provides an interactive prompt to delete them safely. System-critical volumes like root, swap, and data are excluded to prevent accidental deletion.",
|
||||
"script": "tools/pve/clean-orphaned-lvm.sh",
|
||||
@@ -815,7 +815,9 @@
|
||||
2,
|
||||
3
|
||||
],
|
||||
"notes": [],
|
||||
"notes": [
|
||||
"The file `/etc/sysconfig/CosmosCloud` is optional. If you need custom settings, you can create it yourself."
|
||||
],
|
||||
"type": "ct"
|
||||
},
|
||||
{
|
||||
@@ -837,9 +839,9 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Proxmox VE Cron LXC Updater",
|
||||
"name": "PVE Cron LXC Updater",
|
||||
"slug": "cron-update-lxcs",
|
||||
"desc": "This script will add/remove a crontab schedule that updates all LXCs every Sunday at midnight.",
|
||||
"desc": "This script will add/remove a crontab schedule that updates the operating system of all LXCs every Sunday at midnight.",
|
||||
"script": "tools/pve/cron-update-lxcs.sh",
|
||||
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/cron-update-lxcs.sh",
|
||||
"categories": [
|
||||
@@ -1136,7 +1138,8 @@
|
||||
],
|
||||
"notes": [
|
||||
"Type `cat ~/matrix.creds` to see admin username/password.",
|
||||
"Synapse-Admin is running on port 5173"
|
||||
"Synapse-Admin is running on port 5173",
|
||||
"For bridges Installation methods (WhatsApp, Signal, Discord, etc.), see: \u00b4https://docs.mau.fi/bridges/go/setup.html\u00b4"
|
||||
],
|
||||
"type": "ct"
|
||||
},
|
||||
@@ -1222,6 +1225,22 @@
|
||||
"notes": [],
|
||||
"type": "ct"
|
||||
},
|
||||
{
|
||||
"name": "PVE LXC Execute Command",
|
||||
"slug": "lxc-execute",
|
||||
"desc": "This script allows administrators to execute a custom command inside one or multiple LXC containers on a Proxmox VE node. Containers can be selectively excluded via an interactive checklist. If a container is stopped, the script will automatically start it, run the command, and then shut it down again. Only Debian and Ubuntu based containers are supported.",
|
||||
"script": "tools/pve/execute.sh",
|
||||
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/execute.sh",
|
||||
"categories": [
|
||||
1
|
||||
],
|
||||
"notes": [
|
||||
"Execute within the Proxmox shell.",
|
||||
"Non-Debian/Ubuntu containers will be skipped automatically.",
|
||||
"Stopped containers will be started temporarily to run the command, then shut down again."
|
||||
],
|
||||
"type": "pve"
|
||||
},
|
||||
{
|
||||
"name": "Fenrus",
|
||||
"slug": "fenrus",
|
||||
@@ -1398,22 +1417,7 @@
|
||||
"type": "ct"
|
||||
},
|
||||
{
|
||||
"name": "Frigate",
|
||||
"slug": "frigate",
|
||||
"desc": "Frigate is an open source NVR built around real-time AI object detection. All processing is performed locally on your own hardware, and your camera feeds never leave your home.",
|
||||
"script": "ct/frigate.sh",
|
||||
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/frigate.sh",
|
||||
"categories": [
|
||||
15
|
||||
],
|
||||
"notes": [
|
||||
"Discussions (explore more advanced methods): `https://github.com/tteck/Proxmox/discussions/2711`",
|
||||
"go2rtc Interface port:`1984`"
|
||||
],
|
||||
"type": "ct"
|
||||
},
|
||||
{
|
||||
"name": "Proxmox VE LXC Filesystem Trim",
|
||||
"name": "PVE LXC Filesystem Trim",
|
||||
"slug": "fstrim",
|
||||
"desc": "This maintains SSD performance by managing unused blocks. Thin-provisioned storage systems also require management to prevent unnecessary storage use. VMs automate fstrim, while LXC containers need manual or automated fstrim processes for optimal performance.",
|
||||
"script": "tools/pve/fstrim.sh",
|
||||
@@ -1465,6 +1469,23 @@
|
||||
],
|
||||
"type": "ct"
|
||||
},
|
||||
{
|
||||
"name": "Ghostfolio",
|
||||
"slug": "ghostfolio",
|
||||
"desc": "Ghostfolio is an open source wealth management software built with web technology. The application empowers busy people to keep track of stocks, ETFs or cryptocurrencies and make solid, data-driven investment decisions.",
|
||||
"script": "ct/ghostfolio.sh",
|
||||
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/ghostfolio.sh",
|
||||
"categories": [
|
||||
23
|
||||
],
|
||||
"notes": [
|
||||
"Create your first user account by visiting the web interface and clicking 'Get Started'. The first user will automatically get admin privileges.",
|
||||
"Database and Redis credentials: `cat ~/ghostfolio.creds`",
|
||||
"Optional: CoinGecko API keys can be added during installation or later in the .env file for enhanced cryptocurrency data.",
|
||||
"Build process requires 4GB RAM (runtime: ~2GB). A temporary swap file will be created automatically if insufficient memory is detected."
|
||||
],
|
||||
"type": "ct"
|
||||
},
|
||||
{
|
||||
"name": "Gitea-Mirror",
|
||||
"slug": "gitea-mirror",
|
||||
@@ -1515,6 +1536,18 @@
|
||||
],
|
||||
"type": "addon"
|
||||
},
|
||||
{
|
||||
"name": "GlobaLeaks",
|
||||
"slug": "globaleaks",
|
||||
"desc": "GlobaLeaks is a free and open-source whistleblowing software enabling anyone to easily set up and maintain a secure reporting platform.",
|
||||
"script": "ct/globaleaks.sh",
|
||||
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/globaleaks.sh",
|
||||
"categories": [
|
||||
0
|
||||
],
|
||||
"notes": [],
|
||||
"type": "ct"
|
||||
},
|
||||
{
|
||||
"name": "GLPI",
|
||||
"slug": "glpi",
|
||||
@@ -1543,6 +1576,20 @@
|
||||
"notes": [],
|
||||
"type": "ct"
|
||||
},
|
||||
{
|
||||
"name": "GoAway",
|
||||
"slug": "goaway",
|
||||
"desc": "Lightweight DNS sinkhole written in Go with a modern dashboard client. Very good looking new alternative to Pi-Hole and Adguard Home.",
|
||||
"script": "ct/goaway.sh",
|
||||
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/goaway.sh",
|
||||
"categories": [
|
||||
5
|
||||
],
|
||||
"notes": [
|
||||
"Type `cat ~/goaway.creds` to see login credentials."
|
||||
],
|
||||
"type": "ct"
|
||||
},
|
||||
{
|
||||
"name": "Gokapi",
|
||||
"slug": "gokapi",
|
||||
@@ -1623,7 +1670,7 @@
|
||||
{
|
||||
"name": "Grist",
|
||||
"slug": "grist",
|
||||
"desc": "Grist is a modern, open source spreadsheet that goes beyond the grid",
|
||||
"desc": "Grist is like a spreadsheet + database hybrid. It lets you store structured data, use relational links between tables, apply formulas (even with Python), build custom layouts (cards, forms, dashboards), set fine-grained access rules, and visualize data with charts or pivot-tables.",
|
||||
"script": "ct/grist.sh",
|
||||
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/grist.sh",
|
||||
"categories": [
|
||||
@@ -1770,6 +1817,7 @@
|
||||
16
|
||||
],
|
||||
"notes": [
|
||||
"Containerized version doesn't allow Home Assistant add-ons.",
|
||||
"If the LXC is created Privileged, the script will automatically set up USB passthrough.",
|
||||
"config path: `/var/lib/docker/volumes/hass_config/_data`",
|
||||
"Portainer interface: $IP: 9443 - User & password must be set manually within 5 minutes, otherwise a restart of Portainer is required!",
|
||||
@@ -1849,7 +1897,7 @@
|
||||
"type": "ct"
|
||||
},
|
||||
{
|
||||
"name": "Proxmox VE Host Backup",
|
||||
"name": "PVE Host Backup",
|
||||
"slug": "host-backup",
|
||||
"desc": "This script serves as a versatile backup utility, enabling users to specify both the backup path and the directory they want to work in. This flexibility empowers users to select the specific files and directories they wish to back up, making it compatible with a wide range of hosts, not limited to Proxmox.",
|
||||
"script": "tools/pve/host-backup.sh",
|
||||
@@ -2077,6 +2125,24 @@
|
||||
"notes": [],
|
||||
"type": "ct"
|
||||
},
|
||||
{
|
||||
"name": "Joplin Server",
|
||||
"slug": "joplin-server",
|
||||
"desc": "Joplin - the privacy-focused note taking app with sync capabilities for Windows, macOS, Linux, Android and iOS.",
|
||||
"script": "ct/joplin-server.sh",
|
||||
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/joplin-server.sh",
|
||||
"categories": [
|
||||
12
|
||||
],
|
||||
"notes": [
|
||||
"Application can take some time to build, depending on your host speed. Please be patient."
|
||||
],
|
||||
"type": "ct",
|
||||
"default_credentials": {
|
||||
"username": "admin@localhost",
|
||||
"password": "admin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Jupyter Notebook",
|
||||
"slug": "jupyternotebook",
|
||||
@@ -2146,7 +2212,7 @@
|
||||
"type": "ct"
|
||||
},
|
||||
{
|
||||
"name": "Proxmox VE Kernel Clean",
|
||||
"name": "PVE Kernel Clean",
|
||||
"slug": "kernel-clean",
|
||||
"desc": "Cleaning unused kernel images is beneficial for reducing the length of the GRUB menu and freeing up disk space. By removing old, unused kernels, the system is able to conserve disk space and streamline the boot process.",
|
||||
"script": "tools/pve/kernel-clean.sh",
|
||||
@@ -2160,7 +2226,7 @@
|
||||
"type": "pve"
|
||||
},
|
||||
{
|
||||
"name": "Proxmox VE Kernel Pin",
|
||||
"name": "PVE Kernel Pin",
|
||||
"slug": "kernel-pin",
|
||||
"desc": "Kernel Pin is an essential tool for effortlessly managing kernel pinning and unpinning.",
|
||||
"script": "tools/pve/kernel-pin.sh",
|
||||
@@ -2424,7 +2490,7 @@
|
||||
"type": "ct"
|
||||
},
|
||||
{
|
||||
"name": "Container LXC Deletion",
|
||||
"name": "PVE LXC Deletion",
|
||||
"slug": "lxc-delete",
|
||||
"desc": "This script helps manage and delete LXC containers on a Proxmox VE server. It lists all available containers, allowing the user to select one or more for deletion through an interactive menu. Running containers are automatically stopped before deletion, and the user is asked to confirm each action. The script ensures a controlled and efficient container management process.",
|
||||
"script": "tools/pve/lxc-delete.sh",
|
||||
@@ -2641,7 +2707,7 @@
|
||||
"type": "ct"
|
||||
},
|
||||
{
|
||||
"name": "Proxmox VE Processor Microcode",
|
||||
"name": "PVE Processor Microcode",
|
||||
"slug": "microcode",
|
||||
"desc": "Processor Microcode is a layer of low-level software that runs on the processor and provides patches or updates to its firmware. Microcode updates can fix hardware bugs, improve performance, and enhance security features of the processor.\r\n\r\nIt's important to note that the availability of firmware update mechanisms, such as Intel's Management Engine (ME) or AMD's Platform Security Processor (PSP), may vary depending on the processor and its specific implementation. Therefore, it's recommended to consult the documentation for your processor to confirm whether firmware updates can be applied through the operating system.",
|
||||
"script": "tools/pve/microcode.sh",
|
||||
@@ -2735,7 +2801,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Proxmox VE Monitor-All",
|
||||
"name": "PVE Monitor-All",
|
||||
"slug": "monitor-all",
|
||||
"desc": "This script will add Monitor-All to Proxmox VE, which will monitor the status of all your instances, both containers and virtual machines, excluding templates and user-defined ones, and automatically restart or reset them if they become unresponsive. This is particularly useful if you're experiencing problems with Home Assistant becoming non-responsive every few days/weeks. Monitor-All also maintains a log of the entire process, which can be helpful for troubleshooting and monitoring purposes.\r\n\r\n\ud83d\udec8 Virtual machines without the QEMU guest agent installed must be excluded.\r\n\ud83d\udec8 Prior to generating any new CT/VM not found in this repository, it's necessary to halt Proxmox VE Monitor-All by running systemctl stop ping-instances.",
|
||||
"script": "tools/pve/monitor-all.sh",
|
||||
@@ -2785,6 +2851,18 @@
|
||||
],
|
||||
"type": "ct"
|
||||
},
|
||||
{
|
||||
"name": "MyIP",
|
||||
"slug": "myip",
|
||||
"desc": "The best IP Toolbox. Easy to check what's your IPs, IP geolocation, check for DNS leaks, examine WebRTC connections, speed test, ping test, MTR test, check website availability, whois search and more!",
|
||||
"script": "ct/myip.sh",
|
||||
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/myip.sh",
|
||||
"categories": [
|
||||
4
|
||||
],
|
||||
"notes": [],
|
||||
"type": "ct"
|
||||
},
|
||||
{
|
||||
"name": "Mylar3",
|
||||
"slug": "mylar3",
|
||||
@@ -2884,7 +2962,7 @@
|
||||
"type": "ct"
|
||||
},
|
||||
{
|
||||
"name": "Proxmox VE Netdata",
|
||||
"name": "PVE Netdata",
|
||||
"slug": "netdata",
|
||||
"desc": "Netdata is an open-source, real-time performance monitoring tool designed to provide insights into the performance and health of systems and applications. It is often used by system administrators, DevOps professionals, and developers to monitor and troubleshoot issues on servers and other devices.",
|
||||
"script": "tools/addon/netdata.sh",
|
||||
@@ -3305,15 +3383,17 @@
|
||||
"categories": [
|
||||
20
|
||||
],
|
||||
"notes": [],
|
||||
"notes": [
|
||||
"Script contains optional installation of Ollama."
|
||||
],
|
||||
"type": "ct"
|
||||
},
|
||||
{
|
||||
"name": "OpenWrt",
|
||||
"slug": "openwrt",
|
||||
"slug": "openwrt-vm",
|
||||
"desc": "OpenWrt is a powerful open-source firmware that can transform a wide range of networking devices into highly customizable and feature-rich routers, providing users with greater control and flexibility over their network infrastructure.",
|
||||
"script": "vm/openwrt.sh",
|
||||
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/vm/openwrt.sh",
|
||||
"script": "vm/openwrt-vm.sh",
|
||||
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/vm/openwrt-vm.sh",
|
||||
"categories": [
|
||||
4,
|
||||
2
|
||||
@@ -3449,8 +3529,7 @@
|
||||
11
|
||||
],
|
||||
"notes": [
|
||||
"This LXC is very memory-hungry when updating; it requires at least 6GB RAM, but RAM may be reduced to as low as 2GB when running normally",
|
||||
"To use a bind mount for storage, create symlinks to your mount for both `uploads` and `temp-uploads` in `/opt/palmr_data`",
|
||||
"To use a bind mount for storage, create symlinks to your mount for both `uploads` and `temp-uploads` in `/opt/palmr_data`, and uncomment `CUSTOM_PATH` to add the path to your bind mount",
|
||||
"To use Palmr with a reverse proxy, uncomment `SECURE_SITE` in `/opt/palmr/apps/server/.env`"
|
||||
],
|
||||
"type": "ct"
|
||||
@@ -3529,7 +3608,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Proxmox Backup Server Processor Microcode",
|
||||
"name": "PBS Processor Microcode",
|
||||
"slug": "pbs-microcode",
|
||||
"desc": "Processor Microcode is a layer of low-level software that runs on the processor and provides patches or updates to its firmware. Microcode updates can fix hardware bugs, improve performance, and enhance security features of the processor. This script is adapted for the Proxmox Backup Server environment and will only run on bare metal systems. If running in a virtualized environment, the script will exit. Note that firmware update mechanisms, such as Intel's Management Engine (ME) or AMD's Platform Security Processor (PSP), may vary depending on your processor and its implementation. Please consult your processor's documentation to verify if firmware updates can be applied through the operating system.",
|
||||
"script": "tools/pve/pbs_microcode.sh",
|
||||
@@ -3659,6 +3738,21 @@
|
||||
"password": "ipamadmin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "PhpMyAdmin",
|
||||
"slug": "phpmyadmin",
|
||||
"desc": "phpMyAdmin is a free software tool written in PHP, intended to handle the administration of MySQL over the Web. phpMyAdmin supports a wide range of operations on MySQL and MariaDB. Frequently used operations (managing databases, tables, columns, relations, indexes, users, permissions, etc) can be performed via the user interface, while you still have the ability to directly execute any SQL statement.",
|
||||
"script": "tools/addon/phpmyadmin.sh",
|
||||
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/addon/phpmyadmin.sh",
|
||||
"categories": [
|
||||
8
|
||||
],
|
||||
"notes": [
|
||||
"Execute within an existing LXC Console",
|
||||
"To update or uninstall run bash call again"
|
||||
],
|
||||
"type": "addon"
|
||||
},
|
||||
{
|
||||
"name": "Pi.Alert",
|
||||
"slug": "pialert",
|
||||
@@ -3668,9 +3762,7 @@
|
||||
"categories": [
|
||||
4
|
||||
],
|
||||
"notes": [
|
||||
"WARNING: Installation sources scripts outside of Community Scripts repo. Please check the source before installing."
|
||||
],
|
||||
"notes": [],
|
||||
"type": "ct"
|
||||
},
|
||||
{
|
||||
@@ -3743,8 +3835,7 @@
|
||||
13
|
||||
],
|
||||
"notes": [
|
||||
"With Privileged/Unprivileged Hardware Acceleration Support",
|
||||
"WARNING: Installation sources scripts outside of Community Scripts repo. Please check the source before installing."
|
||||
"With Privileged/Unprivileged Hardware Acceleration Support"
|
||||
],
|
||||
"type": "ct"
|
||||
},
|
||||
@@ -3786,8 +3877,7 @@
|
||||
"notes": [
|
||||
"If the LXC is created Privileged, the script will automatically set up USB passthrough.",
|
||||
"config path: `/var/lib/containers/storage/volumes/hass_config/_data`",
|
||||
"Options to Install Portainer or Portainer Agent",
|
||||
"WARNING: Installation sources scripts outside of Community Scripts repo. Please check the source before installing."
|
||||
"Options to Install Portainer or Portainer Agent"
|
||||
],
|
||||
"type": "ct"
|
||||
},
|
||||
@@ -3806,9 +3896,9 @@
|
||||
"type": "ct"
|
||||
},
|
||||
{
|
||||
"name": "Proxmox Backup Server Post Install",
|
||||
"name": "PBS Post Install",
|
||||
"slug": "post-pbs-install",
|
||||
"desc": "The script will give options to Disable the Enterprise Repo, Add/Correct PBS Sources, Enable the No-Subscription Repo, Add Test Repo, Disable Subscription Nag, Update Proxmox Backup Server and Reboot PBS.",
|
||||
"desc": "The script is designed for Proxmox Backup Server (PBS) and will give options to Disable the Enterprise Repo, Add/Correct PBS Sources, Enable the No-Subscription Repo, Add Test Repo, Disable Subscription Nag, Update Proxmox Backup Server and Reboot PBS.",
|
||||
"script": "tools/pve/post-pbs-install.sh",
|
||||
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/post-pbs-install.sh",
|
||||
"categories": [
|
||||
@@ -3822,9 +3912,9 @@
|
||||
"type": "pve"
|
||||
},
|
||||
{
|
||||
"name": "Proxmox Mail Gateway Post Install",
|
||||
"name": "PMG Post Install",
|
||||
"slug": "post-pmg-install",
|
||||
"desc": "The script will give options to Disable the Enterprise Repo, Add/Correct PMG Sources, Enable the No-Subscription Repo, Add Test Repo, Disable Subscription Nag, Update Proxmox Mail Gateway and Reboot PMG.",
|
||||
"desc": "The script is designed for Proxmox Mail Gateway and will give options to Disable the Enterprise Repo, Add/Correct PMG Sources, Enable the No-Subscription Repo, Add Test Repo, Disable Subscription Nag, Update Proxmox Mail Gateway and Reboot PMG.",
|
||||
"script": "tools/pve/post-pmg-install.sh",
|
||||
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/post-pmg-install.sh",
|
||||
"categories": [
|
||||
@@ -3838,7 +3928,7 @@
|
||||
"type": "pve"
|
||||
},
|
||||
{
|
||||
"name": "Proxmox VE Post Install",
|
||||
"name": "PVE Post Install",
|
||||
"slug": "post-pve-install",
|
||||
"desc": "This script provides options for managing Proxmox VE repositories, including disabling the Enterprise Repo, adding or correcting PVE sources, enabling the No-Subscription Repo, adding the test Repo, disabling the subscription nag, updating Proxmox VE, and rebooting the system.",
|
||||
"script": "tools/pve/post-pve-install.sh",
|
||||
@@ -3959,7 +4049,7 @@
|
||||
"type": "ct"
|
||||
},
|
||||
{
|
||||
"name": "Proxmox Backup Server",
|
||||
"name": "Proxmox Backup Server (PBS)",
|
||||
"slug": "proxmox-backup-server",
|
||||
"desc": "Proxmox Backup Server is an enterprise backup solution, for backing up and restoring VMs, containers, and physical hosts. By supporting incremental, fully deduplicated backups, Proxmox Backup Server significantly reduces network load and saves valuable storage space.",
|
||||
"script": "ct/proxmox-backup-server.sh",
|
||||
@@ -3978,7 +4068,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Proxmox Datacenter Manager",
|
||||
"name": "Proxmox Datacenter Manager (PDM)",
|
||||
"slug": "proxmox-datacenter-manager",
|
||||
"desc": "The Proxmox Datacenter Manager project has been developed with the objective of providing a centralized overview of all your individual nodes and clusters. It also enables basic management like migrations of virtual guests without any cluster network requirements. ",
|
||||
"script": "ct/proxmox-datacenter-manager.sh",
|
||||
@@ -3993,7 +4083,7 @@
|
||||
"type": "ct"
|
||||
},
|
||||
{
|
||||
"name": "Proxmox Mail Gateway",
|
||||
"name": "Proxmox Mail Gateway (PMG)",
|
||||
"slug": "proxmox-mail-gateway",
|
||||
"desc": "Proxmox Mail Gateway is the leading open-source email security solution helping you to protect your mail server against all email threats from the moment they emerge.",
|
||||
"script": "ct/proxmox-mail-gateway.sh",
|
||||
@@ -4236,8 +4326,8 @@
|
||||
"name": "Resilio Sync",
|
||||
"slug": "resiliosync",
|
||||
"desc": "Fast, reliable, and simple file sync and share solution, powered by P2P technology. Sync files across all your devices without storing them in the cloud.",
|
||||
"script": "ct/resilio-sync.sh",
|
||||
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/resilio-sync.sh",
|
||||
"script": "ct/resiliosync.sh",
|
||||
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/resiliosync.sh",
|
||||
"categories": [
|
||||
11
|
||||
],
|
||||
@@ -4315,7 +4405,7 @@
|
||||
"type": "ct"
|
||||
},
|
||||
{
|
||||
"name": "Proxmox VE CPU Scaling Governor",
|
||||
"name": "PVE CPU Scaling Governor",
|
||||
"slug": "scaling-governor",
|
||||
"desc": "The CPU scaling governor determines how the CPU frequency is adjusted based on the workload, with the goal of either conserving power or improving performance. By scaling the frequency up or down, the operating system can optimize the CPU usage and conserve energy when possible. Generic Scaling Governors",
|
||||
"script": "tools/pve/scaling-governor.sh",
|
||||
@@ -4328,6 +4418,20 @@
|
||||
],
|
||||
"type": "pve"
|
||||
},
|
||||
{
|
||||
"name": "Scraparr",
|
||||
"slug": "scraparr",
|
||||
"desc": "Scraparr is a Prometheus exporter for the *arr suite (Sonarr, Radarr, Lidarr, etc.). It provides metrics that can be scraped by Prometheus to monitor and visualize the health and performance of your *arr applications.",
|
||||
"script": "ct/scraparr.sh",
|
||||
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/scraparr.sh",
|
||||
"categories": [
|
||||
14
|
||||
],
|
||||
"notes": [
|
||||
"Edit config file then restart the scraparr service: `systemctl restart scraparr`"
|
||||
],
|
||||
"type": "ct"
|
||||
},
|
||||
{
|
||||
"name": "SearXNG",
|
||||
"slug": "searxng",
|
||||
@@ -4401,6 +4505,20 @@
|
||||
"password": "admin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "SigNoz",
|
||||
"slug": "signoz",
|
||||
"desc": "SigNoz is an open-source Datadog or New Relic alternative. Get APM, logs, traces, metrics, exceptions, & alerts in a single tool.",
|
||||
"script": "ct/signoz.sh",
|
||||
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/signoz.sh",
|
||||
"categories": [
|
||||
9
|
||||
],
|
||||
"notes": [
|
||||
"The first user you register will be the admin user."
|
||||
],
|
||||
"type": "ct"
|
||||
},
|
||||
{
|
||||
"name": "Silverbullet",
|
||||
"slug": "silverbullet",
|
||||
@@ -4532,6 +4650,18 @@
|
||||
"password": "null"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Stylus",
|
||||
"slug": "stylus",
|
||||
"desc": "Stylus (style + status) is a lightweight status page for infrastructure and networks. Configure a set of bash scripts that test the various parts of your infrastructure, set up visualizations with minimal configuration, and Stylus will generate you a dashboard for your system.",
|
||||
"script": "ct/stylus.sh",
|
||||
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/stylus.sh",
|
||||
"categories": [
|
||||
4
|
||||
],
|
||||
"notes": [],
|
||||
"type": "ct"
|
||||
},
|
||||
{
|
||||
"name": "Suwayomi-Server",
|
||||
"slug": "suwayomi-server",
|
||||
@@ -4678,6 +4808,20 @@
|
||||
],
|
||||
"type": "ct"
|
||||
},
|
||||
{
|
||||
"name": "Telegraf",
|
||||
"slug": "telegraf",
|
||||
"desc": "Telegraf collects and sends time series data from databases, systems, and IoT sensors. It has no external dependencies, is easy to install, and requires minimal memory.",
|
||||
"script": "ct/telegraf.sh",
|
||||
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/telegraf.sh",
|
||||
"categories": [
|
||||
9
|
||||
],
|
||||
"notes": [
|
||||
"Make sure to configure an output for the telegraf config and start the service with `systemctl start telegraf`."
|
||||
],
|
||||
"type": "ct"
|
||||
},
|
||||
{
|
||||
"name": "The Lounge",
|
||||
"slug": "the-lounge",
|
||||
@@ -4742,9 +4886,13 @@
|
||||
9
|
||||
],
|
||||
"notes": [
|
||||
"Please check and update the '/opt/tracktor/app/backend/.env' file if using behind reverse proxy."
|
||||
"Please check and update the '/opt/tracktor.env' file if using behind reverse proxy."
|
||||
],
|
||||
"type": "ct"
|
||||
"type": "ct",
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": "123456"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Traefik",
|
||||
@@ -4800,6 +4948,18 @@
|
||||
],
|
||||
"type": "ct"
|
||||
},
|
||||
{
|
||||
"name": "Tunarr",
|
||||
"slug": "tunarr",
|
||||
"desc": "Create a classic TV experience using your own media - IPTV backed by Plex/Jellyfin/Emby.",
|
||||
"script": "ct/tunarr.sh",
|
||||
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/tunarr.sh",
|
||||
"categories": [
|
||||
13
|
||||
],
|
||||
"notes": [],
|
||||
"type": "ct"
|
||||
},
|
||||
{
|
||||
"name": "TurnKey",
|
||||
"slug": "turnkey",
|
||||
@@ -4898,6 +5058,18 @@
|
||||
],
|
||||
"type": "vm"
|
||||
},
|
||||
{
|
||||
"name": "UHF Server",
|
||||
"slug": "uhf",
|
||||
"desc": "UHF Server is a powerful companion app that lets you seamlessly schedule and record your favorite shows from the UHF app.",
|
||||
"script": "ct/uhf.sh",
|
||||
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/uhf.sh",
|
||||
"categories": [
|
||||
13
|
||||
],
|
||||
"notes": [],
|
||||
"type": "ct"
|
||||
},
|
||||
{
|
||||
"name": "Umami",
|
||||
"slug": "umami",
|
||||
@@ -4987,21 +5159,22 @@
|
||||
"type": "ct"
|
||||
},
|
||||
{
|
||||
"name": "Proxmox VE LXC Updater",
|
||||
"name": "PVE LXC Updater",
|
||||
"slug": "update-lxcs",
|
||||
"desc": "This script has been created to simplify and speed up the process of updating all LXC containers across various Linux distributions, such as Ubuntu, Debian, Devuan, Alpine Linux, CentOS-Rocky-Alma, Fedora, and ArchLinux. It's designed to automatically skip templates and specific containers during the update, enhancing its convenience and usability.",
|
||||
"desc": "This script has been created to simplify and speed up the process of updating the operating system running inside LXC containers across various Linux distributions, such as Ubuntu, Debian, Devuan, Alpine Linux, CentOS-Rocky-Alma, Fedora, and ArchLinux. It's designed to automatically skip templates and specific containers during the update, enhancing its convenience and usability.",
|
||||
"script": "tools/pve/update-lxcs.sh",
|
||||
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/update-lxcs.sh",
|
||||
"categories": [
|
||||
1
|
||||
],
|
||||
"notes": [
|
||||
"Execute within the Proxmox shell"
|
||||
"Execute within the Proxmox shell",
|
||||
"The script updates only the operating system of the LXC container. It DOES NOT update the application installed within the container!"
|
||||
],
|
||||
"type": "pve"
|
||||
},
|
||||
{
|
||||
"name": "Proxmox Update Repositories",
|
||||
"name": "PVE Update Repositories",
|
||||
"slug": "update-repo",
|
||||
"desc": "This script updates repository links in LXC containers, replacing old links from the tteck repository with links to the new community-scripts repository to fix issues related to updating scripts.",
|
||||
"script": "tools/pve/update-repo.sh",
|
||||
@@ -5014,6 +5187,20 @@
|
||||
],
|
||||
"type": "pve"
|
||||
},
|
||||
{
|
||||
"name": "UpSnap",
|
||||
"slug": "upsnap",
|
||||
"desc": "UpSnap is a self-hosted web app that lets you wake up, manage and monitor devices on your network with ease. Built with SvelteKit, Go and PocketBase, it offers a clean dashboard, scheduled wake-ups, device discovery and secure user management.",
|
||||
"script": "ct/upsnap.sh",
|
||||
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/upsnap.sh",
|
||||
"categories": [
|
||||
4
|
||||
],
|
||||
"notes": [
|
||||
"The first user you register will be the admin user."
|
||||
],
|
||||
"type": "ct"
|
||||
},
|
||||
{
|
||||
"name": "Uptime Kuma",
|
||||
"slug": "uptimekuma",
|
||||
@@ -5055,6 +5242,20 @@
|
||||
],
|
||||
"type": "ct"
|
||||
},
|
||||
{
|
||||
"name": "Verdaccio",
|
||||
"slug": "verdaccio",
|
||||
"desc": "Verdaccio is a lightweight private npm proxy registry built with Node.js. It allows you to host your own npm registry with minimal configuration, providing a private npm repository for your projects. Verdaccio supports npm, yarn, and pnpm, and can cache packages from the public npm registry, allowing for faster installs and protection against npm registry outages. It includes a web interface for browsing packages, authentication and authorization features, and can be easily integrated into your development workflow.",
|
||||
"script": "ct/verdaccio.sh",
|
||||
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/verdaccio.sh",
|
||||
"categories": [
|
||||
20
|
||||
],
|
||||
"notes": [
|
||||
"To create the first user, run: npm adduser --registry http://<container-ip>:4873"
|
||||
],
|
||||
"type": "ct"
|
||||
},
|
||||
{
|
||||
"name": "VictoriaMetrics",
|
||||
"slug": "victoriametrics",
|
||||
@@ -5093,6 +5294,20 @@
|
||||
"notes": [],
|
||||
"type": "ct"
|
||||
},
|
||||
{
|
||||
"name": "Warracker",
|
||||
"slug": "warracker",
|
||||
"desc": "Warracker is an open source, self-hostable warranty tracker to monitor expirations, store receipts, files. You own the data, your rules!",
|
||||
"script": "ct/warracker.sh",
|
||||
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/warracker.sh",
|
||||
"categories": [
|
||||
12
|
||||
],
|
||||
"notes": [
|
||||
"The first user you register will be the admin user."
|
||||
],
|
||||
"type": "ct"
|
||||
},
|
||||
{
|
||||
"name": "Wastebin",
|
||||
"slug": "wastebin",
|
||||
@@ -5322,8 +5537,10 @@
|
||||
9
|
||||
],
|
||||
"notes": [
|
||||
"Database credentials: `cat zabbix.creds`",
|
||||
"Zabbix agent 2 is used by default"
|
||||
"Database credentials: `cat ~/zabbix.creds`",
|
||||
"You can choose between Zabbix agent (classic) and agent2 (modern) during installation",
|
||||
"For agent2 the PostgreSQL plugin is installed by default; all plugins are optional",
|
||||
"If agent2 with NVIDIA plugin is installed in an environment without GPU, the installer disables it automatically"
|
||||
],
|
||||
"type": "ct",
|
||||
"default_credentials": {
|
||||
|
8
menu
8
menu
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
@@ -13,13 +13,13 @@
|
||||
# This script serves as the main entry point for ProxMenux,
|
||||
# a menu-driven tool designed for Proxmox VE management.
|
||||
#
|
||||
# - Displays the ProxMenu logo on startup.
|
||||
# - Displays the ProxMenux logo on startup.
|
||||
# - Loads necessary configurations and language settings.
|
||||
# - Checks for available updates and installs them if confirmed.
|
||||
# - Downloads and executes the latest main menu script.
|
||||
#
|
||||
# Key Features:
|
||||
# - Ensures ProxMenu is always up-to-date by fetching the latest version.
|
||||
# - Ensures ProxMenux is always up-to-date by fetching the latest version.
|
||||
# - Uses whiptail for interactive menus and language selection.
|
||||
# - Loads utility functions and translation support.
|
||||
# - Maintains a cache system to improve performance.
|
||||
@@ -67,7 +67,7 @@ check_updates() {
|
||||
if whiptail --title "$(translate "Update Available")" \
|
||||
--yesno "$(translate "New version available") ($REMOTE_VERSION)\n\n$(translate "Do you want to update now?")" \
|
||||
10 60 --defaultno; then
|
||||
msg_warn "$(translate "Starting ProxMenu update...")"
|
||||
msg_warn "$(translate "Starting ProxMenux update...")"
|
||||
|
||||
if wget -qO "$INSTALL_SCRIPT" "$REPO_URL/install_proxmenux.sh"; then
|
||||
chmod +x "$INSTALL_SCRIPT"
|
||||
|
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
# ==========================================================
|
||||
# ProxMenu - Network Management and Repair Tool
|
||||
# ProxMenux - Network Management and Repair Tool
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
257
scripts/global/remove-banner-pve9_2.sh
Normal file
257
scripts/global/remove-banner-pve9_2.sh
Normal file
@@ -0,0 +1,257 @@
|
||||
#!/bin/bash
|
||||
# ==========================================================
|
||||
# Remove Subscription Banner - Proxmox VE 9.x (Clean Version)
|
||||
# ==========================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
|
||||
BASE_DIR="/usr/local/share/proxmenux"
|
||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||
TOOLS_JSON="/usr/local/share/proxmenux/installed_tools.json"
|
||||
|
||||
if [[ -f "$UTILS_FILE" ]]; then
|
||||
source "$UTILS_FILE"
|
||||
fi
|
||||
|
||||
|
||||
load_language
|
||||
initialize_cache
|
||||
|
||||
ensure_tools_json() {
|
||||
[ -f "$TOOLS_JSON" ] || echo "{}" > "$TOOLS_JSON"
|
||||
}
|
||||
|
||||
register_tool() {
|
||||
command -v jq >/dev/null 2>&1 || return 0
|
||||
local tool="$1" state="$2"
|
||||
ensure_tools_json
|
||||
jq --arg t "$tool" --argjson v "$state" '.[$t]=$v' "$TOOLS_JSON" \
|
||||
> "$TOOLS_JSON.tmp" && mv "$TOOLS_JSON.tmp" "$TOOLS_JSON"
|
||||
}
|
||||
|
||||
JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js"
|
||||
MIN_JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.min.js"
|
||||
GZ_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js.gz"
|
||||
MOBILE_TPL="/usr/share/pve-yew-mobile-gui/index.html.tpl"
|
||||
APT_HOOK="/etc/apt/apt.conf.d/no-nag-script"
|
||||
PATCH_BIN="/usr/local/bin/pve-remove-nag.sh"
|
||||
|
||||
MARK_JS="PROXMENUX_NAG_REMOVED_v2"
|
||||
MARK_MOBILE="<!-- PROXMENUX: MOBILE NAG PATCH v2 -->"
|
||||
|
||||
|
||||
verify_js_integrity() {
|
||||
local file="$1"
|
||||
[ -f "$file" ] || return 1
|
||||
[ -s "$file" ] || return 1
|
||||
grep -Eq 'Ext|function|var|const|let' "$file" || return 1
|
||||
if LC_ALL=C grep -qP '\x00' "$file" 2>/dev/null; then
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
create_backup() {
|
||||
local file="$1"
|
||||
local backup_dir="$BASE_DIR/backups"
|
||||
local timestamp
|
||||
timestamp=$(date +%Y%m%d_%H%M%S)
|
||||
local backup_file="$backup_dir/$(basename "$file").backup.$timestamp"
|
||||
mkdir -p "$backup_dir"
|
||||
if [ -f "$file" ]; then
|
||||
cp -a "$file" "$backup_file"
|
||||
ls -t "$backup_dir"/"$(basename "$file")".backup.* 2>/dev/null | tail -n +6 | xargs -r rm -f 2>/dev/null || true
|
||||
echo "$backup_file"
|
||||
fi
|
||||
}
|
||||
|
||||
# ----------------------------------------------------
|
||||
|
||||
create_patch_script() {
|
||||
cat > "$PATCH_BIN" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js"
|
||||
MIN_JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.min.js"
|
||||
GZ_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js.gz"
|
||||
MOBILE_TPL="/usr/share/pve-yew-mobile-gui/index.html.tpl"
|
||||
MARK_JS="PROXMENUX_NAG_REMOVED_v2"
|
||||
MARK_MOBILE="<!-- PROXMENUX: MOBILE NAG PATCH v2 -->"
|
||||
BASE_DIR="/usr/local/share/proxmenux"
|
||||
|
||||
verify_js_integrity() {
|
||||
local file="$1"
|
||||
[ -f "$file" ] && [ -s "$file" ] && grep -Eq 'Ext|function' "$file" && ! LC_ALL=C grep -qP '\x00' "$file" 2>/dev/null
|
||||
}
|
||||
|
||||
patch_web() {
|
||||
[ -f "$JS_FILE" ] || return 0
|
||||
grep -q "$MARK_JS" "$JS_FILE" && return 0
|
||||
|
||||
local backup_dir="$BASE_DIR/backups"
|
||||
mkdir -p "$backup_dir"
|
||||
local backup="$backup_dir/$(basename "$JS_FILE").backup.$(date +%Y%m%d_%H%M%S)"
|
||||
cp -a "$JS_FILE" "$backup"
|
||||
trap "cp -a '$backup' '$JS_FILE' 2>/dev/null || true" ERR
|
||||
|
||||
sed -i '1s|^|/* '"$MARK_JS"' */\n|' "$JS_FILE"
|
||||
|
||||
local patterns_found=0
|
||||
|
||||
if grep -q "res\.data\.status\.toLowerCase() !== 'active'" "$JS_FILE"; then
|
||||
sed -i "s/res\.data\.status\.toLowerCase() !== 'active'/false/g" "$JS_FILE"
|
||||
patterns_found=$((patterns_found + 1))
|
||||
fi
|
||||
|
||||
if grep -q "subscriptionActive: ''" "$JS_FILE"; then
|
||||
sed -i "s/subscriptionActive: ''/subscriptionActive: true/g" "$JS_FILE"
|
||||
patterns_found=$((patterns_found + 1))
|
||||
fi
|
||||
|
||||
if grep -q "title: gettext('No valid subscription')" "$JS_FILE"; then
|
||||
sed -i "s/title: gettext('No valid subscription')/title: gettext('Community Edition')/g" "$JS_FILE"
|
||||
patterns_found=$((patterns_found + 1))
|
||||
fi
|
||||
|
||||
if grep -q "icon: Ext\.Msg\.WARNING" "$JS_FILE"; then
|
||||
sed -i "s/icon: Ext\.Msg\.WARNING/icon: Ext.Msg.INFO/g" "$JS_FILE"
|
||||
patterns_found=$((patterns_found + 1))
|
||||
fi
|
||||
|
||||
if grep -q "subscription = !(" "$JS_FILE"; then
|
||||
sed -i "s/subscription = !(/subscription = false \&\& (/g" "$JS_FILE"
|
||||
patterns_found=$((patterns_found + 1))
|
||||
fi
|
||||
|
||||
# Si nada coincidió (cambio upstream), restaura y sal limpio
|
||||
if [ "${patterns_found:-0}" -eq 0 ]; then
|
||||
cp -a "$backup" "$JS_FILE"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Verificación final
|
||||
if ! verify_js_integrity "$JS_FILE"; then
|
||||
cp -a "$backup" "$JS_FILE"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Limpiar artefactos/cachés
|
||||
rm -f "$MIN_JS_FILE" "$GZ_FILE" 2>/dev/null || true
|
||||
find /var/cache/pve-manager/ -name "*.js*" -delete 2>/dev/null || true
|
||||
find /var/lib/pve-manager/ -name "*.js*" -delete 2>/dev/null || true
|
||||
find /var/cache/nginx/ -type f -delete 2>/dev/null || true
|
||||
|
||||
trap - ERR
|
||||
}
|
||||
|
||||
patch_mobile() {
|
||||
[ -f "$MOBILE_TPL" ] || return 0
|
||||
grep -q "$MARK_MOBILE" "$MOBILE_TPL" && return 0
|
||||
|
||||
local backup_dir="$BASE_DIR/backups"
|
||||
mkdir -p "$backup_dir"
|
||||
cp -a "$MOBILE_TPL" "$backup_dir/$(basename "$MOBILE_TPL").backup.$(date +%Y%m%d_%H%M%S)"
|
||||
|
||||
cat >> "$MOBILE_TPL" <<EOM
|
||||
$MARK_MOBILE
|
||||
<script>
|
||||
(function() {
|
||||
'use strict';
|
||||
function removeSubscriptionElements() {
|
||||
try {
|
||||
const dialogs = document.querySelectorAll('dialog.pwt-outer-dialog');
|
||||
dialogs.forEach(d => {
|
||||
const text = (d.textContent || '').toLowerCase();
|
||||
if (text.includes('subscription') || text.includes('no valid')) { d.remove(); }
|
||||
});
|
||||
const cards = document.querySelectorAll('.pwt-card.pwt-p-2.pwt-d-flex.pwt-interactive.pwt-justify-content-center');
|
||||
cards.forEach(c => {
|
||||
const text = (c.textContent || '').toLowerCase();
|
||||
const hasButton = c.querySelector('button');
|
||||
if (!hasButton && (text.includes('subscription') || text.includes('no valid'))) { c.remove(); }
|
||||
});
|
||||
const alerts = document.querySelectorAll('[class*="alert"], [class*="warning"], [class*="notice"]');
|
||||
alerts.forEach(a => {
|
||||
const text = (a.textContent || '').toLowerCase();
|
||||
if (text.includes('subscription') || text.includes('no valid')) { a.remove(); }
|
||||
});
|
||||
} catch (e) { console.warn('Error removing subscription elements:', e); }
|
||||
}
|
||||
if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', removeSubscriptionElements); }
|
||||
else { removeSubscriptionElements(); }
|
||||
const observer = new MutationObserver(removeSubscriptionElements);
|
||||
if (document.body) {
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
const interval = setInterval(removeSubscriptionElements, 500);
|
||||
setTimeout(() => { try { observer.disconnect(); clearInterval(interval); } catch(e){} }, 30000);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
EOM
|
||||
}
|
||||
|
||||
reload_services() {
|
||||
systemctl is-active --quiet pveproxy 2>/dev/null && {
|
||||
systemctl reload pveproxy 2>/dev/null || systemctl restart pveproxy 2>/dev/null || true
|
||||
}
|
||||
systemctl is-active --quiet nginx 2>/dev/null && {
|
||||
systemctl reload nginx 2>/dev/null || true
|
||||
}
|
||||
systemctl is-active --quiet pvedaemon 2>/dev/null && {
|
||||
systemctl reload pvedaemon 2>/dev/null || true
|
||||
}
|
||||
find /var/cache/pve-manager/ -type f -delete 2>/dev/null || true
|
||||
find /var/lib/pve-manager/ -type f -delete 2>/dev/null || true
|
||||
}
|
||||
|
||||
main() {
|
||||
patch_web || return 1
|
||||
patch_mobile
|
||||
reload_services
|
||||
}
|
||||
|
||||
main
|
||||
EOF
|
||||
chmod 755 "$PATCH_BIN"
|
||||
}
|
||||
# ----------------------------------------------------
|
||||
|
||||
|
||||
create_apt_hook() {
|
||||
cat > "$APT_HOOK" <<'EOF'
|
||||
/* ProxMenux: reapply nag patch after upgrades */
|
||||
DPkg::Post-Invoke { "/usr/local/bin/pve-remove-nag.sh || true"; };
|
||||
EOF
|
||||
chmod 644 "$APT_HOOK"
|
||||
apt-config dump >/dev/null 2>&1 || { msg_warn "APT hook syntax issue"; rm -f "$APT_HOOK"; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
remove_subscription_banner_pve9() {
|
||||
local pve_version
|
||||
pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+\.[0-9]+' | head -1 || true)
|
||||
local pve_major="${pve_version%%.*}"
|
||||
|
||||
msg_info "$(translate "Detected Proxmox VE ${pve_version:-9.x} – removing subscription banner")"
|
||||
|
||||
create_patch_script
|
||||
create_apt_hook
|
||||
|
||||
if ! "$PATCH_BIN"; then
|
||||
msg_error "$(translate "Error applying patches")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
register_tool "subscription_banner" true
|
||||
msg_ok "$(translate "Subscription banner removed successfully.")"
|
||||
msg_ok "$(translate "Refresh your browser to see changes.")"
|
||||
}
|
||||
|
||||
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
remove_subscription_banner_pve9
|
||||
fi
|
@@ -158,8 +158,6 @@ EOF
|
||||
msg_ok "$(translate "Non-free firmware warnings disabled")"
|
||||
fi
|
||||
|
||||
cleanup_duplicate_repos
|
||||
|
||||
update_output=$(apt-get update 2>&1)
|
||||
update_exit_code=$?
|
||||
|
||||
|
128
scripts/gpu_tpu/install_coral_pve9.sh
Normal file
128
scripts/gpu_tpu/install_coral_pve9.sh
Normal file
@@ -0,0 +1,128 @@
|
||||
#!/bin/bash
|
||||
# ProxMenux - Coral TPU Installer (PVE 9.x)
|
||||
# =========================================
|
||||
# Author : MacRimi
|
||||
# License : MIT
|
||||
# Version : 1.3 (PVE9, silent build)
|
||||
# Last Updated: 25/09/2025
|
||||
# =========================================
|
||||
|
||||
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
|
||||
BASE_DIR="/usr/local/share/proxmenux"
|
||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||
LOG_FILE="/tmp/coral_install.log"
|
||||
|
||||
if [[ -f "$UTILS_FILE" ]]; then
|
||||
source "$UTILS_FILE"
|
||||
fi
|
||||
|
||||
|
||||
load_language
|
||||
initialize_cache
|
||||
|
||||
pre_install_prompt() {
|
||||
if ! dialog --title "$(translate 'Coral TPU Installation')" --yesno \
|
||||
"\n$(translate 'Installing Coral TPU drivers requires rebooting the server after installation. Do you want to proceed?')" 10 70; then
|
||||
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
install_coral_host() {
|
||||
show_proxmenux_logo
|
||||
: >"$LOG_FILE"
|
||||
|
||||
|
||||
|
||||
msg_info "$(translate 'Installing build dependencies...')"
|
||||
apt-get update -qq >>"$LOG_FILE" 2>&1
|
||||
apt-get install -y git devscripts dh-dkms dkms proxmox-headers-$(uname -r) >>"$LOG_FILE" 2>&1
|
||||
if [[ $? -ne 0 ]]; then msg_error "$(translate 'Error installing build dependencies. Check /tmp/coral_install.log')"; exit 1; fi
|
||||
msg_ok "$(translate 'Build dependencies installed.')"
|
||||
|
||||
|
||||
|
||||
cd /tmp || exit 1
|
||||
rm -rf gasket-driver >>"$LOG_FILE" 2>&1
|
||||
msg_info "$(translate 'Cloning Google Coral driver repository...')"
|
||||
git clone https://github.com/google/gasket-driver.git >>"$LOG_FILE" 2>&1
|
||||
if [[ $? -ne 0 ]]; then msg_error "$(translate 'Could not clone the repository. Check /tmp/coral_install.log')"; exit 1; fi
|
||||
msg_ok "$(translate 'Repository cloned successfully.')"
|
||||
|
||||
|
||||
|
||||
cd /tmp/gasket-driver || exit 1
|
||||
msg_info "$(translate 'Patching source for kernel compatibility...')"
|
||||
|
||||
|
||||
sed -i 's/\.llseek = no_llseek/\.llseek = noop_llseek/' src/gasket_core.c
|
||||
|
||||
sed -i 's/^MODULE_IMPORT_NS(DMA_BUF);/MODULE_IMPORT_NS("DMA_BUF");/' src/gasket_page_table.c
|
||||
|
||||
sed -i "s/\(linux-headers-686-pae | linux-headers-amd64 | linux-headers-generic | linux-headers\)/\1 | proxmox-headers-$(uname -r) | pve-headers-$(uname -r)/" debian/control
|
||||
if [[ $? -ne 0 ]]; then msg_error "$(translate 'Patching failed. Check /tmp/coral_install.log')"; exit 1; fi
|
||||
msg_ok "$(translate 'Source patched successfully.')"
|
||||
|
||||
|
||||
|
||||
msg_info "$(translate 'Building DKMS package...')"
|
||||
debuild -us -uc -tc -b >>"$LOG_FILE" 2>&1
|
||||
if [[ $? -ne 0 ]]; then msg_error "$(translate 'Failed to build DKMS package. Check /tmp/coral_install.log')"; exit 1; fi
|
||||
msg_ok "$(translate 'DKMS package built successfully.')"
|
||||
|
||||
|
||||
|
||||
msg_info "$(translate 'Installing DKMS package...')"
|
||||
dpkg -i ../gasket-dkms_*.deb >>"$LOG_FILE" 2>&1 || true
|
||||
if ! dpkg -s gasket-dkms >/dev/null 2>&1; then
|
||||
msg_error "$(translate 'Failed to install DKMS package. Check /tmp/coral_install.log')"; exit 1
|
||||
fi
|
||||
msg_ok "$(translate 'DKMS package installed.')"
|
||||
|
||||
|
||||
|
||||
msg_info "$(translate 'Compiling Coral TPU drivers for current kernel...')"
|
||||
dkms remove -m gasket -v 1.0 -k "$(uname -r)" >>"$LOG_FILE" 2>&1 || true
|
||||
dkms add -m gasket -v 1.0 >>"$LOG_FILE" 2>&1 || true
|
||||
dkms build -m gasket -v 1.0 -k "$(uname -r)" >>"$LOG_FILE" 2>&1
|
||||
if [[ $? -ne 0 ]]; then
|
||||
sed -n '1,200p' /var/lib/dkms/gasket/1.0/build/make.log >>"$LOG_FILE" 2>&1 || true
|
||||
msg_error "$(translate 'DKMS build failed. Check /tmp/coral_install.log')"; exit 1
|
||||
fi
|
||||
dkms install -m gasket -v 1.0 -k "$(uname -r)" >>"$LOG_FILE" 2>&1
|
||||
if [[ $? -ne 0 ]]; then msg_error "$(translate 'DKMS install failed. Check /tmp/coral_install.log')"; exit 1; fi
|
||||
msg_ok "$(translate 'Drivers compiled and installed via DKMS.')"
|
||||
|
||||
|
||||
|
||||
msg_info "$(translate 'Loading modules...')"
|
||||
modprobe gasket >>"$LOG_FILE" 2>&1 || true
|
||||
modprobe apex >>"$LOG_FILE" 2>&1 || true
|
||||
if lsmod | grep -q '\bapex\b'; then
|
||||
msg_ok "$(translate 'Modules loaded.')"
|
||||
msg_success "$(translate 'Coral TPU drivers installed and loaded successfully.')"
|
||||
else
|
||||
msg_warn "$(translate 'Installation finished but drivers are not loaded. Please check dmesg and /tmp/coral_install.log')"
|
||||
fi
|
||||
|
||||
|
||||
|
||||
echo "---- dmesg | grep -i apex (last lines) ----" >>"$LOG_FILE"
|
||||
dmesg | grep -i apex | tail -n 20 >>"$LOG_FILE" 2>&1
|
||||
}
|
||||
|
||||
restart_prompt() {
|
||||
if whiptail --title "$(translate 'Coral TPU Installation')" --yesno \
|
||||
"$(translate 'The installation requires a server restart to apply changes. Do you want to restart now?')" 10 70; then
|
||||
msg_warn "$(translate 'Restarting the server...')"
|
||||
reboot
|
||||
else
|
||||
msg_success "$(translate 'Completed. Press Enter to return to menu...')"
|
||||
read -r
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
pre_install_prompt
|
||||
install_coral_host
|
||||
restart_prompt
|
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Revision : @Blaspt (USB passthrough via udev rule with persistent /dev/coral)
|
||||
|
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
157
scripts/lxc/jd2_2.sh
Normal file
157
scripts/lxc/jd2_2.sh
Normal file
@@ -0,0 +1,157 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script para instalar JDownloader en un contenedor LXC desde el host Proxmox
|
||||
# Autor: MacRimi
|
||||
|
||||
# Mostrar lista de CTs
|
||||
CT_LIST=$(pct list | awk 'NR>1 {print $1, $3}')
|
||||
if [ -z "$CT_LIST" ]; then
|
||||
whiptail --title "Error" --msgbox "No hay contenedores LXC disponibles en el sistema." 8 50
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Seleccionar CT
|
||||
CTID=$(whiptail --title "Instalación de JDownloader" --menu "Selecciona el contenedor donde instalar JDownloader:" 20 60 10 $CT_LIST 3>&1 1>&2 2>&3)
|
||||
if [ -z "$CTID" ]; then
|
||||
whiptail --title "Cancelado" --msgbox "No se ha seleccionado ningún contenedor." 8 40
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Solicitar email
|
||||
EMAIL=$(whiptail --title "Cuenta My JDownloader" --inputbox "Introduce tu correo electrónico para vincular JDownloader:" 10 60 3>&1 1>&2 2>&3)
|
||||
if [ -z "$EMAIL" ]; then
|
||||
whiptail --title "Error" --msgbox "No se ha introducido ningún correo." 8 40
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Solicitar contraseña con confirmación
|
||||
while true; do
|
||||
PASSWORD=$(whiptail --title "Cuenta My JDownloader" --passwordbox "Introduce tu contraseña de My JDownloader:" 10 60 3>&1 1>&2 2>&3)
|
||||
[ -z "$PASSWORD" ] && whiptail --title "Error" --msgbox "No se ha introducido ninguna contraseña." 8 40 && exit 1
|
||||
|
||||
CONFIRM=$(whiptail --title "Confirmación de contraseña" --passwordbox "Repite tu contraseña para confirmar:" 10 60 3>&1 1>&2 2>&3)
|
||||
[ "$PASSWORD" = "$CONFIRM" ] && break
|
||||
whiptail --title "Error" --msgbox "Las contraseñas no coinciden. Intenta de nuevo." 8 50
|
||||
done
|
||||
|
||||
# Confirmación final
|
||||
whiptail --title "Confirmar datos" --yesno "¿Deseas continuar con los siguientes datos?\n\nCorreo: $EMAIL\nContraseña: (oculta)\n\nEsta información se usará para vincular el contenedor con tu cuenta de My.JDownloader." 14 60
|
||||
[ $? -ne 0 ] && whiptail --title "Cancelado" --msgbox "Instalación cancelada por el usuario." 8 40 && exit 1
|
||||
|
||||
clear
|
||||
echo "🔍 Detectando sistema operativo dentro del CT $CTID..."
|
||||
OS_ID=$(pct exec "$CTID" -- awk -F= '/^ID=/{gsub("\"",""); print $2}' /etc/os-release)
|
||||
|
||||
echo "Sistema detectado: $OS_ID"
|
||||
echo "🧰 Preparando entorno..."
|
||||
|
||||
case "$OS_ID" in
|
||||
debian)
|
||||
# Repositorio adicional para Java 8
|
||||
pct exec "$CTID" -- wget -q http://www.mirbsd.org/~tg/Debs/sources.txt/wtf-bookworm.sources
|
||||
pct exec "$CTID" -- mv wtf-bookworm.sources /etc/apt/sources.list.d/
|
||||
pct exec "$CTID" -- apt update -y
|
||||
pct exec "$CTID" -- apt install -y openjdk-8-jdk wget
|
||||
JAVA_PATH="/usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java"
|
||||
;;
|
||||
ubuntu)
|
||||
pct exec "$CTID" -- apt update -y
|
||||
pct exec "$CTID" -- apt install -y openjdk-8-jdk wget
|
||||
JAVA_PATH="/usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java"
|
||||
;;
|
||||
alpine)
|
||||
pct exec "$CTID" -- apk update
|
||||
pct exec "$CTID" -- apk add openjdk8 wget
|
||||
JAVA_PATH="/usr/lib/jvm/java-1.8-openjdk/bin/java"
|
||||
;;
|
||||
*)
|
||||
echo "❌ Sistema operativo no soportado: $OS_ID"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Crear carpeta de instalación
|
||||
pct exec "$CTID" -- mkdir -p /opt/jdownloader
|
||||
pct exec "$CTID" -- bash -lc '
|
||||
set -e
|
||||
mkdir -p /opt/jdownloader
|
||||
cd /opt/jdownloader
|
||||
if [ ! -f JDownloader.jar ]; then
|
||||
if ls JDownloader.jar.backup.* >/dev/null 2>&1; then
|
||||
cp -a "$(ls -t JDownloader.jar.backup.* | head -1)" JDownloader.jar
|
||||
else
|
||||
curl -fSLo JDownloader.jar https://installer.jdownloader.org/JDownloader.jar
|
||||
fi
|
||||
fi
|
||||
chown root:root JDownloader.jar
|
||||
chmod 0644 JDownloader.jar
|
||||
'
|
||||
|
||||
|
||||
|
||||
# Crear archivo de configuración JSON para My JDownloader
|
||||
pct exec "$CTID" -- bash -c "mkdir -p /opt/jdownloader/cfg && cat > /opt/jdownloader/cfg/org.jdownloader.api.myjdownloader.MyJDownloaderSettings.json" <<EOF
|
||||
{
|
||||
"email" : "$EMAIL",
|
||||
"password" : "$PASSWORD",
|
||||
"enabled" : true
|
||||
}
|
||||
EOF
|
||||
|
||||
|
||||
# Crear servicio según sistema
|
||||
if [[ "$OS_ID" == "alpine" ]]; then
|
||||
# Servicio OpenRC para Alpine
|
||||
pct exec "$CTID" -- bash -c 'cat > /etc/init.d/jdownloader <<EOF
|
||||
#!/sbin/openrc-run
|
||||
|
||||
command="/usr/bin/java"
|
||||
command_args="-jar /opt/jdownloader/JDownloader.jar -norestart"
|
||||
pidfile="/var/run/jdownloader.pid"
|
||||
name="JDownloader"
|
||||
|
||||
depend() {
|
||||
need net
|
||||
}
|
||||
EOF'
|
||||
|
||||
pct exec "$CTID" -- chmod +x /etc/init.d/jdownloader
|
||||
pct exec "$CTID" -- rc-update add jdownloader default
|
||||
pct exec "$CTID" -- rc-service jdownloader start
|
||||
|
||||
else
|
||||
# Servicio systemd para Debian/Ubuntu
|
||||
pct exec "$CTID" -- bash -lc 'cat > /etc/systemd/system/jdownloader.service <<'"'"'EOF'"'"'
|
||||
[Unit]
|
||||
Description=JDownloader
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/opt/jdownloader
|
||||
ExecStartPre=/usr/bin/test -s /opt/jdownloader/JDownloader.jar
|
||||
ExecStart=/usr/bin/java -jar /opt/jdownloader/JDownloader.jar -norestart
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl daemon-reload
|
||||
systemctl enable jdownloader
|
||||
systemctl restart jdownloader
|
||||
systemctl status jdownloader --no-pager || true
|
||||
'
|
||||
|
||||
pct exec "$CTID" -- systemctl daemon-reexec
|
||||
pct exec "$CTID" -- systemctl daemon-reload
|
||||
pct exec "$CTID" -- systemctl enable jdownloader
|
||||
pct exec "$CTID" -- systemctl start jdownloader
|
||||
fi
|
||||
|
||||
pct exec "$CTID" -- reboot
|
||||
|
||||
echo -e "\n\033[1;32m✅ JDownloader se ha instalado correctamente en el CT $CTID y está funcionando como servicio.\033[0m"
|
||||
echo -e "\n➡️ Accede a \033[1;34mhttps://my.jdownloader.org\033[0m con tu cuenta para gestionarlo.\n"
|
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
@@ -197,7 +197,7 @@ show_version_info() {
|
||||
info_message+="$(translate "No installation information available.")\n"
|
||||
fi
|
||||
|
||||
info_message+="\n$(translate "ProxMenu files:")\n"
|
||||
info_message+="\n$(translate "ProxMenux files:")\n"
|
||||
[ -f "$INSTALL_DIR/$MENU_SCRIPT" ] && info_message+="✓ $MENU_SCRIPT → $INSTALL_DIR/$MENU_SCRIPT\n" || info_message+="✗ $MENU_SCRIPT\n"
|
||||
[ -f "$UTILS_FILE" ] && info_message+="✓ utils.sh → $UTILS_FILE\n" || info_message+="✗ utils.sh\n"
|
||||
[ -f "$CONFIG_FILE" ] && info_message+="✓ config.json → $CONFIG_FILE\n" || info_message+="✗ config.json\n"
|
||||
@@ -237,8 +237,8 @@ uninstall_proxmenu() {
|
||||
install_type=$(detect_installation_type)
|
||||
|
||||
if ! dialog --clear --backtitle "ProxMenux Configuration" \
|
||||
--title "Uninstall ProxMenu" \
|
||||
--yesno "\n$(translate "Are you sure you want to uninstall ProxMenu?")" 8 60; then
|
||||
--title "Uninstall ProxMenux" \
|
||||
--yesno "\n$(translate "Are you sure you want to uninstall ProxMenux?")" 8 60; then
|
||||
return
|
||||
fi
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - LXC Conversion Management Menu
|
||||
# ProxMenux - LXC Conversion Management Menu
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
@@ -99,14 +99,15 @@ show_menu() {
|
||||
--title "$(translate "$menu_title")" \
|
||||
--menu "$(translate "Select an option:")" 20 70 10 \
|
||||
1 "$(translate "Settings post-install Proxmox")" \
|
||||
2 "$(translate "Help and Info Commands")" \
|
||||
3 "$(translate "Hardware: GPUs and Coral-TPU")" \
|
||||
4 "$(translate "Create VM from template or script")" \
|
||||
5 "$(translate "Disk and Storage Manager")" \
|
||||
2 "$(translate "Hardware: GPUs and Coral-TPU")" \
|
||||
3 "$(translate "Create VM from template or script")" \
|
||||
4 "$(translate "Disk and Storage Manager")" \
|
||||
5 "$(translate "Mount and Share Manager")" \
|
||||
6 "$(translate "Proxmox VE Helper Scripts")" \
|
||||
7 "$(translate "Network Management")" \
|
||||
8 "$(translate "Utilities and Tools")" \
|
||||
9 "$(translate "Settings")" \
|
||||
h "$(translate "Help and Info Commands")" \
|
||||
s "$(translate "Settings")" \
|
||||
0 "$(translate "Exit")" 2>"$TEMP_FILE"
|
||||
|
||||
local EXIT_STATUS=$?
|
||||
@@ -122,15 +123,16 @@ show_menu() {
|
||||
|
||||
case $OPTION in
|
||||
1) exec bash <(curl -s "$REPO_URL/scripts/menus/menu_post_install.sh") ;;
|
||||
2) bash <(curl -s "$REPO_URL/scripts/help_info_menu.sh") ;;
|
||||
3) exec bash <(curl -s "$REPO_URL/scripts/menus/hw_grafics_menu.sh") ;;
|
||||
4) exec bash <(curl -s "$REPO_URL/scripts/menus/create_vm_menu.sh") ;;
|
||||
5) exec bash <(curl -s "$REPO_URL/scripts/menus/storage_menu.sh") ;;
|
||||
2) exec bash <(curl -s "$REPO_URL/scripts/menus/hw_grafics_menu.sh") ;;
|
||||
3) exec bash <(curl -s "$REPO_URL/scripts/menus/create_vm_menu.sh") ;;
|
||||
4) exec bash <(curl -s "$REPO_URL/scripts/menus/storage_menu.sh") ;;
|
||||
5) exec bash <(curl -s "$REPO_URL/scripts/menus/share_menu.sh") ;;
|
||||
6) exec bash <(curl -s "$REPO_URL/scripts/menus/menu_Helper_Scripts.sh") ;;
|
||||
7) exec bash <(curl -s "$REPO_URL/scripts/menus/network_menu.sh") ;;
|
||||
8) exec bash <(curl -s "$REPO_URL/scripts/menus/utilities_menu.sh") ;;
|
||||
9) exec bash <(curl -s "$REPO_URL/scripts/menus/config_menu.sh") ;;
|
||||
0) clear; msg_ok "$(translate "Thank you for using ProxMenu. Goodbye!")"; rm -f "$TEMP_FILE"; exit 0 ;;
|
||||
h) bash <(curl -s "$REPO_URL/scripts/help_info_menu.sh") ;;
|
||||
s) exec bash <(curl -s "$REPO_URL/scripts/menus/config_menu.sh") ;;
|
||||
0) clear; msg_ok "$(translate "Thank you for using ProxMenux. Goodbye!")"; rm -f "$TEMP_FILE"; exit 0 ;;
|
||||
*) msg_warn "$(translate "Invalid option")"; sleep 2 ;;
|
||||
esac
|
||||
done
|
||||
|
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
@@ -71,7 +71,7 @@ show_menu() {
|
||||
7) exec bash <(curl -s "$REPO_URL/scripts/menus/network_menu.sh") ;;
|
||||
8) exec bash <(curl -s "$REPO_URL/scripts/menus/utilities_menu.sh") ;;
|
||||
9) exec bash <(curl -s "$REPO_URL/scripts/menus/config_menu.sh") ;;
|
||||
0) clear; msg_ok "$(translate "Thank you for using ProxMenu. Goodbye!")"; rm -f "$TEMP_FILE"; exit 0 ;;
|
||||
0) clear; msg_ok "$(translate "Thank you for using ProxMenux. Goodbye!")"; rm -f "$TEMP_FILE"; exit 0 ;;
|
||||
*) msg_warn "$(translate "Invalid option")"; sleep 2 ;;
|
||||
esac
|
||||
done
|
||||
|
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
# ==========================================================
|
||||
# ProxMenu - Network Management and Repair Tool
|
||||
# ProxMenux - Network Management and Repair Tool
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
@@ -143,7 +143,7 @@ remove_subscription_banner() {
|
||||
msg_warn "Banner removal cancelled by user."
|
||||
return 1
|
||||
fi
|
||||
bash <(curl -fsSL "$REPO_URL/scripts/global/remove-banner-pve9.sh")
|
||||
bash <(curl -fsSL "$REPO_URL/scripts/global/remove-banner-pve9_2.sh")
|
||||
else
|
||||
if ! whiptail --title "Proxmox VE 8.x Subscription Banner Removal" \
|
||||
--yesno "Do you want to remove the Proxmox subscription banner from the web interface for PVE $pve_version?" 10 70; then
|
||||
|
@@ -795,7 +795,7 @@ EOF
|
||||
)
|
||||
|
||||
local selected
|
||||
selected=$(dialog --clear --backtitle "ProxMenu - $(translate "System Utilities")" \
|
||||
selected=$(dialog --clear --backtitle "ProxMenux - $(translate "System Utilities")" \
|
||||
--title "$(translate "Select utilities to install")" \
|
||||
--checklist "$(translate "Use SPACE to select/deselect, ENTER to confirm")" \
|
||||
20 70 12 "${utilities[@]}" 2>&1 >/dev/tty)
|
||||
@@ -807,7 +807,7 @@ EOF
|
||||
local selected="$1"
|
||||
|
||||
if [ -z "$selected" ]; then
|
||||
dialog --clear --backtitle "ProxMenu" \
|
||||
dialog --clear --backtitle "ProxMenux" \
|
||||
--title "$(translate "No Selection")" \
|
||||
--msgbox "$(translate "No utilities were selected")" 8 40
|
||||
return
|
||||
@@ -975,7 +975,7 @@ EOF
|
||||
|
||||
local selected
|
||||
selected=$(
|
||||
dialog --clear --backtitle "ProxMenu - $(translate "System Utilities")" \
|
||||
dialog --clear --backtitle "ProxMenux - $(translate "System Utilities")" \
|
||||
--title "$(translate "Select utilities to install")" \
|
||||
--checklist "$(translate "Use SPACE to select/deselect, ENTER to confirm")" \
|
||||
20 70 12 "${utilities[@]}" 3>&1 1>&2 2>&3
|
||||
@@ -992,7 +992,7 @@ EOF
|
||||
|
||||
|
||||
if [[ -z "$selected" ]]; then
|
||||
dialog --clear --backtitle "ProxMenu" \
|
||||
dialog --clear --backtitle "ProxMenux" \
|
||||
--title "$(translate "No Selection")" \
|
||||
--msgbox "$(translate "No utilities were selected")" 8 40
|
||||
return 0
|
||||
@@ -1051,7 +1051,7 @@ EOF
|
||||
local dlg_status=$?
|
||||
|
||||
if [[ $dlg_status -ne 0 ]]; then
|
||||
dialog --clear --backtitle "ProxMenu" \
|
||||
dialog --clear --backtitle "ProxMenux" \
|
||||
--title "$(translate "Canceled")" \
|
||||
--msgbox "$(translate "Action canceled by user")" 8 40
|
||||
|
||||
@@ -2902,7 +2902,7 @@ remove_subscription_banner() {
|
||||
|
||||
if [[ "$pve_version" -ge 9 ]]; then
|
||||
|
||||
bash <(curl -fsSL "$REPO_URL/scripts/global/remove-banner-pve9.sh")
|
||||
bash <(curl -fsSL "$REPO_URL/scripts/global/remove-banner-pve9_2.sh")
|
||||
else
|
||||
|
||||
bash <(curl -fsSL "$REPO_URL/scripts/global/remove-banner-pve8.sh")
|
||||
|
@@ -150,7 +150,7 @@ uninstall_apt_upgrade() {
|
||||
|
||||
################################################################
|
||||
|
||||
uninstall_subscription_banner() {
|
||||
uninstall_subscription_banner_() {
|
||||
msg_info "$(translate "Restoring subscription banner...")"
|
||||
|
||||
# Remove APT hook
|
||||
@@ -163,6 +163,111 @@ uninstall_subscription_banner() {
|
||||
register_tool "subscription_banner" false
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
uninstall_subscription_banner() {
|
||||
msg_info "$(translate "Restoring subscription banner...")"
|
||||
|
||||
local JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js"
|
||||
local MOBILE_TPL="/usr/share/pve-manager/templates/index.html.tpl"
|
||||
local PATCH_BIN="/usr/local/bin/pve-remove-nag.sh"
|
||||
local BASE_DIR="/opt/pve-nag-buster"
|
||||
local MIN_JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.min.js"
|
||||
local GZ_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js.gz"
|
||||
local MARK_MOBILE=" PVE9 Mobile Subscription Banner Removal "
|
||||
|
||||
local restored=false
|
||||
|
||||
# Remove APT hook (both old and new versions)
|
||||
for hook in /etc/apt/apt.conf.d/*nag* /etc/apt/apt.conf.d/no-nag-script; do
|
||||
if [[ -e "$hook" ]]; then
|
||||
rm -f "$hook"
|
||||
msg_ok "$(translate "Removed APT hook: $hook")"
|
||||
fi
|
||||
done
|
||||
|
||||
# Remove patch script
|
||||
if [[ -f "$PATCH_BIN" ]]; then
|
||||
rm -f "$PATCH_BIN"
|
||||
msg_ok "$(translate "Removed patch script: $PATCH_BIN")"
|
||||
fi
|
||||
|
||||
# Restore JavaScript file from backups (new script method)
|
||||
if [[ -d "$BASE_DIR/backups" ]]; then
|
||||
local latest_backup
|
||||
latest_backup=$(ls -t "$BASE_DIR/backups"/proxmoxlib.js.backup.* 2>/dev/null | head -1)
|
||||
if [[ -n "$latest_backup" && -f "$latest_backup" ]]; then
|
||||
if [[ -s "$latest_backup" ]] && grep -q "Ext\|function" "$latest_backup" && ! grep -q $'\0' "$latest_backup"; then
|
||||
cp -a "$latest_backup" "$JS_FILE"
|
||||
msg_ok "$(translate "Restored from backup: $latest_backup")"
|
||||
restored=true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Restore from old script backups (if new method didn't work)
|
||||
if [[ "$restored" == false ]]; then
|
||||
local old_backup
|
||||
old_backup=$(ls -t "${JS_FILE}".backup.pve9.* "${JS_FILE}".backup.* 2>/dev/null | head -1)
|
||||
if [[ -n "$old_backup" && -f "$old_backup" ]]; then
|
||||
if [[ -s "$old_backup" ]] && grep -q "Ext\|function" "$old_backup" && ! grep -q $'\0' "$old_backup"; then
|
||||
cp -a "$old_backup" "$JS_FILE"
|
||||
msg_ok "$(translate "Restored from old backup: $old_backup")"
|
||||
restored=true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Restore mobile template if patched
|
||||
if [[ -f "$MOBILE_TPL" ]] && grep -q "$MARK_MOBILE" "$MOBILE_TPL"; then
|
||||
local mobile_backup
|
||||
mobile_backup=$(ls -t "$BASE_DIR/backups"/index.html.tpl.backup.* 2>/dev/null | head -1)
|
||||
if [[ -n "$mobile_backup" && -f "$mobile_backup" ]]; then
|
||||
cp -a "$mobile_backup" "$MOBILE_TPL"
|
||||
msg_ok "$(translate "Restored mobile template from backup")"
|
||||
else
|
||||
# Remove the patch manually if no backup available
|
||||
sed -i "/^$MARK_MOBILE$/,\$d" "$MOBILE_TPL"
|
||||
msg_ok "$(translate "Removed mobile template patches")"
|
||||
fi
|
||||
fi
|
||||
|
||||
# If no valid backups found, reinstall packages
|
||||
if [[ "$restored" == false ]]; then
|
||||
msg_info "$(translate "No valid backups found, reinstalling packages...")"
|
||||
|
||||
if apt --reinstall install proxmox-widget-toolkit -y >/dev/null 2>&1; then
|
||||
msg_ok "$(translate "Reinstalled proxmox-widget-toolkit")"
|
||||
restored=true
|
||||
else
|
||||
msg_error "$(translate "Failed to reinstall proxmox-widget-toolkit")"
|
||||
fi
|
||||
|
||||
# Also try to reinstall pve-manager if mobile template was patched
|
||||
if [[ -f "$MOBILE_TPL" ]] && grep -q "$MARK_MOBILE" "$MOBILE_TPL"; then
|
||||
apt --reinstall install pve-manager -y >/dev/null 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
|
||||
rm -f "$MIN_JS_FILE" "$GZ_FILE" 2>/dev/null || true
|
||||
find /var/cache/pve-manager/ -name "*.js*" -delete 2>/dev/null || true
|
||||
find /var/lib/pve-manager/ -name "*.js*" -delete 2>/dev/null || true
|
||||
find /var/cache/nginx/ -type f -delete 2>/dev/null || true
|
||||
|
||||
register_tool "subscription_banner" false
|
||||
|
||||
if [[ "$restored" == true ]]; then
|
||||
msg_ok "$(translate "Subscription banner restored successfully")"
|
||||
msg_ok "$(translate "Refresh your browser to see changes (server restart may be required)")"
|
||||
else
|
||||
msg_error "$(translate "Failed to restore subscription banner completely")"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
|
||||
################################################################
|
||||
|
||||
uninstall_time_sync() {
|
||||
@@ -583,7 +688,6 @@ migrate_installed_tools() {
|
||||
return
|
||||
fi
|
||||
|
||||
clear
|
||||
show_proxmenux_logo
|
||||
msg_info "$(translate 'Detecting previous optimizations...')"
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
# ==========================================================
|
||||
# ProxMenu CT - NFS Client Manager for Proxmox LXC
|
||||
# ProxMenux CT - NFS Client Manager for Proxmox LXC
|
||||
# ==========================================================
|
||||
# Based on ProxMenux by MacRimi
|
||||
# ==========================================================
|
||||
|
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
# ==========================================================
|
||||
# ProxMenu Host - NFS Host Manager for Proxmox Host
|
||||
# ProxMenux Host - NFS Host Manager for Proxmox Host
|
||||
# ==========================================================
|
||||
# Based on ProxMenux by MacRimi
|
||||
# ==========================================================
|
||||
|
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
# ==========================================================
|
||||
# ProxMenu CT - NFS Manager for Proxmox LXC (Simple + Universal)
|
||||
# ProxMenux CT - NFS Manager for Proxmox LXC (Simple + Universal)
|
||||
# ==========================================================
|
||||
# Based on ProxMenux by MacRimi
|
||||
# ==========================================================
|
||||
@@ -201,25 +201,25 @@ get_network_config() {
|
||||
select_export_options() {
|
||||
EXPORT_OPTIONS=$(whiptail --title "$(translate "Export Options")" --menu \
|
||||
"\n$(translate "Select export permissions:")" 15 70 3 \
|
||||
"1" "$(translate "Read-Write (recommended)")" \
|
||||
"1" "$(translate "Read-Write (universal)")" \
|
||||
"2" "$(translate "Read-Only")" \
|
||||
"3" "$(translate "Custom options")" 3>&1 1>&2 2>&3)
|
||||
|
||||
case "$EXPORT_OPTIONS" in
|
||||
1)
|
||||
OPTIONS="rw,sync,no_subtree_check,all_squash,anonuid=0,anongid=101000"
|
||||
OPTIONS="rw,sync,no_subtree_check,no_root_squash"
|
||||
;;
|
||||
2)
|
||||
OPTIONS="ro,sync,no_subtree_check,all_squash,anonuid=0,anongid=101000"
|
||||
OPTIONS="ro,sync,no_subtree_check,no_root_squash"
|
||||
;;
|
||||
3)
|
||||
OPTIONS=$(whiptail --inputbox "$(translate "Enter custom NFS options:")" \
|
||||
10 70 "rw,sync,no_subtree_check,all_squash,anonuid=0,anongid=101000" \
|
||||
10 70 "rw,sync,no_subtree_check,no_root_squash" \
|
||||
--title "$(translate "Custom Options")" 3>&1 1>&2 2>&3)
|
||||
[[ -z "$OPTIONS" ]] && OPTIONS="rw,sync,no_subtree_check,all_squash,anonuid=0,anongid=101000"
|
||||
[[ -z "$OPTIONS" ]] && OPTIONS="rw,sync,no_subtree_check,no_root_squash"
|
||||
;;
|
||||
*)
|
||||
OPTIONS="rw,sync,no_subtree_check,all_squash,anonuid=0,anongid=101000"
|
||||
OPTIONS="rw,sync,no_subtree_check,no_root_squash"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
# ==========================================================
|
||||
# ProxMenu CT - Samba Client Manager for Proxmox LXC
|
||||
# ProxMenux CT - Samba Client Manager for Proxmox LXC
|
||||
# ==========================================================
|
||||
# Based on ProxMenux by MacRimi
|
||||
# ==========================================================
|
||||
|
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
# ==========================================================
|
||||
# ProxMenu Host - Samba Host Manager for Proxmox Host
|
||||
# ProxMenux Host - Samba Host Manager for Proxmox Host
|
||||
# ==========================================================
|
||||
# Based on ProxMenux by MacRimi
|
||||
# ==========================================================
|
||||
|
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
# ==========================================================
|
||||
# ProxMenu CT - Samba Manager for Proxmox LXC
|
||||
# ProxMenux CT - Samba Manager for Proxmox LXC
|
||||
# ==========================================================
|
||||
# Based on ProxMenux by MacRimi
|
||||
# ==========================================================
|
||||
|
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - Mount independent disk on Proxmox host
|
||||
# ProxMenux - Mount independent disk on Proxmox host
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - Mount point from host into LXC container (CT)
|
||||
# ProxMenux - Mount point from host into LXC container (CT)
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# License : MIT
|
||||
|
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - Mount independent disk on Proxmox host
|
||||
# ProxMenux - Mount independent disk on Proxmox host
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenu - A menu-driven script for Proxmox VE management
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
|
@@ -1 +1 @@
|
||||
1.1.5
|
||||
1.1.6
|
@@ -83,7 +83,7 @@ export default function HelpAndInfoPage() {
|
||||
|
||||
<div className="space-y-4 mt-6">
|
||||
<p className="text-lg text-black">
|
||||
ProxMenu provides an interactive command reference menu for Proxmox VE through a dialog-based interface.
|
||||
ProxMenux provides an interactive command reference menu for Proxmox VE through a dialog-based interface.
|
||||
Select one of the categories below to explore the available commands.
|
||||
</p>
|
||||
|
||||
@@ -170,12 +170,12 @@ export default function HelpAndInfoPage() {
|
||||
<div className="mt-16 mb-6">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<Book className="h-8 w-8 mr-2 text-blue-500" />
|
||||
<h2 className="text-2xl font-bold text-black">ProxMenu Guides</h2>
|
||||
<h2 className="text-2xl font-bold text-black">ProxMenux Guides</h2>
|
||||
</div>
|
||||
|
||||
<p className="text-lg mb-6 text-black">
|
||||
Check out the guides section for additional resources, tutorials, and documentation to help you get the most
|
||||
out of Proxmox VE and ProxMenu.
|
||||
out of Proxmox VE and ProxMenux.
|
||||
</p>
|
||||
|
||||
<div className="flex justify-center">
|
||||
|
@@ -111,7 +111,7 @@ export default function LinuxResourcesPage() {
|
||||
|
||||
<p className="text-lg mb-8 text-gray-700">
|
||||
A collection of useful resources for learning Linux commands, security practices, monitoring tools, and
|
||||
more. These resources complement the commands available in ProxMenu and will help you deepen your knowledge
|
||||
more. These resources complement the commands available in ProxMenux and will help you deepen your knowledge
|
||||
of Linux system administration.
|
||||
</p>
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user