mirror of
https://github.com/hansjone/netx.git
synced 2026-10-18 12:40:44 +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()
|
client = _ume_client()
|
||||||
st = client.token_status()
|
st = client.token_status()
|
||||||
expires_in = int(st.get("expires_in_s") or 0)
|
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()
|
client.renew_token()
|
||||||
_set_runtime_task("token_keepalive", status="running", last_run_at=datetime.now(timezone.utc), last_error="")
|
_set_runtime_task("token_keepalive", status="running", last_run_at=datetime.now(timezone.utc), last_error="")
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
@ -445,6 +446,11 @@ def on_startup() -> None:
|
||||||
if _runtime_is_paused("alarms_current_auto_sync"):
|
if _runtime_is_paused("alarms_current_auto_sync"):
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
continue
|
continue
|
||||||
|
_set_runtime_task(
|
||||||
|
"alarms_current_auto_sync",
|
||||||
|
status="running",
|
||||||
|
last_error="正在拉取 UME 当前告警…",
|
||||||
|
)
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
try:
|
try:
|
||||||
client = _ume_client()
|
client = _ume_client()
|
||||||
|
|
@ -482,6 +488,11 @@ def on_startup() -> None:
|
||||||
if _runtime_is_paused("inventory_auto_sync"):
|
if _runtime_is_paused("inventory_auto_sync"):
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
continue
|
continue
|
||||||
|
_set_runtime_task(
|
||||||
|
"inventory_auto_sync",
|
||||||
|
status="running",
|
||||||
|
last_error="正在拉取 UME 网元清单…",
|
||||||
|
)
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
try:
|
try:
|
||||||
client = _ume_client()
|
client = _ume_client()
|
||||||
|
|
|
||||||
|
|
@ -335,7 +335,7 @@ function App() {
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route path="/ume" element={<UmePage />} />
|
<Route path="/ume" element={<UmePage toastOk={showOk} toastError={showError} />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
{toast && <div className={`toast toast--${toast.type}`}>{toast.text}</div>}
|
{toast && <div className={`toast toast--${toast.type}`}>{toast.text}</div>}
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
|
|
|
||||||
|
|
@ -268,6 +268,29 @@ pre {
|
||||||
border-color: #5f1f2a;
|
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 {
|
.skeleton-line {
|
||||||
height: 32px;
|
height: 32px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,13 @@ import {
|
||||||
} from "../services/api";
|
} from "../services/api";
|
||||||
import { formatSystemTime } from "../utils/time";
|
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 queryClient = useQueryClient();
|
||||||
const [tokenOpError, setTokenOpError] = useState("");
|
const [tokenOpError, setTokenOpError] = useState("");
|
||||||
const [runtimeTaskError, setRuntimeTaskError] = useState("");
|
const [runtimeTaskError, setRuntimeTaskError] = useState("");
|
||||||
|
|
@ -52,36 +58,58 @@ export function UmePage() {
|
||||||
});
|
});
|
||||||
const tokenRefreshMutation = useMutation({
|
const tokenRefreshMutation = useMutation({
|
||||||
mutationFn: refreshUmeToken,
|
mutationFn: refreshUmeToken,
|
||||||
|
onMutate: () => {
|
||||||
|
setTokenOpError("");
|
||||||
|
},
|
||||||
onSuccess: async (res) => {
|
onSuccess: async (res) => {
|
||||||
if (!res?.ok) {
|
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 {
|
} else {
|
||||||
setTokenOpError("");
|
setTokenOpError("");
|
||||||
|
toastOk?.(res.changed ? "续期成功,token 已更新" : "续期完成(与此前一致)");
|
||||||
}
|
}
|
||||||
await queryClient.invalidateQueries({ queryKey: ["umeTokenStatus"] });
|
await queryClient.invalidateQueries({ queryKey: ["umeTokenStatus"] });
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["umeSyncStatus"] });
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
setTokenOpError(String(err));
|
const msg = String(err);
|
||||||
|
setTokenOpError(msg);
|
||||||
|
toastError?.(msg);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const tokenDisconnectMutation = useMutation({
|
const tokenDisconnectMutation = useMutation({
|
||||||
mutationFn: disconnectUmeToken,
|
mutationFn: disconnectUmeToken,
|
||||||
|
onMutate: () => {
|
||||||
|
setTokenOpError("");
|
||||||
|
},
|
||||||
onSuccess: async (res) => {
|
onSuccess: async (res) => {
|
||||||
if (!res?.ok) {
|
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 {
|
} else {
|
||||||
setTokenOpError("");
|
setTokenOpError("");
|
||||||
|
toastOk?.("已断开 UME token");
|
||||||
}
|
}
|
||||||
await queryClient.invalidateQueries({ queryKey: ["umeTokenStatus"] });
|
await queryClient.invalidateQueries({ queryKey: ["umeTokenStatus"] });
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["umeSyncStatus"] });
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
setTokenOpError(String(err));
|
const msg = String(err);
|
||||||
|
setTokenOpError(msg);
|
||||||
|
toastError?.(msg);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const tokenExpiresIn = Number(tokenStatusQuery.data?.expires_in_s || 0);
|
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"
|
? "down"
|
||||||
|
: tokenNeedsRenewal
|
||||||
|
? "warn"
|
||||||
: tokenExpiresIn < 15
|
: tokenExpiresIn < 15
|
||||||
? "down"
|
? "down"
|
||||||
: tokenExpiresIn < 60
|
: tokenExpiresIn < 60
|
||||||
|
|
@ -131,11 +159,23 @@ export function UmePage() {
|
||||||
<article className="card card--full">
|
<article className="card card--full">
|
||||||
<h3>UME Token 状态</h3>
|
<h3>UME Token 状态</h3>
|
||||||
<div className="actions-row actions-row--inline">
|
<div className="actions-row actions-row--inline">
|
||||||
<span className={`conn-pill conn-pill--${tokenStatusQuery.data?.has_token ? "up" : "down"}`}>
|
<span
|
||||||
token: {tokenStatusQuery.data?.has_token ? "connected" : "disconnected"}
|
className={`conn-pill conn-pill--${!hasToken ? "down" : tokenNeedsRenewal ? "warn" : "up"}`}
|
||||||
|
title={
|
||||||
|
tokenNeedsRenewal
|
||||||
|
? "库中 token 缺少有效过期时间或已过期,请点「手动续期/登录」或等待后台自动续期"
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
token: {!hasToken ? "disconnected" : tokenNeedsRenewal ? "connected (需续期)" : "connected"}
|
||||||
</span>
|
</span>
|
||||||
<span className={`conn-pill conn-pill--${tokenLevel}`}>
|
<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>
|
</span>
|
||||||
{tokenStatusQuery.data?.token_preview ? <span className="conn-pill">preview: {tokenStatusQuery.data.token_preview}</span> : null}
|
{tokenStatusQuery.data?.token_preview ? <span className="conn-pill">preview: {tokenStatusQuery.data.token_preview}</span> : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -144,13 +184,42 @@ export function UmePage() {
|
||||||
onClick={() => queryClient.invalidateQueries({ queryKey: ["umeTokenStatus"] })}
|
onClick={() => queryClient.invalidateQueries({ queryKey: ["umeTokenStatus"] })}
|
||||||
disabled={tokenStatusQuery.isFetching}
|
disabled={tokenStatusQuery.isFetching}
|
||||||
>
|
>
|
||||||
刷新状态
|
{tokenStatusQuery.isFetching ? (
|
||||||
|
<>
|
||||||
|
<span className="inline-spinner" aria-hidden />
|
||||||
|
刷新中…
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"刷新状态"
|
||||||
|
)}
|
||||||
</button>
|
</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>
|
||||||
<button onClick={() => tokenDisconnectMutation.mutate()} disabled={tokenDisconnectMutation.isPending}>
|
<button
|
||||||
断开 token
|
type="button"
|
||||||
|
onClick={() => tokenDisconnectMutation.mutate()}
|
||||||
|
disabled={tokenRefreshMutation.isPending || tokenDisconnectMutation.isPending}
|
||||||
|
>
|
||||||
|
{tokenDisconnectMutation.isPending ? (
|
||||||
|
<>
|
||||||
|
<span className="inline-spinner" aria-hidden />
|
||||||
|
断开中…
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"断开 token"
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{(tokenOpError || tokenRefreshMutation.error || tokenDisconnectMutation.error) && (
|
{(tokenOpError || tokenRefreshMutation.error || tokenDisconnectMutation.error) && (
|
||||||
|
|
|
||||||
|
|
@ -148,12 +148,13 @@ export type UmeAlarmItem = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UmeTokenStatus = {
|
export type UmeTokenStatus = {
|
||||||
ok: boolean;
|
ok?: boolean;
|
||||||
has_token: boolean;
|
has_token: boolean;
|
||||||
expires_in_s: number;
|
expires_in_s: number;
|
||||||
expires_at_epoch_s: number;
|
expires_at_epoch_s: number;
|
||||||
auth_header: string;
|
auth_header: string;
|
||||||
token_preview?: string;
|
token_preview?: string;
|
||||||
|
changed?: boolean;
|
||||||
error_kind?: string;
|
error_kind?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue