Files

242 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```