diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index e43d15620..954d29a3a 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -444,6 +444,7 @@ jobs:
BASE_URL=http://localhost:13581 node test-issue-1122-details-row-clamp-e2e.js 2>&1 | tee -a e2e-output.txt
BASE_URL=http://localhost:13581 node test-issue-1128-packets-layout-e2e.js 2>&1 | tee -a e2e-output.txt
BASE_URL=http://localhost:13581 node test-issue-189-group-caret-e2e.js 2>&1 | tee -a e2e-output.txt
+ BASE_URL=http://localhost:13581 node test-issue-254-affinity-toggle-mobile-aria-e2e.js 2>&1 | tee -a e2e-output.txt
BASE_URL=http://localhost:13581 node test-issue-1128-multi-viewport-e2e.js 2>&1 | tee -a e2e-output.txt
BASE_URL=http://localhost:13581 node test-issue-1136-live-region-e2e.js 2>&1 | tee -a e2e-output.txt
BASE_URL=http://localhost:13581 node test-live-multibyte-only-e2e.js 2>&1 | tee -a e2e-output.txt
diff --git a/public/mobile-page-actions.js b/public/mobile-page-actions.js
index 2f0c08d17..2e23c802a 100644
--- a/public/mobile-page-actions.js
+++ b/public/mobile-page-actions.js
@@ -7,6 +7,9 @@
const SLOT_ID = 'navPageActions';
function isMobile() { return window.innerWidth <= MOBILE_BP; }
+ // #254: packets.js renders a group row as select-hash under this breakpoint,
+ // where the #1461 #7 redirect below makes activating it select.
+ window.MobilePageActions = { isMobile: isMobile };
function ensureSlot() {
let slot = document.getElementById(SLOT_ID);
diff --git a/public/nodes.js b/public/nodes.js
index 516220da8..4928620e9 100644
--- a/public/nodes.js
+++ b/public/nodes.js
@@ -548,6 +548,38 @@
// ─── End neighbor helpers ─────────────────────────────────────────────────
+ // #254: the Affinity Debug card (only shown with debugAffinity). Its heading is
+ // a disclosure button, with caret-right when collapsed and caret-down when
+ // expanded (#189), and aria-expanded on the button follows the body. The click
+ // is handled by onFullBodyClick, delegated from #nodeFullBody.
+ function affinityDebugCaretHtml(expanded) {
+ return '';
+ }
+
+ function renderAffinityDebugCard() {
+ return `
+
+
+
Loading debug data…
+
+
`;
+ }
+
+ function toggleAffinityDebug(btn) {
+ const expanded = btn.getAttribute('aria-expanded') !== 'true';
+ btn.setAttribute('aria-expanded', String(expanded));
+ const body = btn.ownerDocument.getElementById(btn.getAttribute('aria-controls'));
+ if (body) body.hidden = !expanded;
+ const icon = btn.querySelector('.toggle-icon');
+ if (icon) icon.innerHTML = affinityDebugCaretHtml(expanded);
+ return expanded;
+ }
+
+ function onFullBodyClick(e) {
+ const btn = e.target.closest && e.target.closest('.affinity-debug-toggle');
+ if (btn) toggleAffinityDebug(btn);
+ }
+
let directNode = null; // set when navigating directly to #/nodes/:pubkey
let regionChangeHandler = null;
@@ -570,6 +602,9 @@
`;
document.getElementById('nodeBackBtn').addEventListener('click', () => { location.hash = '#/nodes'; });
+ // Wired once here: loadFullNode re-renders the body (theme-refresh) and
+ // would stack listeners.
+ document.getElementById('nodeFullBody').addEventListener('click', onFullBodyClick);
loadFullNode(directNode);
// Escape to go back to nodes list
document.addEventListener('keydown', function nodesEsc(e) {
@@ -916,12 +951,7 @@
Loading neighbors…
-
-
':''"> Affinity Debug
-
-
Loading debug data…
-
-
+ ${renderAffinityDebugCard()}
Paths Through This Node
@@ -2148,6 +2178,9 @@
window._nodesRenderNodeTimestampText = renderNodeTimestampText;
window._nodesGetStatusInfo = getStatusInfo;
window._nodesGetStatusTooltip = getStatusTooltip;
+ window._nodesRenderAffinityDebugCard = renderAffinityDebugCard;
+ window._nodesToggleAffinityDebug = toggleAffinityDebug;
+ window._nodesOnFullBodyClick = onFullBodyClick;
// #862: Expose search filter logic for testing
window._nodesMatchesSearch = function(node, query) {
diff --git a/public/packets.js b/public/packets.js
index a06d8028d..826c3cd65 100644
--- a/public/packets.js
+++ b/public/packets.js
@@ -2404,6 +2404,29 @@
}
}
+ // #254: mobile-page-actions.js owns the mobile breakpoint and the #1461 #7
+ // redirect of a group-row click to select-hash. Without it nothing redirects.
+ function groupRowSelectsOnActivate() {
+ return !!(window.MobilePageActions && window.MobilePageActions.isMobile());
+ }
+
+ // A group row's action and aria-expanded depend on that breakpoint, so a
+ // resize that crosses it (e.g. a phone rotating) re-renders the visible rows.
+ let _groupRowsSelect = false;
+ let _groupRowModeTimer = null;
+ function _onGroupRowModeResize() {
+ clearTimeout(_groupRowModeTimer);
+ _groupRowModeTimer = setTimeout(() => {
+ const selects = groupRowSelectsOnActivate();
+ if (selects === _groupRowsSelect) return;
+ _groupRowsSelect = selects;
+ if (!_displayGrouped) return;
+ _lastVisibleStart = -1;
+ _lastVisibleEnd = -1;
+ renderVisibleRows();
+ }, 150);
+ }
+
// Build HTML for a single grouped packet row
function buildGroupRowHtml(p, entryIdx = -1) {
const isExpanded = expandedHashes.has(p.hash);
@@ -2440,8 +2463,12 @@
// channels.js, network-digest.js, analytics.js and route-view.js. The row that
// toggles reports its state in aria-expanded; a single-observation row cannot
// expand, so it has neither.
+ // #254: under the mobile breakpoint activating a group row selects it (#1461
+ // #7, the expand column is hidden there), so the row is select-hash and
+ // claims no expanded state.
+ const _grpToggles = !isSingle && !groupRowSelectsOnActivate();
const _grpCaret = isSingle ? '' : '';
- let html = `
+ let html = `
${_grpCaret}
${groupRegion ? `${groupRegion}` : '—'}
${renderTimestampCell(p.latest)}
@@ -4257,6 +4284,8 @@
_themeRefreshHandler = () => { if (typeof renderTableRows === 'function') renderTableRows(); };
window.addEventListener('theme-refresh', _themeRefreshHandler);
window.addEventListener('storage', _onStorageChange);
+ _groupRowsSelect = groupRowSelectsOnActivate();
+ window.addEventListener('resize', _onGroupRowModeResize);
var result = init(app, routeParam);
// Install channel color picker on packets table (M2, #271)
if (window.ChannelColorPicker) window.ChannelColorPicker.installPacketsTable();
@@ -4265,6 +4294,8 @@
destroy: function() {
if (_themeRefreshHandler) { window.removeEventListener('theme-refresh', _themeRefreshHandler); _themeRefreshHandler = null; }
window.removeEventListener('storage', _onStorageChange);
+ window.removeEventListener('resize', _onGroupRowModeResize);
+ clearTimeout(_groupRowModeTimer);
return destroy();
}
});
diff --git a/public/style.css b/public/style.css
index 6c2007117..1990f320d 100644
--- a/public/style.css
+++ b/public/style.css
@@ -2860,6 +2860,14 @@ button.ch-item.ch-item-encrypted .ch-badge { filter: grayscale(0.6); }
margin-bottom: 12px; border: 1px solid var(--border);
}
.node-full-card h4 { margin: 0 0 10px; font-size: 14px; color: var(--text-muted); text-transform: uppercase; letter-spacing: .5px; }
+/* #254: the Affinity Debug heading is a disclosure button. It keeps the h4
+ * look and spans the heading, so the whole row toggles as before. */
+.node-full-card h4 .affinity-debug-toggle {
+ display: flex; align-items: center; gap: 6px; width: 100%;
+ margin: 0; padding: 0; border: 0; background: none; cursor: pointer;
+ font: inherit; color: inherit; letter-spacing: inherit; text-transform: inherit; text-align: start;
+}
+@media (pointer: coarse) { .node-full-card h4 .affinity-debug-toggle { min-height: 48px; } }
.node-activity-list { display: flex; flex-direction: column; gap: 6px; }
.node-activity-item {
display: flex; gap: 8px; font-size: 13px; padding: 6px 0;
diff --git a/test-all.sh b/test-all.sh
index a505c5a17..9ce3bdbcc 100755
--- a/test-all.sh
+++ b/test-all.sh
@@ -57,6 +57,7 @@ run test-top-routes-overlay.js
run test-important-links-byte-filter.js
run test-url-state.js
run test-node-adverts.js
+run test-issue-254-affinity-debug-toggle.js
run test-perf-go-runtime.js
run test-channel-psk-ux.js
run test-channel-sidebar-layout.js
diff --git a/test-issue-254-affinity-debug-toggle.js b/test-issue-254-affinity-debug-toggle.js
new file mode 100644
index 000000000..7e388e0ae
--- /dev/null
+++ b/test-issue-254-affinity-debug-toggle.js
@@ -0,0 +1,196 @@
+/**
+ * #254: the node page's Affinity Debug card (shown only with debugAffinity, e.g.
+ * localStorage meshcore-affinity-debug=true) folds its body in and out.
+ *
+ * Its heading used to carry an inline onclick whose value held the Phosphor
+ * sprite markup with unescaped double quotes, so the browser cut the attribute
+ * short and the handler was a syntax error: the body could never open. Its
+ * carets were also reversed against the #189 convention (collapsed caret-right,
+ * expanded caret-down).
+ *
+ * Tests the real nodes.js in a vm sandbox:
+ * - the card renders a disclosure