diff --git a/netx_api/main.py b/netx_api/main.py index d5b9e7a..f7bd7cd 100644 --- a/netx_api/main.py +++ b/netx_api/main.py @@ -423,7 +423,8 @@ def on_startup() -> None: client = _ume_client() st = client.token_status() expires_in = int(st.get("expires_in_s") or 0) - if bool(st.get("has_token")) and expires_in > 0 and expires_in < renew_before_s: + # Renew when missing/invalid TTL (0) or nearing expiry — previously 0 skipped renew forever. + if bool(st.get("has_token")) and (expires_in <= 0 or expires_in < renew_before_s): client.renew_token() _set_runtime_task("token_keepalive", status="running", last_run_at=datetime.now(timezone.utc), last_error="") except Exception: @@ -445,6 +446,11 @@ def on_startup() -> None: if _runtime_is_paused("alarms_current_auto_sync"): time.sleep(1) continue + _set_runtime_task( + "alarms_current_auto_sync", + status="running", + last_error="正在拉取 UME 当前告警…", + ) db = SessionLocal() try: client = _ume_client() @@ -482,6 +488,11 @@ def on_startup() -> None: if _runtime_is_paused("inventory_auto_sync"): time.sleep(1) continue + _set_runtime_task( + "inventory_auto_sync", + status="running", + last_error="正在拉取 UME 网元清单…", + ) db = SessionLocal() try: client = _ume_client() diff --git a/web/src/App.tsx b/web/src/App.tsx index 4d037ee..ce343b9 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -335,7 +335,7 @@ function App() { /> } /> - } /> + } /> {toast &&
{toast.text}
} diff --git a/web/src/index.css b/web/src/index.css index b3864bc..61dd515 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -268,6 +268,29 @@ pre { border-color: #5f1f2a; } +.inline-spinner { + display: inline-block; + width: 14px; + height: 14px; + margin-right: 8px; + border: 2px solid rgba(255, 255, 255, 0.22); + border-top-color: rgba(255, 255, 255, 0.85); + border-radius: 50%; + vertical-align: -2px; + animation: inline-spin 0.65s linear infinite; +} + +@keyframes inline-spin { + to { + transform: rotate(360deg); + } +} + +.conn-pill--warn { + border-color: #664813; + color: #e7c46d; +} + .skeleton-line { height: 32px; border-radius: 8px; diff --git a/web/src/pages/UmePage.tsx b/web/src/pages/UmePage.tsx index cf08593..3899a3c 100644 --- a/web/src/pages/UmePage.tsx +++ b/web/src/pages/UmePage.tsx @@ -11,7 +11,13 @@ import { } from "../services/api"; import { formatSystemTime } from "../utils/time"; -export function UmePage() { +export type UmePageProps = { + /** Optional toasts from App shell (fixed bottom-right). */ + toastOk?: (message: string) => void; + toastError?: (message: string) => void; +}; + +export function UmePage({ toastOk, toastError }: UmePageProps) { const queryClient = useQueryClient(); const [tokenOpError, setTokenOpError] = useState(""); const [runtimeTaskError, setRuntimeTaskError] = useState(""); @@ -52,41 +58,63 @@ export function UmePage() { }); const tokenRefreshMutation = useMutation({ mutationFn: refreshUmeToken, + onMutate: () => { + setTokenOpError(""); + }, onSuccess: async (res) => { if (!res?.ok) { - setTokenOpError(String(res.error || res.error_kind || "token_refresh_failed")); + const msg = String(res.error || res.error_kind || "token_refresh_failed"); + setTokenOpError(msg); + toastError?.(msg); } else { setTokenOpError(""); + toastOk?.(res.changed ? "续期成功,token 已更新" : "续期完成(与此前一致)"); } await queryClient.invalidateQueries({ queryKey: ["umeTokenStatus"] }); + await queryClient.invalidateQueries({ queryKey: ["umeSyncStatus"] }); }, onError: (err) => { - setTokenOpError(String(err)); + const msg = String(err); + setTokenOpError(msg); + toastError?.(msg); }, }); const tokenDisconnectMutation = useMutation({ mutationFn: disconnectUmeToken, + onMutate: () => { + setTokenOpError(""); + }, onSuccess: async (res) => { if (!res?.ok) { - setTokenOpError(String(res.error || res.error_kind || "token_disconnect_failed")); + const msg = String(res.error || res.error_kind || "token_disconnect_failed"); + setTokenOpError(msg); + toastError?.(msg); } else { setTokenOpError(""); + toastOk?.("已断开 UME token"); } await queryClient.invalidateQueries({ queryKey: ["umeTokenStatus"] }); + await queryClient.invalidateQueries({ queryKey: ["umeSyncStatus"] }); }, onError: (err) => { - setTokenOpError(String(err)); + const msg = String(err); + setTokenOpError(msg); + toastError?.(msg); }, }); const tokenExpiresIn = Number(tokenStatusQuery.data?.expires_in_s || 0); - const tokenLevel = !tokenStatusQuery.data?.has_token + const hasToken = Boolean(tokenStatusQuery.data?.has_token); + const tokenNeedsRenewal = hasToken && tokenExpiresIn <= 0; + const tokenLevel = !hasToken ? "down" - : tokenExpiresIn < 15 - ? "down" - : tokenExpiresIn < 60 - ? "unknown" - : "up"; + : tokenNeedsRenewal + ? "warn" + : tokenExpiresIn < 15 + ? "down" + : tokenExpiresIn < 60 + ? "unknown" + : "up"; const neQuery = useQuery({ queryKey: ["umeNE", neKeyword, nePage, nePageSize], queryFn: () => fetchUmeNe({ keyword: neKeyword, page: nePage, pageSize: nePageSize }), @@ -131,11 +159,23 @@ export function UmePage() {

UME Token 状态

- - token: {tokenStatusQuery.data?.has_token ? "connected" : "disconnected"} + + token: {!hasToken ? "disconnected" : tokenNeedsRenewal ? "connected (需续期)" : "connected"} - expires_in: {typeof tokenStatusQuery.data?.expires_in_s === "number" ? `${tokenStatusQuery.data.expires_in_s}s` : "-"} + expires_in:{" "} + {typeof tokenStatusQuery.data?.expires_in_s === "number" + ? tokenNeedsRenewal + ? "需续期 / 0s" + : `${tokenStatusQuery.data.expires_in_s}s` + : "-"} {tokenStatusQuery.data?.token_preview ? preview: {tokenStatusQuery.data.token_preview} : null}
@@ -144,13 +184,42 @@ export function UmePage() { onClick={() => queryClient.invalidateQueries({ queryKey: ["umeTokenStatus"] })} disabled={tokenStatusQuery.isFetching} > - 刷新状态 + {tokenStatusQuery.isFetching ? ( + <> + + 刷新中… + + ) : ( + "刷新状态" + )} - - {(tokenOpError || tokenRefreshMutation.error || tokenDisconnectMutation.error) && ( diff --git a/web/src/types.ts b/web/src/types.ts index 152680f..ef01a10 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -148,12 +148,13 @@ export type UmeAlarmItem = { }; export type UmeTokenStatus = { - ok: boolean; + ok?: boolean; has_token: boolean; expires_in_s: number; expires_at_epoch_s: number; auth_header: string; token_preview?: string; + changed?: boolean; error_kind?: string; error?: string; };