From 16c6f8fb04a6cfafb1532990e2a76ee237288322 Mon Sep 17 00:00:00 2001 From: Gabriel Costa Date: Fri, 14 Aug 2026 21:35:31 +0100 Subject: [PATCH 1/2] feat: Add app and API versions Signed-off-by: Gabriel Costa --- src/components/layout/Header.test.tsx | 26 +++++++++++++------ src/components/layout/Header.tsx | 32 +++++++++++++++++++----- src/components/ui/hover-card.tsx | 36 +++++++++++++++++++++++++++ src/vite-env.d.ts | 5 ++++ vite.config.ts | 9 +++++++ vitest.config.ts | 7 ++++++ 6 files changed, 102 insertions(+), 13 deletions(-) create mode 100644 src/components/ui/hover-card.tsx diff --git a/src/components/layout/Header.test.tsx b/src/components/layout/Header.test.tsx index adfac03..a1ebb3b 100644 --- a/src/components/layout/Header.test.tsx +++ b/src/components/layout/Header.test.tsx @@ -1,4 +1,5 @@ import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { describe, expect, it, vi } from "vitest"; import { Header } from "./Header"; import { useQuery } from "../../hooks/useQuery"; @@ -26,19 +27,17 @@ describe("Header", () => { ); }; - it("renders without version when useQuery returns no data", () => { + it("always shows the UI's own package version", () => { vi.mocked(useQuery).mockReturnValue({ data: null } as unknown as ReturnType); renderHeader(); expect(screen.getByTestId("quick-nav")).toBeInTheDocument(); expect(screen.getByTestId("profile-menu")).toBeInTheDocument(); - - // Check that there is no version string visible (like v1.2.3) - const versionMatch = screen.queryByText(/v\d+\.\d+\.\d+/); - expect(versionMatch).not.toBeInTheDocument(); + expect(screen.getByText(`v${__APP_VERSION__}`)).toBeInTheDocument(); }); - it("renders with version when useQuery returns version data", () => { + it("shows supported and live API versions in the hover popover", async () => { + const user = userEvent.setup(); vi.mocked(useQuery).mockReturnValue({ data: { app: { @@ -48,6 +47,19 @@ describe("Header", () => { } as unknown as ReturnType); renderHeader(); - expect(screen.getByText("v1.0.0")).toBeInTheDocument(); + await user.hover(screen.getByText(`v${__APP_VERSION__}`)); + + expect(await screen.findByText(`v${__SUPPORTED_API_VERSION__}`)).toBeInTheDocument(); + expect(await screen.findByText("v1.0.0")).toBeInTheDocument(); + }); + + it("shows the live API version as unavailable when useQuery returns no data", async () => { + const user = userEvent.setup(); + vi.mocked(useQuery).mockReturnValue({ data: null } as unknown as ReturnType); + renderHeader(); + + await user.hover(screen.getByText(`v${__APP_VERSION__}`)); + + expect(await screen.findByText("unavailable")).toBeInTheDocument(); }); }); diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index aaa9a4b..6d2b053 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -2,6 +2,8 @@ import { BookOpen } from "lucide-react"; import { useQuery } from "../../hooks/useQuery"; import { GitHubIcon } from "../icons/GitHubIcon"; import { SidebarTrigger } from "../ui/sidebar"; +import { HoverCard, HoverCardContent, HoverCardTrigger } from "../ui/hover-card"; +import { Typography } from "../ui/typography"; import { HeaderProfileMenu } from "./HeaderProfileMenu"; import { HeaderQuickNav } from "./HeaderQuickNav"; @@ -16,18 +18,36 @@ interface VersionResponse { export function Header() { const { data: versionData } = useQuery("/version?partial=false"); - const appVersion = versionData?.app?.version; + const liveApiVersion = versionData?.app?.version; return (
- {appVersion ? ( - - v{appVersion} - - ) : null} + + + + v{__APP_VERSION__} + + + +
+ + ContextForge supported API:{" "} + + v{__SUPPORTED_API_VERSION__} + + + + Live API version:{" "} + + {liveApiVersion ? `v${liveApiVersion}` : "unavailable"} + + +
+
+
) { + return ; +} + +function HoverCardTrigger({ ...props }: React.ComponentProps) { + return ; +} + +function HoverCardContent({ + className, + align = "center", + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +export { HoverCard, HoverCardTrigger, HoverCardContent }; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 11f02fe..ffe7f8e 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1 +1,6 @@ /// + +// Injected by vite.config.ts `define` from package.json's version field. +declare const __APP_VERSION__: string; +// Injected by vite.config.ts `define` from openapi.json's info.version field. +declare const __SUPPORTED_API_VERSION__: string; diff --git a/vite.config.ts b/vite.config.ts index 3834e66..0be7323 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,11 +2,20 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; import path from "path"; +import pkg from "./package.json"; +import openapiSpec from "./openapi.json"; // https://vite.dev/config/ export default defineConfig({ plugins: [react(), tailwindcss()], + // Exposes the UI's own package version and the ContextForge API version + // it was generated against to the client bundle (see Header.tsx). + define: { + __APP_VERSION__: JSON.stringify(pkg.version), + __SUPPORTED_API_VERSION__: JSON.stringify(openapiSpec.info.version), + }, + css: { postcss: { plugins: [], diff --git a/vitest.config.ts b/vitest.config.ts index 6e0e37a..2a40677 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -4,9 +4,16 @@ import { defineConfig } from "vitest/config"; import react from "@vitejs/plugin-react"; import path from "path"; +import pkg from "./package.json"; +import openapiSpec from "./openapi.json"; export default defineConfig({ plugins: [react()] as any, + // Mirrors vite.config.ts — exposes the same version constants to code under test. + define: { + __APP_VERSION__: JSON.stringify(pkg.version), + __SUPPORTED_API_VERSION__: JSON.stringify(openapiSpec.info.version), + }, css: { postcss: { plugins: [], From 362c69e31796dc7ac190d4ec62a21455d6c25a66 Mon Sep 17 00:00:00 2001 From: Gabriel Costa Date: Fri, 14 Aug 2026 22:32:17 +0100 Subject: [PATCH 2/2] Address comments Signed-off-by: Gabriel Costa --- build-constants.ts | 10 ++++++++++ src/components/layout/Header.tsx | 5 ++++- vite.config.ts | 10 ++-------- vitest.config.ts | 10 +++------- 4 files changed, 19 insertions(+), 16 deletions(-) create mode 100644 build-constants.ts diff --git a/build-constants.ts b/build-constants.ts new file mode 100644 index 0000000..fbf14ae --- /dev/null +++ b/build-constants.ts @@ -0,0 +1,10 @@ +import pkg from "./package.json"; +import openapiSpec from "./openapi.json"; + +// Shared `define` block for vite.config.ts and vitest.config.ts — exposes the +// UI's own package version and the ContextForge API version it was generated +// against to the client bundle (see Header.tsx). +export const versionDefines = { + __APP_VERSION__: JSON.stringify(pkg.version), + __SUPPORTED_API_VERSION__: JSON.stringify(openapiSpec.info.version), +}; diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 6d2b053..a68aab2 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -27,7 +27,10 @@ export function Header() { - + v{__APP_VERSION__} diff --git a/vite.config.ts b/vite.config.ts index 0be7323..f18f1ff 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,19 +2,13 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; import path from "path"; -import pkg from "./package.json"; -import openapiSpec from "./openapi.json"; +import { versionDefines } from "./build-constants"; // https://vite.dev/config/ export default defineConfig({ plugins: [react(), tailwindcss()], - // Exposes the UI's own package version and the ContextForge API version - // it was generated against to the client bundle (see Header.tsx). - define: { - __APP_VERSION__: JSON.stringify(pkg.version), - __SUPPORTED_API_VERSION__: JSON.stringify(openapiSpec.info.version), - }, + define: versionDefines, css: { postcss: { diff --git a/vitest.config.ts b/vitest.config.ts index 2a40677..a80cd3b 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -4,16 +4,12 @@ import { defineConfig } from "vitest/config"; import react from "@vitejs/plugin-react"; import path from "path"; -import pkg from "./package.json"; -import openapiSpec from "./openapi.json"; +import { versionDefines } from "./build-constants"; export default defineConfig({ plugins: [react()] as any, - // Mirrors vite.config.ts — exposes the same version constants to code under test. - define: { - __APP_VERSION__: JSON.stringify(pkg.version), - __SUPPORTED_API_VERSION__: JSON.stringify(openapiSpec.info.version), - }, + // Mirrors vite.config.ts's version constants for code under test. + define: versionDefines, css: { postcss: { plugins: [],