45 個 component + 6 個 page 統一從舊 font-mono 遷移到 font-body (DM Mono),確保設計系統一致性。 font-body = DM Mono (等寬),視覺效果相同但走新設計 token。 保留: font-heading (Syne)、font-dot-matrix (VT323/DSEG7) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
148 lines
4.6 KiB
TypeScript
148 lines
4.6 KiB
TypeScript
'use client'
|
||
/* eslint-disable no-console -- Error boundary requires console for crash logging */
|
||
|
||
import type { ErrorInfo, ReactNode } from 'react';
|
||
import React, { Component } from 'react'
|
||
import { useTranslations } from 'next-intl'
|
||
import { useAgentStore } from '@/stores/agent.store'
|
||
|
||
interface ErrorBoundaryTranslations {
|
||
systemFailure: string;
|
||
criticalError: string;
|
||
escalating: string;
|
||
forceRestart: string;
|
||
detectingAnomaly: string;
|
||
autoHealingAttempt: (attempt: number) => string;
|
||
}
|
||
|
||
interface InnerProps {
|
||
children: ReactNode;
|
||
fallbackMessage?: string;
|
||
translations: ErrorBoundaryTranslations;
|
||
}
|
||
|
||
interface State {
|
||
hasError: boolean;
|
||
retryCount: number;
|
||
}
|
||
|
||
class AutoHealingErrorBoundaryInner extends Component<InnerProps, State> {
|
||
public state: State = {
|
||
hasError: false,
|
||
retryCount: 0
|
||
};
|
||
|
||
public static getDerivedStateFromError(_: Error): Partial<State> {
|
||
// 只設定 hasError,保留 retryCount 防止無限重試
|
||
return { hasError: true };
|
||
}
|
||
|
||
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||
console.error('[AWOOOI] Frontend component crashed:', error, errorInfo);
|
||
// 檢查是否已達重試上限
|
||
if (this.state.retryCount < 3) {
|
||
this.attemptAutoHealing();
|
||
}
|
||
}
|
||
|
||
private attemptAutoHealing = () => {
|
||
const { retryCount } = this.state;
|
||
|
||
if (retryCount >= 3) {
|
||
console.error('[AWOOOI] Auto-healing failed after 3 attempts. Escalating to L3 AIOps.');
|
||
return;
|
||
}
|
||
|
||
// L1 Auto-Healing Logic
|
||
console.log(`[AWOOOI] Attempting auto-healing (Attempt ${retryCount + 1}/3)...`);
|
||
|
||
// 1. Clear toxic stored state
|
||
if (typeof window !== 'undefined') {
|
||
try {
|
||
localStorage.clear();
|
||
sessionStorage.clear();
|
||
} catch (e) {
|
||
console.warn('Failed to clear storage:', e);
|
||
}
|
||
}
|
||
|
||
// 2. Reset Zustand store explicitly outside of React render cycle
|
||
try {
|
||
if (typeof useAgentStore.getState().reset === 'function') {
|
||
useAgentStore.getState().reset();
|
||
}
|
||
} catch (e) {
|
||
console.warn('[AWOOOI] Failed to reset Agent Store during auto-healing', e);
|
||
}
|
||
|
||
// 3. Exponential Backoff remount
|
||
const delay = Math.pow(2, retryCount) * 1000; // 1s, 2s, 4s
|
||
setTimeout(() => {
|
||
console.log('[AWOOOI] Remounting component...');
|
||
this.setState({ hasError: false, retryCount: retryCount + 1 });
|
||
}, delay);
|
||
}
|
||
|
||
public render() {
|
||
const { translations } = this.props;
|
||
|
||
if (this.state.hasError) {
|
||
if (this.state.retryCount >= 3) {
|
||
return (
|
||
<div className="flex flex-col items-center justify-center p-8 bg-[#111111] text-white border border-red-500/30 rounded-lg max-w-lg mx-auto mt-12">
|
||
<h2 className="text-xl font-body text-red-500 mb-4">{translations.systemFailure}</h2>
|
||
<p className="font-body text-sm text-gray-400 mb-4">{this.props.fallbackMessage || translations.criticalError}</p>
|
||
<p className="font-body text-xs text-purple-400 mb-6">{translations.escalating}</p>
|
||
<button
|
||
onClick={() => window.location.reload()}
|
||
className="px-6 py-2 bg-white text-black font-body text-sm rounded-full hover:bg-gray-200 transition-colors"
|
||
>
|
||
{translations.forceRestart}
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="flex flex-col items-center justify-center p-12 space-y-4 animate-pulse">
|
||
<div className="text-red-500 font-body text-sm font-bold tracking-widest">{translations.detectingAnomaly}</div>
|
||
<div className="text-gray-400 font-body text-xs">{translations.autoHealingAttempt(this.state.retryCount + 1)}</div>
|
||
<div className="w-48 h-1 bg-gray-800 rounded overflow-hidden mt-4">
|
||
<div className="h-full bg-white opacity-50 animate-ping"></div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return this.props.children;
|
||
}
|
||
}
|
||
|
||
// Wrapper component that provides translations via hook
|
||
interface Props {
|
||
children: ReactNode;
|
||
fallbackMessage?: string;
|
||
}
|
||
|
||
export function AutoHealingErrorBoundary({ children, fallbackMessage }: Props) {
|
||
const t = useTranslations('errorBoundary');
|
||
|
||
const translations: ErrorBoundaryTranslations = {
|
||
systemFailure: t('systemFailure'),
|
||
criticalError: t('criticalError'),
|
||
escalating: t('escalating'),
|
||
forceRestart: t('forceRestart'),
|
||
detectingAnomaly: t('detectingAnomaly'),
|
||
autoHealingAttempt: (attempt: number) => t('autoHealingAttempt', { attempt }),
|
||
};
|
||
|
||
return (
|
||
<AutoHealingErrorBoundaryInner
|
||
translations={translations}
|
||
fallbackMessage={fallbackMessage}
|
||
>
|
||
{children}
|
||
</AutoHealingErrorBoundaryInner>
|
||
);
|
||
}
|