import { useEffect, useMemo, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { createConfigSyncCycle, fetchCliTargets, fetchConfigSyncCycleTasks, fetchConfigSyncCycles, fetchConfigSyncDashboard, pauseConfigSyncCycle, resumeConfigSyncCycle, stopConfigSyncCycle, updateConfigSyncPolicy, } from "../services/api"; import { queryKeys } from "../constants/queryKeys"; import { useI18n } from "../i18n"; import { useToast } from "../hooks/useToast"; import type { CliTargetItem, ConfigSyncTargetRef } from "../types"; import { pageCount } from "../utils/display"; import { formatSystemTime } from "../utils/time"; const POLL_MS = 2500; const TARGET_PAGE_SIZE = 20; export function ConfigSyncPage() { const { t } = useI18n(); const { showOk, showError } = useToast(); const queryClient = useQueryClient(); const [cyclePage, setCyclePage] = useState(1); const [expandedCycleId, setExpandedCycleId] = useState(""); const [taskPage, setTaskPage] = useState(1); const [taskStatus, setTaskStatus] = useState(""); const [taskKeyword, setTaskKeyword] = useState(""); const [enabled, setEnabled] = useState(false); const [intervalDays, setIntervalDays] = useState(3); const [concurrency, setConcurrency] = useState(5); const [scopeMode, setScopeMode] = useState<"all" | "selected">("all"); const [historyKeep, setHistoryKeep] = useState(3); const [cycleKeep, setCycleKeep] = useState(30); const [selectedMap, setSelectedMap] = useState>({}); const [policyHydrated, setPolicyHydrated] = useState(false); const [targetKeyword, setTargetKeyword] = useState(""); const [targetPage, setTargetPage] = useState(1); const dashQuery = useQuery({ queryKey: queryKeys.configSyncDashboard, queryFn: fetchConfigSyncDashboard, staleTime: 1000, refetchInterval: (q) => { const running = q.state.data?.running_cycle; return running && (running.status === "running" || running.status === "paused" || running.status === "pending") ? POLL_MS : false; }, }); useEffect(() => { if (!dashQuery.data || policyHydrated) return; const p = dashQuery.data.policy; setEnabled(Boolean(p.enabled)); setIntervalDays(Number(p.interval_days || 3)); setConcurrency(Number(p.concurrency || 5)); setScopeMode(p.scope_mode === "selected" ? "selected" : "all"); setHistoryKeep(Number(p.history_keep ?? 3)); setCycleKeep(Math.max(0, Math.min(200, Number(p.cycle_keep ?? 30)))); const map: Record = {}; for (const ref of p.selected_targets || []) { map[`${ref.source}:${ref.id}`] = { source: ref.source, id: ref.id }; } setSelectedMap(map); setPolicyHydrated(true); }, [dashQuery.data, policyHydrated]); const cyclesQuery = useQuery({ queryKey: queryKeys.configSyncCycles(cyclePage), queryFn: () => fetchConfigSyncCycles({ page: cyclePage, pageSize: 10 }), staleTime: 1000, refetchInterval: () => (dashQuery.data?.running_cycle ? POLL_MS : false), }); const tasksQuery = useQuery({ queryKey: queryKeys.configSyncCycleTasks(expandedCycleId, taskPage, taskStatus, taskKeyword), queryFn: () => fetchConfigSyncCycleTasks({ cycleId: expandedCycleId, page: taskPage, pageSize: 20, status: taskStatus, keyword: taskKeyword, }), enabled: Boolean(expandedCycleId), staleTime: 800, refetchInterval: () => (dashQuery.data?.running_cycle?.id === expandedCycleId ? POLL_MS : false), }); const targetsQuery = useQuery({ queryKey: queryKeys.cliTargets(targetKeyword, targetPage, TARGET_PAGE_SIZE), queryFn: () => fetchCliTargets({ source: "all", keyword: targetKeyword, page: targetPage, pageSize: TARGET_PAGE_SIZE }), enabled: scopeMode === "selected", staleTime: 5000, }); const refresh = async () => { await Promise.all([ queryClient.invalidateQueries({ queryKey: queryKeys.configSyncDashboard }), queryClient.invalidateQueries({ queryKey: queryKeys.configSyncCyclesAll }), queryClient.invalidateQueries({ queryKey: queryKeys.configSyncCycleTasksAll }), ]); }; const savePolicyMut = useMutation({ mutationFn: () => updateConfigSyncPolicy({ enabled, interval_days: intervalDays, concurrency, scope_mode: scopeMode, history_keep: historyKeep, cycle_keep: cycleKeep, selected_targets: Object.values(selectedMap), }), onSuccess: async (saved) => { // Apply server response immediately — do NOT flip policyHydrated false then // rehydrate from a possibly-stale dashboard cache (checkbox "pops back" bug). setEnabled(Boolean(saved.enabled)); setIntervalDays(Number(saved.interval_days || 3)); setConcurrency(Number(saved.concurrency || 5)); setScopeMode(saved.scope_mode === "selected" ? "selected" : "all"); setHistoryKeep(Number(saved.history_keep ?? 3)); setCycleKeep(Math.max(0, Math.min(200, Number(saved.cycle_keep ?? 30)))); const map: Record = {}; for (const ref of saved.selected_targets || []) { map[`${ref.source}:${ref.id}`] = { source: ref.source, id: ref.id }; } setSelectedMap(map); setPolicyHydrated(true); queryClient.setQueryData(queryKeys.configSyncDashboard, (prev: unknown) => { if (!prev || typeof prev !== "object") return prev; return { ...(prev as object), policy: saved }; }); showOk(t("configSync.policySaved")); await refresh(); }, onError: (err) => showError(String(err)), }); const startMut = useMutation({ mutationFn: (mode: "full" | "retry_failed") => createConfigSyncCycle({ mode }), onSuccess: async () => { showOk(t("configSync.started")); await refresh(); }, onError: (err) => showError(String(err)), }); const pauseMut = useMutation({ mutationFn: (id: string) => pauseConfigSyncCycle(id), onSuccess: async () => { showOk(t("configSync.paused")); await refresh(); }, onError: (err) => showError(String(err)), }); const resumeMut = useMutation({ mutationFn: (id: string) => resumeConfigSyncCycle(id), onSuccess: async () => { showOk(t("configSync.resumed")); await refresh(); }, onError: (err) => showError(String(err)), }); const stopMut = useMutation({ mutationFn: (id: string) => stopConfigSyncCycle(id), onSuccess: async () => { showOk(t("configSync.stopped")); await refresh(); }, onError: (err) => showError(String(err)), }); const dash = dashQuery.data; const running = dash?.running_cycle; const last = dash?.last_cycle; const cycles = cyclesQuery.data?.items ?? []; const cycleTotal = Number(cyclesQuery.data?.total || 0); const cyclePages = pageCount(cycleTotal, 10); const selectedCount = useMemo(() => Object.keys(selectedMap).length, [selectedMap]); const toggleTarget = (row: CliTargetItem) => { const source = row.source === "ume" ? "ume" : "managed"; const key = `${source}:${row.id}`; setSelectedMap((prev) => { const next = { ...prev }; if (next[key]) delete next[key]; else next[key] = { source, id: row.id }; return next; }); }; return (

{t("configSync.title")}

{running?.status === "running" || running?.status === "pending" ? ( ) : null} {running?.status === "paused" ? ( ) : null} {running && (running.status === "running" || running.status === "paused" || running.status === "pending") ? ( ) : null}
{t("configSync.kpi.snapshots")}
{dash?.snapshot_count ?? "—"}
{t("configSync.kpi.running")}
{running ? `${running.status} · ${running.success_count}/${running.planned_count}` : t("configSync.kpi.idle")}
{t("configSync.kpi.last")}
{last ? `${last.status} · ok ${last.success_count} / fail ${last.fail_count}` : t("common.empty")}
{t("configSync.kpi.nextDue")}
{dash?.next_due_at ? formatSystemTime(dash.next_due_at) : t("common.empty")}

{t("configSync.policyTitle")}

{scopeMode === "selected" ? (

{t("configSync.selectedCount", { count: String(selectedCount) })}

{ setTargetKeyword(e.target.value); setTargetPage(1); }} />
{(targetsQuery.data?.items ?? []).map((row) => { const source = row.source === "ume" ? "ume" : "managed"; const key = `${source}:${row.id}`; return ( ); })}
{t("configSync.col.source")} {t("configSync.col.name")} IP {t("configSync.col.vendor")}
toggleTarget(row)} /> {source} {row.name} {row.ip_address} {row.vendor || "-"}
{t("common.pagerMeta", { total: String(targetsQuery.data?.total ?? 0), page: String(targetPage), pages: String(pageCount(Number(targetsQuery.data?.total || 0), TARGET_PAGE_SIZE)), })}
) : null}

{t("configSync.cyclesTitle")}

{cycles.map((c) => ( ))} {!cycles.length ? ( ) : null}
ID {t("configSync.col.trigger")} {t("configSync.col.status")} {t("configSync.col.progress")} {t("configSync.col.started")} {t("configSync.col.ended")} {t("configSync.col.actions")}
{c.id.slice(0, 8)} {c.trigger_mode} {c.status} {c.success_count}/{c.planned_count} · fail {c.fail_count} {c.started_at ? formatSystemTime(c.started_at) : "-"} {c.ended_at ? formatSystemTime(c.ended_at) : "-"}
{c.status === "running" || c.status === "pending" ? ( ) : null} {c.status === "paused" ? ( ) : null} {c.status === "running" || c.status === "paused" || c.status === "pending" ? ( ) : null}
{t("configSync.cyclesEmpty")}
{t("common.pagerMeta", { total: String(cycleTotal), page: String(cyclePage), pages: String(cyclePages), })}
{expandedCycleId ? (
setExpandedCycleId("")} >
e.stopPropagation()} >

{t("configSync.tasksTitle")}

{expandedCycleId.slice(0, 8)} {(() => { const c = cycles.find((x) => x.id === expandedCycleId); return c ? ` · ${c.trigger_mode} · ${c.status} · ${c.success_count}/${c.planned_count}` : ""; })()}

{ setTaskKeyword(e.target.value); setTaskPage(1); }} />
{tasksQuery.isLoading ?

{t("common.refreshing")}

: null} {tasksQuery.isError ? (

{tasksQuery.error instanceof Error ? tasksQuery.error.message : String(tasksQuery.error)}

) : null}
{(tasksQuery.data?.items ?? []).map((task) => ( ))} {!tasksQuery.isLoading && !(tasksQuery.data?.items ?? []).length ? ( ) : null}
{t("configSync.col.name")} IP {t("configSync.col.vendor")} {t("configSync.col.source")} {t("configSync.col.status")} {t("configSync.col.message")}
{task.ne_name || task.target_id} {task.ne_ip} {task.vendor || "-"} {task.source} {task.status} {task.message || "-"}
{t("common.empty")}
{t("common.pagerMeta", { total: String(tasksQuery.data?.total ?? 0), page: String(taskPage), pages: String(pageCount(Number(tasksQuery.data?.total || 0), 20)), })}
) : null}
); }