initial commit

This commit is contained in:
WeeXnes 2025-02-26 09:38:06 +01:00
commit 6dc118c8c7
24 changed files with 682 additions and 0 deletions

49
app.vue Normal file
View file

@ -0,0 +1,49 @@
<template>
<div class="min-h-screen flex flex-col">
<!-- Navbar -->
<nav class="navbar bg-base-100 shadow-lg px-4">
<div class="flex-1">
<NuxtLink to="/" class="btn btn-ghost text-xl">Server Panel</NuxtLink>
</div>
<div class="hidden lg:flex">
<ul class="menu menu-horizontal px-1">
<li><NuxtLink to="/">Dashboard</NuxtLink></li>
<li><NuxtLink to="/settings">Settings</NuxtLink></li>
</ul>
</div>
<div class="lg:hidden">
<button @click="toggleMenu" class="btn btn-square btn-ghost">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="inline-block w-6 h-6 stroke-current">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-7 6h7"/>
</svg>
</button>
</div>
<!-- Mobile Menu -->
<div v-if="menuOpen" class="absolute top-16 left-0 w-full bg-base-100 shadow-lg lg:hidden z-50">
<ul class="menu menu-vertical p-4">
<li><NuxtLink to="/" @click="toggleMenu">Dashboard</NuxtLink></li>
<li><NuxtLink to="/settings" @click="toggleMenu">Settings</NuxtLink></li>
</ul>
</div>
</nav>
<!-- Main Content -->
<main class="flex-1 p-4">
<NuxtPage />
</main>
<!-- Footer -->
<footer class="footer p-4 bg-neutral text-neutral-content text-center">
<a href="https://github.com/WeeXnes"> ServerPanel by WeeXnes</a>
</footer>
</div>
</template>
<script setup>
import { ref } from 'vue'
const menuOpen = ref(false)
const toggleMenu = () => {
menuOpen.value = !menuOpen.value
}
</script>

6
nuxt.config.ts Normal file
View file

@ -0,0 +1,6 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2024-11-01',
devtools: { enabled: true },
modules: ['@nuxtjs/tailwindcss'],
})

27
package.json Normal file
View file

@ -0,0 +1,27 @@
{
"name": "nuxt-app",
"private": true,
"type": "module",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"nuxt": "^3.15.4",
"vue": "latest",
"vue-router": "latest",
"@nuxtjs/tailwindcss": "^6.13.1",
"axios": "^1.7.9",
"bcryptjs": "^3.0.0",
"daisyui": "^4.12.23",
"dotenv": "^16.4.7",
"jsonwebtoken": "^9.0.2",
"systeminformation": "^5.25.11"
},
"devDependencies": {
"@types/jsonwebtoken": "^9.0.8"
}
}

280
pages/index.vue Normal file
View file

@ -0,0 +1,280 @@
<script setup lang="ts">
import type { VM } from '~/types/VM';
import { reactive, ref } from 'vue';
import axios from 'axios';
import type {networkInterface} from "~/types/networkInterface";
import type {serviceInterface} from "~/types/serviceInterface";
const { data: virtualMachines } = await useAsyncData<VM[]>('vms', () => $fetch('/api/getVMs'));
const ignoreCache = true;
const startVm = async (vm: any) => {
try {
const response = await axios.post('/api/controlVM', {
action: 'start',
vm: vm
});
console.log(response.data);
if(response.data.status == 'success') {
virtualMachines.value?.forEach(vm_list => {
if(vm.name == vm_list.name) {
vm_list.state = "on";
}
})
}
} catch (error) {
console.error(`Error starting VM: ${vm.name}`, error);
}
};
const shutdownVm = async (vm: any) => {
try {
const response = await axios.post('/api/controlVM', {
action: 'shutdown',
vm: vm
});
console.log(response.data);
if(response.data.status == 'success') {
virtualMachines.value?.forEach(vm_list => {
if(vm.name == vm_list.name) {
vm_list.state = "off";
}
})
}
} catch (error) {
console.error(`Error shutting down VM: ${vm.name}`, error);
}
};
const osInfo = reactive({
isLoaded: false,
name: '',
version: '',
kernel: '',
architecture: ''
});
const serviceInfo = reactive({
isLoaded: false,
services: [] as serviceInterface[],
});
const cpuInfo = reactive({
isLoaded: false,
manufacturer: '',
model: '',
cores: 0,
speed: 0,
mainTemp: 0,
maxTemp: 0
});
const memoryInfo = reactive({
isLoaded: false,
total: 0,
free: 0,
used: 0
});
const networkInfo = reactive({
isLoaded: true,
interfacesList:[] as networkInterface[]
})
const fetchServiceInfo = async () => {
try{
let services = await $fetch('/api/getServices')
services?.forEach((interface_obj) => {
serviceInfo.services.push(interface_obj)
});
serviceInfo.isLoaded = true;
}catch(error){
console.error(`Error fetchOsInfo: ${error}`);
}
}
const fetchNetworkInfo = async () => {
try{
let networkInfoFetch = await $fetch('/api/getNetworkInterfaces')
networkInfoFetch?.forEach((interface_obj) => {
networkInfo.interfacesList.push(interface_obj)
});
networkInfo.isLoaded = true
}catch(error){
console.error(`Error fetchOsInfo: ${error}`);
}
}
const fetchOsInfo = async () => {
try{
let systemInfoFetch = await $fetch('/api/getSystem')
console.log(systemInfoFetch)
osInfo.name = systemInfoFetch?.platform || 'N/A'
osInfo.version = systemInfoFetch?.distro || 'N/A'
osInfo.kernel = systemInfoFetch?.kernel || 'N/A'
osInfo.architecture = systemInfoFetch?.arch || 'N/A'
osInfo.isLoaded = true
}catch(error){
console.error(`Error fetchOsInfo: ${error}`);
}
}
const fetchCpuTemp = async () => {
try {
let cpuInfoFetch = await $fetch('/api/getCpu')
console.log(cpuInfoFetch)
cpuInfo.manufacturer = cpuInfoFetch?.info.manufacturer || 'N/A'
cpuInfo.model = cpuInfoFetch?.info.brand || 'N/A'
cpuInfo.cores = cpuInfoFetch?.info.cores || 0
cpuInfo.speed = cpuInfoFetch?.info.speed || 0
cpuInfo.mainTemp = cpuInfoFetch?.temps.main || 0
cpuInfo.maxTemp = cpuInfoFetch?.temps.max || 0
} catch (error) {
console.error('Error fetching CPU temperature:', error);
}
};
const fetchMemoryInfo = async () => {
try{
let memoryInfoFetch = await $fetch('/api/getMemory')
console.log(memoryInfoFetch)
let ram_cache = ignoreCache ? (memoryInfoFetch?.cached ?? 0) : 0;
if(memoryInfoFetch?.total != null)
memoryInfo.total = Math.round( memoryInfoFetch?.total/ (1024 * 1024 * 1024)) || 0
if(memoryInfoFetch?.free != null)
memoryInfo.free = Math.round( (memoryInfoFetch?.free + ram_cache) / (1024 * 1024 * 1024)) || 0
if(memoryInfoFetch?.used != null)
memoryInfo.used = Math.round( (memoryInfoFetch?.used - ram_cache) / (1024 * 1024 * 1024)) || 0
memoryInfo.isLoaded = true
}catch(error){
console.error(`Error fetchOsInfo: ${error}`);
}
}
onMounted(async () => {
await fetchOsInfo()
await fetchCpuTemp()
cpuInfo.isLoaded = true
await fetchMemoryInfo()
await fetchNetworkInfo()
await fetchServiceInfo()
const intervalId = setInterval(fetchCpuTemp, 7000);
onUnmounted(() => {
clearInterval(intervalId);
});
})
</script>
<template>
<div class="flex flex-col items-center justify-center py-16 px-6">
<div class="grid md:grid-cols-3 gap-6 w-full max-w-5xl mb-8">
<div class="card bg-base-100 shadow-2xl p-6 opacity-0 transition-opacity duration-500 ease-in-out"
:class="{'opacity-100': osInfo.isLoaded}">
<h2 class="text-xl font-bold text-center">OS Info</h2>
<div class="mt-4 text-sm">
<p><strong>Operating System:</strong> {{ osInfo.name }}</p>
<p><strong>Version:</strong> {{ osInfo.version }}</p>
<p><strong>Kernel:</strong> {{ osInfo.kernel }}</p>
<p><strong>Architecture:</strong> {{ osInfo.architecture }}</p>
</div>
</div>
<div class="card bg-base-100 shadow-2xl p-6 opacity-0 transition-opacity duration-500 ease-in-out"
:class="{'opacity-100': cpuInfo.isLoaded}">
<h2 class="text-xl font-bold text-center">CPU Info</h2>
<div class="mt-4 text-sm">
<p><strong>Manufacturer:</strong> {{ cpuInfo.manufacturer }}</p>
<p><strong>Model:</strong> {{ cpuInfo.model }}</p>
<p><strong>Core Count:</strong> {{ cpuInfo.cores }}</p>
<p><strong>Speed:</strong> {{ cpuInfo.speed }} GHz</p>
<p><strong>Main Temp:</strong> {{ cpuInfo.mainTemp }} °C</p>
<p><strong>Max Temp:</strong> {{ cpuInfo.maxTemp }} °C</p>
</div>
</div>
<div class="card bg-base-100 shadow-2xl p-6 opacity-0 transition-opacity duration-500 ease-in-out"
:class="{'opacity-100': memoryInfo.isLoaded}">
<h2 class="text-xl font-bold text-center">Memory Info</h2>
<div class="mt-4 text-sm">
<p><strong>Total Memory:</strong> {{ memoryInfo.total }} GB</p>
<p><strong>Free Memory:</strong> {{ memoryInfo.free }} GB</p>
<p><strong>Used Memory:</strong> {{ memoryInfo.used }} GB</p>
</div>
</div>
<div v-for="ifs in networkInfo.interfacesList" class="card bg-base-100 shadow-2xl p-6">
<h2 :class="{
'text-green-500': ifs.state === 'up',
'text-red-500': ifs.state === 'down',
'text-yellow-500': ifs.state === 'unknown'
}"
class="text-xl font-bold text-center">Interface: {{ ifs.name }}</h2>
<div class="mt-4 text-sm">
<p><strong>Interface Name:</strong> {{ ifs.name }}</p>
<p><strong>IPv4:</strong> {{ ifs.ip4 }} / {{ ifs.ip4subnet }}</p>
<p><strong>IPv6:</strong> {{ ifs.ip6 }}</p>
<p><strong>State:</strong> {{ ifs.state }}</p>
</div>
</div>
</div>
<h1 class="text-4xl font-bold text-center mb-6">QEMU Virtual Machines</h1>
<div class="grid md:grid-cols-3 gap-6 w-full max-w-5xl mb-8">
<div
v-for="vm in virtualMachines"
class="card bg-base-100 shadow-2xl p-6"
>
<h2 :class="vm.state === 'on' ? 'text-green-500' : 'text-red-500'" class="text-xl font-bold text-center">
{{ vm.name }}
</h2>
<div class="mt-2 text-sm">
<p><strong>OS:</strong> {{ vm.os }}</p>
<p><strong>CPU(s):</strong> {{ vm.vCpuCount }}</p>
<p><strong>Max Memory:</strong> {{ vm.maxMemory }} MB</p>
<p><strong>Autostart:</strong> {{ vm.autostart ? 'Enabled' : 'Disabled' }}</p>
</div>
<div class="form-control mt-4 flex flex-row gap-2">
<button @click="startVm(vm)" class="btn btn-primary w-1/2">Start</button>
<button @click="shutdownVm(vm)" class="btn btn-error w-1/2">Shutdown</button>
</div>
</div>
</div>
<h1 v-if="serviceInfo.isLoaded" class="text-4xl font-bold text-center mb-6">Services</h1>
<div v-if="serviceInfo.isLoaded" class="grid md:grid-cols-3 gap-6 w-full max-w-5xl">
<div
v-for="service in serviceInfo.services"
class="card bg-base-100 shadow-2xl p-6">
<h2 :class="service.state === true ? 'text-green-500' : 'text-red-500'" class="text-xl font-bold text-center">
{{ service.name }}
</h2>
<div class="mt-2 text-sm">
<p><strong>Name:</strong> {{ service.name }}</p>
<p><strong>State:</strong> {{ service.state ? "Running" : "Not Running" }}</p>
</div>
<div class="form-control mt-4 flex flex-row gap-2">
<button class="btn btn-primary w-1/2">Start</button>
<button class="btn btn-error w-1/2">Stop</button>
</div>
</div>
</div>
</div>
</template>

65
pages/login.vue Normal file
View file

@ -0,0 +1,65 @@
<template>
<div class="flex items-center justify-center py-16">
<div class="card w-96 bg-base-100 shadow-2xl p-6">
<h2 class="text-2xl font-bold text-center">Login</h2>
<form @submit.prevent="handleLogin" class="mt-4">
<div class="form-control">
<label class="label">
<span class="label-text">E-Mail</span>
</label>
<input
v-model="email"
type="email"
placeholder="Enter your email"
class="input input-bordered"
required
/>
</div>
<div class="form-control mt-2">
<label class="label">
<span class="label-text">Password</span>
</label>
<input
v-model="password"
type="password"
placeholder="Enter your password"
class="input input-bordered"
required
/>
</div>
<div class="form-control mt-4">
<button type="submit" class="btn btn-primary w-full">Login</button>
</div>
</form>
<div class="mt-4 text-center text-sm">
<p>Don't have an account? <a href="/register" class="text-blue-500 hover:underline">Register here</a></p>
</div>
</div>
</div>
</template>
<script lang="ts">
import { ref } from 'vue';
import axios from 'axios';
import { useRouter } from 'vue-router';
export default {
data() {
return {
email: '',
password: ''
};
},
methods: {
async handleLogin() {
}
}
};
</script>

16
pages/settings.vue Normal file
View file

@ -0,0 +1,16 @@
<script setup lang="ts">
import type { VM } from '../types/VM';
import axios from 'axios';
</script>
<template>
<h1>hola</h1>
</template>
<style scoped>
</style>

21
panel.config.ts Normal file
View file

@ -0,0 +1,21 @@
import { reactive } from "vue";
export const settings = reactive({
ignoreCache: false,
enable_services: false,
enable_qemu_controls: true,
qemu_vms: [
{
name: "Gameserver",
os: "Ubuntu 24.04"
},
{
name: "Ubuntu_VM1",
os: "Ubuntu 24.04"
},
],
systemctl_services:[
"libvirt",
"frp"
]
});

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

1
public/robots.txt Normal file
View file

@ -0,0 +1 @@

27
server/api/controlVM.ts Normal file
View file

@ -0,0 +1,27 @@
import { exec } from 'child_process';
export default defineEventHandler(async (event) => {
const body = await readBody(event);
const { action, vm } = body;
try {
const command = action === 'start'
? `virsh start ${vm.name}`
: `virsh shutdown ${vm.name}`;
await new Promise((resolve, reject) => {
exec(command, (error, stdout, stderr) => {
if (error || stderr) {
reject(`Error: ${stderr || error?.message}`);
}
resolve(stdout);
});
});
return { status: 'success', message: `VM ${action} successful`, vm };
} catch (error) {
return { status: 'error', message: `Failed to ${action} VM`, error: error };
}
});

16
server/api/getCpu.ts Normal file
View file

@ -0,0 +1,16 @@
import si from 'systeminformation';
export default defineEventHandler(async () => {
try {
const cpuData = await si.cpu();
const cpuTemp = await si.cpuTemperature();
return {
info: cpuData, // `info` is the key, `cpuData` is the value
temps: cpuTemp // `temps` is the key, `cpuTemp` is the value
};
} catch (error) {
console.error('Error fetching CPU info:', error);
}
});

12
server/api/getMemory.ts Normal file
View file

@ -0,0 +1,12 @@
import si from 'systeminformation';
export default defineEventHandler(async () => {
try {
const memoryData = await si.mem();
return memoryData;
} catch (error) {
console.error('Error fetching CPU info:', error);
}
});

View file

@ -0,0 +1,45 @@
import { execSync } from 'child_process';
import si from 'systeminformation';
import {VM} from "~/types/VM";
import {networkInterface} from "~/types/networkInterface";
export default defineEventHandler(async () => {
try {
const cpuData = await si.cpu();
const cpuTemp = await si.cpuTemperature();
const osInfo = await si.osInfo();
const network = await si.networkInterfaces();
const interfaces: networkInterface[] = [];
if (Array.isArray(network)) {
network.forEach((interface_obj) => {
interfaces.push({
name: interface_obj.ifaceName,
ip4: interface_obj.ip4,
ip6: interface_obj.ip6,
ip4subnet: interface_obj.ip4subnet,
ip6subnet: interface_obj.ip6subnet,
state: interface_obj.operstate as "up" | "down" | "unknown"
})
});
} else {
console.log(network.ifaceName + " is reachable at " + network.ip4);
interfaces.push({
name: network.ifaceName,
ip4: network.ip4,
ip6: network.ip6,
ip4subnet: network.ip4subnet,
ip6subnet: network.ip6subnet,
state: network.operstate as "up" | "down" | "unknown"
})
}
interfaces.forEach(obj => {
console.log(obj.name + " is " + obj.state);
})
return interfaces;
} catch (error) {
console.error('Error fetching CPU info:', error);
}
});

30
server/api/getServices.ts Normal file
View file

@ -0,0 +1,30 @@
import { execSync } from 'child_process';
import si from 'systeminformation';
import {VM} from "~/types/VM";
import {serviceInterface} from "~/types/serviceInterface";
import {settings} from "~/panel.config";
export default defineEventHandler(async () => {
try {
const services = await si.services(settings.systemctl_services.join(', '));
const interfaces: serviceInterface[] = [];
if (Array.isArray(services)) {
console.log(`services is array`);
services.forEach((interface_obj) => {
interfaces.push({
name: interface_obj.name,
state: interface_obj.running
})
});
} else {
interfaces.push(services);
}
return interfaces;
} catch (error) {
console.error('Error fetching CPU info:', error);
}
});

10
server/api/getSettings.ts Normal file
View file

@ -0,0 +1,10 @@
import si from 'systeminformation';
import {settings} from "~/panel.config";
export default defineEventHandler(async () => {
try {
return settings
} catch (error) {
console.error('Error fetching CPU info:', error);
}
});

12
server/api/getSystem.ts Normal file
View file

@ -0,0 +1,12 @@
import si from 'systeminformation';
export default defineEventHandler(async () => {
try {
const systemData = await si.osInfo();
return systemData;
} catch (error) {
console.error('Error fetching CPU info:', error);
}
});

28
server/api/getVMs.ts Normal file
View file

@ -0,0 +1,28 @@
import { execSync } from 'child_process';
import {VM} from "~/types/VM";
import {settings} from "~/panel.config";
export default defineEventHandler(() => {
let vmNames = ["Gameserver", "Ubuntu_VM1"]
const virtualMachines: VM[] = [];
settings.qemu_vms.forEach(vm => {
const vCpuCount = parseInt(execSync(`LANG=C virsh dominfo ${vm.name} | grep 'CPU(s)' | awk '{print $2}'`).toString().trim());
const maxMemory = parseInt(execSync(`LANG=C virsh dominfo ${vm.name} | grep 'Max memory' | awk '{print $3}'`).toString().trim()) / 1024;
const autostartValue = execSync(`LANG=C virsh dominfo ${vm.name} | grep 'Autostart' | awk '{print $2}'`).toString().trim();
const autostart = autostartValue === "enable";
const stateValue = execSync(`LANG=C virsh dominfo ${vm.name} | grep 'State' | awk '{print $2, $3}'`).toString().trim();
const state: 'on' | 'off' = stateValue === "running" ? 'on' : 'off';
virtualMachines.push({
name: vm.name,
os: vm.os,
vCpuCount: vCpuCount,
maxMemory: maxMemory,
autostart: autostart,
state: state
});
});
return virtualMachines;
});

7
server/plugins/init.ts Normal file
View file

@ -0,0 +1,7 @@
import { defineNitroPlugin } from "#imports";
export default defineNitroPlugin((nitroApp) => {
console.log("Loading Server Settings");
});

3
server/tsconfig.json Normal file
View file

@ -0,0 +1,3 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}

3
tailwind.config.js Normal file
View file

@ -0,0 +1,3 @@
module.exports = {
plugins: [require('daisyui')],
};

4
tsconfig.json Normal file
View file

@ -0,0 +1,4 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}

8
types/VM.ts Normal file
View file

@ -0,0 +1,8 @@
export interface VM {
name: string;
os: string;
vCpuCount: number;
maxMemory: number;
autostart: boolean;
state: "on" | "off";
}

View file

@ -0,0 +1,8 @@
export interface networkInterface {
name: string;
ip4: string;
ip6: string;
ip4subnet: string;
ip6subnet: string;
state: "up" | "down" | "unknown";
}

View file

@ -0,0 +1,4 @@
export interface serviceInterface {
name: string;
state: boolean;
}