Context
PR #15 (ux/15-eventos, commits e4bf68f through 4650b4a) added the Eventos review drawer. Three entry paths open the drawer:
- Case A: "Abrir evento" button on a review card →
setEventOpener(..., "events") + hash change
- Case B: Monitoramento map dot click →
setEventOpener(..., "dashboard") + native <a href> navigation
- Case C: Direct
#/events/{id} URL load
After handleHashRoute opens the drawer, pressing Escape closes it for cases A and C but is a silent no-op for case B.
Root cause
Escape handler (app.js:1953-1974, round-2 RISK2 guard):
const openerView = eventOpener && eventOpener.view;
if (openerView && typeof eventosViewIsActive === "function" && !eventosViewIsActive(openerView)) return;
For case B, eventOpener.view = "dashboard" (round-4 RISK2 fix at app.js:2121, because the dot lives inside #view-dashboard which is display: none while events is active). When the drawer is open, the active view is always #view-events — never #view-dashboard. So eventosViewIsActive("dashboard") returns false and Escape silently bails.
The original purpose of this guard was to prevent Escape from silently closing a hidden drawer; in case B the guard fires for the wrong reason (it isn't the drawer that's hidden, it's the opener's origin view).
Reproduction
- Open
http://127.0.0.1:8081, log in
- Click Monitoramento tab
- Click a colored dot on the minimap → drawer opens, eventos view active
- Press Escape → nothing happens
- (Contrast: same flow with Eventos tab + "+ Abrir evento" → Escape closes the drawer correctly)
Suggested fix
Gate Escape on the drawer's own visibility, not the opener's view:
const drawer = document.getElementById("eventos-drawer");
if (!drawer || drawer.hidden) return;
// separate signal: track whether the opener is in a still-active view
if (eventOpener && eventOpener.focusEl && document.body.contains(eventOpener.focusEl)) {
try { eventOpener.focusEl.focus(); } catch {}
}
(document.body.contains(focusEl) already exists in closeEventDrawer at app.js:960 and correctly handles the dot-in-hidden-view case — focusing a detached or display:none element is a no-op in browsers anyway.)
Discovery
Sonnet r6 round-6 review (/tmp/sonnet-review-prompt-r6.md) flagged this as RISK. Acknowledged as out of scope for the PR.
Test gap
The vm test harness's document.addEventListener is a no-op, so Escape behavior has never been exercised by any of the 15 scenarios. A new test would need to either use the browser MCP / browser-harness or extend the stub to track listeners.
Context
PR #15 (
ux/15-eventos, commitse4bf68fthrough4650b4a) added the Eventos review drawer. Three entry paths open the drawer:setEventOpener(..., "events")+ hash changesetEventOpener(..., "dashboard")+ native<a href>navigation#/events/{id}URL loadAfter
handleHashRouteopens the drawer, pressing Escape closes it for cases A and C but is a silent no-op for case B.Root cause
Escape handler (app.js:1953-1974, round-2 RISK2 guard):
For case B,
eventOpener.view = "dashboard"(round-4 RISK2 fix at app.js:2121, because the dot lives inside#view-dashboardwhich isdisplay: nonewhile events is active). When the drawer is open, the active view is always#view-events— never#view-dashboard. SoeventosViewIsActive("dashboard")returns false and Escape silently bails.The original purpose of this guard was to prevent Escape from silently closing a hidden drawer; in case B the guard fires for the wrong reason (it isn't the drawer that's hidden, it's the opener's origin view).
Reproduction
http://127.0.0.1:8081, log inSuggested fix
Gate Escape on the drawer's own visibility, not the opener's view:
(
document.body.contains(focusEl)already exists incloseEventDrawerat app.js:960 and correctly handles the dot-in-hidden-view case — focusing a detached ordisplay:noneelement is a no-op in browsers anyway.)Discovery
Sonnet r6 round-6 review (
/tmp/sonnet-review-prompt-r6.md) flagged this as RISK. Acknowledged as out of scope for the PR.Test gap
The vm test harness's
document.addEventListeneris a no-op, so Escape behavior has never been exercised by any of the 15 scenarios. A new test would need to either use the browser MCP / browser-harness or extend the stub to track listeners.