mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 03:10:46 +08:00
Show DSH alarm hub multi-subscriber status instead of OClaw single link.
Track per-connection metadata on the hub and surface it in the UME key-alert UI so operators can see which Netx Ops clients are subscribed. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
0f8a06bfba
commit
5096117ad4
8 changed files with 328 additions and 46 deletions
|
|
@ -1025,7 +1025,24 @@ const en = {
|
|||
title: "AI alarm monitor",
|
||||
showPanel: "Details",
|
||||
hidePanel: "Close",
|
||||
help: "Exact match by notificationId, or substring match on alarm description (case-insensitive; nativeProbableCause, etc.). Optionally restrict to UME inventory ne_type values (multi-select; empty = all devices). Pushed via OClaw WebSocket. Shares NETX_OCLAW_ANALYZE_TOKEN.",
|
||||
help: "Exact match by notificationId, or substring match on alarm description (case-insensitive; nativeProbableCause, etc.). Optionally restrict to UME inventory ne_type values (multi-select; empty = all devices). Matched alerts are broadcast by NetX (server) to subscribed Netx Ops (DSH) clients.",
|
||||
hub: "DSH subscribers",
|
||||
hubConnected: "subscribed",
|
||||
hubEmpty: "no subscribers",
|
||||
hubSubscribers: "subscribers",
|
||||
hubPublished: "published",
|
||||
hubDeliverOk: "deliver ok",
|
||||
hubDeliverFail: "deliver fail",
|
||||
hubConnections: "Active connections",
|
||||
hubColId: "id",
|
||||
hubColUser: "user",
|
||||
hubColRemote: "remote",
|
||||
hubColClient: "client",
|
||||
hubColConnected: "connected",
|
||||
hubColLastSeen: "last seen",
|
||||
hubEmptyConnections: "No Netx Ops clients are connected to the alarm hub",
|
||||
hubPath: "path",
|
||||
legacyOclaw: "Legacy OClaw outbound",
|
||||
ws: "OClaw WSS",
|
||||
wsConnected: "connected",
|
||||
wsDisconnected: "disconnected",
|
||||
|
|
@ -1046,7 +1063,7 @@ const en = {
|
|||
neTypesEmpty: "No device types yet — sync Inventory first",
|
||||
neTypesAll: "All",
|
||||
forwardOnClear: "Push on alarm clear",
|
||||
forwardOnClearHelp: "Global switch for all monitor rules. By default only new/updated alarms are pushed; when enabled, clear notifications are also sent when UME reports alarms cleared.",
|
||||
forwardOnClearHelp: "Global switch for all monitor rules. By default only new/updated alarms are pushed; when enabled, clear notifications are also sent to subscribed Netx Ops clients.",
|
||||
add: "Add rules",
|
||||
adding: "Adding…",
|
||||
delete: "Delete",
|
||||
|
|
|
|||
|
|
@ -1017,7 +1017,24 @@ const zh = {
|
|||
title: "AI 告警监控",
|
||||
showPanel: "详情",
|
||||
hidePanel: "关闭",
|
||||
help: "按 notificationId 精确匹配,或按告警描述关键字模糊匹配(不区分大小写,匹配 nativeProbableCause 等字段);可选限定 UME Inventory 中的网元类型 (ne_type),不选则匹配全部设备。经 OClaw WebSocket 推送到 WhatsApp 群。鉴权与 NETX_OCLAW_ANALYZE_TOKEN 共用。",
|
||||
help: "按 notificationId 精确匹配,或按告警描述关键字模糊匹配(不区分大小写,匹配 nativeProbableCause 等字段);可选限定 UME Inventory 中的网元类型 (ne_type),不选则匹配全部设备。命中后由 NetX 作为服务器广播给已订阅的 Netx Ops(DSH)客户端。",
|
||||
hub: "DSH 订阅",
|
||||
hubConnected: "有订阅",
|
||||
hubEmpty: "无订阅",
|
||||
hubSubscribers: "订阅数",
|
||||
hubPublished: "已广播",
|
||||
hubDeliverOk: "投递成功",
|
||||
hubDeliverFail: "投递失败",
|
||||
hubConnections: "当前连接",
|
||||
hubColId: "连接 ID",
|
||||
hubColUser: "用户",
|
||||
hubColRemote: "远端",
|
||||
hubColClient: "客户端",
|
||||
hubColConnected: "连入时间",
|
||||
hubColLastSeen: "最近心跳",
|
||||
hubEmptyConnections: "当前没有 Netx Ops 客户端连入告警 hub",
|
||||
hubPath: "路径",
|
||||
legacyOclaw: "遗留 OClaw 出站",
|
||||
ws: "OClaw WSS",
|
||||
wsConnected: "已连接",
|
||||
wsDisconnected: "未连接",
|
||||
|
|
@ -1038,7 +1055,7 @@ const zh = {
|
|||
neTypesEmpty: "暂无设备类型,请先同步 Inventory",
|
||||
neTypesAll: "全部",
|
||||
forwardOnClear: "告警清除时也推送",
|
||||
forwardOnClearHelp: "全局开关:对所有监控规则生效。默认只在告警新增/更新时推送;勾选后,UME 上报告警已清除时也会向 WhatsApp 发送清除通知。",
|
||||
forwardOnClearHelp: "全局开关:对所有监控规则生效。默认只在告警新增/更新时推送;勾选后,UME 上报告警已清除时也会向已订阅的 Netx Ops 推送清除通知。",
|
||||
add: "添加规则",
|
||||
adding: "添加中…",
|
||||
delete: "删除",
|
||||
|
|
|
|||
|
|
@ -432,11 +432,15 @@ export function UmePage() {
|
|||
[neQuery.data, expandedNeId],
|
||||
);
|
||||
|
||||
const keyAlertHub = keyAlertMonitorQuery.data?.dsh_alarm_hub;
|
||||
const keyAlertHubConnections = keyAlertHub?.connections || [];
|
||||
const keyAlertHubSubscribers = Number(keyAlertHub?.subscribers || keyAlertHubConnections.length || 0);
|
||||
const keyAlertForwarder = keyAlertMonitorQuery.data?.forwarder;
|
||||
const keyAlertRules = keyAlertMonitorQuery.data?.rules || [];
|
||||
const keyAlertTotal = Number(keyAlertMonitorQuery.data?.total || keyAlertRules.length);
|
||||
const keyAlertPages = pageCount(keyAlertTotal, keyAlertPageSize);
|
||||
const keyAlertForwardOnClear = Boolean(keyAlertMonitorQuery.data?.config?.forward_on_clear);
|
||||
const hubWsPill = keyAlertHubSubscribers > 0 ? "up" : "down";
|
||||
const oclawWsPill =
|
||||
!keyAlertForwarder?.enabled
|
||||
? "unknown"
|
||||
|
|
@ -445,6 +449,7 @@ export function UmePage() {
|
|||
: keyAlertForwarder.connected
|
||||
? "up"
|
||||
: "down";
|
||||
const showLegacyOclaw = Boolean(keyAlertForwarder?.enabled);
|
||||
|
||||
const runtimeTaskLabel = (task: string) => {
|
||||
const key = `ume.tasks.runtimeTask.${task}`;
|
||||
|
|
@ -852,25 +857,30 @@ export function UmePage() {
|
|||
</button>
|
||||
</div>
|
||||
<div className="ume-entry__pills actions-row actions-row--inline">
|
||||
<span className={`conn-pill conn-pill--${oclawWsPill}`}>
|
||||
{t("ume.keyAlert.ws")}:{" "}
|
||||
{!keyAlertForwarder?.enabled
|
||||
? t("ume.keyAlert.wsDisabled")
|
||||
: keyAlertForwarder.paused
|
||||
<span className={`conn-pill conn-pill--${hubWsPill}`}>
|
||||
{t("ume.keyAlert.hub")}:{" "}
|
||||
{keyAlertHubSubscribers > 0
|
||||
? t("ume.keyAlert.hubConnected")
|
||||
: t("ume.keyAlert.hubEmpty")}
|
||||
</span>
|
||||
<span className="conn-pill">
|
||||
{t("ume.keyAlert.hubSubscribers")}: {keyAlertHubSubscribers}
|
||||
</span>
|
||||
<span className="conn-pill">
|
||||
{t("ume.keyAlert.hubPublished")}: {Number(keyAlertHub?.published || 0)}
|
||||
</span>
|
||||
<span className="conn-pill">
|
||||
{t("ume.keyAlert.hubDeliverOk")}: {Number(keyAlertHub?.deliver_ok || 0)}
|
||||
</span>
|
||||
{showLegacyOclaw ? (
|
||||
<span className={`conn-pill conn-pill--${oclawWsPill}`} title={t("ume.keyAlert.legacyOclaw")}>
|
||||
{t("ume.keyAlert.ws")}:{" "}
|
||||
{keyAlertForwarder?.paused
|
||||
? t("ume.keyAlert.wsPaused")
|
||||
: keyAlertForwarder.connected
|
||||
: keyAlertForwarder?.connected
|
||||
? t("ume.keyAlert.wsConnected")
|
||||
: t("ume.keyAlert.wsDisconnected")}
|
||||
</span>
|
||||
{keyAlertForwarder?.enabled ? (
|
||||
<>
|
||||
<span className="conn-pill">
|
||||
{t("ume.keyAlert.publishedOk")}: {Number(keyAlertForwarder.published_ok || 0)}
|
||||
</span>
|
||||
<span className="conn-pill">
|
||||
{t("ume.keyAlert.queue")}: {Number(keyAlertForwarder.queue_size || 0)}
|
||||
</span>
|
||||
</>
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</article>
|
||||
|
|
@ -964,16 +974,22 @@ export function UmePage() {
|
|||
<div className="ops-detail-modal__title">
|
||||
<h3>{t("ume.keyAlert.title")}</h3>
|
||||
<p className="muted">
|
||||
{t("ume.keyAlert.ws")}:{" "}
|
||||
{!keyAlertForwarder?.enabled
|
||||
? t("ume.keyAlert.wsDisabled")
|
||||
: keyAlertForwarder.paused
|
||||
? t("ume.keyAlert.wsPaused")
|
||||
: keyAlertForwarder.connected
|
||||
? t("ume.keyAlert.wsConnected")
|
||||
: t("ume.keyAlert.wsDisconnected")}
|
||||
{keyAlertForwarder?.enabled
|
||||
? ` · ${t("ume.keyAlert.publishedOk")} ${Number(keyAlertForwarder.published_ok || 0)} · ${t("ume.keyAlert.queue")} ${Number(keyAlertForwarder.queue_size || 0)}`
|
||||
{t("ume.keyAlert.hub")}:{" "}
|
||||
{keyAlertHubSubscribers > 0
|
||||
? t("ume.keyAlert.hubConnected")
|
||||
: t("ume.keyAlert.hubEmpty")}
|
||||
{` · ${t("ume.keyAlert.hubSubscribers")} ${keyAlertHubSubscribers}`}
|
||||
{` · ${t("ume.keyAlert.hubPublished")} ${Number(keyAlertHub?.published || 0)}`}
|
||||
{` · ${t("ume.keyAlert.hubDeliverOk")} ${Number(keyAlertHub?.deliver_ok || 0)}`}
|
||||
{keyAlertHub?.path ? ` · ${t("ume.keyAlert.hubPath")} ${keyAlertHub.path}` : ""}
|
||||
{showLegacyOclaw
|
||||
? ` · ${t("ume.keyAlert.legacyOclaw")}: ${
|
||||
keyAlertForwarder?.paused
|
||||
? t("ume.keyAlert.wsPaused")
|
||||
: keyAlertForwarder?.connected
|
||||
? t("ume.keyAlert.wsConnected")
|
||||
: t("ume.keyAlert.wsDisconnected")
|
||||
}`
|
||||
: ""}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -992,6 +1008,62 @@ export function UmePage() {
|
|||
</div>
|
||||
|
||||
<div className="ops-detail-modal__scroll ops-detail-modal__scroll--flow">
|
||||
<div className="ume-modal-form">
|
||||
<div className="muted ume-modal-form__label">{t("ume.keyAlert.hubConnections")}</div>
|
||||
{keyAlertHubConnections.length === 0 ? (
|
||||
<p className="muted">{t("ume.keyAlert.hubEmptyConnections")}</p>
|
||||
) : (
|
||||
<div className="table-wrap">
|
||||
<table className="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("ume.keyAlert.hubColId")}</th>
|
||||
<th>{t("ume.keyAlert.hubColUser")}</th>
|
||||
<th>{t("ume.keyAlert.hubColRemote")}</th>
|
||||
<th>{t("ume.keyAlert.hubColClient")}</th>
|
||||
<th>{t("ume.keyAlert.hubColConnected")}</th>
|
||||
<th>{t("ume.keyAlert.hubColLastSeen")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{keyAlertHubConnections.map((conn) => (
|
||||
<tr key={conn.id}>
|
||||
<td>
|
||||
<code>{conn.id}</code>
|
||||
</td>
|
||||
<td>{conn.user || "-"}</td>
|
||||
<td>{conn.remote || "-"}</td>
|
||||
<td>{conn.client || "-"}</td>
|
||||
<td>
|
||||
{conn.connected_at
|
||||
? formatSystemTime(conn.connected_at)
|
||||
: "-"}
|
||||
</td>
|
||||
<td>
|
||||
{conn.last_seen_at
|
||||
? formatSystemTime(conn.last_seen_at)
|
||||
: "-"}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
{showLegacyOclaw ? (
|
||||
<p className="muted ume-modal-form__hint">
|
||||
{t("ume.keyAlert.legacyOclaw")}:{" "}
|
||||
{keyAlertForwarder?.paused
|
||||
? t("ume.keyAlert.wsPaused")
|
||||
: keyAlertForwarder?.connected
|
||||
? t("ume.keyAlert.wsConnected")
|
||||
: t("ume.keyAlert.wsDisconnected")}
|
||||
{` · ${t("ume.keyAlert.publishedOk")} ${Number(keyAlertForwarder?.published_ok || 0)}`}
|
||||
{` · ${t("ume.keyAlert.queue")} ${Number(keyAlertForwarder?.queue_size || 0)}`}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="ume-modal-form">
|
||||
<div className="ops-detail-modal__toolbar filter-inline">
|
||||
<label className="muted">{t("ume.keyAlert.matchType")}</label>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,26 @@
|
|||
export type DshAlarmHubConnection = {
|
||||
id: string;
|
||||
user: string;
|
||||
remote?: string;
|
||||
client?: string;
|
||||
connected_at?: string;
|
||||
last_seen_at?: string;
|
||||
};
|
||||
|
||||
export type DshAlarmHubStatus = {
|
||||
enabled: boolean;
|
||||
path?: string;
|
||||
subscribers: number;
|
||||
published?: number;
|
||||
deliver_ok?: number;
|
||||
deliver_fail?: number;
|
||||
connections?: DshAlarmHubConnection[];
|
||||
};
|
||||
|
||||
export type IntegrationStatus = {
|
||||
netx_api: { status: "up" | "down" | "unknown"; [k: string]: unknown };
|
||||
db: { status: "up" | "down" | "unknown"; latency_ms?: number; error?: string; [k: string]: unknown };
|
||||
dsh_alarm_hub?: DshAlarmHubStatus;
|
||||
oclaw_bridge?: {
|
||||
status: "up" | "down" | "unknown";
|
||||
mode?: string;
|
||||
|
|
@ -58,6 +78,9 @@ export type UmeKeyAlertMonitorResponse = {
|
|||
config?: {
|
||||
forward_on_clear: boolean;
|
||||
};
|
||||
/** Primary: NetX hub ← netxops clients (multi-subscriber). */
|
||||
dsh_alarm_hub?: DshAlarmHubStatus;
|
||||
/** Legacy: NetX → OClaw outbound bridge (single link). */
|
||||
forwarder: UmeKeyAlertForwarderStatus;
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue