From bfbac92df53d65ba9b00af2dc89d8d2ab47ad560 Mon Sep 17 00:00:00 2001 From: "s.esmaeili" Date: Mon, 12 Oct 2026 12:42:42 +0330 Subject: [PATCH] fix(apps): back button follows real history; screens re-read on entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported: from the profile page, going to login and pressing back landed on home instead of the profile — with the instruction to audit the whole structure rather than that one case. Seven defects, four reachable in ordinary use. NAVIGATION PageBar's back arrow pushed a FIXED route name (`backTo`) and ignored history, so it could not know where the person came from. That was true of 14 of the 15 PageBar uses in each app — only SearchView used router.back() — so it was never one screen's bug. Inherited from the reference, which pushed a fixed name in every component's back arrow; not replicated (D4). `backTo` is now a fallback, used only when there is nothing to go back to (a shared link, a refresh). All 14 declarations are kept untouched, because that is exactly when they are right. Answering "is there anywhere to go back to?" needs care: history.length counts entries from before the app was opened, so a link opened in a fresh tab would look navigable and back would walk the person out of the app. navStore records the history `position` of the app's first navigation and compares the current one against it — direction-proof, unlike a counter, which only grows and would be wrong after one back-and-forward. Confirmed vue-router writes `position` on every entry rather than assuming it. Three pushes became replaces, each of which left behind a page the guards immediately bounce, making "back" a no-op or a trip home: - after sign-in the login screen stayed in history — the worst, since the same file already honoured ?redirect=, so signing in and pressing back disagreed about where the person belonged; - the 401 handler kept the page whose request had just failed; - complete-profile kept a form that is unreachable once the documents are in. CartView's post-order push is left alone: back lands on the emptied cart, which is a real screen, not a dead end. READ FRESHNESS Owner's rule: fetch what a screen needs when it is opened, rather than caching with a lifetime. Better than the TTL I had proposed — a TTL guesses how long data stays true, while entering the screen is when it actually matters. OrdersView was the worst: it is in the keep-alive list but only loaded in onMounted, so the second and every later visit made no request at all, and the first read the cache. Order status changes on the admin side and nothing here invalidates it, so the list could be stale for a whole session. Ledger entries had the same shape and a stronger reason — they are written by the accounting side, which this app can never learn about. The gold price was forced only by pull-to-refresh on the two screens that show it most prominently. The catalogue is deliberately NOT re-read, by the owner's own reasoning that this must not add server load: it is the entire product list in one response, it changes when an admin edits it, and pull-to-refresh and placing an order already invalidate it. The cache layer itself is sound — concurrent callers share one in-flight promise, and a response arriving after an invalidate is returned but not stored, so a write cannot lose a race against a read in flight. Its one real gap was that nothing ever went stale. The policy is now written at the top of queryCache.ts so a TTL is not re-introduced. Both apps vue-tsc clean. Not click-tested in a browser; the back-button behaviour is worth one manual pass. اپ: دکمه بازگشت به صفحه قبلی واقعی برمی‌گردد و اطلاعات هر صفحه در هر بازدید به‌روز می‌شود. Co-Authored-By: Claude Opus 5 --- .../user-app-v2/src/components/ui/PageBar.vue | 39 +++++++- apps/user-app-v2/src/main.ts | 5 +- apps/user-app-v2/src/router/index.ts | 5 + apps/user-app-v2/src/stores/navStore.ts | 38 ++++++++ apps/user-app-v2/src/stores/queryCache.ts | 21 ++++- apps/user-app-v2/src/views/HomeView.vue | 28 +++++- .../src/views/user/CompleteProfileView.vue | 10 +- .../src/views/user/DashboardView.vue | 11 ++- apps/user-app-v2/src/views/user/LoginView.vue | 6 +- .../user-app-v2/src/views/user/OrdersView.vue | 30 +++++- .../src/views/user/TransactionsView.vue | 14 ++- apps/user-app/src/components/ui/PageBar.vue | 32 ++++++- apps/user-app/src/main.ts | 5 +- apps/user-app/src/router/index.ts | 9 ++ apps/user-app/src/stores/navStore.ts | 38 ++++++++ apps/user-app/src/stores/queryCache.ts | 21 ++++- apps/user-app/src/views/HomeView.vue | 28 +++++- .../src/views/user/CompleteProfileView.vue | 10 +- .../user-app/src/views/user/DashboardView.vue | 11 ++- apps/user-app/src/views/user/LoginView.vue | 6 +- apps/user-app/src/views/user/OrdersView.vue | 32 ++++++- .../src/views/user/TransactionsView.vue | 14 ++- docs/parity/CHANGELOG.md | 91 +++++++++++++++++++ docs/parity/ISSUES.md | 26 ++++++ docs/parity/PROGRESS.md | 2 +- 25 files changed, 498 insertions(+), 34 deletions(-) diff --git a/apps/user-app-v2/src/components/ui/PageBar.vue b/apps/user-app-v2/src/components/ui/PageBar.vue index e18c58e..af5d9ea 100644 --- a/apps/user-app-v2/src/components/ui/PageBar.vue +++ b/apps/user-app-v2/src/components/ui/PageBar.vue @@ -30,13 +30,24 @@ diff --git a/apps/user-app-v2/src/main.ts b/apps/user-app-v2/src/main.ts index 10237f9..4f9397e 100644 --- a/apps/user-app-v2/src/main.ts +++ b/apps/user-app-v2/src/main.ts @@ -42,7 +42,10 @@ app.use(router); setUnauthorizedHandler(() => { useSessionStore().logout(); if (router.currentRoute.value.name !== 'login') { - void router.push({ name: 'login' }); + // `replace`: the page whose request just failed is behind a dead session, + // so leaving it in history only gives the person a back button that returns + // them to it and bounces straight back here. + void router.replace({ name: 'login' }); } }); // Ported from legacy main.ts:32-46 — Vector.svg is both the while-loading and diff --git a/apps/user-app-v2/src/router/index.ts b/apps/user-app-v2/src/router/index.ts index fb19467..8e577a6 100644 --- a/apps/user-app-v2/src/router/index.ts +++ b/apps/user-app-v2/src/router/index.ts @@ -187,6 +187,11 @@ router.afterEach((to) => { const nav = useNavStore(); const transition = useTransitionStore(); + // Remembers where in the browser's history this app instance started, which + // is what PageBar's back button compares against before choosing between a + // real history-back and its fallback route. + nav.noteFirstNavigation(); + transition.setTransitionFlag(false); transition.setTransitionEnded(false); setTimeout(() => { diff --git a/apps/user-app-v2/src/stores/navStore.ts b/apps/user-app-v2/src/stores/navStore.ts index cdb3331..c28a407 100644 --- a/apps/user-app-v2/src/stores/navStore.ts +++ b/apps/user-app-v2/src/stores/navStore.ts @@ -18,6 +18,18 @@ interface NavState { showNav: boolean // legacy passes `undefined` to clear the transition (router/index.ts:270) pageTransition: string | undefined + /** + * The history index this app started at, or null before the first + * navigation. + * + * Answers "is there anywhere inside this app to go back to?", which + * `history.length` cannot: it also counts entries from before the app was + * opened, so it says yes for a link opened in a fresh tab and the back button + * would leave the app. Comparing the CURRENT history position against the one + * we started at is direction-proof — it stays correct however many times the + * person goes back and forward, where a plain counter would only ever grow. + */ + entryIndex: number | null } export const useNavStore = defineStore('nav', { @@ -32,8 +44,34 @@ export const useNavStore = defineStore('nav', { activeLink: 1, showNav: true, pageTransition: 'right', + entryIndex: null, }), + getters: { + /** + * True when `router.back()` lands somewhere inside this app rather than on + * whatever the person was looking at before they opened it. + */ + canGoBack: (state) => { + if (state.entryIndex === null) return false + const current = window.history.state?.position + // `position` is written by vue-router on every entry it creates. If it is + // missing (a hand-written history entry, an old browser), assume we must + // not leave the app and let the caller fall back. + return typeof current === 'number' ? current > state.entryIndex : false + }, + }, actions: { + /** + * Records where in history this app instance began. Called once, on the + * first navigation — every later entry sits above it, so anything above is + * ours to go back through. + */ + noteFirstNavigation() { + if (this.entryIndex !== null) return + const position = window.history.state?.position + this.entryIndex = typeof position === 'number' ? position : 0 + }, + setNavVisibility(data: boolean) { this.showNav = data }, diff --git a/apps/user-app-v2/src/stores/queryCache.ts b/apps/user-app-v2/src/stores/queryCache.ts index b7693a7..7643b90 100644 --- a/apps/user-app-v2/src/stores/queryCache.ts +++ b/apps/user-app-v2/src/stores/queryCache.ts @@ -9,8 +9,25 @@ // - `force` → always asks the server (pull-to-refresh, the // profile tab's balance, reaching the end of a list) // -// Entries never expire on a timer: data changes when the user asks for it or -// when a write here makes it wrong, and those writes call `invalidate`. +// Entries never expire on a timer, deliberately (owner ruling 2026-10-12: «هربار +// با رفتن به اون صفحه اون مقادیر مورد نیاز رو درخواست بزنه … هم سربار برای سرور +// نداره، هم درخواست الکی نمیخوره، هم دیتاها به‌روز میمونه»). A TTL guesses how +// long data stays true; entering a screen is the moment it actually matters. +// +// So the policy is: +// +// - a screen that shows server-owned data re-reads it with `force` when it is +// opened (`onMounted`, plus `onActivated` for the kept-alive views), while +// `peek` paints the previous copy instantly so the refresh is invisible; +// - the cached copy exists for that first paint and to collapse concurrent +// callers into one request, NOT to avoid asking again; +// - the catalogue is the exception: it is the whole product list in one +// response, changes when an admin edits it, and is already invalidated by +// pull-to-refresh and by placing an order. +// +// `force` is what makes this work for anything changed OUTSIDE the app — an +// order's status, a ledger entry, the gold price. Nothing here can know those +// changed, so not asking means never seeing them. // // Entries live in a plain Map, not reactive state: the catalogue is large and // making it deeply reactive would cost more than the request it saves. diff --git a/apps/user-app-v2/src/views/HomeView.vue b/apps/user-app-v2/src/views/HomeView.vue index 73a58c2..75d8f5f 100644 --- a/apps/user-app-v2/src/views/HomeView.vue +++ b/apps/user-app-v2/src/views/HomeView.vue @@ -80,7 +80,7 @@