|
|
|
@@ -1,11 +1,19 @@ |
|
|
|
"use client"; |
|
|
|
|
|
|
|
import { useEffect } from "react"; |
|
|
|
import { useRouter } from "next/navigation"; |
|
|
|
import { useEffect, useMemo, useState } from "react"; |
|
|
|
|
|
|
|
const STORAGE_KEY = "fpsms_server_wait_until_ms"; |
|
|
|
const WAIT_SECONDS = 30; |
|
|
|
const RELOAD_INTERVAL_SECONDS = 5; |
|
|
|
|
|
|
|
function formatSeconds(s: number) { |
|
|
|
const v = Math.max(0, Math.floor(s)); |
|
|
|
return `${v} 秒`; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* When a server-side exception occurs (e.g. backend down during deploy), |
|
|
|
* redirect to login instead of showing the default error page. |
|
|
|
* show a reconnect countdown and retry instead of immediate redirect. |
|
|
|
*/ |
|
|
|
export default function Error({ |
|
|
|
error, |
|
|
|
@@ -14,17 +22,55 @@ export default function Error({ |
|
|
|
error: Error & { digest?: string }; |
|
|
|
reset: () => void; |
|
|
|
}) { |
|
|
|
const router = useRouter(); |
|
|
|
const [remaining, setRemaining] = useState(WAIT_SECONDS); |
|
|
|
|
|
|
|
const waitUntilMs = useMemo(() => { |
|
|
|
if (typeof window === "undefined") return Date.now() + WAIT_SECONDS * 1000; |
|
|
|
const existing = Number(window.localStorage.getItem(STORAGE_KEY) || "0"); |
|
|
|
const now = Date.now(); |
|
|
|
if (existing && existing > now) return existing; |
|
|
|
const next = now + WAIT_SECONDS * 1000; |
|
|
|
window.localStorage.setItem(STORAGE_KEY, String(next)); |
|
|
|
return next; |
|
|
|
}, []); |
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
// Redirect to login so user can re-authenticate after backend restart |
|
|
|
router.replace("/login"); |
|
|
|
}, [router]); |
|
|
|
const start = Date.now(); |
|
|
|
const tick = () => { |
|
|
|
const now = Date.now(); |
|
|
|
const msLeft = Math.max(0, waitUntilMs - now); |
|
|
|
setRemaining(msLeft / 1000); |
|
|
|
|
|
|
|
// When waiting time ends, go to login. |
|
|
|
if (msLeft <= 0) { |
|
|
|
window.localStorage.removeItem(STORAGE_KEY); |
|
|
|
window.location.href = "/login"; |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
tick(); |
|
|
|
const interval = window.setInterval(tick, 250); |
|
|
|
|
|
|
|
// Reload periodically to give backend time to come back. |
|
|
|
const reloadTimer = window.setInterval(() => { |
|
|
|
const elapsedSec = (Date.now() - start) / 1000; |
|
|
|
if (elapsedSec >= WAIT_SECONDS) return; |
|
|
|
window.location.reload(); |
|
|
|
}, RELOAD_INTERVAL_SECONDS * 1000); |
|
|
|
|
|
|
|
return () => { |
|
|
|
window.clearInterval(interval); |
|
|
|
window.clearInterval(reloadTimer); |
|
|
|
}; |
|
|
|
}, [waitUntilMs]); |
|
|
|
|
|
|
|
return ( |
|
|
|
<div className="flex min-h-[200px] flex-col items-center justify-center gap-2 p-4 text-center"> |
|
|
|
<div className="flex min-h-[200px] flex-col items-center justify-center gap-2 p-6 text-center"> |
|
|
|
<p className="text-sm font-semibold text-slate-700 dark:text-slate-200"> |
|
|
|
連線異常,伺服器暫停中 |
|
|
|
</p> |
|
|
|
<p className="text-sm text-slate-600 dark:text-slate-400"> |
|
|
|
連線異常,正在導向登入頁… |
|
|
|
系統會在後台恢復後自動重試。倒數中:{formatSeconds(remaining)} |
|
|
|
</p> |
|
|
|
</div> |
|
|
|
); |
|
|
|
|