feat(maintenance): show maintenance page when the API is in maintenance
The backend now answers 503 with code MAINTENANCE_MODE while maintenance is on. Without this change a visitor got a red error toast over a broken page client-side, and a silently empty page server-side, because fetchReq discards the status and returns null on any failure. - lib/maintenance.js detects the state by BOTH status 503 and the error code; a bare 503 can come from a reverse proxy and is not maintenance - The axios interceptor checks it before the 401 branch, so a maintenance response never triggers the refresh-token path or logs the user out - fetchReq redirects to /maintenance, with a silentMaintenance opt-out used by getStateInfo: that one runs inside generateMetadata and while rendering the maintenance page itself, where a redirect is either ineffective or loops - redirect() works by throwing, so the try/catch blocks in the doctors, clinics and specialties pages now rethrow NEXT_REDIRECT instead of swallowing it - clinicApi.js handles 503 too; it previously rendered maintenance as a clinic with zero doctors - The page reuses the existing 404 design and is marked noindex Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
# نمایش حالت تعمیرات (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` | `<CustomToastify />` در خط 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 ساده که `<MaintenancePage />` را رندر میکند
|
||||
- `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 و اطمینان از برگشت خودکار سایت.
|
||||
Reference in New Issue
Block a user