From 9479d7c73826abc3dff20189bb27fbf77657f0cb Mon Sep 17 00:00:00 2001
From: zowks <67444066+zowks@users.noreply.github.com>
Date: Sat, 16 May 2026 05:12:44 +0000
Subject: [PATCH 1/2] feat(frontend): add `/stats` page
---
packages/backend/src/index.dto.ts | 1 +
.../src/lib/remotes/clusters.remote.ts | 32 +++++++++
.../frontend/src/lib/utils/formatBytes.ts | 2 +-
.../src/routes/(app)/stats/+layout.svelte | 9 +++
.../src/routes/(app)/stats/+page.svelte | 66 +++++++++++++++++++
5 files changed, 109 insertions(+), 1 deletion(-)
create mode 100644 packages/frontend/src/routes/(app)/stats/+layout.svelte
create mode 100644 packages/frontend/src/routes/(app)/stats/+page.svelte
diff --git a/packages/backend/src/index.dto.ts b/packages/backend/src/index.dto.ts
index e4dab74..d41ab7d 100644
--- a/packages/backend/src/index.dto.ts
+++ b/packages/backend/src/index.dto.ts
@@ -13,3 +13,4 @@ export {
type GetClusterLogsDto,
} from './clusters/dto/get-cluster-logs.dto.js';
export { GetClusterStatsSchema, type GetClusterStatsDto } from './clusters/dto/get-cluster-stats.dto.js';
+export { type GetAggregateStatsDto } from './clusters/dto/get-aggregate-stats.dto.js';
diff --git a/packages/frontend/src/lib/remotes/clusters.remote.ts b/packages/frontend/src/lib/remotes/clusters.remote.ts
index bf5ae59..2f75e36 100644
--- a/packages/frontend/src/lib/remotes/clusters.remote.ts
+++ b/packages/frontend/src/lib/remotes/clusters.remote.ts
@@ -1,6 +1,7 @@
import { command, getRequestEvent, query } from "$app/server";
import { env } from "$env/dynamic/private";
import {
+ type GetAggregateStatsDto,
type GetClusterDto,
type GetClusterLogsDto,
type GetClusterStatsDto,
@@ -226,3 +227,34 @@ export const getClusterStatsLive = query.live(
}
},
);
+
+export const getClustersStatsLive = query.live(async function* () {
+ const token = getRequestEvent().cookies.get("token");
+
+ const response = await fetch(new URL("/clusters/stats/stream?interval=2", env.API_URL), {
+ headers: {
+ Accept: "text/event-stream",
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ switch (response.status) {
+ case 200: {
+ if (!response.body) return error(500, "An error occurred");
+
+ const chunks = response.body
+ .pipeThrough(new TextDecoderStream())
+ .pipeThrough(new EventSourceParserStream())
+ // @ts-ignore svelte-check false positive
+ .values();
+
+ for await (const chunk of chunks) yield JSON.parse(chunk.data) as GetAggregateStatsDto;
+ break;
+ }
+ case 401:
+ return error(401, "Unauthorized");
+
+ default:
+ return error(500, "An error occurred");
+ }
+});
diff --git a/packages/frontend/src/lib/utils/formatBytes.ts b/packages/frontend/src/lib/utils/formatBytes.ts
index cb32466..fcd4c97 100644
--- a/packages/frontend/src/lib/utils/formatBytes.ts
+++ b/packages/frontend/src/lib/utils/formatBytes.ts
@@ -1,5 +1,5 @@
export default function formatBytes(bytes: number) {
const suffixes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const i = Math.max(0, Math.floor(Math.log(bytes) / Math.log(1024)));
- return (!bytes && "0 Bytes") || (bytes / Math.pow(1024, i)).toFixed(2) + " " + suffixes[i];
+ return (!bytes && "0 Bytes") || (bytes / Math.pow(1024, i)).toPrecision(3) + " " + suffixes[i];
}
diff --git a/packages/frontend/src/routes/(app)/stats/+layout.svelte b/packages/frontend/src/routes/(app)/stats/+layout.svelte
new file mode 100644
index 0000000..58dccd8
--- /dev/null
+++ b/packages/frontend/src/routes/(app)/stats/+layout.svelte
@@ -0,0 +1,9 @@
+
+
+
+ {@render children()}
+
diff --git a/packages/frontend/src/routes/(app)/stats/+page.svelte b/packages/frontend/src/routes/(app)/stats/+page.svelte
new file mode 100644
index 0000000..b31e380
--- /dev/null
+++ b/packages/frontend/src/routes/(app)/stats/+page.svelte
@@ -0,0 +1,66 @@
+
+
+{#snippet card(
+ label: string,
+ icon: typeof Icon,
+ percentage: number,
+ value: string[],
+)}
+ {@const Icon = icon}
+
+
+
+
+
+
{label}
+ {#each value as value}
+
{value}
+ {/each}
+
+
+
+
+
+{/snippet}
+
+
+ {@render card("CPU", CpuIcon, cpu, [cpu.toPrecision(2) + "%"])}
+ {@render card("Memory", MemoryStickIcon, memory, [
+ memory.toPrecision(2) + "%",
+ formatBytes(memoryUsage),
+ ])}
+
From 40c4000d6ee5320167a80fa0d91ec7ad25b5aaee Mon Sep 17 00:00:00 2001
From: zowks <67444066+zowks@users.noreply.github.com>
Date: Sat, 16 May 2026 12:49:43 +0000
Subject: [PATCH 2/2] refactor(frontend): moved dashboard to `/clusters`
---
packages/frontend/src/hooks.server.ts | 11 +++++++++++
packages/frontend/src/lib/remotes/auth.remote.ts | 2 +-
.../src/routes/(app)/{ => clusters}/+layout.svelte | 0
.../src/routes/(app)/{ => clusters}/+page.svelte | 0
.../{ => clusters}/components/ClusterActions.svelte | 0
.../(app)/{ => clusters}/components/ClusterCPU.svelte | 0
.../{ => clusters}/components/ClusterLogs.svelte | 0
.../{ => clusters}/components/ClusterMemory.svelte | 0
.../{ => clusters}/components/ClusterShards.svelte | 0
.../{ => clusters}/components/ClusterStatus.svelte | 0
.../(app)/{ => clusters}/components/Header.svelte | 0
11 files changed, 12 insertions(+), 1 deletion(-)
create mode 100644 packages/frontend/src/hooks.server.ts
rename packages/frontend/src/routes/(app)/{ => clusters}/+layout.svelte (100%)
rename packages/frontend/src/routes/(app)/{ => clusters}/+page.svelte (100%)
rename packages/frontend/src/routes/(app)/{ => clusters}/components/ClusterActions.svelte (100%)
rename packages/frontend/src/routes/(app)/{ => clusters}/components/ClusterCPU.svelte (100%)
rename packages/frontend/src/routes/(app)/{ => clusters}/components/ClusterLogs.svelte (100%)
rename packages/frontend/src/routes/(app)/{ => clusters}/components/ClusterMemory.svelte (100%)
rename packages/frontend/src/routes/(app)/{ => clusters}/components/ClusterShards.svelte (100%)
rename packages/frontend/src/routes/(app)/{ => clusters}/components/ClusterStatus.svelte (100%)
rename packages/frontend/src/routes/(app)/{ => clusters}/components/Header.svelte (100%)
diff --git a/packages/frontend/src/hooks.server.ts b/packages/frontend/src/hooks.server.ts
new file mode 100644
index 0000000..3da0e53
--- /dev/null
+++ b/packages/frontend/src/hooks.server.ts
@@ -0,0 +1,11 @@
+import { redirect, type HandleClientError } from "@sveltejs/kit";
+
+export const handleError: HandleClientError = async ({ status }) => {
+ switch (status) {
+ case 404:
+ return redirect(303, "/clusters");
+
+ default:
+ break;
+ }
+};
diff --git a/packages/frontend/src/lib/remotes/auth.remote.ts b/packages/frontend/src/lib/remotes/auth.remote.ts
index 3b244c0..30df2f5 100644
--- a/packages/frontend/src/lib/remotes/auth.remote.ts
+++ b/packages/frontend/src/lib/remotes/auth.remote.ts
@@ -45,7 +45,7 @@ export const login = form(loginSchema, async (data, issue) => {
const { token } = await response.json();
cookies.set("token", token, { path: "/" });
- return redirect(303, "/");
+ return redirect(303, "/clusters");
}
case 400:
return error(400, "Invalid fields");
diff --git a/packages/frontend/src/routes/(app)/+layout.svelte b/packages/frontend/src/routes/(app)/clusters/+layout.svelte
similarity index 100%
rename from packages/frontend/src/routes/(app)/+layout.svelte
rename to packages/frontend/src/routes/(app)/clusters/+layout.svelte
diff --git a/packages/frontend/src/routes/(app)/+page.svelte b/packages/frontend/src/routes/(app)/clusters/+page.svelte
similarity index 100%
rename from packages/frontend/src/routes/(app)/+page.svelte
rename to packages/frontend/src/routes/(app)/clusters/+page.svelte
diff --git a/packages/frontend/src/routes/(app)/components/ClusterActions.svelte b/packages/frontend/src/routes/(app)/clusters/components/ClusterActions.svelte
similarity index 100%
rename from packages/frontend/src/routes/(app)/components/ClusterActions.svelte
rename to packages/frontend/src/routes/(app)/clusters/components/ClusterActions.svelte
diff --git a/packages/frontend/src/routes/(app)/components/ClusterCPU.svelte b/packages/frontend/src/routes/(app)/clusters/components/ClusterCPU.svelte
similarity index 100%
rename from packages/frontend/src/routes/(app)/components/ClusterCPU.svelte
rename to packages/frontend/src/routes/(app)/clusters/components/ClusterCPU.svelte
diff --git a/packages/frontend/src/routes/(app)/components/ClusterLogs.svelte b/packages/frontend/src/routes/(app)/clusters/components/ClusterLogs.svelte
similarity index 100%
rename from packages/frontend/src/routes/(app)/components/ClusterLogs.svelte
rename to packages/frontend/src/routes/(app)/clusters/components/ClusterLogs.svelte
diff --git a/packages/frontend/src/routes/(app)/components/ClusterMemory.svelte b/packages/frontend/src/routes/(app)/clusters/components/ClusterMemory.svelte
similarity index 100%
rename from packages/frontend/src/routes/(app)/components/ClusterMemory.svelte
rename to packages/frontend/src/routes/(app)/clusters/components/ClusterMemory.svelte
diff --git a/packages/frontend/src/routes/(app)/components/ClusterShards.svelte b/packages/frontend/src/routes/(app)/clusters/components/ClusterShards.svelte
similarity index 100%
rename from packages/frontend/src/routes/(app)/components/ClusterShards.svelte
rename to packages/frontend/src/routes/(app)/clusters/components/ClusterShards.svelte
diff --git a/packages/frontend/src/routes/(app)/components/ClusterStatus.svelte b/packages/frontend/src/routes/(app)/clusters/components/ClusterStatus.svelte
similarity index 100%
rename from packages/frontend/src/routes/(app)/components/ClusterStatus.svelte
rename to packages/frontend/src/routes/(app)/clusters/components/ClusterStatus.svelte
diff --git a/packages/frontend/src/routes/(app)/components/Header.svelte b/packages/frontend/src/routes/(app)/clusters/components/Header.svelte
similarity index 100%
rename from packages/frontend/src/routes/(app)/components/Header.svelte
rename to packages/frontend/src/routes/(app)/clusters/components/Header.svelte