diff --git a/apps/web/src/app/[locale]/action-logs/page.tsx b/apps/web/src/app/[locale]/action-logs/page.tsx index f623e6986..09f247308 100644 --- a/apps/web/src/app/[locale]/action-logs/page.tsx +++ b/apps/web/src/app/[locale]/action-logs/page.tsx @@ -13,9 +13,13 @@ * - 執行時間、耗時、簽核者資訊 * * i18n: 100% next-intl,零硬編碼 + * + * 版本: v1.1 + * 變更: 2026-03-31 (台北時區) - Claude Code + * - #19 P2: 新增 AbortController 防止 unmount 時記憶體洩漏 */ -import { useState, useEffect, useCallback } from 'react' +import { useState, useEffect, useCallback, useRef } from 'react' import { useTranslations } from 'next-intl' import { AppLayout } from '@/components/layout' import { DataPincerPanel } from '@/components/cyber' @@ -171,6 +175,9 @@ export default function ActionLogPage({ const [totalPages, setTotalPages] = useState(1) const [totalCount, setTotalCount] = useState(0) + // #19 P2: AbortController 防止 unmount 時記憶體洩漏 + const abortControllerRef = useRef(null) + // ========================================================================== // Fetch Audit Logs // ========================================================================== @@ -178,13 +185,21 @@ export default function ActionLogPage({ const apiBaseUrl = getApiBaseUrl() if (!apiBaseUrl) return + // 取消前一次未完成的請求 + abortControllerRef.current?.abort() + const controller = new AbortController() + abortControllerRef.current = controller + setIsLoading(true) setError(null) try { const response = await fetch( `${apiBaseUrl}/api/v1/audit-logs?page=${pageNum}&page_size=10`, - { headers: { 'Content-Type': 'application/json' } } + { + headers: { 'Content-Type': 'application/json' }, + signal: controller.signal, + } ) if (!response.ok) { @@ -197,6 +212,10 @@ export default function ActionLogPage({ setTotalPages(data.total_pages) setTotalCount(data.count) } catch (err) { + // 忽略 AbortError (正常行為) + if (err instanceof Error && err.name === 'AbortError') { + return + } const message = err instanceof Error ? err.message : 'Unknown error' setError(message) console.error('[ActionLog] Fetch error:', message) // eslint-disable-line no-console @@ -215,6 +234,7 @@ export default function ActionLogPage({ try { const response = await fetch(`${apiBaseUrl}/api/v1/audit-logs/stats`, { headers: { 'Content-Type': 'application/json' }, + signal: abortControllerRef.current?.signal, }) if (response.ok) { @@ -222,16 +242,25 @@ export default function ActionLogPage({ setStats(data) } } catch (err) { + // 忽略 AbortError + if (err instanceof Error && err.name === 'AbortError') { + return + } console.error('[ActionLog] Stats fetch error:', err) // eslint-disable-line no-console } }, []) // ========================================================================== - // Initial Fetch + // Initial Fetch + Cleanup // ========================================================================== useEffect(() => { fetchLogs(1) fetchStats() + + // #19 P2: Cleanup - 組件 unmount 時取消所有請求 + return () => { + abortControllerRef.current?.abort() + } }, [fetchLogs, fetchStats]) // ========================================================================== diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index c14e7bf28..62f03bf6c 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -16,7 +16,9 @@ | **#15 SSE + 樂觀更新** | ✅ **完成** (`8c8664c`) | | **#16 DOM Bypass** | ✅ **完成** (`0b87018`) | | **#17 i18n Hydration** | ✅ **完成** (`f25e94e`) | -| **首席架構師審查** | ✅ **96/100 OUTSTANDING** (前端 P1) | +| **#18 SSE Exponential Backoff** | ✅ **已實作** (4 stores 已含) | +| **#19 Action Logs AbortController** | ✅ **完成** (2026-03-31) | +| **首席架構師審查** | ✅ **96/100 + 98/100 OUTSTANDING** (前端 P1+P2) | | **Phase A/B/C P1** | ✅ **97/100 OUTSTANDING** | | **K8sRepository** | ✅ **leWOOOgo 積木化封裝** | | **OTEL 追蹤** | ✅ **Telegram Gateway 完整 Span** |