feat(ecosystem): update PM2 configuration for control plane and web server
Deploy to Raspberry Pi / deploy (push) Successful in 52s
Deploy to Raspberry Pi / deploy (push) Successful in 52s
- Renamed the control process to `jflow-control` and updated the script path to `control.js`. - Introduced a new `jflow-web` process for the production UI, serving on port 8500. - Enhanced environment variable handling for both processes, ensuring proper port assignments. - Updated README to reflect new process architecture and usage instructions for starting the application.
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
const HOP_BY_HOP = new Set([
|
||||
"connection",
|
||||
"keep-alive",
|
||||
"proxy-authenticate",
|
||||
"proxy-authorization",
|
||||
"te",
|
||||
"trailer",
|
||||
"transfer-encoding",
|
||||
"upgrade",
|
||||
"host",
|
||||
"content-length",
|
||||
]);
|
||||
|
||||
const REPLY_SKIP = new Set([
|
||||
"connection",
|
||||
"keep-alive",
|
||||
"transfer-encoding",
|
||||
"content-encoding",
|
||||
"content-length",
|
||||
]);
|
||||
|
||||
/**
|
||||
* Control origin used when the UI or HTTP process proxies to control.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function controlOrigin() {
|
||||
const explicit = process.env.JFLOW_CONTROL_URL?.trim();
|
||||
if (explicit) return explicit.replace(/\/$/, "");
|
||||
const port = Number(process.env.JFLOW_CONTROL_PORT ?? 8600);
|
||||
return `http://127.0.0.1:${port}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP API origin (workflow triggers + REST).
|
||||
* @returns {string}
|
||||
*/
|
||||
export function httpOrigin() {
|
||||
const explicit = process.env.JFLOW_HTTP_URL?.trim();
|
||||
if (explicit) return explicit.replace(/\/$/, "");
|
||||
const port = Number(process.env.JFLOW_HTTP_PORT ?? process.env.PORT ?? 8700);
|
||||
return `http://127.0.0.1:${port}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forward the incoming request to `origin`, preserving path + query.
|
||||
* @param {import("fastify").FastifyRequest} req
|
||||
* @param {import("fastify").FastifyReply} reply
|
||||
* @param {string} origin
|
||||
* @param {{ unreachableMessage?: string }} [opts]
|
||||
*/
|
||||
export async function proxyToOrigin(req, reply, origin, opts = {}) {
|
||||
const target = `${origin.replace(/\/$/, "")}${req.raw.url ?? "/"}`;
|
||||
const headers = {};
|
||||
for (const [key, value] of Object.entries(req.headers)) {
|
||||
if (value == null || HOP_BY_HOP.has(key.toLowerCase())) continue;
|
||||
headers[key] = Array.isArray(value) ? value.join(", ") : String(value);
|
||||
}
|
||||
|
||||
const method = req.method.toUpperCase();
|
||||
const hasBody = method !== "GET" && method !== "HEAD";
|
||||
let body;
|
||||
if (hasBody) {
|
||||
if (Buffer.isBuffer(req.body)) body = req.body;
|
||||
else if (typeof req.body === "string") body = req.body;
|
||||
else if (req.body != null) {
|
||||
body = JSON.stringify(req.body);
|
||||
if (!headers["content-type"]) headers["content-type"] = "application/json";
|
||||
}
|
||||
}
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = await fetch(target, { method, headers, body });
|
||||
} catch (err) {
|
||||
const message = opts.unreachableMessage ?? "upstream unreachable";
|
||||
req.log.warn({ err, target }, `proxy: ${message}`);
|
||||
return reply.code(502).send({ error: message });
|
||||
}
|
||||
|
||||
reply.code(res.status);
|
||||
res.headers.forEach((value, key) => {
|
||||
if (REPLY_SKIP.has(key.toLowerCase())) return;
|
||||
reply.header(key, value);
|
||||
});
|
||||
return reply.send(Buffer.from(await res.arrayBuffer()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Forward `/ops/*` to the control plane (same-origin UI in production).
|
||||
* @param {import("fastify").FastifyRequest} req
|
||||
* @param {import("fastify").FastifyReply} reply
|
||||
*/
|
||||
export async function proxyOpsToControl(req, reply) {
|
||||
return proxyToOrigin(req, reply, controlOrigin(), {
|
||||
unreachableMessage: "control plane unreachable",
|
||||
});
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
"start:api": "node server.js",
|
||||
"start:worker": "node worker.js",
|
||||
"start:control": "node control.js",
|
||||
"start:web": "node web-server.js",
|
||||
"migrate": "node -e \"import('./db.js').then((m) => m.migrate().then(() => process.exit(0)))\"",
|
||||
"test:plugins": "JFLOW_PLUGINS_DIR=./data/plugins-smoke-test JFLOW_DB_PATH=./data/plugins-smoke.db node test/plugins-smoke.js",
|
||||
"test:workflow-history": "JFLOW_WORKFLOWS_DIR=./data/workflow-history-smoke JFLOW_DB_PATH=./data/workflow-history-smoke.db node test/workflow-history-smoke.js",
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Production UI server (:8500).
|
||||
* Serves packages/web/dist and proxies /api, /ops, /admin, /u like Vite in dev.
|
||||
* Always-on — survives Ops stop of jflow-http.
|
||||
*/
|
||||
import fs from "fs";
|
||||
import fastify from "fastify";
|
||||
import fastifyStatic from "@fastify/static";
|
||||
import { log } from "./logger.js";
|
||||
import { WEB_DIST } from "./paths.js";
|
||||
import {
|
||||
controlOrigin,
|
||||
httpOrigin,
|
||||
proxyToOrigin,
|
||||
} from "./ops-proxy.js";
|
||||
|
||||
if (!fs.existsSync(WEB_DIST)) {
|
||||
log.error(
|
||||
{ WEB_DIST },
|
||||
"web dist missing — run `pnpm build` before starting the UI server",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const port = Number(process.env.JFLOW_UI_PORT ?? 8500);
|
||||
const control = controlOrigin();
|
||||
const http = httpOrigin();
|
||||
|
||||
const server = fastify({ loggerInstance: log });
|
||||
|
||||
/**
|
||||
* @param {import("fastify").FastifyRequest} req
|
||||
* @param {import("fastify").FastifyReply} reply
|
||||
*/
|
||||
async function proxyApi(req, reply) {
|
||||
const url = req.raw.url ?? "";
|
||||
// Match Vite: /api/auth → control (login works when HTTP is stopped).
|
||||
if (url === "/api/auth" || url.startsWith("/api/auth/") || url.startsWith("/api/auth?")) {
|
||||
return proxyToOrigin(req, reply, control, {
|
||||
unreachableMessage: "control plane unreachable",
|
||||
});
|
||||
}
|
||||
return proxyToOrigin(req, reply, http, {
|
||||
unreachableMessage: "HTTP API unreachable",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("fastify").FastifyRequest} req
|
||||
* @param {import("fastify").FastifyReply} reply
|
||||
*/
|
||||
async function proxyOps(req, reply) {
|
||||
return proxyToOrigin(req, reply, control, {
|
||||
unreachableMessage: "control plane unreachable",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("fastify").FastifyRequest} req
|
||||
* @param {import("fastify").FastifyReply} reply
|
||||
*/
|
||||
async function proxyHttp(req, reply) {
|
||||
return proxyToOrigin(req, reply, http, {
|
||||
unreachableMessage: "HTTP API unreachable",
|
||||
});
|
||||
}
|
||||
|
||||
server.all("/api", proxyApi);
|
||||
server.all("/api/*", proxyApi);
|
||||
server.all("/ops", proxyOps);
|
||||
server.all("/ops/*", proxyOps);
|
||||
server.all("/admin", proxyHttp);
|
||||
server.all("/admin/*", proxyHttp);
|
||||
server.all("/u", proxyHttp);
|
||||
server.all("/u/*", proxyHttp);
|
||||
|
||||
await server.register(fastifyStatic, {
|
||||
root: WEB_DIST,
|
||||
wildcard: false,
|
||||
});
|
||||
|
||||
server.setNotFoundHandler((req, reply) => {
|
||||
const url = req.raw.url ?? "";
|
||||
if (
|
||||
url.startsWith("/api") ||
|
||||
url.startsWith("/u/") ||
|
||||
url.startsWith("/admin") ||
|
||||
url.startsWith("/ops")
|
||||
) {
|
||||
return reply.code(404).send({ error: "not found" });
|
||||
}
|
||||
return reply.sendFile("index.html");
|
||||
});
|
||||
|
||||
async function shutdown() {
|
||||
try {
|
||||
await server.close();
|
||||
} catch (err) {
|
||||
log.error({ err }, "web-server shutdown error");
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
process.on("SIGINT", shutdown);
|
||||
process.on("SIGTERM", shutdown);
|
||||
|
||||
try {
|
||||
await server.listen({ host: "0.0.0.0", port });
|
||||
log.info(
|
||||
{ port, WEB_DIST, control, http },
|
||||
"UI server listening (static + proxy)",
|
||||
);
|
||||
} catch (err) {
|
||||
log.error({ err }, "failed to start UI server");
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user