fix: add btnClearUpdateLock markup for PR #5688 v2 frontend path

Address Greptile P1 review (discussion r3530941293) on commit e8f7e28a:
the v2 frontend recovery path was dead at the DOM level. static/ui.js
references $('btnClearUpdateLock') but no matching element existed in
static/index.html, so users hitting a lock conflict never saw a clickable
affordance -- the error panel was the only visible state.

Add the missing button next to btnForceUpdate in the update-banner action
row:

  <button class="update-btn" id="btnClearUpdateLock"
          style="display:none"
          onclick="applyClearUpdateLock(this)">
    Clear lock and retry update
  </button>

Style mirrors the neutral update-btn (NOT the red --error used by the
destructive Force update) so the recovery affordance doesn't read as a
warning. Hidden by default; revealed by the existing _showUpdateError()
helper when res.lock_conflict is set.

Regression tests in TestClearLockButton (3 tests):

- test_clear_lock_button_exists
- test_clear_lock_button_hidden_by_default
- test_clear_lock_button_calls_applyClearUpdateLock_handler

These lock in the contract that the button element must be present and
correctly bound; future refactors that rename the id or strip the button
will fail loud. Mirrors the existing TestIndexHtmlBanner conventions
for btnForceUpdate.

Verification:

- ./scripts/test.sh tests/test_updates.py  -k lock -> 35 passed
- ./scripts/test.sh tests/test_update_banner_fixes.py  -> 102 passed
- ./scripts/test.sh tests/test_api_timeout.py  -> 8 passed
- ruff_lint --diff upstream/master  -> 0 new violations
This commit is contained in:
b3nw 2026-07-06 17:46:30 +00:00
parent e8f7e28a3c
commit dcec232dfa
2 changed files with 34 additions and 0 deletions

View file

@ -415,6 +415,7 @@
<button class="update-btn" onclick="dismissUpdate()">Later</button>
<button class="update-btn update-primary" id="btnApplyUpdate" onclick="applyUpdates()">Update Now</button>
<button class="update-btn" id="btnForceUpdate" style="display:none;background:var(--error,#e05);color:#fff;border-color:var(--error,#e05)" onclick="forceUpdate(this)">Force update</button>
<button class="update-btn" id="btnClearUpdateLock" style="display:none" onclick="applyClearUpdateLock(this)">Clear lock and retry update</button>
</div>
</div>
<div class="update-banner" id="staleClientBanner">

View file

@ -1805,6 +1805,39 @@ class TestIndexHtmlBanner:
)
class TestClearLockButton:
"""PR #5688 follow-up: clear-lock button must exist in index.html and be
hidden by default. Without this, the v2 frontend recovery path is dead --
$('btnClearUpdateLock') returns null at runtime so the lock-only error
branch in _showUpdateError() never reveals a clickable affordance (P1).
"""
def test_clear_lock_button_exists(self):
src = read('static/index.html')
assert 'id="btnClearUpdateLock"' in src, (
"index.html must have #btnClearUpdateLock button (hidden by "
"default) -- PR #5688 v2 frontend path"
)
def test_clear_lock_button_hidden_by_default(self):
src = read('static/index.html')
m = re.search(r'id="btnClearUpdateLock"[^>]*>', src)
assert m, "#btnClearUpdateLock not found"
tag = m.group(0)
assert 'display:none' in tag, (
"#btnClearUpdateLock must be hidden by default (display:none)"
)
def test_clear_lock_button_calls_applyClearUpdateLock_handler(self):
src = read('static/index.html')
m = re.search(r'id="btnClearUpdateLock"[^>]*>', src)
assert m, "#btnClearUpdateLock not found"
tag = m.group(0)
assert 'applyClearUpdateLock(this)' in tag, (
"#btnClearUpdateLock onclick must invoke applyClearUpdateLock"
)
# ── Regression: sequential webui+agent update — restart coordination ──────────
class TestSequentialUpdateRestartCoordination: