Fix WebSocket drops: add bidirectional keepalive pings
Frontend now sends client→server ping every 15s during analysis to keep the GCP LB idle timeout alive from both directions. Backend responds with pong. Previously only server→client heartbeats were sent, which didn't reset the proxy's client-side idle timer. Also updates favicon to Oliver brand mark (gold M). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e98143de55
commit
e85681b775
3 changed files with 62 additions and 6 deletions
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,12 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||
<rect width="32" height="32" rx="6" fill="#0487B6"/>
|
||||
<text x="16" y="22" font-family="Arial,sans-serif" font-size="18" font-weight="bold" fill="white" text-anchor="middle">M</text>
|
||||
</svg>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 100 100">
|
||||
<!-- Generator: Adobe Illustrator 30.2.1, SVG Export Plug-In . SVG Version: 2.1.1 Build 1) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
fill: #ffcb05;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path class="st0" d="M75.85,88.28l-.12-45.97-21.94,37.88h-7.78l-21.82-36.87v45H7.98V11.68h14.28l27.9,47.63,27.47-47.63h14.16l.23,76.61h-16.17Z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 265 B After Width: | Height: | Size: 469 B |
|
|
@ -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<typeof setInterval> | 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<typeof setInterval> | 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'));
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue