From e306c3ce28604ae85f632cdfb4c809497a0d51eb Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 13 Sep 2026 13:51:36 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20a=20link=20into=20a=20tab=20=E2=80=94?= =?UTF-8?q?=20app.pedshub.com/#resources=20opens=20My=20Resources?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The app has no pages and is not getting any. Another site can still send someone straight to a tab by naming it in the hash: it is read once at load, stored where sign-in looks for the last tab, and dropped from the URL, so it survives the round trip through the SSO and does not stick to the address bar. #resources, #deck and #decks all mean My Resources. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU --- public/js/app.js | 18 +++++++++++++++ test/open-tab-from-hash.test.js | 41 +++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 test/open-tab-from-hash.test.js diff --git a/public/js/app.js b/public/js/app.js index 54647b9d..cd1ad61d 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -2,6 +2,24 @@ // APP.JS — Core utilities, tabs, model selector, helpers // ============================================================ +// ── A link into a tab, without routes ─────────────────────── +// The app has no pages: every tab lives at "/". Another site can still send +// someone straight to one — app.pedshub.com/#resources opens My Resources — +// by naming the tab in the hash. It is read once, stored where sign-in looks +// for the last tab, and removed from the URL, so it survives the trip to the +// SSO and back and does not stick to the address bar afterwards. +var TAB_ALIASES = { resources: 'myresources', deck: 'myresources', decks: 'myresources', assistant: 'assistant' }; +function openTabFromHash(win, doc, storage) { + var wanted = String((win.location && win.location.hash) || '').replace(/^#/, '').toLowerCase(); + wanted = TAB_ALIASES[wanted] || wanted; + if (!wanted) return null; + if (wanted !== 'assistant' && !doc.querySelector('.tab-btn[data-tab="' + wanted + '"]')) return null; + try { storage.setItem('ped_last_tab', wanted); } catch (e) {} + try { win.history.replaceState(null, '', win.location.pathname + win.location.search); } catch (e) {} + return wanted; +} +try { openTabFromHash(window, document, window.localStorage); } catch (e) {} + // ── Client-side error logging ────────────────────────────── function sendError(data) { try { diff --git a/test/open-tab-from-hash.test.js b/test/open-tab-from-hash.test.js new file mode 100644 index 00000000..85015cfa --- /dev/null +++ b/test/open-tab-from-hash.test.js @@ -0,0 +1,41 @@ +// app.pedshub.com/#resources opens My Resources. The app has no routes and is +// not getting any; a hash is read once, stored as the last tab, and dropped. +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); + +const src = fs.readFileSync(path.join(__dirname, '..', 'public/js/app.js'), 'utf8'); +const start = src.indexOf('var TAB_ALIASES'); +const end = src.indexOf('try { openTabFromHash(window'); +const openTabFromHash = new Function(src.slice(start, end) + '; return openTabFromHash;')(); + +function world(hash, tabs) { + const stored = {}; const replaced = []; + return { + win: { location: { hash, pathname: '/', search: '?sso=ok' }, history: { replaceState: (_s, _t, url) => replaced.push(url) } }, + doc: { querySelector: sel => tabs.some(t => sel === '.tab-btn[data-tab="' + t + '"]') ? {} : null }, + storage: { setItem: (k, v) => { stored[k] = v; } }, stored, replaced + }; +} + +test('#resources stores My Resources as the tab to open and clears the hash', () => { + const w = world('#resources', ['encounter', 'myresources']); + assert.equal(openTabFromHash(w.win, w.doc, w.storage), 'myresources'); + assert.equal(w.stored.ped_last_tab, 'myresources'); + assert.deepEqual(w.replaced, ['/?sso=ok'], 'the query survives, the hash goes'); +}); + +test('a tab that does not exist, or no hash at all, changes nothing', () => { + for (const hash of ['#nonsense', '', '#']) { + const w = world(hash, ['encounter']); + assert.equal(openTabFromHash(w.win, w.doc, w.storage), null); + assert.deepEqual(w.stored, {}); assert.deepEqual(w.replaced, []); + } +}); + +test('the hash is honoured by the same code that restores the last tab after sign-in', () => { + const auth = fs.readFileSync(path.join(__dirname, '..', 'public/js/auth.js'), 'utf8'); + assert.match(auth, /localStorage\.getItem\('ped_last_tab'\)/); + assert.match(src, /try \{ openTabFromHash\(window, document, window\.localStorage\); \} catch/); +});