"use client"; import { Badge } from "@/components/ui/Badge"; import { Button } from "@/components/ui/Button"; import { Input, Select } from "@/components/ui/Field"; import { Modal } from "@/components/ui/Modal"; import { Spinner } from "@/components/ui/Spinner"; import { InfoTip, Tooltip } from "@/components/ui/Tooltip"; import { api, CarpSwitchPreview, Host, MaintenanceState } from "@/lib/api"; import { useLanguage } from "@/components/LanguageProvider"; import type { TranslationParams } from "@/lib/i18n"; import { useReadOnly } from "@/lib/useReadOnly"; import { ArrowLeftRight, ArrowRight } from "lucide-react"; import { useState } from "react"; export function carpMaintenanceWarning(host: Host, state: MaintenanceState, t: (key: string, params?: TranslationParams) => string): string | null { if (!host.cluster_name || state.carp_maintenance_mode) return null; const peer = state.peers?.find((peer) => peer.id !== host.id && peer.carp_maintenance_mode); return peer ? t("carp.peerInMaintenance", { peer: peer.name }) : null; } export function CarpSwitchControl({ host, state, disabled = false, onChanged }: { host: Host; state: MaintenanceState; disabled?: boolean; onChanged: () => unknown | Promise; }) { const readOnly = useReadOnly(); const { t, errorMessage, locale } = useLanguage(); const [plan, setPlan] = useState(null); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const [name, setName] = useState(""); const [ack, setAck] = useState(false); const [autoRevertMinutes, setAutoRevertMinutes] = useState(30); const maintenanceMode = Boolean(state.carp_maintenance_mode); const role = state.carp_status ?? host.carp_status; if (!host.cluster_name || (!maintenanceMode && role !== "MASTER")) return null; const enter = !maintenanceMode; // Entering maintenance on a second node while one is already demoted // leaves both stuck (observed live twice) — steer to the right host // instead of letting this button open a preview that the backend would // reject anyway. const peerInMaintenance = enter ? state.peers?.find((peer) => peer.id !== host.id && peer.carp_maintenance_mode) : undefined; if (peerInMaintenance) return null; async function preview() { setBusy(true);setError(null); try { setPlan(await api.previewCarpSwitch(host.id));setName("");setAck(false); } catch (caught) { setError(errorMessage(caught, "carp.previewFailed")); } finally { setBusy(false); } } async function execute() { if (!plan) return; setBusy(true);setError(null); try { await api.executeCarpSwitch(host.id,plan.preview_id,name,ack,plan.action==="carp_enter"?(autoRevertMinutes||null):null); setPlan(null); await onChanged(); } catch (caught) { setError(errorMessage(caught, "carp.outcomeUnclear")); setPlan(null); await onChanged(); } finally { setBusy(false); } } return <>
{error &&

{error}

}
{plan && { if (!busy) setPlan(null); }}>

{plan.action === "carp_enter" ? t("carp.handoverShort", { host: host.name }) : t("carp.leaveShort")}

{plan.state.peers.map((peer) => { const current = peer.carp_status ?? peer.status; const expected = plan.action === "carp_enter" ? (peer.id === host.id ? "BACKUP" : "MASTER") : null; return (

{peer.name} {current}{expected && <>{expected}}

); })}

{t("carp.previewValidShort", { time: new Date(plan.expires_at).toLocaleTimeString(locale) })}

{plan.state.auto_revert_window && ( {t("carp.autoRevertRunning", { time: new Date(plan.state.auto_revert_window.auto_revert_at).toLocaleString(locale) })} )} {plan.action === "carp_enter" && (
{t("carp.autoRevertAfter")}
)} {error &&

{error}

}
} ; }