Add LLDP collect retry for failed and weak-evidence NEs.

Mirror config-sync retry_failed so operators can re-collect hard failures and empty/stub LLDP results without re-running the full scope.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-08-11 11:25:57 +08:00
parent 527472976a
commit dcdf4147a6
10 changed files with 423 additions and 23 deletions

View file

@ -319,6 +319,7 @@ const en = {
lldpLinks: {
title: "LLDP links",
collectNow: "Collect now",
retryFailed: "Retry failed",
started: "LLDP collect started",
pause: "Pause",
resume: "Resume",
@ -396,6 +397,7 @@ const en = {
manual: "Manual",
schedule: "Scheduled",
topology: "Topology canvas",
retry_failed: "Retry failed",
},
jobScope: {
all: "All NEs",

View file

@ -316,6 +316,7 @@ const zh = {
lldpLinks: {
title: "LLDP 链路",
collectNow: "立即采集",
retryFailed: "一键重试失败",
started: "已启动 LLDP 采集",
pause: "暂停",
resume: "继续",
@ -392,6 +393,7 @@ const zh = {
manual: "手动",
schedule: "周期",
topology: "拓扑画布",
retry_failed: "重试失败",
},
jobScope: {
all: "全部网元",

View file

@ -48,11 +48,23 @@ function lldpScopeLabel(t: (k: string) => string, scope: string): string {
function lldpItemResultLabel(
t: (k: string) => string,
it: { ok?: boolean; parser_stub?: boolean | string | null; unmatched_count?: number; unmatched?: unknown[] },
it: {
ok?: boolean;
parser_stub?: boolean | string | null;
unmatched_count?: number;
unmatched?: unknown[];
error?: string | null;
},
): string {
if (!it.ok) return t("lldpLinks.itemResult.fail");
const err = String(it.error || "").trim();
const weak =
Boolean(it.parser_stub) ||
err === "parser_stub" ||
err === "empty_cli_output" ||
err === "vendor_or_device_type_required";
if (!it.ok || weak) return t("lldpLinks.itemResult.fail");
const unmatchedCount = it.unmatched_count ?? (it.unmatched?.length || 0);
if (it.parser_stub || unmatchedCount > 0) return t("lldpLinks.itemResult.warn");
if (unmatchedCount > 0) return t("lldpLinks.itemResult.warn");
return t("lldpLinks.itemResult.ok");
}
@ -250,7 +262,7 @@ export function LldpLinksPage() {
});
const startMut = useMutation({
mutationFn: () => startLldpCollect(),
mutationFn: (mode: "full" | "retry_failed" = "full") => startLldpCollect({ mode }),
onSuccess: async () => {
showOk(t("lldpLinks.started"));
await refresh();
@ -322,10 +334,17 @@ export function LldpLinksPage() {
type="button"
className="btn-primary"
disabled={Boolean(running) || startMut.isPending}
onClick={() => startMut.mutate()}
onClick={() => startMut.mutate("full")}
>
{t("lldpLinks.collectNow")}
</button>
<button
type="button"
disabled={Boolean(running) || startMut.isPending || !(last && (last.fail_count || 0) > 0)}
onClick={() => startMut.mutate("retry_failed")}
>
{t("lldpLinks.retryFailed")}
</button>
{running?.status === "running" || running?.status === "pending" ? (
<button type="button" onClick={() => pauseMut.mutate(running.id)} disabled={pauseMut.isPending}>
{t("lldpLinks.pause")}
@ -379,7 +398,7 @@ export function LldpLinksPage() {
<div className="pt-list-kpi__label">{t("lldpLinks.kpi.last")}</div>
<div className="pt-list-kpi__value" style={{ fontSize: 15 }}>
{last
? `${last.status} · +${last.edges_added} / ~${last.edges_updated}`
? `${last.status} · ok ${last.success_count ?? "—"} / fail ${last.fail_count ?? 0}`
: t("common.empty")}
</div>
</div>
@ -697,6 +716,11 @@ export function LldpLinksPage() {
</td>
<td>
{job.done}/{job.total}
{(job.fail_count || 0) > 0 ? (
<div className="muted">
ok {job.success_count ?? 0} / fail {job.fail_count}
</div>
) : null}
</td>
<td>
+{job.edges_added} / ~{job.edges_updated}

View file

@ -1384,8 +1384,11 @@ export const fetchLldpCollectDashboard = () =>
export const updateLldpCollectPolicy = (body: Partial<LldpCollectPolicy>) =>
apiPut<LldpCollectPolicy>("/v1/topology/lldp-collect/policy", body);
export const startLldpCollect = () =>
apiPost<{ ok: boolean; job: TopologyDiscoverJob }>("/v1/topology/lldp-collect/start", {});
export const startLldpCollect = (body?: { mode?: "full" | "retry_failed"; job_id?: string }) =>
apiPost<{ ok: boolean; job: TopologyDiscoverJob }>("/v1/topology/lldp-collect/start", {
mode: body?.mode || "full",
...(body?.job_id ? { job_id: body.job_id } : {}),
});
export const pauseLldpCollectJob = (jobId: string) =>
apiPost<TopologyDiscoverJob>(`/v1/topology/lldp-collect/jobs/${encodeURIComponent(jobId)}/pause`, {});

View file

@ -715,6 +715,8 @@ export type LldpCollectJobSummary = {
status: string;
total: number;
done: number;
success_count?: number;
fail_count?: number;
edges_added: number;
edges_updated: number;
edges_stale: number;