feat: VM/CT Start/Stop im Frontend + Agent proxmox_action Handler

- Agent: neuer 'proxmox_action' Command (start/stop/shutdown fuer VMs und LXC)
- Backend: POST /api/v1/agents/{id}/proxmox/action Endpoint
- Frontend: Start/Stop/Shutdown Buttons in VMs- und Container-Tab
  - Play (gruen) = starten (nur wenn gestoppt)
  - Power (gelb) = Graceful Shutdown (nur VMs, nur wenn running)
  - Stop (rot) = Hard Stop (wenn running)
- api/client.js: proxmoxAction() Methode
This commit is contained in:
cynfo3000
2026-03-09 20:22:45 +01:00
parent 4cd9764cec
commit 291d2e3c17
4 changed files with 204 additions and 11 deletions
+4
View File
@@ -224,6 +224,10 @@ class ApiClient {
return this.post(`/api/v1/agents/${agentId}/exec`, { command, timeout })
}
proxmoxAction(agentId, type, vmid, action) {
return this.post(`/api/v1/agents/${agentId}/proxmox/action`, { type, vmid, action })
}
// Users
getUsers() {
return this.get('/api/v1/users')
+105 -11
View File
@@ -117,9 +117,9 @@ export default function ProxmoxPanel({ agentId, customers, onClose, onReload })
) : tab === 'overview' ? (
<OverviewTab sys={sys} proxmox={proxmox} />
) : tab === 'vms' ? (
<VmsTab proxmox={proxmox} />
<VmsTab proxmox={proxmox} agentId={agentId} />
) : tab === 'containers' ? (
<ContainersTab proxmox={proxmox} />
<ContainersTab proxmox={proxmox} agentId={agentId} />
) : tab === 'storage' ? (
<StorageTab proxmox={proxmox} />
) : tab === 'zfs' ? (
@@ -617,16 +617,76 @@ function OverviewTab({ sys, proxmox }) {
)
}
function VmsTab({ proxmox }) {
function VmActionButtons({ status, loading, onStart, onStop, onShutdown }) {
const isRunning = status === 'running'
const isStopped = status === 'stopped'
if (loading) {
return <span className="text-xs text-gray-500 animate-pulse">...</span>
}
return (
<div className="flex items-center gap-1">
{isStopped && (
<button
onClick={onStart}
title="Starten"
className="p-1 rounded text-green-400 hover:bg-green-900/40 hover:text-green-300 transition-colors"
>
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M6.3 2.84A1.5 1.5 0 004 4.11v11.78a1.5 1.5 0 002.3 1.27l9.344-5.891a1.5 1.5 0 000-2.538L6.3 2.84z" clipRule="evenodd" />
</svg>
</button>
)}
{isRunning && onShutdown && (
<button
onClick={onShutdown}
title="Graceful Shutdown"
className="p-1 rounded text-yellow-400 hover:bg-yellow-900/40 hover:text-yellow-300 transition-colors"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M5.636 5.636a9 9 0 1012.728 0M12 3v9" />
</svg>
</button>
)}
{isRunning && (
<button
onClick={onStop}
title="Hard Stop"
className="p-1 rounded text-red-400 hover:bg-red-900/40 hover:text-red-300 transition-colors"
>
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M4 4h12v12H4V4z" clipRule="evenodd" />
</svg>
</button>
)}
</div>
)
}
function VmsTab({ proxmox, agentId }) {
const vms = proxmox.vms || []
// Sort: running first, then by name
const [pending, setPending] = useState({}) // vmid -> true wenn Action läuft
const [errors, setErrors] = useState({})
const sortedVms = [...vms].sort((a, b) => {
if (a.status === 'running' && b.status !== 'running') return -1
if (a.status !== 'running' && b.status === 'running') return 1
return a.name.localeCompare(b.name)
})
const doAction = async (vmid, action) => {
setPending(p => ({ ...p, [vmid]: true }))
setErrors(e => ({ ...e, [vmid]: null }))
try {
await api.proxmoxAction(agentId, 'vm', vmid, action)
} catch (err) {
setErrors(e => ({ ...e, [vmid]: err.message }))
} finally {
setPending(p => ({ ...p, [vmid]: false }))
}
}
return (
<>
<h3 className="text-sm font-medium text-gray-300">Virtuelle Maschinen ({vms.length})</h3>
@@ -644,6 +704,7 @@ function VmsTab({ proxmox }) {
<th className="px-3 py-2">RAM</th>
<th className="px-3 py-2">Disk</th>
<th className="px-3 py-2">Uptime</th>
<th className="px-3 py-2"></th>
</tr>
</thead>
<tbody className="divide-y divide-gray-700/50">
@@ -653,21 +714,31 @@ function VmsTab({ proxmox }) {
<td className="px-3 py-2 text-white font-medium">{vm.name}</td>
<td className="px-3 py-2">
<StatusBadge status={vm.status} />
{errors[vm.vmid] && <div className="text-red-400 text-xs mt-1">{errors[vm.vmid]}</div>}
</td>
<td className="px-3 py-2 text-gray-400">
{vm.cpu_usage != null ? `${vm.cpu_usage.toFixed(1)}%` : '—'}
</td>
<td className="px-3 py-2 text-gray-400">
{vm.memory_used && vm.memory_max ?
{vm.memory_used && vm.memory_max ?
`${formatBytes(vm.memory_used)} / ${formatBytes(vm.memory_max)}` : '—'}
</td>
<td className="px-3 py-2 text-gray-400">
{vm.disk_used != null && vm.disk_max ?
{vm.disk_used != null && vm.disk_max ?
`${formatBytes(vm.disk_used)} / ${formatBytes(vm.disk_max)}` : '—'}
</td>
<td className="px-3 py-2 text-gray-400">
{vm.uptime ? formatUptime(vm.uptime) : '—'}
</td>
<td className="px-3 py-2">
<VmActionButtons
status={vm.status}
loading={!!pending[vm.vmid]}
onStart={() => doAction(vm.vmid, 'start')}
onStop={() => doAction(vm.vmid, 'stop')}
onShutdown={() => doAction(vm.vmid, 'shutdown')}
/>
</td>
</tr>
))}
</tbody>
@@ -678,16 +749,29 @@ function VmsTab({ proxmox }) {
)
}
function ContainersTab({ proxmox }) {
function ContainersTab({ proxmox, agentId }) {
const containers = proxmox.containers || []
// Sort: running first, then by name
const [pending, setPending] = useState({})
const [errors, setErrors] = useState({})
const sortedCt = [...containers].sort((a, b) => {
if (a.status === 'running' && b.status !== 'running') return -1
if (a.status !== 'running' && b.status === 'running') return 1
return a.name.localeCompare(b.name)
})
const doAction = async (vmid, action) => {
setPending(p => ({ ...p, [vmid]: true }))
setErrors(e => ({ ...e, [vmid]: null }))
try {
await api.proxmoxAction(agentId, 'ct', vmid, action)
} catch (err) {
setErrors(e => ({ ...e, [vmid]: err.message }))
} finally {
setPending(p => ({ ...p, [vmid]: false }))
}
}
return (
<>
<h3 className="text-sm font-medium text-gray-300">Container ({containers.length})</h3>
@@ -704,6 +788,7 @@ function ContainersTab({ proxmox }) {
<th className="px-3 py-2">CPU</th>
<th className="px-3 py-2">RAM</th>
<th className="px-3 py-2">Uptime</th>
<th className="px-3 py-2"></th>
</tr>
</thead>
<tbody className="divide-y divide-gray-700/50">
@@ -713,17 +798,26 @@ function ContainersTab({ proxmox }) {
<td className="px-3 py-2 text-white font-medium">{ct.name}</td>
<td className="px-3 py-2">
<StatusBadge status={ct.status} />
{errors[ct.vmid] && <div className="text-red-400 text-xs mt-1">{errors[ct.vmid]}</div>}
</td>
<td className="px-3 py-2 text-gray-400">
{ct.cpu_usage != null ? `${ct.cpu_usage.toFixed(1)}%` : '—'}
</td>
<td className="px-3 py-2 text-gray-400">
{ct.memory_used && ct.memory_max ?
{ct.memory_used && ct.memory_max ?
`${formatBytes(ct.memory_used)} / ${formatBytes(ct.memory_max)}` : '—'}
</td>
<td className="px-3 py-2 text-gray-400">
{ct.uptime ? formatUptime(ct.uptime) : '—'}
</td>
<td className="px-3 py-2">
<VmActionButtons
status={ct.status}
loading={!!pending[ct.vmid]}
onStart={() => doAction(ct.vmid, 'start')}
onStop={() => doAction(ct.vmid, 'stop')}
/>
</td>
</tr>
))}
</tbody>