From 614d25c189f93fcb77642a5d9e1687cc79f6ae72 Mon Sep 17 00:00:00 2001 From: hibna Date: Sun, 22 Feb 2026 10:16:42 +0000 Subject: [PATCH] Add internal daemon routes and service management scripts --- .gitignore | 3 +- apps/api/src/index.ts | 2 + apps/api/src/routes/internal/index.ts | 72 ++++++++++++++++++++++++ scripts/panelctl.sh | 81 +++++++++++++++++++++++++++ scripts/run-api.sh | 5 ++ scripts/run-daemon.sh | 6 ++ scripts/run-web.sh | 5 ++ 7 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 apps/api/src/routes/internal/index.ts create mode 100755 scripts/panelctl.sh create mode 100755 scripts/run-api.sh create mode 100755 scripts/run-daemon.sh create mode 100755 scripts/run-web.sh diff --git a/.gitignore b/.gitignore index 8247a3f..daf7c35 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ dist/ .env .env.local .env.*.local +daemon-dev.yml # IDE .idea/ @@ -36,4 +37,4 @@ build/ # Claude .claude/ -plans.md \ No newline at end of file +plans.md diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 5ad3fb0..7cdb173 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -7,6 +7,7 @@ import dbPlugin from './plugins/db.js'; import authPlugin from './plugins/auth.js'; import authRoutes from './routes/auth/index.js'; import organizationRoutes from './routes/organizations/index.js'; +import internalRoutes from './routes/internal/index.js'; import daemonNodeRoutes from './routes/nodes/daemon.js'; import nodeRoutes from './routes/nodes/index.js'; import serverRoutes from './routes/servers/index.js'; @@ -85,6 +86,7 @@ await app.register(authRoutes, { prefix: '/api/auth' }); await app.register(organizationRoutes, { prefix: '/api/organizations' }); await app.register(adminRoutes, { prefix: '/api/admin' }); await app.register(daemonNodeRoutes, { prefix: '/api/nodes' }); +await app.register(internalRoutes, { prefix: '/api/internal' }); // Nested org routes: nodes and servers are scoped to an org await app.register( diff --git a/apps/api/src/routes/internal/index.ts b/apps/api/src/routes/internal/index.ts new file mode 100644 index 0000000..b0e52c7 --- /dev/null +++ b/apps/api/src/routes/internal/index.ts @@ -0,0 +1,72 @@ +import { Type } from '@sinclair/typebox'; +import type { FastifyInstance, FastifyRequest } from 'fastify'; +import { eq } from 'drizzle-orm'; +import { nodes } from '@source/database'; +import { AppError } from '../../lib/errors.js'; + +function extractBearerToken(authHeader?: string): string | null { + if (!authHeader) return null; + const [scheme, token] = authHeader.split(' '); + if (!scheme || !token || scheme.toLowerCase() !== 'bearer') return null; + return token; +} + +async function requireDaemonToken(app: FastifyInstance, request: FastifyRequest): Promise { + const token = extractBearerToken( + typeof request.headers.authorization === 'string' + ? request.headers.authorization + : undefined, + ); + + if (!token) { + throw AppError.unauthorized('Missing daemon bearer token', 'DAEMON_AUTH_MISSING'); + } + + const node = await app.db.query.nodes.findFirst({ + where: eq(nodes.daemonToken, token), + columns: { id: true }, + }); + + if (!node) { + throw AppError.unauthorized('Invalid daemon token', 'DAEMON_AUTH_INVALID'); + } +} + +export default async function internalRoutes(app: FastifyInstance) { + app.get('/schedules/due', async (request) => { + await requireDaemonToken(app, request); + return { tasks: [] }; + }); + + app.post( + '/schedules/:taskId/ack', + { + schema: { + params: Type.Object({ + taskId: Type.String(), + }), + }, + }, + async (request) => { + await requireDaemonToken(app, request); + const { taskId } = request.params as { taskId: string }; + return { success: true, taskId }; + }, + ); + + app.post( + '/servers/:serverUuid/backup', + { + schema: { + params: Type.Object({ + serverUuid: Type.String(), + }), + }, + }, + async (request) => { + await requireDaemonToken(app, request); + const { serverUuid } = request.params as { serverUuid: string }; + return { success: true, serverUuid }; + }, + ); +} diff --git a/scripts/panelctl.sh b/scripts/panelctl.sh new file mode 100755 index 0000000..e743a0e --- /dev/null +++ b/scripts/panelctl.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="/root/codex/source-gamepanel" +PNPM="/root/.nvm/versions/node/v24.13.1/bin/pnpm" +SERVICES=( + "source-gamepanel-api.service" + "source-gamepanel-web.service" + "source-gamepanel-daemon.service" +) + +usage() { + cat <<'EOF' +Usage: panelctl + +Commands: + start Start all services + stop Stop all services + restart Restart all services + status Show status of all services + logs Tail logs of all services + rebuild Rebuild API/Web/Daemon binaries + update Pull latest code, rebuild, migrate DB, restart services + deploy Rebuild current code, migrate DB, restart services +EOF +} + +rebuild_all() { + cd "$ROOT" + "$PNPM" install --frozen-lockfile + "$PNPM" --filter @source/api build + "$PNPM" --filter @source/web build + source "$HOME/.cargo/env" + cd "$ROOT/apps/daemon" + cargo build --release +} + +migrate_db() { + cd "$ROOT" + "$PNPM" db:migrate +} + +cmd="${1:-}" +case "$cmd" in + start) + systemctl start "${SERVICES[@]}" + ;; + stop) + systemctl stop "${SERVICES[@]}" + ;; + restart) + systemctl restart "${SERVICES[@]}" + ;; + status) + systemctl --no-pager --full status "${SERVICES[@]}" + ;; + logs) + journalctl -u "${SERVICES[0]}" -u "${SERVICES[1]}" -u "${SERVICES[2]}" -f + ;; + rebuild) + rebuild_all + ;; + update) + cd "$ROOT" + git pull --ff-only + rebuild_all + migrate_db + systemctl restart "${SERVICES[@]}" + systemctl --no-pager --full status "${SERVICES[@]}" + ;; + deploy) + rebuild_all + migrate_db + systemctl restart "${SERVICES[@]}" + systemctl --no-pager --full status "${SERVICES[@]}" + ;; + *) + usage + exit 1 + ;; +esac diff --git a/scripts/run-api.sh b/scripts/run-api.sh new file mode 100755 index 0000000..8239812 --- /dev/null +++ b/scripts/run-api.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd /root/codex/source-gamepanel/apps/api +exec /root/.nvm/versions/node/v24.13.1/bin/pnpm dev diff --git a/scripts/run-daemon.sh b/scripts/run-daemon.sh new file mode 100755 index 0000000..02af6a8 --- /dev/null +++ b/scripts/run-daemon.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd /root/codex/source-gamepanel/apps/daemon +export DAEMON_CONFIG=/root/codex/source-gamepanel/daemon-dev.yml +exec /root/codex/source-gamepanel/apps/daemon/target/release/gamepanel-daemon diff --git a/scripts/run-web.sh b/scripts/run-web.sh new file mode 100755 index 0000000..6f1c783 --- /dev/null +++ b/scripts/run-web.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd /root/codex/source-gamepanel/apps/web +exec /root/.nvm/versions/node/v24.13.1/bin/pnpm dev --host 0.0.0.0 --port 5173