Merge PR #140: refactor split ports (npmSteven) with polish fixes

Adds split-port runtime support: API and dashboard can run on separate ports.
- PORT remains canonical/base
- API_PORT optionally overrides API listener
- DASHBOARD_PORT optionally overrides dashboard listener
- API bridge server proxies OpenAI-compatible routes with 30s timeout
- Centralized port resolution in src/lib/runtime/ports.ts
- Unit tests for runtime port resolution (14 tests)
- Healthcheck extracted into scripts/healthcheck.mjs
This commit is contained in:
diegosouzapw
2026-02-27 16:30:17 -03:00
24 changed files with 513 additions and 59 deletions

14
scripts/healthcheck.mjs Normal file
View File

@@ -0,0 +1,14 @@
#!/usr/bin/env node
/**
* Docker healthcheck script for OmniRoute.
* Checks the /api/settings endpoint on the dashboard port.
* Used by Dockerfile and docker-compose files.
*/
const port = process.env.DASHBOARD_PORT || process.env.PORT || "20128";
fetch(`http://127.0.0.1:${port}/api/settings`)
.then((r) => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
})
.catch(() => process.exit(1));

22
scripts/run-next.mjs Normal file
View File

@@ -0,0 +1,22 @@
#!/usr/bin/env node
import {
resolveRuntimePorts,
withRuntimePortEnv,
spawnWithForwardedSignals,
} from "./runtime-env.mjs";
const mode = process.argv[2] === "start" ? "start" : "dev";
const runtimePorts = resolveRuntimePorts();
const { dashboardPort } = runtimePorts;
const args = ["./node_modules/next/dist/bin/next", mode, "--port", String(dashboardPort)];
if (mode === "dev") {
args.splice(2, 0, "--webpack");
}
spawnWithForwardedSignals(process.execPath, args, {
stdio: "inherit",
env: withRuntimePortEnv(process.env, runtimePorts),
});

View File

@@ -0,0 +1,14 @@
#!/usr/bin/env node
import {
resolveRuntimePorts,
withRuntimePortEnv,
spawnWithForwardedSignals,
} from "./runtime-env.mjs";
const runtimePorts = resolveRuntimePorts();
spawnWithForwardedSignals("node", ["server.js"], {
stdio: "inherit",
env: withRuntimePortEnv(process.env, runtimePorts),
});

43
scripts/runtime-env.mjs Normal file
View File

@@ -0,0 +1,43 @@
import { spawn } from "node:child_process";
export function parsePort(value, fallback) {
const parsed = Number.parseInt(String(value), 10);
return Number.isFinite(parsed) && parsed > 0 && parsed <= 65535 ? parsed : fallback;
}
export function resolveRuntimePorts() {
const basePort = parsePort(process.env.PORT || "20128", 20128);
const apiPort = parsePort(process.env.API_PORT || String(basePort), basePort);
const dashboardPort = parsePort(process.env.DASHBOARD_PORT || String(basePort), basePort);
return { basePort, apiPort, dashboardPort };
}
export function withRuntimePortEnv(env, runtimePorts) {
const { basePort, apiPort, dashboardPort } = runtimePorts;
return {
...env,
OMNIROUTE_PORT: String(basePort),
PORT: String(dashboardPort),
DASHBOARD_PORT: String(dashboardPort),
API_PORT: String(apiPort),
};
}
export function spawnWithForwardedSignals(command, args, options = {}) {
const child = spawn(command, args, options);
child.on("exit", (code, signal) => {
if (signal) {
process.kill(process.pid, signal);
return;
}
process.exit(code ?? 0);
});
process.on("SIGINT", () => child.kill("SIGINT"));
process.on("SIGTERM", () => child.kill("SIGTERM"));
return child;
}