/** * Embedded HTTP server for the OpenFinClaw dashboard. * Binds to 127.0.0.1 only (loopback) for local access. * * The server instance is stored on globalThis so that a plugin hot-reload * (register() called a second time after module cache is cleared) can close * the previous server before binding a new one — avoiding EADDRINUSE. */ import type { Server } from "node:http"; import { createServer } from "node:http"; import { getDb } from "../db/db.js"; import { handleRoute } from "./routes.js"; export interface DashboardLogger { info(msg: string): void; } /** globalThis key — survives module hot-reloads within the same process. */ const GLOBAL_SERVER_KEY = "__openfinclaw_http_server__" as const; /** * Start the dashboard HTTP server. * If a previous server instance exists (from a prior register() call), * it is closed first so the port is released. */ export function startHttpServer(port: number, logger: DashboardLogger): void { const server = createServer((req, res) => { try { handleRoute(getDb(), req, res); } catch (err) { if (!res.headersSent) { res.writeHead(500, { "Content-Type": "text/plain" }); } res.end(`Internal Server Error: ${String(err)}`); } }); server.on("error", (err) => { logger.info(`[OpenFinClaw] Dashboard server error: ${String(err)}`); }); /** Bind server after ensuring the port is free. */ function listen() { (globalThis as Record)[GLOBAL_SERVER_KEY] = server; server.listen(port, "127.0.0.1", () => { logger.info(`[OpenFinClaw] Dashboard available at http://127.0.0.1:${port}`); }); } // Close previous server left over from a hot-reload, then bind const prev = (globalThis as Record)[GLOBAL_SERVER_KEY] as Server | undefined; if (prev) { logger.info("[OpenFinClaw] Closing previous Dashboard server (hot-reload)"); prev.close(listen); } else { listen(); } }