feat: add console link next to VM/LXC names

Clicking the name opens the Proxmox web console in a new tab.
URLs are constructed in the API routes using the configured PROXMOX_API_URL.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-05-07 10:22:29 +01:00
parent d5e85ce3c1
commit 7baf6a50fa
4 changed files with 80 additions and 6 deletions

View File

@@ -1,6 +1,9 @@
import { NextResponse } from 'next/server';
import { getNodes, getLXC } from '@/lib/proxmox';
const PROXMOX_API_URL = process.env.PROXMOX_API_URL || 'https://proxmox:8006/api2/json';
const PROXMOX_WEBUI = PROXMOX_API_URL.replace('/api2/json', '');
/** @param {import('next').Request} _req */
export async function GET(_req) {
try {
@@ -10,7 +13,13 @@ export async function GET(_req) {
for (const node of nodes) {
try {
const lxcs = await getLXC(node.name);
allLXC.push(...lxcs.map((lxc) => ({ ...lxc, node: node.name })));
allLXC.push(
...lxcs.map((lxc) => ({
...lxc,
node: node.name,
consoleUrl: `${PROXMOX_WEBUI}/pve/console?node=${encodeURIComponent(node.name)}&vmid=${lxc.vmid}`,
})),
);
} catch { /* skip node */ }
}

View File

@@ -1,6 +1,9 @@
import { NextResponse } from 'next/server';
import { getNodes, getVMs } from '@/lib/proxmox';
const PROXMOX_API_URL = process.env.PROXMOX_API_URL || 'https://proxmox:8006/api2/json';
const PROXMOX_WEBUI = PROXMOX_API_URL.replace('/api2/json', '');
/** @param {import('next').Request} _req */
export async function GET(_req) {
try {
@@ -10,7 +13,13 @@ export async function GET(_req) {
for (const node of nodes) {
try {
const vms = await getVMs(node.name);
allVMs.push(...vms.map((vm) => ({ ...vm, node: node.name })));
allVMs.push(
...vms.map((vm) => ({
...vm,
node: node.name,
consoleUrl: `${PROXMOX_WEBUI}/pve/console?node=${encodeURIComponent(node.name)}&vmid=${vm.vmid}`,
})),
);
} catch { /* skip node */ }
}

View File

@@ -36,7 +36,21 @@ export default function LXCList({ lxc, onPowerAction }) {
{running.map((item) => (
<tr key={item.vmid} className="border-b border-border-card last:border-0">
<td className="px-4 py-2 text-text-secondary">{item.vmid}</td>
<td className="px-4 py-2 font-medium text-text-primary">{item.name}</td>
<td className="px-4 py-2">
{item.consoleUrl ? (
<a
href={item.consoleUrl}
target="_blank"
rel="noopener noreferrer"
className="font-medium text-text-primary hover:text-accent"
>
{item.name}
<span className="ml-1 text-[10px] text-text-secondary">[]</span>
</a>
) : (
<span className="font-medium text-text-primary">{item.name}</span>
)}
</td>
<td className="px-4 py-2 text-text-secondary">{item.node}</td>
<td className="px-4 py-2 text-text-secondary">{(item.cpu * 100).toFixed(1)}%</td>
<td className="px-4 py-2 text-text-secondary">{formatBytes(item.memory?.used || 0)} / {formatBytes(item.memory?.total || 0)}</td>
@@ -82,7 +96,21 @@ export default function LXCList({ lxc, onPowerAction }) {
{stopped.map((item) => (
<tr key={item.vmid} className="border-b border-border-card last:border-0">
<td className="px-4 py-2 text-text-secondary">{item.vmid}</td>
<td className="px-4 py-2 font-medium text-text-primary">{item.name}</td>
<td className="px-4 py-2">
{item.consoleUrl ? (
<a
href={item.consoleUrl}
target="_blank"
rel="noopener noreferrer"
className="font-medium text-text-primary hover:text-accent"
>
{item.name}
<span className="ml-1 text-[10px] text-text-secondary">[]</span>
</a>
) : (
<span className="font-medium text-text-primary">{item.name}</span>
)}
</td>
<td className="px-4 py-2 text-text-secondary">{item.node}</td>
<td className="px-4 py-2 text-text-secondary">{formatBytes(item.disk?.used || 0)}</td>
<td className="px-4 py-2"><StatusBadge status={item.status} /></td>

View File

@@ -37,7 +37,21 @@ export default function VMList({ vms, onPowerAction }) {
{running.map((vm) => (
<tr key={vm.vmid} className="border-b border-border-card last:border-0">
<td className="px-4 py-2 text-text-secondary">{vm.vmid}</td>
<td className="px-4 py-2 font-medium text-text-primary">{vm.name}</td>
<td className="px-4 py-2">
{vm.consoleUrl ? (
<a
href={vm.consoleUrl}
target="_blank"
rel="noopener noreferrer"
className="font-medium text-text-primary hover:text-accent"
>
{vm.name}
<span className="ml-1 text-[10px] text-text-secondary">[]</span>
</a>
) : (
<span className="font-medium text-text-primary">{vm.name}</span>
)}
</td>
<td className="px-4 py-2 text-text-secondary">{vm.node}</td>
<td className="px-4 py-2 text-text-secondary">{(vm.cpu * 100).toFixed(1)}%</td>
<td className="px-4 py-2 text-text-secondary">{formatBytes(vm.memory?.used || 0)} / {formatBytes(vm.memory?.total || 0)}</td>
@@ -83,7 +97,21 @@ export default function VMList({ vms, onPowerAction }) {
{stopped.map((vm) => (
<tr key={vm.vmid} className="border-b border-border-card last:border-0">
<td className="px-4 py-2 text-text-secondary">{vm.vmid}</td>
<td className="px-4 py-2 font-medium text-text-primary">{vm.name}</td>
<td className="px-4 py-2">
{vm.consoleUrl ? (
<a
href={vm.consoleUrl}
target="_blank"
rel="noopener noreferrer"
className="font-medium text-text-primary hover:text-accent"
>
{vm.name}
<span className="ml-1 text-[10px] text-text-secondary">[]</span>
</a>
) : (
<span className="font-medium text-text-primary">{vm.name}</span>
)}
</td>
<td className="px-4 py-2 text-text-secondary">{vm.node}</td>
<td className="px-4 py-2 text-text-secondary">{formatBytes(vm.disk?.used || 0)}</td>
<td className="px-4 py-2"><StatusBadge status={vm.status} /></td>