import { Button, Input, Modal } from "@heroui/react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { ListPager } from "../../components/ListPager"; import { AppModalShell } from "../../components/ui/AppModalShell"; import { FieldSelect } from "../../components/ui/FieldSelect"; import { useDebouncedValue } from "../../hooks/useDebouncedValue"; import { useToast } from "../../hooks/useToast"; import { useI18n } from "../../i18n"; import { bizStateBulkDeleteBatches, bizStateCollectNow, bizStateCreateTask, bizStateDeleteBatch, bizStateDeleteTask, bizStateDiscover, bizStateDownloadExport, bizStateGetBatch, bizStateGetBatchCommand, bizStateGetTask, bizStateListBatches, bizStateListBatchMetricRows, bizStateListProfiles, bizStateListTasks, bizStatePatchTask, bizStatePauseTask, bizStatePurgeTask, bizStateSetBatchBaseline, bizStateSetBindings, bizStateStartTask, fetchCliTargets, formatErr, } from "../../services/api"; import type { CliTargetItem } from "../../types"; import { pageCount } from "../../utils/display"; import { writeClipboardText } from "../../utils/clipboard"; import { formatSystemTime } from "../../utils/time"; import { jobChipColor, NmStatusChip, sourceChipColor } from "./nmChips"; type TaskRow = { id: string; source?: string; ne_name: string; ne_ip: string; vendor: string; note?: string; purpose?: string; status: string; collect_running: boolean; last_error: string; last_collect_ended_at?: string | null; interval_sec?: number; }; type Placeholder = { name: string; required?: boolean; bind_mode?: string; discover_profile_id?: string; }; type Profile = { profile_id: string; title: string; command_template: string; description: string; metric_id: string; kind?: string; placeholders?: Placeholder[]; }; type BatchRow = { id: string; status: string; row_count: number; command_count: number; started_at?: string | null; is_baseline?: boolean; protected?: boolean; protect_reasons?: string[]; }; type Candidate = { value: string; label: string; rd?: string }; type SheetCol = { key: string; header: string }; type SheetCmd = { id: string; raw_command: string; parse_status?: string; row_count?: number; message?: string; has_raw?: boolean; profile_id?: string; }; type SheetTab = { id: string; title: string; rowCount: number; commands: SheetCmd[]; }; type TaskTab = "profiles" | "batches"; type NeSourceFilter = "all" | "managed" | "ume"; const NE_PAGE_SIZE = 10; const SHEET_PAGE_SIZE_OPTIONS = [20, 50, 100, 200]; const DEFAULT_SHEET_PAGE_SIZE = 50; function fmtIntervalLabel( sec: number, daysLabel: string, hoursLabel: string, secondsLabel: string, ) { const s = Math.max(1, Math.round(Number(sec || 0)) || 1); if (s % 86400 === 0) return `${s / 86400} ${daysLabel}`; if (s % 3600 === 0) return `${s / 3600} ${hoursLabel}`; return `${s} ${secondsLabel}`; } function secToIntervalUi(sec: number): { value: number; unit: "days" | "hours" | "seconds" } { const s = Math.max(1, Math.round(Number(sec || 3600)) || 3600); if (s % 86400 === 0) return { value: s / 86400, unit: "days" }; if (s % 3600 === 0) return { value: s / 3600, unit: "hours" }; return { value: s, unit: "seconds" }; } function intervalUiToSec(value: number, unit: "days" | "hours" | "seconds") { const n = Math.max(1, Number(value) || 1); if (unit === "days") return n * 86400; if (unit === "hours") return n * 3600; return Math.max(60, n); } function intervalUnitMax(unit: "days" | "hours" | "seconds") { if (unit === "days") return 365; if (unit === "hours") return 8760; return 604800; // up to 7 days in seconds } function convertIntervalValue( value: number, from: "days" | "hours" | "seconds", to: "days" | "hours" | "seconds", ) { const sec = intervalUiToSec(value, from); if (to === "days") return Math.max(1, Math.min(365, Math.round(sec / 86400) || 1)); if (to === "hours") return Math.max(1, Math.min(8760, Math.round(sec / 3600) || 1)); return Math.max(60, Math.min(604800, sec)); } function fmtTime(v?: string | null) { if (!v) return "—"; return formatSystemTime(v) || v; } function cellText(v: unknown): string { if (v === null || v === undefined) return ""; if (typeof v === "object") return JSON.stringify(v); return String(v); } function neSourceOf(row: CliTargetItem): "managed" | "ume" { return row.source === "ume" ? "ume" : "managed"; } function columnsFromRows(rows: Record[]): SheetCol[] { const keys: string[] = []; for (const row of rows) { for (const k of Object.keys(row || {})) { if (!keys.includes(k)) keys.push(k); } } return keys.map((k) => ({ key: k, header: k })); } function buildSheetTabs(batch: any, t: (k: string) => string): SheetTab[] { const tabs: SheetTab[] = []; const cmds = (batch?.commands || []) as SheetCmd[]; if (cmds.length) { tabs.push({ id: "commands", title: "Commands", rowCount: cmds.length, commands: cmds, }); } for (const s of (batch?.sheets || []) as any[]) { const mid = String(s?.metric_id || "").trim(); if (!mid || mid === "vrf_list") continue; const title = String(s?.title || "").trim() || mid; tabs.push({ id: mid, title, rowCount: Number(s?.row_count || 0), commands: Array.isArray(s?.commands) ? (s.commands as SheetCmd[]) : [], }); } return tabs; } export function BizStatePage() { const { t } = useI18n(); const { showOk, showError } = useToast(); const [tasks, setTasks] = useState([]); const [busy, setBusy] = useState(false); const [collectingIds, setCollectingIds] = useState>({}); const [listKeyword, setListKeyword] = useState(""); const debouncedListKw = useDebouncedValue(listKeyword, 250); const [purposeFilter, setPurposeFilter] = useState<"all" | "portrait" | "cutover_hf">("all"); // create-task modal (all valid CLI targets) const [createOpen, setCreateOpen] = useState(false); const [neKeyword, setNeKeyword] = useState(""); const debouncedNeKw = useDebouncedValue(neKeyword, 300); const [neSource, setNeSource] = useState("all"); const [nePage, setNePage] = useState(1); const [neTotal, setNeTotal] = useState(0); const [neItems, setNeItems] = useState([]); const [neLoading, setNeLoading] = useState(false); const [selectedNe, setSelectedNe] = useState(null); // task modal const [taskId, setTaskId] = useState(""); const [detail, setDetail] = useState(null); const [profiles, setProfiles] = useState([]); const [batches, setBatches] = useState([]); const [taskTab, setTaskTab] = useState("profiles"); const [intervalValue, setIntervalValue] = useState(1); const [intervalUnit, setIntervalUnit] = useState<"days" | "hours" | "seconds">("hours"); const [retentionDays, setRetentionDays] = useState(30); const [dailyKeepEnabled, setDailyKeepEnabled] = useState(false); const [dailyKeepCount, setDailyKeepCount] = useState(10); const [selectedBatchIds, setSelectedBatchIds] = useState([]); // VRF bind (inside task modal) const [bindItemId, setBindItemId] = useState(""); const [candidates, setCandidates] = useState([]); const [selectedVrfs, setSelectedVrfs] = useState([]); const [discoverCmd, setDiscoverCmd] = useState(""); // batch workbook modal (summary + lazy-paged metric sheets) const [batchDetail, setBatchDetail] = useState(null); const [sheetId, setSheetId] = useState(""); const [sheetKeyword, setSheetKeyword] = useState(""); const [sheetColumn, setSheetColumn] = useState(""); const debouncedSheetKw = useDebouncedValue(sheetKeyword, 250); const [sheetPage, setSheetPage] = useState(1); const [sheetPageSize, setSheetPageSize] = useState(DEFAULT_SHEET_PAGE_SIZE); const [sheetRows, setSheetRows] = useState[]>([]); const [sheetColumns, setSheetColumns] = useState([]); const [sheetTotal, setSheetTotal] = useState(0); const [sheetLoading, setSheetLoading] = useState(false); const [rawLogOpen, setRawLogOpen] = useState(false); const [rawLogLoading, setRawLogLoading] = useState(false); const [rawLogCmd, setRawLogCmd] = useState(""); const [rawLogText, setRawLogText] = useState(""); const [rawLogMeta, setRawLogMeta] = useState(""); const refreshTasks = useCallback(async () => { const purpose = purposeFilter === "all" ? "" : purposeFilter === "portrait" ? "portrait" : "cutover_hf"; const res = await bizStateListTasks(purpose); const items = (res.items || []) as TaskRow[]; setTasks(items); return items; }, [purposeFilter]); useEffect(() => { void (async () => { try { await refreshTasks(); } catch (e) { showError(formatErr(e)); } })(); // eslint-disable-next-line react-hooks/exhaustive-deps -- refresh when purpose filter / refreshTasks changes }, [refreshTasks]); // While a collect is running, refresh batch counters so cmd/row progress is visible. useEffect(() => { if (!taskId || !detail?.collect_running) return; let cancelled = false; const tick = async () => { try { if (cancelled) return; await loadTask(taskId); } catch { /* ignore transient poll errors */ } }; const timer = window.setInterval(() => void tick(), 3000); void tick(); return () => { cancelled = true; window.clearInterval(timer); }; // eslint-disable-next-line react-hooks/exhaustive-deps -- poll while collect_running }, [taskId, detail?.collect_running]); useEffect(() => { if (!createOpen) return; let cancelled = false; setNeLoading(true); void (async () => { try { const res = await fetchCliTargets({ source: neSource, keyword: debouncedNeKw, page: nePage, pageSize: NE_PAGE_SIZE, }); if (cancelled) return; setNeItems(res.items || []); setNeTotal(Number(res.total || 0)); } catch (e) { if (!cancelled) showError(formatErr(e)); } finally { if (!cancelled) setNeLoading(false); } })(); return () => { cancelled = true; }; }, [createOpen, neSource, debouncedNeKw, nePage]); const nePages = pageCount(neTotal, NE_PAGE_SIZE); const filteredTasks = useMemo(() => { const kw = debouncedListKw.trim().toLowerCase(); if (!kw) return tasks; return tasks.filter((row) => { const blob = `${row.ne_name} ${row.ne_ip} ${row.vendor} ${row.note || ""} ${row.purpose || ""} ${row.status} ${row.source || ""} ${row.last_error}`.toLowerCase(); return blob.includes(kw); }); }, [tasks, debouncedListKw]); const collectProfiles = useMemo( () => profiles.filter((p) => (p.kind || "collect") === "collect"), [profiles], ); const sheetTabs = useMemo( () => (batchDetail ? buildSheetTabs(batchDetail, t) : []), [batchDetail, t], ); const activeSheet = useMemo(() => { if (!sheetTabs.length) return null; return sheetTabs.find((s) => s.id === sheetId) || sheetTabs[0]; }, [sheetTabs, sheetId]); const commandsSheetColumns = useMemo( () => [ { key: "raw_command", header: t("bizState.colCommand") }, { key: "metric_id", header: "metric" }, { key: "parse_status", header: t("bizState.colStatus") }, { key: "row_count", header: t("bizState.colRows") }, { key: "message", header: t("bizState.colMessage") }, { key: "_actions", header: t("bizState.colActions") }, ], [t], ); const displayColumns = useMemo(() => { if (!activeSheet) return []; if (activeSheet.id === "commands") return commandsSheetColumns; return sheetColumns.length ? sheetColumns : [{ key: "_empty", header: "—" }]; }, [activeSheet, commandsSheetColumns, sheetColumns]); const displayRows = useMemo(() => { if (!activeSheet) return []; if (activeSheet.id === "commands") { const cmds = (batchDetail?.commands || []) as Record[]; const kw = debouncedSheetKw.trim().toLowerCase(); const filtered = !kw ? cmds : cmds.filter((row) => { const keys = sheetColumn ? [sheetColumn] : ["raw_command", "metric_id", "parse_status", "row_count", "message"]; return keys.some((k) => cellText(row[k]).toLowerCase().includes(kw)); }); const start = (sheetPage - 1) * sheetPageSize; return filtered.slice(start, start + sheetPageSize).map((c) => ({ ...c })); } return sheetRows; }, [ activeSheet, batchDetail, debouncedSheetKw, sheetColumn, sheetPage, sheetPageSize, sheetRows, ]); const displayTotal = useMemo(() => { if (!activeSheet) return 0; if (activeSheet.id === "commands") { const cmds = (batchDetail?.commands || []) as Record[]; const kw = debouncedSheetKw.trim().toLowerCase(); if (!kw) return cmds.length; return cmds.filter((row) => { const keys = sheetColumn ? [sheetColumn] : ["raw_command", "metric_id", "parse_status", "row_count", "message"]; return keys.some((k) => cellText(row[k]).toLowerCase().includes(kw)); }).length; } return sheetTotal; }, [activeSheet, batchDetail, debouncedSheetKw, sheetColumn, sheetTotal]); const loadSheetPage = useCallback( async (batchId: string, metricId: string, page: number, pageSize: number, kw: string, column: string) => { if (!batchId || !metricId || metricId === "commands") return; setSheetLoading(true); try { const res = await bizStateListBatchMetricRows({ batchId, metricId, page, pageSize, kw, column, }); setSheetRows(res.items || []); setSheetTotal(Number(res.total || 0)); const cols = (res.columns || []).map((c) => ({ key: c.key, // Always show original field key; never localized display_name. header: c.key || c.header || "", })); setSheetColumns(cols.length ? cols : columnsFromRows(res.items || [])); } catch (e) { showError(formatErr(e)); setSheetRows([]); setSheetTotal(0); } finally { setSheetLoading(false); } }, [showError], ); useEffect(() => { if (!batchDetail?.id || !activeSheet || activeSheet.id === "commands") return; void loadSheetPage( String(batchDetail.id), activeSheet.id, sheetPage, sheetPageSize, debouncedSheetKw, sheetColumn, ); }, [ batchDetail?.id, activeSheet, sheetPage, sheetPageSize, debouncedSheetKw, sheetColumn, loadSheetPage, ]); const openCreate = () => { setCreateOpen(true); setSelectedNe(null); setNeKeyword(""); setNeSource("all"); setNePage(1); }; const closeCreate = () => { setCreateOpen(false); setSelectedNe(null); }; const loadTask = async (id: string) => { const task = await bizStateGetTask(id); setDetail(task); const ui = secToIntervalUi(Number(task.interval_sec || 3600)); setIntervalValue(ui.value); setIntervalUnit(ui.unit); setRetentionDays(Math.max(1, Number(task.retention_days || 30))); setDailyKeepEnabled(Boolean(task.daily_keep_enabled)); setDailyKeepCount(Math.max(1, Number(task.daily_keep_count || 10))); const b = await bizStateListBatches(id, 200); setBatches((b.items || []) as BatchRow[]); setSelectedBatchIds([]); const p = await bizStateListProfiles({ vendor: task.vendor || "", device_type: task.device_type || "", }); setProfiles((p.items || []) as Profile[]); }; const openTask = async (id: string, tab: TaskTab = "profiles") => { setTaskId(id); setTaskTab(tab); setBindItemId(""); setCandidates([]); setBatchDetail(null); try { await loadTask(id); } catch (e) { showError(formatErr(e)); setTaskId(""); } }; const closeTask = () => { setTaskId(""); setDetail(null); setBindItemId(""); setCandidates([]); }; const createTask = async () => { if (!selectedNe) return; const source = neSourceOf(selectedNe); setBusy(true); try { const task = await bizStateCreateTask({ source, ne_id: selectedNe.id, ne_name: selectedNe.name, ne_ip: selectedNe.ip_address, vendor: selectedNe.vendor || "", device_type: selectedNe.device_type || "", }); showOk(t("bizState.created")); closeCreate(); await refreshTasks(); await openTask(String(task.id), "profiles"); } catch (e) { showError(t("bizState.createFailed") + ": " + formatErr(e)); } finally { setBusy(false); } }; const setScheduleEnabled = async (id: string, enabled: boolean) => { setBusy(true); try { if (enabled) { await bizStateStartTask(id); } else { await bizStatePauseTask(id); } showOk(enabled ? t("bizState.started") : t("bizState.paused")); if (taskId === id) await loadTask(id); await refreshTasks(); } catch (e) { showError(formatErr(e)); } finally { setBusy(false); } }; const saveSchedule = async () => { if (!taskId) return; setBusy(true); try { await bizStatePatchTask(taskId, { interval_sec: intervalUiToSec(intervalValue, intervalUnit), retention_days: Math.max(1, Number(retentionDays) || 30), daily_keep_enabled: dailyKeepEnabled, daily_keep_count: Math.max(1, Number(dailyKeepCount) || 10), }); showOk(t("bizState.scheduleSaved")); await loadTask(taskId); await refreshTasks(); } catch (e) { showError(formatErr(e)); } finally { setBusy(false); } }; const toggleBatchSelect = (id: string, protectedBatch: boolean) => { if (protectedBatch) return; setSelectedBatchIds((prev) => prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id], ); }; const selectDeletableBatches = () => { setSelectedBatchIds(batches.filter((b) => !b.protected).map((b) => b.id)); }; const markBaseline = async (batchId: string, marked: boolean) => { setBusy(true); try { await bizStateSetBatchBaseline(batchId, marked); if (taskId) await loadTask(taskId); } catch (e) { showError(formatErr(e)); } finally { setBusy(false); } }; const removeBatch = async (batchId: string) => { if (!window.confirm(t("bizState.confirmDeleteBatch"))) return; setBusy(true); try { await bizStateDeleteBatch(batchId); showOk(t("bizState.batchDeleted")); if (taskId) await loadTask(taskId); } catch (e) { showError(formatErr(e)); } finally { setBusy(false); } }; const bulkRemoveBatches = async () => { if (!selectedBatchIds.length) return; if ( !window.confirm( t("bizState.confirmBulkDelete").replace("{{count}}", String(selectedBatchIds.length)), ) ) { return; } setBusy(true); try { const res = await bizStateBulkDeleteBatches(selectedBatchIds); showOk( t("bizState.bulkDeleted") .replace("{{deleted}}", String(res.deleted_count || 0)) .replace("{{skipped}}", String((res.skipped || []).length)), ); if (taskId) await loadTask(taskId); } catch (e) { showError(formatErr(e)); } finally { setBusy(false); } }; const purgeNow = async () => { if (!taskId) return; setBusy(true); try { const res = await bizStatePurgeTask(taskId); showOk(t("bizState.purgeOk").replace("{{dropped}}", String(res.dropped || 0))); await loadTask(taskId); } catch (e) { showError(formatErr(e)); } finally { setBusy(false); } }; const setTaskCollecting = (id: string, on: boolean) => { setCollectingIds((prev) => { if (on) { if (prev[id]) return prev; return { ...prev, [id]: true }; } if (!prev[id]) return prev; const next = { ...prev }; delete next[id]; return next; }); }; const collectNowForTask = async (id: string, fromModal = false) => { setTaskCollecting(id, true); try { await bizStateCollectNow(id); showOk(t("bizState.collecting")); if (fromModal && taskId === id) { setTaskTab("batches"); } // Heavy show-interface can take ~20 minutes; poll long enough and refresh batches. const deadline = Date.now() + 32 * 60 * 1000; while (Date.now() < deadline) { await new Promise((r) => setTimeout(r, 3000)); if (fromModal && taskId === id) { try { await loadTask(id); const task = await bizStateGetTask(id); setDetail(task); if (!task.collect_running) break; } catch { break; } continue; } const items = await refreshTasks(); const latest = items.find((x) => x.id === id); if (!latest?.collect_running) break; } await refreshTasks(); if (fromModal && taskId === id) { await loadTask(id); } } catch (e) { showError(formatErr(e)); } finally { setTaskCollecting(id, false); } }; const collectNow = async () => { if (!taskId) return; await collectNowForTask(taskId, true); }; const removeTask = async (id: string) => { if (!window.confirm(t("bizState.confirmDelete"))) return; setBusy(true); try { await bizStateDeleteTask(id); showOk(t("bizState.deleted")); if (taskId === id) closeTask(); await refreshTasks(); } catch (e) { showError(formatErr(e)); } finally { setBusy(false); } }; const toggleProfileItem = async (profile: Profile, enable: boolean) => { if (!taskId || !detail) return; const items = [...(detail.items || [])]; const idx = items.findIndex((it: any) => it.source_profile_id === profile.profile_id); if (enable) { if (idx >= 0) items[idx] = { ...items[idx], enabled: true }; else { items.push({ source_profile_id: profile.profile_id, kind: "catalog", enabled: true, title: profile.title, bindings: [], }); } } else if (idx >= 0) { items[idx] = { ...items[idx], enabled: false }; } setBusy(true); try { await bizStatePatchTask(taskId, { items }); await loadTask(taskId); } catch (e) { showError(formatErr(e)); } finally { setBusy(false); } }; const startDiscover = async (item: any) => { if (!taskId) return; const prof = profiles.find((p) => p.profile_id === item.source_profile_id); const ph = (prof?.placeholders || [])[0]; if (!ph) { showError(t("bizState.noNeedBind")); return; } setBusy(true); setBindItemId(item.id); try { const res = await bizStateDiscover({ task_id: taskId, collect_profile_id: item.source_profile_id, placeholder: ph.name, }); if (!res.ok) { showError(res.error || t("bizState.discoverFailed")); setCandidates([]); return; } setDiscoverCmd(res.command || ""); const cand = (res.candidates || []) as Candidate[]; setCandidates(cand); const existing = (item.bindings || []) .filter((b: any) => b.placeholder === ph.name) .map((b: any) => String(b.value)); setSelectedVrfs(existing.length ? existing : cand.map((c) => c.value)); } catch (e) { showError(formatErr(e)); } finally { setBusy(false); } }; const saveBindings = async (values?: string[]) => { if (!taskId || !bindItemId) return; const item = (detail?.items || []).find((it: any) => it.id === bindItemId); const prof = profiles.find((p) => p.profile_id === item?.source_profile_id); const phName = (prof?.placeholders || [])[0]?.name || "vrf"; const bindOptional = (prof?.placeholders || []).every((ph) => ph.required === false); const picked = values !== undefined ? values : selectedVrfs; if (!picked.length && !bindOptional) return; setBusy(true); try { await bizStateSetBindings( taskId, bindItemId, picked.map((v) => ({ placeholder: phName, value: v })), ); showOk(t("bizState.bindingsSaved")); await loadTask(taskId); setBindItemId(""); setCandidates([]); } catch (e) { showError(formatErr(e)); } finally { setBusy(false); } }; const openBatch = async (batchId: string) => { try { const d = await bizStateGetBatch(batchId); setBatchDetail(d); setSheetKeyword(""); setSheetColumn(""); setSheetPage(1); setSheetRows([]); setSheetColumns([]); setSheetTotal(0); const built = buildSheetTabs(d, t); const prefer = built.find((s) => s.id !== "commands" && s.rowCount > 0) || built.find((s) => s.id !== "commands") || built[0]; setSheetId(prefer?.id || ""); } catch (e) { showError(formatErr(e)); } }; const closeBatch = () => { setBatchDetail(null); setSheetId(""); setSheetKeyword(""); setSheetColumn(""); setSheetPage(1); setSheetRows([]); setSheetColumns([]); setSheetTotal(0); setRawLogOpen(false); setRawLogText(""); }; const openRawLog = async (commandId: string) => { if (!batchDetail?.id || !commandId) return; setRawLogOpen(true); setRawLogLoading(true); setRawLogText(""); setRawLogCmd(""); setRawLogMeta(""); try { const d = await bizStateGetBatchCommand(String(batchDetail.id), commandId); setRawLogCmd(String(d.raw_command || "")); setRawLogText(String(d.raw_text || "")); const bits = [ d.parse_status, d.metric_id, d.row_count != null ? `${d.row_count} rows` : "", d.collected_at ? fmtTime(d.collected_at) : "", ].filter(Boolean); setRawLogMeta(bits.join(" · ")); } catch (e) { showError(formatErr(e)); setRawLogOpen(false); } finally { setRawLogLoading(false); } }; const selectSheet = (id: string) => { setSheetId(id); setSheetKeyword(""); setSheetColumn(""); setSheetPage(1); setSheetRows([]); setSheetColumns([]); setSheetTotal(0); }; const runningCount = tasks.filter((x) => x.status === "running").length; return (

