All checks were successful
CD / Build and push images (push) Successful in 2m59s
CI / Lint, typecheck, test (push) Successful in 2m0s
CI / Auth e2e pack (push) Successful in 2m10s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m14s
CD / Promote to Int (push) Successful in 11s
The editor now edits over the collaboration server instead of REST — the moment Dorfteich becomes collaborative (ADR 0003, realtime-collaboration.md). Web: - New `useCollabProvider` hook binds a page's Y.Doc to a HocuspocusProvider. The document loads and persists through the collab server (#35); there is no REST autosave and no REST seed (a REST seed would fork the doc lineage and duplicate content). The collab token is fetched lazily on every (re)connect via an async token function, so an expired token is replaced transparently and a permission change takes effect on the next reconnect. - Connection-state UI replaces the save indicator: connecting / connected ("Live") / reconnecting / offline, driven by provider status + navigator online state. Read-only (`ro`) tokens make the editor non-editable with a reason; an oversize-document stateless error (#35) surfaces a banner. - Removed `use-page-autosave.ts` and `yjs-base64.ts` (no longer used). API: - `PUT /pages/:id/state` is retired and returns 410 `rest_state_write_retired` (the criterion deferred here from #35). Collab is the sole writer of page state; the read paths remain. Removed the now-dead `saveState` service. e2e / CI: - The e2e static server proxies the `/collab` WebSocket upgrade (mirrors Caddy); vite dev gains a `/collab` ws proxy. The auth-e2e CI job starts the collab server and runs a new collab pack. - New `collab.spec.ts`: two browsers converge on one page (the milestone headline), and offline edits continue locally and sync on reconnect. The read-only live assertion is a `test.fixme` until real read-only grants exist — under interim access seeing and modifying coincide, so no `ro` token is issued yet (that arrives with #53). Reworked the api/trash tests and the content editor-basics test off the retired REST write path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PGdhRiwU1WRL4XxJfZYipY
110 lines
3.7 KiB
JavaScript
110 lines
3.7 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Minimal server for e2e runs: serves the built SPA (apps/web/dist) with
|
|
* SPA fallback, proxies /api/* to the api process, and proxies the /collab
|
|
* WebSocket to the collab process — mirroring what nginx + Caddy do in
|
|
* production, without a dev server that may die under CI memory pressure.
|
|
* Node builtins only.
|
|
*
|
|
* PORT=5173 API_TARGET=http://127.0.0.1:3001 COLLAB_TARGET=http://127.0.0.1:3002 \
|
|
* node scripts/e2e-static-server.mjs
|
|
*/
|
|
import { readFile } from 'node:fs/promises';
|
|
import { createServer, request as httpRequest } from 'node:http';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const DIST = path.join(path.dirname(fileURLToPath(import.meta.url)), '../apps/web/dist');
|
|
const PORT = Number(process.env.PORT ?? 5173);
|
|
const API_TARGET = new URL(process.env.API_TARGET ?? 'http://127.0.0.1:3001');
|
|
const COLLAB_TARGET = new URL(process.env.COLLAB_TARGET ?? 'http://127.0.0.1:3002');
|
|
|
|
const MIME = {
|
|
'.html': 'text/html; charset=utf-8',
|
|
'.js': 'application/javascript',
|
|
'.css': 'text/css',
|
|
'.json': 'application/json',
|
|
'.svg': 'image/svg+xml',
|
|
'.png': 'image/png',
|
|
'.ico': 'image/x-icon',
|
|
'.woff2': 'font/woff2',
|
|
};
|
|
|
|
function proxyApi(req, res) {
|
|
const upstream = httpRequest(
|
|
{
|
|
hostname: API_TARGET.hostname,
|
|
port: API_TARGET.port,
|
|
path: req.url,
|
|
method: req.method,
|
|
headers: { ...req.headers, host: `${API_TARGET.hostname}:${API_TARGET.port}` },
|
|
},
|
|
(upstreamRes) => {
|
|
res.writeHead(upstreamRes.statusCode ?? 502, upstreamRes.headers);
|
|
upstreamRes.pipe(res);
|
|
},
|
|
);
|
|
upstream.on('error', () => {
|
|
res.writeHead(502).end('api unreachable');
|
|
});
|
|
req.pipe(upstream);
|
|
}
|
|
|
|
async function serveStatic(req, res) {
|
|
const urlPath = new URL(req.url, 'http://x').pathname;
|
|
// Path traversal guard: resolve inside dist or fall back to the shell.
|
|
const filePath = path.normalize(path.join(DIST, urlPath));
|
|
const target =
|
|
filePath.startsWith(DIST) && path.extname(filePath) ? filePath : path.join(DIST, 'index.html');
|
|
try {
|
|
const body = await readFile(target);
|
|
res.writeHead(200, {
|
|
'Content-Type': MIME[path.extname(target)] ?? 'application/octet-stream',
|
|
});
|
|
res.end(body);
|
|
} catch {
|
|
res.writeHead(404).end('not found');
|
|
}
|
|
}
|
|
|
|
const server = createServer((req, res) => {
|
|
if (req.url.startsWith('/api/')) proxyApi(req, res);
|
|
else void serveStatic(req, res);
|
|
});
|
|
|
|
// Proxy the collab WebSocket (Caddy forwards /collab without stripping the
|
|
// prefix in production; mirror that here so the editor connects the same way).
|
|
server.on('upgrade', (req, socket, head) => {
|
|
if (!req.url.startsWith('/collab')) {
|
|
socket.destroy();
|
|
return;
|
|
}
|
|
const upstream = httpRequest({
|
|
hostname: COLLAB_TARGET.hostname,
|
|
port: COLLAB_TARGET.port,
|
|
path: req.url,
|
|
method: req.method,
|
|
headers: { ...req.headers, host: `${COLLAB_TARGET.hostname}:${COLLAB_TARGET.port}` },
|
|
});
|
|
upstream.on('upgrade', (upstreamRes, upstreamSocket, upstreamHead) => {
|
|
const statusLine = `HTTP/1.1 ${upstreamRes.statusCode} ${upstreamRes.statusMessage}\r\n`;
|
|
const headerLines = Object.entries(upstreamRes.headers)
|
|
.map(([key, value]) => `${key}: ${value}`)
|
|
.join('\r\n');
|
|
socket.write(`${statusLine}${headerLines}\r\n\r\n`);
|
|
if (upstreamHead?.length) socket.write(upstreamHead);
|
|
upstreamSocket.pipe(socket);
|
|
socket.pipe(upstreamSocket);
|
|
upstreamSocket.on('error', () => socket.destroy());
|
|
});
|
|
upstream.on('error', () => socket.destroy());
|
|
if (head?.length) upstream.write(head);
|
|
upstream.end();
|
|
});
|
|
|
|
server.listen(PORT, () =>
|
|
console.log(
|
|
`e2e static server on :${PORT} → api ${API_TARGET.href} collab ${COLLAB_TARGET.href}`,
|
|
),
|
|
);
|