Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d9b052b9a | ||
|
|
b1ae502435 | ||
|
|
ca559acb3f | ||
|
|
53faaca5ab | ||
|
|
2ee3317b00 | ||
|
|
40b882672a | ||
|
|
bb710b46b0 | ||
|
|
9e53a9b92c | ||
|
|
7f01d76c2b | ||
|
|
a1db6e782d | ||
|
|
7222c9ae63 | ||
|
|
a8ee1c229e | ||
|
|
a06c3d2e36 | ||
|
|
bc5c924c8e | ||
|
|
cc3403363c | ||
|
|
e44053a25d | ||
|
|
6ed842dae9 | ||
|
|
0926415ffa | ||
|
|
ac638a3586 | ||
|
|
3de5951e48 | ||
|
|
5775ec1e3b | ||
|
|
d50de9232f | ||
|
|
547fd6affb | ||
|
|
460671e753 | ||
|
|
32188d93b4 | ||
|
|
27dbc1f40e | ||
|
|
2b6500c697 | ||
|
|
0e8c855818 | ||
|
|
936dd63976 | ||
|
|
4dceeb2e7f | ||
|
|
a4a1b03215 | ||
|
|
f91082d2a3 | ||
|
|
82574e7308 | ||
|
|
8d99a58600 | ||
|
|
f1b71ca5b8 | ||
|
|
a092e5b548 | ||
|
|
37be53dae8 | ||
|
|
a8723f6875 |
@@ -0,0 +1,98 @@
|
||||
# Gitea Actions: ARM64 native Docker build
|
||||
|
||||
name: Docker Build (ARM64)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'Dockerfile'
|
||||
- 'Dockerfile.canvas'
|
||||
- 'src/**'
|
||||
- 'frontend/**'
|
||||
- 'package*.json'
|
||||
- '.dockerignore'
|
||||
- '.gitea/**'
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'Dockerfile'
|
||||
- 'Dockerfile.canvas'
|
||||
- 'src/**'
|
||||
- 'frontend/**'
|
||||
- 'package*.json'
|
||||
- '.dockerignore'
|
||||
- '.gitea/**'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: docker-arm64-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
REGISTRY: gitea.forkless.com
|
||||
IMAGE_MCP: gitea.forkless.com/forkless/excalidraw-mcp-sentinel
|
||||
IMAGE_CANVAS: gitea.forkless.com/forkless/excalidraw-mcp-sentinel-canvas
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
name: Build & Push
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Build MCP Server
|
||||
run: |
|
||||
docker build \
|
||||
--tag ${{ env.IMAGE_MCP }}:latest \
|
||||
--tag ${{ env.IMAGE_MCP }}:${{ github.sha }} \
|
||||
--file Dockerfile .
|
||||
|
||||
- name: Build Canvas Server
|
||||
run: |
|
||||
docker build \
|
||||
--tag ${{ env.IMAGE_CANVAS }}:latest \
|
||||
--tag ${{ env.IMAGE_CANVAS }}:${{ github.sha }} \
|
||||
--file Dockerfile.canvas .
|
||||
|
||||
- name: Login to Gitea Container Registry
|
||||
if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
|
||||
run: |
|
||||
echo "${{ secrets.GITEATOKEN }}" | \
|
||||
docker login ${{ env.REGISTRY }} \
|
||||
--username "${{ secrets.GITEAUSER }}" \
|
||||
--password-stdin
|
||||
|
||||
- name: Push latest
|
||||
if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
|
||||
run: |
|
||||
docker push ${{ env.IMAGE_MCP }}:latest
|
||||
docker push ${{ env.IMAGE_MCP }}:${{ github.sha }}
|
||||
docker push ${{ env.IMAGE_CANVAS }}:latest
|
||||
docker push ${{ env.IMAGE_CANVAS }}:${{ github.sha }}
|
||||
|
||||
- name: Tag version
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
run: |
|
||||
VERSION=${GITHUB_REF#refs/tags/v}
|
||||
docker tag ${{ env.IMAGE_MCP }}:latest ${{ env.IMAGE_MCP }}:$VERSION
|
||||
docker tag ${{ env.IMAGE_CANVAS }}:latest ${{ env.IMAGE_CANVAS }}:$VERSION
|
||||
docker push ${{ env.IMAGE_MCP }}:$VERSION
|
||||
docker push ${{ env.IMAGE_CANVAS }}:$VERSION
|
||||
|
||||
- name: Smoke test (PR only)
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
docker run -d --network host \
|
||||
--name test-canvas \
|
||||
${{ env.IMAGE_CANVAS }}:${{ github.sha }}
|
||||
trap "docker rm -f test-canvas" EXIT
|
||||
for i in $(seq 1 15); do
|
||||
if curl -sf http://localhost:3000/health > /dev/null 2>&1; then
|
||||
echo "Canvas healthy"
|
||||
exit 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
echo "Health check timeout"
|
||||
exit 1
|
||||
+7
-7
@@ -2,9 +2,9 @@
|
||||
# Builds the MCP server with SQLite persistence
|
||||
|
||||
# Stage 1: Build backend (TypeScript compilation + native modules)
|
||||
FROM node:20-slim AS builder
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*
|
||||
RUN apk add --no-cache python3 make g++
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -16,12 +16,12 @@ COPY tsconfig.json ./
|
||||
RUN npm run build:server
|
||||
|
||||
# Stage 2: Production MCP Server
|
||||
FROM node:20-slim AS production
|
||||
FROM node:20-alpine AS production
|
||||
|
||||
RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*
|
||||
RUN apk add --no-cache python3 make g++
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs && \
|
||||
adduser --system --uid 1001 --gid 1001 nodejs
|
||||
RUN addgroup -S -g 1001 nodejs && \
|
||||
adduser -S -u 1001 -G nodejs nodejs
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -29,7 +29,7 @@ COPY package*.json ./
|
||||
RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev
|
||||
|
||||
# Remove build tools after native modules are compiled
|
||||
RUN apt-get purge -y python3 make g++ && apt-get autoremove -y
|
||||
RUN apk del python3 make g++
|
||||
|
||||
COPY --from=builder /app/dist ./dist
|
||||
|
||||
|
||||
+12
-9
@@ -1,11 +1,14 @@
|
||||
# Dockerfile for Canvas Server (Optional)
|
||||
# Provides the web interface, REST API, and SQLite persistence
|
||||
# Provides the web interface, REST API, and SQLite persistence (Alpine)
|
||||
|
||||
# Stage 1: Build frontend
|
||||
FROM node:20-slim AS frontend-builder
|
||||
FROM node:20-alpine AS frontend-builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ARG CUSTOM_VITE_WS_URL
|
||||
ENV CUSTOM_VITE_WS_URL=$CUSTOM_VITE_WS_URL
|
||||
|
||||
COPY package*.json ./
|
||||
RUN --mount=type=cache,target=/root/.npm npm ci --ignore-scripts
|
||||
|
||||
@@ -14,9 +17,9 @@ COPY vite.config.js ./
|
||||
RUN npm run build:frontend
|
||||
|
||||
# Stage 2: Build backend (TypeScript compilation + native modules)
|
||||
FROM node:20-slim AS backend-builder
|
||||
FROM node:20-alpine AS backend-builder
|
||||
|
||||
RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*
|
||||
RUN apk add --no-cache python3 make g++
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -28,12 +31,12 @@ COPY tsconfig.json ./
|
||||
RUN npm run build:server
|
||||
|
||||
# Stage 3: Production Canvas Server
|
||||
FROM node:20-slim AS production
|
||||
FROM node:20-alpine AS production
|
||||
|
||||
RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*
|
||||
RUN apk add --no-cache python3 make g++
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs && \
|
||||
adduser --system --uid 1001 --gid 1001 nodejs
|
||||
RUN addgroup -S -g 1001 nodejs && \
|
||||
adduser -S -u 1001 -G nodejs nodejs
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -41,7 +44,7 @@ COPY package*.json ./
|
||||
RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev
|
||||
|
||||
# Remove build tools after native modules are compiled
|
||||
RUN apt-get purge -y python3 make g++ && apt-get autoremove -y
|
||||
RUN apk del python3 make g++
|
||||
|
||||
COPY --from=backend-builder /app/dist ./dist
|
||||
COPY --from=frontend-builder /app/dist/frontend ./dist/frontend
|
||||
|
||||
@@ -15,6 +15,8 @@ services:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.canvas
|
||||
args:
|
||||
- CUSTOM_VITE_WS_URL=${CUSTOM_VITE_WS_URL:-excalidraw.forkless.com}
|
||||
image: celstnblacc/excalidraw-mcp-sentinel-canvas:latest
|
||||
container_name: mcp-excalidraw-canvas
|
||||
ports:
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
# Agent Handoff: ARM64 Docker Build & Collaboration Setup
|
||||
|
||||
## Overview
|
||||
|
||||
Gitea server at `gitea.forkless.com` (RPi 5, native ARM64) hosts the repo
|
||||
`forkless/excalidraw-mcp-sentinel`. An ARM64 Gitea Actions runner is registered
|
||||
on the Gitea server itself (labels: `arm64`, `ubuntu-latest`). Runner #1.
|
||||
|
||||
Local workspace: `/home/pe1085/development/excalidraw-mcp-sentinel`
|
||||
Remote: `https://gitea.forkless.com/forkless/excalidraw-mcp-sentinel`
|
||||
|
||||
---
|
||||
|
||||
## What Was Done
|
||||
|
||||
### 1. Gitea Actions Workflow
|
||||
`.gitea/workflows/docker.yml` — builds both Docker images on ARM64 runner:
|
||||
- `gitea.forkless.com/forkless/excalidraw-mcp-sentinel:latest`
|
||||
- `gitea.forkless.com/forkless/excalidraw-mcp-sentinel-canvas:latest`
|
||||
|
||||
Triggers on push to `main` (paths: Dockerfile, src/**, frontend/**, .gitea/**),
|
||||
PRs, and `workflow_dispatch`. **Avoid empty commits** — they don't match the
|
||||
`paths` filter and won't trigger a build.
|
||||
|
||||
### 2. Alpine Base Images
|
||||
Both `Dockerfile` and `Dockerfile.canvas` changed from `node:20-slim` to
|
||||
`node:20-alpine`. Build tools via `apk add --no-cache python3 make g++`.
|
||||
|
||||
### 3. AMD64 Runner Removed
|
||||
There was a local x86_64 Gitea Actions runner (`wsl-runner`, id=1) on this
|
||||
machine that competed with the ARM64 runner. It was stopped and its `.runner`
|
||||
file deleted from `/home/pe1085/development/gitea-test/`. It had
|
||||
`ubuntu-latest:docker://catthehacker/ubuntu:full-latest` — would build amd64
|
||||
images that can't run on ARM64. **Do not re-register it.** Only the RPi 5
|
||||
ARM64 runner should exist.
|
||||
|
||||
### 4. Frontend WebSocket URL — `CUSTOM_VITE_WS_URL`
|
||||
**New env var** `CUSTOM_VITE_WS_URL` controls the frontend's WebSocket target.
|
||||
Hardcoded `wss://excalidraw.forkless.com` replaced with a Vite build-time define.
|
||||
|
||||
**Where:**
|
||||
- `vite.config.js` — reads `process.env.CUSTOM_VITE_WS_URL`, defaults to
|
||||
`wss://excalidraw.forkless.com`, exposes as `import.meta.env.CUSTOM_VITE_WS_URL`
|
||||
- `frontend/src/App.tsx` — uses `import.meta.env.CUSTOM_VITE_WS_URL`
|
||||
|
||||
**URL normalization** (auto handles user input):
|
||||
- `excalidraw.forkless.com` → `wss://excalidraw.forkless.com`
|
||||
- `wss://excalidraw.forkless.com/socket.io/foo` → `wss://excalidraw.forkless.com`
|
||||
- `ws://dev.local:5173` → `ws://dev.local:5173` (preserves ws://)
|
||||
- `https://example.com:3002/path` → `wss://example.com:3002`
|
||||
- `10.0.101.100` → `wss://10.0.101.100`
|
||||
- `[::1]:3000` → `wss://[::1]:3000`
|
||||
- Port is preserved if present, omitted if absent.
|
||||
|
||||
### 5. `.env.example`
|
||||
Created at repo root. Documents all key env vars.
|
||||
```env
|
||||
CUSTOM_VITE_WS_URL=excalidraw.forkless.com # auto-normalized
|
||||
CANVAS_PORT=3000
|
||||
HOST=0.0.0.0
|
||||
EXCALIDRAW_API_KEY=
|
||||
ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000
|
||||
EXCALIDRAW_DB_PATH=~/.excalidraw-mcp/excalidraw.db
|
||||
LOG_LEVEL=info
|
||||
```
|
||||
|
||||
### 6. Docker Build Integration
|
||||
`Dockerfile.canvas` frontend-builder stage accepts `ARG CUSTOM_VITE_WS_URL`
|
||||
and sets it as `ENV` so Vite picks it up at build time.
|
||||
|
||||
`docker-compose.yml` canvas service passes it as build arg:
|
||||
```yaml
|
||||
args:
|
||||
- CUSTOM_VITE_WS_URL=${CUSTOM_VITE_WS_URL:-excalidraw.forkless.com}
|
||||
```
|
||||
|
||||
To override at build time:
|
||||
```bash
|
||||
docker compose build --build-arg CUSTOM_VITE_WS_URL=wss://my-server.com canvas
|
||||
# or
|
||||
CUSTOM_VITE_WS_URL=my-server.com docker compose build canvas
|
||||
```
|
||||
|
||||
### 7. Reverse Proxy Config (NPM v2.15.1 / OpenResty)
|
||||
The server at `excalidraw.forkless.com` runs behind Nginx Proxy Manager.
|
||||
**Critical settings for WebSocket to work:**
|
||||
- **WebSockets Support** toggle enabled in NPM
|
||||
- The WebSocket upgrade headers (`Upgrade`, `Connection`) are forwarded
|
||||
- **Do NOT** add custom nginx proxy config in the Advanced tab unless the
|
||||
toggle alone doesn't work — the toggle handles it. Custom config can
|
||||
conflict and break NPM entirely (requires container restart to recover).
|
||||
|
||||
### 8. `trust proxy` in Express
|
||||
`src/server.ts` line 43:
|
||||
```ts
|
||||
app.set('trust proxy', 1);
|
||||
```
|
||||
Required because NPM sets `X-Forwarded-For` and express-rate-limit rejects
|
||||
requests without this setting.
|
||||
|
||||
### 9. `ALLOWED_ORIGINS` Env Var
|
||||
The `verifyWsClient()` in `src/security.ts` checks `Origin` header against
|
||||
`ALLOWED_ORIGINS`. Default: `http://localhost:3000`, `http://127.0.0.1:3000`.
|
||||
**Must include `https://excalidraw.forkless.com` for production.**
|
||||
|
||||
### 10. Gitea Actions Secrets
|
||||
Set in Gitea repo → Settings → Actions → Secrets:
|
||||
- `GITEAUSER` = `forkless`
|
||||
- `GITEATOKEN` = Gitea PAT with `write:repository` scope
|
||||
|
||||
### 11. `collabServerUrl` Prop (Removed)
|
||||
`collabServerUrl` and `onCollabDialogOpen` props were removed from App.tsx.
|
||||
These are **not documented/working props** in `@excalidraw/excalidraw` v0.18.x
|
||||
and were ignored by the library. The collab URL is hardcoded in the npm bundle
|
||||
and connects to `wss://oss-collab.excalidraw.com` by default — this is fine.
|
||||
|
||||
### 12. CORS Flood Suppression (`og-image-3.png`)
|
||||
|
||||
**Problem:** `@excalidraw/excalidraw` v0.18 internally fetches
|
||||
`https://excalidraw.com/og-image-3.png` (social-share preload) via `fetch()` on
|
||||
mount. On non-excalidraw.com origins, the server's CORS header doesn't match →
|
||||
6× CORS errors flood the console, drowning out real logs.
|
||||
|
||||
**Fix:** fetch interceptor in `frontend/index.html` `<head>`.
|
||||
Returns 204 No Content immediately for any `fetch()` whose URL contains
|
||||
`og-image-3.png`. Request never leaves the browser → no CORS preflight →
|
||||
no error.
|
||||
|
||||
```html
|
||||
<script>
|
||||
window.fetch = new Proxy(window.fetch, {
|
||||
apply(target, thisArg, args) {
|
||||
var url = args[0];
|
||||
if (typeof url === 'string' && url.indexOf('og-image-3.png') !== -1) {
|
||||
return Promise.resolve(new Response(null, { status: 204 }));
|
||||
}
|
||||
return Reflect.apply(target, thisArg, args);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
**Why not nginx:** The browser fetches directly from `excalidraw.com`, not
|
||||
through the fork's reverse proxy. Nginx on the fork domain never sees the
|
||||
request. A DNS-level intercept (pointing `excalidraw.com` at your own nginx)
|
||||
would work, but requires SSL certs for `excalidraw.com` and control over DNS
|
||||
resolution — not available in a hosted web-editor NPM setup.
|
||||
|
||||
**Scope:** Single hardcoded URL inside the npm package — no other references in
|
||||
the repo's own code (`src/`, `frontend/`, configs, docs — 111 files searched, 0
|
||||
hits).
|
||||
|
||||
**To verify:** Open browser console after deploy — zero CORS errors from
|
||||
`og-image-3.png`.
|
||||
|
||||
### 13. MCP Server Fixes
|
||||
|
||||
**Code changes in `src/index.ts`:**
|
||||
|
||||
1. **Auto-title junk removed** (`create_element` handler) — no longer auto-injects "Title"/"Text here" on containers when `text` is provided. Card layout still works via explicit `title`+`subtitle` params.
|
||||
|
||||
2. **X-Tenant-Id header scoped to local** — `canvasHeaders()` sends `X-Tenant-Id` only when `EXPRESS_SERVER_URL` is unset (local canvas). Remote canvases use default tenant routing.
|
||||
|
||||
3. **Skip local canvas server** — `startCanvasServer()` is skipped entirely when `EXPRESS_SERVER_URL` points to a remote host. Prevents port conflicts.
|
||||
|
||||
4. **MCP process stability** — `stdin.on('close')` handler uses `setTimeout(shutdown, 1000)` instead of immediate `process.exit(0)`. Prevents crash on transient disconnects during CodeWhale init.
|
||||
|
||||
**MCP config (`~/.codewhale/mcp.json`):**
|
||||
- Server registered as `excalidraw` with stdio transport
|
||||
- Points to `https://excalidraw.forkless.com` (production)
|
||||
- Uses `/tmp/excalidraw-mcp.db` for local DB (separate from production)
|
||||
|
||||
**Known issue:** Auto-title junk still appears when production canvas runs old Docker image. The fix is in `main` but the container must be redeployed. Until then, delete stray `_title`/`_subtitle` elements via API.
|
||||
|
||||
### 14. Favicon Suppressor
|
||||
`frontend/index.html` — added `<link rel="icon" href="data:,">` to prevent 404 on `/favicon.ico`. Combined with the fetch interceptor (section 12), the console is now clean of network errors.
|
||||
|
||||
---
|
||||
|
||||
## What Still Needs Work
|
||||
|
||||
### 1. WebSocket Auth Hardening
|
||||
When `EXCALIDRAW_API_KEY` is set, the WS requires auth. The frontend sends
|
||||
the key in a `hello` message after receiving `auth_required`. Without the
|
||||
key, the connection drops. Consider:
|
||||
- Rate-limit WS connections by IP
|
||||
- Reject no-origin connections in `verifyWsClient` (currently allows)
|
||||
|
||||
### 2. Docker Image Tags
|
||||
Only `:latest` and `:$sha` are pushed. Version tags (`v*`) trigger is in
|
||||
the workflow but untested.
|
||||
|
||||
### 3. Production Container Needs Redeploy
|
||||
The auto-title fix (section 13) is committed and the Docker image is built,
|
||||
but the running container at `excalidraw.forkless.com` still runs the old
|
||||
image. Until redeployed, elements created via REST API batch will have
|
||||
stray `_title`/`_subtitle` junk elements. Delete them via:
|
||||
```bash
|
||||
curl -s https://excalidraw.forkless.com/api/elements | python3 -c "import sys,json;d=json.load(sys.stdin);[print(e['id']) for e in d['elements'] if e.get('text') in ('Title','Text here')]" | xargs -I{} curl -s -X DELETE "https://excalidraw.forkless.com/api/elements/{}"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Credentials & Access
|
||||
|
||||
- **Gitea API token**: stored in `~/.profile` as `GITEA_TOKEN`
|
||||
- **Git credentials**: stored via `git credential-store` for
|
||||
`gitea.forkless.com` — username `forkless`, password = Gitea PAT
|
||||
- To use the token in exec_shell:
|
||||
`source ~/.profile && echo "$GITEA_TOKEN"` (DO NOT source `~/.profile`
|
||||
if the user objects — but the token is stored there)
|
||||
- Alternative: read via git credential helper:
|
||||
`TOKEN=$(git credential fill <<< $'protocol=https\nhost=gitea.forkless.com\npath=/forkless/excalidraw-mcp-sentinel.git\n' 2>/dev/null | grep "^password=" | cut -d= -f2)`
|
||||
|
||||
### Git Credential Helper Fix
|
||||
The system had a broken `credential.helper` for `gitea.forkless.com` pointing
|
||||
to `!tea login helper` — but the binary is `tea-cli`, not `tea`. Fixed with:
|
||||
```bash
|
||||
git config --global --replace-all credential.https://gitea.forkless.com.helper '!tea-cli login helper'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Commands
|
||||
|
||||
```bash
|
||||
# Check latest build
|
||||
TOKEN=$(git credential fill <<< $'protocol=https\nhost=gitea.forkless.com\npath=/forkless/excalidraw-mcp-sentinel.git\n' 2>/dev/null | grep "^password=" | cut -d= -f2) && curl -s -H "Authorization: token $TOKEN" "https://gitea.forkless.com/api/v1/repos/forkless/excalidraw-mcp-sentinel/actions/runs?limit=5" | python3 -c "import sys,json;d=json.load(sys.stdin);[print(f'Run {r[\"id\"]}: {r[\"status\"]:>10} {r.get(\"conclusion\",\"-\"):>10} {r[\"head_sha\"][:12]}') for r in d.get('workflow_runs',[])]"
|
||||
|
||||
# Check Docker image manifest
|
||||
docker manifest inspect gitea.forkless.com/forkless/excalidraw-mcp-sentinel:latest
|
||||
|
||||
# Push (credential helper auto-handles auth)
|
||||
git push origin main
|
||||
|
||||
# Trigger dispatch (if paths filter blocks push trigger)
|
||||
TOKEN=$(git credential fill <<< $'protocol=https\nhost=gitea.forkless.com\npath=/forkless/excalidraw-mcp-sentinel.git\n' 2>/dev/null | grep "^password=" | cut -d= -f2) && curl -s -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" "https://gitea.forkless.com/api/v1/repos/forkless/excalidraw-mcp-sentinel/actions/workflows/docker.yml/dispatches" -d '{"ref":"main"}'
|
||||
|
||||
# Build canvas with custom WS URL
|
||||
CUSTOM_VITE_WS_URL=my-ws-server.com docker compose build canvas
|
||||
```
|
||||
@@ -3,6 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" href="data:,">
|
||||
<title>Excalidraw POC - Backend API Integration</title>
|
||||
<style>
|
||||
body {
|
||||
@@ -725,6 +726,20 @@
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
<!-- CORS flood suppressor: intercepts @excalidraw/excalidraw internal fetch to
|
||||
https://excalidraw.com/og-image-3.png which triggers 6× CORS errors on
|
||||
non-excalidraw.com origins. Returns 204 immediately, never leaves browser. -->
|
||||
<script>
|
||||
window.fetch = new Proxy(window.fetch, {
|
||||
apply(target, thisArg, args) {
|
||||
var url = args[0];
|
||||
if (typeof url === 'string' && url.indexOf('og-image-3.png') !== -1) {
|
||||
return Promise.resolve(new Response(null, { status: 204 }));
|
||||
}
|
||||
return Reflect.apply(target, thisArg, args);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
+13
-2
@@ -435,8 +435,19 @@ function App(): JSX.Element {
|
||||
return
|
||||
}
|
||||
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const wsUrl = `${protocol}//${window.location.host}`
|
||||
let rawUrl = import.meta.env.CUSTOM_VITE_WS_URL || 'wss://excalidraw.forkless.com'
|
||||
let scheme = 'wss://'
|
||||
try {
|
||||
let parseable = rawUrl
|
||||
if (rawUrl.startsWith('ws://')) { scheme = 'ws://'; parseable = rawUrl.replace(/^ws:\/\//, 'http://') }
|
||||
else if (rawUrl.startsWith('wss://')) { parseable = rawUrl.replace(/^wss:\/\//, 'https://') }
|
||||
const parsed = new URL(parseable)
|
||||
rawUrl = scheme + parsed.host
|
||||
} catch {
|
||||
// Bare hostname/IP — prepend wss://
|
||||
rawUrl = 'wss://' + rawUrl
|
||||
}
|
||||
const wsUrl = rawUrl
|
||||
|
||||
websocketRef.current = new WebSocket(wsUrl)
|
||||
|
||||
|
||||
+24
-17
@@ -144,9 +144,13 @@ interface SyncResponse {
|
||||
function canvasHeaders(extra?: Record<string, string>): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-Id': dbGetActiveTenantId(),
|
||||
...extra
|
||||
};
|
||||
// Only send tenant header when targeting local canvas (EXPRESS_SERVER_URL unset).
|
||||
// Remote servers (e.g. production) handle routing via default tenant.
|
||||
if (!process.env.EXPRESS_SERVER_URL) {
|
||||
headers['X-Tenant-Id'] = dbGetActiveTenantId();
|
||||
}
|
||||
// Forward API key to canvas when auth is enabled — required for two-service
|
||||
// Docker deployments where canvas runs with EXCALIDRAW_API_KEY set.
|
||||
const apiKey = process.env.EXCALIDRAW_API_KEY;
|
||||
@@ -1090,16 +1094,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest)
|
||||
const id = customId || generateId();
|
||||
const normalizedFont = normalizeFontFamily(elementProps.fontFamily);
|
||||
|
||||
// Auto-populate title+subtitle for container types unless text is explicitly set
|
||||
// Use explicit text if provided, otherwise fall back to title/title+subtitle card layout
|
||||
const CONTAINER_TYPES = new Set(['rectangle', 'ellipse', 'diamond']);
|
||||
const isContainer = CONTAINER_TYPES.has(params.type);
|
||||
const hasExplicitText = elementProps.text !== undefined;
|
||||
const effectiveTitle = title ?? (isContainer && !hasExplicitText ? 'Title' : undefined);
|
||||
const effectiveSubtitle = subtitle ?? (isContainer && !hasExplicitText && effectiveTitle ? 'Description' : undefined);
|
||||
|
||||
const effectiveText = effectiveTitle ?? elementProps.text;
|
||||
const effectiveFontSize = effectiveTitle ? (titleFontSize ?? 24) : (elementProps.fontSize ?? USER_PREFS.fontSize);
|
||||
const effectiveFontFamily = effectiveTitle
|
||||
const effectiveText = title ?? elementProps.text;
|
||||
const effectiveSubtitle = isContainer && subtitle ? subtitle : undefined;
|
||||
const effectiveFontSize = title ? (titleFontSize ?? 24) : (elementProps.fontSize ?? USER_PREFS.fontSize);
|
||||
const effectiveFontFamily = title
|
||||
? (normalizeFontFamily(titleFontFamily) ?? USER_PREFS.fontFamily)
|
||||
: (normalizedFont ?? USER_PREFS.fontFamily);
|
||||
|
||||
@@ -1128,7 +1129,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest)
|
||||
const excalidrawElement = convertTextToLabel(element);
|
||||
|
||||
// Card layout: title (bound) + subtitle (grouped standalone text)
|
||||
const groupId = (effectiveTitle && effectiveSubtitle && isContainer) ? generateId() : undefined;
|
||||
const groupId = (title && effectiveSubtitle && isContainer) ? generateId() : undefined;
|
||||
|
||||
// Add groupId to container if using card layout
|
||||
if (groupId) {
|
||||
@@ -2911,12 +2912,17 @@ async function runServer(): Promise<void> {
|
||||
|
||||
applyTenant(workspacePath);
|
||||
|
||||
try {
|
||||
await startCanvasServer();
|
||||
logger.info('Canvas server started — lifecycle managed by MCP process');
|
||||
} catch (canvasError) {
|
||||
logger.warn('Canvas server failed to start:', (canvasError as Error).message);
|
||||
logger.warn('MCP tools will work without real-time canvas sync');
|
||||
// Skip local canvas server when targeting a remote EXPRESS_SERVER_URL
|
||||
if (!process.env.EXPRESS_SERVER_URL) {
|
||||
try {
|
||||
await startCanvasServer();
|
||||
logger.info('Canvas server started — lifecycle managed by MCP process');
|
||||
} catch (canvasError) {
|
||||
logger.warn('Canvas server failed to start:', (canvasError as Error).message);
|
||||
logger.warn('MCP tools will work without real-time canvas sync');
|
||||
}
|
||||
} else {
|
||||
logger.info('Remote canvas server configured, skipping local canvas start');
|
||||
}
|
||||
|
||||
// HTTP mode: one shared process serves many clients over Streamable HTTP.
|
||||
@@ -2997,7 +3003,8 @@ async function runServer(): Promise<void> {
|
||||
}
|
||||
|
||||
server.onclose = shutdown;
|
||||
process.stdin.on('close', shutdown);
|
||||
// Delay exit on stdin close — prevents crash if CodeWhale briefly disconnects during init
|
||||
process.stdin.on('close', () => setTimeout(shutdown, 1000));
|
||||
process.on('SIGTERM', shutdown);
|
||||
process.on('SIGINT', shutdown);
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const app: Application = express();
|
||||
app.set('trust proxy', 1);
|
||||
const httpServer = createServer(app);
|
||||
const wss = new WebSocketServer({ server: httpServer, verifyClient: verifyWsClient });
|
||||
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
const wsUrl = process.env.CUSTOM_VITE_WS_URL || 'wss://excalidraw.forkless.com';
|
||||
|
||||
export default defineConfig({
|
||||
root: 'frontend',
|
||||
plugins: [react()],
|
||||
define: {
|
||||
'import.meta.env.CUSTOM_VITE_WS_URL': JSON.stringify(wsUrl),
|
||||
},
|
||||
build: {
|
||||
outDir: '../dist/frontend',
|
||||
emptyOutDir: true,
|
||||
|
||||
Reference in New Issue
Block a user