CI/CD + deploy: Gitea Actions -> registry -> Dockge -> NPM (rcai.brasse-pc.eu)
Some checks failed
build-and-push / build (push) Has been cancelled
Some checks failed
build-and-push / build (push) Has been cancelled
Server:
- serverar byggd web-frontend statiskt + SPA-fallback (WEB_DIST)
- host-mountad persistent katalog via DATA_DIR (/srv/docker/agent-helm -> /app/data),
förberedd för framtida db/auth enligt övriga appars konvention
- esbuild-bundle -> dist/server.cjs (en fil, inga node_modules i runtime)
Web:
- WS-URL defaultar till samma origin i prod (wss://rcai.brasse-pc.eu), localhost i dev
Deploy-artefakter:
- Dockerfile: multi-stage, bygger web + bundlar server; --filter utesluter daemon
(ingen node-pty-kompilering); kör som uid 1000 (= brasse på host)
- .gitea/workflows/build.yaml: push master -> bygg arm64 -> push localhost:5000
- deploy/act-runner.{compose,config}.yaml: Gitea Actions-runner i Docker (docker.sock,
job-containrar får värdens docker for build/push)
- deploy/agent-helm.compose.yaml: Dockge-stack (srv_default, expose 8787, data-mount,
x-dockge.urls)
- deploy/DEPLOY.md: full runbook (runner, bygge, stack, NPM, uppdatering, felsökning)
Verifierat lokalt: typecheck rent, server-bundle serverar UI+health+assets+SPA-fallback,
data-dir skapas. Docker-bygget validerat så långt daemonen är nere (filter bekräftat).
This commit is contained in:
@@ -10,6 +10,9 @@
|
||||
* - Routing: web:input -> rätt daemons pty. daemon:output -> alla prenumeranter.
|
||||
*/
|
||||
import http from "node:http";
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { WebSocketServer, WebSocket } from "ws";
|
||||
// @xterm/headless och addon-serialize är CommonJS — default-importera värdet och
|
||||
// destrukturera; ta typen via `import type` (raderas i runtime).
|
||||
@@ -37,6 +40,11 @@ if (!TOKEN) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Persistent katalog (host-mountad på Pi5: /srv/docker/agent-helm -> /app/data).
|
||||
// Här hamnar framtida db/auth-state. Skapas vid start om den är satt.
|
||||
const DATA_DIR = process.env.DATA_DIR ?? "";
|
||||
if (DATA_DIR) fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
|
||||
interface Session {
|
||||
meta: SessionMeta;
|
||||
daemon: WebSocket | null;
|
||||
@@ -180,12 +188,58 @@ function handleClose(ws: WebSocket): void {
|
||||
}
|
||||
}
|
||||
|
||||
const WEB_DIST = process.env.WEB_DIST ?? "";
|
||||
|
||||
const MIME: Record<string, string> = {
|
||||
".html": "text/html; charset=utf-8",
|
||||
".js": "text/javascript",
|
||||
".mjs": "text/javascript",
|
||||
".css": "text/css",
|
||||
".json": "application/json",
|
||||
".svg": "image/svg+xml",
|
||||
".ico": "image/x-icon",
|
||||
".png": "image/png",
|
||||
".webmanifest": "application/manifest+json",
|
||||
".woff2": "font/woff2",
|
||||
".map": "application/json",
|
||||
};
|
||||
|
||||
/** Serverar den byggda web-frontenden (om WEB_DIST är satt) med SPA-fallback. */
|
||||
function serveStatic(req: IncomingMessage, res: ServerResponse): boolean {
|
||||
if (!WEB_DIST) return false;
|
||||
const urlPath = decodeURIComponent((req.url ?? "/").split("?")[0]);
|
||||
const indexHtml = path.join(WEB_DIST, "index.html");
|
||||
let filePath = path.normalize(path.join(WEB_DIST, urlPath));
|
||||
// Path-traversal-skydd + SPA: "/" och rutter utan filändelse -> index.html
|
||||
if (!filePath.startsWith(WEB_DIST) || urlPath === "/" || !path.extname(filePath)) {
|
||||
filePath = indexHtml;
|
||||
}
|
||||
fs.readFile(filePath, (err, buf) => {
|
||||
if (err) {
|
||||
fs.readFile(indexHtml, (err2, idx) => {
|
||||
if (err2) {
|
||||
res.writeHead(404);
|
||||
res.end("not found");
|
||||
} else {
|
||||
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
||||
res.end(idx);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
res.writeHead(200, { "content-type": MIME[path.extname(filePath)] ?? "application/octet-stream" });
|
||||
res.end(buf);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
if (req.url === "/health") {
|
||||
res.writeHead(200, { "content-type": "application/json" });
|
||||
res.end(JSON.stringify({ ok: true, sessions: sessions.size }));
|
||||
return;
|
||||
}
|
||||
if (serveStatic(req, res)) return;
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
});
|
||||
@@ -208,4 +262,6 @@ wss.on("connection", (ws) => {
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log(`[server] lyssnar på :${PORT} (WebSocket + /health)`);
|
||||
if (WEB_DIST) console.log(`[server] serverar web-UI från ${WEB_DIST}`);
|
||||
if (DATA_DIR) console.log(`[server] data-dir: ${DATA_DIR}`);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user