Skip to content

Commit 0e9ed75

Browse files
authored
Merge pull request #2016 from rarepops/feat/ui-server-version
feat(ui): show serving binary version (#1820)
2 parents c38aa35 + e0785b3 commit 0e9ed75

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
@@ -70,6 +70,10 @@
7070

7171
/* ── Constants ────────────────────────────────────────────────── */
7272

73+
#ifndef CBM_VERSION
74+
#define CBM_VERSION "dev"
75+
#endif
76+
7377
/* Max JSON-RPC request body size (1 MB) — transport enforces the same cap. */
7478
#define MAX_BODY_SIZE CBM_HTTP_MAX_BODY
7579

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

154160
/* ── 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)