# نمایش حالت تعمیرات (Maintenance) در سایت عمومی ## پروژه `nobat724_front` **cross-repo** — پرامپت همتا (که باید **اول** اجرا شود): `clinicpro/.claude/prompt/maintenance-mode.md` ## زمینه در backend یک Maintenance Mode مرکزی پیاده می‌شود: وقتی ادمین آن را روشن کند، هر درخواست به `/api/v1/...` (به‌جز چند مسیر whitelist‌شده) با این پاسخ برمی‌گردد: ``` HTTP/1.1 503 Service Unavailable Retry-After: 600 Content-Type: application/json { "success": false, "data": null, "errors": [ { "code": "MAINTENANCE_MODE", "message": "<پیام قابل ویرایش از پنل ادمین>" } ] } ``` قرارداد تشخیص: **status = 503 و `errors[0].code === 'MAINTENANCE_MODE'`**. هر دو شرط باید چک شوند (503 خالی می‌تواند از nginx/load balancer هم بیاید). بدون تغییر در این ریپو، رفتار فعلی این می‌شود: - درخواست‌های client-side: interceptor در [services/api.js:94](services/api.js#L94) فقط یک `toast.error` قرمز نشان می‌دهد و صفحه خالی/شکسته می‌ماند - درخواست‌های server-side: `fetchReq` در [lib/req.js:16](lib/req.js#L16) روی هر خطا `null` برمی‌گرداند — صفحه بدون داده رندر می‌شود و **این حالت از یک صفحه واقعاً خالی قابل تفکیک نیست** هدف: به‌جای این‌ها یک صفحه تمیز maintenance با پیام آمده از backend. ## فایل‌های مرتبط | فایل | نقش | |------|-----| | `services/api.js` | axios instance + response interceptor (نقطه تشخیص client-side) | | `lib/req.js` | `fetchReq()` server-side — الان status را دور می‌ریزد | | `app/maintenance/page.js` | **جدید** — صفحه تعمیرات | | `components/maintenance/index.js` | **جدید** — UI کامل صفحه | | `components/notFound/index.js` | الگوی طراحی full-page state که باید کپی شود | | `app/layout.js` | `` در خط 140 | | `app/error.js` | error boundary فعلی | ## وضعیت فعلی `services/api.js:72-98` ```js api.interceptors.response.use( (response) => response.data, async (error) => { const original = error?.config; if ( error?.response?.status === 401 && original?.requireAuth && !original._retried && typeof window !== "undefined" ) { original._retried = true; const token = await refreshAccessToken(); if (token) { original.headers = { ...original.headers, Authorization: `Bearer ${token}` }; return api(original); } handleSessionExpired(); return Promise.reject(error); } if (typeof window !== "undefined" && !error?.config?.skipErrorToast) { toast.error(extractErrorMessage(error)); } return Promise.reject(error); } ); ``` `lib/req.js` ```js export const fetchReq = async (url, headers) => { try { const response = await axiosInstance.get(url, headers); return response.data; } catch (error) { console.error("fetchReq error:", error.message); return null; } }; ``` --- ## وظایف ### ۱. helper مشترک تشخیص فایل جدید `lib/maintenance.js`: ```js export const MAINTENANCE_CODE = "MAINTENANCE_MODE"; /** از یک خطای axios تشخیص می‌دهد که آیا پاسخ maintenance است */ export function isMaintenanceError(error) { const res = error?.response; if (res?.status !== 503) return false; return res?.data?.errors?.[0]?.code === MAINTENANCE_CODE; } /** از body پاسخ 503 پیام را استخراج می‌کند */ export function maintenanceMessage(data) { return data?.errors?.[0]?.message ?? "سامانه موقتاً در دسترس نیست. لطفاً چند دقیقه دیگر مجدداً تلاش کنید."; } ``` ### ۲. تشخیص client-side در interceptor در `services/api.js` **قبل از** بلاک 401 (چون 503 هرگز نباید مسیر refresh token را طی کند): ```js if (isMaintenanceError(error)) { if (typeof window !== "undefined") { const msg = maintenanceMessage(error.response.data); try { sessionStorage.setItem("maintenance_message", msg); } catch {} if (window.location.pathname !== "/maintenance") { window.location.replace("/maintenance"); } } return Promise.reject(error); } ``` نکات: - **حتماً قبل از بلاک 401** — وگرنه اگر مسیر auth هم روزی 503 بدهد، وارد حلقه refresh می‌شود. - **هیچ toast نشان نده** برای maintenance؛ ریدایرکت جایگزین آن است. مطمئن شو بلاک `toast.error` پایین اجرا نمی‌شود (به خاطر `return` زودهنگام). - `window.location.replace` عمداً استفاده شده نه `router.push` — چون interceptor خارج از React tree است و ریدایرکت باید کل state آلوده را دور بریزد. همچنین `replace` باعث می‌شود دکمه back کاربر را به صفحه شکسته برنگرداند. - شرط `pathname !== "/maintenance"` الزامی است تا اگر خود صفحه maintenance درخواستی زد، حلقه ریدایرکت بی‌نهایت نشود. ### ۳. تشخیص server-side در `fetchReq` `lib/req.js` نباید روی 503 مثل بقیه خطاها `null` برگرداند. رفتار پیشنهادی: throw کردن یک خطای مشخص که در `app/error.js` قابل تشخیص باشد — یا (ساده‌تر و مطمئن‌تر) ریدایرکت مستقیم: ```js import { redirect } from "next/navigation"; import { isMaintenanceError, maintenanceMessage } from "@/lib/maintenance"; export const fetchReq = async (url, headers) => { try { const response = await axiosInstance.get(url, headers); return response.data; } catch (error) { if (isMaintenanceError(error)) { redirect("/maintenance"); } console.error("fetchReq error:", error.message); return null; } }; ``` **بحرانی:** `redirect()` در Next.js با پرتاب یک خطای خاص (`NEXT_REDIRECT`) کار می‌کند. اگر `fetchReq` داخل یک `try/catch` دیگر در صفحه صدا زده شود، آن catch خطای redirect را می‌بلعد و ریدایرکت انجام نمی‌شود. همه‌ی call siteهای `fetchReq` را چک کن؛ هر جا داخل `try/catch` است، خطای `NEXT_REDIRECT` باید rethrow شود (`if (e?.digest?.startsWith("NEXT_REDIRECT")) throw e;`). همچنین `redirect()` را نمی‌توان داخل `generateMetadata` به‌درستی استفاده کرد — آن‌جا فقط بگذار `null` برگردد و صفحه خودش ریدایرکت کند. ### ۴. صفحه `/maintenance` `app/maintenance/page.js`: - Server Component ساده که `` را رندر می‌کند - `export const dynamic = "force-dynamic"` تا کش نشود - `generateMetadata` صادر کند با `title` مناسب و **`robots: { index: false, follow: false }`** — صفحه تعمیرات نباید ایندکس شود - بهتر: در همین صفحه یک بار سمت سرور `GET /api/v1/...` سبک بزن (مثلاً همان endpoint سلامت یا هر endpoint عمومی) تا اگر maintenance **تمام شده بود**، کاربر را به `/` برگرداند — وگرنه کاربری که این URL را باز نگه داشته برای همیشه صفحه تعمیرات می‌بیند `components/maintenance/index.js`: - طراحی را از `components/notFound/index.js` کپی کن — همان ساختار، همان MUI `Button`، همان breakpointهای Tailwind، همان لحن فارسی. **صفحه جدید با طراحی جدید نساز.** - پیام: اول از `sessionStorage.getItem("maintenance_message")` (client) بخوان؛ اگر نبود متن پیش‌فرض فارسی - دکمه «تلاش مجدد» که `window.location.href = "/"` می‌کند - اختیاری: یک `setInterval` هر ۶۰ ثانیه که خودکار `/` را چک کند و اگر بالا آمد ریدایرکت کند **نکته:** دکمه `components/notFound/index.js:28` در حال حاضر نه `onClick` دارد نه `href` — کنترل مرده است. آن باگ را کپی نکن. ### ۵. جلوگیری از حلقه - صفحه `/maintenance` نباید هیچ درخواست `requireAuth` بزند - اگر لایه‌های دیگری هم مستقیم fetch می‌زنند (`services/clinicApi.js` که native fetch است و روی `!response.ok` استثنا را می‌بلعد و آرایه خالی برمی‌گرداند)، آن‌ها را هم برای 503 تطبیق بده — وگرنه صفحه کلینیک در حالت maintenance «۰ پزشک» نشان می‌دهد که گمراه‌کننده است ## نکات مهم - تشخیص **فقط** با ترکیب `503 + code === 'MAINTENANCE_MODE'`. صرفِ 503 کافی نیست. - 503 هرگز نباید مسیر refresh token / `handleSessionExpired` را فعال کند — کاربر نباید در حالت تعمیرات logout شود. - `robots: noindex` روی صفحه maintenance الزامی است. - `CustomToastify` در [app/CustomToastify.js:6-14](app/CustomToastify.js#L6-L14) برای همه‌ی toastها آیکون تیک ثابت دارد؛ به همین دلیل هم نمایش خطای maintenance با toast مناسب نیست. - ترتیب اجرا: **اول** پرامپت backend (`clinicpro/.claude/prompt/maintenance-mode.md`)، بعد این. برای تست، maintenance را از پنل ادمین روشن کن و سایت عمومی را باز کن. - تست دستی: (۱) صفحه اصلی (server-side render) (۲) صفحه پزشک `/doctor/[uuid]` (۳) یک اکشن client-side مثل جستجو (۴) خاموش کردن maintenance و اطمینان از برگشت خودکار سایت.