fix: scope overview approval re-bind to re-rendered subtrees (fixes #207)

refreshApprovals re-rendered only #ov-metrics and #ov-attention but then
called bindEvents on the whole overview root, stacking click/keydown
listeners on unchanged shell nodes (agent rows, omnibar) on every 10s poll.
Scope the re-bind to the two re-rendered subtrees so stale nodes keep a
single listener. paint() still does the full-tree bind on initial render.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
This commit is contained in:
MervinPraisonandClaude authored and praisonai-triage-agent[bot] committed 2026-07-30 06:32:21 +00:00
1 parent 90315326f7
commit 6ffc84e9ba
2 files changed
+64 -3

No files matched your search

@@ -449,9 +449,14 @@ async function refreshApprovals() {
data.pending = pending;
const metricsEl = _container.querySelector('#ov-metrics');
const attnEl = _container.querySelector('#ov-attention');
if (metricsEl) metricsEl.innerHTML = renderMetrics(data);
if (attnEl) attnEl.innerHTML = renderAttention(data);
bindEvents(_container, data);
if (metricsEl) {
metricsEl.innerHTML = renderMetrics(data);
bindEvents(metricsEl, data);
}
if (attnEl) {
attnEl.innerHTML = renderAttention(data);
bindEvents(attnEl, data);
}
}
function startPolling() {
+56
View File
@@ -0,0 +1,56 @@
"""Regression tests for issue #207.
The Agent Command Center overview polled pending approvals every 10s and
re-rendered only ``#ov-metrics`` and ``#ov-attention`` but then called
``bindEvents(_container, data)`` over the whole overview root. Interactive
elements in the page shell that were not re-rendered (agent rows, omnibar)
accumulated stacked click/keydown listeners.
These tests assert the committed frontend view source
``src/praisonaiui/templates/frontend/plugins/views/overview.js`` scopes the
re-bind to only the subtrees that were re-rendered, so stale shell nodes no
longer stack listeners.
"""
from __future__ import annotations
import re
from pathlib import Path
OVERVIEW_VIEW = (
Path(__file__).resolve().parents[2]
/ "src" / "praisonaiui" / "templates" / "frontend" / "plugins" / "views"
/ "overview.js"
)
def _view() -> str:
return OVERVIEW_VIEW.read_text()
def _refresh_approvals_body() -> str:
src = _view()
start = src.index("async function refreshApprovals()")
end = src.index("function startPolling()", start)
return src[start:end]
class TestScopedBind:
def test_metrics_subtree_rebound(self):
assert "bindEvents(metricsEl, data)" in _refresh_approvals_body()
def test_attention_subtree_rebound(self):
assert "bindEvents(attnEl, data)" in _refresh_approvals_body()
def test_no_full_container_rebind_in_refresh(self):
assert "bindEvents(_container, data)" not in _refresh_approvals_body()
def test_paint_still_binds_full_container(self):
src = _view()
start = src.index("async function paint()")
end = src.index("async function refreshApprovals()", start)
assert "bindEvents(_container, data)" in src[start:end]
def test_full_container_bind_only_in_paint(self):
# Exactly one full-tree bind remains (paint's initial render).
assert len(re.findall(r"bindEvents\(_container, data\)", _view())) == 1