功能:SSL 憑證過期偵測與提醒、歷史戰績改為分頁載入
根本原因: 1. 憑證過期時瀏覽器只回 Failed to fetch,前端無法辨識原因,使用者誤以為是 DB 連不上(本次 7/1 憑證過期即是如此)。 2. 歷史戰績一次撈出全部資料(400+ 筆、逾 100KB),筆數持續成長會越來越慢。 影響:所有 API 的連線錯誤訊息、歷史戰績頁載入速度、Docker 憑證掛載路徑與檔名。 修法: - 後端讀取 cert.pem 到期日(60 秒快取),由 /api/version 與 /api/health 回傳 certNotAfter - 前端把到期日存進 localStorage;fetch 失敗時依離線、憑證已過期、其他連線問題分別顯示明確訊息 - 憑證到期前 7 天於頁面跳出「SSL 憑證提醒」通知,可手動關閉 - /api/history 支援 page/pageSize(預設 20 筆、上限 100),回傳 total 與 totalPages - 歷史戰績頁加入分頁控制列(上一頁/頁碼含省略號/下一頁/總筆數),刪除後自動重新載入並修正超出範圍的頁碼 - 憑證掛載目錄改為 /volume1/docker/certs,檔名改為 cert.pem / chain.pem / privkey.pem,app 容器同步掛載以讀取到期日 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+49
-1
@@ -5,6 +5,7 @@ import {
|
||||
createLiveRoom,
|
||||
loadMatchResults,
|
||||
releaseLiveRoom,
|
||||
rememberCertNotAfter,
|
||||
saveMatchHistory,
|
||||
sendLiveRoomHeartbeat,
|
||||
updateLiveRoom,
|
||||
@@ -106,6 +107,8 @@ const STREAK_TITLES: Record<number, string> = {
|
||||
const PWA_UPDATE_EVENT = 'badminton-scoreboard:pwa-update-ready'
|
||||
const APP_VERSION_POLL_MS = 30000
|
||||
const LIVE_ROOM_HEARTBEAT_MS = 10_000
|
||||
// 憑證到期前 7 天開始提醒,避免到期當天才突然全部連不上。
|
||||
const CERT_EXPIRY_WARNING_MS = 7 * 24 * 60 * 60 * 1000
|
||||
|
||||
function App() {
|
||||
const location = useLocation()
|
||||
@@ -143,6 +146,8 @@ function App() {
|
||||
const [victoryAnnouncement, setVictoryAnnouncement] = useState<VictoryAnnouncement | null>(null)
|
||||
const [voiceAnnouncement, setVoiceAnnouncement] = useState<VoiceAnnouncement | null>(null)
|
||||
const [pwaUpdateReady, setPwaUpdateReady] = useState(false)
|
||||
const [certExpiryWarning, setCertExpiryWarning] = useState('')
|
||||
const certWarningDismissedRef = useRef(false)
|
||||
const [liveRoomSession, setLiveRoomSession] = useState<LiveRoomSession | null>(null)
|
||||
const [navigationLockMessage, setNavigationLockMessage] = useState('')
|
||||
// 結算完成後遞增,通知記分板自動打開選隊伍面板讓人選下一場。
|
||||
@@ -256,12 +261,36 @@ function App() {
|
||||
}
|
||||
|
||||
const payload = (await response.json()) as {
|
||||
certNotAfter?: string | null
|
||||
ok?: boolean
|
||||
version?: string
|
||||
}
|
||||
const nextVersion = payload.version?.trim()
|
||||
|
||||
if (!active || !nextVersion) {
|
||||
if (!active) {
|
||||
return
|
||||
}
|
||||
|
||||
rememberCertNotAfter(payload.certNotAfter)
|
||||
|
||||
const certNotAfterTime = payload.certNotAfter ? Date.parse(payload.certNotAfter) : NaN
|
||||
|
||||
if (
|
||||
Number.isFinite(certNotAfterTime) &&
|
||||
certNotAfterTime - Date.now() < CERT_EXPIRY_WARNING_MS &&
|
||||
!certWarningDismissedRef.current
|
||||
) {
|
||||
const expiryLabel = new Date(certNotAfterTime).toLocaleString('zh-TW', {
|
||||
hour12: false,
|
||||
})
|
||||
setCertExpiryWarning(
|
||||
certNotAfterTime < Date.now()
|
||||
? `SSL 憑證已於 ${expiryLabel} 過期,請盡快更新伺服器憑證。`
|
||||
: `SSL 憑證將於 ${expiryLabel} 到期,請記得更新伺服器憑證。`,
|
||||
)
|
||||
}
|
||||
|
||||
if (!nextVersion) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1096,6 +1125,25 @@ function App() {
|
||||
{navigationLockMessage}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{certExpiryWarning && !pwaUpdateReady ? (
|
||||
<div className="pwa-update-toast" role="alert">
|
||||
<div className="pwa-update-copy">
|
||||
<strong>SSL 憑證提醒</strong>
|
||||
<span>{certExpiryWarning}</span>
|
||||
</div>
|
||||
<button
|
||||
className="pwa-update-button"
|
||||
onClick={() => {
|
||||
certWarningDismissedRef.current = true
|
||||
setCertExpiryWarning('')
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
知道了
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user