fix: 메인 채팅 WebSocket에 Wake Lock 적용 — 외부 접속 시 잦은 끊김 대응

사용자가 "어제 집에서는 괜찮았는데 지금 외부에서 연결하니깐 자꾸
끊어지네"라고 보고. 서버는 이미 30초마다 ping을 보내 프록시/OS 유휴
타임아웃(92초 패턴)을 피하는 방어 로직이 있고(server.ts wsPingInterval),
60초 무응답이면 서버가 직접 연결을 끊는다.

메인 채팅 페이지엔 이 ping에 응답할 JS 타이머를 살려두는 장치가 없었다.
음성통화 페이지(voice-call.js)는 정확히 같은 증상(폰 화면 꺼짐 → 탭
백그라운드 스로틀링 → 소켓이 서버 ping에 응답 못 함 → 끊김)을 07-09에
Wake Lock으로 이미 고쳤는데, 메인 채팅 쪽엔 이 조치가 빠져 있었다 —
집에서는 화면이 잘 안 꺼지니 덜 겪고, 외부에서 폰으로 쓸 때 화면이
자주 잠기니 더 자주 겪는 것과도 맞아떨어진다.

voice-call.js와 같은 패턴을 그대로 적용: WS가 열릴 때 Wake Lock을
요청하고, visibilitychange로 탭이 다시 보일 때 재요청(Wake Lock은 탭이
숨겨지면 자동 해제되므로), WS가 닫히면 명시적으로 해제.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
kim
2026-08-10 14:19:40 +09:00
co-authored by Claude Opus 5
parent 5d88d34b33
commit 82283c4dc2
+31 -1
View File
@@ -2,6 +2,7 @@
const _DBG = false; // set true to enable streaming/editor debug logs
const API = window.location.origin || 'http://127.0.0.1:18789';
let ws = null;
let wsWakeLock = null;
let selectedJobId = null;
let allJobs = [];
let pollInterval = null;
@@ -349,11 +350,36 @@ function updateAgentMode() {
// ---- Chat ----
// ---- WebSocket ----
// Screen Wake Lock keeps this main WS connection alive on mobile. Without it, the OS
// screen-timeout (idle reading a reply, thinking before typing) turns the screen off, the
// tab gets backgrounded, and the browser throttles/suspends it — the socket misses the
// server's 30s pings, gets terminated after ~60s, and the client sees a "connection keeps
// dropping" pattern that never shows up on a desktop/always-on connection. Same root cause
// as the voice-call page's drop bug (fixed 2026-07-09 with the same pattern, see
// voice-call.js) — added here 2026-08-10 after the same symptom showed up on the main chat
// while used away from home wifi. A Wake Lock is auto-released whenever the tab loses
// visibility, so it has to be re-requested once the tab is visible again.
async function acquireWsWakeLock() {
if (!('wakeLock' in navigator)) return;
try {
wsWakeLock = await navigator.wakeLock.request('screen');
wsWakeLock.addEventListener('release', () => { wsWakeLock = null; });
} catch (e) {
console.warn('[app] wsWakeLock request failed:', e.message);
}
}
document.addEventListener('visibilitychange', () => {
if (ws && ws.readyState === WebSocket.OPEN && wsWakeLock === null && document.visibilityState === 'visible') {
acquireWsWakeLock();
}
});
function connectWS() {
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
const token = getAuthToken();
const qs = token ? `?token=${encodeURIComponent(token)}` : '';
ws = new WebSocket(`${proto}://${API.replace(/^https?:\/\//, '')}/ws${qs}`);
ws.onopen = () => { acquireWsWakeLock(); };
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (msg.type === 'jobs_update') {
@@ -514,7 +540,11 @@ function connectWS() {
if (agentId) pauseAgentExecution(agentId, msg.reason);
}
};
ws.onclose = () => setTimeout(connectWS, 2000);
ws.onclose = () => {
try { wsWakeLock && wsWakeLock.release(); } catch {}
wsWakeLock = null;
setTimeout(connectWS, 2000);
};
ws.onerror = () => ws.close();
}