diff --git a/src/web/public/script.js b/src/web/public/script.js
new file mode 100644
index 0000000..6952f16
--- /dev/null
+++ b/src/web/public/script.js
@@ -0,0 +1,36 @@
+function formatUptime(seconds) {
+ if (seconds < 0) return "0s";
+
+ const days = Math.floor(seconds / (3600 * 24));
+ const hours = Math.floor((seconds % (3600 * 24)) / 3600);
+ const minutes = Math.floor((seconds % 3600) / 60);
+ const secs = Math.floor(seconds % 60);
+
+ const parts = [];
+ if (days > 0) parts.push(`${days}d`);
+ if (hours > 0) parts.push(`${hours}h`);
+ if (minutes > 0) parts.push(`${minutes}m`);
+ parts.push(`${secs}s`);
+
+ return parts.join(" ");
+}
+
+function updateUptime() {
+ const el = document.getElementById("uptime-display");
+ if (!el) return;
+
+ const startTimestamp = parseInt(el.getAttribute("data-start-timestamp"), 10);
+ if (isNaN(startTimestamp)) return;
+
+ const now = Date.now();
+ const elapsedSeconds = (now - startTimestamp) / 1000;
+
+ el.textContent = formatUptime(elapsedSeconds);
+}
+
+document.addEventListener("DOMContentLoaded", () => {
+ // Update immediately to prevent stale content flash if possible
+ updateUptime();
+ // Update every second
+ setInterval(updateUptime, 1000);
+});
diff --git a/src/web/routes/home.ts b/src/web/routes/home.ts
index abcfbf5..c18e24a 100644
--- a/src/web/routes/home.ts
+++ b/src/web/routes/home.ts
@@ -1,6 +1,10 @@
import { BaseLayout } from "../views/layout";
+import { formatUptime } from "../utils/format";
export function homeRoute(): Response {
+ const uptime = formatUptime(process.uptime());
+ const startTimestamp = Date.now() - (process.uptime() * 1000);
+
const content = `
Welcome
@@ -9,6 +13,7 @@ export function homeRoute(): Response {
Status
System operational.
+
Uptime: ${uptime}
`;
diff --git a/src/web/utils/format.test.ts b/src/web/utils/format.test.ts
new file mode 100644
index 0000000..c88bf1d
--- /dev/null
+++ b/src/web/utils/format.test.ts
@@ -0,0 +1,24 @@
+import { describe, expect, it } from "bun:test";
+import { formatUptime } from "./format";
+
+describe("formatUptime", () => {
+ it("formats seconds correctly", () => {
+ expect(formatUptime(45)).toBe("45s");
+ });
+
+ it("formats minutes and seconds", () => {
+ expect(formatUptime(65)).toBe("1m 5s");
+ });
+
+ it("formats hours, minutes, and seconds", () => {
+ expect(formatUptime(3665)).toBe("1h 1m 5s");
+ });
+
+ it("formats days correctly", () => {
+ expect(formatUptime(90061)).toBe("1d 1h 1m 1s");
+ });
+
+ it("handles zero", () => {
+ expect(formatUptime(0)).toBe("0s");
+ });
+});
diff --git a/src/web/utils/format.ts b/src/web/utils/format.ts
new file mode 100644
index 0000000..b03cc65
--- /dev/null
+++ b/src/web/utils/format.ts
@@ -0,0 +1,20 @@
+/**
+ * Formats a duration in seconds into a human-readable string.
+ * Example: 3665 -> "1h 1m 5s"
+ */
+export function formatUptime(seconds: number): string {
+ if (seconds < 0) return "0s";
+
+ const days = Math.floor(seconds / (3600 * 24));
+ const hours = Math.floor((seconds % (3600 * 24)) / 3600);
+ const minutes = Math.floor((seconds % 3600) / 60);
+ const secs = Math.floor(seconds % 60);
+
+ const parts = [];
+ if (days > 0) parts.push(`${days}d`);
+ if (hours > 0) parts.push(`${hours}h`);
+ if (minutes > 0) parts.push(`${minutes}m`);
+ parts.push(`${secs}s`);
+
+ return parts.join(" ");
+}
diff --git a/src/web/views/layout.ts b/src/web/views/layout.ts
index 434c60e..e68e4f7 100644
--- a/src/web/views/layout.ts
+++ b/src/web/views/layout.ts
@@ -29,6 +29,7 @@ export function BaseLayout({ title, content }: LayoutProps): string {
+