chore: pre-existing WIP (overview/realtime/format/types/use-instances)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-21 21:14:45 +05:00
parent 6769e5b218
commit df2c59eb3f
5 changed files with 59 additions and 7 deletions

View File

@@ -1,7 +1,7 @@
import { Cpu, MemoryStick, HardDrive, Clock, Wifi, Globe, Server, ArrowDown, ArrowUp } from 'lucide-react'; import { Cpu, MemoryStick, HardDrive, Clock, Wifi, Globe, Server, ArrowDown, ArrowUp } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { useVMLiveMetrics, useVMConfig, useVMInterfaces } from '@/hooks/use-instances'; import { useVMLiveMetrics, useVMConfig, useVMInterfaces } from '@/hooks/use-instances';
import { formatBytes, formatUptime, formatPercent } from '@/lib/format'; import { formatBytes, formatUptime, formatPercent, vmVcpuCount } from '@/lib/format';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { DiskInfo } from '@/types'; import type { DiskInfo } from '@/types';
@@ -165,7 +165,7 @@ export default function OverviewTab({ serverId, vmid, type, node }: Props) {
<MetricCard <MetricCard
label={t('common.cpu')} label={t('common.cpu')}
value={formatPercent(cpuPercent)} value={formatPercent(cpuPercent)}
subtitle={config?.cores ? `${config.cores} ${t('common.cores')}` : undefined} subtitle={vmVcpuCount(config) ? `${vmVcpuCount(config)} ${t('common.cores')}` : undefined}
percent={cpuPercent} percent={cpuPercent}
icon={Cpu} icon={Cpu}
/> />
@@ -258,7 +258,7 @@ export default function OverviewTab({ serverId, vmid, type, node }: Props) {
<InfoRow label={t('common.vmid')} value={vmid} /> <InfoRow label={t('common.vmid')} value={vmid} />
<InfoRow label={t('common.node')} value={node} /> <InfoRow label={t('common.node')} value={node} />
<InfoRow label={t('common.type')} value={type === 'qemu' ? t('common.qemu_virtual_machine') : t('common.lxc_container')} /> <InfoRow label={t('common.type')} value={type === 'qemu' ? t('common.qemu_virtual_machine') : t('common.lxc_container')} />
<InfoRow label={t('common.vcpu')} value={config?.cores} /> <InfoRow label={t('common.vcpu')} value={vmVcpuCount(config)} />
<InfoRow label={t('common.memory')} value={config?.memory ? `${config.memory} MB` : undefined} /> <InfoRow label={t('common.memory')} value={config?.memory ? `${config.memory} MB` : undefined} />
<InfoRow label={t('common.os_type')} value={config?.ostype as string} /> <InfoRow label={t('common.os_type')} value={config?.ostype as string} />
<InfoRow label={t('common.boot_order')} value={config?.boot as string} /> <InfoRow label={t('common.boot_order')} value={config?.boot as string} />

View File

@@ -178,7 +178,17 @@ export function useVMConfig(serverId: number, vmid: number, type: string, node:
}); });
} }
export function useVMRrddata(serverId: number, vmid: number, type: string, node: string, timeframe = 'hour') { // RRD refresh cadence per timeframe — matches Proxmox's native RRA resolution,
// so the chart advances as new samples land instead of staying frozen.
const RRD_REFETCH_MS: Record<string, number> = {
hour: 60_000, // 1-min resolution
day: 300_000, // ~5-min
week: 1_800_000, // ~30-min
month: 3_600_000, // ~hourly
year: 3_600_000,
};
export function useVMRrddata(serverId: number, vmid: number, type: string, node: string, timeframe = 'hour', enabled = true) {
const prefix = type === 'lxc' ? 'container' : 'vm'; const prefix = type === 'lxc' ? 'container' : 'vm';
return useQuery({ return useQuery({
queryKey: vmKeys.rrddata(serverId, vmid, timeframe), queryKey: vmKeys.rrddata(serverId, vmid, timeframe),
@@ -186,6 +196,8 @@ export function useVMRrddata(serverId: number, vmid: number, type: string, node:
const res = await apiClient.get<{ data: unknown[]; timeframe: string }>(`/proxmox/api/${serverId}/${prefix}/${vmid}/rrddata?node=${node}&timeframe=${timeframe}`); const res = await apiClient.get<{ data: unknown[]; timeframe: string }>(`/proxmox/api/${serverId}/${prefix}/${vmid}/rrddata?node=${node}&timeframe=${timeframe}`);
return res.data ?? []; return res.data ?? [];
}, },
enabled,
refetchInterval: enabled ? (RRD_REFETCH_MS[timeframe] ?? 60_000) : false,
}); });
} }
@@ -274,7 +286,11 @@ export function useSetVMOwner(serverId: number, vmid: number) {
return useMutation({ return useMutation({
mutationFn: (user_id: number | null) => mutationFn: (user_id: number | null) =>
apiClient.put(`/proxmox/api/${serverId}/vm/${vmid}/owner`, { user_id }), apiClient.put(`/proxmox/api/${serverId}/vm/${vmid}/owner`, { user_id }),
onSuccess: () => qc.invalidateQueries({ queryKey: ['vm-owner', serverId, vmid] }), onSuccess: () => {
qc.invalidateQueries({ queryKey: ['vm-owner', serverId, vmid] });
// Refresh the instances list so the owner column updates without a manual reload.
qc.invalidateQueries({ queryKey: vmKeys.all });
},
}); });
} }

