Files
CC-Switch/src/components/providers/ProviderHealthBadge.tsx
T
Jason b642ef0633 fix(failover): patch P1-P3 reliability gaps surfaced by team review
- Forwarder buffers non-streaming bodies and primes streaming first
  chunk before signaling success, so body timeouts and SSE first-chunk
  failures route through the circuit breaker instead of being recorded
  as success on response-header arrival
- Atomic enable-failover: switch to P1 before persisting the flag, and
  roll back auto-added queue entries when the switch is rejected
  (e.g. official providers)
- Hot-reload circuit breaker config on per-app proxy config change
  instead of waiting for a proxy restart
- FailoverToggle / FailoverQueueManager / AutoFailoverConfigPanel
  require proxy takeover for the active app; the backend command also
  rejects enabling when takeover is off
- ProviderHealthBadge consumes the backend is_healthy flag instead of
  hardcoding the 5-failure threshold

Cleanup:
- impl From<&AppProxyConfig> for CircuitBreakerConfig and use it from
  the command layer
- Collapse three identical TabsContent blocks into a single map
2026-05-14 21:35:02 +08:00

78 lines
2.1 KiB
TypeScript

import { cn } from "@/lib/utils";
import { ProviderHealthStatus } from "@/types/proxy";
import { useTranslation } from "react-i18next";
interface ProviderHealthBadgeProps {
consecutiveFailures: number;
isHealthy?: boolean;
className?: string;
}
/**
* 供应商健康状态徽章
* 根据连续失败次数显示不同颜色的状态指示器
*/
export function ProviderHealthBadge({
consecutiveFailures,
isHealthy,
className,
}: ProviderHealthBadgeProps) {
const { t } = useTranslation();
// 根据失败次数计算状态
const getStatus = () => {
if (consecutiveFailures === 0) {
return {
labelKey: "health.operational",
labelFallback: "正常",
status: ProviderHealthStatus.Healthy,
color: "bg-green-500",
// 使用更深/柔和的背景色,去除可能的白色内容感
bgColor: "bg-green-500/10",
textColor: "text-green-600 dark:text-green-400",
};
} else if (isHealthy !== false) {
return {
labelKey: "health.degraded",
labelFallback: "降级",
status: ProviderHealthStatus.Degraded,
color: "bg-yellow-500",
bgColor: "bg-yellow-500/10",
textColor: "text-yellow-600 dark:text-yellow-400",
};
} else {
return {
labelKey: "health.circuitOpen",
labelFallback: "熔断",
status: ProviderHealthStatus.Failed,
color: "bg-red-500",
bgColor: "bg-red-500/10",
textColor: "text-red-600 dark:text-red-400",
};
}
};
const statusConfig = getStatus();
const label = t(statusConfig.labelKey, {
defaultValue: statusConfig.labelFallback,
});
return (
<div
className={cn(
"inline-flex items-center gap-1.5 px-2 py-1 rounded-full text-xs font-medium",
statusConfig.bgColor,
statusConfig.textColor,
className,
)}
title={t("health.consecutiveFailures", {
count: consecutiveFailures,
defaultValue: `连续失败 ${consecutiveFailures}`,
})}
>
<div className={cn("w-2 h-2 rounded-full", statusConfig.color)} />
<span>{label}</span>
</div>
);
}