window.__ModuleLoader__.load({ id: "dsh-netxops", factory: (require) => { var module = { exports: {} }; var exports = module.exports; Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); var __defProp = Object.defineProperty; var __getOwnPropNames = Object.getOwnPropertyNames; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __hasOwnProp = Object.prototype.hasOwnProperty; function __accessProp(key) { return this[key]; } var __toCommonJS = (from) => { var entry = (__moduleCache ??= new WeakMap).get(from), desc; if (entry) return entry; entry = __defProp({}, "__esModule", { value: true }); if (from && typeof from === "object" || typeof from === "function") { for (var key of __getOwnPropNames(from)) if (!__hasOwnProp.call(entry, key)) __defProp(entry, key, { get: __accessProp.bind(from, key), enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } __moduleCache.set(from, entry); return entry; }; var __moduleCache; var __returnValue = (v) => v; function __exportSetter(name, newValue) { this[name] = __returnValue.bind(null, newValue); } var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true, configurable: true, set: __exportSetter.bind(all, name) }); }; // src/client/index.ts var exports_client = {}; __export(exports_client, { inject: () => inject, apply: () => apply }); 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/im-delivery-catalog.ts var IM_DELIVERY_CATALOG_ENDPOINT = "im-delivery.catalog"; var EMPTY_IM_DELIVERY_CATALOG = { available: true, options: [], hint: "", loading: false }; function asImDeliveryCatalog(value) { if (value === null || typeof value !== "object" || Array.isArray(value)) { return { ...EMPTY_IM_DELIVERY_CATALOG, available: false }; } const row = value; const raw = Array.isArray(row.options) ? row.options : []; const options = []; for (const entry of raw) { if (entry === null || typeof entry !== "object" || Array.isArray(entry)) continue; const item = entry; const botId = typeof item.botId === "string" ? item.botId.trim() : ""; const targetId = typeof item.targetId === "string" ? item.targetId.trim() : ""; if (!botId || !targetId) continue; options.push({ botId, targetId, name: typeof item.name === "string" && item.name.trim() ? item.name.trim() : targetId, kind: typeof item.kind === "string" ? item.kind : "", channel: typeof item.channel === "string" ? item.channel : "im" }); } return { available: row.available !== false, options, hint: typeof row.hint === "string" ? row.hint : "", loading: false }; } async function fetchImDeliveryCatalog(call, signal) { const result = await call(NETXOPS_RPC_CHANNEL, IM_DELIVERY_CATALOG_ENDPOINT, {}, signal); if (result !== null && typeof result === "object" && result.ok === true) { return asImDeliveryCatalog(result.value); } if (result !== null && typeof result === "object" && result.ok === false) { return { ...EMPTY_IM_DELIVERY_CATALOG, available: false, hint: String(result.error?.message ?? "rpc failed") }; } return asImDeliveryCatalog(result); } function imCatalogOptionKey(botId, targetId) { return `${botId}::${targetId}`; } function parseImCatalogOptionKey(value) { const at = value.indexOf("::"); if (at <= 0) return { botId: "", targetId: "" }; return { botId: value.slice(0, at), targetId: value.slice(at + 2) }; } // src/client/styles.ts var STYLE_ID = "dsh-netxops-card-css"; var CSS = ` .dsh-nx-card{list-style:none;border:.5px solid var(--dsw-alias-border-l4);border-radius:16px;background:var(--dsw-alias-bg-layer-3);transition:border-color .16s,background .16s} .dsh-nx-card:hover{border-color:var(--dsw-alias-label-dimmed)} .dsh-nx-cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)} .dsh-nx-header{width:100%;appearance:none;border:0;background:none;font:inherit;color:inherit;text-align:left;cursor:pointer;display:flex;align-items:center;gap:12px;padding:14px 16px;border-radius:12px} .dsh-nx-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px} .dsh-nx-headText{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px} .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} .dsh-nx-readOnly{margin:12px 0 0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary)} .dsh-nx-field{display:flex;flex-direction:column;gap:6px;padding:12px 0} .dsh-nx-field+.dsh-nx-field{border-top:.5px solid var(--dsw-alias-border-l2)} .dsh-nx-fieldHead{display:flex;align-items:center;gap:8px} .dsh-nx-label{flex:1;min-width:0;font-size:13px;font-weight:500;line-height:1.5;color:var(--dsw-alias-label-primary)} .dsh-nx-badges{display:inline-flex;align-items:center;gap:8px} .dsh-nx-badge{border-radius:999px;padding:1px 8px;font-size:11px;line-height:17px;white-space:nowrap;font-weight:500;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)} .dsh-nx-badgeMuted{border-radius:999px;padding:1px 8px;font-size:11px;line-height:17px;white-space:nowrap;color:var(--dsw-alias-label-tertiary)} .dsh-nx-reset{border:none;background:none;padding:0;font:inherit;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-secondary);cursor:pointer} .dsh-nx-reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)} .dsh-nx-input{height:34px;padding:0 12px;border:.5px solid var(--dsw-alias-border-l4);border-radius:8px;background:var(--dsw-alias-bg-layer-3);font:inherit;font-size:13px;line-height:1.5;color:var(--dsw-alias-label-primary)} .dsh-nx-select{appearance:auto;cursor:pointer} .dsh-nx-input:focus-visible{outline:none;border-color:var(--dsw-alias-brand-primary)} .dsh-nx-inputInvalid{border-color:var(--dsw-alias-label-error)} .dsh-nx-hint{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary)} .dsh-nx-checkRow{display:flex;align-items:flex-start;gap:10px;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-secondary);cursor:pointer} .dsh-nx-checkRow input{margin-top:2px;flex:none} .dsh-nx-groupBlock{display:flex;flex-direction:column;gap:8px;padding:8px 0 4px} .dsh-nx-groupTitle{font-size:12px;font-weight:600;line-height:1.5;color:var(--dsw-alias-label-primary)} .dsh-nx-groupChecks{display:flex;flex-direction:column;gap:6px;padding-left:2px} .dsh-nx-invalid{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-error)} .dsh-nx-footer{display:flex;align-items:center;justify-content:flex-end;gap:8px;padding:12px 0 4px;border-top:.5px solid var(--dsw-alias-border-l2)} .dsh-nx-failed{flex:1;min-width:0;margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-error)} .dsh-nx-btn{appearance:none;border:1px solid transparent;border-radius:8px;padding:5px 14px;font:inherit;font-size:13px;line-height:1.5;cursor:pointer} .dsh-nx-discard{border-color:var(--dsw-alias-border-l2);background:none;color:var(--dsw-alias-label-secondary)} .dsh-nx-discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)} .dsh-nx-save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)} .dsh-nx-btn:disabled{opacity:.4;cursor:default} `; function ensureStyles() { if (typeof document === "undefined") return; if (document.getElementById(STYLE_ID)) return; const tag = document.createElement("style"); tag.id = STYLE_ID; tag.textContent = CSS; document.head.appendChild(tag); } // 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", children: [ /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-fieldHead", children: [ /* @__PURE__ */ jsx_runtime.jsx("label", { className: "dsh-nx-label", htmlFor: props.id, children: props.label }), props.field.overridden ? /* @__PURE__ */ jsx_runtime.jsxs("span", { className: "dsh-nx-badges", children: [ /* @__PURE__ */ jsx_runtime.jsx("span", { className: "dsh-nx-badge", children: props.overriddenLabel }), /* @__PURE__ */ jsx_runtime.jsx("button", { type: "button", className: "dsh-nx-reset", disabled: props.disabled, onClick: props.onReset, children: props.resetLabel }) ] }) : null ] }), /* @__PURE__ */ jsx_runtime.jsx("input", { id: props.id, className: props.field.invalid ? "dsh-nx-input dsh-nx-inputInvalid" : "dsh-nx-input", type: "text", value: props.field.text, disabled: props.disabled, "aria-invalid": props.field.invalid || undefined, onChange: (event) => { props.onEdit(event.target.value); } }), /* @__PURE__ */ jsx_runtime.jsx("p", { className: props.field.invalid ? "dsh-nx-invalid" : "dsh-nx-hint", children: props.field.invalid ? props.invalidLabel : props.hint }) ] }); } function ImDeliveryPicker(props) { const options = props.catalog.options; const selectedKey = props.botId && props.targetId ? imCatalogOptionKey(props.botId, props.targetId) : ""; const matched = options.some((row) => imCatalogOptionKey(row.botId, row.targetId) === selectedKey); const [manual, setManual] = import_react.useState(() => !!(selectedKey && !matched)); import_react.useEffect(() => { if (selectedKey && matched) setManual(false); else if (selectedKey && !matched) setManual(true); }, [selectedKey, matched]); const selectValue = manual ? "__manual__" : matched ? selectedKey : ""; const showManual = manual || selectedKey && !matched; return /* @__PURE__ */ jsx_runtime.jsxs(jsx_runtime.Fragment, { children: [ /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-field", children: [ /* @__PURE__ */ jsx_runtime.jsx("div", { className: "dsh-nx-fieldHead", children: /* @__PURE__ */ jsx_runtime.jsx("label", { className: "dsh-nx-label", htmlFor: "netxops-im-target", children: props.labels.target }) }), /* @__PURE__ */ jsx_runtime.jsxs("select", { id: "netxops-im-target", className: "dsh-nx-input dsh-nx-select", value: selectValue, disabled: props.disabled, onChange: (event) => { const value = event.target.value; if (value === "__manual__") { setManual(true); return; } setManual(false); if (!value) { props.onPick("", ""); return; } const next = parseImCatalogOptionKey(value); props.onPick(next.botId, next.targetId); }, children: [ /* @__PURE__ */ jsx_runtime.jsx("option", { value: "", children: options.length === 0 ? props.labels.none : `— ${props.labels.target} —` }), options.map((row) => /* @__PURE__ */ jsx_runtime.jsx("option", { value: imCatalogOptionKey(row.botId, row.targetId), children: `${row.name} · ${row.channel || "im"} · ${row.targetId}` }, imCatalogOptionKey(row.botId, row.targetId))), /* @__PURE__ */ jsx_runtime.jsx("option", { value: "__manual__", children: props.labels.manual }) ] }), !props.catalog.available || options.length === 0 ? /* @__PURE__ */ jsx_runtime.jsx("p", { className: "dsh-nx-hint", children: props.catalog.hint || props.labels.unavailable }) : null ] }), showManual ? /* @__PURE__ */ jsx_runtime.jsxs(jsx_runtime.Fragment, { children: [ /* @__PURE__ */ jsx_runtime.jsx(ValueField, { id: "netxops-im-bot-id", label: props.labels.botId, hint: props.labels.botHint, field: props.botField, overriddenLabel: props.labels.overridden, resetLabel: props.labels.reset, invalidLabel: props.labels.invalid, disabled: props.disabled, onEdit: props.onEditBot, onReset: props.onResetBot }), /* @__PURE__ */ jsx_runtime.jsx(ValueField, { id: "netxops-im-target-id", label: props.labels.targetId, hint: props.labels.targetHint, field: props.targetField, overriddenLabel: props.labels.overridden, resetLabel: props.labels.reset, invalidLabel: props.labels.invalid, disabled: props.disabled, onEdit: props.onEditTarget, onReset: props.onResetTarget }) ] }) : null ] }); } function CapabilityGroupBlock(props) { return /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-groupBlock", children: [ /* @__PURE__ */ jsx_runtime.jsx("div", { className: "dsh-nx-groupTitle", children: props.title }), /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-groupChecks", children: [ /* @__PURE__ */ jsx_runtime.jsxs("label", { className: "dsh-nx-checkRow", children: [ /* @__PURE__ */ jsx_runtime.jsx("input", { type: "checkbox", checked: props.inPreset.text === "true", disabled: props.disabled, onChange: (event) => { props.onEditInPreset(event.target.checked); } }), /* @__PURE__ */ jsx_runtime.jsx("span", { children: props.inPresetLabel }) ] }), /* @__PURE__ */ jsx_runtime.jsxs("label", { className: "dsh-nx-checkRow", children: [ /* @__PURE__ */ jsx_runtime.jsx("input", { type: "checkbox", checked: props.published.text === "true", disabled: props.disabled, onChange: (event) => { props.onEditPublic(event.target.checked); } }), /* @__PURE__ */ jsx_runtime.jsx("span", { children: props.publicLabel }) ] }) ] }) ] }); } function NetxopsCard(props) { ensureStyles(); const { t } = props; const state = props.useNetxopsCard((snapshot) => snapshot); const [open, setOpen] = import_react.useState(false); const saveStarted = import_react.useRef(false); import_react.useEffect(() => { if (state.saving) { saveStarted.current = true; return; } if (!saveStarted.current) return; saveStarted.current = false; if (!state.dirty && !state.failed) setOpen(false); }, [state.dirty, state.failed, state.saving]); if (!state.available) return null; 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: [ /* @__PURE__ */ jsx_runtime.jsxs("button", { type: "button", className: "dsh-nx-header", "aria-expanded": open, "aria-label": `${t(open ? "collapse" : "expand")}: ${title}`, onClick: () => { setOpen(!open); }, children: [ /* @__PURE__ */ jsx_runtime.jsxs("span", { className: "dsh-nx-headText", children: [ /* @__PURE__ */ jsx_runtime.jsx("span", { className: "dsh-nx-name", children: title }), /* @__PURE__ */ jsx_runtime.jsx("span", { className: "dsh-nx-desc", children: t("description") }) ] }), 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") }) : null, /* @__PURE__ */ jsx_runtime.jsx("span", { className: open ? "dsh-nx-chevron dsh-nx-chevronOpen" : "dsh-nx-chevron", "aria-hidden": true, children: "▾" }) ] }), open ? /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-body", children: [ !state.writable ? /* @__PURE__ */ jsx_runtime.jsx("p", { className: "dsh-nx-readOnly", role: "status", children: t("readOnly") }) : null, /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-field", children: [ /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-fieldHead", children: [ /* @__PURE__ */ jsx_runtime.jsx("label", { className: "dsh-nx-label", htmlFor: "netxops-api-token", children: t("apiToken") }), /* @__PURE__ */ jsx_runtime.jsx("span", { className: "dsh-nx-badges", children: /* @__PURE__ */ jsx_runtime.jsx("span", { className: state.apiTokenConfigured ? "dsh-nx-badge" : "dsh-nx-badgeMuted", children: state.apiTokenConfigured ? t("apiTokenSet") : t("apiTokenUnset") }) }) ] }), /* @__PURE__ */ jsx_runtime.jsx("input", { id: "netxops-api-token", className: "dsh-nx-input", type: "password", autoComplete: "off", value: state.apiToken.text, disabled: !state.apiTokenWritable, onChange: (event) => { props.edit("apiToken", event.target.value); } }), /* @__PURE__ */ jsx_runtime.jsx("p", { className: "dsh-nx-hint", children: state.apiTokenRemoteReady ? t("apiTokenHint") : t("apiTokenUnavailable") }) ] }), /* @__PURE__ */ jsx_runtime.jsx(ValueField, { id: "netxops-api-url", label: t("apiUrl"), hint: t("apiUrlHint"), field: state.apiUrl, overriddenLabel: t("overridden"), resetLabel: t("reset"), invalidLabel: t("invalid"), disabled, onEdit: (text) => { props.edit("apiUrl", text); }, onReset: () => { props.resetField("apiUrl"); } }), /* @__PURE__ */ jsx_runtime.jsx(ValueField, { id: "netxops-lang", label: t("lang"), hint: t("langHint"), field: state.lang, overriddenLabel: t("overridden"), resetLabel: t("reset"), invalidLabel: t("invalid"), disabled, onEdit: (text) => { props.edit("lang", text); }, onReset: () => { props.resetField("lang"); } }), /* @__PURE__ */ jsx_runtime.jsx(ValueField, { id: "netxops-nms-provider", label: t("nmsProvider"), hint: t("nmsProviderHint"), field: state.nmsProvider, overriddenLabel: t("overridden"), resetLabel: t("reset"), invalidLabel: t("invalid"), disabled, onEdit: (text) => { props.edit("nmsProvider", text); }, onReset: () => { props.resetField("nmsProvider"); } }), /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-field", children: [ /* @__PURE__ */ jsx_runtime.jsx("div", { className: "dsh-nx-fieldHead", children: /* @__PURE__ */ jsx_runtime.jsx("span", { className: "dsh-nx-label", children: t("capabilityGroups") }) }), /* @__PURE__ */ jsx_runtime.jsx("p", { className: "dsh-nx-hint", children: t("capabilityGroupsHint") }), /* @__PURE__ */ jsx_runtime.jsx(CapabilityGroupBlock, { title: t("groupOps"), inPresetLabel: t("groupInPreset"), publicLabel: t("groupPublic"), inPreset: state.groupOpsInPreset, published: state.groupOpsPublic, disabled, onEditInPreset: (checked) => { props.edit("groupOpsInPreset", checked ? "true" : "false"); }, onEditPublic: (checked) => { props.edit("groupOpsPublic", checked ? "true" : "false"); } }), /* @__PURE__ */ jsx_runtime.jsx(CapabilityGroupBlock, { title: t("groupTopology"), inPresetLabel: t("groupInPreset"), publicLabel: t("groupPublic"), inPreset: state.groupTopologyInPreset, published: state.groupTopologyPublic, disabled, onEditInPreset: (checked) => { props.edit("groupTopologyInPreset", checked ? "true" : "false"); }, onEditPublic: (checked) => { props.edit("groupTopologyPublic", checked ? "true" : "false"); } }) ] }), /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-field", children: [ /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-fieldHead", children: [ /* @__PURE__ */ jsx_runtime.jsx("label", { className: "dsh-nx-label", htmlFor: "netxops-alarm-push", children: t("alarmPushEnabled") }), /* @__PURE__ */ jsx_runtime.jsxs("span", { className: "dsh-nx-badges", children: [ 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 ] }) ] }), /* @__PURE__ */ jsx_runtime.jsxs("label", { className: "dsh-nx-checkRow", htmlFor: "netxops-alarm-push", children: [ /* @__PURE__ */ jsx_runtime.jsx("input", { id: "netxops-alarm-push", type: "checkbox", checked: state.alarmPushEnabled.text === "true", disabled, onChange: (event) => { props.edit("alarmPushEnabled", event.target.checked ? "true" : "false"); } }), /* @__PURE__ */ jsx_runtime.jsx("span", { 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", { className: "dsh-nx-field", children: [ /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-fieldHead", children: [ /* @__PURE__ */ jsx_runtime.jsx("label", { className: "dsh-nx-label", htmlFor: "netxops-alarm-dsh", children: t("alarmDeliverDsh") }), state.alarmDeliverDsh.overridden ? /* @__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("alarmDeliverDsh"); }, children: t("reset") }) ] }) : null ] }), /* @__PURE__ */ jsx_runtime.jsxs("label", { className: "dsh-nx-checkRow", htmlFor: "netxops-alarm-dsh", children: [ /* @__PURE__ */ jsx_runtime.jsx("input", { id: "netxops-alarm-dsh", type: "checkbox", checked: state.alarmDeliverDsh.text === "true", disabled, onChange: (event) => { props.edit("alarmDeliverDsh", event.target.checked ? "true" : "false"); } }), /* @__PURE__ */ jsx_runtime.jsx("span", { children: t("alarmDeliverDshHint") }) ] }) ] }), /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-field", children: [ /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-fieldHead", children: [ /* @__PURE__ */ jsx_runtime.jsx("label", { className: "dsh-nx-label", htmlFor: "netxops-alarm-im", children: t("alarmDeliverIm") }), state.alarmDeliverIm.overridden ? /* @__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("alarmDeliverIm"); }, children: t("reset") }) ] }) : null ] }), /* @__PURE__ */ jsx_runtime.jsxs("label", { className: "dsh-nx-checkRow", htmlFor: "netxops-alarm-im", children: [ /* @__PURE__ */ jsx_runtime.jsx("input", { id: "netxops-alarm-im", type: "checkbox", checked: state.alarmDeliverIm.text === "true", disabled, onChange: (event) => { props.edit("alarmDeliverIm", event.target.checked ? "true" : "false"); } }), /* @__PURE__ */ jsx_runtime.jsx("span", { children: t("alarmDeliverImHint") }) ] }) ] }), /* @__PURE__ */ jsx_runtime.jsx(ImDeliveryPicker, { catalog: state.imDeliveryCatalog, botId: state.imBotId.text, targetId: state.imTargetId.text, disabled, labels: { target: t("imTarget"), none: t("imTargetNone"), manual: t("imTargetManual"), unavailable: t("imCatalogUnavailable"), botId: t("imBotId"), botHint: t("imBotIdHint"), targetId: t("imTargetId"), targetHint: t("imTargetIdHint"), overridden: t("overridden"), reset: t("reset"), invalid: t("invalid") }, botField: state.imBotId, targetField: state.imTargetId, onPick: (botId, targetId) => { props.edit("imBotId", botId); props.edit("imTargetId", targetId); }, onEditBot: (text) => { props.edit("imBotId", text); }, onEditTarget: (text) => { props.edit("imTargetId", text); }, onResetBot: () => { props.resetField("imBotId"); }, onResetTarget: () => { props.resetField("imTargetId"); } }), /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-footer", children: [ state.failed ? /* @__PURE__ */ jsx_runtime.jsx("p", { className: "dsh-nx-failed", role: "status", children: t("saveFailed") }) : null, /* @__PURE__ */ jsx_runtime.jsx("button", { type: "button", className: "dsh-nx-btn dsh-nx-discard", disabled: !state.dirty || state.saving, onClick: props.discard, children: t("discard") }), /* @__PURE__ */ jsx_runtime.jsx("button", { type: "button", className: "dsh-nx-btn dsh-nx-save", disabled: blocked, onClick: props.save, children: t(state.saving ? "saving" : "save") }) ] }) ] }) : null ] }); } // src/client/snapshot-store.ts function createSnapshotStore(init) { let state = init; const listeners = new Set; return { getSnapshot: () => state, subscribe: (listener) => { listeners.add(listener); return () => { listeners.delete(listener); }; }, set: (next) => { state = next; for (const listener of [...listeners]) listener(); } }; } // src/client/card-form.ts function textField(field) { return { field, format: (value) => typeof value === "string" ? value : "", parse: (text) => { const trimmed = text.trim(); return trimmed === "" ? { kind: "clear" } : { kind: "set", value: trimmed }; } }; } function booleanField(field) { return { field, format: (value) => value === true ? "true" : "false", parse: (text) => { const normalized = text.trim().toLowerCase(); if (normalized === "true" || normalized === "1" || normalized === "yes") { return { kind: "set", value: true }; } if (normalized === "false" || normalized === "0" || normalized === "no" || normalized === "") { return { kind: "clear" }; } return; } }; } function booleanFieldPersistFalse(field) { return { field, format: (value) => value === true ? "true" : "false", parse: (text) => { const normalized = text.trim().toLowerCase(); if (normalized === "true" || normalized === "1" || normalized === "yes") { return { kind: "set", value: true }; } if (normalized === "false" || normalized === "0" || normalized === "no" || normalized === "") { return { kind: "set", value: false }; } return; } }; } class CardForm { scope; specs; secretSpecs; staged = new Map; listeners = new Set; saving = false; failed = false; constructor(scope, specs, secrets = []) { this.scope = scope; this.specs = new Map(specs.map((spec) => [spec.field, spec])); this.secretSpecs = new Map(secrets.map((spec) => [spec.field, spec])); scope.subscribe(() => { this.publish(); }); } bind(project) { const store = createSnapshotStore(project()); this.listeners.add(() => { store.set(project()); }); return store; } shell() { const snapshot = this.scope.getSnapshot(); const plan = this.plan(); return { available: snapshot.status === "ready", writable: snapshot.writable, dirty: plan.length > 0, invalid: plan.some((item) => item.run === undefined), saving: this.saving, failed: this.failed }; } field(field) { const staged = this.staged.get(field); if (this.secretSpecs.has(field)) { return { text: staged?.text ?? "", overridden: false, invalid: false }; } const spec = this.spec(field); if (staged === undefined) { return { text: spec.format(this.sectionValue(field)), overridden: this.stored(field), invalid: false }; } const write = staged.clear ? { kind: "clear" } : spec.parse(staged.text); return { text: staged.text, overridden: write?.kind === "set", invalid: write === undefined }; } actions() { return { edit: (field, text) => { this.stage(field, { text, clear: false }); }, resetField: (field) => { this.stage(field, { text: this.spec(field).format(this.baseValue(field)), clear: true }); }, save: () => { this.save(); }, discard: () => { if (this.staged.size === 0 && !this.failed) return; this.staged.clear(); this.failed = false; this.publish(); } }; } async save() { const plan = this.plan(); const writes = plan.flatMap((item) => item.run === undefined ? [] : [item.run]); if (plan.length === 0 || this.saving || writes.length !== plan.length) return; this.saving = true; this.failed = false; this.publish(); let landed = true; for (const write of writes) { landed = await write() && landed; } if (landed) this.staged.clear(); this.saving = false; this.failed = !landed; this.publish(); } plan() { const plan = []; for (const [field, staged] of this.staged) { const secret = this.secretSpecs.get(field); if (secret !== undefined) { const value = staged.text.trim(); if (value !== "") plan.push({ field, run: () => secret.write(value) }); continue; } const spec = this.spec(field); if (staged.clear) { if (this.stored(field)) plan.push({ field, run: () => this.clear(field) }); continue; } if (staged.text === spec.format(this.sectionValue(field))) continue; const write = spec.parse(staged.text); if (write === undefined) plan.push({ field, run: undefined }); else if (write.kind === "clear") plan.push({ field, run: () => this.clear(field) }); else plan.push({ field, run: () => this.store(field, write.value) }); } return plan; } async clear(field) { await this.scope.unset(field); return !this.stored(field); } async store(field, value) { await this.scope.set(field, value); return this.userLayer()?.[field] === value; } stage(field, edit) { this.staged.set(field, edit); this.failed = false; this.publish(); } spec(field) { const spec = this.specs.get(field); if (spec === undefined) throw new Error(`netxops card has no field ${field}`); return spec; } snapshotOf() { return this.scope.getSnapshot(); } sectionValue(field) { return this.snapshotOf().value?.[field]; } baseValue(field) { return this.snapshotOf().base?.[field]; } userLayer() { return this.snapshotOf().user; } stored(field) { const user = this.userLayer(); return user !== undefined && Object.hasOwn(user, field); } publish() { for (const listener of this.listeners) listener(); } } // src/client/controller.ts var NETXOPS_NS = "netxops"; var DEFAULT_TOKEN_REF = "NETX_API_TOKEN"; var API_TOKEN_FIELD = "apiToken"; var STATUS_POLL_MS = 2000; class NetxopsCardController { scope; ctx; form; store; credential = { ref: "", configured: false, writable: false, remoteReady: false }; rpcCall; alarmPushStatus = null; imDeliveryCatalog = { ...EMPTY_IM_DELIVERY_CATALOG }; pollTimer; pollInFlight = false; catalogInFlight = false; constructor(scope, ctx) { this.scope = scope; this.ctx = ctx; this.form = new CardForm(scope, [ textField("apiUrl"), textField("lang"), textField("nmsProvider"), booleanFieldPersistFalse("groupOpsInPreset"), booleanField("groupOpsPublic"), booleanField("groupTopologyInPreset"), booleanField("groupTopologyPublic"), booleanField("alarmPushEnabled"), booleanFieldPersistFalse("alarmDeliverDsh"), booleanField("alarmDeliverIm"), textField("imBotId"), textField("imTargetId") ], [{ field: API_TOKEN_FIELD, write: (text) => this.writeToken(text) }]); this.store = this.form.bind(() => this.projection()); scope.subscribe(() => { this.readCredential(); }); this.readCredential(); } setCredentialsAvailable(ready) { if (this.credential.remoteReady === ready) return; this.credential = { ...this.credential, remoteReady: ready, writable: ready }; this.store.set(this.projection()); if (ready) this.readCredential(); } setAlarmPushRpc(call) { this.rpcCall = call; if (call === undefined) { this.stopStatusPoll(); let changed = false; if (this.alarmPushStatus !== null) { this.alarmPushStatus = null; changed = true; } if (this.imDeliveryCatalog.options.length > 0 || this.imDeliveryCatalog.available !== true) { this.imDeliveryCatalog = { ...EMPTY_IM_DELIVERY_CATALOG }; changed = true; } if (changed) this.store.set(this.projection()); return; } this.startStatusPoll(); this.refreshAlarmPushStatus(); this.refreshImDeliveryCatalog(); } startStatusPoll() { if (this.pollTimer !== undefined) return; this.pollTimer = setInterval(() => { this.refreshAlarmPushStatus(); this.refreshImDeliveryCatalog(); }, 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; } } async refreshImDeliveryCatalog() { const call = this.rpcCall; if (call === undefined || this.catalogInFlight) return; this.catalogInFlight = true; try { const next = await fetchImDeliveryCatalog(call); const prev = this.imDeliveryCatalog; const sameOptions = prev.options.length === next.options.length && prev.options.every((row, index) => { const other = next.options[index]; return other && row.botId === other.botId && row.targetId === other.targetId && row.name === other.name && row.channel === other.channel; }); if (prev.available === next.available && prev.hint === next.hint && sameOptions) return; this.imDeliveryCatalog = next; this.store.set(this.projection()); } catch {} finally { this.catalogInFlight = false; } } projection() { return { ...this.form.shell(), apiUrl: this.form.field("apiUrl"), lang: this.form.field("lang"), nmsProvider: this.form.field("nmsProvider"), groupOpsInPreset: this.form.field("groupOpsInPreset"), groupOpsPublic: this.form.field("groupOpsPublic"), groupTopologyInPreset: this.form.field("groupTopologyInPreset"), groupTopologyPublic: this.form.field("groupTopologyPublic"), alarmPushEnabled: this.form.field("alarmPushEnabled"), alarmDeliverDsh: this.form.field("alarmDeliverDsh"), alarmDeliverIm: this.form.field("alarmDeliverIm"), imBotId: this.form.field("imBotId"), imTargetId: this.form.field("imTargetId"), apiToken: this.form.field(API_TOKEN_FIELD), apiTokenConfigured: this.credential.configured, apiTokenWritable: this.credential.remoteReady && this.credential.writable, apiTokenRemoteReady: this.credential.remoteReady, alarmPushStatus: this.alarmPushStatus, imDeliveryCatalog: this.imDeliveryCatalog }; } credentials() { return this.ctx.get("remote.credentials"); } async readCredential() { const ref = refOf(this.scope.getSnapshot()); const api = this.credentials(); if (api === undefined) { if (ref !== this.credential.ref || this.credential.remoteReady) { this.credential = { ref, configured: false, writable: false, remoteReady: false }; this.store.set(this.projection()); } return; } if (ref !== this.credential.ref) { this.credential = { ref, configured: false, writable: true, remoteReady: true }; this.store.set(this.projection()); } const response = await api.describe([ref]); if (!response.ok || ref !== refOf(this.scope.getSnapshot())) return; const view = response.value?.[ref]; const next = { ref, configured: view?.configured ?? false, writable: view?.writable ?? true, remoteReady: true }; if (next.configured === this.credential.configured && next.writable === this.credential.writable && next.remoteReady === this.credential.remoteReady) return; this.credential = next; this.store.set(this.projection()); } refreshCredential(ref) { if (ref !== this.credential.ref) return; this.readCredential(); } inject() { return { hooks: { netxopsCard: this.store }, ...this.form.actions() }; } async writeToken(value) { const api = this.credentials(); if (api === undefined) return false; await api.set(refOf(this.scope.getSnapshot()), value); await this.readCredential(); return this.credential.configured; } } function refOf(snapshot) { const declared = snapshot.value?.tokenCredentialRef; return declared !== undefined && declared.length > 0 ? declared : DEFAULT_TOKEN_REF; } // src/client/locales.ts var en = { title: "Netx Ops", description: "netx API, bearer token, and capability groups (ops / topology).", apiUrl: "API URL", apiUrlHint: "netx REST root, e.g. http://127.0.0.1:8890 (also used for alarm subscribe)", lang: "Language", langHint: "Response language hint (zh / en).", capabilityGroups: "Capability groups", capabilityGroupsHint: "One group ↔ one skill. ops = netx-ops (alarms + CLI login + paths); topology = netx-topology. Defaults: ops on; topology and public off. New sessions after save. Other agents: dsh-netxops/tools-ops|topology.", nmsProvider: "nms provider", nmsProviderHint: "Vendor adapter id. Supported today: zte-ume (REST still /v1/ume/*; model tools are netx__*Nms*).", groupOps: "ops — skill netx-ops (NMS alarms/inventory + managed CLI login + paths)", groupTopology: "topology — skill netx-topology (canvas / fabric / dual_unit)", groupInPreset: "In Netx Ops preset", groupPublic: "Publish to other presets", alarmPushEnabled: "Key-alarm push", alarmPushEnabledHint: "Dial out to netx when a matched key alarm arrives. Choose DSH and/or IM sinks below.", alarmPushStatus: "Push link", alarmPushPhaseDisabled: "Off", alarmPushPhaseIdle: "Idle", alarmPushPhaseConnecting: "Connecting…", alarmPushPhaseAuthenticating: "Authenticating…", alarmPushPhaseConnected: "Connected", alarmPushPhaseReconnecting: "Reconnecting…", alarmPushPhaseAuthFailed: "Auth failed", alarmPushPhaseError: "Error", alarmDeliverDsh: "Deliver to DSH session", alarmDeliverDshHint: "Open/follow the sticky「Netx 关键告警」session on this Host.", alarmDeliverIm: "Deliver to WhatsApp / IM", alarmDeliverImHint: "Requires dsh-im-ops ≥ops.24. Pick a saved delivery target below (create targets under IM → Delivery settings).", imBotId: "IM Bot ID", imBotIdHint: "Filled from the picker, or enter manually.", imTargetId: "IM Target ID", imTargetIdHint: "Filled from the picker, or enter manually.", imTarget: "Delivery target", imTargetNone: "(Create a target in IM delivery settings first)", imTargetManual: "Enter manually…", imCatalogUnavailable: "Cannot load targets — install/update dsh-im-ops (≥ops.24) and save at least one delivery target.", 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.", apiTokenSet: "Configured", apiTokenUnset: "Not set", overridden: "Overridden", reset: "Reset", invalid: "Invalid value", expand: "Expand", collapse: "Collapse", unsaved: "Unsaved", readOnly: "This document is read-only.", saveFailed: "Save failed — drafts kept for correction.", discard: "Discard", save: "Save", saving: "Saving…" }; var zh = { title: "Netx Ops", description: "netx API、Bearer Token,以及能力组(ops / topology)。", apiUrl: "API 地址", apiUrlHint: "netx REST 根地址,例如 http://127.0.0.1:8890(告警订阅复用同一地址)", lang: "语言", langHint: "响应语言提示(zh / en)。", capabilityGroups: "能力组", capabilityGroupsHint: "一组一个 skill。ops = netx-ops(告警 + 纳管登录 + 路径);topology = netx-topology。默认 ops 开;topology / 公开关。保存后新会话生效。其他 Agent:dsh-netxops/tools-ops|topology。", nmsProvider: "nms provider", nmsProviderHint: "厂商适配器 id。当前支持:zte-ume(REST 仍为 /v1/ume/*;模型工具名为 netx__*Nms*)。", groupOps: "ops — skill netx-ops(NMS 告警/库存 + 纳管 CLI 登录 + 路径)", groupTopology: "topology — skill netx-topology(画布 / Fabric / dual_unit)", groupInPreset: "在 Netx Ops 预设中启用", groupPublic: "对其他预设公开", alarmPushEnabled: "关键告警推送", alarmPushEnabledHint: "主动连接 netx;匹配到关键告警后按下述开关投递。可只开 DSH、只开 IM,或两者都开。", alarmPushStatus: "推送链路", alarmPushPhaseDisabled: "未开启", alarmPushPhaseIdle: "空闲", alarmPushPhaseConnecting: "连接中…", alarmPushPhaseAuthenticating: "鉴权中…", alarmPushPhaseConnected: "已连接", alarmPushPhaseReconnecting: "重连中…", alarmPushPhaseAuthFailed: "鉴权失败", alarmPushPhaseError: "异常", alarmDeliverDsh: "投递到 DSH 会话", alarmDeliverDshHint: "在本机打开/续写 sticky「Netx 关键告警」会话。", alarmDeliverIm: "投递到 WhatsApp / IM", alarmDeliverImHint: "需要 dsh-im-ops ≥ops.24。请在下方下拉选择已保存的投递目标(先在 IM「投递设置」新建)。", imBotId: "IM Bot ID", imBotIdHint: "由下拉自动填入,也可手动改。", imTargetId: "IM Target ID", imTargetIdHint: "由下拉自动填入,也可手动改。", imTarget: "投递目标", imTargetNone: "(请先在 IM「投递设置」新建目标)", imTargetManual: "手动填写…", imCatalogUnavailable: "无法加载投递目标 — 请安装/更新 dsh-im-ops(≥ops.24)并至少保存一个投递目标。", apiToken: "API Token", apiTokenHint: "写入凭据 NETX_API_TOKEN(不会进 settings)。留空表示保留已有 token。", apiTokenUnavailable: "当前 DSH 未提供 remote.credentials。请用 scripts/set-netx-token.ps1(或 .sh)写入 token;若 harness 在监视凭据文件则无需重启。", apiTokenSet: "已配置", apiTokenUnset: "未设置", overridden: "已覆盖", reset: "重置", invalid: "无效值", expand: "展开", collapse: "收起", unsaved: "未保存", readOnly: "当前文档只读。", saveFailed: "保存失败,草稿已保留以便修改。", discard: "丢弃", save: "保存", saving: "保存中…" }; // src/client/index.ts var LOCALE_NS = "settings.netxops"; var inject = [ "slots", "locale", "remote", "settingsScope" ]; function apply(ctx) { ctx.effect(() => ctx.locale.register(LOCALE_NS, { zh, en }), "netxops: locales"); const card = new NetxopsCardController(ctx.settingsScope.bind({ namespace: NETXOPS_NS }), ctx); ctx.inject(["remote.credentials"], (credCtx) => { card.setCredentialsAvailable(true); credCtx.effect(() => { const off = credCtx.remote.$on("credentials/reference-updated", (ref) => { card.refreshCredential(String(ref)); }); return () => { off(); card.setCredentialsAvailable(false); }; }, "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, locale: LOCALE_NS, inject: () => card.inject() }, NetxopsCard)); } return module.exports; } });