feat: show live build version in web UI
Build & Push Docker Image / build (push) Successful in 2m36s

- /api/version returns version, commitSha, buildDate, trashRetentionDays
- CI passes COMMIT_SHA (full), BUILD_DATE and VERSION (git tag) as
  docker build args; Dockerfile bakes them as env
- sidebar footer shows version/sha linked to the Gitea commit
- admin System tab shows version, commit link, build date, trash retention
This commit is contained in:
opencode
2026-08-02 02:13:27 +02:00
parent 2f20b1e5c2
commit 803802a5b6
11 changed files with 247 additions and 3 deletions
+10
View File
@@ -8,4 +8,14 @@ router.get("/healthz", (_req, res) => {
res.json(data);
});
router.get("/version", (_req, res) => {
const retention = Number(process.env["TRASH_RETENTION_DAYS"] ?? "0");
res.json({
version: process.env["VERSION"] || "dev",
commitSha: process.env["COMMIT_SHA"] || null,
buildDate: process.env["BUILD_DATE"] || null,
trashRetentionDays: Number.isFinite(retention) ? retention : 0,
});
});
export default router;
@@ -1,6 +1,7 @@
import { Link, useLocation } from "wouter";
import { LayoutDashboard, Wrench, PlusCircle, BarChart3, LogIn, LogOut, User, ShieldCheck, AlertTriangle, Trash2 } from "lucide-react";
import { useAuth } from "@/hooks/use-auth";
import { useGetVersion, getGetVersionQueryKey } from "@workspace/api-client-react";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { ThemeToggle } from "@/components/theme-toggle";
@@ -8,6 +9,9 @@ import { ThemeToggle } from "@/components/theme-toggle";
export function Layout({ children }: { children: React.ReactNode }) {
const [location] = useLocation();
const { user, isLoading, isAuthenticated, isAdmin, isLocalMode, tier, hasFeature, login, logout } = useAuth();
const { data: version } = useGetVersion({
query: { queryKey: getGetVersionQueryKey(), staleTime: Infinity, retry: false },
});
const links = [
{ href: "/", label: "Dashboard", icon: LayoutDashboard },
@@ -95,6 +99,24 @@ export function Layout({ children }: { children: React.ReactNode }) {
</Button>
)}
</div>
<div className="px-4 py-2.5 border-t flex items-center justify-between text-[11px] text-muted-foreground">
<span className="font-medium">toolr</span>
{version?.commitSha ? (
<a
href={`https://git.kubebase.de/admin/tool-evaluator/commit/${version.commitSha}`}
target="_blank"
rel="noreferrer"
className="font-mono hover:underline"
title={version.commitSha}
>
{version.version && version.version !== "dev"
? version.version
: `sha-${version.commitSha.slice(0, 7)}`}
</a>
) : version?.version && version.version !== "dev" ? (
<span className="font-mono">{version.version}</span>
) : null}
</div>
</aside>
<main className="flex-1 flex flex-col min-w-0">
+56 -1
View File
@@ -6,8 +6,10 @@ import {
useUpdateUser,
useDeleteUser,
useListAuditLogs,
useGetVersion,
getListUsersQueryKey,
getListAuditLogsQueryKey,
getGetVersionQueryKey,
} from "@workspace/api-client-react";
import { useQueryClient } from "@tanstack/react-query";
import { useAuth } from "@/hooks/use-auth";
@@ -22,7 +24,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { Skeleton } from "@/components/ui/skeleton";
import { useToast } from "@/hooks/use-toast";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Users, ScrollText, ShieldAlert, Plus, Pencil, Trash2, Clock, AlertTriangle, Wrench } from "lucide-react";
import { Users, ScrollText, ShieldAlert, Plus, Pencil, Trash2, Clock, AlertTriangle, Wrench, Server } from "lucide-react";
import { format } from "date-fns";
import { AdminToolsTab } from "@/components/admin-tools-tab";
@@ -49,6 +51,9 @@ export default function Admin() {
{ limit: 100 },
{ query: { queryKey: getListAuditLogsQueryKey({ limit: 100 }), enabled: isAdmin } },
);
const { data: versionInfo } = useGetVersion({
query: { queryKey: getGetVersionQueryKey(), staleTime: Infinity, retry: false, enabled: isAdmin },
});
const createUser = useCreateUser();
const updateUser = useUpdateUser();
@@ -154,6 +159,9 @@ export default function Admin() {
<TabsTrigger value="audit" className="gap-2">
<ScrollText className="w-4 h-4" /> Audit Log
</TabsTrigger>
<TabsTrigger value="system" className="gap-2">
<Server className="w-4 h-4" /> System
</TabsTrigger>
</TabsList>
<TabsContent value="users">
@@ -274,6 +282,53 @@ export default function Admin() {
</CardContent>
</Card>
</TabsContent>
<TabsContent value="system">
<Card>
<CardHeader>
<CardTitle>System</CardTitle>
<CardDescription>Build information of the currently live deployment.</CardDescription>
</CardHeader>
<CardContent>
<div className="divide-y">
<div className="flex items-center justify-between py-3">
<span className="text-sm text-muted-foreground">Version</span>
<span className="text-sm font-medium">{versionInfo?.version || "dev"}</span>
</div>
<div className="flex items-center justify-between py-3">
<span className="text-sm text-muted-foreground">Commit</span>
{versionInfo?.commitSha ? (
<a
href={`https://git.kubebase.de/admin/tool-evaluator/commit/${versionInfo.commitSha}`}
target="_blank"
rel="noreferrer"
className="text-sm font-mono text-primary hover:underline"
title={versionInfo.commitSha}
>
{versionInfo.commitSha.slice(0, 7)}
</a>
) : (
<span className="text-sm text-muted-foreground"></span>
)}
</div>
<div className="flex items-center justify-between py-3">
<span className="text-sm text-muted-foreground">Build date</span>
<span className="text-sm">
{versionInfo?.buildDate ? format(new Date(versionInfo.buildDate), "dd.MM.yyyy HH:mm") : "—"}
</span>
</div>
<div className="flex items-center justify-between py-3">
<span className="text-sm text-muted-foreground">Trash retention</span>
<span className="text-sm">
{(versionInfo?.trashRetentionDays ?? 0) > 0
? `${versionInfo?.trashRetentionDays} days`
: "Keep forever"}
</span>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>