{t("bizState.title")}

{t("bizState.colNe")}
{tasks.length}
{t("bizState.scheduleOn")}
{runningCount}
setListKeyword(e.target.value)} /> setPurposeFilter(e.target.value as "all" | "portrait" | "cutover_hf") } aria-label={t("bizState.purposeFilter")} >
{filteredTasks.map((row) => ( ))} {!filteredTasks.length ? ( ) : null}
{t("bizState.colNe")} {t("bizState.colSource")} {t("bizState.colPurpose")} {t("bizState.colInterval")} {t("bizState.scheduleEnabled")} {t("bizState.colStatus")} {t("bizState.colLast")} {t("bizState.colActions")}
{row.ne_name || row.ne_ip || "—"}
{row.vendor || "—"} · {row.ne_ip || "—"} {row.note ? ` · ${row.note}` : ""}
{row.last_error ?
{row.last_error}
: null}
{row.source || "managed"} {row.purpose === "cutover_hf" ? t("bizState.purposeCutoverHf") : t("bizState.purposePortrait")} {fmtIntervalLabel( Number(row.interval_sec || 3600), t("bizState.intervalUnitDays"), t("bizState.intervalUnitHours"), t("bizState.intervalUnitSeconds"), )}
{row.collect_running ? ( {t("bizState.collecting")} ) : row.status === "running" ? ( {t("bizState.scheduleOn")} ) : row.status === "paused" ? ( {t("bizState.statusPaused")} ) : ( {t("bizState.scheduleOff")} )}
{fmtTime(row.last_collect_ended_at)}
{row.status === "running" ? ( ) : ( )}
{t("bizState.empty")}
{/* Create task: pick any CLI target with search */} {t("bizState.create")}

