mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 00:43:17 +08:00
UME: renew token when expires_in is 0; token UI loading + toasts; sync thread status hint
- keepalive: call renew when has_token and (expires_in<=0 or below renew_before) so 0s stuck state self-heals\n- alarms/inventory auto loops: set last_error while pulling so table is not stuck at init with no hint\n- UmePage: warn pills for connected+需续期, spinners on token actions, toast ok/err from App, invalidate sync status after token ops Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
def534e0a6
commit
0afc77c06b
5 changed files with 126 additions and 22 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -335,7 +335,7 @@ function App() {
|
|||
/>
|
||||
}
|
||||
/>
|
||||
<Route path="/ume" element={<UmePage />} />
|
||||
<Route path="/ume" element={<UmePage toastOk={showOk} toastError={showError} />} />
|
||||
</Routes>
|
||||
{toast && <div className={`toast toast--${toast.type}`}>{toast.text}</div>}
|
||||
</AppLayout>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<article className="card card--full">
|
||||
<h3>UME Token 状态</h3>
|
||||
<div className="actions-row actions-row--inline">
|
||||
<span className={`conn-pill conn-pill--${tokenStatusQuery.data?.has_token ? "up" : "down"}`}>
|
||||
token: {tokenStatusQuery.data?.has_token ? "connected" : "disconnected"}
|
||||
<span
|
||||
className={`conn-pill conn-pill--${!hasToken ? "down" : tokenNeedsRenewal ? "warn" : "up"}`}
|
||||
title={
|
||||
tokenNeedsRenewal
|
||||
? "库中 token 缺少有效过期时间或已过期,请点「手动续期/登录」或等待后台自动续期"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
token: {!hasToken ? "disconnected" : tokenNeedsRenewal ? "connected (需续期)" : "connected"}
|
||||
</span>
|
||||
<span className={`conn-pill conn-pill--${tokenLevel}`}>
|
||||
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`
|
||||
: "-"}
|
||||
</span>
|
||||
{tokenStatusQuery.data?.token_preview ? <span className="conn-pill">preview: {tokenStatusQuery.data.token_preview}</span> : null}
|
||||
</div>
|
||||
|
|
@ -144,13 +184,42 @@ export function UmePage() {
|
|||
onClick={() => queryClient.invalidateQueries({ queryKey: ["umeTokenStatus"] })}
|
||||
disabled={tokenStatusQuery.isFetching}
|
||||
>
|
||||
刷新状态
|
||||
{tokenStatusQuery.isFetching ? (
|
||||
<>
|
||||
<span className="inline-spinner" aria-hidden />
|
||||
刷新中…
|
||||
</>
|
||||
) : (
|
||||
"刷新状态"
|
||||
)}
|
||||
</button>
|
||||
<button onClick={() => tokenRefreshMutation.mutate()} disabled={tokenRefreshMutation.isPending}>
|
||||
手动续期/登录
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => tokenRefreshMutation.mutate()}
|
||||
disabled={tokenRefreshMutation.isPending || tokenDisconnectMutation.isPending}
|
||||
>
|
||||
{tokenRefreshMutation.isPending ? (
|
||||
<>
|
||||
<span className="inline-spinner" aria-hidden />
|
||||
续期中…
|
||||
</>
|
||||
) : (
|
||||
"手动续期/登录"
|
||||
)}
|
||||
</button>
|
||||
<button onClick={() => tokenDisconnectMutation.mutate()} disabled={tokenDisconnectMutation.isPending}>
|
||||
断开 token
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => tokenDisconnectMutation.mutate()}
|
||||
disabled={tokenRefreshMutation.isPending || tokenDisconnectMutation.isPending}
|
||||
>
|
||||
{tokenDisconnectMutation.isPending ? (
|
||||
<>
|
||||
<span className="inline-spinner" aria-hidden />
|
||||
断开中…
|
||||
</>
|
||||
) : (
|
||||
"断开 token"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{(tokenOpError || tokenRefreshMutation.error || tokenDisconnectMutation.error) && (
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue