App Preview

This commit is contained in:
2026-02-22 03:35:26 +00:00
parent ab03b175e7
commit ca45301e3e
10 changed files with 638 additions and 2 deletions
+86 -1
View File
@@ -7,6 +7,7 @@ import { verify } from './servers/jwt';
import { officerdb, TokenBlacklist } from 'officerdb';
import { terminalWebsocket, initTerminalSidecars } from './servers/api/terminal/websocket';
import { piWebsocket } from './servers/api/pi/websocket';
import { findEntryBySlug, touchEntry } from './servers/api/dev-server/router';
import officerWeb from './apps/officer-web/index.html';
const { PORT = '5000' } = process.env;
@@ -15,13 +16,16 @@ type WSData = {
userId: number;
email: string;
role: string;
provider: 'terminal' | 'pi';
provider: 'terminal' | 'pi' | 'dev-server';
sandboxed: boolean;
sessionId?: string;
cwd?: string;
command?: string;
cols?: number;
rows?: number;
devServerPort?: number;
devServerSlug?: string;
wsProxyPath?: string;
};
const handlers: Record<string, any> = {
@@ -29,6 +33,56 @@ const handlers: Record<string, any> = {
pi: piWebsocket,
};
// Dev-server WebSocket proxy: bridges client WS ↔ upstream dev server WS (for HMR etc.)
type UpstreamState = { ws: WebSocket; queue: (string | Buffer)[]; ready: boolean };
const devServerUpstreams = new Map<ServerWebSocket<WSData>, UpstreamState>();
const devServerWebsocket = {
open(ws: ServerWebSocket<WSData>) {
const { devServerPort, wsProxyPath } = ws.data;
const upstream = new WebSocket(`ws://localhost:${devServerPort}${wsProxyPath}`);
const state: UpstreamState = { ws: upstream, queue: [], ready: false };
devServerUpstreams.set(ws, state);
upstream.addEventListener('open', () => {
state.ready = true;
for (const msg of state.queue) upstream.send(msg);
state.queue.length = 0;
});
upstream.addEventListener('message', (event) => {
ws.send(event.data as string | ArrayBuffer);
});
upstream.addEventListener('close', () => {
devServerUpstreams.delete(ws);
ws.close();
});
upstream.addEventListener('error', () => {
devServerUpstreams.delete(ws);
ws.close();
});
},
message(ws: ServerWebSocket<WSData>, raw: string | Buffer) {
const state = devServerUpstreams.get(ws);
if (!state) return;
if (state.ready) {
state.ws.send(raw);
} else {
state.queue.push(raw);
}
},
close(ws: ServerWebSocket<WSData>) {
const state = devServerUpstreams.get(ws);
if (state) {
state.ws.close();
devServerUpstreams.delete(ws);
}
},
};
handlers['dev-server'] = devServerWebsocket;
async function upgradeWs(req: Request, server: any, provider: 'terminal' | 'pi') {
const token = new URL(req.url).searchParams.get('token');
if (!token) return new Response('Unauthorized', { status: 401 });
@@ -60,6 +114,33 @@ async function upgradeWs(req: Request, server: any, provider: 'terminal' | 'pi')
}
}
function upgradeDevServerWs(req: Request, server: any) {
const url = new URL(req.url);
const match = url.pathname.match(/^\/api\/dev-server-proxy\/([^/]+)(\/.*)?$/);
if (!match) return new Response('Not found', { status: 404 });
const slug = match[1]!;
const entry = findEntryBySlug(slug);
if (!entry) return new Response('No dev server running', { status: 404 });
touchEntry(entry);
const wsProxyPath = match[2] || '/';
const ok = server.upgrade(req, {
data: {
userId: 0,
email: '',
role: '',
provider: 'dev-server' as const,
sandboxed: false,
devServerPort: entry.port,
devServerSlug: slug,
wsProxyPath,
},
});
if (!ok) return new Response('Upgrade failed', { status: 500 });
}
const server = serve({
port: Number(PORT),
idleTimeout: 60,
@@ -71,6 +152,10 @@ const server = serve({
if (await file.exists()) return new Response(file);
return new Response(null, { status: 404 });
},
'/api/dev-server-proxy/*': (req, server) => {
if (req.headers.get('upgrade') === 'websocket') return upgradeDevServerWs(req, server);
return honoServer.fetch(req, server);
},
'/api/terminal/ws': (req, server) => upgradeWs(req, server, 'terminal'),
'/api/pi/chat/ws': (req, server) => upgradeWs(req, server, 'pi'),
'/': officerWeb,