211 lines
8.7 KiB
Markdown
211 lines
8.7 KiB
Markdown
# 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`.
|
||
|
||
---
|
||
|
||
## 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.
|
||
|
||
---
|
||
|
||
## 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
|
||
```
|