mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-09 16:53:17 +08:00
Show key-alarm WSS status on the Netx Ops Plugins card (0.1.16).
Host publishes connection phases over Connection RPC; the card polls and badges Connected / Reconnecting / Auth failed so operators need not curl logs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
12f7018103
commit
e8b7d7331f
14 changed files with 783 additions and 55 deletions
227
lib/client.js
227
lib/client.js
|
|
@ -53,6 +53,65 @@ module.exports = __toCommonJS(exports_client);
|
|||
// src/client/NetxopsCard.tsx
|
||||
var import_react = require("react");
|
||||
|
||||
// src/client/alarm-push-status-view.ts
|
||||
var NETXOPS_RPC_CHANNEL = "/netxops";
|
||||
var ALARM_PUSH_STATUS_ENDPOINT = "alarm-push.status";
|
||||
var EMPTY = {
|
||||
phase: "disabled",
|
||||
enabled: false,
|
||||
wsUrl: "",
|
||||
detail: "",
|
||||
updatedAt: 0,
|
||||
lastConnectedAt: null,
|
||||
lastError: null
|
||||
};
|
||||
function asAlarmPushStatus(value) {
|
||||
if (value === null || typeof value !== "object" || Array.isArray(value))
|
||||
return { ...EMPTY };
|
||||
const row = value;
|
||||
const phase = typeof row.phase === "string" ? row.phase : "disabled";
|
||||
return {
|
||||
phase,
|
||||
enabled: row.enabled === true,
|
||||
wsUrl: typeof row.wsUrl === "string" ? row.wsUrl : "",
|
||||
detail: typeof row.detail === "string" ? row.detail : "",
|
||||
updatedAt: typeof row.updatedAt === "number" ? row.updatedAt : 0,
|
||||
lastConnectedAt: typeof row.lastConnectedAt === "number" ? row.lastConnectedAt : null,
|
||||
lastError: typeof row.lastError === "string" ? row.lastError : null
|
||||
};
|
||||
}
|
||||
async function fetchAlarmPushStatus(call, signal) {
|
||||
const result = await call(NETXOPS_RPC_CHANNEL, ALARM_PUSH_STATUS_ENDPOINT, {}, signal);
|
||||
if (result !== null && typeof result === "object" && result.ok === true) {
|
||||
return asAlarmPushStatus(result.value);
|
||||
}
|
||||
if (result !== null && typeof result === "object" && result.ok === false) {
|
||||
return {
|
||||
...EMPTY,
|
||||
phase: "error",
|
||||
enabled: true,
|
||||
detail: "rpc_error",
|
||||
lastError: String(result.error?.message ?? "rpc failed")
|
||||
};
|
||||
}
|
||||
return asAlarmPushStatus(result);
|
||||
}
|
||||
function alarmPushTone(phase) {
|
||||
switch (phase) {
|
||||
case "connected":
|
||||
return "ok";
|
||||
case "connecting":
|
||||
case "authenticating":
|
||||
case "reconnecting":
|
||||
return "warn";
|
||||
case "auth_failed":
|
||||
case "error":
|
||||
return "err";
|
||||
default:
|
||||
return "mute";
|
||||
}
|
||||
}
|
||||
|
||||
// src/client/styles.ts
|
||||
var STYLE_ID = "dsh-netxops-card-css";
|
||||
var CSS = `
|
||||
|
|
@ -65,6 +124,10 @@ var CSS = `
|
|||
.dsh-nx-name{font-size:15px;font-weight:600;line-height:1.4;color:var(--dsw-alias-label-primary)}
|
||||
.dsh-nx-desc{font-size:13px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}
|
||||
.dsh-nx-pending{flex:none;border-radius:999px;padding:1px 8px;font-size:11px;line-height:17px;font-weight:500;white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}
|
||||
.dsh-nx-status{flex:none;border-radius:999px;padding:1px 8px;font-size:11px;line-height:17px;font-weight:500;white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-tertiary)}
|
||||
.dsh-nx-statusOk{color:var(--dsw-alias-label-primary);background:color-mix(in srgb, var(--dsw-alias-label-primary) 12%, transparent)}
|
||||
.dsh-nx-statusWarn{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-module-platform)}
|
||||
.dsh-nx-statusErr{color:var(--dsw-alias-label-error);background:color-mix(in srgb, var(--dsw-alias-label-error) 12%, transparent)}
|
||||
.dsh-nx-chevron{flex:none;color:var(--dsw-alias-label-tertiary);transition:transform .16s}
|
||||
.dsh-nx-chevronOpen{transform:rotate(180deg)}
|
||||
.dsh-nx-body{border-top:.5px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}
|
||||
|
|
@ -106,6 +169,36 @@ function ensureStyles() {
|
|||
|
||||
// src/client/NetxopsCard.tsx
|
||||
var jsx_runtime = require("react/jsx-runtime");
|
||||
function phaseLocaleKey(phase) {
|
||||
switch (phase) {
|
||||
case "disabled":
|
||||
return "alarmPushPhaseDisabled";
|
||||
case "idle":
|
||||
return "alarmPushPhaseIdle";
|
||||
case "connecting":
|
||||
return "alarmPushPhaseConnecting";
|
||||
case "authenticating":
|
||||
return "alarmPushPhaseAuthenticating";
|
||||
case "connected":
|
||||
return "alarmPushPhaseConnected";
|
||||
case "reconnecting":
|
||||
return "alarmPushPhaseReconnecting";
|
||||
case "auth_failed":
|
||||
return "alarmPushPhaseAuthFailed";
|
||||
case "error":
|
||||
return "alarmPushPhaseError";
|
||||
default:
|
||||
return "alarmPushPhaseDisabled";
|
||||
}
|
||||
}
|
||||
function StatusBadge(props) {
|
||||
const tone = alarmPushTone(props.phase);
|
||||
const className = tone === "ok" ? "dsh-nx-status dsh-nx-statusOk" : tone === "warn" ? "dsh-nx-status dsh-nx-statusWarn" : tone === "err" ? "dsh-nx-status dsh-nx-statusErr" : "dsh-nx-status";
|
||||
return /* @__PURE__ */ jsx_runtime.jsx("span", {
|
||||
className,
|
||||
children: props.label
|
||||
});
|
||||
}
|
||||
function ValueField(props) {
|
||||
return /* @__PURE__ */ jsx_runtime.jsxs("div", {
|
||||
className: "dsh-nx-field",
|
||||
|
|
@ -176,6 +269,8 @@ function NetxopsCard(props) {
|
|||
const title = t("title");
|
||||
const disabled = !state.writable;
|
||||
const blocked = !state.dirty || state.invalid || state.saving;
|
||||
const pushStatus = state.alarmPushStatus;
|
||||
const showHeaderStatus = pushStatus !== null && (pushStatus.enabled || pushStatus.phase !== "disabled");
|
||||
return /* @__PURE__ */ jsx_runtime.jsxs("li", {
|
||||
className: open ? "dsh-nx-card dsh-nx-cardOpen" : "dsh-nx-card",
|
||||
children: [
|
||||
|
|
@ -201,6 +296,10 @@ function NetxopsCard(props) {
|
|||
})
|
||||
]
|
||||
}),
|
||||
showHeaderStatus && pushStatus ? /* @__PURE__ */ jsx_runtime.jsx(StatusBadge, {
|
||||
phase: pushStatus.phase,
|
||||
label: t(phaseLocaleKey(pushStatus.phase))
|
||||
}) : null,
|
||||
state.dirty ? /* @__PURE__ */ jsx_runtime.jsx("span", {
|
||||
className: "dsh-nx-pending",
|
||||
children: t("unsaved")
|
||||
|
|
@ -300,24 +399,32 @@ function NetxopsCard(props) {
|
|||
htmlFor: "netxops-alarm-push",
|
||||
children: t("alarmPushEnabled")
|
||||
}),
|
||||
state.alarmPushEnabled.overridden ? /* @__PURE__ */ jsx_runtime.jsxs("span", {
|
||||
/* @__PURE__ */ jsx_runtime.jsxs("span", {
|
||||
className: "dsh-nx-badges",
|
||||
children: [
|
||||
/* @__PURE__ */ jsx_runtime.jsx("span", {
|
||||
className: "dsh-nx-badge",
|
||||
children: t("overridden")
|
||||
}),
|
||||
/* @__PURE__ */ jsx_runtime.jsx("button", {
|
||||
type: "button",
|
||||
className: "dsh-nx-reset",
|
||||
disabled,
|
||||
onClick: () => {
|
||||
props.resetField("alarmPushEnabled");
|
||||
},
|
||||
children: t("reset")
|
||||
})
|
||||
pushStatus ? /* @__PURE__ */ jsx_runtime.jsx(StatusBadge, {
|
||||
phase: pushStatus.phase,
|
||||
label: `${t("alarmPushStatus")}: ${t(phaseLocaleKey(pushStatus.phase))}`
|
||||
}) : null,
|
||||
state.alarmPushEnabled.overridden ? /* @__PURE__ */ jsx_runtime.jsxs(jsx_runtime.Fragment, {
|
||||
children: [
|
||||
/* @__PURE__ */ jsx_runtime.jsx("span", {
|
||||
className: "dsh-nx-badge",
|
||||
children: t("overridden")
|
||||
}),
|
||||
/* @__PURE__ */ jsx_runtime.jsx("button", {
|
||||
type: "button",
|
||||
className: "dsh-nx-reset",
|
||||
disabled,
|
||||
onClick: () => {
|
||||
props.resetField("alarmPushEnabled");
|
||||
},
|
||||
children: t("reset")
|
||||
})
|
||||
]
|
||||
}) : null
|
||||
]
|
||||
}) : null
|
||||
})
|
||||
]
|
||||
}),
|
||||
/* @__PURE__ */ jsx_runtime.jsxs("label", {
|
||||
|
|
@ -337,7 +444,16 @@ function NetxopsCard(props) {
|
|||
children: t("alarmPushEnabledHint")
|
||||
})
|
||||
]
|
||||
})
|
||||
}),
|
||||
pushStatus?.wsUrl ? /* @__PURE__ */ jsx_runtime.jsx("p", {
|
||||
className: "dsh-nx-hint",
|
||||
children: pushStatus.wsUrl
|
||||
}) : null,
|
||||
pushStatus?.lastError ? /* @__PURE__ */ jsx_runtime.jsx("p", {
|
||||
className: "dsh-nx-invalid",
|
||||
role: "status",
|
||||
children: pushStatus.lastError
|
||||
}) : null
|
||||
]
|
||||
}),
|
||||
/* @__PURE__ */ jsx_runtime.jsxs("div", {
|
||||
|
|
@ -580,6 +696,7 @@ class CardForm {
|
|||
var NETXOPS_NS = "netxops";
|
||||
var DEFAULT_TOKEN_REF = "NETX_API_TOKEN";
|
||||
var API_TOKEN_FIELD = "apiToken";
|
||||
var STATUS_POLL_MS = 2000;
|
||||
|
||||
class NetxopsCardController {
|
||||
scope;
|
||||
|
|
@ -592,6 +709,10 @@ class NetxopsCardController {
|
|||
writable: false,
|
||||
remoteReady: false
|
||||
};
|
||||
rpcCall;
|
||||
alarmPushStatus = null;
|
||||
pollTimer;
|
||||
pollInFlight = false;
|
||||
constructor(scope, ctx) {
|
||||
this.scope = scope;
|
||||
this.ctx = ctx;
|
||||
|
|
@ -614,6 +735,48 @@ class NetxopsCardController {
|
|||
if (ready)
|
||||
this.readCredential();
|
||||
}
|
||||
setAlarmPushRpc(call) {
|
||||
this.rpcCall = call;
|
||||
if (call === undefined) {
|
||||
this.stopStatusPoll();
|
||||
if (this.alarmPushStatus !== null) {
|
||||
this.alarmPushStatus = null;
|
||||
this.store.set(this.projection());
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.startStatusPoll();
|
||||
this.refreshAlarmPushStatus();
|
||||
}
|
||||
startStatusPoll() {
|
||||
if (this.pollTimer !== undefined)
|
||||
return;
|
||||
this.pollTimer = setInterval(() => {
|
||||
this.refreshAlarmPushStatus();
|
||||
}, STATUS_POLL_MS);
|
||||
}
|
||||
stopStatusPoll() {
|
||||
if (this.pollTimer === undefined)
|
||||
return;
|
||||
clearInterval(this.pollTimer);
|
||||
this.pollTimer = undefined;
|
||||
}
|
||||
async refreshAlarmPushStatus() {
|
||||
const call = this.rpcCall;
|
||||
if (call === undefined || this.pollInFlight)
|
||||
return;
|
||||
this.pollInFlight = true;
|
||||
try {
|
||||
const next = await fetchAlarmPushStatus(call);
|
||||
const prev = this.alarmPushStatus;
|
||||
if (prev && prev.phase === next.phase && prev.enabled === next.enabled && prev.wsUrl === next.wsUrl && prev.detail === next.detail && prev.lastError === next.lastError && prev.lastConnectedAt === next.lastConnectedAt)
|
||||
return;
|
||||
this.alarmPushStatus = next;
|
||||
this.store.set(this.projection());
|
||||
} catch {} finally {
|
||||
this.pollInFlight = false;
|
||||
}
|
||||
}
|
||||
projection() {
|
||||
return {
|
||||
...this.form.shell(),
|
||||
|
|
@ -623,7 +786,8 @@ class NetxopsCardController {
|
|||
apiToken: this.form.field(API_TOKEN_FIELD),
|
||||
apiTokenConfigured: this.credential.configured,
|
||||
apiTokenWritable: this.credential.remoteReady && this.credential.writable,
|
||||
apiTokenRemoteReady: this.credential.remoteReady
|
||||
apiTokenRemoteReady: this.credential.remoteReady,
|
||||
alarmPushStatus: this.alarmPushStatus
|
||||
};
|
||||
}
|
||||
credentials() {
|
||||
|
|
@ -695,6 +859,15 @@ var en = {
|
|||
langHint: "Response language hint (zh / en).",
|
||||
alarmPushEnabled: "Key-alarm push",
|
||||
alarmPushEnabledHint: "Dial out to netx and open/follow a DSH session when a matched key alarm arrives. WhatsApp/im is optional and not required.",
|
||||
alarmPushStatus: "Push link",
|
||||
alarmPushPhaseDisabled: "Off",
|
||||
alarmPushPhaseIdle: "Idle",
|
||||
alarmPushPhaseConnecting: "Connecting…",
|
||||
alarmPushPhaseAuthenticating: "Authenticating…",
|
||||
alarmPushPhaseConnected: "Connected",
|
||||
alarmPushPhaseReconnecting: "Reconnecting…",
|
||||
alarmPushPhaseAuthFailed: "Auth failed",
|
||||
alarmPushPhaseError: "Error",
|
||||
apiToken: "API token",
|
||||
apiTokenHint: "Stored as credential NETX_API_TOKEN (never written into settings). Leave blank to keep the current token.",
|
||||
apiTokenUnavailable: "This DSH build does not expose remote.credentials. Set the token with scripts/set-netx-token.ps1 (or .sh), then restart is not required if credentials are watched.",
|
||||
|
|
@ -721,6 +894,15 @@ var zh = {
|
|||
langHint: "响应语言提示(zh / en)。",
|
||||
alarmPushEnabled: "关键告警推送",
|
||||
alarmPushEnabledHint: "主动连接 netx;匹配到关键告警后在本机打开/续写一条 DSH 会话。不必安装 WhatsApp / im。",
|
||||
alarmPushStatus: "推送链路",
|
||||
alarmPushPhaseDisabled: "未开启",
|
||||
alarmPushPhaseIdle: "空闲",
|
||||
alarmPushPhaseConnecting: "连接中…",
|
||||
alarmPushPhaseAuthenticating: "鉴权中…",
|
||||
alarmPushPhaseConnected: "已连接",
|
||||
alarmPushPhaseReconnecting: "重连中…",
|
||||
alarmPushPhaseAuthFailed: "鉴权失败",
|
||||
alarmPushPhaseError: "异常",
|
||||
apiToken: "API Token",
|
||||
apiTokenHint: "写入凭据 NETX_API_TOKEN(不会进 settings)。留空表示保留已有 token。",
|
||||
apiTokenUnavailable: "当前 DSH 未提供 remote.credentials。请用 scripts/set-netx-token.ps1(或 .sh)写入 token;若 harness 在监视凭据文件则无需重启。",
|
||||
|
|
@ -762,6 +944,17 @@ function apply(ctx) {
|
|||
};
|
||||
}, "netxops: credential invalidations");
|
||||
});
|
||||
ctx.inject(["connection"], (connCtx) => {
|
||||
const call = connCtx.connection?.rpc?.call?.bind(connCtx.connection.rpc);
|
||||
if (typeof call !== "function") {
|
||||
connCtx.logger.warn("netxops: connection.rpc.call unavailable — alarm status UI disabled");
|
||||
return;
|
||||
}
|
||||
card.setAlarmPushRpc(call);
|
||||
connCtx.effect(() => () => {
|
||||
card.setAlarmPushRpc(undefined);
|
||||
}, "netxops: clear alarm-push rpc");
|
||||
});
|
||||
ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
|
||||
name: "settings.plugin.item",
|
||||
key: NETXOPS_NS,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue