sched: custom single-binary (embedded engine+admin api+morda+mcp+maria) — artifacts in .admin; compose on sched-custom
This commit is contained in:
93
host-stacks/vds-kzntsv/sched-custom/custom-entry.mjs
Normal file
93
host-stacks/vds-kzntsv/sched-custom/custom-entry.mjs
Normal file
@@ -0,0 +1,93 @@
|
||||
// sched custom single-binary: embedded engine + admin API + UI morda + MCP (Streamable HTTP) + storage (MariaDB/MySQL).
|
||||
// One HTTP server on :8080, routed:
|
||||
// /api/* → createAdminApi.handleRequest (health open, rest Bearer SCHED_ADMIN_KEY)
|
||||
// /mcp → createMcpHttpHandler (Streamable HTTP MCP, projection of admin API)
|
||||
// /sched-ui.bundle.js → static UI bundle
|
||||
// / → UI morda (html)
|
||||
// Engine via createDaemon (runs ticks + sync; we supply storage+adminApi ourselves on OUR server).
|
||||
import { createServer } from 'node:http';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { createDaemon } from '@schedjs/daemon';
|
||||
import { createAdminApi } from '@schedjs/admin-api';
|
||||
import { createMysqlStorage, dbNameFromUrl } from '@schedjs/storage-mysql';
|
||||
import { createPool } from 'mysql2/promise';
|
||||
import { AdminApiClient } from '@schedjs/mcp';
|
||||
|
||||
// Subpath imports are blocked by npm `exports` masks — import the dist modules by
|
||||
// absolute file URL (a direct file reference, not a package specifier).
|
||||
const { createMcpHttpHandler } = await import(pathToFileURL('/app/node_modules/@schedjs/mcp/dist/http.js').href);
|
||||
const { mordaHtml, DEFAULT_UI_BUNDLE } = await import(pathToFileURL('/app/node_modules/@schedjs/ui/dist/morda.js').href);
|
||||
|
||||
const PORT = Number(process.env.SCHED_PORT ?? 8080);
|
||||
const HOST = process.env.SCHED_HOST ?? '0.0.0.0';
|
||||
const ADMIN_KEY = process.env.SCHED_ADMIN_KEY; // Bearer; unset → open (dev)
|
||||
const TASKS = process.env.SCHED_TASKS ?? '/app/tasks.json';
|
||||
|
||||
// --- storage (MariaDB/MySQL) ---
|
||||
const MYSQL_URL = process.env.MYSQL_URL;
|
||||
if (!MYSQL_URL) throw new Error('MYSQL_URL is not set — export MYSQL_URL=<connstring>');
|
||||
const pool = createPool(MYSQL_URL);
|
||||
const storage = await createMysqlStorage(pool); // self-migrates on open
|
||||
const dbName = dbNameFromUrl(MYSQL_URL);
|
||||
|
||||
// --- daemon: engine only (we own the http server) ---
|
||||
const daemon = createDaemon({
|
||||
storage,
|
||||
tasksPath: TASKS,
|
||||
// no `admin` here — createDaemon would bind its own /api-only server; we route ourselves
|
||||
});
|
||||
await daemon.start();
|
||||
const engine = daemon.engine;
|
||||
const adminApi = createAdminApi({ engine, storage, auth: ADMIN_KEY ? { apiKey: ADMIN_KEY } : undefined, version: '0.12.1-custom' });
|
||||
|
||||
// --- MCP: projection of admin API via HTTP client to ourselves ---
|
||||
// AdminApiClient appends paths like '/tasks' to baseUrl → must end at the /api mount point.
|
||||
const ADMIN_BASE = `http://127.0.0.1:${PORT}/api`;
|
||||
const mcpClient = new AdminApiClient(ADMIN_BASE, ADMIN_KEY);
|
||||
const mcpHandler = createMcpHttpHandler({ client: mcpClient, readonly: process.env.SCHED_MCP_READONLY === '1' });
|
||||
|
||||
// --- static UI bundle bytes ---
|
||||
let uiBundle = null;
|
||||
try { uiBundle = readFileSync(DEFAULT_UI_BUNDLE); } catch { /* bundle missing → morda 404s its script */ }
|
||||
|
||||
const server = createServer(async (req, res) => {
|
||||
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
|
||||
const p = url.pathname;
|
||||
try {
|
||||
if (p === '/api' || p.startsWith('/api/')) {
|
||||
req.url = p.replace(/^\/api/, '') + url.search; // strip /api mount prefix
|
||||
await adminApi.handleRequest(req, res);
|
||||
return;
|
||||
}
|
||||
if (p === '/mcp' || p.startsWith('/mcp/')) {
|
||||
await mcpHandler(req, res);
|
||||
return;
|
||||
}
|
||||
if (p === '/sched-ui.bundle.js' && uiBundle) {
|
||||
res.writeHead(200, { 'content-type': 'application/javascript' });
|
||||
res.end(uiBundle);
|
||||
return;
|
||||
}
|
||||
// UI morda for everything else (root + non-api paths)
|
||||
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
|
||||
res.end(mordaHtml({ apiBase: '/api', bundleSrc: '/sched-ui.bundle.js', tokenKey: 'sched-token' }));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'content-type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(PORT, HOST, () => {
|
||||
process.stderr.write(`sched custom up: http://${HOST}:${PORT} (storage=mysql db=${dbName}, admin_auth=${ADMIN_KEY ? 'on' : 'OFF'}, mcp=on)\n`);
|
||||
});
|
||||
|
||||
// Graceful stop on SIGTERM
|
||||
for (const sig of ['SIGTERM', 'SIGINT']) {
|
||||
process.on(sig, () => {
|
||||
daemon.stop();
|
||||
server.close();
|
||||
pool.end();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user