Update AppImage

This commit is contained in:
MacRimi
2025-10-05 13:50:29 +02:00
parent 18ccff5759
commit 49050c042d
3 changed files with 145 additions and 140 deletions

View File

@@ -4,7 +4,7 @@ import { useState } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card" import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
import { Badge } from "./ui/badge" import { Badge } from "./ui/badge"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./ui/dialog" import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./ui/dialog"
import { Wifi, Globe, Shield, Activity, Network, Router, AlertCircle, Zap } from "lucide-react" import { Wifi, Activity, Network, Router, AlertCircle, Zap } from "lucide-react"
import useSWR from "swr" import useSWR from "swr"
interface NetworkData { interface NetworkData {
@@ -57,6 +57,7 @@ interface NetworkInterface {
bond_active_slave?: string | null bond_active_slave?: string | null
bridge_members?: string[] bridge_members?: string[]
bridge_physical_interface?: string bridge_physical_interface?: string
bridge_bond_slaves?: string[]
packet_loss_in?: number packet_loss_in?: number
packet_loss_out?: number packet_loss_out?: number
vmid?: number vmid?: number
@@ -213,45 +214,6 @@ export function NetworkMetrics() {
</CardContent> </CardContent>
</Card> </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 className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Packets</CardTitle>
<Globe className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-foreground">{packetsRecvK}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>
{networkData.traffic.packet_loss_in !== undefined && networkData.traffic.packet_loss_in > 0 && (
<p className="text-xs text-yellow-500 mt-2">Loss: {networkData.traffic.packet_loss_in}%</p>
)}
{(!networkData.traffic.packet_loss_in || networkData.traffic.packet_loss_in === 0) && (
<p className="text-xs text-muted-foreground mt-2">No packet loss</p>
)}
</CardContent>
</Card>
</div>
{networkData.physical_interfaces && networkData.physical_interfaces.length > 0 && (
<Card className="bg-card border-border"> <Card className="bg-card border-border">
<CardHeader> <CardHeader>
<CardTitle className="text-foreground flex items-center"> <CardTitle className="text-foreground flex items-center">
@@ -335,7 +297,7 @@ export function NetworkMetrics() {
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
)} </div>
{networkData.bridge_interfaces && networkData.bridge_interfaces.length > 0 && ( {networkData.bridge_interfaces && networkData.bridge_interfaces.length > 0 && (
<Card className="bg-card border-border"> <Card className="bg-card border-border">
@@ -368,7 +330,7 @@ export function NetworkMetrics() {
{typeBadge.label} {typeBadge.label}
</Badge> </Badge>
{interface_.bridge_physical_interface && ( {interface_.bridge_physical_interface && (
<div className="text-sm text-blue-500 font-medium flex items-center gap-1"> <div className="text-sm text-blue-500 font-medium flex items-center gap-1 flex-wrap">
{interface_.bridge_physical_interface} {interface_.bridge_physical_interface}
{interface_.bridge_physical_interface.startsWith("bond") && {interface_.bridge_physical_interface.startsWith("bond") &&
networkData.physical_interfaces && ( networkData.physical_interfaces && (
@@ -388,6 +350,11 @@ export function NetworkMetrics() {
})()} })()}
</> </>
)} )}
{interface_.bridge_bond_slaves && interface_.bridge_bond_slaves.length > 0 && (
<span className="text-muted-foreground text-xs">
({interface_.bridge_bond_slaves.join(", ")})
</span>
)}
</div> </div>
)} )}
</div> </div>
@@ -732,7 +699,15 @@ export function NetworkMetrics() {
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{selectedInterface.bridge_members.length > 0 ? ( {selectedInterface.bridge_members.length > 0 ? (
selectedInterface.bridge_members selectedInterface.bridge_members
.filter((member) => !member.startsWith(("enp", "eth", "eno", "ens", "wlan", "wlp"))) .filter(
(member) =>
!member.startsWith("enp") &&
!member.startsWith("eth") &&
!member.startsWith("eno") &&
!member.startsWith("ens") &&
!member.startsWith("wlan") &&
!member.startsWith("wlp"),
)
.map((member, idx) => ( .map((member, idx) => (
<Badge <Badge
key={idx} key={idx}

View File

@@ -17,7 +17,6 @@ import {
Network, Network,
Power, Power,
RotateCcw, RotateCcw,
Download,
StopCircle, StopCircle,
} from "lucide-react" } from "lucide-react"
import useSWR from "swr" import useSWR from "swr"
@@ -426,13 +425,13 @@ export function VirtualMachines() {
setVMDetails(null) setVMDetails(null)
}} }}
> >
<DialogContent className="max-w-3xl max-h-[90vh] overflow-hidden flex flex-col"> <DialogContent className="max-w-4xl max-h-[95vh] overflow-y-auto">
<DialogHeader className="flex-shrink-0 pb-4 border-b border-border"> <DialogHeader className="pb-4 border-b border-border">
<DialogTitle className="flex items-center gap-2"> <DialogTitle className="flex items-center gap-2 flex-wrap">
<Server className="h-5 w-5" /> <Server className="h-5 w-5" />
{selectedVM?.name} <span className="text-lg">{selectedVM?.name}</span>
{selectedVM && ( {selectedVM && (
<div className="flex items-center gap-2 ml-auto"> <div className="flex items-center gap-2">
<Badge variant="outline" className={getTypeBadge(selectedVM.type).color}> <Badge variant="outline" className={getTypeBadge(selectedVM.type).color}>
{getTypeBadge(selectedVM.type).label} {getTypeBadge(selectedVM.type).label}
</Badge> </Badge>
@@ -444,36 +443,72 @@ export function VirtualMachines() {
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
<div className="flex-1 overflow-y-auto space-y-6 py-4"> <div className="space-y-6 py-4">
{selectedVM && ( {selectedVM && (
<> <>
{/* Basic Information */} {/* Basic Information */}
<div> <div>
<h3 className="text-sm font-semibold text-muted-foreground mb-3">Basic Information</h3> <h3 className="text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> Basic Information
</h3>
<div className="grid grid-cols-2 lg:grid-cols-3 gap-4">
<div> <div>
<div className="text-sm text-muted-foreground">VMID</div> <div className="text-xs text-muted-foreground mb-1">Name</div>
<div className="font-medium text-foreground">{selectedVM.vmid}</div> <div className="font-semibold text-foreground">{selectedVM.name}</div>
</div> </div>
<div> <div>
<div className="text-sm text-muted-foreground">CPU Usage</div> <div className="text-xs text-muted-foreground mb-1">Type</div>
<div className="font-medium text-foreground">{(selectedVM.cpu * 100).toFixed(1)}%</div> <Badge variant="outline" className={getTypeBadge(selectedVM.type).color}>
{getTypeBadge(selectedVM.type).label}
</Badge>
</div> </div>
<div> <div>
<div className="text-sm text-muted-foreground">Memory</div> <div className="text-xs text-muted-foreground mb-1">VMID</div>
<div className="font-medium text-foreground"> <div className="font-semibold text-foreground">{selectedVM.vmid}</div>
</div>
<div>
<div className="text-xs text-muted-foreground mb-1">Status</div>
<Badge variant="outline" className={getStatusColor(selectedVM.status)}>
{selectedVM.status.toUpperCase()}
</Badge>
</div>
<div>
<div className="text-xs text-muted-foreground mb-1">CPU Usage</div>
<div
className={`font-semibold ${
(selectedVM.cpu * 100) > 80
? "text-red-500"
: selectedVM.cpu * 100 > 60
? "text-yellow-500"
: "text-green-500"
}`}
>
{(selectedVM.cpu * 100).toFixed(1)}%
</div>
</div>
<div>
<div className="text-xs text-muted-foreground mb-1">Memory</div>
<div
className={`font-semibold ${
((selectedVM.mem / selectedVM.maxmem) * 100) > 80
? "text-red-500"
: (selectedVM.mem / selectedVM.maxmem) * 100 > 60
? "text-yellow-500"
: "text-blue-500"
}`}
>
{(selectedVM.mem / 1024 ** 3).toFixed(1)} / {(selectedVM.maxmem / 1024 ** 3).toFixed(1)} GB {(selectedVM.mem / 1024 ** 3).toFixed(1)} / {(selectedVM.maxmem / 1024 ** 3).toFixed(1)} GB
</div> </div>
</div> </div>
<div> <div>
<div className="text-sm text-muted-foreground">Disk</div> <div className="text-xs text-muted-foreground mb-1">Disk</div>
<div className="font-medium text-foreground"> <div className="font-semibold text-foreground">
{(selectedVM.disk / 1024 ** 3).toFixed(1)} / {(selectedVM.maxdisk / 1024 ** 3).toFixed(1)} GB {(selectedVM.disk / 1024 ** 3).toFixed(1)} / {(selectedVM.maxdisk / 1024 ** 3).toFixed(1)} GB
</div> </div>
</div> </div>
<div> <div>
<div className="text-sm text-muted-foreground">Uptime</div> <div className="text-xs text-muted-foreground mb-1">Uptime</div>
<div className="font-medium text-foreground">{formatUptime(selectedVM.uptime)}</div> <div className="font-semibold text-foreground">{formatUptime(selectedVM.uptime)}</div>
</div> </div>
</div> </div>
</div> </div>
@@ -484,94 +519,90 @@ export function VirtualMachines() {
) : vmDetails?.config ? ( ) : vmDetails?.config ? (
<> <>
<div> <div>
<h3 className="text-sm font-semibold text-muted-foreground mb-3">Resources</h3> <h3 className="text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> Resources
</h3>
<div className="grid grid-cols-2 lg:grid-cols-3 gap-4">
{vmDetails.config.cores && ( {vmDetails.config.cores && (
<div> <div>
<div className="text-sm text-muted-foreground">CPU Cores</div> <div className="text-xs text-muted-foreground mb-1">CPU Cores</div>
<div className="font-medium text-foreground">{vmDetails.config.cores}</div> <div className="font-semibold text-blue-500">{vmDetails.config.cores}</div>
</div> </div>
)} )}
{vmDetails.config.sockets && ( {vmDetails.config.sockets && (
<div> <div>
<div className="text-sm text-muted-foreground">CPU Sockets</div> <div className="text-xs text-muted-foreground mb-1">CPU Sockets</div>
<div className="font-medium text-foreground">{vmDetails.config.sockets}</div> <div className="font-semibold text-foreground">{vmDetails.config.sockets}</div>
</div> </div>
)} )}
{vmDetails.config.memory && ( {vmDetails.config.memory && (
<div> <div>
<div className="text-sm text-muted-foreground">Memory</div> <div className="text-xs text-muted-foreground mb-1">Memory</div>
<div className="font-medium text-foreground">{vmDetails.config.memory} MB</div> <div className="font-semibold text-blue-500">{vmDetails.config.memory} MB</div>
</div> </div>
)} )}
{vmDetails.config.swap && ( {vmDetails.config.swap && (
<div> <div>
<div className="text-sm text-muted-foreground">Swap</div> <div className="text-xs text-muted-foreground mb-1">Swap</div>
<div className="font-medium text-foreground">{vmDetails.config.swap} MB</div> <div className="font-semibold text-foreground">{vmDetails.config.swap} MB</div>
</div> </div>
)} )}
{vmDetails.config.rootfs && ( {vmDetails.config.rootfs && (
<div className="col-span-1 sm:col-span-2"> <div className="col-span-2 lg:col-span-3">
<div className="text-sm text-muted-foreground">Root Filesystem</div> <div className="text-xs text-muted-foreground mb-1">Root Filesystem</div>
<div className="font-medium text-foreground text-sm break-all"> <div className="font-medium text-foreground text-sm break-all font-mono">
{vmDetails.config.rootfs} {vmDetails.config.rootfs}
</div> </div>
</div> </div>
)} )}
{vmDetails.config.scsi0 && ( {Object.keys(vmDetails.config)
<div className="col-span-1 sm:col-span-2"> .filter((key) => key.match(/^(scsi|sata|ide|virtio)\d+$/))
<div className="text-sm text-muted-foreground">SCSI Disk 0</div> .map((diskKey) => (
<div className="font-medium text-foreground text-sm break-all"> <div key={diskKey} className="col-span-2 lg:col-span-3">
{vmDetails.config.scsi0} <div className="text-xs text-muted-foreground mb-1">
{diskKey.toUpperCase().replace(/(\d+)/, " $1")}
</div>
<div className="font-medium text-foreground text-sm break-all font-mono">
{vmDetails.config[diskKey]}
</div> </div>
</div> </div>
)} ))}
{vmDetails.config.ide0 && (
<div className="col-span-1 sm:col-span-2">
<div className="text-sm text-muted-foreground">IDE Disk 0</div>
<div className="font-medium text-foreground text-sm break-all">{vmDetails.config.ide0}</div>
</div>
)}
</div> </div>
</div> </div>
{/* Network Configuration */} {/* Network Configuration */}
<div> <div>
<h3 className="text-sm font-semibold text-muted-foreground mb-3">Network</h3> <h3 className="text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide">
<div className="grid grid-cols-1 gap-4"> Network
{vmDetails.config.net0 && ( </h3>
<div> <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div className="text-sm text-muted-foreground">Network Interface 0</div> {Object.keys(vmDetails.config)
<div className="font-medium text-foreground text-sm break-all">{vmDetails.config.net0}</div> .filter((key) => key.match(/^net\d+$/))
.map((netKey) => (
<div key={netKey} className="col-span-1">
<div className="text-xs text-muted-foreground mb-1">
Network Interface {netKey.replace("net", "")}
</div> </div>
)} <div className="font-medium text-green-500 text-sm break-all font-mono">
{vmDetails.config.net1 && ( {vmDetails.config[netKey]}
<div>
<div className="text-sm text-muted-foreground">Network Interface 1</div>
<div className="font-medium text-foreground text-sm break-all">{vmDetails.config.net1}</div>
</div> </div>
)}
{vmDetails.config.net2 && (
<div>
<div className="text-sm text-muted-foreground">Network Interface 2</div>
<div className="font-medium text-foreground text-sm break-all">{vmDetails.config.net2}</div>
</div> </div>
)} ))}
{vmDetails.config.nameserver && ( {vmDetails.config.nameserver && (
<div> <div>
<div className="text-sm text-muted-foreground">DNS Nameserver</div> <div className="text-xs text-muted-foreground mb-1">DNS Nameserver</div>
<div className="font-medium text-foreground">{vmDetails.config.nameserver}</div> <div className="font-medium text-foreground font-mono">{vmDetails.config.nameserver}</div>
</div> </div>
)} )}
{vmDetails.config.searchdomain && ( {vmDetails.config.searchdomain && (
<div> <div>
<div className="text-sm text-muted-foreground">Search Domain</div> <div className="text-xs text-muted-foreground mb-1">Search Domain</div>
<div className="font-medium text-foreground">{vmDetails.config.searchdomain}</div> <div className="font-medium text-foreground">{vmDetails.config.searchdomain}</div>
</div> </div>
)} )}
{vmDetails.config.hostname && ( {vmDetails.config.hostname && (
<div> <div>
<div className="text-sm text-muted-foreground">Hostname</div> <div className="text-xs text-muted-foreground mb-1">Hostname</div>
<div className="font-medium text-foreground">{vmDetails.config.hostname}</div> <div className="font-medium text-foreground">{vmDetails.config.hostname}</div>
</div> </div>
)} )}
@@ -580,17 +611,19 @@ export function VirtualMachines() {
{/* Options */} {/* Options */}
<div> <div>
<h3 className="text-sm font-semibold text-muted-foreground mb-3">Options</h3> <h3 className="text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> Options
</h3>
<div className="grid grid-cols-2 lg:grid-cols-3 gap-4">
{vmDetails.config.onboot !== undefined && ( {vmDetails.config.onboot !== undefined && (
<div> <div>
<div className="text-sm text-muted-foreground">Start on Boot</div> <div className="text-xs text-muted-foreground mb-1">Start on Boot</div>
<Badge <Badge
variant="outline" variant="outline"
className={ className={
vmDetails.config.onboot vmDetails.config.onboot
? "bg-green-500/10 text-green-500" ? "bg-green-500/10 text-green-500 border-green-500/20"
: "bg-red-500/10 text-red-500" : "bg-red-500/10 text-red-500 border-red-500/20"
} }
> >
{vmDetails.config.onboot ? "Yes" : "No"} {vmDetails.config.onboot ? "Yes" : "No"}
@@ -599,13 +632,13 @@ export function VirtualMachines() {
)} )}
{vmDetails.config.unprivileged !== undefined && ( {vmDetails.config.unprivileged !== undefined && (
<div> <div>
<div className="text-sm text-muted-foreground">Unprivileged</div> <div className="text-xs text-muted-foreground mb-1">Unprivileged</div>
<Badge <Badge
variant="outline" variant="outline"
className={ className={
vmDetails.config.unprivileged vmDetails.config.unprivileged
? "bg-green-500/10 text-green-500" ? "bg-green-500/10 text-green-500 border-green-500/20"
: "bg-yellow-500/10 text-yellow-500" : "bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
} }
> >
{vmDetails.config.unprivileged ? "Yes" : "No"} {vmDetails.config.unprivileged ? "Yes" : "No"}
@@ -614,25 +647,25 @@ export function VirtualMachines() {
)} )}
{vmDetails.config.ostype && ( {vmDetails.config.ostype && (
<div> <div>
<div className="text-sm text-muted-foreground">OS Type</div> <div className="text-xs text-muted-foreground mb-1">OS Type</div>
<div className="font-medium text-foreground">{vmDetails.config.ostype}</div> <div className="font-medium text-foreground">{vmDetails.config.ostype}</div>
</div> </div>
)} )}
{vmDetails.config.arch && ( {vmDetails.config.arch && (
<div> <div>
<div className="text-sm text-muted-foreground">Architecture</div> <div className="text-xs text-muted-foreground mb-1">Architecture</div>
<div className="font-medium text-foreground">{vmDetails.config.arch}</div> <div className="font-medium text-foreground">{vmDetails.config.arch}</div>
</div> </div>
)} )}
{vmDetails.config.boot && ( {vmDetails.config.boot && (
<div> <div>
<div className="text-sm text-muted-foreground">Boot Order</div> <div className="text-xs text-muted-foreground mb-1">Boot Order</div>
<div className="font-medium text-foreground">{vmDetails.config.boot}</div> <div className="font-medium text-foreground">{vmDetails.config.boot}</div>
</div> </div>
)} )}
{vmDetails.config.features && ( {vmDetails.config.features && (
<div className="col-span-1 sm:col-span-2"> <div className="col-span-2 lg:col-span-3">
<div className="text-sm text-muted-foreground">Features</div> <div className="text-xs text-muted-foreground mb-1">Features</div>
<div className="font-medium text-foreground text-sm">{vmDetails.config.features}</div> <div className="font-medium text-foreground text-sm">{vmDetails.config.features}</div>
</div> </div>
)} )}
@@ -643,7 +676,9 @@ export function VirtualMachines() {
{/* Control Actions */} {/* Control Actions */}
<div> <div>
<h3 className="text-sm font-semibold text-muted-foreground mb-3">Control Actions</h3> <h3 className="text-sm font-semibold text-muted-foreground mb-3 uppercase tracking-wide">
Control Actions
</h3>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<Button <Button
variant="outline" variant="outline"
@@ -683,18 +718,6 @@ export function VirtualMachines() {
</Button> </Button>
</div> </div>
</div> </div>
{/* Download Logs */}
<div>
<Button
variant="outline"
className="w-full bg-transparent"
onClick={() => handleDownloadLogs(selectedVM.vmid, selectedVM.name)}
>
<Download className="h-4 w-4 mr-2" />
Download Logs
</Button>
</div>
</> </>
)} )}
</div> </div>

View File

@@ -987,7 +987,9 @@ def get_bridge_info(bridge_name):
bridge_info = { bridge_info = {
'members': [], 'members': [],
'physical_interface': None, 'physical_interface': None,
'physical_duplex': 'unknown' # Added physical_duplex field 'physical_duplex': 'unknown', # Added physical_duplex field
# Added bond_slaves to show physical interfaces
'bond_slaves': []
} }
try: try:
@@ -1003,8 +1005,12 @@ def get_bridge_info(bridge_name):
bridge_info['physical_interface'] = member bridge_info['physical_interface'] = member
print(f"[v0] Bridge {bridge_name} connected to bond: {member}") print(f"[v0] Bridge {bridge_name} connected to bond: {member}")
# Get duplex from bond's active slave
bond_info = get_bond_info(member) bond_info = get_bond_info(member)
if bond_info['slaves']:
bridge_info['bond_slaves'] = bond_info['slaves']
print(f"[v0] Bond {member} slaves: {bond_info['slaves']}")
# Get duplex from bond's active slave
if bond_info['active_slave']: if bond_info['active_slave']:
try: try:
net_if_stats = psutil.net_if_stats() net_if_stats = psutil.net_if_stats()
@@ -1160,6 +1166,7 @@ def get_network_info():
interface_info['bridge_members'] = bridge_info['members'] interface_info['bridge_members'] = bridge_info['members']
interface_info['bridge_physical_interface'] = bridge_info['physical_interface'] interface_info['bridge_physical_interface'] = bridge_info['physical_interface']
interface_info['bridge_physical_duplex'] = bridge_info['physical_duplex'] interface_info['bridge_physical_duplex'] = bridge_info['physical_duplex']
interface_info['bridge_bond_slaves'] = bridge_info['bond_slaves']
# Override bridge duplex with physical interface duplex # Override bridge duplex with physical interface duplex
if bridge_info['physical_duplex'] != 'unknown': if bridge_info['physical_duplex'] != 'unknown':
interface_info['duplex'] = bridge_info['physical_duplex'] interface_info['duplex'] = bridge_info['physical_duplex']