From beb56b1a8bc56fec4814ab0ed1cc299a73a84169 Mon Sep 17 00:00:00 2001 From: Nathan Esquenazi Date: Sun, 5 Apr 2026 16:20:12 +0000 Subject: [PATCH] fix: apply_update concurrency lock, boot.js settings-fail guard, dead workspace code, test_updates URL param - api/updates.py: add _apply_lock to prevent concurrent stash/pull/pop - static/boot.js: set check_for_updates:false on settings fetch failure - static/panels.js: remove dead settingsWorkspace references (element removed from HTML) - api/routes.py + static/boot.js: add ?test_updates=1 URL param for testing banner without being behind on git (localhost-only simulate endpoint) --- api/routes.py | 10 +++++++++- api/updates.py | 11 +++++++++++ static/boot.js | 9 ++++++--- static/panels.js | 18 +----------------- 4 files changed, 27 insertions(+), 21 deletions(-) diff --git a/api/routes.py b/api/routes.py index 60b9195..5fc7ffa 100644 --- a/api/routes.py +++ b/api/routes.py @@ -231,7 +231,15 @@ def handle_get(handler, parsed) -> bool: settings = load_settings() if not settings.get('check_for_updates', True): return j(handler, {'disabled': True}) - force = parse_qs(parsed.query).get('force', ['0'])[0] == '1' + qs = parse_qs(parsed.query) + force = qs.get('force', ['0'])[0] == '1' + # ?simulate=1 returns fake behind counts for UI testing (localhost only) + if qs.get('simulate', ['0'])[0] == '1' and handler.client_address[0] == '127.0.0.1': + return j(handler, { + 'webui': {'name': 'webui', 'behind': 3, 'current_sha': 'abc1234', 'latest_sha': 'def5678', 'branch': 'master'}, + 'agent': {'name': 'agent', 'behind': 1, 'current_sha': 'aaa0001', 'latest_sha': 'bbb0002', 'branch': 'master'}, + 'checked_at': 0, + }) from api.updates import check_for_updates return j(handler, check_for_updates(force=force)) diff --git a/api/updates.py b/api/updates.py index aa18fe3..f59da6e 100644 --- a/api/updates.py +++ b/api/updates.py @@ -24,6 +24,7 @@ except ImportError: _update_cache = {'webui': None, 'agent': None, 'checked_at': 0} _cache_lock = threading.Lock() _check_in_progress = False +_apply_lock = threading.Lock() # prevents concurrent stash/pull/pop on same repo CACHE_TTL = 1800 # 30 minutes @@ -108,6 +109,16 @@ def check_for_updates(force=False): def apply_update(target): """Stash, pull --ff-only, pop for the given target repo.""" + if not _apply_lock.acquire(blocking=False): + return {'ok': False, 'message': 'Update already in progress'} + try: + return _apply_update_inner(target) + finally: + _apply_lock.release() + + +def _apply_update_inner(target): + """Inner implementation of apply_update, called under _apply_lock.""" if target == 'webui': path = REPO_ROOT elif target == 'agent': diff --git a/static/boot.js b/static/boot.js index e5a8c24..4e74ff8 100644 --- a/static/boot.js +++ b/static/boot.js @@ -309,10 +309,13 @@ document.querySelectorAll('.suggestion').forEach(btn=>{ (async()=>{ // Load send key preference let _bootSettings={}; - try{const s=await api('/api/settings');_bootSettings=s;window._sendKey=s.send_key||'enter';window._showTokenUsage=!!s.show_token_usage;window._showCliSessions=!!s.show_cli_sessions;const _theme=s.theme||'dark';document.documentElement.dataset.theme=_theme;localStorage.setItem('hermes-theme',_theme);}catch(e){window._sendKey='enter';window._showTokenUsage=false;window._showCliSessions=false;} + try{const s=await api('/api/settings');_bootSettings=s;window._sendKey=s.send_key||'enter';window._showTokenUsage=!!s.show_token_usage;window._showCliSessions=!!s.show_cli_sessions;const _theme=s.theme||'dark';document.documentElement.dataset.theme=_theme;localStorage.setItem('hermes-theme',_theme);}catch(e){window._sendKey='enter';window._showTokenUsage=false;window._showCliSessions=false;_bootSettings={check_for_updates:false};} // Non-blocking update check (fire-and-forget, once per tab session) - if(_bootSettings.check_for_updates!==false&&!sessionStorage.getItem('hermes-update-checked')&&!sessionStorage.getItem('hermes-update-dismissed')){ - api('/api/updates/check').then(d=>{sessionStorage.setItem('hermes-update-checked','1');if((d.webui&&d.webui.behind>0)||(d.agent&&d.agent.behind>0))_showUpdateBanner(d);}).catch(()=>{}); + // ?test_updates=1 in URL forces banner display for testing (bypasses sessionStorage guards) + const _testUpdates=new URLSearchParams(location.search).get('test_updates')==='1'; + if(_testUpdates||(_bootSettings.check_for_updates!==false&&!sessionStorage.getItem('hermes-update-checked')&&!sessionStorage.getItem('hermes-update-dismissed'))){ + const _checkUrl='/api/updates/check'+(_testUpdates?'?simulate=1':''); + api(_checkUrl).then(d=>{if(!_testUpdates)sessionStorage.setItem('hermes-update-checked','1');if((d.webui&&d.webui.behind>0)||(d.agent&&d.agent.behind>0))_showUpdateBanner(d);}).catch(()=>{}); } // Fetch active profile try{const p=await api('/api/profile/active');S.activeProfile=p.name||'default';}catch(e){S.activeProfile='default';} diff --git a/static/panels.js b/static/panels.js index 65b3293..bb3ea9a 100644 --- a/static/panels.js +++ b/static/panels.js @@ -995,21 +995,6 @@ async function loadSettingsPanel(){ modelSel.value=settings.default_model||''; modelSel.addEventListener('change',_markSettingsDirty,{once:false}); } - // Populate workspace dropdown from /api/workspaces - const wsSel=$('settingsWorkspace'); - if(wsSel){ - wsSel.innerHTML=''; - try{ - const wsData=await api('/api/workspaces'); - for(const w of (wsData.workspaces||[])){ - const opt=document.createElement('option'); - opt.value=w.path;opt.textContent=w.name||w.path; - wsSel.appendChild(opt); - } - }catch(e){} - wsSel.value=settings.default_workspace||''; - wsSel.addEventListener('change',_markSettingsDirty,{once:false}); - } // Send key preference const sendKeySel=$('settingsSendKey'); if(sendKeySel){sendKeySel.value=settings.send_key||'enter';sendKeySel.addEventListener('change',_markSettingsDirty,{once:false});} @@ -1043,7 +1028,6 @@ async function loadSettingsPanel(){ async function saveSettings(andClose){ const model=($('settingsModel')||{}).value; - const workspace=($('settingsWorkspace')||{}).value; const sendKey=($('settingsSendKey')||{}).value; const showTokenUsage=!!($('settingsShowTokenUsage')||{}).checked; const showCliSessions=!!($('settingsShowCliSessions')||{}).checked; @@ -1051,7 +1035,7 @@ async function saveSettings(andClose){ const theme=($('settingsTheme')||{}).value||'dark'; const body={}; if(model) body.default_model=model; - if(workspace) body.default_workspace=workspace; + if(sendKey) body.send_key=sendKey; body.theme=theme; body.show_token_usage=showTokenUsage;