feat(deploy): run shared toolkit socket
Lint and Build Checks / Lint, Test, and Build (push) Failing after 364h2m28s

This commit is contained in:
2026-06-24 15:35:08 -05:00
parent 47567589e4
commit 79d36c4e2d
15 changed files with 443 additions and 95 deletions
+1
View File
@@ -0,0 +1 @@
VITE_TOOLKIT_SOCKET_URL=http://bone-kingdom.tabitha.tealthrone:5174
+16
View File
@@ -21,18 +21,23 @@ steps:
validate: validate:
image: node:24-bookworm image: node:24-bookworm
environment:
VITE_TOOLKIT_SOCKET_URL: http://bone-kingdom.tabitha.tealthrone:5174
commands: commands:
- npm ci - npm ci
- npm run lint - npm run lint
- npm run test -- --runInBand - npm run test -- --runInBand
- npm run web:build - npm run web:build
- npm run deploy:frontend-health
deploy: deploy:
image: node:24-bookworm image: node:24-bookworm
environment: environment:
DEPLOY_TARGET_URL: http://bone-kingdom.tabitha.tealthrone/ DEPLOY_TARGET_URL: http://bone-kingdom.tabitha.tealthrone/
DEPLOY_TOOLKIT_SOCKET_URL: http://bone-kingdom.tabitha.tealthrone:5174
DEPLOY_HOST: 100.108.87.106 DEPLOY_HOST: 100.108.87.106
DEPLOY_USER: Tabitha DEPLOY_USER: Tabitha
DEPLOY_REPO_PATH: /Users/Tabitha/.openclaw/workspace_father/blb-throne-of-the-bone-king
DEPLOY_PATH: /Users/Tabitha/.openclaw/workspace_father/blb-throne-of-the-bone-king/dist/renderer DEPLOY_PATH: /Users/Tabitha/.openclaw/workspace_father/blb-throne-of-the-bone-king/dist/renderer
DEPLOY_SSH_PRIVATE_KEY: DEPLOY_SSH_PRIVATE_KEY:
from_secret: deploy_ssh_private_key from_secret: deploy_ssh_private_key
@@ -46,8 +51,19 @@ steps:
- ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts - ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts
- ssh "$DEPLOY_USER@$DEPLOY_HOST" "mkdir -p '$DEPLOY_PATH'" - ssh "$DEPLOY_USER@$DEPLOY_HOST" "mkdir -p '$DEPLOY_PATH'"
- rsync -az --delete dist/renderer/ "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/" - rsync -az --delete dist/renderer/ "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/"
- rsync -az package.json package-lock.json "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_REPO_PATH/"
- rsync -az --delete socket/ "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_REPO_PATH/socket/"
- rsync -az --delete scripts/deploy/ "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_REPO_PATH/scripts/deploy/"
- rsync -az --ignore-existing src/data/toolkit/ "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_REPO_PATH/src/data/toolkit/"
- rsync -az --ignore-existing src/assets/images/uploads/ "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_REPO_PATH/src/assets/images/uploads/"
- rsync -az --ignore-existing src/assets/music/uploads/ "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_REPO_PATH/src/assets/music/uploads/"
- ssh "$DEPLOY_USER@$DEPLOY_HOST" "cd '$DEPLOY_REPO_PATH' && TOOLKIT_SOCKET_HOST=0.0.0.0 TOOLKIT_SOCKET_PORT=5174 TOOLKIT_SOCKET_CORS_ORIGIN='http://127.0.0.1:5173,http://localhost:5173,http://bone-kingdom.tabitha.tealthrone' sh scripts/deploy/restart-toolkit-socket.sh '$DEPLOY_REPO_PATH'"
- curl -fsS "$DEPLOY_TOOLKIT_SOCKET_URL/health" >/tmp/bone-kingdom-socket-health.json
- grep -q '"ok":true' /tmp/bone-kingdom-socket-health.json
- curl -fsS "$DEPLOY_TARGET_URL" >/tmp/bone-kingdom-index.html - curl -fsS "$DEPLOY_TARGET_URL" >/tmp/bone-kingdom-index.html
- grep -q '<div id="root">' /tmp/bone-kingdom-index.html - grep -q '<div id="root">' /tmp/bone-kingdom-index.html
- curl -fsS "${DEPLOY_TARGET_URL%/}/health" >/tmp/bone-kingdom-frontend-health.json
- grep -q '"ok":true' /tmp/bone-kingdom-frontend-health.json
depends_on: depends_on:
- validate - validate
+3
View File
@@ -70,6 +70,7 @@ __tests__/ # Jest tests
- Change Log content is sourced from `src/data/toolkit/changelog.json`. - Change Log content is sourced from `src/data/toolkit/changelog.json`.
- Credits Toolkit data is sourced from `src/data/toolkit/credits.json` and synced through the socket server. - Credits Toolkit data is sourced from `src/data/toolkit/credits.json` and synced through the socket server.
- Toolkit Images and Audio records can upload files through the socket into `src/assets/images/uploads/` and `src/assets/music/uploads/`. - Toolkit Images and Audio records can upload files through the socket into `src/assets/images/uploads/` and `src/assets/music/uploads/`.
- Deployed Toolkit authoring uses `http://bone-kingdom.tabitha.tealthrone:5174` and intentionally runs from the shared repo workspace so edits update development JSON/assets while coding continues.
- Toolkit seed datasets live under `src/data/toolkit/` and are exported through `src/data/index.ts`. - Toolkit seed datasets live under `src/data/toolkit/` and are exported through `src/data/index.ts`.
- Shared data interfaces live under `src/data/types/`. - Shared data interfaces live under `src/data/types/`.
@@ -132,6 +133,8 @@ npm run web
npm run dev:toolkit npm run dev:toolkit
npm run dev:all npm run dev:all
npm run dev:clear npm run dev:clear
npm run deploy:frontend-health
npm run deploy:toolkit
npm run electron:dev npm run electron:dev
npm run web:build npm run web:build
npm run electron:build npm run electron:build
+21
View File
@@ -124,6 +124,27 @@ Socket health check:
curl http://127.0.0.1:5174/health curl http://127.0.0.1:5174/health
``` ```
## Shared Deploy Toolkit Socket
The hosted browser app at `http://bone-kingdom.tabitha.tealthrone/` is built with `VITE_TOOLKIT_SOCKET_URL=http://bone-kingdom.tabitha.tealthrone:5174`. That socket is intentionally restarted from the shared repo workspace instead of from `dist/`, so Toolkit edits made by another person update the development JSON and upload folders directly:
- JSON content: `src/data/toolkit/`
- Uploaded images: `src/assets/images/uploads/`
- Uploaded audio: `src/assets/music/uploads/`
Woodpecker deploys the static renderer, restarts the shared Toolkit socket with `scripts/deploy/restart-toolkit-socket.sh`, and validates both:
```bash
curl http://bone-kingdom.tabitha.tealthrone/health
curl http://bone-kingdom.tabitha.tealthrone:5174/health
```
For a manual shared-socket restart on the host:
```bash
TOOLKIT_SOCKET_HOST=0.0.0.0 npm run deploy:toolkit
```
## Assets ## Assets
Image assets live in `src/assets/images/`. Fonts live in `src/assets/fonts/`. Music lives in `src/assets/music/`. Browser SCSS entrypoints live in `src/assets/scss/`. Image assets live in `src/assets/images/`. Fonts live in `src/assets/fonts/`. Music lives in `src/assets/music/`. Browser SCSS entrypoints live in `src/assets/scss/`.
+61
View File
@@ -0,0 +1,61 @@
import { getToolkitSocketUrl } from '../src/socket/toolkitSocketClient';
const originalToolkitSocketUrl = process.env.TOOLKIT_SOCKET_URL;
const originalLocation = Object.getOwnPropertyDescriptor(
globalThis,
'location',
);
function setTestLocation(hostname: string, protocol = 'http:') {
Object.defineProperty(globalThis, 'location', {
configurable: true,
value: {
hostname,
protocol,
},
});
}
function restoreTestLocation() {
if (originalLocation == null) {
Reflect.deleteProperty(globalThis, 'location');
return;
}
Object.defineProperty(globalThis, 'location', originalLocation);
}
describe('toolkit socket client configuration', () => {
afterEach(() => {
if (originalToolkitSocketUrl == null) {
delete process.env.TOOLKIT_SOCKET_URL;
} else {
process.env.TOOLKIT_SOCKET_URL = originalToolkitSocketUrl;
}
restoreTestLocation();
});
it('uses explicit toolkit socket URL configuration first', () => {
process.env.TOOLKIT_SOCKET_URL = 'http://socket.example.test:5174';
setTestLocation('bone-kingdom.tabitha.tealthrone');
expect(getToolkitSocketUrl()).toBe('http://socket.example.test:5174');
});
it('uses local socket defaults for localhost development', () => {
delete process.env.TOOLKIT_SOCKET_URL;
setTestLocation('localhost');
expect(getToolkitSocketUrl()).toBe('http://127.0.0.1:5174');
});
it('derives the deployed socket URL from the current browser host', () => {
delete process.env.TOOLKIT_SOCKET_URL;
setTestLocation('bone-kingdom.tabitha.tealthrone');
expect(getToolkitSocketUrl()).toBe(
'http://bone-kingdom.tabitha.tealthrone:5174',
);
});
});
+2
View File
@@ -178,6 +178,7 @@ Canonical list lives in `manifest.llm.json` → `knownGaps`. Current highlights:
- `wiki/` currently stores reference images and loose wiki/design assets - `wiki/` currently stores reference images and loose wiki/design assets
- Toolkit JSON files now contain starter data based on the current wiki schemas; Credits, Changelog, and Legal live under `src/data/toolkit/` - Toolkit JSON files now contain starter data based on the current wiki schemas; Credits, Changelog, and Legal live under `src/data/toolkit/`
- Toolkit media uploads are written by the local socket server under `src/assets/images/uploads/` and `src/assets/music/uploads/` - Toolkit media uploads are written by the local socket server under `src/assets/images/uploads/` and `src/assets/music/uploads/`
- Deployed Toolkit authoring uses a shared socket at `http://bone-kingdom.tabitha.tealthrone:5174` that runs from the repo workspace and writes to development JSON/assets
## Notes for LLMs ## Notes for LLMs
- **Read `manifest.llm.json` for task routing** — use `taskRoutes` to pick the narrowest file set - **Read `manifest.llm.json` for task routing** — use `taskRoutes` to pick the narrowest file set
@@ -189,6 +190,7 @@ Canonical list lives in `manifest.llm.json` → `knownGaps`. Current highlights:
- **Persistence is platform-split** — extend `persistStorage.ts` / `persistStorage.web.ts`, not stores directly - **Persistence is platform-split** — extend `persistStorage.ts` / `persistStorage.web.ts`, not stores directly
- `src/data/AGENTS.md` contains extra rules for JSON content and Toolkit seed data - `src/data/AGENTS.md` contains extra rules for JSON content and Toolkit seed data
- Data consumers should import from `src/data/index.ts`; keep `src/data/types/` aligned with JSON schema changes - Data consumers should import from `src/data/index.ts`; keep `src/data/types/` aligned with JSON schema changes
- Keep the deployed Toolkit socket tied to the shared repo workspace; do not move it to static `dist/` output or it will stop updating editable JSON/assets for parallel work
- Before committing or pushing meaningful deliverables, update `src/data/toolkit/changelog.json`; Husky bumps build metadata but does not write release notes - Before committing or pushing meaningful deliverables, update `src/data/toolkit/changelog.json`; Husky bumps build metadata but does not write release notes
- iOS Pods and `node_modules/` are not committed; run `pod install` after pulling native dep changes - iOS Pods and `node_modules/` are not committed; run `pod install` after pulling native dep changes
- `dist/` build artifacts are gitignored - `dist/` build artifacts are gitignored
+17 -46
View File
@@ -54,12 +54,7 @@
"purpose": "Human-facing overview for setup, commands, architecture, toolkit behavior, and validation." "purpose": "Human-facing overview for setup, commands, architecture, toolkit behavior, and validation."
} }
], ],
"globalReadOrder": [ "globalReadOrder": ["llm.txt", "manifest.llm.json", "AGENTS.md", "README.md"],
"llm.txt",
"manifest.llm.json",
"AGENTS.md",
"README.md"
],
"globalReadOrderSemantics": "Broad orientation order. Task routers should prefer taskRoutes read arrays to avoid over-reading.", "globalReadOrderSemantics": "Broad orientation order. Task routers should prefer taskRoutes read arrays to avoid over-reading.",
"documentationFiles": [ "documentationFiles": [
{ {
@@ -122,6 +117,8 @@
"entry": "socket/server.mjs", "entry": "socket/server.mjs",
"devCommand": "npm run dev:toolkit", "devCommand": "npm run dev:toolkit",
"healthUrl": "http://127.0.0.1:5174/health", "healthUrl": "http://127.0.0.1:5174/health",
"deployCommand": "npm run deploy:toolkit",
"deployHealthUrl": "http://bone-kingdom.tabitha.tealthrone:5174/health",
"primary": true "primary": true
}, },
{ {
@@ -167,6 +164,8 @@
"dev:toolkit": "node socket/server.mjs", "dev:toolkit": "node socket/server.mjs",
"dev:all": "concurrently -k -n app,toolkit -c blue,magenta \"npm run dev\" \"npm run dev:toolkit\"", "dev:all": "concurrently -k -n app,toolkit -c blue,magenta \"npm run dev\" \"npm run dev:toolkit\"",
"dev:clear": "node socket/clear.mjs", "dev:clear": "node socket/clear.mjs",
"deploy:frontend-health": "node scripts/deploy/write-frontend-health.mjs",
"deploy:toolkit": "sh scripts/deploy/restart-toolkit-socket.sh",
"electron:dev": "npm run electron:compile && cross-env NODE_ENV=development VITE_DEV_SERVER_URL=http://127.0.0.1:5173 concurrently -k \"vite --host 127.0.0.1\" \"wait-on http://127.0.0.1:5173 && electron .\"", "electron:dev": "npm run electron:compile && cross-env NODE_ENV=development VITE_DEV_SERVER_URL=http://127.0.0.1:5173 concurrently -k \"vite --host 127.0.0.1\" \"wait-on http://127.0.0.1:5173 && electron .\"",
"electron:build": "npm run web:build && npm run electron:compile", "electron:build": "npm run web:build && npm run electron:compile",
"electron:compile": "tsc -p tsconfig.electron.json", "electron:compile": "tsc -p tsconfig.electron.json",
@@ -178,11 +177,7 @@
}, },
"architecture": { "architecture": {
"rootComponent": "App.tsx", "rootComponent": "App.tsx",
"componentTree": [ "componentTree": ["SafeAreaProvider", "GameViewport", "AppNavigator"],
"SafeAreaProvider",
"GameViewport",
"AppNavigator"
],
"navigation": { "navigation": {
"type": "zustand-store with animated fade transitions", "type": "zustand-store with animated fade transitions",
"routes": "src/navigation/routes.ts", "routes": "src/navigation/routes.ts",
@@ -237,13 +232,7 @@
"androidDefault", "androidDefault",
"phoneLarge" "phoneLarge"
], ],
"volumeKeys": [ "volumeKeys": ["master", "music", "ambience", "speech", "sfx"]
"master",
"music",
"ambience",
"speech",
"sfx"
]
}, },
"screens": [ "screens": [
"src/screens/HomeScreen.tsx", "src/screens/HomeScreen.tsx",
@@ -313,11 +302,7 @@
"hook": "src/hooks/useBackgroundMusic.ts", "hook": "src/hooks/useBackgroundMusic.ts",
"webHook": "src/hooks/useBackgroundMusic.web.ts", "webHook": "src/hooks/useBackgroundMusic.web.ts",
"defaultTrack": "src/assets/music/throne-of-the-bone-king.mp3", "defaultTrack": "src/assets/music/throne-of-the-bone-king.mp3",
"volumeControls": [ "volumeControls": ["master", "music", "mute"]
"master",
"music",
"mute"
]
}, },
"toolkit": { "toolkit": {
"screen": "src/screens/ToolkitScreen.tsx", "screen": "src/screens/ToolkitScreen.tsx",
@@ -358,10 +343,7 @@
"webEntry": "src/web/main.tsx", "webEntry": "src/web/main.tsx",
"electronMain": "electron/main.ts", "electronMain": "electron/main.ts",
"electronPreload": "electron/preload.ts", "electronPreload": "electron/preload.ts",
"buildOutputs": [ "buildOutputs": ["dist/renderer/", "dist/electron/"]
"dist/renderer/",
"dist/electron/"
]
}, },
"taskRoutes": { "taskRoutes": {
"ui-screen": { "ui-screen": {
@@ -416,11 +398,7 @@
"Electron main/preload compile to dist/electron/.", "Electron main/preload compile to dist/electron/.",
"Vite emits renderer assets to dist/renderer/." "Vite emits renderer assets to dist/renderer/."
], ],
"verify": [ "verify": ["npm run lint", "npm run web:build", "npm run electron:build"],
"npm run lint",
"npm run web:build",
"npm run electron:build"
],
"update": [ "update": [
"manifest.llm.json when targets, build outputs, or dev commands change" "manifest.llm.json when targets, build outputs, or dev commands change"
] ]
@@ -465,9 +443,7 @@
"Keep Changelog, Credits, and Legal JSON under src/data/toolkit/.", "Keep Changelog, Credits, and Legal JSON under src/data/toolkit/.",
"Update src/data/toolkit/changelog.json before committing or pushing meaningful deliverables unless explicitly skipped." "Update src/data/toolkit/changelog.json before committing or pushing meaningful deliverables unless explicitly skipped."
], ],
"verify": [ "verify": ["npm run test -- --runInBand"]
"npm run test -- --runInBand"
]
}, },
"realtime": { "realtime": {
"description": "Socket.IO server/client wiring for Toolkit connection status and content sync.", "description": "Socket.IO server/client wiring for Toolkit connection status and content sync.",
@@ -516,9 +492,7 @@
"<target source>", "<target source>",
"<related __tests__ file>" "<related __tests__ file>"
], ],
"verify": [ "verify": ["npm run test -- --runInBand"]
"npm run test -- --runInBand"
]
}, },
"docs": { "docs": {
"description": "README, AGENTS, llm.txt, manifest, docs, or wiki maintenance.", "description": "README, AGENTS, llm.txt, manifest, docs, or wiki maintenance.",
@@ -627,14 +601,13 @@
"No JSON Schema validator exists yet for manifest.llm.json.", "No JSON Schema validator exists yet for manifest.llm.json.",
"Toolkit content panels use socket-backed structured authoring forms for current JSON fields.", "Toolkit content panels use socket-backed structured authoring forms for current JSON fields.",
"Toolkit audio and image resources can upload local files through the Toolkit socket into src/assets uploads folders.", "Toolkit audio and image resources can upload local files through the Toolkit socket into src/assets uploads folders.",
"The deployed Toolkit socket is expected to run from the shared repo workspace so deployed browser authoring updates development JSON and uploaded assets.",
"Toolkit JSON schemas are first-pass wiki-derived interfaces and should remain flexible while authoring forms stabilize.", "Toolkit JSON schemas are first-pass wiki-derived interfaces and should remain flexible while authoring forms stabilize.",
"Native android/ and ios/ folders are not currently present in this checkout.", "Native android/ and ios/ folders are not currently present in this checkout.",
"No database exists yet; editable content is intentionally JSON-backed until schemas stabilize." "No database exists yet; editable content is intentionally JSON-backed until schemas stabilize."
], ],
"verificationBaseline": { "verificationBaseline": {
"logicOrStoreChanges": [ "logicOrStoreChanges": ["npm run test -- --runInBand"],
"npm run test -- --runInBand"
],
"uiChanges": [ "uiChanges": [
"npm run lint", "npm run lint",
"npm run test -- --runInBand", "npm run test -- --runInBand",
@@ -648,12 +621,10 @@
"socketChanges": [ "socketChanges": [
"npm run test -- --runInBand", "npm run test -- --runInBand",
"npm run dev:toolkit", "npm run dev:toolkit",
"curl http://127.0.0.1:5174/health" "curl http://127.0.0.1:5174/health",
], "curl http://bone-kingdom.tabitha.tealthrone:5174/health"
"mobileChanges": [
"npm run lint",
"npm run test -- --runInBand"
], ],
"mobileChanges": ["npm run lint", "npm run test -- --runInBand"],
"docsChanges": [ "docsChanges": [
"node -e \"JSON.parse(require('fs').readFileSync('manifest.llm.json','utf8')); console.log('manifest ok')\"", "node -e \"JSON.parse(require('fs').readFileSync('manifest.llm.json','utf8')); console.log('manifest ok')\"",
"npm run lint" "npm run lint"
+2
View File
@@ -9,6 +9,8 @@
"dev:all": "concurrently -k -n app,toolkit -c blue,magenta \"npm run dev\" \"npm run dev:toolkit\"", "dev:all": "concurrently -k -n app,toolkit -c blue,magenta \"npm run dev\" \"npm run dev:toolkit\"",
"dev:clear": "node socket/clear.mjs", "dev:clear": "node socket/clear.mjs",
"dev:toolkit": "node socket/server.mjs", "dev:toolkit": "node socket/server.mjs",
"deploy:frontend-health": "node scripts/deploy/write-frontend-health.mjs",
"deploy:toolkit": "sh scripts/deploy/restart-toolkit-socket.sh",
"electron:build": "npm run web:build && npm run electron:compile", "electron:build": "npm run web:build && npm run electron:compile",
"electron:compile": "tsc -p tsconfig.electron.json", "electron:compile": "tsc -p tsconfig.electron.json",
"electron:dev": "npm run electron:compile && cross-env NODE_ENV=development VITE_DEV_SERVER_URL=http://127.0.0.1:5173 concurrently -k \"vite --host 127.0.0.1\" \"wait-on http://127.0.0.1:5173 && electron .\"", "electron:dev": "npm run electron:compile && cross-env NODE_ENV=development VITE_DEV_SERVER_URL=http://127.0.0.1:5173 concurrently -k \"vite --host 127.0.0.1\" \"wait-on http://127.0.0.1:5173 && electron .\"",
+61
View File
@@ -0,0 +1,61 @@
#!/bin/sh
set -eu
repo_path="${1:-$(pwd)}"
socket_host="${TOOLKIT_SOCKET_HOST:-0.0.0.0}"
socket_port="${TOOLKIT_SOCKET_PORT:-5174}"
socket_cors_origin="${TOOLKIT_SOCKET_CORS_ORIGIN:-http://127.0.0.1:5173,http://localhost:5173,http://bone-kingdom.tabitha.tealthrone}"
pid_file="$repo_path/.toolkit-socket.pid"
log_directory="$repo_path/logs"
log_file="$log_directory/toolkit-socket.log"
cd "$repo_path"
if [ ! -f package.json ] || [ ! -f socket/server.mjs ]; then
echo "[deploy:toolkit] $repo_path is not a Bone Kingdom repo checkout."
exit 1
fi
mkdir -p "$log_directory" \
"$repo_path/src/assets/images/uploads" \
"$repo_path/src/assets/music/uploads"
if [ ! -d "$repo_path/node_modules/socket.io" ]; then
echo "[deploy:toolkit] node_modules missing socket.io; installing workspace dependencies."
npm install --no-audit --no-fund
fi
if [ -f "$pid_file" ]; then
existing_pid="$(cat "$pid_file")"
if [ -n "$existing_pid" ] && kill -0 "$existing_pid" 2>/dev/null; then
echo "[deploy:toolkit] stopping existing socket process $existing_pid"
kill -TERM "$existing_pid"
sleep 1
fi
fi
echo "[deploy:toolkit] starting socket on $socket_host:$socket_port"
TOOLKIT_SOCKET_HOST="$socket_host" \
TOOLKIT_SOCKET_PORT="$socket_port" \
TOOLKIT_SOCKET_CORS_ORIGIN="$socket_cors_origin" \
nohup node socket/server.mjs >"$log_file" 2>&1 &
socket_pid="$!"
printf '%s\n' "$socket_pid" > "$pid_file"
sleep 1
if ! kill -0 "$socket_pid" 2>/dev/null; then
echo "[deploy:toolkit] socket failed to start; recent log output follows."
tail -n 80 "$log_file" || true
exit 1
fi
if command -v curl >/dev/null 2>&1; then
curl -fsS "http://127.0.0.1:$socket_port/health" >/dev/null
fi
echo "[deploy:toolkit] socket running as $socket_pid"
+23
View File
@@ -0,0 +1,23 @@
import { mkdir, writeFile } from 'node:fs/promises';
import { createRequire } from 'node:module';
import path from 'node:path';
const require = createRequire(import.meta.url);
const packageJson = require('../../package.json');
const outputPath = process.argv[2] ?? 'dist/renderer/health';
const commit =
process.env.CI_COMMIT_SHA ??
process.env.VITE_LAST_SUCCESSFUL_COMMIT ??
'unknown';
const payload = {
ok: true,
service: 'blb-frontend',
version: packageJson.version,
commit,
generatedAt: new Date().toISOString(),
};
await mkdir(path.dirname(outputPath), { recursive: true });
await writeFile(outputPath, `${JSON.stringify(payload)}\n`, 'utf8');
+41
View File
@@ -0,0 +1,41 @@
# Toolkit Socket
The Toolkit socket is a local/shared Socket.IO authoring server for browser-only Toolkit workflows.
## Local Development
```bash
npm run dev:toolkit
curl http://127.0.0.1:5174/health
```
Default local settings:
- host: `127.0.0.1`
- port: `5174`
- CORS origin: `http://127.0.0.1:5173`
## Shared Deployment
The deployed browser app at `http://bone-kingdom.tabitha.tealthrone/` connects to:
```text
http://bone-kingdom.tabitha.tealthrone:5174
```
Woodpecker restarts this socket from the shared repo workspace with:
```bash
TOOLKIT_SOCKET_HOST=0.0.0.0 \
TOOLKIT_SOCKET_PORT=5174 \
TOOLKIT_SOCKET_CORS_ORIGIN='http://127.0.0.1:5173,http://localhost:5173,http://bone-kingdom.tabitha.tealthrone' \
sh scripts/deploy/restart-toolkit-socket.sh
```
Running from the repo workspace is intentional. Toolkit edits made from the deployed browser update the same source files used during development:
- `src/data/toolkit/*.json`
- `src/assets/images/uploads/`
- `src/assets/music/uploads/`
The deploy script syncs socket code and scripts, but uses `--ignore-existing` for Toolkit JSON and uploaded assets so live content edits are not overwritten by routine deploys.
+49 -21
View File
@@ -3,6 +3,7 @@ import 'dotenv/config';
import { watch } from 'node:fs'; import { watch } from 'node:fs';
import { mkdir, writeFile } from 'node:fs/promises'; import { mkdir, writeFile } from 'node:fs/promises';
import { createServer } from 'node:http'; import { createServer } from 'node:http';
import { createRequire } from 'node:module';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
@@ -11,6 +12,8 @@ import { Server } from 'socket.io';
import contentStore from './contentStore.cjs'; import contentStore from './contentStore.cjs';
const repositoryRoot = fileURLToPath(new URL('..', import.meta.url)); const repositoryRoot = fileURLToPath(new URL('..', import.meta.url));
const require = createRequire(import.meta.url);
const packageJson = require('../package.json');
const defaultWebOrigin = 'http://127.0.0.1:5173'; const defaultWebOrigin = 'http://127.0.0.1:5173';
const host = process.env.TOOLKIT_SOCKET_HOST ?? '127.0.0.1'; const host = process.env.TOOLKIT_SOCKET_HOST ?? '127.0.0.1';
const port = Number.parseInt(process.env.TOOLKIT_SOCKET_PORT ?? '5174', 10); const port = Number.parseInt(process.env.TOOLKIT_SOCKET_PORT ?? '5174', 10);
@@ -25,29 +28,46 @@ if (Number.isNaN(port)) {
throw new Error('TOOLKIT_SOCKET_PORT must be a valid number.'); throw new Error('TOOLKIT_SOCKET_PORT must be a valid number.');
} }
const socketHealthStartedAt = Date.now();
const socketHealthStartedAtIso = new Date(socketHealthStartedAt).toISOString();
function createSocketHealthPayload() {
return {
ok: true,
service: 'blb-toolkit-socket',
version: packageJson.version,
startedAt: socketHealthStartedAtIso,
timestamp: new Date().toISOString(),
uptimeSeconds: Math.max(
0,
Math.round((Date.now() - socketHealthStartedAt) / 1000),
),
};
}
function writeJsonResponse(response, statusCode, body) {
response.writeHead(statusCode, {
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS',
'Access-Control-Allow-Origin': '*',
'Cache-Control': 'no-store',
'Content-Type': 'application/json',
});
response.end(`${JSON.stringify(body)}\n`);
}
const httpServer = createServer((request, response) => { const httpServer = createServer((request, response) => {
if (request.url === '/health') { const pathname = request.url?.split('?')[0];
response.writeHead(200, {
'Content-Type': 'application/json', if (pathname === '/health') {
}); writeJsonResponse(response, 200, createSocketHealthPayload());
response.end(
JSON.stringify({
ok: true,
service: 'blb-toolkit-socket',
}),
);
return; return;
} }
response.writeHead(404, { writeJsonResponse(response, 404, {
'Content-Type': 'application/json', ok: false,
service: 'blb-toolkit-socket',
}); });
response.end(
JSON.stringify({
ok: false,
service: 'blb-toolkit-socket',
}),
);
}); });
const io = new Server(httpServer, { const io = new Server(httpServer, {
@@ -73,7 +93,9 @@ const assetUploadConfig = {
}; };
function getErrorMessage(error) { function getErrorMessage(error) {
return error instanceof Error ? error.message : 'Unknown toolkit content error.'; return error instanceof Error
? error.message
: 'Unknown toolkit content error.';
} }
function createContentError(resource, error) { function createContentError(resource, error) {
@@ -237,7 +259,10 @@ io.on('connection', socket => {
const resource = payload?.resource; const resource = payload?.resource;
try { try {
const changedContent = await createContentRecord(resource, payload?.record); const changedContent = await createContentRecord(
resource,
payload?.record,
);
respond?.({ respond?.({
ok: true, ok: true,
@@ -259,7 +284,10 @@ io.on('connection', socket => {
const resource = payload?.resource; const resource = payload?.resource;
try { try {
const changedContent = await updateContentRecord(resource, payload?.record); const changedContent = await updateContentRecord(
resource,
payload?.record,
);
respond?.({ respond?.({
ok: true, ok: true,
+12
View File
@@ -1,4 +1,16 @@
[ [
{
"id": "deployed-toolkit-socket-workspace",
"version": "0.0.5-20260624-local",
"createdAt": "2026-06-24",
"title": "Deployed Toolkit Socket Workspace",
"summary": "Prepared deployments to run a shared Toolkit socket against the development workspace data and assets.",
"changes": [
"Added deploy-aware Toolkit socket URL selection for built browser clients.",
"Added Woodpecker deploy steps to restart the Toolkit socket and validate frontend and socket health endpoints.",
"Added a shared workspace socket restart script that writes Toolkit JSON and uploaded media into the repo source folders."
]
},
{ {
"id": "toolkit-structured-media-authoring", "id": "toolkit-structured-media-authoring",
"version": "0.0.2-20260624-local", "version": "0.0.2-20260624-local",
+47 -6
View File
@@ -13,6 +13,17 @@ type NodeProcess = {
env?: Partial<Record<string, string>>; env?: Partial<Record<string, string>>;
}; };
type BrowserLocation = {
hostname: string;
protocol: string;
};
type BrowserGlobal = typeof globalThis & {
location?: BrowserLocation;
};
declare const __TOOLKIT_SOCKET_URL__: string | undefined;
export type ToolkitContentResource = EditableToolkitResource; export type ToolkitContentResource = EditableToolkitResource;
export type ToolkitAssetKind = 'audio' | 'image'; export type ToolkitAssetKind = 'audio' | 'image';
@@ -79,9 +90,36 @@ function getMaybeProcess() {
return typeof process === 'undefined' ? undefined : (process as NodeProcess); return typeof process === 'undefined' ? undefined : (process as NodeProcess);
} }
function getConfiguredToolkitSocketUrl() {
if (
typeof __TOOLKIT_SOCKET_URL__ === 'string' &&
__TOOLKIT_SOCKET_URL__.length > 0
) {
return __TOOLKIT_SOCKET_URL__;
}
return getMaybeProcess()?.env?.TOOLKIT_SOCKET_URL;
}
function getBrowserToolkitSocketUrl() {
const location = (globalThis as BrowserGlobal).location;
if (location == null) {
return null;
}
if (location.hostname === '127.0.0.1' || location.hostname === 'localhost') {
return defaultToolkitSocketUrl;
}
return `${location.protocol}//${location.hostname}:5174`;
}
export function getToolkitSocketUrl() { export function getToolkitSocketUrl() {
return ( return (
getMaybeProcess()?.env?.TOOLKIT_SOCKET_URL ?? defaultToolkitSocketUrl getConfiguredToolkitSocketUrl() ??
getBrowserToolkitSocketUrl() ??
defaultToolkitSocketUrl
); );
} }
@@ -103,7 +141,9 @@ export function getToolkitSocket() {
return toolkitSocket; return toolkitSocket;
} }
function isToolkitContentPayload(value: unknown): value is ToolkitContentPayload { function isToolkitContentPayload(
value: unknown,
): value is ToolkitContentPayload {
if (value == null || typeof value !== 'object') { if (value == null || typeof value !== 'object') {
return false; return false;
} }
@@ -117,9 +157,7 @@ function isToolkitContentPayload(value: unknown): value is ToolkitContentPayload
); );
} }
function parseToolkitContentResponse( function parseToolkitContentResponse(response: unknown): ToolkitContentPayload {
response: unknown,
): ToolkitContentPayload {
if (response == null || typeof response !== 'object') { if (response == null || typeof response !== 'object') {
throw new Error('Toolkit content response was empty.'); throw new Error('Toolkit content response was empty.');
} }
@@ -172,7 +210,10 @@ function parseToolkitAssetUploadResponse(
throw new Error(uploadResponse.error ?? 'Toolkit asset upload failed.'); throw new Error(uploadResponse.error ?? 'Toolkit asset upload failed.');
} }
if (uploadResponse.ok === true && isToolkitAssetUploadPayload(uploadResponse)) { if (
uploadResponse.ok === true &&
isToolkitAssetUploadPayload(uploadResponse)
) {
return uploadResponse; return uploadResponse;
} }
+87 -22
View File
@@ -1,7 +1,8 @@
import path from 'node:path'; import path from 'node:path';
import type { ServerResponse } from 'node:http';
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite'; import { defineConfig, loadEnv, type PluginOption } from 'vite';
import packageJson from './package.json'; import packageJson from './package.json';
@@ -17,25 +18,89 @@ const platformExtensions = [
'.json', '.json',
]; ];
export default defineConfig({ const frontendHealthStartedAt = Date.now();
define: { const frontendHealthStartedAtIso = new Date(
'import.meta.env.VITE_APP_VERSION': JSON.stringify(packageJson.version), frontendHealthStartedAt,
}, ).toISOString();
plugins: [react()],
resolve: { function createFrontendHealthPayload() {
alias: [{ find: /^react-native$/, replacement: 'react-native-web' }], return {
dedupe: ['react', 'react-dom', 'react-native-web'], ok: true,
extensions: platformExtensions, service: 'blb-frontend',
}, version: packageJson.version,
optimizeDeps: { startedAt: frontendHealthStartedAtIso,
exclude: ['react-native-safe-area-context'], timestamp: new Date().toISOString(),
}, uptimeSeconds: Math.max(
build: { 0,
outDir: path.resolve(__dirname, 'dist/renderer'), Math.round((Date.now() - frontendHealthStartedAt) / 1000),
emptyOutDir: true, ),
}, };
server: { }
host: '127.0.0.1',
port: 5173, function writeJsonResponse(
}, response: ServerResponse,
statusCode: number,
body: object,
) {
response.writeHead(statusCode, {
'Cache-Control': 'no-store',
'Content-Type': 'application/json',
});
response.end(`${JSON.stringify(body)}\n`);
}
function frontendHealthPlugin(): PluginOption {
return {
name: 'blb-frontend-health',
configurePreviewServer(server) {
server.middlewares.use((request, response, next) => {
if (request.url?.split('?')[0] !== '/health') {
next();
return;
}
writeJsonResponse(response, 200, createFrontendHealthPayload());
});
},
configureServer(server) {
server.middlewares.use((request, response, next) => {
if (request.url?.split('?')[0] !== '/health') {
next();
return;
}
writeJsonResponse(response, 200, createFrontendHealthPayload());
});
},
};
}
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, __dirname, '');
const toolkitSocketUrl =
env.VITE_TOOLKIT_SOCKET_URL ?? env.TOOLKIT_SOCKET_URL ?? '';
return {
define: {
__TOOLKIT_SOCKET_URL__: JSON.stringify(toolkitSocketUrl),
'import.meta.env.VITE_APP_VERSION': JSON.stringify(packageJson.version),
},
plugins: [frontendHealthPlugin(), react()],
resolve: {
alias: [{ find: /^react-native$/, replacement: 'react-native-web' }],
dedupe: ['react', 'react-dom', 'react-native-web'],
extensions: platformExtensions,
},
optimizeDeps: {
exclude: ['react-native-safe-area-context'],
},
build: {
outDir: path.resolve(__dirname, 'dist/renderer'),
emptyOutDir: true,
},
server: {
host: '127.0.0.1',
port: 5173,
},
};
}); });