diff --git a/AGENTS.md b/AGENTS.md index b67403c..e32fa9d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,12 +35,12 @@ day one. `recovery.ts` (12-word BIP39 secret + salted verifier), `transfer.ts`/`lan.ts` (QR/LAN), content loaders (`quran.ts`, `tafsir.ts`, `audio.ts`, `hadith-full.ts`), `privacy.ts` (sensitivity + network manifest), `strings.ts` (AR/EN - dictionary, ~1295 keys), `i18n.ts`. + dictionary, ~1307 keys), `i18n.ts`. - `app/components/` — React UI. `app/components/views/` route views, `app/components/library/` Quran/hadith, `transfer.tsx` (export infra), `profile-scope.tsx`, `login-gate.tsx`. - `app/` — routes: `/`(today), `/account`, `/calendar`, `/insights`, - `/review`, `/deen`, `/library`, `/recovery`. + `/deen`, `/library`, `/recovery`. - `e2e/` — Playwright (serial; `npm run test:e2e` pins `--workers=1`). - `public/data/` — generated bundles (only touched via pipeline). - `docs/`, `DOCUMENTATION.md`, `SECURITY.md`, `GOVERNANCE.md` — ground diff --git a/CHANGELOG.md b/CHANGELOG.md index 20d2f3d..a71e998 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ or architecture-level changes are listed. ## Unreleased +- **الحصاد folded into /deen**: the previous `/review` night-checklist page + now lives as a third "المراجعة / Review" tab inside the Deen journey + (`/deen`, tabs: Today · Library · Review); `/review` permanently + redirects (`308`→`/deen`) so old bookmarks, shared links, and PWA + notification deep-links keep working; nav, sitemap, SW `CORE` (bumped + to `wird-v7`), manifest shortcuts, notification defaults, metadata, + SEO/docs/audit counts, and all static + e2e suites updated. - Branch security: GitHub-server protection ENABLED on `main` + `develop` (`gh` API 2026-09-23): require PR + `CI / verify` status check (strict), linear history, no force pushes, no deletions, no admins bypass — diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 33c3810..b58aab1 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -100,8 +100,7 @@ husky pre-commit (lint-staged) + commit-msg hooks. | `/` | `app/page.tsx` | Today: hero, habits, rescue plan, goals, night review | | `/calendar` | `app/calendar/` | 30-day month grid, Friday plan | | `/insights` | `app/insights/` | Real stats, balance radar, coach brief, Hijri year | -| `/review` | `app/review/` | End-of-day checklist (tri-state) + score + mood | -| `/deen` | `app/deen/` | Deen journey: levels, quests, deeds, reflections, streaks (see §3) | +| `/deen` | `app/deen/` | Deen journey — tabs: Today · Library · Review (see §3) | | `/library` | `app/library/` | Tabs: Adhkar · Quran · Hadith · Paths · Dreams | | `/account` | `app/account/` | Profile, backup, transfer, reminders, times, theme | | `/recovery` | `app/recovery/` | Last-resort recovery: inspect, emergency export, restore, surgical reset | @@ -114,6 +113,10 @@ Shared chrome (sidebar, bottom nav, header, zikr dock, login gate) lives in (`WirdProvider` + `useWird()`). Views that need props live in `app/components/views/`. +> `/review` was folded into `/deen` (Review tab) and now permanently +> redirects — old bookmarks, shared links, and PWA notification deep-links +> keep working. + ## 3. Features by route **Today (`/`)** — hero ring (live %), day-mode pills, focus card, quick tools, @@ -126,10 +129,11 @@ reflection (persisted textarea), tiered return screen after ≥3 absent days adaptive companion card (one ranked guidance + verified verse/hadith, see §14), kids quests, qada / fasting / breaker cards, Ramadan banner in Ramadan. -**Review (`/review`)** — auto-built checklist from today's real data plus six -heart-check items; tri-state (done/partial/missed); mood + gratitude; weighted -score saved immutably per day (`wird-reviews-v1`, merged into history); -one-line night context for strong/low/return days (see §14). +**Review (Deen journey, `/deen` → Review tab)** — auto-built checklist from +today's real data plus six heart-check items; tri-state (done/partial/missed); +mood + gratitude; weighted score saved immutably per day (`wird-reviews-v1`, +merged into history); one-line night context for strong/low/return days +(see §14). Retains tab state in memory only, like the other deen tabs. **Insights (`/insights`)** — period pills (1–365d buckets), weekly bars, 4 metrics, coach brief (at-risk → neglect → pace → lift + praise), 6-axis diff --git a/MASTER_AUDIT.md b/MASTER_AUDIT.md index a60890d..6de3c29 100644 --- a/MASTER_AUDIT.md +++ b/MASTER_AUDIT.md @@ -30,8 +30,8 @@ the JSONs). This run's numbers: `AUDIT_REPORT.md`. Frozen snapshot: ## 5–7. Routes, journeys, navigation — PASS (+1 FIXED) -- Inventory: 10 routes (`/`, account, calendar, insights, library, - recovery, review, private-plans, terms, privacy); static check +- Inventory: 10 routes (`/`, account, calendar, insights, deen, library, + recovery, private-plans, terms, privacy); static check `app/lib/__tests__/audit.test.ts` asserts every `href="/…"` and `router.push("/…")` resolves, sitemap lists the public routes, and the SW `CORE` shell matches existing routes (a stale shell entry would diff --git a/README.md b/README.md index 472b556..25b56d0 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Node 22 (`.nvmrc`). CI (`.github/workflows/ci.yml`) runs typecheck → lint → - **اليوم**: صلوات وأوراد، خطة إنقاذ، جلسة قرآن بمؤقت، بطاقات الجمعة/الوتر، عادات مخصصة، تحديات، عهود، ركن الصغار، قضاء الفوائت، الصيام، كسر العادات، وضع رمضان التلقائي. - **المرشد التكيفي**: بطاقة واحدة ذكية (26 حالة: عودة، تعثر، زخم، إرهاق…) مع آية/حديث موثّقين من مجموعات محلية، ومسار توبة اختياري بعد الغياب الطويل — دون أحكام ودون ادعاء معرفة القلوب. -- **الحصاد** (`/review`): مراجعة ليلية tri-state مع درجة ومزاج وامتنان — تُحفظ كسجل يومي، مع سطر سياق ليلي. +- **الدين** (`/deen`): رحلة اليوم (أذكار ومهام وأعمال صالحة) + مكتبة وتعلم + مراجعة ليلية tri-state مع درجة ومزاج وامتنان — تُحفظ كسجل يومي، مع سطر سياق ليلي. - **التقدّم** (`/insights`): طبقات تحليلية محلية قابلة للتفسير (اتجاهات، عبادة-عبادة، قرآن وحفظ، أذكار، أهداف وتحديات وعهود، عودات واستمرارية، خريطة نشاط، مراجعة شهرية/سنوية، إنجازات) + مدى مخصص للمقارنة — لا بيانات مفتعلة أبدًا. - **المكتبة** (`/library`): قارئ قرآن كامل دون إنترنت (بحث، علامات، حفظ بتاريخ مراجعة)، مختارات الكتب التسعة + الأربعون النووية، مسارات علمية ٤ مستويات × ٨ علوم، لوحة الأحلام. - **العودة**: شاشة رجوع متدرجة حسب عمق الغياب (3/7/14/30/90 يومًا) + سلّم تنبيهات محلية + مواقيت يدوية بعدّادات حية + وضع المسجد. diff --git a/app/components/views/deen-page.tsx b/app/components/views/deen-page.tsx index 708d276..e7ce734 100644 --- a/app/components/views/deen-page.tsx +++ b/app/components/views/deen-page.tsx @@ -1,16 +1,18 @@ -// Deen page shell (STEP 10): Today/Library tabs over the two deen -// views. Tab selection is in-memory only (never in the URL), so no -// tracking state can leak into history, titles, or shared links. +// Deen page shell (STEP 10): Today/Library/Review tabs over the three deen +// views. Tab selection is in-memory only (never in the URL), so no tracking +// state can leak into history, titles, or shared links. Review is the folded +// /review (الحصاد) checklist, reachable now only through /deen. "use client"; import { useState } from "react"; import { useT } from "../../lib/i18n"; import { DeenTodayView } from "./deen-today"; import { DeenLibraryView } from "./deen-library"; +import { ReviewTab } from "./review-tab"; export function DeenPage() { const t = useT(); - const [tab, setTab] = useState<"today" | "library">("today"); + const [tab, setTab] = useState<"today" | "library" | "review">("today"); return ( <>
@@ -32,8 +34,23 @@ export function DeenPage() { > {t("dn.library")} +
- {tab === "today" ? : } + {tab === "today" ? ( + + ) : tab === "library" ? ( + + ) : ( + + )} ); } diff --git a/app/review/page.tsx b/app/components/views/review-tab.tsx similarity index 92% rename from app/review/page.tsx rename to app/components/views/review-tab.tsx index a837e73..f20b3fe 100644 --- a/app/review/page.tsx +++ b/app/components/views/review-tab.tsx @@ -1,14 +1,20 @@ -// Review page (/review): tri-state nightly checklist + score/mood/gratitude. -// Submits write BOTH the per-day review record (enveloped) and the history -// merge. Includes one honest night-context line for strong/low/return days. - +// Review tab (Deen journey, /deen): tri-state nightly checklist + score/mood/ +// gratitude. Submits write BOTH the per-day review record (enveloped) and the +// history merge. Includes one honest night-context line for strong/low/return +// days. Moved here from /review (now a permanent redirect to /deen). "use client"; import { useEffect, useMemo, useState } from "react"; -import { dayId, hijriParts, loadFromStorage, QURAN_GOAL_PAGES, saveToStorage } from "../lib/wird"; -import { assessUser, selectGuidance } from "../lib/companion"; -import { useT } from "../lib/i18n"; -import { useWird } from "../components/wird-store"; +import { + dayId, + hijriParts, + loadFromStorage, + QURAN_GOAL_PAGES, + saveToStorage, +} from "../../lib/wird"; +import { assessUser, selectGuidance } from "../../lib/companion"; +import { useT } from "../../lib/i18n"; +import { useWird } from "../wird-store"; type Status = "done" | "partial" | "missed"; type Mood = "good" | "ok" | "low" | null; @@ -47,7 +53,7 @@ function loadReviews(): Record { return loadFromStorage>("wird-reviews-v1", {}); } -export default function ReviewPage() { +export function ReviewTab() { const t = useT(); const { done, @@ -118,8 +124,8 @@ export default function ReviewPage() { const gradeTitle = loadedScore >= 85 ? t("rv.g85") : loadedScore >= 60 ? t("rv.g60") : t("rv.gLow"); - // Night context (2.39): one honest line for the states that matter at - // night — no card, no logging (Today owns the guidance log). + // Night context: one honest line for the states that matter at night — no + // card, no logging (Today owns the guidance log). const nightLine = useMemo(() => { try { const todayId = dayId(); diff --git a/app/deen/page.tsx b/app/deen/page.tsx index 51ccfcd..0cfa0b9 100644 --- a/app/deen/page.tsx +++ b/app/deen/page.tsx @@ -1,7 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 // Deen route (/deen): the STEP 10 journey space — today dashboard plus -// library/learning tabs (in-memory tab state, no URL params, static -// generic title). Server component; views own all client state. +// library/learning and nightly review tabs (in-memory tab state, no URL +// params, static generic title). Server component; views own all client +// state. Review was folded here from /review (permanent redirect). import type { Metadata } from "next"; import { DeenPage } from "../components/views/deen-page"; @@ -9,11 +10,11 @@ export const metadata: Metadata = { // Discreet by design: short generic title, one plain description of // the public feature — never user data, never scores or ranks. title: "دين", - description: "رحلة الدين: مستويات تتبع ومهام وأعمال صالحة وتأمل خاص.", + description: "رحلة الدين: متابعة اليوم، أذكار وتعلم، ومراجعة ليلية خاصة.", alternates: { canonical: "/deen" }, openGraph: { title: "دين", - description: "رحلة الدين: مستويات تتبع ومهام وأعمال صالحة وتأمل خاص.", + description: "رحلة الدين: متابعة اليوم، أذكار وتعلم، ومراجعة ليلية خاصة.", url: "/deen", images: [{ url: "/opengraph-image", width: 1200, height: 630, alt: "Wird — Daily Companion" }], }, diff --git a/app/lib/__tests__/audit.test.ts b/app/lib/__tests__/audit.test.ts index 128c7bf..4c81ab5 100644 --- a/app/lib/__tests__/audit.test.ts +++ b/app/lib/__tests__/audit.test.ts @@ -40,7 +40,6 @@ describe("route inventory (audit 4-5)", () => { "/insights", "/library", "/recovery", - "/review", "/private-plans", "/terms", "/privacy", @@ -66,7 +65,6 @@ describe("route inventory (audit 4-5)", () => { for (const r of [ "/calendar", "/insights", - "/review", "/deen", "/library", "/account", @@ -80,7 +78,6 @@ describe("route inventory (audit 4-5)", () => { "/", "/calendar", "/insights", - "/review", "/deen", "/library", "/account", diff --git a/app/lib/__tests__/seo.test.ts b/app/lib/__tests__/seo.test.ts index f1f19dc..94e53cd 100644 --- a/app/lib/__tests__/seo.test.ts +++ b/app/lib/__tests__/seo.test.ts @@ -40,16 +40,7 @@ describe("titles, descriptions, canonicals", () => { expect(seen.has(t), `duplicate title "${t}" in ${rel} (first: ${seen.get(t)})`).toBe(false); seen.set(t, rel); } - for (const r of [ - "calendar", - "insights", - "review", - "library", - "account", - "terms", - "privacy", - "deen", - ]) { + for (const r of ["calendar", "insights", "library", "account", "terms", "privacy", "deen"]) { expect( [...seen.values()].some((v) => v.startsWith(`${r}/`)), `route ${r} titled`, @@ -64,16 +55,7 @@ describe("titles, descriptions, canonicals", () => { return read(`app/${r}/page.tsx`); } }; - for (const r of [ - "calendar", - "insights", - "review", - "library", - "account", - "terms", - "privacy", - "deen", - ]) { + for (const r of ["calendar", "insights", "library", "account", "terms", "privacy", "deen"]) { expect(srcOf(r), `${r} description`).toMatch(/description:\s*"/); } // Recovery is fully discreet: no metadata export at all, so the @@ -119,7 +101,7 @@ describe("long-tail content, internal links, duplicate content", () => { }); it("home links descriptively to every core route", () => { const src = read("app/page.tsx"); - for (const route of ["/library", "/deen", "/review", "/insights"]) { + for (const route of ["/library", "/deen", "/calendar", "/insights"]) { expect(src, `internal link ${route}`).toContain(`Link href="${route}"`); } expect(src, "descriptive anchor text (nav.* labels)").toContain('t("nav.library")'); @@ -134,16 +116,7 @@ describe("long-tail content, internal links, duplicate content", () => { } }; const seen = new Map(); - for (const r of [ - "calendar", - "insights", - "review", - "library", - "account", - "terms", - "privacy", - "deen", - ]) { + for (const r of ["calendar", "insights", "library", "account", "terms", "privacy", "deen"]) { const d = srcOf(r).match(/description:\s*"([^"]+)"/); if (!d) continue; expect( diff --git a/app/lib/notify.ts b/app/lib/notify.ts index ffe15d4..282f69c 100644 --- a/app/lib/notify.ts +++ b/app/lib/notify.ts @@ -41,7 +41,7 @@ export async function ensurePermission(): Promise { } } -export function fireNotification(title: string, body: string, url = "/review"): void { +export function fireNotification(title: string, body: string, url = "/deen"): void { try { if (!canNotify() || Notification.permission !== "granted") return; if ("serviceWorker" in navigator && navigator.serviceWorker.controller) { diff --git a/app/lib/strings.ts b/app/lib/strings.ts index e72987b..9a6533f 100644 --- a/app/lib/strings.ts +++ b/app/lib/strings.ts @@ -8,7 +8,6 @@ const AR: Record = { "nav.today": "اليوم", "nav.calendar": "التقويم", "nav.insights": "التقدّم", - "nav.review": "الحصاد", "nav.library": "الأذكار", "nav.account": "حسابي", "nav.skip": "تخطَّ إلى المحتوى", @@ -1141,6 +1140,7 @@ const AR: Record = { "dn.ach.disciplined": "المنضبط", "dn.ach.disciplinedD": "حافظت على قاعدة شخصية 7 أيام.", "dn.library": "المكتبة والتعلم", + "dn.review": "المراجعة", "dn.mustahabb": "النمو بالمستحبات", "dn.mustahabbSub": "ممارسات مستحبة مجمعة للنمو. كل عنصر يوضح إن كان موثقا أو هدفا شخصيا — والأحكام تختلف بين المذاهب.", @@ -1340,7 +1340,6 @@ const EN: Record = { "nav.today": "Today", "nav.calendar": "Calendar", "nav.insights": "Progress", - "nav.review": "Review", "nav.library": "Adhkar", "nav.account": "Account", "nav.skip": "Skip to content", @@ -2496,6 +2495,7 @@ const EN: Record = { "dn.ach.disciplined": "Disciplined", "dn.ach.disciplinedD": "Kept a personal discipline rule for 7 days.", "dn.library": "Library and learning", + "dn.review": "Review", "dn.mustahabb": "Mustahabb growth", "dn.mustahabbSub": "Recommended practices grouped for growth. Each item shows whether it is sourced or a personal goal - rulings vary by school.", diff --git a/app/lib/wird.ts b/app/lib/wird.ts index 316e755..218b57c 100644 --- a/app/lib/wird.ts +++ b/app/lib/wird.ts @@ -213,7 +213,6 @@ export const NAV_ITEMS: [string, string, string][] = [ ["today", "⌂", "اليوم"], ["calendar", "▦", "التقويم"], ["insights", "↗", "التقدّم"], - ["review", "☾", "الحصاد"], ["deen", "✦", "الدين"], ["library", "◈", "الأذكار"], ["account", "◌", "حسابي"], @@ -223,7 +222,6 @@ export const NAV_HREFS: Record = { today: "/", calendar: "/calendar", insights: "/insights", - review: "/review", deen: "/deen", library: "/library", account: "/account", diff --git a/app/page.tsx b/app/page.tsx index 2ed4f94..9e4c9b3 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -430,7 +430,7 @@ export default function TodayPage() { const handleGuideAction = (kind: string) => { try { if (kind === "rescue") scrollToId("rescue-plan"); - else if (kind === "review") router.push("/review"); + else if (kind === "review") router.push("/deen"); else if (kind === "core") { setMinimumPlan(true); scrollToId("rescue-plan"); @@ -1821,7 +1821,7 @@ export default function TodayPage() { {t("nav.deen")}
  • - {t("nav.review")} + {t("nav.calendar")}
  • {t("nav.insights")} diff --git a/app/review/layout.tsx b/app/review/layout.tsx deleted file mode 100644 index b28b719..0000000 --- a/app/review/layout.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Review route metadata (STEP 11): server layout because the page is -// a client component. Describes the actual checklist flow, nothing more. -import type { Metadata } from "next"; - -export const metadata: Metadata = { - title: "الحصاد", - description: "مراجعة المساء من بياناتك الحقيقية: قائمة ومزاج وامتنان ودرجة.", - alternates: { canonical: "/review" }, - openGraph: { - title: "الحصاد", - description: "مراجعة المساء من بياناتك الحقيقية: قائمة ومزاج وامتنان ودرجة.", - url: "/review", - images: [{ url: "/opengraph-image", width: 1200, height: 630, alt: "Wird — Daily Companion" }], - }, -}; - -export default function ReviewLayout({ children }: Readonly<{ children: React.ReactNode }>) { - return <>{children}; -} diff --git a/app/sitemap.ts b/app/sitemap.ts index 9aea4be..8ff243e 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -6,7 +6,6 @@ const routes = [ "", "/calendar", "/insights", - "/review", "/deen", "/library", "/account", diff --git a/audit/audit-manifest.json b/audit/audit-manifest.json index 57a180f..e0bbe1c 100644 --- a/audit/audit-manifest.json +++ b/audit/audit-manifest.json @@ -1,13 +1,13 @@ { - "generated": "2026-09-15T02:19:15.435Z", - "commit": "d824f0f", + "generated": "2026-09-23T01:21:48.535Z", + "commit": "97ec9db", "version": "0.1.0", "node": "22", "counts": { - "routes": 11, + "routes": 10, "featureDocs": 15, "storageKeys": 59, - "unitTests": 282, + "unitTests": 287, "e2eTests": 54, "integrations": 3 } diff --git a/audit/features.json b/audit/features.json index e6284ef..db3dfc4 100644 --- a/audit/features.json +++ b/audit/features.json @@ -41,7 +41,7 @@ { "doc": "docs/features/goals-habits-review.md", "title": "Goals, habits, and review", - "modules": ["app/lib/wird.ts", "app/lib/history.ts", "app/components/views/review.tsx"] + "modules": ["app/lib/wird.ts", "app/lib/history.ts", "app/components/views/review-tab.tsx"] }, { "doc": "docs/features/hadith.md", diff --git a/audit/routes.json b/audit/routes.json index 2741175..5dfd515 100644 --- a/audit/routes.json +++ b/audit/routes.json @@ -35,10 +35,6 @@ "route": "/recovery", "file": "app/recovery/page.tsx" }, - { - "route": "/review", - "file": "app/review/page.tsx" - }, { "route": "/terms", "file": "app/terms/page.tsx" diff --git a/audit/test-matrix.json b/audit/test-matrix.json index 94664a0..5b2acc0 100644 --- a/audit/test-matrix.json +++ b/audit/test-matrix.json @@ -5,6 +5,7 @@ "app/lib/__tests__/analytics-perf.test.ts", "app/lib/__tests__/analytics.test.ts", "app/lib/__tests__/audit.test.ts", + "app/lib/__tests__/branch-guard.test.ts", "app/lib/__tests__/catalog.test.ts", "app/lib/__tests__/coach.test.ts", "app/lib/__tests__/companion.test.ts", @@ -36,7 +37,7 @@ "app/lib/__tests__/transfer.test.ts", "app/lib/__tests__/vault.test.ts" ], - "tests": 282 + "tests": 287 }, "e2e": { "files": [ diff --git a/docs/INDEXING.md b/docs/INDEXING.md index b3b78ac..137ddd9 100644 --- a/docs/INDEXING.md +++ b/docs/INDEXING.md @@ -67,7 +67,7 @@ data can decide. owner-side once GSC shows impressions-vs-clicks: rewrite the meta description, then `npm run build` auto-deploys. - **Internal links**: homepage footer links to `/library`, `/deen`, - `/review`, `/insights` with descriptive anchors (route labels), not + `/calendar`, `/insights` with descriptive anchors (route labels), not "click here" — enforced by a unit test. - **Duplicate content**: `seo.test.ts` now fails if any two listed routes share a title or description, and canonicals are same-route diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md index 4bb3efb..155a3ea 100644 --- a/docs/PERFORMANCE.md +++ b/docs/PERFORMANCE.md @@ -58,7 +58,7 @@ Data that is large and not immediately needed is code-split and lazy: - `lib/hadith-full.ts` (CDN hadith) loads on demand, not on app mount. - `lib/quran.ts` corpus loads on mount but behind `Suspense`; pages - `/library`, `/review`, `/insights` do not block on Quran data. + `/library`, `/deen`, `/insights` do not block on Quran data. - Tafsir CDN calls are per-ayah, never bulk-fetched. - Mirror books (Ahmed/Darimi) download + parse once per session: the Arabic book and the EN map derive from one shared payload diff --git a/docs/SEO.md b/docs/SEO.md index 312be78..1171cf8 100644 --- a/docs/SEO.md +++ b/docs/SEO.md @@ -18,8 +18,9 @@ except titles/descriptions/canonicals where they were missing. ## Title strategy -`%s | ورد` template (root `lang="ar"`): التقويم، التقدم، الحصاد، -المكتبة، حسابي، دين (discreet single word), Terms of Use, Privacy +`%s | ورد` template (root `lang="ar"`): التقويم، التقدم، المكتبة، +حسابي، دين (discreet single word — Today is root; Review lives inside +/deen as a tab, no separate title), Terms of Use, Privacy Policy (EN pages keep EN titles). One title per route, enforced unique by test. diff --git a/docs/adr/ADR-003.md b/docs/adr/ADR-003.md index 068ac06..56dbdc3 100644 --- a/docs/adr/ADR-003.md +++ b/docs/adr/ADR-003.md @@ -13,8 +13,8 @@ computation on multi-year histories. Analytics is a **pure, deterministic layer**: `app/lib/history.ts` (day records + sliding windows) feeds `app/lib/analytics.ts` (metrics, trends, insights) which feeds `app/lib/coach.ts` (a short human brief). All -functions are pure; the UI (`/insights`, `/review`, camera-roll views) -renders their output. Nothing is computed server-side. +functions are pure; the UI (`/insights`, `/deen` review tab, camera-roll +views) renders their output. Nothing is computed server-side. ## Alternatives diff --git a/docs/features/README.md b/docs/features/README.md index d3bfd39..da12fc4 100644 --- a/docs/features/README.md +++ b/docs/features/README.md @@ -13,7 +13,7 @@ Use this index when you need to understand a feature before modifying it. | **Quran reader** | [quran.md](quran.md) | `app/lib/quran.ts`, `app/lib/tafsir.ts`, `app/components/library/quran-reader.tsx` | | **Hadith library** | [hadith.md](hadith.md) | `app/lib/hadith-full.ts`, `app/components/library/hadith-library.tsx` | | **Adhkar** | [adhkar.md](adhkar.md) | `app/components/views/adhkar.tsx`, `app/lib/wird.ts` | -| **Goals / Habits / Review** | [goals-habits-review.md](goals-habits-review.md) | `app/lib/wird.ts`, `app/lib/history.ts`, `app/review/page.tsx` | +| **Goals / Habits / Review** | [goals-habits-review.md](goals-habits-review.md) | `app/lib/wird.ts`, `app/lib/history.ts`, `app/components/views/review-tab.tsx` | | **Private plans (self-management)** | [private-recovery.md](private-recovery.md) | `app/lib/private-plans.ts`, `app/components/views/private-plans.tsx` | | **Deen journey (levels/gamification)** | [deen.md](deen.md) | `app/lib/deen.ts`, `app/lib/deen-catalog.ts`, `app/components/views/deen-today.tsx` | | **Storage / Schema** | [storage.md](storage.md) · [VERSIONING.md](../VERSIONING.md) | `app/lib/schema.ts`, `app/lib/crypto.ts` | diff --git a/docs/features/deen.md b/docs/features/deen.md index f968491..dd287bb 100644 --- a/docs/features/deen.md +++ b/docs/features/deen.md @@ -5,8 +5,10 @@ voluntary tracking levels, quests, a good-deeds library, awareness reflections, streaks, and achievements. The app **never ranks faith, calculates reward, counts sins, or leads by shame** (ADR-007). -- Route: `/deen` (nav-listed, sitemap-listed, SW-precached; Today and - Library tabs, tab state in-memory only — never in URLs). +- Route: `/deen` (nav-listed, sitemap-listed, SW-precached; Today, + Library, and Review tabs, tab state in-memory only — never in URLs). + The former `/review` (الحصاد) checklist lives here as the Review tab; + the old URL permanently redirects to `/deen`. - Content: `app/lib/deen-catalog.ts` (catalogs only — no user data; `verified` vs `personal-goal`/`habit`/`advice` kinds, sources on every verified row, shared school-variance notes). @@ -14,9 +16,10 @@ calculates reward, counts sins, or leads by shame** (ADR-007). the existing `loadFromStorage`/`saveToStorage` seam — no database, no new storage architecture). - Views: `app/components/views/deen-page.tsx` (tabs), - `deen-today.tsx` (dashboard), `deen-library.tsx` (library/learn). + `deen-today.tsx` (dashboard), `deen-library.tsx` (library/learn), + `review-tab.tsx` (nightly review checklist). - Copy: `dn.*` keys in `app/lib/strings.ts` (AR/EN parity + - `guidance-safety` scan cover all 316 pairs). + `guidance-safety` scan cover all 317 pairs). ## Contract (what the system promises) diff --git a/docs/features/goals-habits-review.md b/docs/features/goals-habits-review.md index ac772f2..5eb430b 100644 --- a/docs/features/goals-habits-review.md +++ b/docs/features/goals-habits-review.md @@ -7,7 +7,8 @@ Goals, recurring habits (catalog + customs), and the daily review flow. pledges, qada, kids datasets). - Day aggregation: `app/lib/history.ts` (DayRecord merges habit toggles monotonically). -- Review UI: `app/components/views/review.tsx`, routes `/review`, `/calendar`. +- Review UI: `app/components/views/review-tab.tsx` (Deen journey tab, /deen), + routes /deen, /calendar; /review redirects permanently to /deen. **Key behaviors:** diff --git a/docs/offline-architecture.md b/docs/offline-architecture.md index 2c0a971..a32b66f 100644 --- a/docs/offline-architecture.md +++ b/docs/offline-architecture.md @@ -134,10 +134,10 @@ an explicit opt-in product decision: ## Service worker contract (5.14) -`public/sw.js` (CACHE `wird-v6`, no build step): +`public/sw.js` (CACHE `wird-v7`, no build step): -- Precaches route shells (`/`, `/calendar`, `/insights`, `/review`, - `/deen`, `/library`, `/account`, `/recovery`, `/private-plans`), manifest, and +- Precaches route shells (`/`, `/calendar`, `/insights`, `/deen`, + `/library`, `/account`, `/recovery`, `/private-plans`), manifest, and the five `public/data` bundles at install (best-effort). - Navigations: network-first (fresh shell + instant updates when online), falling back to the precached route shell, else `/` — so an offline diff --git a/e2e/audit.spec.ts b/e2e/audit.spec.ts index 5c15b25..664eee1 100644 --- a/e2e/audit.spec.ts +++ b/e2e/audit.spec.ts @@ -29,7 +29,7 @@ test("core journey touches loopback hosts only", async ({ page }) => { } catch {} }); await ensureProfile(page); - for (const route of ["/calendar", "/insights", "/review", "/account", "/recovery"]) { + for (const route of ["/calendar", "/insights", "/deen", "/account", "/recovery"]) { await page.goto(route); await expect(page.locator("aside.sidebar")).toBeVisible({ timeout: 15000 }); } @@ -88,7 +88,7 @@ test("back, forward, refresh, duplicate tab stay valid", async ({ page, context await page.reload(); await expect(page.locator("aside.sidebar")).toBeVisible(); const tab2 = await context.newPage(); - await tab2.goto("/review"); + await tab2.goto("/deen"); await expect(tab2.locator("aside.sidebar")).toBeVisible(); await tab2.close(); expect(badLogs).toEqual([]); diff --git a/e2e/offline.spec.ts b/e2e/offline.spec.ts index 459f4e0..8582d07 100644 --- a/e2e/offline.spec.ts +++ b/e2e/offline.spec.ts @@ -58,7 +58,7 @@ test("core flows work with external network blocked", async ({ page }) => { "/", "/calendar", "/insights", - "/review", + "/deen", "/library", "/account", "/recovery", diff --git a/e2e/seo.spec.ts b/e2e/seo.spec.ts index 70a2540..fbc1727 100644 --- a/e2e/seo.spec.ts +++ b/e2e/seo.spec.ts @@ -18,7 +18,6 @@ test("head carries title, description, canonical, OG, JSON-LD", async ({ page }) for (const [route, titlePart] of [ ["/calendar", "التقويم"], ["/insights", "التقدم"], - ["/review", "الحصاد"], ["/library", "المكتبة"], ["/account", "حسابي"], ["/deen", "دين"], @@ -73,6 +72,12 @@ test("robots, sitemap, verification file serve correctly", async ({ request }) = expect((await v.text()).trim()).toBe("google-site-verification: google373507699530d312.html"); }); +test("former /review permanently redirects to /deen", async ({ request }) => { + const res = await request.get("/review", { maxRedirects: 0 }); + expect([307, 308].includes(res.status()), `redirect status ${res.status()}`).toBe(true); + expect(res.headers()["location"]).toContain("/deen"); +}); + test("unknown URLs return a real 404, trailing slash resolves cleanly", async ({ page, request, diff --git a/e2e/smoke.spec.ts b/e2e/smoke.spec.ts index a77f687..b67f650 100644 --- a/e2e/smoke.spec.ts +++ b/e2e/smoke.spec.ts @@ -47,11 +47,12 @@ test("today loads, toggles persist, no hydration errors", async ({ page }) => { expect(badLogs).toEqual([]); }); -test("routes render: calendar, review, insights, library", async ({ page }) => { +test("routes render: calendar, deen review, insights, library", async ({ page }) => { await ensureProfile(page); await page.goto("/calendar"); await expect(page.getByRole("heading", { name: /تقويم رحلتك/ })).toBeVisible(); - await page.goto("/review"); + await page.goto("/deen"); + await page.getByRole("tab", { name: /المراجعة|Review/ }).click(); await expect(page.getByRole("heading", { name: /ماذا فعلت اليوم/ })).toBeVisible(); await page.goto("/insights"); await expect(page.getByRole("heading", { name: /خطواتك الهادئة/ })).toBeVisible(); diff --git a/next.config.ts b/next.config.ts index ed5affa..5ab55aa 100644 --- a/next.config.ts +++ b/next.config.ts @@ -35,6 +35,19 @@ const nextConfig: NextConfig = { turbopack: { root: __dirname, }, + async redirects() { + // /review (الحصاد) folded into the /deen journey as a third tab — keep + // the old URL alive forever so bookmarks, shared links, and the PWA + // notification never break. No query or fragment to carry over (client + // state is in-memory by design, see DOCUMENTATION.md route table). + return [ + { + source: "/review", + destination: "/deen", + permanent: true, + }, + ]; + }, async headers() { const headers = [...securityHeaders]; if (process.env.NODE_ENV === "production") { diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest index 19492f4..210eb38 100644 --- a/public/manifest.webmanifest +++ b/public/manifest.webmanifest @@ -14,7 +14,7 @@ "categories": ["productivity", "lifestyle"], "shortcuts": [ { "name": "اليوم", "url": "/" }, - { "name": "حصاد اليوم", "url": "/review" }, + { "name": "الدين", "url": "/deen" }, { "name": "الأذكار", "url": "/library" } ], "icons": [ diff --git a/public/sw.js b/public/sw.js index 9c563cc..9db0209 100644 --- a/public/sw.js +++ b/public/sw.js @@ -4,12 +4,11 @@ * network-first (fresh shell when online) with cache fallback (usable * Today screen when offline). Bump CACHE on shell changes — activate * purges older versions and the in-app update banner applies them. */ -const CACHE = "wird-v6"; +const CACHE = "wird-v7"; const CORE = [ "/", "/calendar", "/insights", - "/review", "/deen", "/library", "/account", @@ -129,7 +128,7 @@ self.addEventListener("message", (event) => { self.addEventListener("notificationclick", (event) => { event.notification.close(); - const url = (event.notification.data && event.notification.data.url) || "/review"; + const url = (event.notification.data && event.notification.data.url) || "/deen"; event.waitUntil( self.clients.matchAll({ type: "window", includeUncontrolled: true }).then((wins) => { for (const w of wins) {