Skip to content

Commit e0785b3

Browse files
committed
feat(ui): show serving binary version
Expose the compiled server version through the existing UI config endpoint and render it beside the product name. Keep older or unavailable config responses compatible by hiding the label, with focused frontend and native endpoint coverage. Fixes DeusData#1820 Signed-off-by: Rares Popa <2606875+rarepops@users.noreply.github.com>
1 parent 4d8ac9e commit e0785b3

4 files changed

Lines changed: 112 additions & 2 deletions

File tree

‎graph-ui/src/App.test.tsx‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/* @vitest-environment jsdom */
2+
import "@testing-library/jest-dom/vitest";
3+
import { cleanup, render, screen, waitFor } from "@testing-library/react";
4+
import { afterEach, describe, expect, it, vi } from "vitest";
5+
import { App } from "./App";
6+
import { messages } from "./lib/i18n";
7+
8+
vi.mock("./components/GraphTab", () => ({ GraphTab: () => null }));
9+
vi.mock("./components/StatsTab", () => ({ StatsTab: () => null }));
10+
vi.mock("./components/ControlTab", () => ({ ControlTab: () => null }));
11+
vi.mock("./lib/i18n", async (importOriginal) => {
12+
const actual = await importOriginal<typeof import("./lib/i18n")>();
13+
return { ...actual, useUiMessages: () => messages.en };
14+
});
15+
16+
describe("App", () => {
17+
afterEach(() => {
18+
cleanup();
19+
vi.unstubAllGlobals();
20+
window.history.replaceState(null, "", "/");
21+
});
22+
23+
it("shows the serving binary version", async () => {
24+
vi.stubGlobal("fetch", vi.fn(async () =>
25+
new Response(JSON.stringify({ lang: "en", version: "0.10.8" }), {
26+
status: 200,
27+
headers: { "Content-Type": "application/json" },
28+
}),
29+
));
30+
31+
render(<App />);
32+
33+
expect(await screen.findByText("v0.10.8")).toBeVisible();
34+
});
35+
36+
it("hides the version when the config has no string version", async () => {
37+
const fetchMock = vi.fn(async () =>
38+
new Response(JSON.stringify({ lang: "en", version: 108 }), { status: 200 }),
39+
);
40+
vi.stubGlobal("fetch", fetchMock);
41+
42+
render(<App />);
43+
44+
await waitFor(() => expect(fetchMock).toHaveBeenCalledWith("/api/ui-config"));
45+
expect(screen.queryByTitle("Server version")).not.toBeInTheDocument();
46+
});
47+
48+
it("hides the version when the config request fails", async () => {
49+
const fetchMock = vi.fn(async () => {
50+
throw new Error("offline");
51+
});
52+
vi.stubGlobal("fetch", fetchMock);
53+
54+
render(<App />);
55+
56+
await waitFor(() => expect(fetchMock).toHaveBeenCalledWith("/api/ui-config"));
57+
expect(screen.queryByTitle("Server version")).not.toBeInTheDocument();
58+
});
59+
});

