Skip to content

Commit 57e7bf1

Browse files
committed
feat(admin): show server version in sidebar, login and setup screens
The sidebar pill was hardcoded to "v0.9.1" and the setup wizard to "v0.1.0", both of which were stale. Login displayed nothing. Expose the server version via /api/health (already public, rate-limit exempt) and add a small useVersion() hook with one cached fetch shared across mounts. Sidebar, Login and Setup pull the live value; the pill renders nothing until the first response lands so we never flash a wrong number.
1 parent f2a8495 commit 57e7bf1

5 files changed

Lines changed: 61 additions & 7 deletions

File tree

admin/src/components/Shell.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { NavLink, useNavigate } from "react-router-dom";
33
import Icon from "./Icon.tsx";
44
import { VaultbaseLogo } from "./VaultbaseLogo.tsx";
55
import { useAuth } from "../stores/auth.ts";
6+
import { useVersion } from "../stores/version.ts";
67

78
interface NavItem { to: string; label: string; icon: string; end?: boolean }
89
interface NavSection { label: string; items: NavItem[] }
@@ -16,8 +17,6 @@ const SECTIONS: NavSection[] = [
1617
{ to: "/_/", label: "Dashboard", icon: "grid", end: true },
1718
{ to: "/_/collections", label: "Collections", icon: "stack" },
1819
{ to: "/_/logs", label: "Logs", icon: "scroll" },
19-
{ to: "/_/api-preview", label: "API preview", icon: "play" },
20-
{ to: "/_/sql", label: "SQL", icon: "database" },
2120
],
2221
},
2322
{
@@ -27,6 +26,13 @@ const SECTIONS: NavSection[] = [
2726
{ to: "/_/flags", label: "Feature flags", icon: "flag" },
2827
{ to: "/_/webhooks", label: "Webhooks", icon: "arrowUp" },
2928
{ to: "/_/api-tokens", label: "API tokens", icon: "key" },
29+
],
30+
},
31+
{
32+
label: "Tools",
33+
items: [
34+
{ to: "/_/api-preview", label: "API preview", icon: "play" },
35+
{ to: "/_/sql", label: "SQL", icon: "database" },
3036
{ to: "/_/mcp", label: "MCP", icon: "zap" },
3137
],
3238
},
@@ -42,6 +48,7 @@ const SECTIONS: NavSection[] = [
4248

4349
export const Sidebar: React.FC = () => {
4450
const adminEmail = useAuth((s) => s.email);
51+
const version = useVersion();
4552
const navigate = useNavigate();
4653
const initial = adminEmail ? adminEmail[0]!.toLowerCase() : "a";
4754
const truncatedEmail = adminEmail && adminEmail.length > 22
@@ -55,7 +62,7 @@ export const Sidebar: React.FC = () => {
5562
<VaultbaseLogo size={20} />
5663
</span>
5764
<div className="sb-brand-name">vaultbase</div>
58-
<div className="sb-brand-version mono">v0.9.1</div>
65+
{version && <div className="sb-brand-version mono">v{version}</div>}
5966
</div>
6067
{SECTIONS.map((sec) => (
6168
<div className="sb-section" key={sec.label}>

admin/src/pages/Login.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { api, type ApiResponse } from "../api.ts";
44
import { useAuth } from "../stores/auth.ts";
55
import { VaultbaseLogo } from "../components/VaultbaseLogo.tsx";
66
import Icon from "../components/Icon.tsx";
7+
import { useVersion } from "../stores/version.ts";
78

89
export default function Login() {
910
const [email, setEmail] = useState("");
@@ -12,6 +13,7 @@ export default function Login() {
1213
const [loading, setLoading] = useState(false);
1314
const [error, setError] = useState("");
1415
const navigate = useNavigate();
16+
const version = useVersion();
1517

1618
async function handleSubmit(e: React.FormEvent) {
1719
e.preventDefault();
@@ -38,6 +40,11 @@ export default function Login() {
3840
<div className="auth-brand">
3941
<span className="sb-brand-mark"><VaultbaseLogo size={26} /></span>
4042
<div className="name">vaultbase</div>
43+
{version && (
44+
<span style={{ marginLeft: "auto", fontSize: 11, color: "var(--text-muted)", fontFamily: "var(--font-mono)" }}>
45+
v{version}
46+
</span>
47+
)}
4148
</div>
4249
<div>
4350
<h1 className="auth-title">Welcome back</h1>

admin/src/pages/Setup.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,26 @@ import { useState } from "react";
22
import { api, type ApiResponse } from "../api.ts";
33
import Icon from "../components/Icon.tsx";
44
import { VaultbaseLogo } from "../components/VaultbaseLogo.tsx";
5+
import { useVersion } from "../stores/version.ts";
56

67
type Step = "welcome" | "admin" | "done";
78

89
export default function Setup() {
910
const [step, setStep] = useState<Step>("welcome");
1011
const [adminEmail, setAdminEmail] = useState("");
12+
const version = useVersion();
1113

1214
return (
1315
<div className="auth-shell">
1416
<div className="auth-card wide" style={{ width: 520 }}>
1517
<div className="auth-brand">
1618
<span className="sb-brand-mark"><VaultbaseLogo size={26} /></span>
1719
<div className="name">vaultbase</div>
18-
<span style={{ marginLeft: "auto", fontSize: 11, color: "var(--text-muted)", fontFamily: "var(--font-mono)" }}>
19-
v0.1.0
20-
</span>
20+
{version && (
21+
<span style={{ marginLeft: "auto", fontSize: 11, color: "var(--text-muted)", fontFamily: "var(--font-mono)" }}>
22+
v{version}
23+
</span>
24+
)}
2125
</div>
2226

2327
<StepIndicator step={step} />

admin/src/stores/version.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { useEffect, useState } from "react";
2+
3+
let cached: string | null = null;
4+
let inflight: Promise<string> | null = null;
5+
6+
async function fetchVersion(): Promise<string> {
7+
if (cached) return cached;
8+
if (inflight) return inflight;
9+
inflight = fetch("/api/health", { headers: { accept: "application/json" } })
10+
.then(async (r) => {
11+
const json = await r.json().catch(() => ({}));
12+
const v = json?.data?.version;
13+
cached = typeof v === "string" && v.length > 0 ? v : "";
14+
return cached;
15+
})
16+
.catch(() => {
17+
cached = "";
18+
return cached;
19+
})
20+
.finally(() => { inflight = null; });
21+
return inflight;
22+
}
23+
24+
export function useVersion(): string {
25+
const [v, setV] = useState<string>(cached ?? "");
26+
useEffect(() => {
27+
if (cached) { if (v !== cached) setV(cached); return; }
28+
let alive = true;
29+
fetchVersion().then((value) => { if (alive) setV(value); });
30+
return () => { alive = false; };
31+
}, [v]);
32+
return v;
33+
}

src/server.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,10 @@ export function createServer(config: Config) {
225225
)
226226
.use(makeAdminPlugin())
227227
.use(makeAuthPagesPlugin())
228-
.get("/api/health", () => ({ data: { status: "ok" } }))
228+
.get("/api/health", async () => {
229+
const { VAULTBASE_VERSION } = await import("./core/version.ts");
230+
return { data: { status: "ok", version: VAULTBASE_VERSION } };
231+
})
229232
// Cluster health probe — admin proxies / load-balancers hit this. Worker
230233
// id (if running under cluster mode) helps debug which worker answered.
231234
.get("/_/health", () => ({

0 commit comments

Comments
 (0)