fix(nodes): copy URL buttons produce malformed origin#frag URLs (#1753) (#1755)

## Problem

The **Copy URL** and **Copy short URL** buttons on the node detail page
produced URLs like:

```
https://analyzer.00id.net#/nodes/abcdef…
```

The `/` between the authority and the fragment is missing. RFC 3986
allows that form, but several mobile browsers (and some link-detection
heuristics) reject or mis-parse it.

## Fix

Three sites in `public/nodes.js` concatenated `location.origin` with a
literal that started with `'#/'`. Prepend `/`:

- `public/nodes.js:772` — full Copy URL (full pubkey)
- `public/nodes.js:783` — Copy short URL (8-char prefix)
- `public/nodes.js:1580` — side-pane Copy URL

All three now build `https://analyzer.00id.net/#/nodes/…`, which every
browser accepts.

## Tests

`test-issue-1753-copy-url-slash.js` — extracts every `location.origin +
'<literal>'` site from `public/nodes.js` and asserts the literal starts
with `/`. Wired into `.github/workflows/deploy.yml`.

- **Red commit** `b4df2786` — test added; CI fails on assertion (3 of 4
cases) because the literals still start with `'#/'`.
- **Green commit** `2c59f7c0` — three literals fixed to `'/#/nodes/'`;
test passes (4/4).

## Verification

```
$ node test-issue-1753-copy-url-slash.js
issue-1753 copy-URL slash regression
   found at least 3 location.origin + literal sites in public/nodes.js
   public/nodes.js:772 literal starts with "/#/" (got "/#/nodes/")
   public/nodes.js:783 literal starts with "/#/" (got "/#/nodes/")
   public/nodes.js:1580 literal starts with "/#/" (got "/#/nodes/")
  4 passed, 0 failed
```

`bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master`
→ clean (all gates + warnings green).

Fixes #1753

---------

Co-authored-by: openclaw-bot <bot@openclaw.dev>
This commit is contained in:
Kpa-clawbot
2026-06-20 11:44:00 -07:00
committed by GitHub
co-authored by openclaw-bot
parent f780fe7d0b
commit db5520f70f
3 changed files with 66 additions and 3 deletions
+1
View File
@@ -147,6 +147,7 @@ jobs:
node test-preflight-xss-gate.js
node test-traces.js
node test-issue-1648-m4-emoji-scan.js
node test-issue-1753-copy-url-slash.js
node test-issue-1668-m3-typography.js
node test-mqtt-status-panel.js
node test-issue-1697-mqtt-mobile-e2e.js
+3 -3
View File
@@ -769,7 +769,7 @@
}
// Copy URL
const nodeUrl = location.origin + '#/nodes/' + encodeURIComponent(n.public_key);
const nodeUrl = location.origin + '/#/nodes/' + encodeURIComponent(n.public_key);
document.getElementById('copyUrlBtn')?.addEventListener('click', () => {
const btn = document.getElementById('copyUrlBtn');
window.copyToClipboard(nodeUrl, () => {
@@ -780,7 +780,7 @@
// Copy short URL — issue #772. Uses an 8-char pubkey prefix; the
// backend resolves it to the canonical pubkey when unambiguous.
const shortUrl = location.origin + '#/nodes/' + n.public_key.slice(0, 8);
const shortUrl = location.origin + '/#/nodes/' + n.public_key.slice(0, 8);
document.getElementById('copyShortUrlBtn')?.addEventListener('click', () => {
const btn = document.getElementById('copyShortUrlBtn');
window.copyToClipboard(shortUrl, () => {
@@ -1577,7 +1577,7 @@
const observers = h.observers || [];
const recent = h.recentPackets || [];
const hasLoc = n.lat != null && n.lon != null;
const nodeUrl = location.origin + '#/nodes/' + encodeURIComponent(n.public_key);
const nodeUrl = location.origin + '/#/nodes/' + encodeURIComponent(n.public_key);
// Status calculation via shared helper
const lastHeard = stats.lastHeard;
+62
View File
@@ -0,0 +1,62 @@
/* test-issue-1753-copy-url-slash.js regression test for issue #1753.
*
* Bug: Copy URL / Copy short URL buttons on the node detail page built URLs as
* location.origin + '#/nodes/...' -> https://analyzer.00id.net#/nodes/...
* That's a malformed URL (missing the '/' between authority and fragment).
* Some mobile browsers reject it.
*
* Strategy: behavioral source-level check. The three URL-construction sites
* in public/nodes.js are simple string concatenations of `location.origin`
* with a hardcoded literal. The "real" assertion is on the literal itself
* it MUST start with '/#/' (not '#/'). The test extracts every
* location.origin + '<literal>'
* occurrence in public/nodes.js and asserts each literal begins with '/#/'.
* Reverting the fix (dropping the leading '/') re-introduces the failure.
*/
'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(' ✅ ' + name); }
catch (e) { failed++; console.log(' ❌ ' + name + ': ' + e.message); }
}
const src = fs.readFileSync(path.join(__dirname, 'public/nodes.js'), 'utf8');
// Match: location.origin + '<literal>' (single or double quotes)
const re = /location\.origin\s*\+\s*(['"])([^'"]*)\1/g;
const sites = [];
let m;
while ((m = re.exec(src)) !== null) {
const lineNo = src.slice(0, m.index).split('\n').length;
sites.push({ literal: m[2], lineNo });
}
console.log('issue-1753 copy-URL slash regression');
test('found at least 3 location.origin + literal sites in public/nodes.js', () => {
assert.ok(sites.length >= 3,
'expected >=3 sites, found ' + sites.length + ': ' + JSON.stringify(sites));
});
for (const s of sites) {
test('public/nodes.js:' + s.lineNo + ' literal starts with "/#/" (got ' + JSON.stringify(s.literal) + ')', () => {
assert.ok(
s.literal.startsWith('/#/') || s.literal.startsWith('/'),
'location.origin + ' + JSON.stringify(s.literal) +
' produces malformed URL (missing leading "/"). See issue #1753.'
);
// Specifically forbid '#/' as the literal start — that's the bug.
assert.ok(
!s.literal.startsWith('#/'),
'literal begins with "#/" — concatenating with location.origin yields ' +
'"<origin>#/..." which is malformed. Fix: prepend "/". See issue #1753.'
);
});
}
console.log('\n ' + passed + ' passed, ' + failed + ' failed');
process.exit(failed > 0 ? 1 : 0);