feat(ui,server): surface active instance service URLs in About settings (#2669)

Show the running instance's local server URL and tunnel URL (when a
tunnel is active) as labeled, click-to-open buttons on the About page.
/api/system/info now reports the instance port and tunnel URL, resolved
lazily from the tunnel runtime so each Git-worktree instance identifies
itself in the UI without parsing terminal output.

Refs OPE-194
This commit is contained in:
Serhii Dziupin
2026-08-07 00:25:57 +03:00
committed by GitHub
parent 668a6f54fe
commit 834d2edb87
17 changed files with 214 additions and 0 deletions
+4
View File
@@ -19,6 +19,8 @@ export const createBootstrapRuntime = (dependencies) => {
serverStartedAt,
gracefulShutdown,
getHealthSnapshot,
getServerPort,
getTunnelUrl,
verboseRequestLogs,
uiPassword,
tunnelAuthController,
@@ -81,6 +83,8 @@ export const createBootstrapRuntime = (dependencies) => {
gracefulShutdown,
getHealthSnapshot,
getServerId,
getServerPort,
getTunnelUrl,
tunnelAuthController,
uiAuthController,
});
@@ -67,6 +67,12 @@ export const registerServerStatusRoutes = (app, dependencies) => {
serverStartedAt,
gracefulShutdown,
getHealthSnapshot,
// Port this OpenChamber instance serves on and the tunnel public URL (if
// a tunnel is active). Exposed on /api/system/info so the UI can surface
// the active instance's service URLs. Optional: older wiring omits them
// and the endpoint reports null.
getServerPort = () => null,
getTunnelUrl = () => null,
// Stable server identity (hash of the public signing key — not a secret).
// Exposed on /health and /api/version so a client can verify that a
// learned/probed address belongs to the expected server BEFORE sending its
@@ -356,6 +362,8 @@ export const registerServerStatusRoutes = (app, dependencies) => {
runtime: runtimeName,
pid: process.pid,
startedAt: serverStartedAt,
port: getServerPort(),
tunnelUrl: getTunnelUrl(),
});
});
@@ -791,4 +791,46 @@ describe('client auth routes', () => {
socket: { remoteAddress: '203.0.113.10' },
})).toBe('unknown-public');
});
it('reports null port and tunnel URL on /api/system/info when no getters are wired', async () => {
const app = express();
registerServerStatusRoutes(app, {
process,
serverStartedAt: '2026-01-01T00:00:00.000Z',
gracefulShutdown: vi.fn(async () => {}),
getHealthSnapshot: () => ({ status: 'ok' }),
openchamberVersion: '1.0.0',
runtimeName: 'test',
express,
});
const response = await request(app).get('/api/system/info');
expect(response.status).toBe(200);
expect(response.body.openchamberVersion).toBe('1.0.0');
expect(response.body.runtime).toBe('test');
expect(response.body.pid).toBeTypeOf('number');
expect(response.body.startedAt).toBeTypeOf('string');
expect(response.body.port).toBeNull();
expect(response.body.tunnelUrl).toBeNull();
});
it('reports the instance port and tunnel URL on /api/system/info from the wired getters', async () => {
const app = express();
registerServerStatusRoutes(app, {
process,
serverStartedAt: '2026-01-01T00:00:00.000Z',
gracefulShutdown: vi.fn(async () => {}),
getHealthSnapshot: () => ({ status: 'ok' }),
openchamberVersion: '1.0.0',
runtimeName: 'test',
express,
getServerPort: () => 9988,
getTunnelUrl: () => 'https://worktree-a.example.trycloudflare.com',
});
const response = await request(app).get('/api/system/info');
expect(response.status).toBe(200);
expect(response.body.port).toBe(9988);
expect(response.body.tunnelUrl).toBe('https://worktree-a.example.trycloudflare.com');
});
});