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>
This commit is contained in:
Sanju Sivalingam
2026-02-17 20:12:41 +05:30
parent ea707af83e
commit c395f9d83e
101 changed files with 8824 additions and 82 deletions

View File

@@ -0,0 +1,117 @@
import { env } from '$env/dynamic/public';
const WS_URL = env.PUBLIC_SERVER_WS_URL || 'ws://localhost:8080';
export interface DashboardDevice {
deviceId: string;
name: string;
status: 'online' | 'offline';
}
export interface StepEvent {
sessionId: string;
step: number;
action: Record<string, unknown>;
reasoning: string;
}
export interface GoalStartedEvent {
sessionId: string;
goal: string;
deviceId: string;
}
export interface GoalCompletedEvent {
sessionId: string;
success: boolean;
stepsUsed: number;
}
type MessageHandler = (msg: Record<string, unknown>) => void;
class DashboardWebSocket {
private ws: WebSocket | null = null;
private handlers = new Set<MessageHandler>();
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
private sessionToken: string | null = null;
connected = $state(false);
connect(sessionToken: string) {
this.sessionToken = sessionToken;
this.doConnect();
}
private doConnect() {
if (!this.sessionToken) return;
if (this.ws?.readyState === WebSocket.OPEN) return;
try {
this.ws = new WebSocket(`${WS_URL}/ws/dashboard`);
} catch {
this.scheduleReconnect();
return;
}
this.ws.onopen = () => {
this.ws?.send(JSON.stringify({ type: 'auth', token: this.sessionToken }));
};
this.ws.onmessage = (event) => {
try {
const msg = JSON.parse(event.data) as Record<string, unknown>;
if (msg.type === 'auth_ok') {
this.connected = true;
return;
}
if (msg.type === 'auth_error') {
console.error('[DashboardWS] Auth failed:', msg.message);
return;
}
for (const handler of this.handlers) {
handler(msg);
}
} catch {
// ignore parse errors
}
};
this.ws.onclose = () => {
this.connected = false;
this.scheduleReconnect();
};
this.ws.onerror = () => {
this.connected = false;
};
}
private scheduleReconnect() {
if (this.reconnectTimer) return;
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null;
this.doConnect();
}, 3000);
}
subscribe(handler: MessageHandler) {
this.handlers.add(handler);
return () => this.handlers.delete(handler);
}
disconnect() {
this.sessionToken = null;
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
if (this.ws) {
this.ws.onclose = null;
this.ws.close();
this.ws = null;
}
this.connected = false;
}
}
export const dashboardWs = new DashboardWebSocket();