Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions cmd/server/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,6 @@ type PacketStore struct {
byNode map[string][]*StoreTx // pubkey → transmissions
nodeHashes map[string]map[string]bool // pubkey → Set<hash>
byPathHop map[string][]*StoreTx // lowercase hop/pubkey → transmissions with that hop in path
relayTimes map[string][]int64 // lowercase pubkey → sorted unix-millis of relay events (full pubkeys only)
byPayloadType map[int][]*StoreTx // payload_type → transmissions
loaded bool
totalObs int
Expand Down Expand Up @@ -646,7 +645,6 @@ func NewPacketStore(db *DB, cfg *PacketStoreConfig, cacheTTLs ...map[string]inte
byObserver: make(map[string][]*StoreObs),
byNode: make(map[string][]*StoreTx),
byPathHop: make(map[string][]*StoreTx),
relayTimes: make(map[string][]int64),
nodeHashes: make(map[string]map[string]bool),
byPayloadType: make(map[int][]*StoreTx),
rfCache: make(map[string]*cachedResult),
Expand Down
10 changes: 7 additions & 3 deletions public/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -1176,16 +1176,20 @@ function navigate() {
closeNav();

// Backward-compat redirect: #/traces/<hash> → #/tools/trace/<hash> (issue #944).
// Uses replaceState (not location.hash =) so this redirect doesn't add its
// own history entry, otherwise the back button gets stuck bouncing off it.
if (location.hash.startsWith('#/traces/')) {
location.hash = location.hash.replace('#/traces/', '#/tools/trace/');
history.replaceState(null, '', location.hash.replace('#/traces/', '#/tools/trace/'));
navigate();
return;
}

// Backward-compat redirect: #/roles → #/analytics?tab=roles (issue #1085).
// The Roles page was folded into the Analytics tab strip; old links and
// bookmarks must keep working.
// bookmarks must keep working. Uses replaceState for the same reason as above.
if (location.hash === '#/roles' || location.hash.startsWith('#/roles?') || location.hash.startsWith('#/roles/')) {
location.hash = '#/analytics?tab=roles';
history.replaceState(null, '', '#/analytics?tab=roles');
navigate();
return;
}

Expand Down
2 changes: 1 addition & 1 deletion public/observers.js
Original file line number Diff line number Diff line change
Expand Up @@ -538,7 +538,7 @@ window.preserveCompareSelection = function preserveCompareSelection(prevIds, tbo
var content = window.SlideOver.open({ title: (o.name || o.id) });
var naiveChipHTML = window.ObserversNaiveChip.render(o);
content.innerHTML =
(naiveChipHTML ? '<div style="margin-bottom:10px">' + naiveChipHTML + ' <span class="text-muted">Clock is naive — per-packet timing clamped to ingest time.</span></div>' : '') +
(naiveChipHTML ? '<div style="margin-bottom:10px">' + naiveChipHTML + ' <span class="text-muted">Clock is naive — per-packet timing clamped to ingest time. Clears itself 24h after the last skew event.</span></div>' : '') +
'<dl class="slide-over-dl" style="margin:0;display:grid;grid-template-columns:auto 1fr;gap:6px 12px;font-size:13px">' +
'<dt>Status</dt><dd><span class="health-dot ' + h.cls + '"><svg class="ph-icon" aria-hidden="true" focusable="false"><use href="/icons/phosphor-sprite.svg#ph-circle-fill"></use></svg></span> ' + h.label + '</dd>' +
'<dt>Region</dt><dd>' + (o.iata ? '<span class="badge-region">' + o.iata + '</span>' : '—') + '</dd>' +
Expand Down
1 change: 1 addition & 0 deletions test-all.sh
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ node test-issue-1648-m2-emoji-scan.js
node test-issue-1648-m3-emoji-scan.js
node test-issue-1648-m6-final-sweep.js
node test-issue-1648-m6-lint-self.js
node test-issue-1883-redirect-history.js
node test-issue-1890-og-url.js
node test-traces.js
node test-live-multibyte-filter.js
Expand Down
149 changes: 149 additions & 0 deletions test-issue-1883-redirect-history.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/* Regression tests for legacy-route history replacement (#1883). */
'use strict';

const assert = require('assert');
const fs = require('fs');
const path = require('path');
const vm = require('vm');

const appPath = process.env.APP_JS || path.join(__dirname, 'public', 'app.js');

function makeHistorySandbox() {
const entries = ['#/home'];
let index = 0;
let currentHash = entries[index];

const location = {};
Object.defineProperty(location, 'hash', {
get() { return currentHash; },
set(hash) {
if (hash === currentHash) return;
entries.splice(index + 1);
entries.push(hash);
index++;
currentHash = hash;
},
});

const history = {
get length() { return entries.length; },
replaceState(_state, _title, hash) {
entries[index] = hash;
currentHash = hash;
},
back() {
if (index === 0) return;
index--;
currentHash = entries[index];
},
};

const classList = { add() {}, remove() {}, toggle() {} };
const window = { addEventListener() {}, dispatchEvent() {} };
const document = {
readyState: 'complete',
body: { classList },
createElement: () => ({ id: '', textContent: '', innerHTML: '' }),
head: { appendChild() {} },
getElementById: () => null,
addEventListener() {},
querySelectorAll: () => [],
querySelector: () => null,
};
window.document = document;
window.location = location;
window.history = history;

const sandbox = {
window,
document,
location,
history,
console,
Date,
Infinity,
Math,
Array,
Object,
String,
Number,
JSON,
RegExp,
Error,
TypeError,
parseInt,
parseFloat,
isNaN,
isFinite,
encodeURIComponent,
decodeURIComponent,
setTimeout() {},
clearTimeout() {},
setInterval() {},
clearInterval() {},
fetch: () => Promise.resolve({ json: () => Promise.resolve({}) }),
performance: { now: () => Date.now() },
localStorage: { getItem: () => null, setItem() {}, removeItem() {} },
CustomEvent: class CustomEvent {},
Map,
Promise,
URLSearchParams,
addEventListener() {},
dispatchEvent() {},
requestAnimationFrame() {},
};

vm.createContext(sandbox);
vm.runInContext(fs.readFileSync(appPath, 'utf8'), sandbox, { filename: appPath });
return sandbox;
}

function isLegacyRedirect(hash) {
return hash.startsWith('#/traces/') || hash === '#/roles' ||
hash.startsWith('#/roles?') || hash.startsWith('#/roles/');
}

function assertRedirectReplacesHistory(legacyHash, expectedHash) {
const sandbox = makeHistorySandbox();
const productionNavigate = sandbox.navigate;
let recursiveNavigateCalls = 0;

// Keep the test focused on the redirect branch: the production redirect
// deliberately re-enters navigate(), while rendering the target route is
// covered by the browser suite.
sandbox.navigate = () => { recursiveNavigateCalls++; };

sandbox.location.hash = legacyHash;
const historyLengthBeforeRedirect = sandbox.history.length;
productionNavigate();

assert.strictEqual(sandbox.location.hash, expectedHash,
`${legacyHash} should redirect to ${expectedHash}`);
assert.strictEqual(sandbox.history.length, historyLengthBeforeRedirect,
`${legacyHash} must replace its history entry instead of adding one`);
assert.strictEqual(recursiveNavigateCalls, 1,
`${legacyHash} should render the replacement route immediately`);

sandbox.history.back();
// Browsers dispatch hashchange after Back. Re-run the production router only
// when Back exposed the legacy entry; the old location.hash implementation
// redirects forward again here and reproduces the loop.
if (isLegacyRedirect(sandbox.location.hash)) productionNavigate();

assert.strictEqual(sandbox.location.hash, '#/home',
`Back from ${expectedHash} should return to #/home without a redirect loop`);
}

const cases = [
['#/traces/a1b2c3d4', '#/tools/trace/a1b2c3d4'],
['#/roles', '#/analytics?tab=roles'],
['#/roles?from=bookmark', '#/analytics?tab=roles'],
['#/roles/legacy', '#/analytics?tab=roles'],
];

for (const [legacyHash, expectedHash] of cases) {
assertRedirectReplacesHistory(legacyHash, expectedHash);
console.log(` ✅ ${legacyHash} replaces history and Back returns to #/home`);
}

console.log(`\nredirect history: ${cases.length} passed, 0 failed`);