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 ? (
+ <>
+
+ 刷新中…
+ >
+ ) : (
+ "刷新状态"
+ )}
-