View File

@@ -48,6 +48,24 @@ export function useGlobalRealtimeSync() {
qc.invalidateQueries({ queryKey: vmKeys.resourcesAll }); qc.invalidateQueries({ queryKey: vmKeys.resourcesAll });
}); });
const unsubVmOwnerChanged = ws.onType('vm_owner_changed', (data: unknown) => {
const msg = data as {
vmid: number;
server_id: number;
owner_id: number | null;
owner_user: { username: string; email?: string; full_name?: string } | null;
};
qc.setQueryData<Array<Record<string, unknown>>>(vmKeys.all, (old) =>
old?.map((vm) =>
vm.server_id === msg.server_id && vm.vmid === msg.vmid
? { ...vm, owner_id: msg.owner_id, owner_user: msg.owner_user }
: vm
)
);
// Keep the admin owner panel (settings tab) in sync as well.
qc.invalidateQueries({ queryKey: ['vm-owner', msg.server_id, msg.vmid] });
});
// ── Server events ────────────────────────────────────────────────────── // ── Server events ──────────────────────────────────────────────────────
const invalidateServerCaches = () => { const invalidateServerCaches = () => {
qc.invalidateQueries({ queryKey: nodeKeys.servers }); qc.invalidateQueries({ queryKey: nodeKeys.servers });
@@ -74,6 +92,7 @@ export function useGlobalRealtimeSync() {
unsubVmStatus(); unsubVmStatus();
unsubVmDeleted(); unsubVmDeleted();
unsubVmCreated(); unsubVmCreated();
unsubVmOwnerChanged();
unsubServerAdded(); unsubServerAdded();
unsubServerUpdated(); unsubServerUpdated();
unsubServerDeleted(); unsubServerDeleted();

View File

@@ -28,19 +28,32 @@ export function vmTypeLabel(type: string): string {
return type === 'qemu' ? 'VM' : type === 'lxc' ? 'LXC' : type.toUpperCase(); return type === 'qemu' ? 'VM' : type === 'lxc' ? 'LXC' : type.toUpperCase();
} }
/**
* Total vCPU count from a live Proxmox config.
* QEMU reports `cores` *per socket*, so the real total is `sockets * cores`
* (LXC has no sockets and reports the total in `cores`).
*/
export function vmVcpuCount(config?: { cores?: number; sockets?: number } | null): number | undefined {
if (!config?.cores) return undefined;
return config.cores * (config.sockets ?? 1);
}
/** /**
* Formats a VM/LXC hardware configuration as "N vCPU / X GB RAM / Y GB". * Formats a VM/LXC hardware configuration as "N vCPU / X GB RAM / Y GB".
* `memory` is expected in MB; disk in GB (falls back to `maxdisk` in bytes). * `memory` and `maxdisk` come from the cached instances list in bytes;
* `disk_size` (when present) is already in GB.
*/ */
export function formatVmConfig(vm: { export function formatVmConfig(vm: {
cores?: number; cores?: number;
memory?: number; memory?: number;
maxmem?: number;
disk_size?: number; disk_size?: number;
maxdisk?: number; maxdisk?: number;
}): string { }): string {
const parts: string[] = []; const parts: string[] = [];
if (vm.cores) parts.push(`${vm.cores} vCPU`); if (vm.cores) parts.push(`${vm.cores} vCPU`);
if (vm.memory) parts.push(`${Math.round(vm.memory / 1024)} GB RAM`); const memBytes = vm.memory || vm.maxmem;
if (memBytes) parts.push(`${formatBytes(memBytes, 0)} RAM`);
const diskGb = vm.disk_size ?? (vm.maxdisk ? Math.round(vm.maxdisk / 1024 ** 3) : 0); const diskGb = vm.disk_size ?? (vm.maxdisk ? Math.round(vm.maxdisk / 1024 ** 3) : 0);
if (diskGb) parts.push(`${diskGb} GB`); if (diskGb) parts.push(`${diskGb} GB`);
return parts.join(' / '); return parts.join(' / ');

View File

@@ -120,6 +120,10 @@ export interface VMInstance {
export interface VMConfig { export interface VMConfig {
cores?: number; cores?: number;
/** QEMU socket count; total vCPU = sockets * cores (defaults to 1) */
sockets?: number;
/** QEMU hotplugged vCPU count, when fewer than sockets * cores are online */
vcpus?: number;
memory?: number; memory?: number;
disk?: string; disk?: string;
name?: string; name?: string;