sched: custom single-binary (embedded engine+admin api+morda+mcp+maria) — artifacts in .admin; compose on sched-custom

This commit is contained in:
2026-08-28 00:45:10 +03:00
parent 3872208a92
commit 9b5cb390a7
4 changed files with 143 additions and 19 deletions

View File

@@ -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

View File

@@ -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"]

View 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);
});
}

View File

@@ -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: