diff --git a/AppImage/README.md b/AppImage/README.md
new file mode 100644
index 0000000..b9a3f52
--- /dev/null
+++ b/AppImage/README.md
@@ -0,0 +1,59 @@
+# 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
+
+## Development
+
+\`\`\`bash
+# Install dependencies
+npm install
+
+# Start development server
+npm run dev
+
+# Build for production
+npm run build
+\`\`\`
+
+## Building AppImage
+
+\`\`\`bash
+# Make build script executable
+chmod +x scripts/build_appimage.sh
+
+# Build AppImage
+./scripts/build_appimage.sh
+\`\`\`
+
+## Translation Support
+
+The project includes a translation system for multi-language support:
+
+\`\`\`bash
+# Build translation AppImage
+chmod +x scripts/build_translate_appimage.sh
+./scripts/build_translate_appimage.sh
+\`\`\`
+
+## License
+
+MIT License - see LICENSE file for details.
diff --git a/AppImage/app/api/flask/route.ts b/AppImage/app/api/flask/route.ts
new file mode 100644
index 0000000..a5264ec
--- /dev/null
+++ b/AppImage/app/api/flask/route.ts
@@ -0,0 +1,78 @@
+import { type NextRequest, NextResponse } from "next/server"
+
+// This will be the bridge between Next.js and the Flask server
+// For now, we'll return mock data that simulates what the Flask server would provide
+
+export async function GET(request: NextRequest) {
+ const { searchParams } = new URL(request.url)
+ const endpoint = searchParams.get("endpoint")
+
+ // Mock data that would come from the Flask server running on port 8008
+ const mockData = {
+ system: {
+ cpu_usage: 67.3,
+ memory_usage: 49.4,
+ temperature: 52,
+ uptime: "15d 7h 23m",
+ load_average: [1.23, 1.45, 1.67],
+ },
+ storage: {
+ total: 2000,
+ used: 1250,
+ available: 750,
+ disks: [
+ { name: "/dev/sda", type: "HDD", size: 1000, used: 650, health: "healthy", temp: 42 },
+ { name: "/dev/sdb", type: "HDD", size: 1000, used: 480, health: "healthy", temp: 38 },
+ { name: "/dev/sdc", type: "SSD", size: 500, used: 120, health: "healthy", temp: 35 },
+ { name: "/dev/nvme0n1", type: "NVMe", size: 1000, used: 340, health: "warning", temp: 55 },
+ ],
+ },
+ network: {
+ interfaces: [
+ { name: "vmbr0", type: "Bridge", status: "up", ip: "192.168.1.100/24", speed: "1000 Mbps" },
+ { name: "enp1s0", type: "Physical", status: "up", ip: "192.168.1.101/24", speed: "1000 Mbps" },
+ ],
+ traffic: {
+ incoming: 89,
+ outgoing: 67,
+ },
+ },
+ vms: [
+ {
+ id: 100,
+ name: "web-server-01",
+ status: "running",
+ os: "Ubuntu 22.04",
+ cpu: 4,
+ memory: 8192,
+ disk: 50,
+ uptime: "15d 7h 23m",
+ cpu_usage: 45,
+ memory_usage: 62,
+ disk_usage: 78,
+ },
+ ],
+ }
+
+ try {
+ // In the real implementation, this would make a request to the Flask server
+ // const response = await fetch(`http://localhost:8008/api/${endpoint}`)
+ // const data = await response.json()
+
+ // For now, return mock data based on the endpoint
+ switch (endpoint) {
+ case "system":
+ return NextResponse.json(mockData.system)
+ case "storage":
+ return NextResponse.json(mockData.storage)
+ case "network":
+ return NextResponse.json(mockData.network)
+ case "vms":
+ return NextResponse.json(mockData.vms)
+ default:
+ return NextResponse.json(mockData)
+ }
+ } catch (error) {
+ return NextResponse.json({ error: "Failed to fetch data from Flask server" }, { status: 500 })
+ }
+}
diff --git a/AppImage/app/globals.css b/AppImage/app/globals.css
index f08e712..c21cb4d 100644
--- a/AppImage/app/globals.css
+++ b/AppImage/app/globals.css
@@ -4,6 +4,7 @@
@custom-variant dark (&:is(.dark *));
:root {
+ /* Proxmox light theme colors */
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
@@ -31,54 +32,49 @@
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
- --header-bg: oklch(1 0 0);
- --header-foreground: oklch(0.145 0 0);
}
.dark {
- --background: oklch(0.205 0.005 240); /* Proxmox dark gray #2b2f36 */
- --foreground: oklch(0.985 0 0);
- --card: oklch(0.235 0.005 240); /* Slightly lighter gray for cards #363c45 */
- --card-foreground: oklch(0.985 0 0);
- --popover: oklch(0.235 0.005 240);
- --popover-foreground: oklch(0.985 0 0);
- --primary: oklch(0.985 0 0);
- --primary-foreground: oklch(0.205 0 0);
- --secondary: oklch(0.285 0.005 240); /* Better contrast for secondary elements */
- --secondary-foreground: oklch(0.985 0 0);
- --muted: oklch(0.285 0.005 240);
- --muted-foreground: oklch(0.708 0 0); /* Better contrast for muted text */
- --accent: oklch(0.285 0.005 240);
- --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.335 0.005 240); /* More visible borders */
- --input: oklch(0.285 0.005 240);
- --ring: oklch(0.439 0 0);
- --chart-1: oklch(0.65 0.2 220); /* Bright Blue */
- --chart-2: oklch(0.65 0.2 140); /* Bright Green */
- --chart-3: oklch(0.7 0.2 50); /* Bright Yellow */
- --chart-4: oklch(0.65 0.2 300); /* Bright Purple */
- --chart-5: oklch(0.65 0.2 20); /* Bright Orange */
- --radius-sm: calc(var(--radius) - 4px);
- --radius-md: calc(var(--radius) - 2px);
- --radius-lg: var(--radius);
- --radius-xl: calc(var(--radius) + 4px);
- --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.285 0.005 240);
- --sidebar-accent-foreground: oklch(0.985 0 0);
- --sidebar-border: oklch(0.285 0.005 240);
- --sidebar-ring: oklch(0.439 0 0);
- --header-bg: oklch(0 0 0);
- --header-foreground: oklch(1 0 0);
+ /* Proxmox dark theme with proper gray background (#2b2f36) */
+ --background: #2b2f36; /* Proxmox dark gray */
+ --foreground: #ffffff;
+ --card: #363c45; /* Slightly lighter gray for cards */
+ --card-foreground: #ffffff;
+ --popover: #363c45;
+ --popover-foreground: #ffffff;
+ --primary: #ffffff;
+ --primary-foreground: #2b2f36;
+ --secondary: #4a5058; /* Better contrast for secondary elements */
+ --secondary-foreground: #ffffff;
+ --muted: #4a5058;
+ --muted-foreground: #b4b4b4; /* Better contrast for muted text */
+ --accent: #4a5058;
+ --accent-foreground: #ffffff;
+ --destructive: #ef4444;
+ --destructive-foreground: #ffffff;
+ --border: #525862; /* More visible borders */
+ --input: #4a5058;
+ --ring: #6b7280;
+ /* Updated chart colors to be more vibrant and visible in dark mode */
+ --chart-1: #3b82f6; /* Bright Blue */
+ --chart-2: #10b981; /* Bright Green */
+ --chart-3: #f59e0b; /* Bright Yellow */
+ --chart-4: #8b5cf6; /* Bright Purple */
+ --chart-5: #f97316; /* Bright Orange */
+ --sidebar: #2b2f36;
+ --sidebar-foreground: #ffffff;
+ --sidebar-primary: #6366f1;
+ --sidebar-primary-foreground: #ffffff;
+ --sidebar-accent: #4a5058;
+ --sidebar-accent-foreground: #ffffff;
+ --sidebar-border: #4a5058;
+ --sidebar-ring: #6b7280;
+ /* Header is black only in dark mode */
+ --header-bg: #000000;
+ --header-foreground: #ffffff;
}
@theme inline {
- --font-sans: var(--font-geist-sans);
- --font-mono: var(--font-geist-mono);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
@@ -115,6 +111,9 @@
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
+ /* Custom header colors */
+ --color-header-bg: var(--header-bg);
+ --color-header-foreground: var(--header-foreground);
}
@layer base {
@@ -124,78 +123,80 @@
body {
@apply bg-background text-foreground;
}
- .header-bg {
- background-color: var(--header-bg);
- color: var(--header-foreground);
- backdrop-filter: none; /* Remove any blur effects */
- }
- /* Custom scrollbar with better contrast */
- ::-webkit-scrollbar {
- width: 6px;
- }
-
- ::-webkit-scrollbar-track {
- background: var(--background);
- }
-
- ::-webkit-scrollbar-thumb {
- background: var(--muted);
- border-radius: 3px;
- }
-
- ::-webkit-scrollbar-thumb:hover {
- background: var(--muted-foreground);
- }
-
- /* Better contrast for dark mode content */
- .dark .metric-card {
- background: var(--card);
- border: 1px solid var(--border);
- }
-
- .dark .metric-value {
- color: var(--foreground);
- font-weight: 600;
- }
-
- .dark .metric-label {
- color: var(--muted-foreground);
- }
-
- /* Fix chart axis visibility in dark mode */
- .dark .recharts-cartesian-axis-tick-value {
- fill: var(--muted-foreground) !important;
- }
-
- .dark .recharts-text {
- fill: var(--muted-foreground) !important;
- }
-
- /* Improve server info layout in header - clean design without transparency */
- .server-info {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- padding: 0.25rem 0.75rem;
- border-radius: 0.375rem;
- border: 1px solid rgba(255, 255, 255, 0.2);
- }
-
- .dark .server-info {
- border: 1px solid rgba(255, 255, 255, 0.1);
- }
-
- /* Better spacing for VM/LXC badges */
- .vm-badges {
- display: flex;
- flex-wrap: wrap;
- gap: 0.25rem;
- align-items: center;
- }
-
- .vm-badge {
- font-size: 0.75rem;
- padding: 0.125rem 0.5rem;
- white-space: nowrap;
- }
+}
+
+/* Header styling that adapts to theme */
+.header-bg {
+ background-color: var(--header-bg);
+ color: var(--header-foreground);
+}
+
+/* Custom scrollbar with better contrast */
+::-webkit-scrollbar {
+ width: 6px;
+}
+
+::-webkit-scrollbar-track {
+ background: var(--background);
+}
+
+::-webkit-scrollbar-thumb {
+ background: var(--muted);
+ border-radius: 3px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: var(--muted-foreground);
+}
+
+/* Better contrast for dark mode content */
+.dark .metric-card {
+ background: var(--card);
+ border: 1px solid var(--border);
+}
+
+.dark .metric-value {
+ color: var(--foreground);
+ font-weight: 600;
+}
+
+.dark .metric-label {
+ color: var(--muted-foreground);
+}
+
+/* Fix chart axis visibility in dark mode */
+.dark .recharts-cartesian-axis-tick-value {
+ fill: var(--muted-foreground) !important;
+}
+
+.dark .recharts-text {
+ fill: var(--muted-foreground) !important;
+}
+
+/* Improve server info layout in header - clean design without transparency */
+.server-info {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.25rem 0.75rem;
+ border-radius: 0.375rem;
+ border: 1px solid rgba(255, 255, 255, 0.2);
+}
+
+.dark .server-info {
+ border: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+/* Better spacing for VM/LXC badges */
+.vm-badges {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.25rem;
+ align-items: center;
+}
+
+.vm-badge {
+ font-size: 0.75rem;
+ padding: 0.125rem 0.5rem;
+ white-space: nowrap;
}
diff --git a/AppImage/app/layout.tsx b/AppImage/app/layout.tsx
index f4009cc..27a5ad7 100644
--- a/AppImage/app/layout.tsx
+++ b/AppImage/app/layout.tsx
@@ -9,8 +9,21 @@ import "./globals.css"
export const metadata: Metadata = {
title: "ProxMenux Monitor",
- description: "Proxmox System Dashboard",
+ 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: "#1a1a1a" },
+ ],
}
export default function RootLayout({
@@ -20,13 +33,13 @@ export default function RootLayout({
}>) {
return (
-
-
+
+ Loading...}>
{children}
+
-
)
diff --git a/AppImage/app/page.tsx b/AppImage/app/page.tsx
index c6d59e1..47629db 100644
--- a/AppImage/app/page.tsx
+++ b/AppImage/app/page.tsx
@@ -1,6 +1,6 @@
-import { ProxmoxDashboard } from "../AppImage/components/proxmox-dashboard"
+import { ProxmoxDashboard } from "../components/proxmox-dashboard"
-export default function Page() {
+export default function Home() {
return (
diff --git a/AppImage/components.json b/AppImage/components.json
deleted file mode 100644
index 4ee62ee..0000000
--- a/AppImage/components.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "$schema": "https://ui.shadcn.com/schema.json",
- "style": "new-york",
- "rsc": true,
- "tsx": true,
- "tailwind": {
- "config": "",
- "css": "app/globals.css",
- "baseColor": "neutral",
- "cssVariables": true,
- "prefix": ""
- },
- "aliases": {
- "components": "@/components",
- "utils": "@/lib/utils",
- "ui": "@/components/ui",
- "lib": "@/lib",
- "hooks": "@/hooks"
- },
- "iconLibrary": "lucide"
-}
diff --git a/AppImage/components/network-metrics.tsx b/AppImage/components/network-metrics.tsx
new file mode 100644
index 0000000..8d3a714
--- /dev/null
+++ b/AppImage/components/network-metrics.tsx
@@ -0,0 +1,267 @@
+"use client"
+
+import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
+import { Badge } from "./ui/badge"
+import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, AreaChart, Area } from "recharts"
+import { Wifi, Globe, Shield, Activity, Network, Router } from "lucide-react"
+
+const networkTraffic = [
+ { time: "00:00", incoming: 45, outgoing: 32 },
+ { time: "04:00", incoming: 52, outgoing: 28 },
+ { time: "08:00", incoming: 78, outgoing: 65 },
+ { time: "12:00", incoming: 65, outgoing: 45 },
+ { time: "16:00", incoming: 82, outgoing: 58 },
+ { time: "20:00", incoming: 58, outgoing: 42 },
+ { time: "24:00", incoming: 43, outgoing: 35 },
+]
+
+const connectionData = [
+ { time: "00:00", connections: 1250 },
+ { time: "04:00", connections: 980 },
+ { time: "08:00", connections: 1850 },
+ { time: "12:00", connections: 1650 },
+ { time: "16:00", connections: 2100 },
+ { time: "20:00", connections: 1580 },
+ { time: "24:00", connections: 1320 },
+]
+
+export function NetworkMetrics() {
+ return (
+
+ {/* Network Overview Cards */}
+
+
+
+ Network Traffic
+
+
+
+ 156 MB/s
+
+ ↓ 89 MB/s
+ ↑ 67 MB/s
+
+ Peak: 245 MB/s at 16:30
+
+
+
+
+
+ Active Connections
+
+
+
+ 1,847
+
+
+ Normal
+
+
+
+ ↑ 12% from last hour
+
+
+
+
+
+
+ Firewall Status
+
+
+
+ Active
+
+
+ Protected
+
+
+ 247 blocked attempts today
+
+
+
+
+
+
+
+ Latency
+
+
+
+ 12ms
+
+
+ Excellent
+
+
+ Avg response time
+
+
+
+
+ {/* Network Charts */}
+
+
+
+
+
+ Network Traffic (24h)
+
+
+
+
+
+
+
+
+ [`${value} MB/s`, name === "incoming" ? "Incoming" : "Outgoing"]}
+ />
+
+
+
+
+
+
+
+
+
+
+
+ Active Connections (24h)
+
+
+
+
+
+
+
+
+ [`${value}`, "Connections"]}
+ />
+
+
+
+
+
+
+
+ {/* Network Interfaces */}
+
+
+
+
+ Network Interfaces
+
+
+
+
+ {[
+ {
+ name: "vmbr0",
+ type: "Bridge",
+ status: "up",
+ ip: "192.168.1.100/24",
+ speed: "1000 Mbps",
+ rx: "2.3 GB",
+ tx: "1.8 GB",
+ },
+ {
+ name: "enp1s0",
+ type: "Physical",
+ status: "up",
+ ip: "192.168.1.101/24",
+ speed: "1000 Mbps",
+ rx: "1.2 GB",
+ tx: "890 MB",
+ },
+ {
+ name: "vmbr1",
+ type: "Bridge",
+ status: "up",
+ ip: "10.0.0.1/24",
+ speed: "1000 Mbps",
+ rx: "456 MB",
+ tx: "234 MB",
+ },
+ {
+ name: "tap101i0",
+ type: "TAP",
+ status: "up",
+ ip: "10.0.0.101/24",
+ speed: "1000 Mbps",
+ rx: "123 MB",
+ tx: "89 MB",
+ },
+ ].map((interface_, index) => (
+
+
+
+
+
{interface_.name}
+
+ {interface_.type} • {interface_.speed}
+
+
+
+
+
+
+
IP Address
+
{interface_.ip}
+
+
+
+
RX / TX
+
+ {interface_.rx} / {interface_.tx}
+
+
+
+
+ {interface_.status.toUpperCase()}
+
+
+
+ ))}
+
+
+
+
+ )
+}
diff --git a/AppImage/components/proxmox-dashboard.tsx b/AppImage/components/proxmox-dashboard.tsx
new file mode 100644
index 0000000..626a28a
--- /dev/null
+++ b/AppImage/components/proxmox-dashboard.tsx
@@ -0,0 +1,246 @@
+"use client"
+
+import { useState, useEffect } 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, Languages, 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
+}
+
+export function ProxmoxDashboard() {
+ const [systemStatus, setSystemStatus] = useState({
+ status: "healthy",
+ uptime: "15d 7h 23m",
+ lastUpdate: new Date().toLocaleTimeString(),
+ serverName: "proxmox-01",
+ nodeId: "pve-node-01",
+ })
+ const [isRefreshing, setIsRefreshing] = useState(false)
+ const [isTranslating, setIsTranslating] = useState(false)
+
+ useEffect(() => {
+ const fetchServerInfo = async () => {
+ try {
+ const response = await fetch("/api/flask/system-info")
+ if (response.ok) {
+ const data = await response.json()
+ setSystemStatus((prev) => ({
+ ...prev,
+ serverName: data.hostname || "proxmox-01",
+ nodeId: data.node_id || "pve-node-01",
+ }))
+ }
+ } catch (error) {
+ console.log("[v0] Using default server name due to API error:", error)
+ }
+ }
+
+ fetchServerInfo()
+ }, [])
+
+ const refreshData = async () => {
+ setIsRefreshing(true)
+ await new Promise((resolve) => setTimeout(resolve, 1000))
+ setSystemStatus((prev) => ({
+ ...prev,
+ lastUpdate: new Date().toLocaleTimeString(),
+ }))
+ setIsRefreshing(false)
+ }
+
+ const translatePage = async () => {
+ setIsTranslating(true)
+ try {
+ if ("translate" in document.documentElement.dataset) {
+ const currentLang = document.documentElement.dataset.translate
+ document.documentElement.dataset.translate = currentLang === "yes" ? "no" : "yes"
+ } else {
+ const googleTranslateScript = document.createElement("script")
+ googleTranslateScript.src = "https://translate.google.com/translate_a/element.js?cb=googleTranslateElementInit"
+ document.head.appendChild(googleTranslateScript)
+
+ window.googleTranslateElementInit = () => {
+ new window.google.translate.TranslateElement(
+ {
+ pageLanguage: "en",
+ includedLanguages: "es,en,fr,de,it,pt,ru,zh,ja,ko",
+ layout: window.google.translate.TranslateElement.InlineLayout.SIMPLE,
+ },
+ "google_translate_element",
+ )
+ }
+ }
+ } catch (error) {
+ console.error("Translation error:", error)
+ }
+ setIsTranslating(false)
+ }
+
+ const getStatusIcon = () => {
+ switch (systemStatus.status) {
+ case "healthy":
+ return
+ case "warning":
+ return
+ case "critical":
+ return
+ }
+ }
+
+ const getStatusColor = () => {
+ 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"
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
ProxMenux Monitor
+
Proxmox System Dashboard
+
+
+
+
+
+
+
{systemStatus.nodeId}
+
+
+
+
+
+
+
+ {getStatusIcon()}
+ {systemStatus.status}
+
+
+
Uptime: {systemStatus.uptime}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Overview
+
+
+ Storage
+
+
+ Network
+
+
+ Virtual Machines
+
+
+ System Logs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/AppImage/components/storage-metrics.tsx b/AppImage/components/storage-metrics.tsx
new file mode 100644
index 0000000..b22a03f
--- /dev/null
+++ b/AppImage/components/storage-metrics.tsx
@@ -0,0 +1,243 @@
+"use client"
+
+import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
+import { Progress } from "./ui/progress"
+import { Badge } from "./ui/badge"
+import { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip } from "recharts"
+import { HardDrive, Database, Archive, AlertTriangle, CheckCircle, Activity } from "lucide-react"
+
+const storageData = [
+ { name: "Used", value: 1250, color: "#3b82f6" }, // Blue
+ { name: "Available", value: 750, color: "#10b981" }, // Green
+]
+
+const diskPerformance = [
+ { disk: "sda", read: 45, write: 32, iops: 1250 },
+ { disk: "sdb", read: 67, write: 28, iops: 980 },
+ { disk: "sdc", read: 23, write: 45, iops: 1100 },
+ { disk: "nvme0n1", read: 156, write: 89, iops: 3400 },
+]
+
+export function StorageMetrics() {
+ return (
+
+ {/* Storage Overview Cards */}
+
+
+
+ Total Storage
+
+
+
+ 2.0 TB
+
+ 1.25 TB used • 750 GB available
+
+
+
+
+
+ VM & LXC Storage
+
+
+
+ 890 GB
+
+ 71.2% of allocated space
+
+
+
+
+
+
+
+ Backups
+
+
+
+ 245 GB
+
+
+ 12 Backups
+
+
+ Last backup: 2h ago
+
+
+
+
+
+
+
+ IOPS
+
+
+
+ 6.7K
+
+ Read: 4.2K
+ Write: 2.5K
+
+ Average operations/sec
+
+
+
+
+ {/* Storage Distribution and Performance */}
+
+
+
+
+
+ Storage Distribution
+
+
+
+
+
+
+ Used Storage
+ 1.25 TB (62.5%)
+
+
+
+
+
+
+ Available Storage
+ 750 GB (37.5%)
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Disk Performance
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Disk Details */}
+
+
+
+
+ Storage Devices
+
+
+
+
+ {[
+ { name: "/dev/sda", type: "HDD", size: "1TB", used: "650GB", health: "healthy", temp: "42°C" },
+ { name: "/dev/sdb", type: "HDD", size: "1TB", used: "480GB", health: "healthy", temp: "38°C" },
+ { name: "/dev/sdc", type: "SSD", size: "500GB", used: "120GB", health: "healthy", temp: "35°C" },
+ { name: "/dev/nvme0n1", type: "NVMe", size: "1TB", used: "340GB", health: "warning", temp: "55°C" },
+ ].map((disk, index) => (
+
+
+
+
+
{disk.name}
+
+ {disk.type} • {disk.size}
+
+
+
+
+
+
+
+ {disk.used} / {disk.size}
+
+
+
+
+
+
+
+ {disk.health === "healthy" ? (
+
+ ) : (
+
+ )}
+ {disk.health}
+
+
+
+ ))}
+
+
+
+
+ )
+}
diff --git a/AppImage/components/system-logs.tsx b/AppImage/components/system-logs.tsx
new file mode 100644
index 0000000..5ef052b
--- /dev/null
+++ b/AppImage/components/system-logs.tsx
@@ -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
+ case "warning":
+ return
+ case "info":
+ return
+ default:
+ return
+ }
+ }
+
+ 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 (
+
+ {/* Log Statistics */}
+
+
+
+ Total Logs
+
+
+
+ {logCounts.total}
+ Last 24 hours
+
+
+
+
+
+ Errors
+
+
+
+ {logCounts.error}
+ Requires attention
+
+
+
+
+
+ Warnings
+
+
+
+ {logCounts.warning}
+ Monitor closely
+
+
+
+
+
+ Info
+
+
+
+ {logCounts.info}
+ Normal operations
+
+
+
+
+ {/* Log Filters and Search */}
+
+
+
+
+ System Logs
+
+
+
+
+
+
+
+ setSearchTerm(e.target.value)}
+ className="pl-10 bg-background border-border"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ {filteredLogs.map((log, index) => (
+
+
+
+ {getLevelIcon(log.level)}
+ {log.level.toUpperCase()}
+
+
+
+
+
+
{log.service}
+
{log.timestamp}
+
+
{log.message}
+
Source: {log.source}
+
+
+ ))}
+
+ {filteredLogs.length === 0 && (
+
+
+
No logs found matching your criteria
+
+ )}
+
+
+
+
+
+ )
+}
diff --git a/AppImage/components/system-overview.tsx b/AppImage/components/system-overview.tsx
new file mode 100644
index 0000000..5c9b319
--- /dev/null
+++ b/AppImage/components/system-overview.tsx
@@ -0,0 +1,249 @@
+"use client"
+
+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, Users, Activity, Server, Zap } from "lucide-react"
+
+const cpuData = [
+ { time: "00:00", value: 45 },
+ { time: "04:00", value: 52 },
+ { time: "08:00", value: 78 },
+ { time: "12:00", value: 65 },
+ { time: "16:00", value: 82 },
+ { time: "20:00", value: 58 },
+ { time: "24:00", value: 43 },
+]
+
+const memoryData = [
+ { time: "00:00", used: 12.5, available: 19.5 },
+ { time: "04:00", used: 14.2, available: 17.8 },
+ { time: "08:00", used: 18.7, available: 13.3 },
+ { time: "12:00", used: 16.3, available: 15.7 },
+ { time: "16:00", used: 21.1, available: 10.9 },
+ { time: "20:00", used: 15.8, available: 16.2 },
+ { time: "24:00", used: 13.2, available: 18.8 },
+]
+
+export function SystemOverview() {
+ return (
+
+ {/* Key Metrics Cards */}
+
+
+
+ CPU Usage
+
+
+
+ 67.3%
+
+
+ ↓ 2.1% from last hour
+
+
+
+
+
+
+ Memory Usage
+
+
+
+ 15.8 GB
+
+
+ 49.4% of 32 GB • ↑ 1.2 GB
+
+
+
+
+
+
+ Temperature
+
+
+
+ 52°C
+
+
+ Normal
+
+
+ Max: 78°C • Avg: 48°C
+
+
+
+
+
+ Active VMs & LXC
+
+
+
+ 15
+
+
+ 8 Running VMs
+
+
+ 3 Running LXC
+
+
+ 4 Stopped
+
+
+ Total: 12 VMs • 6 LXC configured
+
+
+
+
+ {/* Charts Section */}
+
+
+
+
+
+ CPU Usage (24h)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Memory Usage (24h)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* System Information */}
+
+
+
+
+
+ System Information
+
+
+
+
+ Hostname:
+ proxmox-01
+
+
+ Version:
+ PVE 8.1.3
+
+
+ Kernel:
+ 6.5.11-7-pve
+
+
+ Architecture:
+ x86_64
+
+
+
+
+
+
+
+
+ Active Sessions
+
+
+
+
+ Web Console:
+
+ 3 active
+
+
+
+ SSH Sessions:
+
+ 1 active
+
+
+
+ API Calls:
+ 247/hour
+
+
+
+
+
+
+
+
+ Power & Performance
+
+
+
+
+ Power State:
+
+ Running
+
+
+
+ Load Average:
+ 1.23, 1.45, 1.67
+
+
+ Boot Time:
+ 2.3s
+
+
+
+
+
+ )
+}
diff --git a/AppImage/components/theme-toggle.tsx b/AppImage/components/theme-toggle.tsx
new file mode 100644
index 0000000..eb4c91c
--- /dev/null
+++ b/AppImage/components/theme-toggle.tsx
@@ -0,0 +1,22 @@
+"use client"
+import { Moon, Sun } from "lucide-react"
+import { useTheme } from "next-themes"
+
+import { Button } from "./ui/button"
+
+export function ThemeToggle() {
+ const { theme, setTheme } = useTheme()
+
+ return (
+
+ )
+}
diff --git a/AppImage/components/ui/accordion.tsx b/AppImage/components/ui/accordion.tsx
deleted file mode 100644
index e538a33..0000000
--- a/AppImage/components/ui/accordion.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-'use client'
-
-import * as React from 'react'
-import * as AccordionPrimitive from '@radix-ui/react-accordion'
-import { ChevronDownIcon } from 'lucide-react'
-
-import { cn } from '@/lib/utils'
-
-function Accordion({
- ...props
-}: React.ComponentProps) {
- return
-}
-
-function AccordionItem({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- )
-}
-
-function AccordionTrigger({
- className,
- children,
- ...props
-}: React.ComponentProps) {
- return (
-
- svg]:rotate-180',
- className,
- )}
- {...props}
- >
- {children}
-
-
-
- )
-}
-
-function AccordionContent({
- className,
- children,
- ...props
-}: React.ComponentProps) {
- return (
-
- {children}
-
- )
-}
-
-export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
diff --git a/AppImage/components/ui/alert-dialog.tsx b/AppImage/components/ui/alert-dialog.tsx
deleted file mode 100644
index 9704452..0000000
--- a/AppImage/components/ui/alert-dialog.tsx
+++ /dev/null
@@ -1,157 +0,0 @@
-'use client'
-
-import * as React from 'react'
-import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
-
-import { cn } from '@/lib/utils'
-import { buttonVariants } from '@/components/ui/button'
-
-function AlertDialog({
- ...props
-}: React.ComponentProps) {
- return
-}
-
-function AlertDialogTrigger({
- ...props
-}: React.ComponentProps) {
- return (
-
- )
-}
-
-function AlertDialogPortal({
- ...props
-}: React.ComponentProps) {
- return (
-
- )
-}
-
-function AlertDialogOverlay({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- )
-}
-
-function AlertDialogContent({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
-
-
-
- )
-}
-
-function AlertDialogHeader({
- className,
- ...props
-}: React.ComponentProps<'div'>) {
- return (
-
- )
-}
-
-function AlertDialogFooter({
- className,
- ...props
-}: React.ComponentProps<'div'>) {
- return (
-
- )
-}
-
-function AlertDialogTitle({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- )
-}
-
-function AlertDialogDescription({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- )
-}
-
-function AlertDialogAction({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- )
-}
-
-function AlertDialogCancel({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- )
-}
-
-export {
- AlertDialog,
- AlertDialogPortal,
- AlertDialogOverlay,
- AlertDialogTrigger,
- AlertDialogContent,
- AlertDialogHeader,
- AlertDialogFooter,
- AlertDialogTitle,
- AlertDialogDescription,
- AlertDialogAction,
- AlertDialogCancel,
-}
diff --git a/AppImage/components/ui/alert.tsx b/AppImage/components/ui/alert.tsx
deleted file mode 100644
index e6751ab..0000000
--- a/AppImage/components/ui/alert.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import * as React from 'react'
-import { cva, type VariantProps } from 'class-variance-authority'
-
-import { cn } from '@/lib/utils'
-
-const alertVariants = cva(
- 'relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current',
- {
- variants: {
- variant: {
- default: 'bg-card text-card-foreground',
- destructive:
- 'text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90',
- },
- },
- defaultVariants: {
- variant: 'default',
- },
- },
-)
-
-function Alert({
- className,
- variant,
- ...props
-}: React.ComponentProps<'div'> & VariantProps) {
- return (
-
- )
-}
-
-function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) {
- return (
-
- )
-}
-
-function AlertDescription({
- className,
- ...props
-}: React.ComponentProps<'div'>) {
- return (
-
- )
-}
-
-export { Alert, AlertTitle, AlertDescription }
diff --git a/AppImage/components/ui/aspect-ratio.tsx b/AppImage/components/ui/aspect-ratio.tsx
deleted file mode 100644
index 40bb120..0000000
--- a/AppImage/components/ui/aspect-ratio.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-'use client'
-
-import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio'
-
-function AspectRatio({
- ...props
-}: React.ComponentProps) {
- return
-}
-
-export { AspectRatio }
diff --git a/AppImage/components/ui/avatar.tsx b/AppImage/components/ui/avatar.tsx
deleted file mode 100644
index aa98465..0000000
--- a/AppImage/components/ui/avatar.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-'use client'
-
-import * as React from 'react'
-import * as AvatarPrimitive from '@radix-ui/react-avatar'
-
-import { cn } from '@/lib/utils'
-
-function Avatar({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- )
-}
-
-function AvatarImage({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- )
-}
-
-function AvatarFallback({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- )
-}
-
-export { Avatar, AvatarImage, AvatarFallback }
diff --git a/AppImage/components/ui/badge.tsx b/AppImage/components/ui/badge.tsx
index fc4126b..78b3863 100644
--- a/AppImage/components/ui/badge.tsx
+++ b/AppImage/components/ui/badge.tsx
@@ -1,46 +1,28 @@
-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'
+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 justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',
+ "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 [a&]:hover:bg-primary/90',
- secondary:
- 'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
- destructive:
- 'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
- outline:
- 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
+ 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',
+ variant: "default",
},
},
)
-function Badge({
- className,
- variant,
- asChild = false,
- ...props
-}: React.ComponentProps<'span'> &
- VariantProps & { asChild?: boolean }) {
- const Comp = asChild ? Slot : 'span'
+export interface BadgeProps extends React.HTMLAttributes, VariantProps {}
- return (
-
- )
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return
}
export { Badge, badgeVariants }
diff --git a/AppImage/components/ui/breadcrumb.tsx b/AppImage/components/ui/breadcrumb.tsx
deleted file mode 100644
index 1750ff2..0000000
--- a/AppImage/components/ui/breadcrumb.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-import * as React from 'react'
-import { Slot } from '@radix-ui/react-slot'
-import { ChevronRight, MoreHorizontal } from 'lucide-react'
-
-import { cn } from '@/lib/utils'
-
-function Breadcrumb({ ...props }: React.ComponentProps<'nav'>) {
- return
-}
-
-function BreadcrumbList({ className, ...props }: React.ComponentProps<'ol'>) {
- return (
-
- )
-}
-
-function BreadcrumbItem({ className, ...props }: React.ComponentProps<'li'>) {
- return (
-
- )
-}
-
-function BreadcrumbLink({
- asChild,
- className,
- ...props
-}: React.ComponentProps<'a'> & {
- asChild?: boolean
-}) {
- const Comp = asChild ? Slot : 'a'
-
- return (
-
- )
-}
-
-function BreadcrumbPage({ className, ...props }: React.ComponentProps<'span'>) {
- return (
-
- )
-}
-
-function BreadcrumbSeparator({
- children,
- className,
- ...props
-}: React.ComponentProps<'li'>) {
- return (
- svg]:size-3.5', className)}
- {...props}
- >
- {children ?? }
-
- )
-}
-
-function BreadcrumbEllipsis({
- className,
- ...props
-}: React.ComponentProps<'span'>) {
- return (
-
-
- More
-
- )
-}
-
-export {
- Breadcrumb,
- BreadcrumbList,
- BreadcrumbItem,
- BreadcrumbLink,
- BreadcrumbPage,
- BreadcrumbSeparator,
- BreadcrumbEllipsis,
-}
diff --git a/AppImage/components/ui/button.tsx b/AppImage/components/ui/button.tsx
index 815443b..495dba8 100644
--- a/AppImage/components/ui/button.tsx
+++ b/AppImage/components/ui/button.tsx
@@ -1,59 +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'
+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 gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+ "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 shadow-xs hover:bg-primary/90',
- destructive:
- 'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
- outline:
- 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
- secondary:
- 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
- ghost:
- 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
- link: 'text-primary underline-offset-4 hover:underline',
+ 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-9 px-4 py-2 has-[>svg]:px-3',
- sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
- lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
- icon: 'size-9',
+ 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',
+ variant: "default",
+ size: "default",
},
},
)
-function Button({
- className,
- variant,
- size,
- asChild = false,
- ...props
-}: React.ComponentProps<'button'> &
- VariantProps & {
- asChild?: boolean
- }) {
- const Comp = asChild ? Slot : 'button'
-
- return (
-
- )
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean
}
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+ return
+ },
+)
+Button.displayName = "Button"
+
export { Button, buttonVariants }
diff --git a/AppImage/components/ui/calendar.tsx b/AppImage/components/ui/calendar.tsx
deleted file mode 100644
index eaa373e..0000000
--- a/AppImage/components/ui/calendar.tsx
+++ /dev/null
@@ -1,213 +0,0 @@
-'use client'
-
-import * as React from 'react'
-import {
- ChevronDownIcon,
- ChevronLeftIcon,
- ChevronRightIcon,
-} from 'lucide-react'
-import { DayButton, DayPicker, getDefaultClassNames } from 'react-day-picker'
-
-import { cn } from '@/lib/utils'
-import { Button, buttonVariants } from '@/components/ui/button'
-
-function Calendar({
- className,
- classNames,
- showOutsideDays = true,
- captionLayout = 'label',
- buttonVariant = 'ghost',
- formatters,
- components,
- ...props
-}: React.ComponentProps & {
- buttonVariant?: React.ComponentProps['variant']
-}) {
- const defaultClassNames = getDefaultClassNames()
-
- return (
- svg]:rotate-180`,
- String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
- className,
- )}
- captionLayout={captionLayout}
- formatters={{
- formatMonthDropdown: (date) =>
- date.toLocaleString('default', { month: 'short' }),
- ...formatters,
- }}
- classNames={{
- root: cn('w-fit', defaultClassNames.root),
- months: cn(
- 'flex gap-4 flex-col md:flex-row relative',
- defaultClassNames.months,
- ),
- month: cn('flex flex-col w-full gap-4', defaultClassNames.month),
- nav: cn(
- 'flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between',
- defaultClassNames.nav,
- ),
- button_previous: cn(
- buttonVariants({ variant: buttonVariant }),
- 'size-(--cell-size) aria-disabled:opacity-50 p-0 select-none',
- defaultClassNames.button_previous,
- ),
- button_next: cn(
- buttonVariants({ variant: buttonVariant }),
- 'size-(--cell-size) aria-disabled:opacity-50 p-0 select-none',
- defaultClassNames.button_next,
- ),
- month_caption: cn(
- 'flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)',
- defaultClassNames.month_caption,
- ),
- dropdowns: cn(
- 'w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5',
- defaultClassNames.dropdowns,
- ),
- dropdown_root: cn(
- 'relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md',
- defaultClassNames.dropdown_root,
- ),
- dropdown: cn(
- 'absolute bg-popover inset-0 opacity-0',
- defaultClassNames.dropdown,
- ),
- caption_label: cn(
- 'select-none font-medium',
- captionLayout === 'label'
- ? 'text-sm'
- : 'rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5',
- defaultClassNames.caption_label,
- ),
- table: 'w-full border-collapse',
- weekdays: cn('flex', defaultClassNames.weekdays),
- weekday: cn(
- 'text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none',
- defaultClassNames.weekday,
- ),
- week: cn('flex w-full mt-2', defaultClassNames.week),
- week_number_header: cn(
- 'select-none w-(--cell-size)',
- defaultClassNames.week_number_header,
- ),
- week_number: cn(
- 'text-[0.8rem] select-none text-muted-foreground',
- defaultClassNames.week_number,
- ),
- day: cn(
- 'relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none',
- defaultClassNames.day,
- ),
- range_start: cn(
- 'rounded-l-md bg-accent',
- defaultClassNames.range_start,
- ),
- range_middle: cn('rounded-none', defaultClassNames.range_middle),
- range_end: cn('rounded-r-md bg-accent', defaultClassNames.range_end),
- today: cn(
- 'bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none',
- defaultClassNames.today,
- ),
- outside: cn(
- 'text-muted-foreground aria-selected:text-muted-foreground',
- defaultClassNames.outside,
- ),
- disabled: cn(
- 'text-muted-foreground opacity-50',
- defaultClassNames.disabled,
- ),
- hidden: cn('invisible', defaultClassNames.hidden),
- ...classNames,
- }}
- components={{
- Root: ({ className, rootRef, ...props }) => {
- return (
-
- )
- },
- Chevron: ({ className, orientation, ...props }) => {
- if (orientation === 'left') {
- return (
-
- )
- }
-
- if (orientation === 'right') {
- return (
-
- )
- }
-
- return (
-
- )
- },
- DayButton: CalendarDayButton,
- WeekNumber: ({ children, ...props }) => {
- return (
-
-
- {children}
-
- |
- )
- },
- ...components,
- }}
- {...props}
- />
- )
-}
-
-function CalendarDayButton({
- className,
- day,
- modifiers,
- ...props
-}: React.ComponentProps) {
- const defaultClassNames = getDefaultClassNames()
-
- const ref = React.useRef(null)
- React.useEffect(() => {
- if (modifiers.focused) ref.current?.focus()
- }, [modifiers.focused])
-
- return (
-