Files
nobat724_front/.claude/prompt/maintenance-mode-client.md
hamedandClaude Fable 5 daf38c8631 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>
2026-07-19 22:01:45 +03:30

10 KiB
Raw Permalink Blame History

نمایش حالت تعمیرات (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 فقط یک toast.error قرمز نشان می‌دهد و صفحه خالی/شکسته می‌ماند
  • درخواست‌های server-side: fetchReq در lib/req.js:16 روی هر خطا 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

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

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:

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 را طی کند):

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 قابل تشخیص باشد — یا (ساده‌تر و مطمئن‌تر) ریدایرکت مستقیم:

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 برای همه‌ی toastها آیکون تیک ثابت دارد؛ به همین دلیل هم نمایش خطای maintenance با toast مناسب نیست.
  • ترتیب اجرا: اول پرامپت backend (clinicpro/.claude/prompt/maintenance-mode.md)، بعد این. برای تست، maintenance را از پنل ادمین روشن کن و سایت عمومی را باز کن.
  • تست دستی: (۱) صفحه اصلی (server-side render) (۲) صفحه پزشک /doctor/[uuid] (۳) یک اکشن client-side مثل جستجو (۴) خاموش کردن maintenance و اطمینان از برگشت خودکار سایت.