diff --git a/apps/user-app-v2/src/router/index.ts b/apps/user-app-v2/src/router/index.ts index 5e0a1f7..fb19467 100644 --- a/apps/user-app-v2/src/router/index.ts +++ b/apps/user-app-v2/src/router/index.ts @@ -4,6 +4,7 @@ import { useNavStore } from '../stores/navStore'; import { useTransitionStore } from '../stores/transitionStore'; import { api } from '../api/client'; import { useCachedReads } from '../api/cachedReads'; +import { toast } from '../composables/useToast'; // Legacy lazy-loaded every route component (`component: () => import(...)`) so // the initial bundle only carries the entry route. Matched here. @@ -131,7 +132,13 @@ router.beforeEach(async (to) => { if (to.name === 'login' && session.isLoggedIn) { return { name: 'home' }; } + // D3 — see the same guard in user-app's router for the full reasoning. + // Legacy showed the guest «ابتدا وارد شوید» (henza_pwa/src/router/index.ts:259) + // and the target redirected silently. The owner kept the target's `redirect` + // so signing in returns the guest to the page they wanted, which legacy did + // not do — «عمداً تکرار نشد — D3/الف». if (to.meta.requiresAuth && !session.isLoggedIn) { + toast.info('ابتدا وارد شوید'); return { name: 'login', query: { redirect: to.fullPath } }; } diff --git a/apps/user-app-v2/src/stores/configStore.ts b/apps/user-app-v2/src/stores/configStore.ts index dff51db..d97cfb6 100644 --- a/apps/user-app-v2/src/stores/configStore.ts +++ b/apps/user-app-v2/src/stores/configStore.ts @@ -17,6 +17,10 @@ // HomeSlider's popup/link/discount click handling works unchanged. // // Not ported (no backend): promotionList, ignoreList, priceList, isLoaded. +// +// `isLoaded` — «عمداً تکرار نشد — D3/ب» (owner, 2026-10-12). Legacy used it as a +// router guard against a bootstrap request this app does not have; see the same +// note in user-app's configStore for the full reasoning (D4). import { defineStore } from 'pinia' import { useCatalogStore } from './catalogStore' import shippingArt from '../assets/img/shipping-and-delivery.png' diff --git a/apps/user-app/src/router/index.ts b/apps/user-app/src/router/index.ts index 690e6bc..6aa00f2 100644 --- a/apps/user-app/src/router/index.ts +++ b/apps/user-app/src/router/index.ts @@ -5,6 +5,7 @@ import { useTransitionStore } from '../stores/transitionStore'; import { api } from '../api/client'; import { useCachedReads } from '../api/cachedReads'; import { afterPageLeave } from '../composables/usePageTransition'; +import { toast } from '../composables/useToast'; // Legacy lazy-loaded every route component (`component: () => import(...)`) so // the initial bundle only carries the entry route. Matched here. @@ -136,7 +137,25 @@ router.beforeEach(async (to) => { if (to.name === 'login' && session.isLoggedIn) { return { name: 'home' }; } + // D3 — legacy told the guest why they were bounced + // (henza_pwa/src/router/index.ts:253-261: `showNotify('ابتدا وارد شوید')`), + // and the target redirected in silence, so tapping «سفارش‌های من» landed on + // the login box with nothing explaining it. + // + // «عمداً تکرار نشد — D3/الف»: legacy then sent the guest to `home` and lost + // where they were going. The owner chose to keep the target's `redirect`, so + // signing in returns them to the page they asked for. The message is the + // parity row; the destination is deliberately better. + // + // Legacy listed five route names; the target gates on `meta.requiresAuth`, + // which also covers the two sales-expert pages — the same rule, applied + // wherever it belongs instead of a hand-kept list. + // + // Only reached when the IAM is off (the default) or unreachable: with silent + // SSO enabled the block above has already left the page, and a toast on a + // document that is being replaced would never be read. Not a gap. if (to.meta.requiresAuth && !session.isLoggedIn) { + toast.info('ابتدا وارد شوید'); return { name: 'login', query: { redirect: to.fullPath } }; } diff --git a/apps/user-app/src/stores/configStore.ts b/apps/user-app/src/stores/configStore.ts index dff51db..dc7af90 100644 --- a/apps/user-app/src/stores/configStore.ts +++ b/apps/user-app/src/stores/configStore.ts @@ -17,6 +17,14 @@ // HomeSlider's popup/link/discount click handling works unchanged. // // Not ported (no backend): promotionList, ignoreList, priceList, isLoaded. +// +// `isLoaded` — «عمداً تکرار نشد — D3/ب» (owner, 2026-10-12). Legacy used it as a +// router guard: a signed-in user whose one big bootstrap request had not landed +// was sent to `home` from any route lacking `meta.skipLoadedCheck` +// (henza_pwa/src/router/index.ts:263-266). The target has no such bootstrap — +// every page fetches its own data — so the flag would guard nothing and only +// add a bounce: opening a shared product link would land on home instead of the +// product. Replicating it would port the breakage, not the behaviour (D4). import { defineStore } from 'pinia' import { useCatalogStore } from './catalogStore' import shippingArt from '../assets/img/shipping-and-delivery.png' diff --git a/docs/PHASE2-D3-PLAN.md b/docs/PHASE2-D3-PLAN.md new file mode 100644 index 0000000..4f5a6fd --- /dev/null +++ b/docs/PHASE2-D3-PLAN.md @@ -0,0 +1,157 @@ +# طرح فاز ۲ — قلم ۲ (D3): سه رفتار جامانده در اپ‌ها + +تاریخ: ۱۴۰۵/۰۷/۲۰ (2026-10-12) + +## وضعیت — تصمیم مالک گرفته شد (۲۰۲۶/۱۰/۱۲) + +> «الف و ب مهم پس به ترتیب برو جلو — ج اصلا اولیوت نداره جزو اخرین چیز هایی هست +> که شاید درستش کنیم» + +| قلم | تصمیم | وضعیت | +|---|---|---| +| **الف** — پیام مهمان | انجام شود | ✅ **انجام شد** — گزینه ب (پیام + حفظ بازگشت به مقصد) در هر دو اپ | +| **ب** — گارد `isLoaded` | انجام شود، طبق پیشنهاد پورت **نشود** | ✅ **ثبت شد** به‌عنوان «عمداً تکرار نشد — D3/ب» در `configStore.ts` هر دو اپ | +| **ج** — آمار بازدید | **آخرین اولویت، شاید هرگز** | ⏸️ معوق — بخش «شکاف ۳» پایین به‌عنوان طرحِ آماده نگه داشته شد | + +--- + +--- + +## چه چیزی قبلاً انجام شده (بررسی شد، ادعا نیست) + +D3 ده کار بود. هفت‌تایش انجام شده است: + +| کار | وضعیت | مدرک | +|---|---|---| +| ۵ صفحه `/report`، `/report/detail`، `/ChildOrder`، `/customerChoice`، `/complete_profile` | ✅ در **هر دو** اپ | `apps/user-app/src/router/index.ts` و همان فایل در `user-app-v2` — هر پنج مسیر ثبت شده با `meta.layout='FullPage'` | +| گارد `sts_verify_step_two` روی تکمیل پروفایل | ✅ | `router/index.ts` گارد `requiresIncompleteProfile`؛ از سرور می‌پرسد نه از حافظه، پس پروفایلی که روی دستگاه دیگر کامل شده هم محترم است | +| سقف موجودی سبد v2 (`capQty`/`maxQty`) | ✅ یکسان در هر دو اپ | `stores/cartStore.ts:80,88` در هر دو | + +**سه کار باقی است** و هر سه را بررسی و در مرجع تأیید کردم. + +--- + +## شکاف ۱ — رفتار مهمان: الان به صفحه ورود پرتاب می‌شود + +**مرجع** (`henza_pwa/src/router/index.ts:253-261`): کاربر وارد‌نشده که یکی از پنج +صفحه شخصی را باز کند، یک پیام «ابتدا وارد شوید» می‌بیند و **به خانه** می‌رود. + +```ts +if (to.name === 'ordersList' || ... || to.name === 'completeProfile') { + showNotify('ابتدا وارد شوید'); + return next({name: 'home'}) +} else return next() +``` + +**تارگت** (`apps/user-app/src/router/index.ts:139-141`): به `/login` می‌رود با +`query.redirect`، و **هیچ پیامی نشان نمی‌دهد**. + +```ts +if (to.meta.requiresAuth && !session.isLoggedIn) { + return { name: 'login', query: { redirect: to.fullPath } }; +} +``` + +**چرا مهم است:** کاربر مهمان که روی «سفارش‌های من» می‌زند، بی‌هیچ توضیحی در صفحه +ورود می‌افتد و نمی‌داند چه شد. رفتار مرجع صریح‌تر است و D3 هم همان را خواسته. + +### ⚠️ یک تضاد که باید خودت تصمیم بگیری + +رفتار فعلی تارگت از یک جهت **بهتر** است: `query.redirect` دارد، یعنی بعد از +ورود، کاربر به همان صفحه‌ای که می‌خواست برمی‌گردد. رفتار مرجع این را ندارد — +کاربر به خانه می‌رود و باید دوباره دستی به آن صفحه برود. + +سه گزینه: + +| گزینه | نتیجه | +|---|---| +| **الف) عیناً مرجع** | toast + خانه. `query.redirect` از بین می‌رود. تطبیق کامل با D3. | +| **ب) بهترین هر دو** (پیشنهاد من) | toast «ابتدا وارد شوید» نشان داده شود، **ولی** مقصد همان صفحه ورود با `redirect` بماند. کاربر هم می‌فهمد چه شد، هم بعد از ورود سرِ جایش برمی‌گردد. در ماتریس به‌عنوان «عمداً تکرار نشد» ثبت می‌شود. | +| **ج) دست نزن** | شکاف باز می‌ماند و در ماتریس ثبت می‌شود. | + +هزینه هر سه یکی است (چند خط). تفاوت فقط در تجربه کاربر است. + +--- + +## شکاف ۲ — گارد `isLoaded` + +**مرجع** (`router/index.ts:263-266`): کاربر وارد‌شده‌ای که داده‌های اولیه‌اش +(`memory.state.isLoaded`) هنوز نیامده، به هر صفحه‌ای جز `home` برود به خانه +برگردانده می‌شود — مگر آن مسیر `meta.skipLoadedCheck` داشته باشد. + +**تارگت**: پورت نشده، و در کد **صریحاً نوشته شده چرا**: + +```ts +// apps/user-app/src/stores/configStore.ts:19 +// Not ported (no backend): promotionList, ignoreList, priceList, isLoaded. +``` + +**نظر من: این یکی را نباید پورت کنیم، و پیشنهادم «نه» است.** + +دلیل: `isLoaded` در مرجع پرچمِ «یک درخواست بزرگ اولیه نشست» بود که تارگت ندارد — +تارگت هر صفحه داده‌ی خودش را می‌گیرد. اگر این گارد را اضافه کنیم، چیزی به کاربر +اضافه نمی‌شود و فقط یک پرش ناخواسته به خانه ساخته می‌شود: مثلاً کاربری که لینک +محصول را از تلگرام باز می‌کند، به‌جای محصول در خانه می‌افتد. این دقیقاً مصداق +«باگ مرجع را پورت نکن» (D4) است. + +اگر موافقی، به‌عنوان **«عمداً تکرار نشد — D4»** ثبت می‌شود و D3 بسته می‌شود. +اگر مخالفی، پورت می‌کنم. + +--- + +## شکاف ۳ — آمار بازدید صفحه‌ها (`log/create` و `log/update`) + +**مرجع** دو مسیر دارد و زمان ماندن کاربر در هر صفحه را اندازه می‌گیرد: + +| | مرجع | +|---|---| +| ورود به صفحه | `POST /log/create/{title}` → یک ردیف با `start_at`، `ip`، `title`، `customer_id` (اگر وارد شده باشد) می‌سازد و `id` را برمی‌گرداند | +| خروج از صفحه | `GET /log/update/{id}` → `end_at` و `time_present` را پر می‌کند | +| صفحه محصول | `data: {product_id}` هم می‌فرستد تا بازدید هر محصول جدا شمرده شود | +| مستثنا | `login` و `userPage::hasSubRoute` | + +مرجع: `henza_server/app/Http/Controllers/Api/LogController.php`، +`henza_pwa/src/router/customAnalytics.ts`. + +**تارگت**: نه مسیر، نه جدول، نه فراخوانی. کامل غایب. + +این بزرگ‌ترین قلم از سه‌تاست — جدول، مدل Prisma، مهاجرت، دو مسیر API، یک +composable در هر اپ، و صفحه‌های گزارش ادمین (`queryTitle`، `queryAllCustomer`، +`queryCustomer`، `ago`) که آمار را نشان می‌دهند. + +### ⚠️ دو ایراد مرجع که عیناً پورت نمی‌کنم + +۱. **مسیر بی‌محافظ است.** در مرجع `log/create` احراز هویت ندارد و هر عنوانی را + می‌پذیرد، پس هر کسی از بیرون می‌تواند جدول را با ردیف‌های جعلی پر کند و آمار + را بی‌معنا یا دیسک را پر کند. پیشنهاد: عنوان فقط از یک فهرست بسته (نام + مسیرهای واقعی) پذیرفته شود و نرخ درخواست محدود شود. +۲. **`GET` که داده را عوض می‌کند.** `log/update` یک `GET` است که ردیف را + می‌نویسد. پیشنهاد: `POST` باشد. (اپ‌های تارگت تازه نوشته می‌شوند، پس سازگاری + عقب‌رو لازم نیست.) + +### ⚠️ و یک پرسش که فقط تو می‌توانی جواب بدهی + +این قابلیت **رفتار هر مشتری را با IP ثبت می‌کند** — کدام صفحه‌ها را دید و چند +ثانیه ماند. فنی‌اش ساده است، ولی این داده‌ی شخصی است و مرجع هیچ‌وقت آن را پاک +نمی‌کرد (جدول بی‌نهایت رشد می‌کند). + +سه پرسش: + +| پرسش | پیشنهاد من | +|---|---| +| این آمار را واقعاً لازم داری، یا در سیستم قدیم هم عملاً نگاهش نمی‌کردی؟ | اگر نگاهش نمی‌کردی، نسازیم. ساده‌ترین قابلیت، قابلیتی است که وجود ندارد. | +| اگر لازم است، چند وقت نگه داشته شود؟ | ۹۰ روز، و قدیمی‌ترها خودکار پاک شوند. | +| IP هم ذخیره شود؟ | نه. برای «کدام صفحه چقدر دیده شد» لازم نیست و حساس‌ترین بخش داده است. | + +--- + +## جمع‌بندی و پیشنهاد ترتیب کار + +| # | قلم | پیشنهاد من | هزینه | +|---|---|---|---| +| ۱ | toast مهمان | **گزینه ب** — هم پیام، هم بازگشت به صفحه مقصد | کم (نیم ساعت، هر دو اپ) | +| ۲ | گارد `isLoaded` | **پورت نشود** — «عمداً تکرار نشد — D4» | صفر | +| ۳ | آمار بازدید | **اول جواب بده لازمش داری یا نه.** اگر بله: با عنوان بسته، `POST`، بدون IP، نگه‌داری ۹۰ روزه | متوسط تا زیاد (جدول + مهاجرت + ۲ مسیر + ۲ اپ + صفحه گزارش ادمین) | + +اگر بگویی «قلم ۱ و ۲ را انجام بده، ۳ را فعلاً نه»، D3 با یک ردیف «عمداً تکرار +نشد» برای آمار بسته می‌شود و می‌رویم سراغ قلم بعدی فاز ۲. diff --git a/docs/parity/CHANGELOG.md b/docs/parity/CHANGELOG.md index 9f2a493..60d6949 100644 --- a/docs/parity/CHANGELOG.md +++ b/docs/parity/CHANGELOG.md @@ -876,3 +876,56 @@ earlier ruling this deviation stays, and it is re-recorded in `ISSUES.md` so it is not "fixed" later by someone restoring parity for its own sake. _No data was deleted in this entry._ + +--- + +## 2026-10-12 — Phase 2 — item #2: D3's three outstanding behaviours + +- **What:** (1) A guest bounced off a personal page now sees «ابتدا وارد شوید» + in both apps. (2) Legacy's `isLoaded` router guard is recorded as deliberately + not ported. (3) Route analytics parked as ANLY-01. +- **Why:** D3's remaining items. Seven of its ten were already built — all five + screens in **both** apps, the `sts_verify_step_two` guard and v2's cart stock + ceiling — confirmed by reading the code rather than trusting the checkmarks. +- **Reference:** `henza_pwa/src/router/index.ts:253-261` (the notify + home), + `:263-266` (the `isLoaded` guard), `router/customAnalytics.ts` + + `henza_server/app/Http/Controllers/Api/LogController.php` (analytics). +- **Target:** both apps' `src/router/index.ts` and `src/stores/configStore.ts`. +- **Matrix row:** APP-03, APP-04, ANLY-01 in `ISSUES.md` §"Phase 2 — item #2". +- **Commit:** see below. + +**The one judgement call.** Legacy showed the toast and then sent the guest to +`home`, discarding where they were going. The target redirected to `/login` with +`query.redirect`, so signing in returns the guest to the page they wanted — that +part is better than the reference, and the gap was only the missing message. +Rather than choose silently, the trade-off went to the owner, who kept the +target's destination. So the message is the parity row and the destination is +**«عمداً تکرار نشد — D3/الف»**. The guard also keys off `meta.requiresAuth` +instead of legacy's hand-kept list of five route names, which means the two +sales-expert pages are covered too. + +**`isLoaded` — argued against, and the owner agreed.** It guarded legacy's one +big bootstrap request, which the target does not have (each page fetches its own +data). Added, it would guard nothing and introduce a bounce: a shared product +link would open `home` instead of the product. That is porting the breakage, not +the behaviour (D4). The reasoning is written into both apps' `configStore.ts`, +beside the line that says the flag was not ported, so the next reader finds it +where they would actually look. + +**Analytics deferred by the owner** («ج اصلا اولیوت نداره»). The design is kept +in `docs/PHASE2-D3-PLAN.md` so it does not have to be re-derived, including the +two reference defects not to copy — `log/create` was unauthenticated and took any +title, so anyone could fill the table from outside, and `log/update` was a `GET` +that wrote — plus the two questions only the owner can answer: how long to keep +per-customer browsing history (legacy never deleted it) and whether to store the +IP at all. + +**Verification.** Neither app has a test runner, so the check is `vue-tsc -b` on +both (clean) plus reading the three things that make the toast actually appear +rather than merely compile: `toast.info` exists (`composables/useToast.ts:66`), +`` is mounted in each `App.vue:25`, and the call sits before the +guard's `return`. A compiling toast with no host would have been invisible. +Noted in the code: with the IAM enabled, silent SSO leaves the page before this +guard runs, so the toast is a no-op there by design, not a defect. + +_No data was deleted in this entry._ diff --git a/docs/parity/ISSUES.md b/docs/parity/ISSUES.md index 2ff9950..6e4fdb8 100644 --- a/docs/parity/ISSUES.md +++ b/docs/parity/ISSUES.md @@ -211,3 +211,15 @@ Severity: `Critical` (money loss / security / data loss) · `High` · `Medium` ruling the target's behaviour stays — identical whenever the client sends the true weight (which the reference app always does, `cart.vue:178`), different only for a forged request, which is the point. + +## Phase 2 — item #2, D3 close-out (2026-10-12) + +Seven of D3's ten items were already built — all five screens exist in **both** +apps, as do the `sts_verify_step_two` guard and v2's cart stock ceiling +(verified in the code, not taken from a checkmark). Three were outstanding: + +| ID | Severity | Where | Description | Evidence | Status | +|---|---|---|---|---|---| +| **APP-03** | Medium (UX) | REF `henza_pwa/src/router/index.ts:253-261` vs TGT `apps/user-app/src/router/index.ts`, `user-app-v2` same | A guest tapping a personal page (orders, transactions, profile, favourites, complete-profile) was redirected **in silence**, so they landed on the login box with nothing saying why. Legacy showed «ابتدا وارد شوید». | Read both guards; legacy calls `showNotify('ابتدا وارد شوید')` then `next({name:'home'})`. | ✅ **FIXED 2026-10-12** — `toast.info('ابتدا وارد شوید')` in both apps' `beforeEach`. **«عمداً تکرار نشد — D3/الف»** for the destination: legacy sent the guest to `home` and lost where they were going; the owner chose to keep the target's `query.redirect`, so signing in returns them to the page they asked for. Also applied via `meta.requiresAuth` rather than legacy's hand-kept list of five names, which additionally covers the two sales-expert pages. Only reached when the IAM is off (the default) or unreachable — silent SSO leaves the page before this runs, and a toast on a document being replaced could not be read. | +| **APP-04** | Low | REF `henza_pwa/src/router/index.ts:263-266` vs TGT both apps `stores/configStore.ts` | Legacy's `isLoaded` router guard sent a signed-in user to `home` from any route without `meta.skipLoadedCheck` until its one big bootstrap request had landed. | Read both; the target's store header already said `isLoaded` was not ported. | ⏹️ **«عمداً تکرار نشد — D3/ب»** (owner, 2026-10-12). The target has no bootstrap request — every page fetches its own data — so the flag would guard nothing and only add a bounce: opening a shared product link would land on home instead of the product. Porting it would port the breakage, not the behaviour (D4). Recorded in both apps' `configStore.ts` so a later reader finds the reasoning where they would look for the flag. | +| **ANLY-01** | Low | REF `henza_server/app/Http/Controllers/Api/LogController.php`, `henza_pwa/src/router/customAnalytics.ts`; routes `api.php:57-58` | Per-page dwell-time analytics: `POST /log/create/{title}` opened a row (`start_at`, `ip`, `title`, `customer_id`, optional `product_id`), `GET /log/update/{id}` closed it; four admin report endpoints aggregated it. Entirely absent from the target — no table, no routes, no calls. | Read the controller, the composable and the routes. | ⏸️ **DEFERRED — lowest priority, may never be built** (owner, 2026-10-12: «ج اصلا اولیوت نداره جزو اخرین چیز هایی هست که شاید درستش کنیم»). The full design is parked in `docs/PHASE2-D3-PLAN.md` §"شکاف ۳", including two reference defects **not** to copy if it is ever built: `log/create` required no auth and accepted any title (anyone could fill the table from outside), and `log/update` was a `GET` that wrote. Open owner questions there too: retention (suggested 90 days — legacy never deleted) and whether to store the IP at all (suggested no). | diff --git a/docs/parity/PROGRESS.md b/docs/parity/PROGRESS.md index 11a4024..8b3a80e 100644 --- a/docs/parity/PROGRESS.md +++ b/docs/parity/PROGRESS.md @@ -19,11 +19,11 @@ The "Current state" table below is the parity project's and was last updated 202 | Field | Value | |---|---| -| **Phase** | **Phase 2 — apps parity — item #1 done** (Phase 1 closed and committed, `83e5d28`) | +| **Phase** | **Phase 2 — apps parity — items #1 and #2 done** (Phase 1 closed, `83e5d28`) | | **Branch** | `main` (the parity branches were merged; see git log 2026-10-08..10-12) | -| **Status** | Phase 1: all in-scope sections closed (§2 §4 §5 §6 §7 §8 §9 ✅ · §3 ⏸️ by D16). Phase 2: **item #1 MON-08 ✅**. **286/286** jest tests, typecheck clean. | -| **Next step** | Phase 2 item #2 — **the 5 screens missing from both apps (D3)**. `02-APPS-MATRIX.md` still to be created. | -| **Blocked on** | nothing. Two additive items await an owner ruling when convenient: **PERM-02** and the open rows below. | +| **Status** | Phase 1: all in-scope sections closed (§2 §4 §5 §6 §7 §8 §9 ✅ · §3 ⏸️ by D16). Phase 2: **#1 MON-08 ✅**, **#2 D3 ✅**. **286/286** jest tests; both apps `vue-tsc` clean. | +| **Next step** | Build **`02-APPS-MATRIX.md`** — the field-by-field apps matrix (D12), which Phase 2 has been running without. Then the remaining app rows it turns up. | +| **Blocked on** | nothing. Awaiting an owner ruling when convenient: **PERM-02**. Deferred by the owner: **ANLY-01** (route analytics, lowest priority). | | **Last updated** | 2026-10-12 | ### Phase 1 close-out (2026-10-12) @@ -207,6 +207,24 @@ actually had: the suite only covered a tampered request, not an honest one. An operator ordering from the panel still keeps the numbers they typed — only an app order is re-priced by the server. +## Phase 2 — item #2 (D3) — ✅ DONE 2026-10-12 + +D3 was ten items, not one. **Seven were already built** — all five screens +(`/report`, `/report/detail`, `/ChildOrder`, `/customerChoice`, +`/complete_profile`) exist in **both** apps, as do the `sts_verify_step_two` +guard and v2's cart stock ceiling. Checked in the code, not read off a +checkmark. + +| Outstanding item | Result | +|---|---| +| guest toast «ابتدا وارد شوید» | ✅ both apps (**APP-03**). The destination stays the target's `/login?redirect=…`, better than legacy's `home` — owner's call, logged as «عمداً تکرار نشد — D3/الف». | +| `isLoaded` router guard | ⏹️ **not ported** (**APP-04**) — guards a bootstrap request the target does not have; would only bounce a shared product link to `home` (D4). Owner agreed. | +| route analytics | ⏸️ **deferred, lowest priority** (**ANLY-01**) — design parked in `docs/PHASE2-D3-PLAN.md`, with the two reference defects not to copy and the two retention/privacy questions still open. | + +⚠️ **Phase 2 is running without its matrix.** `02-APPS-MATRIX.md` still does not +exist, so "what else do the apps miss" rests on the earlier survey rather than a +field-by-field comparison (D12). That is the next item. + The original requirement, for the record: Before anything else in Phase 2: the server must derive `wagePercent`,