{t("bizState.createHint")}

{ setNeKeyword(e.target.value); setNePage(1); }} /> { setNeSource(e.target.value as NeSourceFilter); setNePage(1); }} aria-label={t("bizState.colSource")} > {selectedNe ? ( {t("bizState.selectedNe")}: {selectedNe.name} ({selectedNe.ip_address}) · {neSourceOf(selectedNe)} ) : null}
{neItems.map((row) => { const checked = selectedNe?.id === row.id && neSourceOf(selectedNe) === neSourceOf(row); return ( ); })} {!neItems.length ? ( ) : null}
{t("bizState.colSource")} {t("bizState.colNe")} IP {t("bizState.colVendor")} {t("bizState.colConnect")}
setSelectedNe(row)} /> {row.source} {row.name || "—"} {row.ip_address || "—"} {row.vendor || "—"} {row.connect_status || "—"}
{neLoading ? t("common.refreshing") : t("bizState.neEmpty")}
{/* Task detail modal */} {detail?.ne_name || detail?.ne_ip || t("bizState.detail")} {detail ? ` · ${detail.status}` : ""} {detail ? (
{detail.status === "running" ? ( ) : ( )} {dailyKeepEnabled ? ( ) : null} {detail.vendor || "—"} · {detail.ne_ip || "—"} · {t("bizState.retentionHint")} {dailyKeepEnabled ? ` · ${t("bizState.dailyKeepHint")}` : ""}
) : null} {detail?.last_error ?

{detail.last_error}

: null}
{taskTab === "profiles" ? ( <>
{collectProfiles.map((prof) => { const it = (detail?.items || []).find( (x: any) => x.source_profile_id === prof.profile_id, ); const enabled = Boolean(it?.enabled); const binds = it?.bindings || []; const needsBind = (prof.placeholders || []).length > 0; const bindOptional = (prof.placeholders || []).every( (ph) => ph.required === false, ); return ( ); })}
{t("bizState.enable")} {t("bizState.profiles")} {t("bizState.command")} {t("bizState.params")}
void toggleProfileItem(prof, e.target.checked)} />
{prof.title}
{prof.description ?
{prof.description}
: null}
{prof.command_template} {needsBind ? binds.length ? binds.map((b: any) => b.value).join(", ") : bindOptional ? t("bizState.allVrfsDefault") : t("bizState.unbound") : "—"} {needsBind && enabled && it ? ( ) : null}
{bindItemId && candidates.length ? (

{t("bizState.bindTitle")}

{discoverCmd} · {selectedVrfs.length}

{candidates.map((c) => ( ))}
) : null} ) : (
{batches.map((b) => { const locked = Boolean(b.protected); return ( ); })} {!batches.length ? ( ) : null}
{t("bizState.colTime")} {t("bizState.colStatus")} {t("bizState.colProtect")} {t("bizState.colRows")} {t("bizState.colActions")}
toggleBatchSelect(b.id, locked)} /> {fmtTime(b.started_at)} {b.status} {b.is_baseline ? ( {t("bizState.baseline")} ) : locked ? ( {t("bizState.protected")} ) : ( "—" )} {b.row_count} / {b.command_count} cmd
{b.is_baseline ? ( ) : ( )}
{t("bizState.noBatches")}
)}
{/* Batch workbook: summary + lazy-paged metric sheets */} {t("bizState.batchWorkbook")} {batchDetail ? (
{String(batchDetail.status || "—")} {batchDetail.command_count} cmd · {batchDetail.row_count} rows ·{" "} {fmtTime(batchDetail.started_at)}
{t("bizState.batchId")} {String(batchDetail.id || "")}
) : null}
{sheetTabs.map((s) => ( ))} {!sheetTabs.length ? {t("bizState.sheetEmpty")} : null}
{activeSheet ? ( <> {activeSheet.id !== "commands" && activeSheet.commands.length ? (
{t("bizState.collectCommand")}
{activeSheet.commands.map((c) => (
{c.raw_command || "—"}
{c.parse_status ? ( {c.parse_status} ) : null}
))}
) : null}
{ setSheetColumn(e.target.value); setSheetPage(1); }} aria-label={t("bizState.filterColumn")} > {displayColumns .filter((c) => c.key !== "_actions" && c.key !== "_empty") .map((c) => ( ))} { setSheetKeyword(e.target.value); setSheetPage(1); }} /> {sheetLoading ? t("bizState.sheetLoading") : `${displayTotal} ${t("bizState.colRows")}`}
{displayColumns.map((c) => ( ))} {displayRows.map((row, i) => ( {displayColumns.map((c) => ( ))} ))} {!displayRows.length && !sheetLoading ? ( ) : null} {sheetLoading && activeSheet.id !== "commands" && !displayRows.length ? ( ) : null}
{c.header}
{c.key === "_actions" ? ( ) : c.key === "parse_status" ? ( {cellText(row[c.key]) || "—"} ) : c.key === "raw_command" ? ( {cellText(row[c.key]) || "—"} ) : ( cellText(row[c.key]) || "—" )}
{t("bizState.sheetEmpty")}
{t("bizState.sheetLoading")}
{ setSheetPageSize(n); setSheetPage(1); }} /> ) : (
{t("bizState.sheetEmpty")}
)}
{batchDetail ? ( ) : null}
{/* Raw CLI log viewer */} setRawLogOpen(false)} size="lg" className="app-heroui-modal--lg bs-rawlog-modal" > {t("bizState.rawLogTitle")} {rawLogCmd ? {rawLogCmd} : null} {rawLogMeta ?

{rawLogMeta}

: null} {rawLogLoading ? (
{t("bizState.sheetLoading")}
) : (
{rawLogText || t("bizState.rawLogEmpty")}
)}
); }