diff --git a/backend/app/main.py b/backend/app/main.py
index 265b4d0..3eaf173 100755
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -240,6 +240,9 @@ async def websocket_analyze(websocket: WebSocket):
)
finally:
heartbeat_task.cancel()
+ elif data.get("type") == "ping":
+ # Client keepalive ping — respond with pong
+ await manager.send_message(client_id, {"type": "pong"})
else:
logger.warning(f"[MAIN] Unknown message type: {data.get('type')}")
await manager.send_message(client_id, {
diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg
index 8150704..76f688d 100644
--- a/frontend/public/favicon.svg
+++ b/frontend/public/favicon.svg
@@ -1,4 +1,12 @@
-
+
+
\ No newline at end of file
diff --git a/frontend/services/geminiService.ts b/frontend/services/geminiService.ts
index 5b65cb8..c3d1132 100755
--- a/frontend/services/geminiService.ts
+++ b/frontend/services/geminiService.ts
@@ -53,7 +53,24 @@ export const analyzeProof = async (
const ws = new WebSocket(WS_URL);
let resolved = false;
+ // Send client→server pings every 15s to keep proxy idle timers alive
+ let pingInterval: ReturnType | null = null;
+ const startPing = () => {
+ pingInterval = setInterval(() => {
+ if (ws.readyState === WebSocket.OPEN) {
+ ws.send(JSON.stringify({ type: 'ping' }));
+ }
+ }, 15000);
+ };
+ const stopPing = () => {
+ if (pingInterval !== null) {
+ clearInterval(pingInterval);
+ pingInterval = null;
+ }
+ };
+
ws.onopen = () => {
+ startPing();
// Convert file to base64 and send
const reader = new FileReader();
reader.onloadend = () => {
@@ -114,9 +131,14 @@ export const analyzeProof = async (
onAgentUpdate('Summary');
break;
+ case 'pong':
+ // Response to our ping — ignore
+ break;
+
case 'complete':
// Analysis complete - resolve with full result
resolved = true;
+ stopPing();
ws.close();
resolve({
review: message.result as AgentReview,
@@ -138,6 +160,7 @@ export const analyzeProof = async (
case 'error':
// Error occurred
resolved = true;
+ stopPing();
ws.close();
reject(new Error(message.message || 'Analysis failed'));
break;
@@ -148,6 +171,7 @@ export const analyzeProof = async (
};
ws.onerror = () => {
+ stopPing();
if (!resolved) {
resolved = true;
reject(new Error('WebSocket connection error. Is the backend running?'));
@@ -155,6 +179,7 @@ export const analyzeProof = async (
};
ws.onclose = (event) => {
+ stopPing();
if (!resolved && !event.wasClean) {
resolved = true;
reject(new Error('WebSocket connection closed unexpectedly'));
@@ -182,7 +207,23 @@ export const analyzeWIPProof = async (
const ws = new WebSocket(WS_URL);
let resolved = false;
+ let wipPingInterval: ReturnType | null = null;
+ const startWipPing = () => {
+ wipPingInterval = setInterval(() => {
+ if (ws.readyState === WebSocket.OPEN) {
+ ws.send(JSON.stringify({ type: 'ping' }));
+ }
+ }, 15000);
+ };
+ const stopWipPing = () => {
+ if (wipPingInterval !== null) {
+ clearInterval(wipPingInterval);
+ wipPingInterval = null;
+ }
+ };
+
ws.onopen = () => {
+ startWipPing();
const reader = new FileReader();
reader.onloadend = () => {
const base64Data = (reader.result as string).split(',')[1];
@@ -205,18 +246,20 @@ export const analyzeWIPProof = async (
try {
const message = JSON.parse(event.data);
- if (message.type === 'heartbeat') {
- // Server keepalive — ignore silently
+ if (message.type === 'heartbeat' || message.type === 'pong') {
+ // Keepalive — ignore silently
} else if (message.type === 'agent_completed') {
onAgentUpdate(message.agent_name as AgentName, message.review);
} else if (message.type === 'summary') {
onAgentUpdate('Summary');
} else if (message.type === 'complete') {
resolved = true;
+ stopWipPing();
ws.close();
resolve(message.result?.leadAgentSummary || 'Analysis complete.');
} else if (message.type === 'error') {
resolved = true;
+ stopWipPing();
ws.close();
reject(new Error(message.message || 'Analysis failed'));
}
@@ -226,6 +269,7 @@ export const analyzeWIPProof = async (
};
ws.onerror = () => {
+ stopWipPing();
if (!resolved) {
resolved = true;
reject(new Error('WebSocket connection error'));
@@ -233,6 +277,7 @@ export const analyzeWIPProof = async (
};
ws.onclose = (event) => {
+ stopWipPing();
if (!resolved && !event.wasClean) {
resolved = true;
reject(new Error('Connection closed unexpectedly'));