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 <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-08-05 09:16:52 +08:00
parent 4dd121b42f
commit 83acb5eef1
10 changed files with 106 additions and 128 deletions

View file

@ -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 (
<AppLayout
connections={{
netxApi: integrationsQuery.data?.netx_api?.status ?? (integrationsQuery.isError ? "down" : "unknown"),
netxApiLatencyMs:
typeof integrationsQuery.data?.db?.latency_ms === "number" ? integrationsQuery.data.db.latency_ms : undefined,
oclawBridge:
integrationsQuery.data?.oclaw_bridge?.status ?? (integrationsQuery.isError ? "down" : "unknown"),
oclawBridgeLatencyMs: undefined,
oclawBridgeErrorKind:
typeof integrationsQuery.data?.oclaw_bridge?.error_kind === "string"
? integrationsQuery.data.oclaw_bridge.error_kind
: undefined,
oclawBridgeError:
typeof integrationsQuery.data?.oclaw_bridge?.error === "string"
? integrationsQuery.data.oclaw_bridge.error
: undefined,
oclawBridgeQueueSize:
typeof integrationsQuery.data?.oclaw_bridge?.queue_size === "number"
? integrationsQuery.data.oclaw_bridge.queue_size
: undefined,
oclawBridgePublishedOk:
typeof integrationsQuery.data?.oclaw_bridge?.published_ok === "number"
? integrationsQuery.data.oclaw_bridge.published_ok
: undefined,
}}
>
<AppLayout>
<Suspense fallback={<PageFallback />}>
<Routes>
<Route path="/" element={<WorkbenchPage />} />

View file

@ -1654,6 +1654,7 @@ const en = {
detached: "Detached (grace)",
pending: "Pending",
paused: "Paused",
idle: "Idle",
error: "Error",
failed: "Failed",
},

View file

@ -1645,6 +1645,7 @@ const zh = {
detached: "已断开(宽限)",
pending: "排队",
paused: "暂停",
idle: "空闲",
error: "错误",
failed: "失败",
},

View file

@ -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;
}

View file

@ -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}
</div>
<div className="app-brand__actions">
<div className="app-brand__actions-group">
<span className={`conn-pill conn-pill--on-brand conn-pill--${connections.netxApi}`}>
{t("layout.netxApi")}: {connections.netxApi}
{netxSuffix}
</span>
<span
className={`conn-pill conn-pill--on-brand conn-pill--${connections.oclawBridge}`}
title={oclawTitle}
>
{t("layout.oclawBridge")}: {connections.oclawBridge}
{oclawSuffix}
</span>
</div>
{user ? (
<>
<span className="app-brand__actions-sep" aria-hidden />
<div className="app-brand__actions-group">
<button
type="button"
className={`conn-pill conn-pill--on-brand conn-pill--tasks conn-pill--${activeTasks > 0 ? "up" : "unknown"}`}
title={t("layout.activeTasksHint")}
onClick={() => openOrFocusModule({ moduleId: "audit", path: "/audit/tasks" })}
>
{t("layout.activeTasks", { count: activeTasks })}
</button>
<span
className="conn-pill conn-pill--on-brand conn-pill--user"
title={user.role}
>
{user.username}
</span>
<button
type="button"
className="header-menu__trigger header-menu__trigger--on-brand"
onClick={() => void logout()}
>
{t("auth.logout")}
</button>
<HeaderMenu />
</div>
</>
<div className="app-brand__actions-group">
<button
type="button"
className={`conn-pill conn-pill--on-brand conn-pill--tasks conn-pill--${activeTasks > 0 ? "up" : "unknown"}`}
title={t("layout.activeTasksHint")}
onClick={() => openOrFocusModule({ moduleId: "audit", path: "/audit/tasks" })}
>
{t("layout.activeTasks", { count: activeTasks })}
</button>
<span className="conn-pill conn-pill--on-brand conn-pill--user" title={user.role}>
{user.username}
</span>
<button
type="button"
className="header-menu__trigger header-menu__trigger--on-brand"
onClick={() => void logout()}
>
{t("auth.logout")}
</button>
<HeaderMenu />
</div>
) : (
<HeaderMenu />
)}

View file

@ -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);

View file

@ -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, stri
return tr === key ? raw : tr;
}
function runtimeTaskSubject(
taskId: string,
t: (k: string, vars?: Record<string, string | number>) => 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 | number>) => 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}·`)) {

View file

@ -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}