Files
droidclaw/server/src/index.ts
Sanju Sivalingam c395f9d83e feat: add DB persistence, real-time WebSocket, goal preprocessor, and Android companion app
- Add device/session/step DB persistence in server agent loop
- Add goal preprocessor for compound goals (e.g., "open YouTube and search X")
- Add step-level logging to agent loop
- Fix dashboard WebSocket auth (direct DB token lookup instead of auth.api)
- Fix web layout to use locals.session.token instead of cookie
- Add dashboard-ws.svelte.ts WebSocket store with auto-reconnect
- Rewrite devices page with direct DB queries and real-time updates
- Add device detail page with live step display and session history
- Add Android companion app resources, themes, and screen capture consent

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 20:12:41 +05:30

99 lines
2.8 KiB
TypeScript

import { Hono } from "hono";
import { cors } from "hono/cors";
import { auth } from "./auth.js";
import { env } from "./env.js";
import { handleDeviceMessage, handleDeviceClose } from "./ws/device.js";
import {
handleDashboardMessage,
handleDashboardClose,
} from "./ws/dashboard.js";
import type { WebSocketData } from "./ws/sessions.js";
import { devices } from "./routes/devices.js";
import { goals } from "./routes/goals.js";
import { health } from "./routes/health.js";
const app = new Hono();
// CORS for dashboard
app.use(
"*",
cors({
origin: env.CORS_ORIGIN,
allowHeaders: ["Content-Type", "Authorization"],
allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
credentials: true,
})
);
// Better Auth handler
app.on(["POST", "GET"], "/api/auth/*", (c) => {
return auth.handler(c.req.raw);
});
// REST routes
app.route("/devices", devices);
app.route("/goals", goals);
app.route("/health", health);
// Start server with WebSocket support
const server = Bun.serve<WebSocketData>({
port: env.PORT,
fetch(req, server) {
const url = new URL(req.url);
// WebSocket upgrade for device connections
if (url.pathname === "/ws/device") {
const upgraded = server.upgrade(req, {
data: { path: "/ws/device" as const, authenticated: false },
});
if (upgraded) return undefined;
return new Response("WebSocket upgrade failed", { status: 400 });
}
// WebSocket upgrade for dashboard connections
if (url.pathname === "/ws/dashboard") {
const upgraded = server.upgrade(req, {
data: { path: "/ws/dashboard" as const, authenticated: false },
});
if (upgraded) return undefined;
return new Response("WebSocket upgrade failed", { status: 400 });
}
// Non-WebSocket requests go to Hono
return app.fetch(req);
},
websocket: {
idleTimeout: 120,
sendPings: true,
open(ws) {
console.log(`WebSocket opened: ${ws.data.path}`);
},
message(ws, message) {
const raw =
typeof message === "string"
? message
: new TextDecoder().decode(message);
if (ws.data.path === "/ws/device") {
handleDeviceMessage(ws, raw).catch((err) => {
console.error(`Device message handler error: ${err}`);
});
} else if (ws.data.path === "/ws/dashboard") {
handleDashboardMessage(ws, raw).catch((err) => {
console.error(`Dashboard message handler error: ${err}`);
});
}
},
close(ws, code, reason) {
console.log(`WebSocket closed: ${ws.data.path} device=${ws.data.deviceId ?? "unknown"} code=${code} reason=${reason}`);
if (ws.data.path === "/ws/device") {
handleDeviceClose(ws);
} else if (ws.data.path === "/ws/dashboard") {
handleDashboardClose(ws);
}
},
},
});
console.log(`Server running on port ${server.port}`);