diff --git a/src/app/api/power/route.js b/src/app/api/power/route.js
new file mode 100644
index 0000000..fed109a
--- /dev/null
+++ b/src/app/api/power/route.js
@@ -0,0 +1,28 @@
+import { NextResponse } from 'next/server';
+import { powerControl } from '@/lib/proxmox';
+
+/**
+ * @param {import('next').Request} req
+ */
+export async function POST(req) {
+ try {
+ const { node, vmid, type, action } = await req.json();
+
+ if (!node || !vmid || !type || !action) {
+ return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
+ }
+
+ if (!['qemu', 'lxc'].includes(type)) {
+ return NextResponse.json({ error: 'Invalid type, must be qemu or lxc' }, { status: 400 });
+ }
+
+ if (!['start', 'stop', 'restart'].includes(action)) {
+ return NextResponse.json({ error: 'Invalid action, must be start, stop, or restart' }, { status: 400 });
+ }
+
+ await powerControl(node, vmid, type, action);
+ return NextResponse.json({ ok: true, timestamp: Date.now() });
+ } catch (error) {
+ return NextResponse.json({ error: error.message, ok: false }, { status: 502 });
+ }
+}
diff --git a/src/components/Dashboard.js b/src/components/Dashboard.js
index d088e8a..020ab28 100644
--- a/src/components/Dashboard.js
+++ b/src/components/Dashboard.js
@@ -6,6 +6,7 @@ import NodeList from './NodeList';
import VMList from './VMList';
import LXCList from './LXCList';
import axios from 'axios';
+import { SWRConfig } from 'swr';
const POLL_INTERVAL = Number(process.env.NEXT_PUBLIC_API_POLL_INTERVAL || 30000);
@@ -30,6 +31,23 @@ async function fetchData(url) {
/** @returns {import('react').ReactNode} */
export default function Dashboard() {
const [activeTab, setActiveTab] = useState('nodes');
+ const [powerActionStatus, setPowerActionStatus] = useState(null);
+
+ /**
+ * @param {string} node
+ * @param {number} vmid
+ * @param {string} type
+ * @param {string} action
+ */
+ const handlePowerAction = async (node, vmid, type, action) => {
+ setPowerActionStatus({ text: `${action.charAt(0).toUpperCase() + action.slice(1)}ing ${type === 'qemu' ? 'VM' : 'LXC'} ${vmid}...`, type: 'pending' });
+ try {
+ await axios.post('/api/power', { node, vmid, type, action });
+ setPowerActionStatus({ text: `${action.charAt(0).toUpperCase() + action.slice(1)} successful`, type: 'success' });
+ } catch (err) {
+ setPowerActionStatus({ text: err.message || `${action.charAt(0).toUpperCase() + action.slice(1)} failed`, type: 'error' });
+ }
+ };
const { data: nodesData, error: nodesError } = useSWR('/api/nodes', fetchData, {
refreshInterval: POLL_INTERVAL,
@@ -100,6 +118,16 @@ export default function Dashboard() {
)}
+ {powerActionStatus && (
+
{tabs.map((tab) => (
@@ -119,8 +147,8 @@ export default function Dashboard() {
{/* Tab content */}
{activeTab === 'nodes' && }
- {activeTab === 'vms' && }
- {activeTab === 'lxc' && }
+ {activeTab === 'vms' && }
+ {activeTab === 'lxc' && }
{/* Footer */}
diff --git a/src/components/LXCList.js b/src/components/LXCList.js
index e6cf7e0..8d76b04 100644
--- a/src/components/LXCList.js
+++ b/src/components/LXCList.js
@@ -4,7 +4,10 @@ import StatusBadge from './StatusBadge';
* @param {{ lxc: Array<{ vmid: number; name: string; type: string; status: string; cpu: number; memory: { total: number; used: number }; disk: { total: number; used: number }; node: string }> }} props
* @returns {import('react').ReactNode}
*/
-export default function LXCList({ lxc }) {
+/**
+ * @param {{ lxc: Array<{ vmid: number; name: string; type: string; status: string; cpu: number; memory: { total: number; used: number }; disk: { total: number; used: number }; node: string }>, onPowerAction: (node: string, vmid: number, type: string, action: string) => void }} props
+ */
+export default function LXCList({ lxc, onPowerAction }) {
if (!lxc?.length) {
return
No LXC containers found.
;
}
@@ -38,6 +41,20 @@ export default function LXCList({ lxc }) {
{(item.cpu * 100).toFixed(1)}% |
{formatBytes(item.memory?.used || 0)} / {formatBytes(item.memory?.total || 0)} |
|
+
+
+
+ |
))}
@@ -58,6 +75,7 @@ export default function LXCList({ lxc }) {
Node |
Disk |
Status |
+
Actions |
@@ -68,6 +86,14 @@ export default function LXCList({ lxc }) {
{item.node} |
{formatBytes(item.disk?.used || 0)} |
|
+
+
+ |
))}
diff --git a/src/components/VMList.js b/src/components/VMList.js
index 4477f43..a76dffe 100644
--- a/src/components/VMList.js
+++ b/src/components/VMList.js
@@ -4,7 +4,10 @@ import StatusBadge from './StatusBadge';
* @param {{ vms: Array<{ vmid: number; name: string; type: string; status: string; cpu: number; memory: { total: number; used: number }; disk: { total: number; used: number }; node: string }> }} props
* @returns {import('react').ReactNode}
*/
-export default function VMList({ vms }) {
+/**
+ * @param {{ vms: Array<{ vmid: number; name: string; type: string; status: string; cpu: number; memory: { total: number; used: number }; disk: { total: number; used: number }; node: string }>, onPowerAction: (node: string, vmid: number, type: string, action: string) => void }} props
+ */
+export default function VMList({ vms, onPowerAction }) {
if (!vms?.length) {
return
No VMs found.
;
}
@@ -27,6 +30,7 @@ export default function VMList({ vms }) {
CPU |
Memory |
Status |
+
Actions |
@@ -38,6 +42,20 @@ export default function VMList({ vms }) {
{(vm.cpu * 100).toFixed(1)}% |
{formatBytes(vm.memory?.used || 0)} / {formatBytes(vm.memory?.total || 0)} |
|
+
+
+
+ |
))}
@@ -58,6 +76,7 @@ export default function VMList({ vms }) {
Node |
Disk |
Status |
+
Actions |
@@ -68,6 +87,14 @@ export default function VMList({ vms }) {
{vm.node} |
{formatBytes(vm.disk?.used || 0)} |
|
+
+
+ |
))}
diff --git a/src/lib/proxmox.js b/src/lib/proxmox.js
index d217268..5127cb1 100644
--- a/src/lib/proxmox.js
+++ b/src/lib/proxmox.js
@@ -163,7 +163,20 @@ export async function getLXC(nodeName) {
* Aggregated summary across all nodes.
* @returns {Promise
}
*/
-export async function getSummary() {
+/**
+ * Sends a power action to a VM or LXC.
+ * @param {string} nodeName
+ * @param {number} vmid
+ * @param {'qemu'|'lxc'} type
+ * @param {'start'|'stop'|'restart'} action
+ * @returns {Promise}
+ */
+export async function powerControl(nodeName, vmid, type, action) {
+ await proxmoxFetch(`/nodes/${nodeName}/${type}/${vmid}/status/${action}`, 'POST');
+}
+
+/**
+ * Aggregated summary across all nodes.
const nodes = await getNodes();
let totalVMs = [];
let totalLXC = [];