diff --git a/apps/daemon/sidecar/server.ts b/apps/daemon/sidecar/server.ts index 977a9fe13..36067a677 100644 --- a/apps/daemon/sidecar/server.ts +++ b/apps/daemon/sidecar/server.ts @@ -62,23 +62,19 @@ function attachParentMonitor(stop: () => Promise): void { } export async function startDaemonSidecar(runtime: SidecarRuntimeContext): Promise { - const started = await startServer({ port: parsePort(process.env[DAEMON_PORT_ENV]), returnServer: true }) as unknown as - | Server - | undefined; - if (started == null) { + const started = await startServer({ port: parsePort(process.env[DAEMON_PORT_ENV]), returnServer: true }) as + | string + | { server: Server; url: string }; + if (typeof started === "string") { throw new Error("daemon startServer did not return a server handle"); } - const server = started; - const address = server.address(); - if (address == null || typeof address === "string") { - throw new Error("daemon startServer did not bind to a TCP port"); - } + const serverHandle = started; const state: DaemonStatusSnapshot = { pid: process.pid, state: "running", updatedAt: new Date().toISOString(), - url: `http://127.0.0.1:${address.port}`, + url: serverHandle.url, }; let ipcServer: JsonIpcServerHandle | null = null; let stopped = false; @@ -93,7 +89,7 @@ export async function startDaemonSidecar(runtime: SidecarRuntimeContext undefined); - await closeHttpServer(server).catch(() => undefined); + await closeHttpServer(serverHandle.server).catch(() => undefined); resolveStopped(); } diff --git a/apps/daemon/src/server.ts b/apps/daemon/src/server.ts index 4b7a5f196..c90d62b5d 100644 --- a/apps/daemon/src/server.ts +++ b/apps/daemon/src/server.ts @@ -2277,16 +2277,38 @@ export async function startServer({ port = 7456, returnServer = false } = {}) { } }); - const server = app.listen(port, () => { - resolvedPort = server.address().port; - if (!returnServer) { - console.log(`[od] daemon listening on http://127.0.0.1:${resolvedPort}`); - } + // Wait for `listen` to bind so callers always see the resolved URL — + // critical when port=0 (ephemeral port) and when the embedding sidecar + // needs to advertise the port to a parent process before any request + // can flow. Three callers depend on this contract: + // - `apps/daemon/src/cli.ts` → expects a `url` string + // - `apps/daemon/sidecar/server.ts` → expects `{ url, server }` + // - `apps/daemon/tests/version-route.test.ts` → expects `{ url, server }` + return await new Promise((resolve, reject) => { + const server = app.listen(port, () => { + const address = server.address(); + // `address()` can in theory return `string | AddressInfo | null`. For + // a TCP listener it's always `AddressInfo` with a `.port` — the guard + // is belt-and-braces so an unexpected null never silently produces a + // `http://127.0.0.1:0` URL that callers would then try to fetch. + const boundPort = address && typeof address === 'object' ? address.port : null; + if (!boundPort) { + reject(new Error(`[od] daemon failed to resolve listening port (address=${JSON.stringify(address)})`)); + return; + } + resolvedPort = boundPort; + const url = `http://127.0.0.1:${resolvedPort}`; + if (!returnServer) { + console.log(`[od] daemon listening on ${url}`); + } + resolve(returnServer ? { url, server } : url); + }); + // `app.listen` throws synchronously when the port is already in use on + // some Node versions, but emits an `error` event on others (and for + // EACCES / EADDRNOTAVAIL even on the same Node). Wire the event so the + // returned Promise always settles instead of hanging forever. + server.on('error', reject); }); - - if (returnServer) { - return server; - } } function randomId() { diff --git a/apps/daemon/tests/version-route.test.ts b/apps/daemon/tests/version-route.test.ts index 0f469b1c5..ecae6775f 100644 --- a/apps/daemon/tests/version-route.test.ts +++ b/apps/daemon/tests/version-route.test.ts @@ -7,16 +7,12 @@ describe('/api/version', () => { let baseUrl: string; beforeAll(async () => { - const started = await startServer({ port: 0, returnServer: true }) as http.Server | undefined; - if (started == null) { - throw new Error('startServer did not return a server handle'); - } - const address = started.address(); - if (address == null || typeof address === 'string') { - throw new Error('startServer did not bind to a TCP port'); - } - server = started; - baseUrl = `http://127.0.0.1:${address.port}`; + const started = await startServer({ port: 0, returnServer: true }) as { + url: string; + server: http.Server; + }; + baseUrl = started.url; + server = started.server; }); afterAll(() => new Promise((resolve) => server.close(() => resolve()))); diff --git a/apps/web/src/i18n/locales/hu.ts b/apps/web/src/i18n/locales/hu.ts index 11916bb6e..94cf0ed94 100644 --- a/apps/web/src/i18n/locales/hu.ts +++ b/apps/web/src/i18n/locales/hu.ts @@ -334,6 +334,18 @@ export const hu: Dict = { 'chat.tabChat': 'Csevegés', 'chat.tabComments': 'Megjegyzések', 'chat.commentsSoon': 'Megjegyzések — hamarosan', + 'chat.comments.attached': 'Attached to chat', + 'chat.comments.emptyAttached': 'No comments attached.', + 'chat.comments.saved': 'Saved comments', + 'chat.comments.emptySaved': 'No saved comments.', + 'chat.comments.add': 'Add', + 'chat.comments.addAll': 'Add all', + 'chat.comments.remove': 'Remove', + 'chat.comments.placeholder': 'Comment on this element…', + 'chat.comments.addSend': 'Add & send', + 'chat.comments.updateSend': 'Update & send', + 'chat.comments.removeAttachment': 'Remove comment attachment', + 'chat.comments.removeAttachmentAria': 'Remove comment attachment for {name}', 'chat.conversationsTitle': 'Beszélgetések', 'chat.conversationsAria': 'Beszélgetések előzménye', 'chat.newConversation': 'Új beszélgetés',