mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 03:10:46 +08:00
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:
parent
4dd121b42f
commit
83acb5eef1
10 changed files with 106 additions and 128 deletions
|
|
@ -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: (
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 />} />
|
||||
|
|
|
|||
|
|
@ -1654,6 +1654,7 @@ const en = {
|
|||
detached: "Detached (grace)",
|
||||
pending: "Pending",
|
||||
paused: "Paused",
|
||||
idle: "Idle",
|
||||
error: "Error",
|
||||
failed: "Failed",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1645,6 +1645,7 @@ const zh = {
|
|||
detached: "已断开(宽限)",
|
||||
pending: "排队",
|
||||
paused: "暂停",
|
||||
idle: "空闲",
|
||||
error: "错误",
|
||||
failed: "失败",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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}·`)) {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue