feat(homelab)!: migrate to svelte kit, add more server stats endpoints

This commit is contained in:
2026-01-12 00:09:22 -08:00
parent 1f43dff2c5
commit 73b6cc4f1d
29 changed files with 721 additions and 107 deletions

13
nix/homelab/frontend/src/app.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

View File

@@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<!-- <link rel="stylesheet" href="/src/app.css" /> -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- <title>Homelab</title> -->
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@@ -2,6 +2,12 @@
import { createQuery } from "@tanstack/svelte-query";
import { fetchStats } from "./stats";
import { LoaderCircle } from "@lucide/svelte";
import {
formatDistance,
formatDistanceToNow,
formatDuration,
intervalToDuration,
} from "date-fns";
const query = createQuery(() => ({
queryKey: ["minecraft-server-stats"],
@@ -9,6 +15,21 @@
refetchInterval: 10000,
staleTime: 5000,
}));
const formatUptime = () => {
if (!query.data) {
return "--";
}
const started_at = query.data.uptime.started_at;
const duration = intervalToDuration({ start: started_at, end: new Date() });
$inspect(duration);
if (!duration.days && (duration.hours ?? 0) < 1) {
return formatDistanceToNow(new Date(query.data.uptime.started_at));
}
return formatDuration(duration, {
format: ["days", "hours", "minutes"],
});
};
</script>
{#snippet statItem(label: string, value?: string | number)}
@@ -21,7 +42,7 @@
<div class="border border-zinc-600 rounded-lg p-4">
<h3 class="text-lg font-medium mb-3">
Server Stats {#if query.isError}
<span class="text-sm text-red-500"
<span class="ml-1 text-sm text-red-500"
>an error occured fetching server stats</span
>
{:else if query.isPending}
@@ -31,7 +52,7 @@
<div class="grid grid-cols-2 gap-4 text-zinc-400">
{@render statItem("Players Online", query.data?.players_online ?? "--")}
{@render statItem("Server Status", query.data?.status ?? "--")}
{@render statItem("Uptime", query.data?.uptime ?? "--")}
{@render statItem("Uptime", formatUptime())}
{@render statItem("World Size", query.data?.world_size ?? "--")}
</div>
</div>

View File

@@ -2,13 +2,14 @@ type StatsResponse = {
status: string;
players_online: number;
max_players: number;
uptime: string;
uptime: {
seconds: number;
started_at: string;
};
world_size: string;
};
export const fetchStats = async () => {
const response = await fetch(
"http://localhost:5173/api/minecraft-server-stats",
);
const response = await fetch("/api/minecraft-server-stats");
return (await response.json()) as StatsResponse;
};

View File

@@ -1,9 +0,0 @@
import { mount } from 'svelte'
import './app.css'
import App from './App.svelte'
const app = mount(App, {
target: document.getElementById('app')!,
})
export default app

View File

@@ -0,0 +1,7 @@
<script lang="ts">
import "./layout.css";
let { children } = $props();
</script>
{@render children()}

View File

@@ -0,0 +1,2 @@
export const ssr = false;
export const prerender = false;

View File

@@ -1,26 +1,22 @@
<script lang="ts">
import {
createQuery,
QueryClient,
QueryClientProvider,
} from "@tanstack/svelte-query";
import Actions from "./lib/minecraft/Actions.svelte";
import Stats from "./lib/minecraft/Stats.svelte";
import { QueryClient, QueryClientProvider } from "@tanstack/svelte-query";
import Actions from "$lib/minecraft/Actions.svelte";
import Stats from "$lib/minecraft/Stats.svelte";
import { SvelteQueryDevtools } from "@tanstack/svelte-query-devtools";
const queryClient = new QueryClient();
</script>
<QueryClientProvider client={queryClient}>
<main class="flex justify-center mt-8">
<main class="mt-8 flex justify-center">
<div class="w-full max-w-2xl px-4">
<div class="flex justify-center">
<h1 class="text-2xl font-semibold mb-8">Management</h1>
<h1 class="mb-8 text-2xl font-semibold">Management</h1>
</div>
<!-- Minecraft Section -->
<section class="bg-zinc-800 rounded-lg p-6">
<h2 class="text-xl mb-4">Minecraft</h2>
<section class="rounded-lg bg-zinc-800 p-6">
<h2 class="mb-4 text-xl">Minecraft</h2>
<Actions />
<Stats />
</section>