fix(#1825): add cross-nav links between observer and node detail pages (#1826)

Adds cross-navigation between the observer detail page and the node
detail page for the same pubkey (community feature request from
cwichura).

**Changes**
- `public/observer-detail.js`: new `<a
href="#/nodes/${encodeURIComponent(currentId)}">View node detail →</a>`
inside `.page-header`, next to the `<h2 id="obsTitle">`.
- `public/nodes.js`: new sibling `<a
href="#/observers/${encodeURIComponent(n.public_key)}"
class="btn-primary">Observer →</a>` in the same button row as the
`Analytics` / `Reach` anchors on the full node detail page. Uses the
existing `ph-eye` phosphor icon.

**Test — TDD red→green**
- Red commit: `8c2315e1` (`test(#1825): red — observer<->node cross-link
anchors missing`) — 4/4 assertions fail on master; CI RED.
- Green commit: `ff8f6ed7` — minimum production change; 4/4 assertions
pass locally.

Test file: `test-issue-1825-observer-node-cross-links.js` —
static-source DOM-grep style consistent with the neighbouring
`test-issue-1789-observer-firmware-cols.js` /
`test-observers-headings.js` pattern. It asserts:
1. observer-detail.js contains
`href="#/nodes/${encodeURIComponent(currentId)}"`.
2. That anchor sits inside the `.page-header` block.
3. nodes.js contains
`href="#/observers/${encodeURIComponent(n.public_key)}"`.
4. That anchor is a sibling of the analytics/reach anchors in the same
flex row.

**Notes**
- Pubkeys are `encodeURIComponent`-escaped on both sides
(defense-in-depth; MeshCore pubkeys are hex only).
- No API changes. No CSS changes. No new dependencies.

Fixes #1825

---------

Co-authored-by: meshcore-bot <meshcore-bot@users.noreply.github.com>
This commit is contained in:
Kpa-clawbot
2026-07-06 20:13:04 -07:00
committed by GitHub
co-authored by meshcore-bot
parent 096e16409c
commit ba68069c23
3 changed files with 76 additions and 0 deletions
+1
View File
@@ -617,6 +617,7 @@
<button class="btn-primary" id="copyShortUrlBtn" title="Short URL using an 8-char pubkey prefix — easier to send over the mesh (issue #772)" style="flex:0 0 auto;font-size:12px;padding:4px 10px"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-broadcast"/></svg> Copy short URL</button>
<a href="#/nodes/${encodeURIComponent(n.public_key)}/analytics" class="btn-primary" style="flex:0 0 auto;text-decoration:none;font-size:12px;padding:4px 10px"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-chart-bar"/></svg> Analytics</a>
<a href="#/nodes/${encodeURIComponent(n.public_key)}/reach" class="btn-primary" style="flex:0 0 auto;text-decoration:none;font-size:12px;padding:4px 10px"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-broadcast"/></svg> Reach</a>
<a href="#/observers/${encodeURIComponent(n.public_key)}" class="btn-primary" title="View this pubkey as an observer" style="flex:0 0 auto;text-decoration:none;font-size:12px;padding:4px 10px"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-eye"/></svg> Observer →</a>
</div>
</div>
+1
View File
@@ -81,6 +81,7 @@ window.ObserverDetailNaiveBanner = {
<div class="page-header" style="display:flex;align-items:center;gap:12px;margin-bottom:16px">
<a href="#/observers" class="btn-icon" title="Back to Observers" aria-label="Back">←</a>
<h2 style="margin:0" id="obsTitle">Observer Detail</h2>
<a href="#/nodes/${encodeURIComponent(currentId)}" class="btn-secondary" title="View this pubkey as a node" style="text-decoration:none;font-size:12px;padding:4px 10px">View node detail →</a>
<div style="margin-left:auto;display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<span class="compare-with-group">
<label class="sr-only" for="obsCompareWithPicker">Compare with another observer</label>
@@ -0,0 +1,74 @@
/* test-issue-1825-observer-node-cross-links.js Issue #1825 regression test.
*
* Asserts cross-navigation links exist between the observer detail page and
* the node detail page for the same pubkey:
*
* - public/observer-detail.js renders an anchor `href="#/nodes/${encodeURIComponent(currentId)}"`
* inside the .page-header (near the `<h2 id="obsTitle">`).
* - public/nodes.js renders an anchor `href="#/observers/${encodeURIComponent(n.public_key)}"`
* in the same button row as the analytics/reach anchors on the full node
* detail page.
*
* Static-source test (grep the file text). No server, no DOM.
*/
'use strict';
const fs = require('fs');
const path = require('path');
const assert = require('assert');
let passed = 0, failed = 0;
function test(name, fn) {
try { fn(); passed++; console.log(` \u2713 ${name}`); }
catch (e) { failed++; console.log(` \u2717 ${name}\n ${e.message}`); }
}
console.log('\u2500\u2500 Observer <-> Node cross-links (#1825) \u2500\u2500');
const obsSrc = fs.readFileSync(path.join(__dirname, 'public', 'observer-detail.js'), 'utf8');
const nodesSrc = fs.readFileSync(path.join(__dirname, 'public', 'nodes.js'), 'utf8');
test('observer-detail.js links to #/nodes/${encodeURIComponent(currentId)}', () => {
assert.ok(
/href="#\/nodes\/\$\{encodeURIComponent\(currentId\)\}"/.test(obsSrc),
'expected anchor href="#/nodes/${encodeURIComponent(currentId)}" in observer-detail.js'
);
});
test('observer-detail.js cross-link sits inside the .page-header block', () => {
const m = obsSrc.match(/class="page-header"[\s\S]*?<\/div>/);
assert.ok(m, '.page-header block not found in observer-detail.js');
assert.ok(
/href="#\/nodes\/\$\{encodeURIComponent\(currentId\)\}"/.test(m[0]),
'observer -> node cross-link is not inside the .page-header block'
);
});
test('nodes.js links to #/observers/${encodeURIComponent(n.public_key)}', () => {
assert.ok(
/href="#\/observers\/\$\{encodeURIComponent\(n\.public_key\)\}"/.test(nodesSrc),
'expected anchor href="#/observers/${encodeURIComponent(n.public_key)}" in nodes.js'
);
});
test('nodes.js Observer link is a sibling of the analytics/reach anchors', () => {
// Find the button row that contains both analytics and reach anchors and
// assert it now also contains the observers anchor.
const rowRe = /<div[^>]*display:flex[^>]*flex-wrap:wrap[^>]*>[\s\S]*?<\/div>/g;
let found = false;
let m;
while ((m = rowRe.exec(nodesSrc)) !== null) {
const row = m[0];
if (/href="#\/nodes\/\$\{encodeURIComponent\(n\.public_key\)\}\/analytics"/.test(row) &&
/href="#\/nodes\/\$\{encodeURIComponent\(n\.public_key\)\}\/reach"/.test(row)) {
if (/href="#\/observers\/\$\{encodeURIComponent\(n\.public_key\)\}"/.test(row)) {
found = true;
break;
}
}
}
assert.ok(found, 'observer anchor not found in the same button row as analytics/reach anchors');
});
console.log(`\n${passed} passed, ${failed} failed`);
process.exit(failed ? 1 : 0);