server node, vm & lxc details pages at unique routes

This commit is contained in:
2025-10-13 20:19:13 +02:00
parent 471b13739d
commit d26aa8c9cb
20 changed files with 793 additions and 34 deletions

View File

@@ -0,0 +1,96 @@
<script lang="ts">
import { goto } from '$app/navigation';
import CubeSide from '$lib/icons/cube-side.svelte';
import HardDrive from '$lib/icons/hard-disk.svelte';
import Network from '$lib/icons/Network.svelte';
import CPU from '$lib/icons/cpu.svelte';
import Fingerprint from '$lib/icons/fingerprint.svelte';
import ExtractUp from '$lib/icons/extract-up.svelte';
import InsertDown from '$lib/icons/insert-down.svelte';
import Clock from '$lib/icons/clock.svelte';
import Memory from '$lib/icons/floppy-disk.svelte';
import { formatBytes, formatDuration } from '$lib/utils/conversion';
import type { LXC } from '$lib/interfaces/proxmox';
const { lxc }: { lxc: LXC } = $props();
</script>
<div class="card">
<div class="header">
<div class="icon"><CubeSide /></div>
<span class="name">{lxc.name} <span class="subtle">{lxc.vmid}</span></span>
<span class={`status ${lxc.status === 'running' ? 'ok' : 'error'}`}></span>
</div>
<div class="resource">
<div class="title">
<Network />
<span>Status</span>
</div>
<span>{lxc.status}</span>
<div class="title">
<Network />
<span>CPUs</span>
</div>
<span>{lxc.cpus}</span>
<div class="title">
<CPU />
<span>CPU</span>
</div>
<span>{Math.floor(lxc.cpu * 100) / 100}</span>
<div class="title">
<HardDrive />
<span>Max Disk</span>
</div>
<span>{formatBytes(lxc.maxdisk)}</span>
<div class="title">
<Memory />
<span>Memory</span>
</div>
<span>{formatBytes(lxc.mem)}</span>
<div class="title">
<InsertDown />
<span>Net In</span>
</div>
<span>{formatBytes(lxc.netin)}</span>
<div class="title">
<ExtractUp />
<span>Net Out</span>
</div>
<span>{formatBytes(lxc.netout / 8)}</span>
<div class="title">
<Clock />
<span>Uptime</span>
</div>
<span>{formatDuration(lxc.uptime)}</span>
<div class="title">
<Fingerprint />
<span>lxc ID</span>
</div>
<span>{lxc.vmid}</span>
</div>
<div class="footer">
<button on:click={() => goto(`/servers/lxc/${lxc.vmid}`)}>
<span>Pod details</span>
</button>
</div>
</div>
<style lang="scss">
@import '../styles/card.scss';
.card {
flex-grow: 1;
max-width: 550px;
}
</style>

View File

@@ -24,7 +24,7 @@
link: `https://${node.ip}:8006/#v1:0:=node%2F${node.name}:4:=jsconsole::::::` link: `https://${node.ip}:8006/#v1:0:=node%2F${node.name}:4:=jsconsole::::::`
}, },
{ name: 'Graphs', link: `https://${node.ip}:8006/#v1:0:=node%2F${node.name}:4:5::::::` }, { name: 'Graphs', link: `https://${node.ip}:8006/#v1:0:=node%2F${node.name}:4:5::::::` },
{ name: 'Web', link: `https://${node.ip}:8006/` } { name: 'Details', link: `/servers/node/${node.name}` }
]; ];
let { cpuinfo, memory, uptime, loadavg } = node.info; let { cpuinfo, memory, uptime, loadavg } = node.info;
@@ -116,7 +116,7 @@
<div class="footer"> <div class="footer">
{#each buttons as btn (btn)} {#each buttons as btn (btn)}
<a href={btn.link} target="_blank" rel="noopener noreferrer"> <a href={btn.link} target={btn.link[0] === '/' ? '' : '_blank'} rel="noopener noreferrer">
<button> <button>
<span>{btn.name}</span> <span>{btn.name}</span>
</button> </button>

View File

@@ -0,0 +1,95 @@
<script lang="ts">
import { goto } from '$app/navigation'
import Desktop from '$lib/icons/desktop.svelte';
import HardDrive from '$lib/icons/hard-disk.svelte';
import Network from '$lib/icons/network.svelte';
import CPU from '$lib/icons/cpu.svelte';
import Fingerprint from '$lib/icons/fingerprint.svelte';
import ExtractUp from '$lib/icons/extract-up.svelte';
import InsertDown from '$lib/icons/insert-down.svelte';
import Clock from '$lib/icons/clock.svelte';
import Memory from '$lib/icons/floppy-disk.svelte';
import { formatBytes, formatDuration } from '$lib/utils/conversion';
import type { VM } from '$lib/interfaces/proxmox';
const { vm }: { vm: VM } = $props();
</script>
<div class="card">
<div class="header">
<div class="icon"><Desktop /></div>
<span class="name">{vm.name} <span class="subtle">{vm.vmid}</span></span>
<span class={`status ${vm.status === 'running' ? 'ok' : 'error'}`}></span>
</div>
<div class="resource">
<div class="title">
<Network />
<span>Status</span>
</div>
<span>{vm.status}</span>
<div class="title">
<Network />
<span>CPUs</span>
</div>
<span>{vm.cpus}</span>
<div class="title">
<CPU />
<span>CPU</span>
</div>
<span>{Math.floor(vm.cpu * 100) / 100}</span>
<div class="title">
<HardDrive />
<span>Max Disk</span>
</div>
<span>{formatBytes(vm.maxdisk)}</span>
<div class="title">
<Memory />
<span>Memory</span>
</div>
<span>{formatBytes(vm.mem)}</span>
<div class="title">
<InsertDown />
<span>Net In</span>
</div>
<span>{formatBytes(vm.netin)}</span>
<div class="title">
<ExtractUp />
<span>Net Out</span>
</div>
<span>{formatBytes(vm.netout / 8)}</span>
<div class="title">
<Clock />
<span>Uptime</span>
</div>
<span>{formatDuration(vm.uptime)}</span>
<div class="title">
<Fingerprint />
<span>VM ID</span>
</div>
<span>{vm.vmid}</span>
</div>
<div class="footer">
<button on:click={() => goto(`/servers/vm/${vm.name}`)}>
<span>VM details</span>
</button>
</div>
</div>
<style lang="scss">
@import '../styles/card.scss';
.card {
flex-grow: 1;
max-width: 550px;
}
</style>

View File

@@ -0,0 +1,282 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { formatBytes, formatDuration } from '$lib/utils/conversion';
import Cpu from '$lib/icons/cpu.svelte';
import Section from '../Section.svelte';
import type { Node, VM } from '$lib/interfaces/proxmox';
import FloppyDisk from '$lib/icons/floppy-disk.svelte';
import HardDisk from '$lib/icons/hard-disk.svelte';
import Clock from '$lib/icons/clock.svelte';
import InsertDown from '$lib/icons/insert-down.svelte';
import ExtractUp from '$lib/icons/extract-up.svelte';
import Table from '../Table.svelte';
import Power from '$lib/icons/power.svelte';
const { vm, node }: { vm: VM, node: Node } = $props();
console.log(node)
const drives = Object.entries(vm.config)
.filter(([key, _]) => {
return key.startsWith('scsi');
})
?.map(([key, value]) => {
const { disk, backup, discard, iothread, size } =
/(?<disk>[\w\:\-]+),(backup=(?<backup>[\w|\d]+))?[,]?(discard=(?<discard>[\w|\d]+))?[,]?(iothread=(?<iothread>[\w|\d]+))?[,]?(size=(?<size>[\w|\d]+)\w)?[,]?/.exec(
value
)?.groups || {};
if (!disk) return;
return {
device: key,
mount: disk,
backup: backup === '1' ? 'enabled' : 'disabled',
size: Number(size || 1) * 1024
};
})
.filter((d) => d)
.sort((a, b) =>
Number(a?.device.match(/\d+/)[0]) > Number(b?.device.match(/\d+/)[0]) ? 1 : -1
);
const pcieDevices = Object.entries(vm.config)
.filter(([key, _]) => {
return key.startsWith('hostpci');
})
?.map(([key, value]) => {
return {
device: key,
name: value
};
});
</script>
<div>
<Section title="Resources" description="">
<div class="section-row">
<div class="section-element">
<label>ID</label>
<span>{vm.vmid}</span>
</div>
<div class="section-element">
<label>Memory usage</label>
<span
><span class="icon"><FloppyDisk /></span>{formatBytes(vm.mem).match(/\d+(\.\d+)?/)?.[0]} /
{formatBytes(vm.maxmem)}</span
>
</div>
<div class="section-element">
<label>CPUs</label>
<span>
<span class="icon"><Cpu /></span>
{vm.cpus} cores ({vm.config.sockets})
</span>
</div>
<div class="section-element">
<label>Disk</label>
<span>
<span class="icon"><HardDisk /></span>
{formatBytes(vm.maxdisk)}
</span>
</div>
<div class="section-element">
<label>Uptime</label>
<span>
<span class="icon"><Clock /></span>
{formatDuration(vm.uptime)}
</span>
</div>
</div>
</Section>
<Section title="Status" description="">
<div class="section-row">
<div class="section-element">
<label>State</label>
<span>
<span class="icon"><Power /></span>
{vm.status}
</span>
</div>
<div class="section-element">
<label>CPU load</label>
<span>{Math.floor(vm.cpu * 10000) / 100} %</span>
</div>
<div class="section-element">
<label>Memory usage</label>
<span>{Math.floor((vm.mem / vm.maxmem) * 1000) / 10} %</span>
</div>
<div class="section-element">
<label>Netout</label>
<span>
<span class="icon"><ExtractUp /></span>
{formatBytes(vm.netout)}
</span>
</div>
<div class="section-element">
<label>Netin</label>
<span>
<span class="icon"><InsertDown /></span>
{formatBytes(vm.netin)}
</span>
</div>
<div class="section-element">
<label>Disk read</label>
<span>
<span class="icon"><InsertDown /></span>
{vm.diskread}
</span>
</div>
<div class="section-element">
<label>Disk write</label>
<span>
<span class="icon"><InsertDown /></span>
{vm.diskwrite}
</span>
</div>
</div>
</Section>
<Section title="Operating System" description="">
<div class="section-row">
<div class="section-element">
<label>Name</label>
<span>{vm.os?.name}</span>
</div>
<div class="section-element">
<label>Name</label>
<span>{vm.os?.['pretty-name']}</span>
</div>
<div class="section-element">
<label>Version</label>
<span>{vm.os?.version}</span>
</div>
<div class="section-element">
<label>Kernel</label>
<span>{vm.os?.['kernel-release']}</span>
</div>
</div>
</Section>
<Section title="Filesystem" description="">
{#if drives?.length > 0}
<Table title="FS Devices" columns={['name', 'bus', 'mountpoint', 'type', 'space', 'size']}>
<tbody slot="tbody">
{#each vm.fs as device (device)}
<tr>
<td>{device.name}</td>
<td>
{#each device['disk'] as device (device)}
<span>{device?.['bus-type']}{device?.target}</span>
{/each}
</td>
<td>{device.mountpoint}</td>
<td>{device.type}</td>
<td>{Math.floor((device['used-bytes'] / device['total-bytes']) * 10000) / 100} %</td>
<td>{formatBytes(device['total-bytes'])}</td>
</tr>
{/each}
</tbody>
</Table>
{/if}
</Section>
<Section title="Devices" description="">
{#if vm.network?.length > 0}
<Table title="Network interfaces" columns={['name', 'hardward address', 'ip addresses']}>
<tbody slot="tbody">
{#each vm.network.sort((a,b) => a < b ? 1 : -1) as net_interface (net_interface.name)}
<tr>
<td>{net_interface.name}</td>
<td>{net_interface['hardware-address']}</td>
<td class="ip-addresses">
{#each net_interface['ip-addresses'] as ip (ip['ip-address'])}
<span>{ip['ip-address']}/{ip.prefix}</span>
{/each}
</td>
</tr>
{/each}
</tbody>
</Table>
{/if}
{#if drives?.length > 0}
<Table title="Hard drives" columns={['device', 'mount', 'backup', 'size']}>
<div slot="actions">
<p>Total drives: {drives.length}</p>
<p>Total capacity: {formatBytes(drives.reduce((acc, obj) => acc + obj.size, 0))}</p>
</div>
<tbody slot="tbody">
{#each drives as drive (drive)}
<tr>
<td>{drive.device}</td>
<td>{drive.mount}</td>
<td>{drive.backup}</td>
<td>{formatBytes(drive.size)}</td>
</tr>
{/each}
</tbody>
</Table>
{/if}
{#if pcieDevices?.length > 0}
<Table title="PCIe devices" columns={Object.keys(pcieDevices[0])}>
<tbody slot="tbody">
{#each pcieDevices as pcie (pcie)}
<tr>
<td>{pcie.device}</td>
<td>{pcie.name}</td>
</tr>
{/each}
</tbody>
</Table>
{/if}
</Section>
</div>
<style lang="scss">
:global(article.main-container:not(:first-child)) {
margin-top: 1rem;
}
.section-element {
.icon {
display: inline-block;
--size: 1.3rem;
height: var(--size);
width: var(--size);
padding-right: 0.5rem;
&.spin {
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
animation: rotate 6s linear infinite;
transform-origin: calc((var(--size) / 2) - 2px) calc(var(--size) / 2);
}
}
}
tbody .ip-addresses {
display: flex;
flex-direction: column;
}
</style>

View File

@@ -2,8 +2,8 @@
<svg <svg
version="1.1" version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="768" width="100%"
height="768" height="100%"
viewBox="0 0 768 768" viewBox="0 0 768 768"
> >
<g id="icomoon-ignore"> </g> <g id="icomoon-ignore"> </g>

Before

Width:  |  Height:  |  Size: 816 B

After

Width:  |  Height:  |  Size: 818 B

View File

@@ -1,12 +1,10 @@
<!-- Generated by IcoMoon.io -->
<svg <svg
version="1.1" version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="768" width="100%"
height="768" height="100%"
viewBox="0 0 768 768" viewBox="0 0 768 768"
> >
<g id="icomoon-ignore"> </g>
<path <path
d="M701.2 186.6c0 0 0 0 0 0l-288-147.6c-18.3-9.3-40.1-9.3-58.4 0l-288 147.6c-21.4 11-34.8 32.9-34.8 57v280.9c0 24.1 13.3 45.9 34.8 57l288 147.6c9.1 4.7 19.1 7 29.1 7s20.1-2.3 29.2-7l288-147.6c21.4-11 34.8-32.9 34.8-57v-280.9c0.1-24.1-13.2-45.9-34.7-57zM384 96l249.8 128-249.8 128-249.8-128 249.8-128zM96 276.4l256 131.2v248.2l-256-131.3v-248.1zM416 655.7v-248.1l256-131.2v248.2l-256 131.1z" d="M701.2 186.6c0 0 0 0 0 0l-288-147.6c-18.3-9.3-40.1-9.3-58.4 0l-288 147.6c-21.4 11-34.8 32.9-34.8 57v280.9c0 24.1 13.3 45.9 34.8 57l288 147.6c9.1 4.7 19.1 7 29.1 7s20.1-2.3 29.2-7l288-147.6c21.4-11 34.8-32.9 34.8-57v-280.9c0.1-24.1-13.2-45.9-34.7-57zM384 96l249.8 128-249.8 128-249.8-128 249.8-128zM96 276.4l256 131.2v248.2l-256-131.3v-248.1zM416 655.7v-248.1l256-131.2v248.2l-256 131.1z"
></path> ></path>

Before

Width:  |  Height:  |  Size: 866 B

After

Width:  |  Height:  |  Size: 805 B

View File

@@ -0,0 +1,4 @@
<!-- generated by icomoon.io - licensed Lindua icon -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 768 768" width="100%" height="100%">
<path d="M704 64h-640c-35.3 0-64 28.7-64 64v352c0 35.3 28.7 64 64 64h288v64c0 15.1-12.4 30.6-34 42.6-24.9 13.8-58.3 21.4-94 21.4v32h320v-32c-35.8 0-69.2-7.6-94-21.4-21.6-12-34-27.5-34-42.6v-64h288c35.3 0 64-28.7 64-64v-352c0-35.3-28.7-64-64-64zM423.8 672h-79.6c24.9-16.9 39.8-39.2 39.8-64 0 24.8 14.9 47.1 39.8 64zM64 128h640v288h-640v-288c-0.1 0 0 0 0 0zM64 480v-32h640v32h-640z"></path>
</svg>

After

Width:  |  Height:  |  Size: 556 B

View File

@@ -0,0 +1,5 @@
<!-- generated by icomoon.io - licensed Lindua icon -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 768 768" width="100%" height="100%">
<path d="M656 352h-208v32h192v320h-512v-320h192v-32h-208c-8.8 0-16 7.2-16 16v352c0 8.8 7.2 16 16 16h544c8.8 0 16-7.2 16-16v-352c0-8.8-7.2-16-16-16z"></path>
<path d="M352 141.3v402.7h64v-402.7l81.4 81.4 45.3-45.3-136-136c-12.5-12.5-32.8-12.5-45.3 0l-136 136 45.3 45.3 81.3-81.4z"></path>
</svg>

After

Width:  |  Height:  |  Size: 455 B

View File

@@ -1,12 +1,10 @@
<!-- Generated by IcoMoon.io -->
<svg <svg
version="1.1" version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="768" width="100%"
height="768" height="100%"
viewBox="0 0 768 768" viewBox="0 0 768 768"
> >
<g id="icomoon-ignore"> </g>
<path <path
d="M672 32h-576c-35.3 0-64 28.7-64 64v498.7c0 17.1 6.7 33.1 18.8 45.2l77.3 77.3c12.1 12.1 28.2 18.8 45.2 18.8h498.7c35.3 0 64-28.7 64-64v-576c0-35.3-28.7-64-64-64zM192 64h384v256c0 17.6-14.4 32-32 32h-320c-17.6 0-32-14.4-32-32v-256zM544 704h-288v-192h288v192zM672 672h-96v-176c0-8.8-7.2-16-16-16h-320c-8.8 0-16 7.2-16 16v176h-50.7l-77.3-77.3v-498.7h64v224c0 35.3 28.7 64 64 64h320c35.3 0 64-28.7 64-64v-224h64v576z" d="M672 32h-576c-35.3 0-64 28.7-64 64v498.7c0 17.1 6.7 33.1 18.8 45.2l77.3 77.3c12.1 12.1 28.2 18.8 45.2 18.8h498.7c35.3 0 64-28.7 64-64v-576c0-35.3-28.7-64-64-64zM192 64h384v256c0 17.6-14.4 32-32 32h-320c-17.6 0-32-14.4-32-32v-256zM544 704h-288v-192h288v192zM672 672h-96v-176c0-8.8-7.2-16-16-16h-320c-8.8 0-16 7.2-16 16v176h-50.7l-77.3-77.3v-498.7h64v224c0 35.3 28.7 64 64 64h320c35.3 0 64-28.7 64-64v-224h64v576z"
></path> ></path>

Before

Width:  |  Height:  |  Size: 760 B

After

Width:  |  Height:  |  Size: 699 B

View File

@@ -1,12 +1,10 @@
<!-- Generated by IcoMoon.io -->
<svg <svg
version="1.1" version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="768" width="100%"
height="768" height="100%"
viewBox="0 0 768 768" viewBox="0 0 768 768"
> >
<g id="icomoon-ignore"> </g>
<path <path
d="M640 0h-512c-35.3 0-64 28.7-64 64v640c0 35.3 28.7 64 64 64h512c35.3 0 64-28.7 64-64v-640c0-35.3-28.7-64-64-64zM640 704h-512v-640h512v640c0 0 0 0 0 0z" d="M640 0h-512c-35.3 0-64 28.7-64 64v640c0 35.3 28.7 64 64 64h512c35.3 0 64-28.7 64-64v-640c0-35.3-28.7-64-64-64zM640 704h-512v-640h512v640c0 0 0 0 0 0z"
></path> ></path>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,5 @@
<!-- generated by icomoon.io - licensed Lindua icon -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 768 768" width="100%" height="100%">
<path d="M656 320h-208v32h192v320h-512v-320h192v-32h-208c-8.8 0-16 7.2-16 16v352c0 8.8 7.2 16 16 16h544c8.8 0 16-7.2 16-16v-352c0-8.8-7.2-16-16-16z"></path>
<path d="M270.6 417.4l-45.3 45.3 136 136c6.2 6.2 14.4 9.4 22.6 9.4s16.4-3.1 22.6-9.4l136-136-45.3-45.3-81.2 81.3v-434.7h-64v434.7l-81.4-81.3z"></path>
</svg>

After

Width:  |  Height:  |  Size: 475 B

View File

@@ -0,0 +1,5 @@
<!-- generated by icomoon.io - licensed Lindua icon -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 768 768" width="100%" height="100%">
<path d="M656 64h-544c-8.8 0-16 7.2-16 16v352c0 8.8 7.2 16 16 16h208v-32h-192v-320h512v320h-192v32h208c8.8 0 16-7.2 16-16v-352c0-8.8-7.2-16-16-16z"></path>
<path d="M497.4 350.6l45.3-45.3-136-136c-12.5-12.5-32.8-12.5-45.3 0l-136 136 45.3 45.3 81.4-81.4v434.8h64v-434.7l81.3 81.3z"></path>
</svg>

After

Width:  |  Height:  |  Size: 456 B

View File

@@ -2,8 +2,8 @@
<svg <svg
version="1.1" version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="768" width="100%"
height="768" height="100%"
viewBox="0 0 768 768" viewBox="0 0 768 768"
> >
<g id="icomoon-ignore"> </g> <g id="icomoon-ignore"> </g>

Before

Width:  |  Height:  |  Size: 647 B

After

Width:  |  Height:  |  Size: 649 B

View File

@@ -1,5 +1,4 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 768 768"> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 768 768">
</g>
<path d="M721.4 97.4l-368 368c-21.9 21.9-42.8 16.6-55.1 4.3s-17.6-33.2 4.3-55.1l368-368-45.2-45.2-368 368c-28.3 28.3-33.9 57.7-33.6 77.4 0.2 14.7 3.8 29 10.3 42l-153.5 153.4c-5.3-1.4-10.9-2.2-16.6-2.2-17.1 0-33.2 6.7-45.3 18.7-25 25-25 65.6 0 90.5 12.1 12.1 28.2 18.8 45.3 18.8s33.2-6.7 45.3-18.7c16.7-16.7 22.2-40.4 16.6-61.8l153.4-153.5c13 6.5 27.3 10.1 42 10.3 0.5 0 1 0 1.5 0 19.6 0 48.3-6 75.9-33.6l368-368-45.3-45.3zM86.6 726.6c-6 6-14.1 9.4-22.6 9.4s-16.6-3.3-22.6-9.4c-12.5-12.5-12.5-32.8 0-45.3 0 0 0 0 0 0 6-6 14.1-9.4 22.6-9.4s16.6 3.3 22.6 9.4c12.5 12.5 12.5 32.8 0 45.3z"></path> <path d="M721.4 97.4l-368 368c-21.9 21.9-42.8 16.6-55.1 4.3s-17.6-33.2 4.3-55.1l368-368-45.2-45.2-368 368c-28.3 28.3-33.9 57.7-33.6 77.4 0.2 14.7 3.8 29 10.3 42l-153.5 153.4c-5.3-1.4-10.9-2.2-16.6-2.2-17.1 0-33.2 6.7-45.3 18.7-25 25-25 65.6 0 90.5 12.1 12.1 28.2 18.8 45.3 18.8s33.2-6.7 45.3-18.7c16.7-16.7 22.2-40.4 16.6-61.8l153.4-153.5c13 6.5 27.3 10.1 42 10.3 0.5 0 1 0 1.5 0 19.6 0 48.3-6 75.9-33.6l368-368-45.3-45.3zM86.6 726.6c-6 6-14.1 9.4-22.6 9.4s-16.6-3.3-22.6-9.4c-12.5-12.5-12.5-32.8 0-45.3 0 0 0 0 0 0 6-6 14.1-9.4 22.6-9.4s16.6 3.3 22.6 9.4c12.5 12.5 12.5 32.8 0 45.3z"></path>
<path d="M448 544v32c68.4 0 132.7-26.6 181-75s75-112.6 75-181h-32c0 123.5-100.5 224-224 224z"></path> <path d="M448 544v32c68.4 0 132.7-26.6 181-75s75-112.6 75-181h-32c0 123.5-100.5 224-224 224z"></path>
<path d="M224 320c0-123.5 100.5-224 224-224v-32c-68.4 0-132.7 26.6-181 75s-75 112.6-75 181h32z"></path> <path d="M224 320c0-123.5 100.5-224 224-224v-32c-68.4 0-132.7 26.6-181 75s-75 112.6-75 181h32z"></path>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,4 @@
<!-- generated by icomoon.io - licensed Lindua icon -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 768 768" width="100%" height="100%">
<path d="M704 64h-640c-35.3 0-64 28.7-64 64v352c0 35.3 28.7 64 64 64h288v64c0 15.1-12.4 30.6-34 42.6-24.9 13.8-58.3 21.4-94 21.4v32h320v-32c-35.8 0-69.2-7.6-94-21.4-21.6-12-34-27.5-34-42.6v-64h288c35.3 0 64-28.7 64-64v-352c0-35.3-28.7-64-64-64zM423.8 672h-79.6c24.9-16.9 39.8-39.2 39.8-64 0 24.8 14.9 47.1 39.8 64zM64 128h640v288h-640v-288c-0.1 0 0 0 0 0zM64 480v-32h640v32h-640z"></path>
</svg>

After

Width:  |  Height:  |  Size: 556 B

View File

@@ -66,6 +66,45 @@ interface NodeStatus {
bootInfo: BootInfo; bootInfo: BootInfo;
} }
export interface VM {
cpu: number;
cpus: number;
disk: number;
diskread: number;
diskwrite: number;
maxdisk: number;
maxmem: number;
mem: number;
name: string;
netin: number;
netout: number;
pid: number;
status: string;
uptime: number;
vmid: number;
}
export interface LXC {
cpu: number;
cpus: number;
disk: number;
diskread: number;
diskwrite: number;
maxdisk: number;
maxmem: number;
maxswap: number;
mem: number;
name: string;
netin: number;
netout: number;
pid: number;
status: string;
swap: number;
type: string;
uptime: number;
vmid: number;
}
export interface Node { export interface Node {
info: NodeStatus; info: NodeStatus;
online: number; online: number;

View File

@@ -57,15 +57,64 @@ async function getClusterInfo() {
}); });
} }
export async function vmInfo(nodeName: string, vmId: string) {
const r = buildProxmoxRequest();
r.url += `nodes/${nodeName}/qemu/${vmId}/config`;
return fetch(r.url, r?.options)
.then(resp => resp.json())
.then((response) => response.data)
}
export async function vmCloudInit(nodeName: string, vmId: string) {
const r = buildProxmoxRequest();
vmId = 121
r.url += `nodes/${nodeName}/qemu/${vmId}/cloudinit`;
return fetch(r.url, r?.options)
.then(resp => resp.json())
.then((response) => response.data)
}
export async function vmAgentOS(nodeName: string, vmId: string) {
const r = buildProxmoxRequest();
vmId = 121
r.url += `nodes/${nodeName}/qemu/${vmId}/agent/get-osinfo`;
return fetch(r.url, r?.options)
.then(resp => resp.json())
.then((response) => response.data?.result)
}
export async function vmAgentFS(nodeName: string, vmId: string) {
const r = buildProxmoxRequest();
vmId = 121
r.url += `nodes/${nodeName}/qemu/${vmId}/agent/get-fsinfo`;
return fetch(r.url, r?.options)
.then(resp => resp.json())
.then((response) => response.data?.result)
}
export async function vmAgentNetwork(nodeName: string, vmId: string) {
const r = buildProxmoxRequest();
vmId = 121
r.url += `nodes/${nodeName}/qemu/${vmId}/agent/network-get-interfaces`;
return fetch(r.url, r?.options)
.then(resp => resp.json())
.then((response) => response.data?.result)
}
export async function fetchNodes(): Promise<{ nodes: Node[]; cluster: Cluster | null }> { export async function fetchNodes(): Promise<{ nodes: Node[]; cluster: Cluster | null }> {
try { try {
const { nodes, cluster } = await getClusterInfo(); const { nodes, cluster } = await getClusterInfo();
const infoP = Promise.all(nodes.map((node: Node) => fetchNodeInfo(node))); const infoBulk = Promise.all(nodes.map((node: Node) => fetchNodeInfo(node)));
const vmsP = Promise.all(nodes.map((node: Node) => fetchNodeVMs(node))); const vmsBulk = Promise.all(nodes.map((node: Node) => fetchNodeVMs(node)));
const lxcsP = Promise.all(nodes.map((node: Node) => fetchNodeLXCs(node))); const lxcsBulk = Promise.all(nodes.map((node: Node) => fetchNodeLXCs(node)));
const [info, vms, lxcs] = await Promise.all([infoP, vmsP, lxcsP]); const [info, vms, lxcs] = await Promise.all([infoBulk, vmsBulk, lxcsBulk]);
return { return {
cluster, cluster,

View File

@@ -3,6 +3,9 @@
import PageHeader from '$lib/components/PageHeader.svelte'; import PageHeader from '$lib/components/PageHeader.svelte';
import ServerComp from '$lib/components/Server.svelte'; import ServerComp from '$lib/components/Server.svelte';
import ServerSummary from '$lib/components/ServerSummary.svelte'; import ServerSummary from '$lib/components/ServerSummary.svelte';
import VMComp from '$lib/components/VM.svelte';
import LXCComp from '$lib/components/LXC.svelte';
import type { VM, LXC } from '$lib/components/VM.svelte';
import type { PageData } from './$types'; import type { PageData } from './$types';
let { data }: { data: PageData } = $props(); let { data }: { data: PageData } = $props();
@@ -16,26 +19,25 @@
...allVms ...allVms
.map((vm) => { .map((vm) => {
return { return {
link: `/servers/vm/${vm.name}`, link: `/servers/vm/${vm.vmid}`,
...vm ...vm
}; };
}) })
.filter((d) => d.name), .filter((d) => d.name),
...nodes.map(node => { ...nodes.map((node) => {
return { return {
link: `/servers/node/${node.name}`, link: `/servers/node/${node.name}`,
...node ...node
} };
}), }),
...allLxcs.map(lxc => { ...allLxcs.map((lxc) => {
return { return {
link: `/servers/lxc/${lxc.name}`, link: `/servers/lxc/${lxc.vmid}`,
...lxc ...lxc
} };
}) })
]; ];
}); });
console.log(allLxcs)
</script> </script>
<PageHeader>Servers</PageHeader> <PageHeader>Servers</PageHeader>
@@ -44,18 +46,63 @@ console.log(allLxcs)
<div class="server-list"> <div class="server-list">
{#each nodes as node (node.name)} {#each nodes as node (node.name)}
<div> <ServerComp {node} />
<ServerComp {node} />
</div>
{/each} {/each}
</div> </div>
<details open>
<summary>
<h2>VMs</h2>
</summary>
<div class="vm-list">
{#each allVms as vm (vm.vmid)}
<VMComp {vm} />
{/each}
</div>
</details>
<details open>
<summary>
<h2>LXCs</h2>
</summary>
<div class="vm-list">
{#each allLxcs as lxc (lxc)}
<LXCComp {lxc} />
{/each}
</div>
</details>
<style lang="scss"> <style lang="scss">
.server-list { *:not(:last-child) {
margin-bottom: 2rem;
}
.server-list,
.vm-list {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
align-items: left; align-items: left;
gap: 2rem; gap: 2rem;
&:not(:last-of-type) {
margin-bottom: 2rem;
}
}
h2 {
font-weight: 500;
}
details summary::-webkit-details-marker,
details summary::marker {
display: none;
}
details > summary {
list-style: none;
cursor: pointer;
} }
</style> </style>

View File

@@ -0,0 +1,82 @@
import {
fetchNodes,
vmInfo,
vmCloudInit,
vmAgentOS,
vmAgentFS,
vmAgentNetwork
} from '$lib/server/proxmox';
import type { Node, VM } from '$lib/interfaces/proxmox';
import type { PageServerLoad } from './$types';
const AVAILABLE_RESOURCES = ['node', 'vm', 'lxc'];
const filterResources = (resource: Node | VM, id) => {
if (resource?.name && resource.name === id) {
return resource;
}
return null;
};
export const load: PageServerLoad = async ({ params }) => {
const { kind, id } = params;
console.log('KIND', kind);
if (!AVAILABLE_RESOURCES.includes(kind)) {
return {
error: 'No resource ' + kind,
resource: null
};
}
console.log(params.id);
const cluster = await fetchNodes();
let vm = [];
let nodeId;
const resources: Array<Node | VM> = [];
switch (kind) {
case 'node':
vm = cluster?.nodes.find((n) => n.name === id) || resources;
break;
case 'vm':
nodeId = cluster.nodes.find(
(n) => n.vms.filter((vm) => String(vm.vmid) === id)?.length > 0
)?.name;
const clusterVM = cluster.nodes.flatMap((n) => n.vms)?.find((vm) => String(vm.vmid) === id);
if (!nodeId || !clusterVM.vmid) return;
const [cloudInit, os, fs, network] = await Promise.all([
vmCloudInit(nodeId, clusterVM.vmid),
vmAgentOS(nodeId, clusterVM.vmid),
vmAgentFS(nodeId, clusterVM.vmid),
vmAgentNetwork(nodeId, clusterVM.vmid)
]);
vm = {
config: await vmInfo(nodeId, clusterVM.vmid),
cloudInit,
os,
fs,
network,
...clusterVM
};
// resources = [vm]
break;
case 'lxc':
vm = cluster.nodes.flatMap((n) => n.lxcs)?.find((lxc) => String(lxc.vmid) === id);
break;
default:
console.log('no resources found');
}
// console.log('returning', vm);
return {
resource: vm,
kind: params.kind,
id: params.id,
error: null
};
};

View File

@@ -0,0 +1,53 @@
<script lang="ts">
import JsonViewer from '$lib/components/JsonViewer.svelte';
import PageHeader from '$lib/components/PageHeader.svelte';
import VMDescribe from '$lib/components/prox-describe/VM.svelte';
import External from '$lib/icons/external.svelte';
import Link from '$lib/icons/link.svelte';
import type { Node, VM } from '$lib/interfaces/proxmox';
import type { PageData, PageProps } from './$types';
let { data }: { data: PageData } = $props();
const { error, kind } = data;
const { resource }: { vm: VM; node: Node } = data;
console.log('RESOURCE', resource);
let res = $state(data.kind);
const template = `https://apollo.schleppe:8006/#v1:0:=qemu%2F${resource?.vmid}:4:::::::`
console.log('kind res:', res);
</script>
<PageHeader>{kind || 'Resource'}: {resource?.name || 'not found'}</PageHeader>
{#if error}
<p>{error}</p>
{/if}
{#if resource}
<div class="title">
<a href={template}>View in proxmox</a>
<span class="link"><External /></span>
</div>
{#if kind == 'vm'}
<VMDescribe vm={resource} />
{:else}
<p>{kind} stuffs</p>
<JsonViewer json={resource} />
{/if}
{:else}
<h2>404. '{kind}' resource not found!</h2>
{/if}
<style lang="scss">
.title {
font-size: 1.1rem;
font-family: 'Reckless Neue';
}
.link {
display: inline-block;
height: 1rem;
width: 1rem;
}
</style>