fix(apps): back button follows real history; screens re-read on entry
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ddcc8c5649
commit
bfbac92df5
@@ -1085,3 +1085,94 @@ the order audit trail. Full api suite **308/308, 28 suites**; both apps
|
||||
`vue-tsc` clean.
|
||||
|
||||
_No data was deleted in this entry._
|
||||
|
||||
---
|
||||
|
||||
## 2026-10-12 — Apps — back navigation and read freshness (NAV-01…04, READ-01…03)
|
||||
|
||||
- **What:** The back arrow follows real history instead of a fixed route; three
|
||||
`push`es that stranded the user became `replace`; screens showing
|
||||
server-owned data re-read it on entry.
|
||||
- **Why:** owner report — «از صفحه پروفایل وارد صفحه لاگین میشم بک میزنم میره به
|
||||
صفحه اصلی در صورتی که باید بیاد توی صفحه پروفایل»— with the instruction to
|
||||
audit the whole structure, not just that case. Seven defects, four of them
|
||||
reachable in ordinary use.
|
||||
- **Reference:** the fixed-destination back arrow is inherited —
|
||||
`henza_pwa/.../profile.vue:6`, `Orders.vue:8`, `transactions.vue:7`,
|
||||
`favorits.vue:5` etc. all `$router.push({name:'userPage::hasSubRoute'})`. Not
|
||||
replicated (D4: behaviour, not breakage).
|
||||
- **Target:** both apps' `components/ui/PageBar.vue`, `stores/navStore.ts`,
|
||||
`router/index.ts`, `main.ts`, `views/user/LoginView.vue`,
|
||||
`views/user/CompleteProfileView.vue`, `views/user/OrdersView.vue`,
|
||||
`views/user/TransactionsView.vue`, `views/user/DashboardView.vue`,
|
||||
`views/HomeView.vue`, `stores/queryCache.ts`.
|
||||
- **Matrix row:** NAV-01…04, READ-01…03 in `ISSUES.md`.
|
||||
- **Commit:** see below.
|
||||
|
||||
**The reported bug, precisely.** `/user` has no auth guard, so a guest sees the
|
||||
dashboard and taps «ورود / ثبتنام». History is then dashboard → login. Back
|
||||
should return to the dashboard, but `LoginView` declared `back-to="home"` and
|
||||
`PageBar` **pushed that name**, discarding history. The same was true of 14 of
|
||||
the 15 `PageBar` uses in each app — only `SearchView` used `router.back()`. So
|
||||
this was never one screen's bug.
|
||||
|
||||
**`backTo` is now a fallback, not a destination.** The 14 declarations are kept
|
||||
untouched, because they are the right answer when there is genuinely nowhere to
|
||||
go back to — a shared link, a refresh. What changed is that history wins when it
|
||||
exists.
|
||||
|
||||
**Answering "is there anywhere to go back to?" honestly.** `history.length` is
|
||||
the obvious choice and it is wrong: it counts entries from before the app was
|
||||
opened, so a link opened in a fresh tab looks like it has history and the back
|
||||
button walks the person out of the app. `navStore` instead records the history
|
||||
`position` of the app's first navigation and compares the current one against it.
|
||||
Chosen over a navigation counter because a counter only grows — after one
|
||||
back-and-forward it would claim depth that is not there. Confirmed vue-router
|
||||
writes `position` on every entry (`vue-router.mjs:97,109,145`) rather than
|
||||
assuming it.
|
||||
|
||||
**Three `push`es that should always have been `replace`.** Each left behind a
|
||||
page the guards immediately bounce, so "back" was either a no-op or a trip home:
|
||||
after sign-in the login screen stayed in history (the worst of the three — the
|
||||
file already honoured `?redirect=`, so sign-in and back disagreed about where the
|
||||
person belonged); the 401 handler kept the page whose request had just failed;
|
||||
and complete-profile kept a form that is unreachable once the documents are in.
|
||||
`CartView`'s post-order push was **left alone** — back lands on the emptied
|
||||
cart, which is a real screen rather than a dead end.
|
||||
|
||||
**Read freshness, by the owner's rule** («هربار با رفتن به اون صفحه اون مقادیر
|
||||
مورد نیاز رو درخواست بزنه») — chosen over the TTL that had been proposed,
|
||||
and the better call: a TTL guesses how long data stays true, while entering a
|
||||
screen is when it actually matters.
|
||||
|
||||
The worst finding here was **OrdersView**: 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 one read the cache. Order status changes on the admin side
|
||||
and nothing in the app invalidates it, so the list could be stale for an entire
|
||||
session. Ledger entries had the same shape and a stronger argument — 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 two screens that show it
|
||||
prominently, and `HomeView` being kept alive meant once per app launch.
|
||||
|
||||
**What is deliberately NOT re-read: the catalogue.** By the owner's own
|
||||
reasoning («سربار برای سرور نداره»): it is the whole active product list in one
|
||||
response, it changes when an admin edits it, and pull-to-refresh and placing an
|
||||
order already invalidate it. Re-fetching it on every tab switch is exactly the
|
||||
needless load the ruling exists to avoid. So on Home only the price is re-read.
|
||||
|
||||
**On the cache layer, which the owner also asked about.** Its structure is
|
||||
sound, and two things that are easy to get wrong are right: concurrent callers
|
||||
share one in-flight promise, and a response arriving after an `invalidate` is
|
||||
returned to its caller but not stored, so a write cannot lose a race against a
|
||||
read already in flight. Its one real gap was that nothing ever went stale, which
|
||||
the above fixes at the only point where staleness is observable. The policy is
|
||||
now written at the top of `queryCache.ts` so a TTL is not re-introduced later.
|
||||
|
||||
**Verification.** Neither app has a test runner, so: `vue-tsc -b` clean on both,
|
||||
and each finding was confirmed by reading the code that causes it — the
|
||||
keep-alive list against each view's hooks, every `router.push` against the
|
||||
guards, and `grep back-to=` across both apps. api suite untouched at 308/308.
|
||||
⚠️ Not click-tested in a browser; the back-button behaviour in particular is
|
||||
worth one pass by hand.
|
||||
|
||||
_No data was deleted in this entry._
|
||||
|
||||
Reference in New Issue
Block a user