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:
hamed
2026-07-19 22:01:45 +03:30
co-authored by Claude Fable 5
parent 18484f974d
commit daf38c8631
11 changed files with 393 additions and 4 deletions
+5 -1
View File
@@ -24,8 +24,12 @@ async function fetchSiteContext(host) {
let value = null;
try {
// این تابع از generateMetadata و از خودِ صفحه‌ی تعمیرات هم صدا زده می‌شود؛
// ریدایرکت اینجا یا بی‌اثر است یا حلقه می‌سازد.
const json = await fetchReq(
`${API_URL}/api/v1/site-context?domain=${encodeURIComponent(host)}`
`${API_URL}/api/v1/site-context?domain=${encodeURIComponent(host)}`,
undefined,
{ silentMaintenance: true }
);
if (json?.data?.type === "representation") {
value = json.data.representation; // { uuid, full_name, is_global }
+28
View File
@@ -0,0 +1,28 @@
export const MAINTENANCE_CODE = "MAINTENANCE_MODE";
export const MAINTENANCE_PATH = "/maintenance";
export const DEFAULT_MAINTENANCE_MESSAGE =
"سامانه موقتاً در دسترس نیست. لطفاً چند دقیقه دیگر مجدداً تلاش کنید.";
/**
* تشخیص پاسخ حالت تعمیرات backend.
*
* هر دو شرط لازم است: یک ۵۰۳ خالی می‌تواند از reverse proxy یا load balancer هم
* بیاید و آن حالت تعمیرات نیست.
*/
export function isMaintenanceError(error) {
const response = error?.response;
if (response?.status !== 503) return false;
return response?.data?.errors?.[0]?.code === MAINTENANCE_CODE;
}
export function maintenanceMessage(data) {
return data?.errors?.[0]?.message ?? DEFAULT_MAINTENANCE_MESSAGE;
}
/** خطای redirect نکست با throw کار می‌کند و نباید در catchهای عمومی بلعیده شود. */
export function isNextRedirectError(error) {
return typeof error?.digest === "string" && error.digest.startsWith("NEXT_REDIRECT");
}
+15 -1
View File
@@ -1,5 +1,8 @@
import axios from "axios";
import https from "https";
import { redirect } from "next/navigation";
import { isMaintenanceError, MAINTENANCE_PATH } from "@/lib/maintenance";
export const axiosInstance = axios.create({
...(process.env.NODE_ENV === "development" && {
@@ -7,11 +10,22 @@ export const axiosInstance = axios.create({
}),
});
export const fetchReq = async (url, headers) => {
/**
* @param {object} [options]
* @param {boolean} [options.silentMaintenance] در حالت تعمیرات به‌جای ریدایرکت
* `null` برگردان. برای فراخوانی‌هایی لازم است که داخل `generateMetadata` یا در
* مسیر رندرِ خودِ صفحه‌ی تعمیرات اجرا می‌شوند و ریدایرکت آن‌ها یا بی‌اثر است یا
* حلقه می‌سازد (مثل `getStateInfo`).
*/
export const fetchReq = async (url, headers, options = {}) => {
try {
const response = await axiosInstance.get(url, headers);
return response.data;
} catch (error) {
if (isMaintenanceError(error)) {
if (options.silentMaintenance) return null;
redirect(MAINTENANCE_PATH);
}
console.error("fetchReq error:", error.message);
return null;
}