diff --git a/.env.production b/.env.production
new file mode 100644
index 0000000..d522cac
--- /dev/null
+++ b/.env.production
@@ -0,0 +1 @@
+VITE_TOOLKIT_SOCKET_URL=http://bone-kingdom.tabitha.tealthrone:5174
diff --git a/.woodpecker.yml b/.woodpecker.yml
index 9099ca5..094ac70 100644
--- a/.woodpecker.yml
+++ b/.woodpecker.yml
@@ -21,18 +21,23 @@ steps:
validate:
image: node:24-bookworm
+ environment:
+ VITE_TOOLKIT_SOCKET_URL: http://bone-kingdom.tabitha.tealthrone:5174
commands:
- npm ci
- npm run lint
- npm run test -- --runInBand
- npm run web:build
+ - npm run deploy:frontend-health
deploy:
image: node:24-bookworm
environment:
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_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_SSH_PRIVATE_KEY:
from_secret: deploy_ssh_private_key
@@ -46,8 +51,19 @@ steps:
- ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts
- ssh "$DEPLOY_USER@$DEPLOY_HOST" "mkdir -p '$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
- grep -q '
' /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:
- validate
diff --git a/AGENTS.md b/AGENTS.md
index 466413f..5292adf 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -70,6 +70,7 @@ __tests__/ # Jest tests
- 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.
- 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`.
- Shared data interfaces live under `src/data/types/`.
@@ -132,6 +133,8 @@ npm run web
npm run dev:toolkit
npm run dev:all
npm run dev:clear
+npm run deploy:frontend-health
+npm run deploy:toolkit
npm run electron:dev
npm run web:build
npm run electron:build
diff --git a/README.md b/README.md
index 4cf9e73..9c83880 100644
--- a/README.md
+++ b/README.md
@@ -124,6 +124,27 @@ Socket health check:
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
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/`.
diff --git a/__tests__/toolkitSocketClient.test.ts b/__tests__/toolkitSocketClient.test.ts
new file mode 100644
index 0000000..0e6230d
--- /dev/null
+++ b/__tests__/toolkitSocketClient.test.ts
@@ -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',
+ );
+ });
+});
diff --git a/llm.txt b/llm.txt
index 855c46f..f477867 100644
--- a/llm.txt
+++ b/llm.txt
@@ -178,6 +178,7 @@ Canonical list lives in `manifest.llm.json` → `knownGaps`. Current highlights:
- `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 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
- **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
- `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
+- 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
- iOS Pods and `node_modules/` are not committed; run `pod install` after pulling native dep changes
- `dist/` build artifacts are gitignored
diff --git a/manifest.llm.json b/manifest.llm.json
index 6c573cf..b371fb4 100644
--- a/manifest.llm.json
+++ b/manifest.llm.json
@@ -54,12 +54,7 @@
"purpose": "Human-facing overview for setup, commands, architecture, toolkit behavior, and validation."
}
],
- "globalReadOrder": [
- "llm.txt",
- "manifest.llm.json",
- "AGENTS.md",
- "README.md"
- ],
+ "globalReadOrder": ["llm.txt", "manifest.llm.json", "AGENTS.md", "README.md"],
"globalReadOrderSemantics": "Broad orientation order. Task routers should prefer taskRoutes read arrays to avoid over-reading.",
"documentationFiles": [
{
@@ -122,6 +117,8 @@
"entry": "socket/server.mjs",
"devCommand": "npm run dev:toolkit",
"healthUrl": "http://127.0.0.1:5174/health",
+ "deployCommand": "npm run deploy:toolkit",
+ "deployHealthUrl": "http://bone-kingdom.tabitha.tealthrone:5174/health",
"primary": true
},
{
@@ -167,6 +164,8 @@
"dev:toolkit": "node socket/server.mjs",
"dev:all": "concurrently -k -n app,toolkit -c blue,magenta \"npm run dev\" \"npm run dev:toolkit\"",
"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:build": "npm run web:build && npm run electron:compile",
"electron:compile": "tsc -p tsconfig.electron.json",
@@ -178,11 +177,7 @@
},
"architecture": {
"rootComponent": "App.tsx",
- "componentTree": [
- "SafeAreaProvider",
- "GameViewport",
- "AppNavigator"
- ],
+ "componentTree": ["SafeAreaProvider", "GameViewport", "AppNavigator"],
"navigation": {
"type": "zustand-store with animated fade transitions",
"routes": "src/navigation/routes.ts",
@@ -237,13 +232,7 @@
"androidDefault",
"phoneLarge"
],
- "volumeKeys": [
- "master",
- "music",
- "ambience",
- "speech",
- "sfx"
- ]
+ "volumeKeys": ["master", "music", "ambience", "speech", "sfx"]
},
"screens": [
"src/screens/HomeScreen.tsx",
@@ -313,11 +302,7 @@
"hook": "src/hooks/useBackgroundMusic.ts",
"webHook": "src/hooks/useBackgroundMusic.web.ts",
"defaultTrack": "src/assets/music/throne-of-the-bone-king.mp3",
- "volumeControls": [
- "master",
- "music",
- "mute"
- ]
+ "volumeControls": ["master", "music", "mute"]
},
"toolkit": {
"screen": "src/screens/ToolkitScreen.tsx",
@@ -358,10 +343,7 @@
"webEntry": "src/web/main.tsx",
"electronMain": "electron/main.ts",
"electronPreload": "electron/preload.ts",
- "buildOutputs": [
- "dist/renderer/",
- "dist/electron/"
- ]
+ "buildOutputs": ["dist/renderer/", "dist/electron/"]
},
"taskRoutes": {
"ui-screen": {
@@ -416,11 +398,7 @@
"Electron main/preload compile to dist/electron/.",
"Vite emits renderer assets to dist/renderer/."
],
- "verify": [
- "npm run lint",
- "npm run web:build",
- "npm run electron:build"
- ],
+ "verify": ["npm run lint", "npm run web:build", "npm run electron:build"],
"update": [
"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/.",
"Update src/data/toolkit/changelog.json before committing or pushing meaningful deliverables unless explicitly skipped."
],
- "verify": [
- "npm run test -- --runInBand"
- ]
+ "verify": ["npm run test -- --runInBand"]
},
"realtime": {
"description": "Socket.IO server/client wiring for Toolkit connection status and content sync.",
@@ -516,9 +492,7 @@
"",
""
],
- "verify": [
- "npm run test -- --runInBand"
- ]
+ "verify": ["npm run test -- --runInBand"]
},
"docs": {
"description": "README, AGENTS, llm.txt, manifest, docs, or wiki maintenance.",
@@ -627,14 +601,13 @@
"No JSON Schema validator exists yet for manifest.llm.json.",
"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.",
+ "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.",
"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."
],
"verificationBaseline": {
- "logicOrStoreChanges": [
- "npm run test -- --runInBand"
- ],
+ "logicOrStoreChanges": ["npm run test -- --runInBand"],
"uiChanges": [
"npm run lint",
"npm run test -- --runInBand",
@@ -648,12 +621,10 @@
"socketChanges": [
"npm run test -- --runInBand",
"npm run dev:toolkit",
- "curl http://127.0.0.1:5174/health"
- ],
- "mobileChanges": [
- "npm run lint",
- "npm run test -- --runInBand"
+ "curl http://127.0.0.1:5174/health",
+ "curl http://bone-kingdom.tabitha.tealthrone:5174/health"
],
+ "mobileChanges": ["npm run lint", "npm run test -- --runInBand"],
"docsChanges": [
"node -e \"JSON.parse(require('fs').readFileSync('manifest.llm.json','utf8')); console.log('manifest ok')\"",
"npm run lint"
diff --git a/package.json b/package.json
index 8a1dfca..7ee5551 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,8 @@
"dev:all": "concurrently -k -n app,toolkit -c blue,magenta \"npm run dev\" \"npm run dev:toolkit\"",
"dev:clear": "node socket/clear.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: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 .\"",
diff --git a/scripts/deploy/restart-toolkit-socket.sh b/scripts/deploy/restart-toolkit-socket.sh
new file mode 100755
index 0000000..75e631c
--- /dev/null
+++ b/scripts/deploy/restart-toolkit-socket.sh
@@ -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"
diff --git a/scripts/deploy/write-frontend-health.mjs b/scripts/deploy/write-frontend-health.mjs
new file mode 100644
index 0000000..bb64ac9
--- /dev/null
+++ b/scripts/deploy/write-frontend-health.mjs
@@ -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');
diff --git a/socket/README.md b/socket/README.md
index e69de29..66bf988 100644
--- a/socket/README.md
+++ b/socket/README.md
@@ -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.
diff --git a/socket/server.mjs b/socket/server.mjs
index 183fe8c..3c66984 100644
--- a/socket/server.mjs
+++ b/socket/server.mjs
@@ -3,6 +3,7 @@ import 'dotenv/config';
import { watch } from 'node:fs';
import { mkdir, writeFile } from 'node:fs/promises';
import { createServer } from 'node:http';
+import { createRequire } from 'node:module';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -11,6 +12,8 @@ import { Server } from 'socket.io';
import contentStore from './contentStore.cjs';
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 host = process.env.TOOLKIT_SOCKET_HOST ?? '127.0.0.1';
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.');
}
+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) => {
- if (request.url === '/health') {
- response.writeHead(200, {
- 'Content-Type': 'application/json',
- });
- response.end(
- JSON.stringify({
- ok: true,
- service: 'blb-toolkit-socket',
- }),
- );
+ const pathname = request.url?.split('?')[0];
+
+ if (pathname === '/health') {
+ writeJsonResponse(response, 200, createSocketHealthPayload());
return;
}
- response.writeHead(404, {
- 'Content-Type': 'application/json',
+ writeJsonResponse(response, 404, {
+ ok: false,
+ service: 'blb-toolkit-socket',
});
- response.end(
- JSON.stringify({
- ok: false,
- service: 'blb-toolkit-socket',
- }),
- );
});
const io = new Server(httpServer, {
@@ -73,7 +93,9 @@ const assetUploadConfig = {
};
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) {
@@ -237,7 +259,10 @@ io.on('connection', socket => {
const resource = payload?.resource;
try {
- const changedContent = await createContentRecord(resource, payload?.record);
+ const changedContent = await createContentRecord(
+ resource,
+ payload?.record,
+ );
respond?.({
ok: true,
@@ -259,7 +284,10 @@ io.on('connection', socket => {
const resource = payload?.resource;
try {
- const changedContent = await updateContentRecord(resource, payload?.record);
+ const changedContent = await updateContentRecord(
+ resource,
+ payload?.record,
+ );
respond?.({
ok: true,
diff --git a/src/data/toolkit/changelog.json b/src/data/toolkit/changelog.json
index ec0c0e2..34937b4 100644
--- a/src/data/toolkit/changelog.json
+++ b/src/data/toolkit/changelog.json
@@ -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",
"version": "0.0.2-20260624-local",
diff --git a/src/socket/toolkitSocketClient.ts b/src/socket/toolkitSocketClient.ts
index 9c6f1ee..73bfe95 100644
--- a/src/socket/toolkitSocketClient.ts
+++ b/src/socket/toolkitSocketClient.ts
@@ -13,6 +13,17 @@ type NodeProcess = {
env?: Partial>;
};
+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 ToolkitAssetKind = 'audio' | 'image';
@@ -79,9 +90,36 @@ function getMaybeProcess() {
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() {
return (
- getMaybeProcess()?.env?.TOOLKIT_SOCKET_URL ?? defaultToolkitSocketUrl
+ getConfiguredToolkitSocketUrl() ??
+ getBrowserToolkitSocketUrl() ??
+ defaultToolkitSocketUrl
);
}
@@ -103,7 +141,9 @@ export function getToolkitSocket() {
return toolkitSocket;
}
-function isToolkitContentPayload(value: unknown): value is ToolkitContentPayload {
+function isToolkitContentPayload(
+ value: unknown,
+): value is ToolkitContentPayload {
if (value == null || typeof value !== 'object') {
return false;
}
@@ -117,9 +157,7 @@ function isToolkitContentPayload(value: unknown): value is ToolkitContentPayload
);
}
-function parseToolkitContentResponse(
- response: unknown,
-): ToolkitContentPayload {
+function parseToolkitContentResponse(response: unknown): ToolkitContentPayload {
if (response == null || typeof response !== 'object') {
throw new Error('Toolkit content response was empty.');
}
@@ -172,7 +210,10 @@ function parseToolkitAssetUploadResponse(
throw new Error(uploadResponse.error ?? 'Toolkit asset upload failed.');
}
- if (uploadResponse.ok === true && isToolkitAssetUploadPayload(uploadResponse)) {
+ if (
+ uploadResponse.ok === true &&
+ isToolkitAssetUploadPayload(uploadResponse)
+ ) {
return uploadResponse;
}
diff --git a/vite.config.ts b/vite.config.ts
index 247e32d..0f7d6ed 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,7 +1,8 @@
import path from 'node:path';
+import type { ServerResponse } from 'node:http';
import react from '@vitejs/plugin-react';
-import { defineConfig } from 'vite';
+import { defineConfig, loadEnv, type PluginOption } from 'vite';
import packageJson from './package.json';
@@ -17,25 +18,89 @@ const platformExtensions = [
'.json',
];
-export default defineConfig({
- define: {
- 'import.meta.env.VITE_APP_VERSION': JSON.stringify(packageJson.version),
- },
- plugins: [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,
- },
+const frontendHealthStartedAt = Date.now();
+const frontendHealthStartedAtIso = new Date(
+ frontendHealthStartedAt,
+).toISOString();
+
+function createFrontendHealthPayload() {
+ return {
+ ok: true,
+ service: 'blb-frontend',
+ version: packageJson.version,
+ startedAt: frontendHealthStartedAtIso,
+ timestamp: new Date().toISOString(),
+ uptimeSeconds: Math.max(
+ 0,
+ Math.round((Date.now() - frontendHealthStartedAt) / 1000),
+ ),
+ };
+}
+
+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,
+ },
+ };
});