From 83acb5eef111b5cdcf9699c0bc2b5d90fe8a640a Mon Sep 17 00:00:00 2001 From: oliver Date: Wed, 5 Aug 2026 09:16:52 +0800 Subject: [PATCH] Move integration health into active tasks and fix dark topology labels. Drop header netx-api/oclaw pills, restore UME/OClaw runtime rows in ops tasks, and raise node/edge label contrast on the dark canvas. Co-authored-by: Cursor --- netx_api/ops_tasks_service.py | 51 +++++++++++-- netx_api/ume_support.py | 1 + web/src/App.tsx | 37 +--------- web/src/i18n/en.ts | 1 + web/src/i18n/zh.ts | 1 + web/src/index.css | 31 +++++--- web/src/layout/AppLayout.tsx | 91 ++++++------------------ web/src/pages/UmePage.tsx | 4 +- web/src/pages/audit/TaskOverviewPage.tsx | 15 +++- web/src/pages/topology/ParallelEdge.tsx | 2 - 10 files changed, 106 insertions(+), 128 deletions(-) diff --git a/netx_api/ops_tasks_service.py b/netx_api/ops_tasks_service.py index 8b03047..22bb9a0 100644 --- a/netx_api/ops_tasks_service.py +++ b/netx_api/ops_tasks_service.py @@ -332,13 +332,50 @@ def _ne_connect_items(db: Session, actors: dict[str, str]) -> list[dict[str, Any def _ume_runtime_items() -> list[dict[str, Any]]: try: - from .main import _list_runtime_tasks + from .ume_support import _list_runtime_tasks except Exception: return [] + fwd: dict[str, Any] | None = None + try: + from .oclaw_alarm_forwarder import forwarder_status + + fwd = forwarder_status() + except Exception: + fwd = None + ws_conn: dict[str, Any] | None = None + try: + from .ume_alarm_ws import get_ws_connection_status + + ws_conn = get_ws_connection_status() + except Exception: + ws_conn = None items: list[dict[str, Any]] = [] for row in _list_runtime_tasks(): task = str(row.get("task") or "") status = str(row.get("status") or "unknown") + progress = str(row.get("interval_label") or "") + detail = str(row.get("last_error") or "")[:240] + if task == "oclaw_alarm_forwarder" and isinstance(fwd, dict): + bits: list[str] = [] + if not bool(fwd.get("enabled")): + bits.append("disabled") + elif bool(fwd.get("paused")): + bits.append("paused") + elif bool(fwd.get("connected")): + bits.append("connected") + else: + bits.append("disconnected") + q = int(fwd.get("queue_size") or 0) + if q > 0: + bits.append(f"q={q}") + pub_ok = int(fwd.get("published_ok") or 0) + if pub_ok > 0: + bits.append(f"pub={pub_ok}") + progress = " · ".join([p for p in (progress, *bits) if p]) + elif task == "alarms_current_ws_consumer" and isinstance(ws_conn, dict): + state = str(ws_conn.get("state") or ws_conn.get("status") or "").strip() + if state: + progress = " · ".join([p for p in (progress, state) if p]) # Always show UME background loops so ops can see paused/idle too. items.append( _item( @@ -350,8 +387,8 @@ def _ume_runtime_items() -> list[dict[str, Any]]: actor="system", started_at=None, updated_at=None, - progress=str(row.get("interval_label") or ""), - detail=str(row.get("last_error") or "")[:240], + progress=progress, + detail=detail, href="/ume", ) ) @@ -437,6 +474,7 @@ def list_ops_tasks(db: Session) -> dict[str, Any]: "collecting", "pending", "paused", + "idle", "connecting", "ready", "testing", @@ -451,9 +489,10 @@ def list_ops_tasks(db: Session) -> dict[str, Any]: "connecting": 3, "pending": 4, "paused": 5, - "ready": 6, - "detached": 7, - "error": 8, + "idle": 6, + "ready": 7, + "detached": 8, + "error": 9, } items.sort( key=lambda x: ( diff --git a/netx_api/ume_support.py b/netx_api/ume_support.py index 040506a..68a8136 100644 --- a/netx_api/ume_support.py +++ b/netx_api/ume_support.py @@ -30,6 +30,7 @@ from .runtime_task_messages import ( RT_UME_WS_DISABLED_NO_BASE_URL, RT_WSS_ACTIVE_SKIP_REST, ) +from .oclaw_alarm_forwarder import is_forwarder_enabled from .ume_alarm_ws import ( begin_startup_alarm_sync_gate, complete_startup_alarm_sync_gate, diff --git a/web/src/App.tsx b/web/src/App.tsx index b588064..d5cc421 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,12 +1,9 @@ import { lazy, Suspense } from "react"; import { Navigate, Route, Routes, useLocation } from "react-router-dom"; -import { useQuery } from "@tanstack/react-query"; import { AppLayout } from "./layout/AppLayout"; -import { queryKeys } from "./constants/queryKeys"; import { WorkbenchPage } from "./pages/WorkbenchPage"; import { LoginPage } from "./pages/LoginPage"; import { ForceChangePasswordPage } from "./pages/ForceChangePasswordPage"; -import { fetchIntegrationStatus } from "./services/api"; import { useAuth } from "./auth/AuthContext"; const CollectPage = lazy(() => import("./pages/CollectPage").then((m) => ({ default: m.CollectPage }))); @@ -84,13 +81,6 @@ function PageFallback() { function ProtectedApp() { const { ready, user } = useAuth(); - const integrationsQuery = useQuery({ - queryKey: queryKeys.integrationsStatus, - queryFn: fetchIntegrationStatus, - refetchInterval: 10_000, - staleTime: 5_000, - enabled: ready && Boolean(user) && !user?.must_change_password, - }); if (!ready) { return ( @@ -107,32 +97,7 @@ function ProtectedApp() { } return ( - + }> } /> diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index b0cd873..73e766f 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -1654,6 +1654,7 @@ const en = { detached: "Detached (grace)", pending: "Pending", paused: "Paused", + idle: "Idle", error: "Error", failed: "Failed", }, diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 0dd5e3f..b1ec264 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -1645,6 +1645,7 @@ const zh = { detached: "已断开(宽限)", pending: "排队", paused: "暂停", + idle: "空闲", error: "错误", failed: "失败", }, diff --git a/web/src/index.css b/web/src/index.css index 54322fd..39ecb35 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -7116,7 +7116,7 @@ pre { } .topo-node.is-selected .topo-node__glyph { - outline: 2px solid #334155; + outline: 2px solid #60a5fa; outline-offset: 3px; border-radius: 4px; } @@ -7127,10 +7127,14 @@ pre { } .topo-node.is-selected .topo-node__caption { - color: #0f172a; + color: #f8fafc; font-weight: 700; } +.topo-node.is-selected .topo-node__caption-name { + color: #93c5fd; +} + /* Selected link: light highlight only (do not alter geometry / arrows). */ .topo-canvas .react-flow__edge.selected .react-flow__edge-path { stroke: #2563eb !important; @@ -7155,10 +7159,13 @@ pre { align-items: center; gap: 1px; margin-top: 3px; - color: #334155; + color: #e2e8f0; line-height: 1.2; max-width: 160px; pointer-events: none; + text-shadow: + 0 0 3px rgba(15, 23, 42, 0.95), + 0 1px 2px rgba(15, 23, 42, 0.85); } .topo-node__caption-name { @@ -7166,7 +7173,7 @@ pre { max-width: 100%; font-size: 11px; font-weight: 700; - color: #0f172a; + color: #f1f5f9; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -7176,7 +7183,7 @@ pre { display: block; max-width: 100%; font-size: 10px; - color: #64748b; + color: #94a3b8; font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace; white-space: nowrap; overflow: hidden; @@ -7194,17 +7201,19 @@ pre { .topo-edge-label { font-size: 11px; - color: #cbd5e1; - background: rgba(15, 23, 42, 0.92); - border: 1px solid rgba(148, 163, 184, 0.28); - border-radius: 4px; - padding: 1px 6px; + color: #e2e8f0; + background: transparent; + border: 0; + border-radius: 0; + padding: 0; white-space: nowrap; + text-shadow: + 0 0 3px rgba(15, 23, 42, 0.95), + 0 1px 2px rgba(15, 23, 42, 0.85); } .topo-edge-label.is-selected { color: #93c5fd; - border-color: rgba(59, 130, 246, 0.55); font-weight: 600; } diff --git a/web/src/layout/AppLayout.tsx b/web/src/layout/AppLayout.tsx index e6b2c2b..21610ee 100644 --- a/web/src/layout/AppLayout.tsx +++ b/web/src/layout/AppLayout.tsx @@ -11,23 +11,11 @@ import { openOrFocusModule } from "../utils/moduleWindows"; import { returnToWorkbench } from "../utils/workbench"; import { useAuth } from "../auth/AuthContext"; -type ConnLevel = "up" | "down" | "unknown"; - type Props = { - connections: { - netxApi: ConnLevel; - netxApiLatencyMs?: number; - oclawBridge: ConnLevel; - oclawBridgeLatencyMs?: number; - oclawBridgeErrorKind?: string; - oclawBridgeError?: string; - oclawBridgeQueueSize?: number; - oclawBridgePublishedOk?: number; - }; children: ReactNode; }; -export function AppLayout({ connections, children }: Props) { +export function AppLayout({ children }: Props) { const { t } = useI18n(); const { pathname } = useLocation(); const { user, logout } = useAuth(); @@ -41,22 +29,6 @@ export function AppLayout({ connections, children }: Props) { staleTime: 1500, }); const activeTasks = opsTasksQuery.data?.active ?? 0; - const netxSuffix = - typeof connections.netxApiLatencyMs === "number" ? ` (${connections.netxApiLatencyMs}ms)` : ""; - const oclawSuffix = - connections.oclawBridge === "up" - ? typeof connections.oclawBridgePublishedOk === "number" - ? ` (pub ${connections.oclawBridgePublishedOk})` - : "" - : connections.oclawBridgeErrorKind - ? ` (${connections.oclawBridgeErrorKind})` - : typeof connections.oclawBridgeQueueSize === "number" && connections.oclawBridgeQueueSize > 0 - ? ` (q ${connections.oclawBridgeQueueSize})` - : ""; - const oclawTitle = - connections.oclawBridge === "down" && connections.oclawBridgeError - ? connections.oclawBridgeError - : undefined; useAppWindowRegistration(pathname); @@ -88,47 +60,28 @@ export function AppLayout({ connections, children }: Props) { ) : null}
-
- - {t("layout.netxApi")}: {connections.netxApi} - {netxSuffix} - - - {t("layout.oclawBridge")}: {connections.oclawBridge} - {oclawSuffix} - -
{user ? ( - <> - -
- - - {user.username} - - - -
- +
+ + + {user.username} + + + +
) : ( )} diff --git a/web/src/pages/UmePage.tsx b/web/src/pages/UmePage.tsx index 3c0e617..3abc7ad 100644 --- a/web/src/pages/UmePage.tsx +++ b/web/src/pages/UmePage.tsx @@ -334,7 +334,7 @@ export function UmePage() { setKeyAlertNeTypes([]); showOk(t("ume.keyAlert.addOk")); await queryClient.invalidateQueries({ queryKey: queryKeys.umeKeyAlertMonitorAll }); - await queryClient.invalidateQueries({ queryKey: queryKeys.integrationsStatus }); + await queryClient.invalidateQueries({ queryKey: ["opsTasks"] }); }, onError: (err) => { const msg = String(err); @@ -349,7 +349,7 @@ export function UmePage() { onSuccess: async () => { showOk(t("ume.keyAlert.deleteOk")); await queryClient.invalidateQueries({ queryKey: queryKeys.umeKeyAlertMonitorAll }); - await queryClient.invalidateQueries({ queryKey: queryKeys.integrationsStatus }); + await queryClient.invalidateQueries({ queryKey: ["opsTasks"] }); }, onError: (err) => { const msg = String(err); diff --git a/web/src/pages/audit/TaskOverviewPage.tsx b/web/src/pages/audit/TaskOverviewPage.tsx index 93eed65..27a3e55 100644 --- a/web/src/pages/audit/TaskOverviewPage.tsx +++ b/web/src/pages/audit/TaskOverviewPage.tsx @@ -10,7 +10,7 @@ const POLL_MS = 4000; function statusTone(status: string): string { const s = status.toLowerCase(); if (s === "collecting" || s === "running" || s === "connecting" || s === "testing") return "running"; - if (s === "ready") return "ok"; + if (s === "ready" || s === "idle") return "ok"; if (s === "paused" || s === "pending" || s === "detached") return "paused"; if (s === "failed" || s === "error") return "stopped"; return "other"; @@ -36,9 +36,20 @@ function triggerLabel(trigger: string, t: (k: string, vars?: Record) => string, +): string { + const key = `ume.tasks.runtimeTask.${taskId}`; + const label = t(key); + return label === key ? taskId : label; +} + function taskTitle(row: OpsTaskItem, t: (k: string, vars?: Record) => string): string { const kind = kindLabel(row.kind, t); - const subject = String(row.title || "").trim(); + const subjectRaw = String(row.title || row.id || "").trim(); + const subject = + row.kind === "ume_runtime" && subjectRaw ? runtimeTaskSubject(subjectRaw, t) : subjectRaw; if (!subject) return kind; // Avoid "Kind · Kind · x" if an old backend still prefixed the localized kind. if (subject === kind || subject.startsWith(`${kind} · `) || subject.startsWith(`${kind}·`)) { diff --git a/web/src/pages/topology/ParallelEdge.tsx b/web/src/pages/topology/ParallelEdge.tsx index 14050c3..9b9fcf1 100644 --- a/web/src/pages/topology/ParallelEdge.tsx +++ b/web/src/pages/topology/ParallelEdge.tsx @@ -11,7 +11,6 @@ export function ParallelEdge({ markerEnd, label, labelStyle, - labelBgStyle, data, selected, }: EdgeProps) { @@ -41,7 +40,6 @@ export function ParallelEdge({ transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`, pointerEvents: "none", ...labelStyle, - ...(labelBgStyle || {}), }} > {label}