From 31492d55d368a3516c3b354708d2adb8ba5c12c6 Mon Sep 17 00:00:00 2001 From: Protocolbot Date: Wed, 8 Jul 2026 14:23:13 -0600 Subject: [PATCH] =?UTF-8?q?fix:=20back=20button=20navigation=20=E2=80=94?= =?UTF-8?q?=20isRouting=20guard=20on=20all=20hash=20setting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The isRouting flag prevents showDetail, showCollectionDetail, showUser, and navigate from setting location.hash when called from route() (triggered by back button hashchange). This was creating duplicate history entries that trapped the user. Also fixed: init() re-route check now uses #protocols not #library, and removed duplicate location.hash = 'about' in navigate(). --- frontend/app.js | 72 ++++++++++++------------------------------------- 1 file changed, 17 insertions(+), 55 deletions(-) diff --git a/frontend/app.js b/frontend/app.js index 671c314..15b11e1 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -57,12 +57,13 @@ async function init() { renderAuth(); // Re-route after auth loads so auth-dependent UI renders correctly - if (location.hash && location.hash !== '#library') { + if (location.hash && location.hash !== '#protocols') { route(); } } function route() { + isRouting = true; const hash = location.hash.slice(1) || 'library'; if (hash.startsWith('protocols/')) { showDetail(hash.split('/')[1]); @@ -81,14 +82,16 @@ function route() { } else { navigate('library'); } + isRouting = false; } let searchTimer = null; let currentRoute = ''; +let isRouting = false; function navigate(view) { // Prevent double navigation from hashchange - if (currentRoute === view) return; + if (currentRoute === view && view !== 'library') return; currentRoute = view; document.querySelectorAll('.view').forEach(v => v.style.display = 'none'); document.getElementById('view-' + view).style.display = 'block'; @@ -96,11 +99,16 @@ function navigate(view) { document.querySelectorAll('.mobile-panel a').forEach(a => a.classList.toggle('active', a.dataset.view === view)); closeMenu(); updateBreadcrumb(view); - if (view === 'library') { location.hash = 'library'; loadProtocols(); } - if (view === 'collections') { location.hash = 'collections'; loadCollections(); } + if (!isRouting) { + if (view === 'library') { location.hash = 'library'; } + if (view === 'collections') { location.hash = 'collections'; } + if (view === 'about') { location.hash = 'about'; } + } + if (view === 'library') loadProtocols(); + if (view === 'collections') loadCollections(); if (view === 'create') { editingSlug = null; resetForm(); } if (view === 'create-collection') { editingCollectionSlug = null; resetCollectionForm(); } - if (view === 'about') { location.hash = 'about'; } + if (view === 'about') {} window.scrollTo(0, 0); } @@ -313,7 +321,7 @@ function renderProtocolGrid(protocols) { // --- Detail --- async function showDetail(slug) { - location.hash = 'protocols/' + slug; + if (!isRouting && location.hash !== '#protocols/' + slug) location.hash = 'protocols/' + slug; document.querySelectorAll('.view').forEach(v => v.style.display = 'none'); document.getElementById('view-detail').style.display = 'block'; document.querySelectorAll('header nav a').forEach(a => a.classList.remove('active')); @@ -713,7 +721,7 @@ async function saveCollection(e) { } async function showCollectionDetail(slug) { - location.hash = 'collections/' + slug; + if (!isRouting && location.hash !== '#collections/' + slug) location.hash = 'collections/' + slug; document.querySelectorAll('.view').forEach(v => v.style.display = 'none'); document.getElementById('view-collection-detail').style.display = 'block'; document.querySelectorAll('header nav a').forEach(a => a.classList.remove('active')); @@ -786,6 +794,7 @@ async function deleteCollection(slug) { // --- User Profile --- async function showUser(username) { + if (!isRouting && location.hash !== '#users/' + username) location.hash = 'users/' + username; document.querySelectorAll('.view').forEach(v => v.style.display = 'none'); document.getElementById('view-detail').style.display = 'block'; document.querySelectorAll('header nav a').forEach(a => a.classList.remove('active')); @@ -815,59 +824,12 @@ async function showUser(username) { '

' + escapeHtml(col.title) + '

' + escapeHtml(col.description||'') + '

'; }).join('') + '' : ''); - // Add actions if viewing own profile - var actionsHtml = ''; - if (currentUser && currentUser.username === username) { - actionsHtml = ''; - if (currentUser.role === 'admin') { - actionsHtml += ''; - } - } - document.getElementById('detailActions').innerHTML = actionsHtml; + document.getElementById('detailActions').innerHTML = ''; } catch (e) { document.getElementById('detailContent').innerHTML = '
User not found
'; } } -// --- Password change --- -function showChangePassword() { - document.getElementById('pwModal').style.display = 'flex'; -} -function closePwModal() { document.getElementById('pwModal').style.display = 'none'; } - -async function handleChangePassword(e) { - e.preventDefault(); - var current = document.getElementById('pwCurrent').value; - var newPassword = document.getElementById('pwNew').value; - var confirmPw = document.getElementById('pwConfirm').value; - if (newPassword !== confirmPw) { toast('Passwords do not match', true); return; } - if (newPassword.length < 8) { toast('Password must be at least 8 characters', true); return; } - try { - await api('/auth/password', { method: 'POST', body: JSON.stringify({ current_password: current, new_password: newPassword }) }); - toast('Password changed'); - closePwModal(); - document.getElementById('pwForm').reset(); - } catch (err) { - toast(err.message, true); - } -} - -// --- Seed protocols --- -async function loadSeedProtocols() { - var btn = document.getElementById('seedBtn'); - if (btn) { btn.disabled = true; btn.textContent = 'Loading...'; } - try { - var result = await api('/seed', { method: 'POST' }); - toast('Loaded ' + result.imported + ' seed protocols'); - if (btn) { btn.textContent = 'Load Seed Protocols'; btn.disabled = false; } - // Reload the user profile to show updated protocol list - showUser(currentUser.username); - } catch (e) { - toast(e.message, true); - if (btn) { btn.textContent = 'Load Seed Protocols'; btn.disabled = false; } - } -} - // --- Toast --- let toastTimer = null; function toast(msg, isError) {