fix: poll /health after update instead of blind setTimeout — v0.50.158 (closes #874)

Replaces blind setTimeout reload with /health polling loop. Banner shows restart status with manual Reload button. Works behind reverse proxies. 25 regression tests.
This commit is contained in:
nesquena-hermes
2026-04-22 17:51:12 -07:00
committed by GitHub
parent a72208eaf6
commit e3607855b1
3 changed files with 98 additions and 12 deletions

View File

@@ -1076,6 +1076,10 @@ function dismissReconnect() {
clearInflight();
}
async function refreshSession() {
// When the banner is in post-update restart mode, the "Reload" button
// should do a full page reload — a session refresh would just 502 while
// the server is still restarting.
if (window._restartingForUpdate) { location.reload(); return; }
dismissReconnect();
if (!S.session) return;
try {
@@ -1127,10 +1131,10 @@ async function applyUpdates(){
return;
}
}
showToast('Updated! Restarting\u2026');
showToast('Update applied — restarting');
sessionStorage.removeItem('hermes-update-checked');
sessionStorage.removeItem('hermes-update-dismissed');
setTimeout(()=>location.reload(),2500);
_waitForServerThenReload();
}catch(e){
if(errEl){errEl.textContent='Update failed: '+e.message;errEl.style.display='block';}
else showToast('Update failed: '+e.message);
@@ -1174,16 +1178,49 @@ async function forceUpdate(btn){
btn.disabled=false;btn.textContent='Force update';
return;
}
showToast('Force updated! Restarting\u2026');
showToast('Force update applied — restarting');
sessionStorage.removeItem('hermes-update-checked');
sessionStorage.removeItem('hermes-update-dismissed');
setTimeout(()=>location.reload(),2500);
_waitForServerThenReload();
}catch(e){
if(errEl){errEl.textContent='Force update failed: '+e.message;errEl.style.display='block';}
btn.disabled=false;btn.textContent='Force update';
}
}
// Poll /health after an update-triggered restart, then reload. Replaces the
// blind setTimeout(reload, 2500) that race-lost against slow hardware or
// reverse proxies that 502 immediately when the upstream socket closes (#874).
async function _waitForServerThenReload(opts){
opts=opts||{};
const interval=opts.interval||500;
const maxMs=opts.maxMs||15000;
window._restartingForUpdate=true;
const msgEl=$('reconnectMsg');
const banner=$('reconnectBanner');
if(msgEl) msgEl.textContent='⏳ Restarting… please wait';
if(banner) banner.classList.add('visible');
const deadline=Date.now()+maxMs;
// Give the server a moment to actually begin its restart before the first
// probe — otherwise the old process may still respond ok on the first poll.
await new Promise(r=>setTimeout(r, interval));
while(Date.now()<deadline){
try{
const r=await fetch('/health',{cache:'no-store'});
if(r.ok){
let data={};
try{ data=await r.json(); }catch(_){}
if(data && data.status==='ok'){
location.reload();
return;
}
}
}catch(_){ /* socket closed during restart — retry */ }
await new Promise(r=>setTimeout(r, interval));
}
if(msgEl) msgEl.textContent='⚠️ Server is taking longer than expected — click Reload when ready';
}
function getPendingSessionMessage(session){
const text=String(session?.pending_user_message||'').trim();
if(!text) return null;