Step 3 of docs/CATALOG-PAGINATION-PLAN.md. Step 2 added the targeted
routes; these are the five callers that were fetching all ~16k products
to find a handful of rows:
product screen whole catalogue, then find(id) -> GET /products/:id
cart re-sync whole catalogue for 2-3 lines -> ?ids=
favourites whole catalogue, filtered here -> ?ids=
home categories categories derived from it -> /products/facets
order row labels joined on variantId -> included with the order
Three of these were not merely slow, they were going to be wrong.
Favourites filtered the loaded catalogue, so once the list pages a
favourite outside the loaded slice silently disappears. Home categories
were derived the same way, so a category whose products all sit further
down would vanish from the home screen. Order rows looked their title up
in the catalogue, which ALREADY failed for a product that had since left
the storefront — exactly the old orders history exists for — and
rendered a blank row.
Order labels went to the server rather than to another id lookup: the
item carries productVariantId, not a product id, so ?ids= could not
answer it. One nested include in getOrders returns the variant, its
product and the product's first image. Deliberately NOT filtered by
storefront visibility, so a de-listed product still shows what was
bought; asserted in the new spec. OrderItemCard is synchronous now — no
fetch, no onMounted, the row renders complete on its first frame.
variantLookup is deleted from catalogStore rather than left behind: it
existed only to label order rows, and keeping it would have kept a
reason to load the full catalogue.
getProductsByIds chunks at 100 because the route caps ids there and
drops the rest. A partial answer reads to callers as "these products are
no longer on the storefront" — the cart would silently drop valid lines.
Favourites already run to 874 rows in the imported data (FAV-01).
A failed cart re-sync now leaves the cart alone: it is the person's own
data in localStorage, and dropping lines because the network blinked is
worse than a slightly stale price. The product screen tells a real 404
from a failed request, so only the former renders the not-found page.
One bug introduced and caught while doing this: moving the home
categories to /facets meant memory.init() no longer loaded the catalogue
as a side effect, and the home rails read catalog.products — the home
screen would have rendered empty rails. HomeView now loads it
explicitly. Found by grepping every caller of configStore.init, which is
also how I know HomeView is the only one.
Still on the unpaged list by design: the home rails and the
search/newest lists. Moving those onto paged reads is step 4, kept
separate because it is the risky part — debounce, loading state, and
telling "no results" from "still loading".
api 334/334 across 30 suites; both apps vue-tsc clean. getOrders has a
spec now (3 cases) because its include is a contract both apps depend on
and the method had no test at all. Nothing click-tested — the cart
re-sync and the favourites screen are the two worth a manual pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Reported missing by the owner: in the reference an expert could open any
of their referred customers and use the app AS them — browse, see
balance and debt, place orders — «یکجورایی انگار با اکانت اون ها لاگین
کرده». D21 had deliberately left it out as a session-hijack primitive.
The owner asked for the capability back with the implementation left to
judgement, so the scope is the reference's and the mechanism is not.
Scope: everything, as the reference had it. Its axios interceptor
swapped the bearer token, so every request went as the customer,
document upload included. No route is closed in act-as mode.
Mechanism: the reference minted and handed over the customer's own
permanent api_token. Here the expert keeps their own token and sends
X-Act-As-Customer, resolved in one place — CustomerJwtStrategy
.validateExpert. That single point is why all 17 customer routes work
unchanged: validate() already returns a principal whose `id` is a
Customer id and every customer route reads it. It also fixes four real
defects of the original:
- ownership is re-checked on EVERY request, with referredByAgentId in
the same where clause as the id, so another expert's customer and a
non-existent id both answer 403, and a customer moved to another
expert loses access at once rather than at token expiry;
- nothing durable is handed out — the reference's token sat in
localStorage with no expiry and kept working after the expert left;
- adminUserId survives, so an order placed this way is attributable;
in the reference the request was literally the customer's;
- 403 rather than 401, so the app clears the act-as state instead of
logging a perfectly valid session out.
Performance: one findFirst on the primary key, only when the header is
present, and it replaces the trading-account lookup rather than adding
to it.
Three things the reference never needed, because it swapped the whole
session. queryCache's `me:` keys, favouritesStore and wageQuoteStore all
dropped their data on a TOKEN change, which act-as does not cause — so
an expert would have seen their own profile, orders, balance, ledger and
favourites inside the customer's account. The wage-quote one is the
money case: making charge is per customer, so the figure on screen would
not have been the one the order was priced at. And uploadDocument builds
its own headers, so an expert's upload would have been filed against
their own account, silently.
Added with no reference equivalent: a permanent «در حال مشاهده بهعنوان
…» bar. There, impersonation was invisible once the token had been
swapped, and placing an order on the wrong customer's gold debt was one
tap away. It is also the exit — the reference's only way out was a
logout that ended the expert's own session too. The cart is cleared in
both directions.
CustomerJwtStrategy had no test coverage at all and is now where this
feature's whole authorisation boundary lives: act-as-customer.spec.ts
covers 11 cases, including the header being ignored on a customer token,
the no-header path asserting the extra query does not run, and a revoked
session short-circuiting before the header is read.
api: 308/308 tests, 28 suites; both apps vue-tsc clean.
اپ: کارشناس فروش میتواند وارد حساب مشتریان خود شود، وضعیتشان را ببیند و از طرفشان سفارش ثبت کند.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- The build number is the commit count, so every build raises it with no
file to bump. scripts/app-version.mjs bakes it into each front-end
(__APP_BUILD__) and writes dist/version.json with that app's notes.
- Notes come from commit messages: only Persian lines that start with
one of the four app tags are shown, each to its own app
(docs/RELEASE-NOTES.md).
- Both customer apps and the admin panel poll version.json (every 5
minutes and on return to the tab, bypassing caches). A newer build opens
a modal that cannot be dismissed. «بهروزرسانی» unregisters the service
worker, empties Cache Storage and reloads; localStorage (sign-in, cart,
settings) is kept.
- Replaces legacy's version dialog, which compared against a
package.json version that was never bumped and so never opened, and
useRefresh, which swapped in a new service worker silently.
- nginx (all three apps and nginx-spa.conf): index.html, sw.js and
version.json are never cached. sw.js used to fall under the 6-month rule,
which could pin a browser to an old release.
Checked: served with the real nginx config, a newer version.json opens
the modal with its notes; the same build does not.
اپ: اجرت هر کالا حالا همان است که در فاکتور حساب میشود؛ درصد خود شما و تخفیفها هم در آن آمده است.
اپ: علاقهمندیهای شما ذخیره میشوند و در هر دستگاهی که وارد شوید دیده میشوند.
اپ: از این به بعد هر نسخهی تازه با فهرست تغییراتش اعلام میشود.
پنل: بخش تازهی «اجرت»: فرمول اجرت را با یک نمونه امتحان و بعد تأیید کنید.
پنل: شهرها مثل قبل بهصورت درختاند (کشور، استان، شهر، محله)؛ شهری که درصد ندارد درصد شهر بالاتر را میگیرد.
پنل: «تاریخ تولد» به فرم مشتری عمده و تکی برگشت.
پنل: در گزارش تجمیعی سفارشها، دکمهی «دریافت فایل اکسل» اضافه شد.
پنل: رد احراز هویت مشتری حالا دلیل هم میگیرد.
پنل: با تأیید یا رد سند مالی، به مشتری پیامک میرود؛ در پیامک گروهی، %name با نام مشتری عوض میشود.
پنل: از این به بعد هر نسخهی تازهی پنل با فهرست تغییراتش اعلام میشود.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner approved the deletions. Found by walking imports from each app's
main.ts; sw.ts stays (the PWA plugin's entry).
- user-app: 8 unreachable components, loadingStore.
- user-app-v2: 11 unreachable components, loadingStore.
- both: the unused validators and price helpers, and the gsap and
@ant-design/icons-vue packages.
- the stray root user-app-v2/ folder (an old package.json and
vite.config.ts that nothing referenced).
Both apps type-check clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Applies to both user-app and user-app-v2. Logic and structure are shared;
each app keeps its own look.
Appearance settings
- New "تنظیمات ظاهری" page (/settings/appearance), opened from the
dashboard and available without login. It offers a light/dark/system
theme and a "fast & light" motion switch.
- appearanceStore saves the choice to localStorage. A small inline boot
script in index.html applies it before first paint so there is no
white flash. The "system" option follows OS changes live.
- New ToggleSwitch component.
Dark mode
- Theme LESS tokens now compile to CSS custom properties. Light/dark
values come from a generated virtual:palette.css (themeConfig/
appearance.ts, with colors.json + new colors.dark.json).
- New LESS alpha() plugin replaces fade() on theme tokens.
- New semantic tokens: page, onInk, heroSurface, hairlineStrong,
photoMat, photoStage.
- The dark palette is layered (page < card < panel < border) so
surfaces separate without shadows.
- Hard-coded colors were replaced by tokens according to their role.
- antd inputs/text get dark overrides; vant uses its own
.van-theme-dark.
- Light mode looks the same as before.
Reduced motion
- With html.motion-reduced, transitions are capped at 120ms, keyframe
loops stop, and backdrop blurs and permanent will-change are dropped.
- Spinners and the toast progress bar (which closes the toast) are
exempt via .keep-motion.
- Carousel autoplay, the v1 Lottie and the product morph honor the
setting.
Product detail
- Opening a product is now a container-transform morph from the tapped
card (ProductMorph). It replaces the bottom Drawer and uses composited
transform/opacity only.
- The detail view is seeded with the card's data, so it no longer
opens blank and jumps.
Filters and sheets
- Filters are now a full-width bottom sheet. Selections stay a draft
until applied, so the result list no longer re-renders while
dragging the slider.
- DropDown collapse animates grid-template-rows instead of a capped
max-height.
- v2 IslandSheet is restyled to the v2 design language (API unchanged).
Fixes
- filtersStore.reset() shallow-copied shared arrays, so clearing the
filters never cleared the selected brands.
Remove DialogVant/NotifyVant and add Toast and Sheet components with
useToast, useReveal, and usePageTransition composables. Rework
Navigation, RegisterView, and styles across both apps; rebuild dist assets.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ports "تکمیل پروفایل" end to end (D22).
API
- POST customer/me/complete-profile — three required document ids, the
customer taken from the token, verification step set to the reference's
LIVE value (approved outright; its `= 1` line is commented out at :83 in
favour of `= 2` at :85). Both SMS messages byte-identical and non-blocking
per D20.
- POST customer/me/documents — the reference let the app upload through the
same file/create the admin used, inside its auth:api-customer group
(routes/api.php:95); the target's copy is admin-guarded, so the page could
not have uploaded at all. Reuses AdminUploadsService unchanged, so both
paths share one size limit, MIME whitelist and naming scheme (UPL-01).
- PROFILE_ADMIN_MOBILES replaces the reference's four hardcoded personal
numbers; blank means nobody is notified and the upload still succeeds.
Three reference behaviours deliberately NOT ported, all in ISSUES.md:
- SEC-14: `$customer->update($request->all())` mass-assigned the whole request
body, letting the app write its own credit limit, making charge, gold debt,
active flag or mobile. Replaced with a three-field whitelist.
- SEC-15: four real personal mobiles hardcoded in the source.
- APP-03: refresh()'s unterminated setInterval page reload.
Apps (both, template+script byte-identical, per-app styling only)
- CompleteProfileView + SingleImageUploader
- route /complete_profile, redirecting home once the documents are in, as the
reference's guard did (router/index.ts:266)
- the entry point on the profile screen, shown only while they are missing
(profile.vue:96-102)
Also (D23): JWT_CUSTOMER_SECRET now warns loudly at boot when unset. It falls
back to JWT_SECRET, which signs app and admin tokens with the same key and
leaves the `typ` check as the only barrier — and an expert's app token carries
their AdminUser id in `sub`. Warn rather than refuse to boot (D19, lowest
risk); DELIVERY.md lists it as the first pre-release step (REL-03).
Verified: build 5/5 · jest 45/45 · complete-profile 38/38 (new) ·
expert login 36/36 · apps smoke 43/43 · admin smoke 43/43 ·
B2B scan 34 ok/0 broken · typecheck clean on all four apps.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
D20: OTP_MASTER_CODE login bypass read only from the environment, disabled when
unset, audited on every use, with a boot banner while armed. SMS no longer
aborts the boot in production — it warns and declines silently, so no message
can stop an operation.
D19: port Utility::faTOen onto admin-entered mobiles (before the uniqueness
lookup, which could previously be bypassed with Persian digits), and the
create-only welcome SMS, byte-identical to the reference and fire-and-forget.
user-app-v2 was missing the cart stock ceiling entirely: capQty/maxQty, the
un-orderable guard and the qty clamp are ported, leaving v2's own design intact.
The two cart stores are now byte-identical. isVariantOrderable and the
orderable-first default selection ported too.
jest 45/45; typecheck clean on api, admin-panel, user-app, user-app-v2.
[ref: henza_server/app/Http/Controllers/Adm/AdminController.php:333,348,364,378-387]
[ref: henza_server/app/Models/Utility.php:26]
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Extract product image gallery into a dedicated GallerySlider component
with swipe/tap support
- Add a teleported fullscreen image preview (tap to open, thumbnail strip,
body scroll lock) to SingleProduct
- Restore the Trust section that was accidentally commented out
- Swap back/favorite button positions and recenter the image counter
- Fix Icon.vue: tempIcon was a static ref set once at setup, so it never
updated on prop change when Vue reused the component instance (e.g. cart
qty steppers toggling trash/minus icons); switched to computed + watch
- Rename brand text from "منتزر" to "هنزاگلد"
- Fix a CSS typo in user-app-v2 SingleProduct (rgba alpha value)
- Rebuild dist assets
DashboardView ("میز کار"): Rebuilt from scratch with a new structure, distinct from the old card+grid layout but visually aligned with HomeView (same blurred bar, @ink/@wine palette, staged reveal animation on mount):
Single hero card (ink→wine gradient) showing balance, gold/fiat debt, and identity — no quick-action icons inside it anymore.
Live gold rate folded into a slim inline row at the bottom instead of a titled section.
Iterated shadow/spacing on the hero card per feedback (softer, less clipped).
DialogVant / DialogVantOneButton (shared modal components, used in 7+ call sites: Dashboard, Cart, SingleProduct, Register, App): Replaced Vant's default dialog chrome with the app's own rounded-card language — circular icon badge for the illustration, pill-shaped primary/secondary buttons. No call-site markup changed.
LoginView: Rebuilt from scratch — full-bleed dark hero (brand + step copy) overlapped by a floating white form sheet, animated crossfade transition between the mobile-number and OTP steps, same staged entrance animation as Home/Dashboard. All auth logic (API check, OTP stub, session login) unchanged.
- Fix silent redirect-to-home for unauthenticated users on protected routes;
now redirects to login with a return path
- Restyle OrderCard, SearchInput, Filters, DropDown, and product card image
sizing/centering to match the app's LESS-token design language
- Fix ant-design Slider handle placement/drag under RTL and seed a valid
default range so handles actually render
- Fix chip-row edge padding and top spacing across Orders/Cart/Search lists
- Add pull-to-refresh to the dashboard and fix its header/padding to match
the fixed PageBar pattern
- Remove the floating WhatsApp button from index.html
Phase 2, completing the app-wide rollout of the home/product-detail design
language (LESS colour tokens, BEM class names, hairline-bordered cards,
SectionHead, dashed empty states) onto the account side: dashboard, orders,
transactions, profile, login and register.
Reuse over re-implementation: the dashboard's live rate block is now
components/home/RateStrip.vue rather than a second hand-built strip, and
every screen uses components/ui/PageBar.vue for its chrome.
OrderCard and OrderItemCard are restyled to match. OrderItemCard drops its
always-empty note row: our OrderItem schema has no note field (legacy's
per-item note was a real column there; ours is not), so it could only ever
print a dash.
OrdersView swaps ant Tabs for the same scrolling chip rail the rest of the
redesign uses, and its full-screen Modal with hand-written CSS overrides for
a bottom Drawer, matching how every other detail surface opens.
Deleted AppHeader.vue, orphaned once every screen moved to PageBar. Its
shutter animation was driven by transitionFlag, which nothing reads any
more — the router afterEach comment claiming otherwise is corrected rather
than left asserting something untrue, and the now-inert half is flagged in
place. ScrollFetchSearch loses its isFav prop: it gated two empty-state
branches that rendered the identical image, and its only caller
(FavoritesView) now renders its own.
Behaviour is preserved throughout. Orders still reads GET
/customers/:id/orders and filters client-side; transactions still reads
GET /customers/:id/ledger-entries with legacy's CREDIT/DEBIT colour meaning
intact; profile stays read-only over real fields; login keeps the real
customer lookup plus the documented OTP stub; register stays stubbed with
copy that does not claim an account was created.
Verified in a real browser against the running API and native database:
dashboard 4 tiles with a real balance, orders 1 real order card, profile 4
real spec rows, transactions the correct empty state for a customer with no
ledger rows, and login/register confirmed with the session cleared (the
guard bounces a signed-in user away from /login, which is why they needed a
separate pass). No console errors, no horizontal overflow. vue-tsc and vite
build both clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Applies the home/product-detail design language (LESS colour tokens, BEM
class names, hairline-bordered cards, SectionHead, dashed empty states) to
the screens a shopper actually walks through: search, collection, newest,
cart and favorites.
Two shared pieces are extracted so the language stays consistent rather than
re-typed per screen:
- components/ui/PageBar.vue — the sticky translucent bar with blur and a
hairline, lifted from the home screen's own header so every other screen
gets identical chrome instead of the old dark AppHeader.
- components/product/ProductCard.vue — the catalogue workhorse card. It is
deliberately a sibling of home/ShowcaseCard (which keeps its editorial "wide"
variant and badge) and additionally surfaces stock, which the
catalogue needs and the home screen does not.
Deleted as orphaned once nothing imported them: ProductList,
ProductVerticalFull, ProductVertical, ProductHorizontal, CollectionCard —
all Tailwind utility soup carried over from the legacy PWA.
Behaviour is preserved throughout; only chrome and cards change. The cart
still posts to the real POST /orders with wagePercent from each item's
pinned variant, still gates on a real session, and still keeps the note
drawer and success step. Search keeps its client-side filtering, the Filters
sheet, category chips and collection tiles. No rial price is printed
anywhere, for the same reason as the rest of the redesign: a finished
piece's price is the live gold rate x weight plus wage, computed at order
time and never stored on the product.
FIXED a regression this redesign introduced before it shipped: dropping
ScrollFetchSearch from the search grid meant all 205 catalogue products
mounted at once (205 cards, 205 images). Both search and newest now render
a 20-item window that grows as the user reaches the end.
Verified in a real browser against the running API and native database, not
just a green build: home 13 cards, search 20 -> 40 -> 60 -> 80 on scroll,
newest 20 -> 40 -> 60, cart empty state, favorites 3 cards behind a seeded
session. All images loaded (0 broken), no horizontal overflow, no console
errors beyond pre-existing vue3-lazyload noise that predates this work.
vue-tsc and vite build both clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Design language introduced here and reused across both screens:
- LESS colour tokens (@ink/@inkSoft/@inkMuted/@inkFaint, @wine/@wineSoft,
@canvas/@canvasSoft, @hairline/@hairlineSoft) added to
themeConfig/colors.json and injected globally via vite's modifyVars, so
scoped <style lang="less"> blocks resolve them with no import
- BEM-ish scoped class names, hairline-bordered cards at 14-18px radii,
ink surfaces with a wine accent, dashed-border empty states
- components/home/: SectionHead, ShowcaseCard, ShowcaseSlider, CategoryRail,
RateStrip, AccountStrip
Data honesty is deliberate throughout: no rial price is printed for a piece,
because a finished item's price is the live gold rate x weight plus wage,
computed at order time and never stored on the product. Only genuinely
present fields are shown (weight, making-charge percent, stock, category,
brand). Categories and the hero are derived from the real catalogue, so an
empty catalogue renders an empty state rather than placeholder tiles.
Also fixes a real bug in the product detail view: it ran, at module scope,
history.pushState + window.onpopstate = () => history.go(1). Because the
drawer mounts its slot eagerly, that fired for every product card on the
home screen and permanently hijacked the back button for the session.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>