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 @@