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) {