‎graph-ui/src/App.tsx‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,24 @@ function routeUrl(tab: TabId, project: string | null): string {
3333
export function App() {
3434
const t = useUiMessages();
3535
const [route, setRoute] = useState<RouteState>(readRoute);
36+
const [version, setVersion] = useState<string | null>(null);
3637
const { tab: activeTab, project: selectedProject } = route;
3738

39+
useEffect(() => {
40+
let cancelled = false;
41+
void fetch("/api/ui-config")
42+
.then((response) => (response.ok ? response.json() : null))
43+
.then((config) => {
44+
if (!cancelled && typeof config?.version === "string" && config.version) {
45+
setVersion(config.version);
46+
}
47+
})
48+
.catch(() => {});
49+
return () => {
50+
cancelled = true;
51+
};
52+
}, []);
53+
3854
/* Normalize the URL on first load so it always carries the current route. */
3955
useEffect(() => {
4056
const initial = readRoute();
@@ -73,6 +89,14 @@ export function App() {
7389
<span className="text-[13px] font-semibold text-foreground/90 tracking-tight">
7490
Codebase Memory
7591
</span>
92+
{version && (
93+
<span
94+
className="translate-y-px text-[10px] font-mono text-foreground/30"
95+
title="Server version"
96+
>
97+
{version.startsWith("v") ? version : `v${version}`}
98+
</span>
99+
)}
76100
</div>
77101

78102
{/* Tabs inline in header */}

‎src/ui/http_server.c‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@
6666

6767
/* ── Constants ────────────────────────────────────────────────── */
6868

69+
#ifndef CBM_VERSION
70+
#define CBM_VERSION "dev"
71+
#endif
72+
6973
/* Max JSON-RPC request body size (1 MB) — transport enforces the same cap. */
7074
#define MAX_BODY_SIZE CBM_HTTP_MAX_BODY
7175

@@ -143,8 +147,10 @@ static void handle_ui_config(cbm_http_conn_t *c, const cbm_http_req_t *req) {
143147
* audit forbids hardcoded external URLs in graph-ui source (external
144148
* targets must come from an auditable backend response, same pattern as
145149
* the /api/repo-info deep-links). */
146-
cbm_http_replyf(c, 200, g_cors_json, "{\"lang\":\"%s\",\"upstream_issues_url\":\"%s\"}",
147-
lang_buf, "https://github.com/DeusData/codebase-memory-mcp/issues/new");
150+
cbm_http_replyf(c, 200, g_cors_json,
151+
"{\"lang\":\"%s\",\"version\":\"%s\",\"upstream_issues_url\":\"%s\"}",
152+
lang_buf, CBM_VERSION,
153+
"https://github.com/DeusData/codebase-memory-mcp/issues/new");
148154
}
149155

150156
/* ── Server state ─────────────────────────────────────────────── */

‎tests/test_httpd.c‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
#include <stdatomic.h>
3434
#include <stdlib.h>
3535
#include <string.h>
36+
#ifndef CBM_VERSION
37+
#define CBM_VERSION "dev"
38+
#endif
3639
#ifndef _WIN32
3740
#include <sys/stat.h>
3841
#endif
@@ -1573,6 +1576,23 @@ TEST(ui_server_ui_config_detects_zh_accept_language) {
15731576
PASS();
15741577
}
15751578

1579+
TEST(ui_server_ui_config_includes_serving_version_issue1820) {
1580+
th_server_t ts;
1581+
ASSERT_EQ(th_server_start(&ts), 0);
1582+
1583+
char resp[4096];
1584+
int n = th_http(cbm_http_server_port(ts.srv), "GET /api/ui-config HTTP/1.1\r\n\r\n", resp,
1585+
sizeof(resp));
1586+
ASSERT_TRUE(n > 0);
1587+
ASSERT_EQ(th_status(resp), 200);
1588+
char expected_version[128];
1589+
snprintf(expected_version, sizeof(expected_version), "\"version\":\"%s\"", CBM_VERSION);
1590+
ASSERT_NOT_NULL(strstr(resp, expected_version));
1591+
1592+
th_server_stop(&ts);
1593+
PASS();
1594+
}
1595+
15761596
TEST(ui_server_ui_config_prefers_config_lang) {
15771597
char tmpdir[256];
15781598
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_httpd_cfg_XXXXXX");
@@ -2397,6 +2417,7 @@ SUITE(httpd) {
23972417
RUN_TEST(ui_server_delete_project_invalid_name_keeps_watch);
23982418
RUN_TEST(ui_server_delete_project_unlink_failure_keeps_watch);
23992419
RUN_TEST(ui_server_ui_config_detects_zh_accept_language);
2420+
RUN_TEST(ui_server_ui_config_includes_serving_version_issue1820);
24002421
RUN_TEST(ui_server_ui_config_prefers_config_lang);
24012422
RUN_TEST(ui_server_slow_request_hits_deadline);
24022423
RUN_TEST(ui_server_access_log_redacts_query);

0 commit comments

Comments
 (0)