From 9b5cb390a7007cff0adab6257a5a10cd9470beb2 Mon Sep 17 00:00:00 2001 From: vitya Date: Fri, 28 Aug 2026 00:45:10 +0300 Subject: [PATCH] =?UTF-8?q?sched:=20custom=20single-binary=20(embedded=20e?= =?UTF-8?q?ngine+admin=20api+morda+mcp+maria)=20=E2=80=94=20artifacts=20in?= =?UTF-8?q?=20.admin;=20compose=20on=20sched-custom?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .wiki/concepts/sched-vds-deploy-runbook.md | 13 ++- .../vds-kzntsv/sched-custom/Dockerfile.custom | 29 ++++++ .../vds-kzntsv/sched-custom/custom-entry.mjs | 93 +++++++++++++++++++ host-stacks/vds-kzntsv/sched.compose.yml | 27 +++--- 4 files changed, 143 insertions(+), 19 deletions(-) create mode 100644 host-stacks/vds-kzntsv/sched-custom/Dockerfile.custom create mode 100644 host-stacks/vds-kzntsv/sched-custom/custom-entry.mjs diff --git a/.wiki/concepts/sched-vds-deploy-runbook.md b/.wiki/concepts/sched-vds-deploy-runbook.md index fae9f28..f4ec562 100644 --- a/.wiki/concepts/sched-vds-deploy-runbook.md +++ b/.wiki/concepts/sched-vds-deploy-runbook.md @@ -25,10 +25,10 @@ updated: 2026-08-27 |---|---| | Хост | VDS kzntsv `89.253.255.94` | | Стек Portainer | **Id 28**, name `sched`, endpointId **1** | -| Образ | `registry.kzntsv.site/sched-daemon:0.12.1-mysql` | +| Образ | `registry.kzntsv.site/sched-custom:0.12.1-mysql` (custom single-binary) | Compose source-of-truth | `admin/host-stacks/vds-kzntsv/sched.compose.yml` | | tasks.json source-of-truth | `admin/host-stacks/vds-kzntsv/sched.tasks.json` → VDS `/opt/stacks/sched/tasks.json` | -| Даемон | published `@schedjs/daemon@0.12.1` + `@schedjs/storage-mysql@0.4.0` | +| Даемон | published `@schedjs/daemon@0.12.1` + `@schedjs/storage-mysql@0.4.0` + `@schedjs/admin-api@0.3.0` + `@schedjs/mcp@0.4.1` + `@schedjs/ui@0.3.0` | | БД | MariaDB `mariadb:3306` (сеть `shared-dbs`), база `sched`, роль `sched@%`. Креды: `pass sched/mysql` | | Admin API | `https://sched.vds.kzntsv.site` (traefik websecure, letsEncrypt). `SCHED_ADMIN_KEY` = `pass sched/admin-key` | | mem_limit | `512m` (канон app-стеков) | @@ -62,7 +62,14 @@ docker exec mariadb mariadb -uroot -p"$ROOT_PW" -e " ``` Креды → `pass insert -m sched/mysql` (user/pass/db/host). -**1. Образ** — собрать (см. выше) → `docker login registry.kzntsv.site -u vitya` → `docker push`. (внешний registry, публичный npm, BuildKit-секрет не нужен). +**1. Образ (custom single-binary).** `apps/daemon/Dockerfile.custom` собирает один образ с опубликованными `@schedjs/{daemon,storage-mysql,admin-api,mcp,ui}` + `custom-entry.mjs` — один HTTP-сервер на `:8080`: + - `/api/*` → админ API (health open, остальное Bearer `SCHED_ADMIN_KEY`) + - `/mcp` → Streamable HTTP MCP (projection of admin API, `SCHED_MCP_READONLY=1` read-only) + - `/` → UI morda (web-components) + - `/sched-ui.bundle.js` → статический UI-бандл + Сборка: `docker build -f apps/daemon/Dockerfile.custom -t registry.kzntsv.site/sched-custom:0.12.1-mysql apps/daemon`. + → ЕДИНЫЙ бинарь: даемон + UI-морда + MCP не плодит отдельные контейнеры (обратная связь vitya). + `createDaemon` даёт `engine`+`storage` (admin server НЕ bind — маршрутизация своя). MCP/морда импортируются по **file-URL** (subpath-импорты заблокированы npm `exports`-маской). **2. tasks.json.** Ядро стартует с `{"tasks":[]}` (воркеры — отдельными тасками). Закинуть на VDS: ```bash diff --git a/host-stacks/vds-kzntsv/sched-custom/Dockerfile.custom b/host-stacks/vds-kzntsv/sched-custom/Dockerfile.custom new file mode 100644 index 0000000..6b1ccf5 --- /dev/null +++ b/host-stacks/vds-kzntsv/sched-custom/Dockerfile.custom @@ -0,0 +1,29 @@ +# sched custom single-binary — embedded engine + admin API + UI morda + MCP + storage (MariaDB). +# Built on published @schedjs/* npm packages (artifacts that passed the gates). +# Runs apps/daemon/src/custom-entry.mjs — one HTTP server :8080 routing /api, /mcp, / (morda). +# +# Build: +# docker build -f apps/daemon/Dockerfile.custom -t registry.kzntsv.site/sched-custom:0.12.1-mysql apps/daemon +# Run: +# docker run -e MYSQL_URL=mysql://sched:pw@mariadb:3306/sched?ssl={"rejectUnauthorized":false} \ +# -e SCHED_ADMIN_KEY=... -e SCHED_TASKS=/app/config/tasks.json \ +# registry.kzntsv.site/sched-custom:0.12.1-mysql + +FROM node:24-alpine +WORKDIR /app +ENV NODE_ENV=production + +RUN echo '{"name":"schedd","private":true,"packageManager":"yarn@4.18.0"}' > package.json \ + && corepack enable \ + && printf 'nodeLinker: node-modules\nnpmMinimalAgeGate: 0\n' > .yarnrc.yml \ + && CI=1 yarn add @schedjs/daemon@0.12.1 @schedjs/storage-mysql@0.4.0 @schedjs/admin-api@0.3.0 @schedjs/mcp@0.4.1 @schedjs/ui@0.3.0 mysql2@3 \ + && rm -f .yarnrc.yml \ + && yarn cache clean + +ENV PATH="/app/node_modules/.bin:${PATH}" +COPY src/custom-entry.mjs /app/custom-entry.mjs +# Default tasks.json baked into the image (overridden on prod by a bind mount / SCHED_TASKS). +RUN echo '{"tasks":[]}' > /app/tasks.json +EXPOSE 8080 +VOLUME ["/data"] +ENTRYPOINT ["node", "/app/custom-entry.mjs"] diff --git a/host-stacks/vds-kzntsv/sched-custom/custom-entry.mjs b/host-stacks/vds-kzntsv/sched-custom/custom-entry.mjs new file mode 100644 index 0000000..61ec039 --- /dev/null +++ b/host-stacks/vds-kzntsv/sched-custom/custom-entry.mjs @@ -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='); +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); + }); +} diff --git a/host-stacks/vds-kzntsv/sched.compose.yml b/host-stacks/vds-kzntsv/sched.compose.yml index ab50f6d..c35de45 100644 --- a/host-stacks/vds-kzntsv/sched.compose.yml +++ b/host-stacks/vds-kzntsv/sched.compose.yml @@ -1,28 +1,23 @@ -# sched — шедулер (ядро daemon + admin API) на инфра-VDS. -# Прод: VDS рядом с общей MariaDB (стек 11, сеть shared-dbs, отдельная база sched — свой role, решение 2026-08-27). -# Daemon: published @schedjs/daemon@0.12.1 + @schedjs/storage-mysql@0.4.0 (Dockerfile.mysql). -# Endpoint: https://sched.vds.kzntsv.site (admin API /api/*; health /api/health открыт, остальное Bearer SCHED_ADMIN_KEY). +# sched — custom single-binary (embedded engine + admin API + UI morda + MCP + MariaDB). +# Один контейнер, один HTTP-сервер :8080: /api/* (admin API), /mcp (Streamable HTTP MCP), +# / (UI morda), /sched-ui.bundle.js (static UI bundle). +# Образ registry.kzntsv.site/sched-custom:0.12.1-mysql = published @schedjs/{daemon,storage-mysql,admin-api,mcp,ui} +# + собственный custom-entry.mjs (Dockerfile.custom). +# Прод: VDS рядом с общей MariaDB (стек 11, сеть shared-dbs, отдельная база sched — свой role). +# Endpoint: https://sched.vds.kzntsv.site — /api/* + /mcp, остальное → UI morda. # Deploy: Portainer-managed (см. portainer-stack-management-vds). Env через Portainer (не env_file). services: - schedd: - image: registry.kzntsv.site/sched-daemon:0.12.1-mysql - container_name: schedd + sched: + image: registry.kzntsv.site/sched-custom:0.12.1-mysql + container_name: sched restart: unless-stopped mem_limit: 512m networks: - shared-dbs - proxy - command: - - --tasks - - /app/config/tasks.json - - --storage - - mysql - - --admin-port - - "8080" - - --admin-host - - 0.0.0.0 environment: + SCHED_TASKS: /app/config/tasks.json MYSQL_URL: mysql://sched:${SCHED_DB_PASS}@mariadb:3306/sched?ssl={"rejectUnauthorized":false} SCHED_ADMIN_KEY: ${SCHED_ADMIN_KEY} volumes: