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/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 : "", reasonCode: row.reasonCode === "im_catalog_unavailable" || row.reasonCode === "rpc_failed" ? row.reasonCode : undefined, 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, reasonCode: "rpc_failed", hint: String(result.error?.message ?? "rpc failed") }; } return asImDeliveryCatalog(result); } function imCatalogOptionKey(botId, targetId) { return `${botId}::${targetId}`; } // src/netx/im-targets.ts function imTargetKey(botId, targetId) { return `${botId}::${targetId}`; } function normalizeImTarget(input) { if (input === null || typeof input !== "object" || Array.isArray(input)) return null; const row = input; const botId = typeof row.botId === "string" ? row.botId.trim() : ""; const targetId = typeof row.targetId === "string" ? row.targetId.trim() : ""; if (!botId || !targetId) return null; return { botId, targetId }; } function parseImTargetsJson(text) { const trimmed = text.trim(); if (!trimmed) return []; let parsed; try { parsed = JSON.parse(trimmed); } catch { return []; } if (!Array.isArray(parsed)) return []; const out = []; const seen = new Set; for (const entry of parsed) { const target = normalizeImTarget(entry); if (!target) continue; const key = imTargetKey(target.botId, target.targetId); if (seen.has(key)) continue; seen.add(key); out.push(target); } return out; } function formatImTargetsJson(targets) { const seen = new Set; const rows = []; for (const entry of targets) { const target = normalizeImTarget(entry); if (!target) continue; const key = imTargetKey(target.botId, target.targetId); if (seen.has(key)) continue; seen.add(key); rows.push(target); } return rows.length === 0 ? "" : JSON.stringify(rows); } function resolveImTargets(settings) { const fromList = parseImTargetsJson(typeof settings.imTargets === "string" ? settings.imTargets : ""); if (fromList.length > 0) return fromList; const legacy = normalizeImTarget({ botId: settings.imBotId, targetId: settings.imTargetId }); return legacy ? [legacy] : []; } function setImTargetSelected(current, target, selected) { const key = imTargetKey(target.botId, target.targetId); const without = current.filter((row) => imTargetKey(row.botId, row.targetId) !== key); if (!selected) return without; const normalized = normalizeImTarget(target); return normalized ? [...without, normalized] : without; } // src/netx/kb-manifest.ts var import_node_fs = (() => ({})); // node:path function assertPath(path) { if (typeof path !== "string") throw TypeError("Path must be a string. Received " + JSON.stringify(path)); } function normalizeStringPosix(path, allowAboveRoot) { var res = "", lastSegmentLength = 0, lastSlash = -1, dots = 0, code; for (var i = 0;i <= path.length; ++i) { if (i < path.length) code = path.charCodeAt(i); else if (code === 47) break; else code = 47; if (code === 47) { if (lastSlash === i - 1 || dots === 1) ; else if (lastSlash !== i - 1 && dots === 2) { if (res.length < 2 || lastSegmentLength !== 2 || res.charCodeAt(res.length - 1) !== 46 || res.charCodeAt(res.length - 2) !== 46) { if (res.length > 2) { var lastSlashIndex = res.lastIndexOf("/"); if (lastSlashIndex !== res.length - 1) { if (lastSlashIndex === -1) res = "", lastSegmentLength = 0; else res = res.slice(0, lastSlashIndex), lastSegmentLength = res.length - 1 - res.lastIndexOf("/"); lastSlash = i, dots = 0; continue; } } else if (res.length === 2 || res.length === 1) { res = "", lastSegmentLength = 0, lastSlash = i, dots = 0; continue; } } if (allowAboveRoot) { if (res.length > 0) res += "/.."; else res = ".."; lastSegmentLength = 2; } } else { if (res.length > 0) res += "/" + path.slice(lastSlash + 1, i); else res = path.slice(lastSlash + 1, i); lastSegmentLength = i - lastSlash - 1; } lastSlash = i, dots = 0; } else if (code === 46 && dots !== -1) ++dots; else dots = -1; } return res; } function _format(sep, pathObject) { var dir = pathObject.dir || pathObject.root, base = pathObject.base || (pathObject.name || "") + (pathObject.ext || ""); if (!dir) return base; if (dir === pathObject.root) return dir + base; return dir + sep + base; } function resolve() { var resolvedPath = "", resolvedAbsolute = false, cwd; for (var i = arguments.length - 1;i >= -1 && !resolvedAbsolute; i--) { var path; if (i >= 0) path = arguments[i]; else { if (cwd === undefined) cwd = process.cwd(); path = cwd; } if (assertPath(path), path.length === 0) continue; resolvedPath = path + "/" + resolvedPath, resolvedAbsolute = path.charCodeAt(0) === 47; } if (resolvedPath = normalizeStringPosix(resolvedPath, !resolvedAbsolute), resolvedAbsolute) if (resolvedPath.length > 0) return "/" + resolvedPath; else return "/"; else if (resolvedPath.length > 0) return resolvedPath; else return "."; } function normalize(path) { if (assertPath(path), path.length === 0) return "."; var isAbsolute = path.charCodeAt(0) === 47, trailingSeparator = path.charCodeAt(path.length - 1) === 47; if (path = normalizeStringPosix(path, !isAbsolute), path.length === 0 && !isAbsolute) path = "."; if (path.length > 0 && trailingSeparator) path += "/"; if (isAbsolute) return "/" + path; return path; } function isAbsolute(path) { return assertPath(path), path.length > 0 && path.charCodeAt(0) === 47; } function join() { if (arguments.length === 0) return "."; var joined; for (var i = 0;i < arguments.length; ++i) { var arg = arguments[i]; if (assertPath(arg), arg.length > 0) if (joined === undefined) joined = arg; else joined += "/" + arg; } if (joined === undefined) return "."; return normalize(joined); } function relative(from, to) { if (assertPath(from), assertPath(to), from === to) return ""; if (from = resolve(from), to = resolve(to), from === to) return ""; var fromStart = 1; for (;fromStart < from.length; ++fromStart) if (from.charCodeAt(fromStart) !== 47) break; var fromEnd = from.length, fromLen = fromEnd - fromStart, toStart = 1; for (;toStart < to.length; ++toStart) if (to.charCodeAt(toStart) !== 47) break; var toEnd = to.length, toLen = toEnd - toStart, length = fromLen < toLen ? fromLen : toLen, lastCommonSep = -1, i = 0; for (;i <= length; ++i) { if (i === length) { if (toLen > length) { if (to.charCodeAt(toStart + i) === 47) return to.slice(toStart + i + 1); else if (i === 0) return to.slice(toStart + i); } else if (fromLen > length) { if (from.charCodeAt(fromStart + i) === 47) lastCommonSep = i; else if (i === 0) lastCommonSep = 0; } break; } var fromCode = from.charCodeAt(fromStart + i), toCode = to.charCodeAt(toStart + i); if (fromCode !== toCode) break; else if (fromCode === 47) lastCommonSep = i; } var out = ""; for (i = fromStart + lastCommonSep + 1;i <= fromEnd; ++i) if (i === fromEnd || from.charCodeAt(i) === 47) if (out.length === 0) out += ".."; else out += "/.."; if (out.length > 0) return out + to.slice(toStart + lastCommonSep); else { if (toStart += lastCommonSep, to.charCodeAt(toStart) === 47) ++toStart; return to.slice(toStart); } } function _makeLong(path) { return path; } function dirname(path) { if (assertPath(path), path.length === 0) return "."; var code = path.charCodeAt(0), hasRoot = code === 47, end = -1, matchedSlash = true; for (var i = path.length - 1;i >= 1; --i) if (code = path.charCodeAt(i), code === 47) { if (!matchedSlash) { end = i; break; } } else matchedSlash = false; if (end === -1) return hasRoot ? "/" : "."; if (hasRoot && end === 1) return "//"; return path.slice(0, end); } function basename(path, ext) { if (ext !== undefined && typeof ext !== "string") throw TypeError('"ext" argument must be a string'); assertPath(path); var start = 0, end = -1, matchedSlash = true, i; if (ext !== undefined && ext.length > 0 && ext.length <= path.length) { if (ext.length === path.length && ext === path) return ""; var extIdx = ext.length - 1, firstNonSlashEnd = -1; for (i = path.length - 1;i >= 0; --i) { var code = path.charCodeAt(i); if (code === 47) { if (!matchedSlash) { start = i + 1; break; } } else { if (firstNonSlashEnd === -1) matchedSlash = false, firstNonSlashEnd = i + 1; if (extIdx >= 0) if (code === ext.charCodeAt(extIdx)) { if (--extIdx === -1) end = i; } else extIdx = -1, end = firstNonSlashEnd; } } if (start === end) end = firstNonSlashEnd; else if (end === -1) end = path.length; return path.slice(start, end); } else { for (i = path.length - 1;i >= 0; --i) if (path.charCodeAt(i) === 47) { if (!matchedSlash) { start = i + 1; break; } } else if (end === -1) matchedSlash = false, end = i + 1; if (end === -1) return ""; return path.slice(start, end); } } function extname(path) { assertPath(path); var startDot = -1, startPart = 0, end = -1, matchedSlash = true, preDotState = 0; for (var i = path.length - 1;i >= 0; --i) { var code = path.charCodeAt(i); if (code === 47) { if (!matchedSlash) { startPart = i + 1; break; } continue; } if (end === -1) matchedSlash = false, end = i + 1; if (code === 46) { if (startDot === -1) startDot = i; else if (preDotState !== 1) preDotState = 1; } else if (startDot !== -1) preDotState = -1; } if (startDot === -1 || end === -1 || preDotState === 0 || preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) return ""; return path.slice(startDot, end); } function format(pathObject) { if (pathObject === null || typeof pathObject !== "object") throw TypeError('The "pathObject" argument must be of type Object. Received type ' + typeof pathObject); return _format("/", pathObject); } function parse(path) { assertPath(path); var ret = { root: "", dir: "", base: "", ext: "", name: "" }; if (path.length === 0) return ret; var code = path.charCodeAt(0), isAbsolute2 = code === 47, start; if (isAbsolute2) ret.root = "/", start = 1; else start = 0; var startDot = -1, startPart = 0, end = -1, matchedSlash = true, i = path.length - 1, preDotState = 0; for (;i >= start; --i) { if (code = path.charCodeAt(i), code === 47) { if (!matchedSlash) { startPart = i + 1; break; } continue; } if (end === -1) matchedSlash = false, end = i + 1; if (code === 46) { if (startDot === -1) startDot = i; else if (preDotState !== 1) preDotState = 1; } else if (startDot !== -1) preDotState = -1; } if (startDot === -1 || end === -1 || preDotState === 0 || preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) { if (end !== -1) if (startPart === 0 && isAbsolute2) ret.base = ret.name = path.slice(1, end); else ret.base = ret.name = path.slice(startPart, end); } else { if (startPart === 0 && isAbsolute2) ret.name = path.slice(1, startDot), ret.base = path.slice(1, end); else ret.name = path.slice(startPart, startDot), ret.base = path.slice(startPart, end); ret.ext = path.slice(startDot, end); } if (startPart > 0) ret.dir = path.slice(0, startPart - 1); else if (isAbsolute2) ret.dir = "/"; return ret; } var sep = "/"; var delimiter = ":"; var posix = ((p) => (p.posix = p, p))({ resolve, normalize, isAbsolute, join, relative, _makeLong, dirname, basename, extname, format, parse, sep, delimiter, win32: null, posix: null }); // src/netx/kb-manifest.ts var EMPTY_CONTENT = { hasRegions: false, hasTheory: false, hasPacket: false, hasCommon: false, hasSkills: false }; function unconfiguredKbSnapshot() { return { status: "unconfigured", realRoot: "", operatorName: "", country: "", version: "", content: { ...EMPTY_CONTENT }, paths: {}, errorMessage: "" }; } // src/client/kb-status-view.ts var NETXOPS_RPC_CHANNEL2 = "/netxops"; var KB_STATUS_ENDPOINT = "kb.status"; var KB_RELOAD_ENDPOINT = "kb.reload"; var KB_RESOLVE_ENDPOINT = "kb.resolve"; function asKbSnapshot(value) { const empty = unconfiguredKbSnapshot(); if (value === null || typeof value !== "object" || Array.isArray(value)) return empty; const row = value; const status = row.status === "configured" || row.status === "error" || row.status === "unconfigured" ? row.status : "unconfigured"; const contentRaw = row.content; const content = { ...empty.content }; if (contentRaw !== null && typeof contentRaw === "object" && !Array.isArray(contentRaw)) { for (const [key, flag] of Object.entries(contentRaw)) { content[key] = flag === true; } } const pathsRaw = row.paths; const paths = {}; if (pathsRaw !== null && typeof pathsRaw === "object" && !Array.isArray(pathsRaw)) { for (const [key, value2] of Object.entries(pathsRaw)) { if (typeof value2 === "string" && value2.trim()) paths[key] = value2.trim(); } } return { status, realRoot: typeof row.realRoot === "string" ? row.realRoot : "", operatorName: typeof row.operatorName === "string" ? row.operatorName : "", country: typeof row.country === "string" ? row.country : "", version: typeof row.version === "string" ? row.version : "", content, paths, errorMessage: typeof row.errorMessage === "string" ? row.errorMessage : "" }; } function asRpcOk(result) { return result !== null && typeof result === "object" && result.ok === true; } async function fetchKbStatus(call, signal) { const result = await call(NETXOPS_RPC_CHANNEL2, KB_STATUS_ENDPOINT, {}, signal); if (asRpcOk(result)) { return asKbSnapshot(result.value); } if (result !== null && typeof result === "object" && result.ok === false) { const message = String(result.error?.message ?? "kb.status rpc failed"); return { ...unconfiguredKbSnapshot(), status: "error", errorMessage: message }; } return { ...unconfiguredKbSnapshot(), status: "error", errorMessage: "kb.status unavailable" }; } async function reloadKbStatus(call, signal) { const result = await call(NETXOPS_RPC_CHANNEL2, KB_RELOAD_ENDPOINT, {}, signal); if (asRpcOk(result)) { return asKbSnapshot(result.value); } return fetchKbStatus(call, signal); } async function resolveKbPath(call, path, signal) { let result = await call(NETXOPS_RPC_CHANNEL2, KB_RESOLVE_ENDPOINT, { path }, signal); if (!asRpcOk(result)) { result = await call(NETXOPS_RPC_CHANNEL2, KB_RESOLVE_ENDPOINT, { args: { path } }, signal); } if (asRpcOk(result)) { return asKbSnapshot(result.value); } return { ...unconfiguredKbSnapshot(), status: "error", errorMessage: "kb.resolve unavailable" }; } function kbStatusTone(status) { if (status === "configured") return "ok"; if (status === "error") return "err"; if (status === "unconfigured") return "warn"; return "neutral"; } // src/client/tools-status-view.ts var NETXOPS_RPC_CHANNEL3 = "/netxops"; var TOOLS_STATUS_ENDPOINT = "tools.status"; var EMPTY2 = { toolsEnabled: true, mounts: [], count: 0, version: "", groups: [], plane: "", tokenConfigured: null, apiUrl: "" }; function asToolsMountView(value) { if (value === null || typeof value !== "object" || Array.isArray(value)) return { ...EMPTY2 }; const row = value; const mount = row.mount; const connection = row.connection; let count = 0; let version = ""; let groups = []; let plane = ""; let error; if (mount !== null && typeof mount === "object" && !Array.isArray(mount)) { const m = mount; count = typeof m.count === "number" ? m.count : 0; version = typeof m.version === "string" ? m.version : ""; plane = typeof m.plane === "string" ? m.plane : ""; error = typeof m.error === "string" ? m.error : undefined; if (Array.isArray(m.groups)) { groups = m.groups.filter((g) => typeof g === "string"); } } let tokenConfigured = null; let apiUrl = ""; if (connection !== null && typeof connection === "object" && !Array.isArray(connection)) { const c = connection; tokenConfigured = c.tokenConfigured === true; apiUrl = typeof c.apiUrl === "string" ? c.apiUrl : ""; } const mounts = []; const names = new Set; if (Array.isArray(row.mounts)) { for (const raw of row.mounts) { if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue; const m = raw; const entry = { source: typeof m.source === "string" ? m.source : String(m.plane ?? ""), count: typeof m.count === "number" ? m.count : 0, groups: Array.isArray(m.groups) ? m.groups.filter((g) => typeof g === "string") : [], plane: typeof m.plane === "string" ? m.plane : "", error: typeof m.error === "string" ? m.error : undefined }; mounts.push(entry); if (Array.isArray(m.names)) { for (const name of m.names) if (typeof name === "string") names.add(name); } if (entry.error) error = entry.error; if (typeof m.version === "string" && m.version) version = m.version; } count = names.size; groups = [...new Set(mounts.flatMap((m) => m.groups))]; } const toolsEnabled = !(connection && typeof connection === "object" && connection.toolsEnabled === false); return { count, version, groups, plane, error, tokenConfigured, apiUrl, mounts, toolsEnabled }; } async function fetchToolsStatus(call, signal) { const result = await call(NETXOPS_RPC_CHANNEL3, TOOLS_STATUS_ENDPOINT, {}, signal); if (result !== null && typeof result === "object" && result.ok === true) { return asToolsMountView(result.value); } return { ...EMPTY2 }; } function toolsMountTone(view) { if (!view) return "neutral"; if (view.error) return "err"; if (!view.toolsEnabled) return "neutral"; if (view.count > 0) return "ok"; return "warn"; } // src/client/styles.ts var STYLE_ID = "dsh-netxops-settings-css"; var CSS = ` .dsh-nx-settings{box-sizing:border-box;display:flex;flex-direction:column;gap:16px;min-height:0;padding:4px 4px 24px;color:var(--dsw-alias-label-primary,#1f2329)} .dsh-nx-settings>header{display:flex;flex-direction:column;gap:4px} .dsh-nx-settings h2{margin:0;font-size:20px;font-weight:600;line-height:28px} .dsh-nx-settings-card{border:1px solid var(--dsw-alias-border-l2,#dee0e3);border-radius:12px;padding:16px;background:var(--dsw-alias-bg-layer-1,transparent)} .dsh-nx-settings-card h3{margin:0 0 12px;font-size:15px;font-weight:600;line-height:22px} .dsh-nx-card-head{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;margin-bottom:14px} .dsh-nx-card-head h3{margin:0} .dsh-nx-settings-field{display:flex;flex-direction:column;gap:4px;margin-bottom:12px} .dsh-nx-settings-field:last-child{margin-bottom:0} .dsh-nx-settings-field>label,.dsh-nx-field-label,.dsh-nx-field-head>label{font-size:13px;line-height:20px;color:var(--dsw-alias-label-secondary,#646a73)} .dsh-nx-field-head{display:flex;align-items:center;justify-content:space-between;gap:8px} .dsh-nx-field-head>label,.dsh-nx-field-head>.dsh-nx-field-label{flex:1;min-width:0} .dsh-nx-fieldGrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(230px,100%),1fr));gap:12px 20px} .dsh-nx-fieldGrid>.dsh-nx-settings-field{min-width:0;margin:0} .dsh-nx-toggleField>.dsh-nx-field-head{min-height:28px} .dsh-nx-settings-field input[type=text], .dsh-nx-settings-field input[type=password], .dsh-nx-settings-field input:not([type]), .dsh-nx-settings-field select{ box-sizing:border-box;width:100%;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2,#dfe1e5);border-radius:6px;font:inherit;font-size:13px;line-height:20px;background:var(--dsw-alias-bg-module-platform,#f4f5f7);color:inherit } .dsh-nx-settings-field input:focus-visible, .dsh-nx-settings-field select:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#3370ff);outline-offset:1px} .dsh-nx-settings-field input:disabled, .dsh-nx-settings-field select:disabled{opacity:.55;cursor:default} .dsh-nx-inputInvalid{border-color:var(--dsw-alias-state-error-primary,#d54941)!important} .dsh-nx-hint{margin:0;font-size:12px;line-height:1.5;overflow-wrap:anywhere;color:var(--dsw-alias-label-tertiary,#8f959e)} .dsh-nx-invalid{margin:0;font-size:12px;line-height:1.5;overflow-wrap:anywhere;color:var(--dsw-alias-state-error-primary,#d54941)} .dsh-nx-badges{display:inline-flex;align-items:center;gap:8px;flex:none} .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,#f4f5f7);color:var(--dsw-alias-label-secondary,#646a73)} .dsh-nx-badgeMuted{border-radius:999px;padding:1px 8px;font-size:11px;line-height:17px;white-space:nowrap;color:var(--dsw-alias-label-tertiary,#8f959e)} .dsh-nx-status{border-radius:999px;padding:1px 8px;font-size:11px;line-height:17px;font-weight:500;max-width:100%;overflow-wrap:anywhere;background:var(--dsw-alias-bg-module-platform,#f4f5f7);color:var(--dsw-alias-label-tertiary,#8f959e)} .dsh-nx-statusOk{color:var(--dsw-alias-state-success-primary,#20a162);background:rgba(32,161,98,.12)} .dsh-nx-statusWarn{color:var(--dsw-alias-state-warn-primary,#d97706);background:rgba(217,119,6,.12)} .dsh-nx-statusErr{color:var(--dsw-alias-state-error-primary,#d54941);background:rgba(213,73,65,.12)} .dsh-nx-reset{border:none;background:none;padding:0;font:inherit;font-size:12px;line-height:1.5;color:var(--dsw-alias-state-business-primary,#3370ff);cursor:pointer} .dsh-nx-reset:hover:not(:disabled){opacity:.85} .dsh-nx-reset:disabled{opacity:.4;cursor:default} .dsh-nx-checkRow{display:flex;align-items:flex-start;gap:10px;margin:0;font-size:13px;line-height:1.5;color:var(--dsw-alias-label-primary,#1f2329);cursor:pointer} .dsh-nx-checkRow input{margin:3px 0 0;flex:none} .dsh-nx-settings input[type=checkbox]{accent-color:var(--dsw-alias-state-business-primary,#3370ff)} .dsh-nx-groupBlock{display:flex;flex-direction:column;gap:10px;padding:12px 0} .dsh-nx-groupBlock+.dsh-nx-groupBlock{border-top:1px solid var(--dsw-alias-border-l1,#eef0f3)} .dsh-nx-groupTitle{display:flex;align-items:center;gap:8px;font-size:13px;font-weight:600;line-height:1.5;color:var(--dsw-alias-label-primary,#1f2329)} .dsh-nx-groupChecks{display:flex;flex-wrap:wrap;gap:8px 24px} .dsh-nx-targets{margin-top:0} .dsh-nx-diagnostics{padding-top:12px;border-top:1px solid var(--dsw-alias-border-l1,#eef0f3)} .dsh-nx-diagnostics[open] summary{margin-bottom:8px} .dsh-nx-diagnostics p+p{margin-top:4px} .dsh-nx-settings summary{cursor:pointer;font-size:12px;font-weight:500;line-height:1.6} .dsh-nx-targets[open] .dsh-nx-checkRow{padding:5px 0;flex-wrap:wrap} .dsh-nx-settings input:focus-visible,.dsh-nx-settings button:focus-visible,.dsh-nx-settings summary:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#3370ff);outline-offset:2px} .dsh-nx-settings>.dsh-nx-settings-actions{position:sticky;bottom:0;padding:12px;background:var(--dsw-alias-bg-base,#fff);border-top:1px solid var(--dsw-alias-border-l2,#dfe1e5);flex-wrap:wrap} .dsh-nx-settings-actions{display:flex;align-items:center;gap:8px} .dsh-nx-imTargetList{display:flex;flex-direction:column;gap:8px;padding:4px 0 2px} .dsh-nx-pathRow{display:flex;gap:8px;align-items:stretch} .dsh-nx-pathRow>input{flex:1;min-width:0} .dsh-nx-pathRow>.dsh-nx-btn{flex:none;align-self:stretch} .dsh-nx-settings-msg{font-size:12px;color:var(--dsw-alias-label-secondary,#646a73)} .dsh-nx-settings-msg.err{color:var(--dsw-alias-state-error-primary,#d54941)} .dsh-nx-settings-empty{padding:24px;text-align:center;color:var(--dsw-alias-label-tertiary,#8f959e);font-size:13px} .dsh-nx-btn{appearance:none;display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--dsw-alias-border-l2,#dfe1e5);border-radius:4px;padding:8px 12px;font:inherit;font-size:13px;line-height:20px;cursor:pointer;background:var(--dsw-alias-bg-module-platform,#f4f5f7);color:var(--dsw-alias-label-primary,#1f2329)} .dsh-nx-btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#f7f8fa)} .dsh-nx-btn:disabled{opacity:.4;cursor:default} .dsh-nx-btn-primary{background:var(--dsw-alias-state-business-primary,#3370ff);color:#fff;border-color:transparent} .dsh-nx-btn-primary:hover:not(:disabled){opacity:.9;background:var(--dsw-alias-state-business-primary,#3370ff)} .dsh-nx-btn-ghost{background:transparent} `; function ensureStyles() { if (typeof document === "undefined") return; const existing = document.getElementById(STYLE_ID); if (existing) { if (existing.textContent !== CSS) existing.textContent = CSS; return; } const tag = document.createElement("style"); tag.id = STYLE_ID; tag.textContent = CSS; document.head.appendChild(tag); } // src/session-export-shared.ts var NETXOPS_SESSIONS_EXPORT_PATH = "/api/netxops.sessions.export"; // src/client/sessions-export-view.ts var SESSIONS_EXPORT_STATUS_ENDPOINT = "sessions.export.status"; var EMPTY_STATUS = { available: false, sessionCount: 0, supportsRawArtifacts: false, reasonCode: "rpc_unavailable", reason: "RPC unavailable" }; var REASON_CODES = new Set([ "rpc_unavailable", "rpc_failed", "no_persistence", "no_raw_artifacts", "list_failed", "http_failed", "empty_body" ]); function asReasonCode(value) { return typeof value === "string" && REASON_CODES.has(value) ? value : undefined; } function sessionsExportReasonLocaleKey(code) { switch (code) { case "no_persistence": return "sessionsExportNoPersistence"; case "no_raw_artifacts": return "sessionsExportNoRawArtifacts"; case "list_failed": return "sessionsExportListFailed"; case "rpc_unavailable": return "sessionsExportRpcUnavailable"; case "rpc_failed": return "sessionsExportRpcFailed"; case "http_failed": return "sessionsExportHttpFailed"; case "empty_body": return "sessionsExportEmpty"; default: return "sessionsExportUnavailable"; } } function asSessionsExportStatus(value) { if (value === null || typeof value !== "object" || Array.isArray(value)) { return { ...EMPTY_STATUS }; } const row = value; return { available: row.available === true, sessionCount: typeof row.sessionCount === "number" ? row.sessionCount : 0, supportsRawArtifacts: row.supportsRawArtifacts === true, reasonCode: asReasonCode(row.reasonCode), reason: typeof row.reason === "string" ? row.reason : undefined }; } async function fetchSessionsExportStatus(call, signal) { const result = await call(NETXOPS_RPC_CHANNEL, SESSIONS_EXPORT_STATUS_ENDPOINT, {}, signal); if (result !== null && typeof result === "object" && result.ok === true) { return asSessionsExportStatus(result.value); } if (result !== null && typeof result === "object" && result.ok === false) { return { ...EMPTY_STATUS, reasonCode: "rpc_failed", reason: String(result.error?.message ?? "rpc failed") }; } return asSessionsExportStatus(result); } function hostBase() { const origin = globalThis.location?.origin; return origin !== undefined && origin !== "null" ? origin : "http://dsh.internal"; } function saveBlobDownload(blob, filename) { const objectUrl = URL.createObjectURL(blob); try { const anchor = document.createElement("a"); anchor.href = objectUrl; anchor.download = filename; anchor.rel = "noopener"; anchor.style.display = "none"; document.body.appendChild(anchor); anchor.click(); anchor.remove(); } finally { globalThis.setTimeout(() => { URL.revokeObjectURL(objectUrl); }, 60000); } } class SessionsExportDownloadError extends Error { code; status; detail; constructor(code, message, extra = {}) { super(message); this.name = "SessionsExportDownloadError"; this.code = code; this.status = extra.status; this.detail = extra.detail ?? ""; } } async function downloadAllSessionsExport(fetcher = fetch, save = saveBlobDownload) { const url = new URL(NETXOPS_SESSIONS_EXPORT_PATH, hostBase()); const response = await fetcher(url, { method: "GET", credentials: "include" }); if (!response.ok) { const fromHeader = response.headers.get("x-netxops-export-error") ?? ""; const detail = fromHeader || await response.text().catch(() => ""); throw new SessionsExportDownloadError("http_failed", `Export failed: HTTP ${response.status}${detail === "" ? "" : ` ${detail}`}`, { status: response.status, detail }); } const disposition = response.headers.get("content-disposition") ?? ""; const matched = /filename="([^"]+)"/i.exec(disposition); const filename = matched?.[1] && matched[1].length > 0 ? matched[1] : "dsh-sessions-export.zip"; const countHeader = response.headers.get("x-netxops-session-count"); const sessionCount = countHeader !== null && countHeader !== "" ? Number.parseInt(countHeader, 10) : 0; const blob = await response.blob(); if (blob.size <= 0) { throw new SessionsExportDownloadError("empty_body", "Export failed: empty ZIP body"); } save(blob, filename); return { filename, sessionCount: Number.isFinite(sessionCount) ? sessionCount : 0, bytes: blob.size }; } // src/netx/config-values.ts var volatileWrite = Symbol.for("cosmokit.volatile.write"); function readConfigValue(value) { if (value !== null && typeof value === "object" && volatileWrite in value && "get" in value && typeof value.get === "function") { return value.get(); } return value; } // src/netx/capability-groups.ts var DEFAULT_CAPABILITY_GROUPS = Object.freeze({ ops: Object.freeze({ inPreset: true, public: false }), topology: Object.freeze({ inPreset: false, public: false }), bizMonitor: Object.freeze({ inPreset: true, public: false }) }); var TOOLS_BY_GROUP = Object.freeze({ ops: Object.freeze([ "netx__queryNmsAlarms", "netx__aggregateNmsAlarms", "netx__runNmsDiagnostics", "netx__queryNmsNeInventory", "netx__getNmsNe", "netx__queryNmsAlarmsRaw", "netx__aggregateNmsAlarmsRaw", "netx__listNmsAlarmFields", "netx__sqlQueryNms", "netx__listManagedNe", "netx__getManagedNe", "netx__execManagedNe", "netx__getNeExecJob", "netx__listCliTargets", "netx__findTopologyPaths" ]), topology: Object.freeze([ "netx__getTopologyTree", "netx__getTopologyView", "netx__createTopologyFolder", "netx__addTopologyViewNodes", "netx__removeTopologyViewNodes", "netx__copyTopologyViewNodes", "netx__updateTopologyViewPositions", "netx__projectTopologyNeighbors", "netx__queryTopologyFabricNodes", "netx__classifyTopologyFabricNodes", "netx__queryTopologyNeighborhood", "netx__queryTopologyEdges", "netx__layoutTopologyView", "netx__suggestSinkHubs", "netx__analyzeTopologyViewLayout" ]), bizMonitor: Object.freeze([ "netx__listBizMonitors", "netx__listBizMonitorBatches", "netx__getBizMonitorContext", "netx__getBizMonitorBoard", "netx__listBizMonitorReds", "netx__getBizMonitorDiffs", "netx__getBizCollectBatch", "netx__getBizCollectCommandRaw" ]) }); var SKILL_DIR_BY_GROUP = Object.freeze({ ops: "ops", topology: "topology", bizMonitor: "biz-monitor" }); var CAPABILITY_GROUP_IDS = Object.freeze([ "ops", "topology", "bizMonitor" ]); function parsePresetIds(value) { const current = readConfigValue(value); if (typeof current !== "string") return []; return [...new Set(current.split(/[\s,,]+/).filter(Boolean))]; } // 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 = props.tone ?? (props.phase !== undefined ? alarmPushTone(props.phase) : "neutral"); 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 kbBadgeLabel(t, snapshot) { if (!snapshot || snapshot.status === "unconfigured") { return t("kbStatusUnconfigured"); } if (snapshot.status === "error") { return t("kbStatusError"); } return fillTemplate(t("kbStatusConfigured"), { operator: snapshot.operatorName, country: snapshot.country, version: snapshot.version }); } function fillTemplate(template, vars) { return template.replace(/\{(\w+)\}/g, (match, name) => Object.prototype.hasOwnProperty.call(vars, name) ? vars[name] : match); } function toolsBadgeLabel(t, view) { if (!view) return t("toolsStatusUnknown"); if (!view.toolsEnabled) return t("toolsDisabled"); if (view.error) { return t("toolsStatusError"); } if (view.count > 0) { return fillTemplate(t("toolsStatusMounted"), { count: String(view.count) }); } return t("toolsStatusEmpty"); } function exportStatusMessage(t, status) { const key = sessionsExportReasonLocaleKey(status.reasonCode); const template = t(key); if (key === "sessionsExportListFailed") { return fillTemplate(template, { detail: status.reason || "" }); } return status.reasonCode ? template : status.reason || template; } function exportErrorMessage(t, error) { const key = sessionsExportReasonLocaleKey(error.code); const template = t(key); if (error.code === "http_failed") { const detail = error.detail.trim() === "" ? "" : ` ${error.detail.trim()}`; return fillTemplate(template, { status: error.status !== undefined ? String(error.status) : "", detail }); } if (error.code === "empty_body") return template; return error.fallback || template; } function ValueField(props) { return /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-settings-field", children: [ /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-field-head", children: [ /* @__PURE__ */ jsx_runtime.jsx("label", { htmlFor: props.id, children: props.label }), props.field.overridden ? /* @__PURE__ */ jsx_runtime.jsx("span", { className: "dsh-nx-badges", children: /* @__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-inputInvalid" : undefined, type: "text", placeholder: props.hint, value: props.field.text, disabled: props.disabled, "aria-invalid": props.field.invalid || undefined, onChange: (event) => { props.onEdit(event.target.value); } }), props.field.invalid ? /* @__PURE__ */ jsx_runtime.jsx("p", { className: "dsh-nx-invalid", children: props.invalidLabel }) : null ] }); } function SelectField(props) { const known = props.options.some((row) => row.value === props.field.text); return /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-settings-field", children: [ /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-field-head", children: [ /* @__PURE__ */ jsx_runtime.jsx("label", { htmlFor: props.id, children: props.label }), props.field.overridden ? /* @__PURE__ */ jsx_runtime.jsx("span", { className: "dsh-nx-badges", children: /* @__PURE__ */ jsx_runtime.jsx("button", { type: "button", className: "dsh-nx-reset", disabled: props.disabled, onClick: props.onReset, children: props.resetLabel }) }) : null ] }), /* @__PURE__ */ jsx_runtime.jsxs("select", { id: props.id, className: props.field.invalid ? "dsh-nx-inputInvalid" : undefined, value: props.field.text, disabled: props.disabled, "aria-invalid": props.field.invalid || undefined, onChange: (event) => { props.onEdit(event.target.value); }, children: [ !known && props.field.text ? /* @__PURE__ */ jsx_runtime.jsx("option", { value: props.field.text, children: props.field.text }) : null, props.options.map((row) => /* @__PURE__ */ jsx_runtime.jsx("option", { value: row.value, children: row.label }, row.value)) ] }), props.field.invalid ? /* @__PURE__ */ jsx_runtime.jsx("p", { className: "dsh-nx-invalid", children: props.invalidLabel }) : null ] }); } function ImDeliveryPicker(props) { const options = props.catalog.options; const selected = resolveImTargets({ imTargets: props.targetsJson, imBotId: props.legacyBotId, imTargetId: props.legacyTargetId }); const selectedKeys = new Set(selected.map((row) => imTargetKey(row.botId, row.targetId))); const orphanSelected = selected.filter((row) => !options.some((opt) => imCatalogOptionKey(opt.botId, opt.targetId) === imTargetKey(row.botId, row.targetId))); return /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-settings-field", children: [ /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-field-head", children: [ /* @__PURE__ */ jsx_runtime.jsx("span", { className: "dsh-nx-field-label", children: props.labels.target }), selected.length > 0 ? /* @__PURE__ */ jsx_runtime.jsx("span", { className: "dsh-nx-badgeMuted", children: props.labels.selectedCount.replace("{count}", String(selected.length)) }) : null ] }), options.length === 0 && orphanSelected.length === 0 ? /* @__PURE__ */ jsx_runtime.jsx("p", { className: "dsh-nx-hint", children: !props.catalog.available ? props.catalog.reasonCode ? props.labels.unavailable : props.catalog.hint || props.labels.unavailable : props.labels.none }) : /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-imTargetList", children: [ options.map((row) => { const key = imCatalogOptionKey(row.botId, row.targetId); const checked = selectedKeys.has(key); return /* @__PURE__ */ jsx_runtime.jsxs("label", { className: "dsh-nx-checkRow", children: [ /* @__PURE__ */ jsx_runtime.jsx("input", { type: "checkbox", checked, disabled: props.disabled, onChange: (event) => { props.onChange(setImTargetSelected(selected, { botId: row.botId, targetId: row.targetId }, event.target.checked)); } }), /* @__PURE__ */ jsx_runtime.jsx("span", { children: `${row.name} · ${row.channel || "im"} · ${row.targetId}` }) ] }, key); }), orphanSelected.map((row) => { const key = imTargetKey(row.botId, row.targetId); return /* @__PURE__ */ jsx_runtime.jsxs("label", { className: "dsh-nx-checkRow", children: [ /* @__PURE__ */ jsx_runtime.jsx("input", { type: "checkbox", checked: true, disabled: props.disabled, onChange: (event) => { props.onChange(setImTargetSelected(selected, row, event.target.checked)); } }), /* @__PURE__ */ jsx_runtime.jsx("span", { children: `${row.botId} · ${row.targetId}` }) ] }, `orphan-${key}`); }) ] }), !props.catalog.available && options.length > 0 ? /* @__PURE__ */ jsx_runtime.jsx("p", { className: "dsh-nx-hint", children: props.catalog.reasonCode ? props.labels.unavailable : props.catalog.hint || props.labels.unavailable }) : null ] }); } function PresetTargets(props) { const selected = parsePresetIds(props.field.text); const rows = props.state.presetCatalog.filter((row) => row.id !== "netxops"); const unknown = selected.filter((id) => !rows.some((row) => row.id === id)); return /* @__PURE__ */ jsx_runtime.jsxs("details", { className: "dsh-nx-targets", children: [ /* @__PURE__ */ jsx_runtime.jsxs("summary", { children: [ props.t("selectedPresets"), " · ", selected.length ] }), props.state.presetCatalogError ? /* @__PURE__ */ jsx_runtime.jsx("p", { className: "dsh-nx-invalid", children: props.t("presetCatalogUnavailable") }) : null, [...rows, ...unknown.map((id) => ({ id, name: undefined, broken: undefined }))].map((row) => /* @__PURE__ */ jsx_runtime.jsxs("label", { className: "dsh-nx-checkRow", children: [ /* @__PURE__ */ jsx_runtime.jsx("input", { type: "checkbox", checked: selected.includes(row.id), disabled: props.disabled, onChange: (event) => { props.onEdit((event.target.checked ? [...selected, row.id] : selected.filter((id) => id !== row.id)).join(",")); } }), /* @__PURE__ */ jsx_runtime.jsxs("span", { children: [ row.name || row.id, row.name && row.name !== row.id ? ` (${row.id})` : "" ] }), row.broken ? /* @__PURE__ */ jsx_runtime.jsx("span", { className: "dsh-nx-invalid", title: row.broken, children: props.t("presetUnavailable") }) : null ] }, row.id)), rows.length === 0 && unknown.length === 0 ? /* @__PURE__ */ jsx_runtime.jsx("p", { className: "dsh-nx-hint", children: props.t("noOtherPresets") }) : null ] }); } function CapabilityGroupBlock(props) { return /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-groupBlock", role: "group", "aria-label": props.title, children: [ /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-groupTitle", children: [ /* @__PURE__ */ jsx_runtime.jsx("span", { children: props.title }), props.toolCount !== undefined ? /* @__PURE__ */ jsx_runtime.jsx("span", { className: "dsh-nx-badgeMuted", children: props.toolCount }) : null ] }), /* @__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 }) ] }) ] }), props.targets ] }); } function ToggleField(props) { return /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-settings-field dsh-nx-toggleField", children: [ /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-field-head", children: [ /* @__PURE__ */ jsx_runtime.jsx("label", { htmlFor: props.id, children: props.label }), /* @__PURE__ */ jsx_runtime.jsxs("span", { className: "dsh-nx-badges", children: [ props.trailing, props.overridden ? /* @__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, type: "checkbox", checked: props.checked, disabled: props.disabled, "aria-label": props.label, onChange: (event) => { props.onToggle(event.target.checked); } }) ] }), props.error ] }); } function NetxopsCard(props) { ensureStyles(); const { t } = props; const state = props.useNetxopsCard((snapshot) => snapshot); if (!state.available) { return /* @__PURE__ */ jsx_runtime.jsxs("section", { className: "dsh-nx-settings", "aria-label": t("title"), children: [ /* @__PURE__ */ jsx_runtime.jsx("h2", { children: t("title") }), /* @__PURE__ */ jsx_runtime.jsx("div", { className: "dsh-nx-settings-empty", role: "status", children: t("settingsUnavailable") }) ] }); } const disabled = !state.writable; const blocked = !state.dirty || state.invalid || state.saving; const pushStatus = state.alarmPushStatus; const publicationDisabled = disabled || state.toolsEnabled.text !== "true"; const targets = (field, value) => /* @__PURE__ */ jsx_runtime.jsx(PresetTargets, { field: value, state, disabled: publicationDisabled, t, onEdit: (text) => { props.edit(field, text); } }); return /* @__PURE__ */ jsx_runtime.jsxs("section", { className: "dsh-nx-settings", "aria-label": t("title"), children: [ /* @__PURE__ */ jsx_runtime.jsx("header", { children: /* @__PURE__ */ jsx_runtime.jsx("h2", { children: t("title") }) }), !state.writable ? /* @__PURE__ */ jsx_runtime.jsx("p", { className: "dsh-nx-hint", role: "status", children: t("readOnly") }) : null, /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-settings-card", children: [ /* @__PURE__ */ jsx_runtime.jsx("h3", { children: t("sectionConnection") }), /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-settings-field", children: [ /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-field-head", children: [ /* @__PURE__ */ jsx_runtime.jsx("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", type: "password", autoComplete: "off", placeholder: t("apiTokenHint"), value: state.apiToken.text, disabled: !state.apiTokenWritable, onChange: (event) => { props.edit("apiToken", event.target.value); } }), !state.apiTokenRemoteReady ? /* @__PURE__ */ jsx_runtime.jsx("p", { className: "dsh-nx-invalid", children: t("apiTokenUnavailable") }) : null ] }), /* @__PURE__ */ jsx_runtime.jsx(ValueField, { id: "netxops-api-url", label: t("apiUrl"), hint: t("apiUrlHint"), field: state.apiUrl, resetLabel: t("reset"), invalidLabel: t("apiUrlInvalid"), disabled, onEdit: (text) => { props.edit("apiUrl", text); }, onReset: () => { props.resetField("apiUrl"); } }), /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-fieldGrid", children: [ /* @__PURE__ */ jsx_runtime.jsx(SelectField, { id: "netxops-lang", label: t("lang"), field: state.lang, options: [ { value: "zh", label: t("languageChinese") }, { value: "en", label: t("languageEnglish") } ], resetLabel: t("reset"), invalidLabel: t("invalid"), disabled, onEdit: (text) => { props.edit("lang", text); }, onReset: () => { props.resetField("lang"); } }), /* @__PURE__ */ jsx_runtime.jsx(SelectField, { id: "netxops-nms-provider", label: t("nmsProvider"), field: state.nmsProvider, options: [{ value: "zte-ume", label: "ZTE UME" }], 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-settings-card", children: [ /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-card-head", children: [ /* @__PURE__ */ jsx_runtime.jsx("h3", { children: t("sectionLanguage") }), /* @__PURE__ */ jsx_runtime.jsx("span", { className: "dsh-nx-hint", children: t("languageScope") }) ] }), /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-fieldGrid", children: [ /* @__PURE__ */ jsx_runtime.jsx(SelectField, { id: "netxops-thinking-language", label: t("thinkingLanguage"), field: state.thinkingLanguage, options: [ { value: "auto", label: t("languageAuto") }, { value: "zh-CN", label: t("languageChinese") }, { value: "en", label: t("languageEnglish") } ], resetLabel: t("reset"), invalidLabel: t("invalid"), disabled, onEdit: (text) => { props.edit("thinkingLanguage", text); }, onReset: () => { props.resetField("thinkingLanguage"); } }), /* @__PURE__ */ jsx_runtime.jsx(SelectField, { id: "netxops-reply-language", label: t("replyLanguage"), field: state.replyLanguage, options: [ { value: "follow-user", label: t("languageFollowUser") }, { value: "zh", label: t("languageChinese") }, { value: "en", label: t("languageEnglish") } ], resetLabel: t("reset"), invalidLabel: t("invalid"), disabled, onEdit: (text) => { props.edit("replyLanguage", text); }, onReset: () => { props.resetField("replyLanguage"); } }) ] }) ] }), /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-settings-card", children: [ /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-card-head", children: [ /* @__PURE__ */ jsx_runtime.jsx("h3", { children: t("sectionCapabilities") }), /* @__PURE__ */ jsx_runtime.jsx(StatusBadge, { tone: toolsMountTone(state.toolsStatus), label: toolsBadgeLabel(t, state.toolsStatus) }) ] }), /* @__PURE__ */ jsx_runtime.jsx(ToggleField, { id: "netxops-tools-enabled", label: t("toolsEnabled"), checked: state.toolsEnabled.text === "true", disabled, overridden: state.toolsEnabled.overridden, resetLabel: t("reset"), onToggle: (checked) => { props.edit("toolsEnabled", String(checked)); }, onReset: () => { props.resetField("toolsEnabled"); }, error: state.toolsStatus?.error ? /* @__PURE__ */ jsx_runtime.jsx("p", { className: "dsh-nx-invalid", role: "status", children: state.toolsStatus.error }) : null }), /* @__PURE__ */ jsx_runtime.jsx(CapabilityGroupBlock, { title: t("groupOps"), toolCount: TOOLS_BY_GROUP.ops.length, targets: targets("groupOpsPresets", state.groupOpsPresets), inPresetLabel: t("groupInPreset"), publicLabel: t("groupPublic"), inPreset: state.groupOpsInPreset, published: state.groupOpsPublic, disabled: publicationDisabled, onEditInPreset: (checked) => { props.edit("groupOpsInPreset", checked ? "true" : "false"); }, onEditPublic: (checked) => { props.edit("groupOpsPublic", checked ? "true" : "false"); } }), /* @__PURE__ */ jsx_runtime.jsx(CapabilityGroupBlock, { title: t("groupTopology"), toolCount: TOOLS_BY_GROUP.topology.length, targets: targets("groupTopologyPresets", state.groupTopologyPresets), inPresetLabel: t("groupInPreset"), publicLabel: t("groupPublic"), inPreset: state.groupTopologyInPreset, published: state.groupTopologyPublic, disabled: publicationDisabled, onEditInPreset: (checked) => { props.edit("groupTopologyInPreset", checked ? "true" : "false"); }, onEditPublic: (checked) => { props.edit("groupTopologyPublic", checked ? "true" : "false"); } }), /* @__PURE__ */ jsx_runtime.jsx(CapabilityGroupBlock, { title: t("groupBizMonitor"), toolCount: TOOLS_BY_GROUP.bizMonitor.length, targets: targets("groupBizMonitorPresets", state.groupBizMonitorPresets), inPresetLabel: t("groupInPreset"), publicLabel: t("groupPublic"), inPreset: state.groupBizMonitorInPreset, published: state.groupBizMonitorPublic, disabled: publicationDisabled, onEditInPreset: (checked) => { props.edit("groupBizMonitorInPreset", checked ? "true" : "false"); }, onEditPublic: (checked) => { props.edit("groupBizMonitorPublic", checked ? "true" : "false"); } }), state.toolsStatus && state.toolsStatus.mounts.length > 0 ? /* @__PURE__ */ jsx_runtime.jsxs("details", { className: "dsh-nx-diagnostics", children: [ /* @__PURE__ */ jsx_runtime.jsx("summary", { children: t("mountDetails") }), /* @__PURE__ */ jsx_runtime.jsx("p", { className: "dsh-nx-hint", children: t("mountStatusHint") }), state.toolsStatus.mounts.map((mount, index) => /* @__PURE__ */ jsx_runtime.jsxs("p", { className: mount.error ? "dsh-nx-invalid" : "dsh-nx-hint", children: [ mount.source, " · ", mount.count, " · ", mount.groups.join(", ") || "—", mount.error ? ` · ${mount.error}` : "" ] }, `${mount.source}:${index}`)) ] }) : null ] }), /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-settings-card", children: [ /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-card-head", children: [ /* @__PURE__ */ jsx_runtime.jsx("h3", { children: t("sectionKnowledge") }), /* @__PURE__ */ jsx_runtime.jsx(StatusBadge, { tone: kbStatusTone(state.kbStatus?.status ?? "unconfigured"), label: kbBadgeLabel(t, state.kbStatus) }) ] }), /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-settings-field", children: [ /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-field-head", children: [ /* @__PURE__ */ jsx_runtime.jsx("label", { htmlFor: "netxops-kb-root", children: t("kbRoot") }), /* @__PURE__ */ jsx_runtime.jsx("span", { className: "dsh-nx-badges", children: state.kbRoot.overridden ? /* @__PURE__ */ jsx_runtime.jsx(jsx_runtime.Fragment, { children: /* @__PURE__ */ jsx_runtime.jsx("button", { type: "button", className: "dsh-nx-reset", disabled, onClick: () => { props.resetField("kbRoot"); }, children: t("reset") }) }) : null }) ] }), /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-pathRow", children: [ /* @__PURE__ */ jsx_runtime.jsx("input", { id: "netxops-kb-root", className: state.kbRoot.invalid ? "dsh-nx-inputInvalid" : undefined, type: "text", placeholder: t("kbRootHint"), value: state.kbRoot.text, disabled, "aria-invalid": state.kbRoot.invalid || undefined, onChange: (event) => { props.edit("kbRoot", event.target.value); } }), /* @__PURE__ */ jsx_runtime.jsx("button", { type: "button", className: "dsh-nx-btn", disabled: disabled || !state.kbDirectoryPickerReady, title: !state.kbDirectoryPickerReady ? t("kbBrowseUnavailable") : undefined, onClick: () => { props.browseKbRoot(); }, children: t("kbBrowse") }) ] }), state.kbRoot.invalid ? /* @__PURE__ */ jsx_runtime.jsx("p", { className: "dsh-nx-invalid", children: t("invalid") }) : null, state.kbUiError ? /* @__PURE__ */ jsx_runtime.jsx("p", { className: "dsh-nx-invalid", role: "status", children: fillTemplate(t("kbBrowseFailed"), { detail: state.kbUiError }) }) : null, state.kbStatus?.status === "error" && state.kbStatus.errorMessage ? /* @__PURE__ */ jsx_runtime.jsx("p", { className: "dsh-nx-invalid", role: "status", children: state.kbStatus.errorMessage }) : null, state.kbStatus?.status === "configured" && state.kbStatus.realRoot && state.kbStatus.realRoot !== state.kbRoot.text ? /* @__PURE__ */ jsx_runtime.jsx("p", { className: "dsh-nx-hint", children: state.kbStatus.realRoot }) : null ] }), /* @__PURE__ */ jsx_runtime.jsx(CapabilityGroupBlock, { title: t("groupKb"), targets: targets("groupKbPresets", state.groupKbPresets), inPresetLabel: t("groupInPreset"), publicLabel: t("groupPublic"), inPreset: state.groupKbInPreset, published: state.groupKbPublic, disabled: publicationDisabled, onEditInPreset: (checked) => { props.edit("groupKbInPreset", checked ? "true" : "false"); }, onEditPublic: (checked) => { props.edit("groupKbPublic", checked ? "true" : "false"); } }) ] }), /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-settings-card", children: [ /* @__PURE__ */ jsx_runtime.jsx("h3", { children: t("sectionAlarms") }), /* @__PURE__ */ jsx_runtime.jsx(ToggleField, { id: "netxops-alarm-push", label: t("alarmPushEnabled"), checked: state.alarmPushEnabled.text === "true", disabled, overridden: state.alarmPushEnabled.overridden, resetLabel: t("reset"), onToggle: (checked) => { props.edit("alarmPushEnabled", checked ? "true" : "false"); }, onReset: () => { props.resetField("alarmPushEnabled"); }, trailing: pushStatus ? /* @__PURE__ */ jsx_runtime.jsx(StatusBadge, { phase: pushStatus.phase, label: t(phaseLocaleKey(pushStatus.phase)) }) : null, error: pushStatus?.lastError ? /* @__PURE__ */ jsx_runtime.jsx("p", { className: "dsh-nx-invalid", role: "status", children: pushStatus.lastError }) : null }), /* @__PURE__ */ jsx_runtime.jsx(ToggleField, { id: "netxops-alarm-dsh", label: t("alarmDeliverDsh"), checked: state.alarmDeliverDsh.text === "true", disabled, overridden: state.alarmDeliverDsh.overridden, resetLabel: t("reset"), onToggle: (checked) => { props.edit("alarmDeliverDsh", checked ? "true" : "false"); }, onReset: () => { props.resetField("alarmDeliverDsh"); } }), /* @__PURE__ */ jsx_runtime.jsx(ImDeliveryPicker, { catalog: state.imDeliveryCatalog, targetsJson: state.imTargets.text, legacyBotId: state.imBotId.text, legacyTargetId: state.imTargetId.text, disabled, labels: { target: t("alarmDeliverIm"), none: t("imTargetNone"), selectedCount: t("imTargetSelectedCount"), unavailable: t("imCatalogUnavailable") }, onChange: (targets2) => { props.edit("imTargets", formatImTargetsJson(targets2)); const first = targets2[0]; props.edit("imBotId", first?.botId ?? ""); props.edit("imTargetId", first?.targetId ?? ""); props.edit("alarmDeliverIm", targets2.length > 0 ? "true" : "false"); } }) ] }), /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-settings-card", children: [ /* @__PURE__ */ jsx_runtime.jsx("h3", { children: t("sectionExport") }), /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-settings-field", children: [ state.sessionsExportStatus === null ? /* @__PURE__ */ jsx_runtime.jsx("p", { className: "dsh-nx-hint", children: t("sessionsExportUnavailable") }) : state.sessionsExportStatus.available ? /* @__PURE__ */ jsx_runtime.jsx("p", { className: "dsh-nx-hint", children: t("sessionsExportCount").replace("{count}", String(state.sessionsExportStatus.sessionCount)) }) : /* @__PURE__ */ jsx_runtime.jsx("p", { className: "dsh-nx-hint", children: exportStatusMessage(t, state.sessionsExportStatus) }), state.sessionsExportError ? /* @__PURE__ */ jsx_runtime.jsx("p", { className: "dsh-nx-invalid", role: "status", children: exportErrorMessage(t, state.sessionsExportError) }) : null, state.sessionsExportLastFile && !state.sessionsExportBusy && !state.sessionsExportError ? /* @__PURE__ */ jsx_runtime.jsx("p", { className: "dsh-nx-hint", role: "status", children: t("sessionsExportDone").replace("{file}", state.sessionsExportLastFile) }) : null, /* @__PURE__ */ jsx_runtime.jsx("div", { className: "dsh-nx-settings-actions", children: /* @__PURE__ */ jsx_runtime.jsx("button", { type: "button", className: "dsh-nx-btn", disabled: state.sessionsExportBusy || state.sessionsExportStatus?.available !== true, onClick: () => { props.exportAllSessions(); }, children: t(state.sessionsExportBusy ? "sessionsExportBusy" : "sessionsExportButton") }) }) ] }) ] }), state.dirty || state.saving || state.failed ? /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-settings-actions", children: [ /* @__PURE__ */ jsx_runtime.jsx("button", { type: "button", className: "dsh-nx-btn dsh-nx-btn-primary", disabled: blocked, onClick: props.save, children: t(state.saving ? "saving" : "save") }), /* @__PURE__ */ jsx_runtime.jsx("button", { type: "button", className: "dsh-nx-btn dsh-nx-btn-ghost", disabled: !state.dirty || state.saving, onClick: props.discard, children: t("discard") }), state.dirty && !state.saving ? /* @__PURE__ */ jsx_runtime.jsx("span", { className: "dsh-nx-settings-msg", children: t("unsaved") }) : null, state.failed ? /* @__PURE__ */ jsx_runtime.jsx("span", { className: "dsh-nx-settings-msg err", role: "status", children: t("saveFailed") }) : null ] }) : null ] }); } // src/netx/http.ts function normalizeApiUrl(raw) { const trimmed = typeof raw === "string" ? raw.trim() : ""; if (!trimmed) return { ok: false, error: "empty" }; let parsed; try { parsed = new URL(trimmed); } catch { return { ok: false, error: "invalid_url" }; } if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { return { ok: false, error: "unsupported_scheme" }; } if (parsed.username || parsed.password) { return { ok: false, error: "credentials_in_url" }; } const path = parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/$/, ""); const search = parsed.search && parsed.search !== "?" ? parsed.search : ""; return { ok: true, url: `${parsed.origin}${path}${search}` }; } // 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 presetIdsField(field) { return { field, format: (value) => typeof value === "string" ? value : "", parse: (text) => ({ kind: "set", value: parsePresetIds(text).join(",") }) }; } function apiUrlField(field) { return { field, format: (value) => typeof value === "string" ? value : "", parse: (text) => { const trimmed = text.trim(); if (trimmed === "") return { kind: "clear" }; const normalized = normalizeApiUrl(trimmed); if (!normalized.ok) return; return { kind: "set", value: normalized.url }; } }; } 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; saveQueue = Promise.resolve(); 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(); let committedInvalid = false; for (const field of this.specs.keys()) { if (this.field(field).invalid) { committedInvalid = true; break; } } return { available: snapshot.status === "ready", writable: snapshot.writable, dirty: plan.length > 0, invalid: committedInvalid || 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) { const text = spec.format(this.sectionValue(field)); const write2 = text.trim() === "" ? { kind: "clear" } : spec.parse(text); return { text, overridden: this.stored(field), invalid: text.trim() !== "" && write2 === undefined }; } 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(); } }; } save(fields) { const task = this.saveQueue.then(() => this.saveNow(fields)); this.saveQueue = task.catch(() => {}); return task; } async saveNow(fields) { const snapshot = this.snapshotOf(); if (snapshot.status !== "ready" || !snapshot.writable) { this.failed = true; this.publish(); return; } const staged = new Map([...this.staged].filter(([field]) => !fields || fields.includes(field))); const plan = this.plan().filter((item) => !fields || fields.includes(item.field)); const writes = plan.flatMap((item) => item.run === undefined ? [] : [item.run]); if (writes.length !== plan.length) return; this.saving = true; this.failed = false; this.publish(); let landed = true; try { for (const write of writes) landed = await write() && landed; } catch { landed = false; } finally { if (landed) { for (const [field, edit] of staged) { if (this.staged.get(field) === edit) this.staged.delete(field); } } 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.sectionValue(field) === value || 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 NETXOPS_SECTION_ID = "dsh-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; toolsStatus = null; toolsStatusInFlight = false; presetCatalog = []; presetCatalogError = false; presetCatalogInFlight = false; kbStatus = null; kbPreview = null; kbDirectoryPicker; kbBrowseInFlight = false; kbStatusInFlight = false; kbReloadInFlight = false; kbUiError = null; lastSyncedKbRoot = undefined; imDeliveryCatalog = { ...EMPTY_IM_DELIVERY_CATALOG }; sessionsExportStatus = null; sessionsExportBusy = false; sessionsExportError = null; sessionsExportLastFile = null; pollTimer; pollInFlight = false; catalogInFlight = false; exportStatusInFlight = false; exportInFlight; constructor(scope, ctx) { this.scope = scope; this.ctx = ctx; this.form = new CardForm(scope, [ booleanFieldPersistFalse("toolsEnabled"), presetIdsField("groupOpsPresets"), presetIdsField("groupTopologyPresets"), presetIdsField("groupBizMonitorPresets"), presetIdsField("groupKbPresets"), apiUrlField("apiUrl"), textField("lang"), textField("thinkingLanguage"), textField("replyLanguage"), textField("nmsProvider"), textField("kbRoot"), booleanFieldPersistFalse("groupOpsInPreset"), booleanFieldPersistFalse("groupOpsPublic"), booleanFieldPersistFalse("groupTopologyInPreset"), booleanFieldPersistFalse("groupTopologyPublic"), booleanFieldPersistFalse("groupBizMonitorInPreset"), booleanFieldPersistFalse("groupBizMonitorPublic"), booleanFieldPersistFalse("groupKbInPreset"), booleanFieldPersistFalse("groupKbPublic"), booleanFieldPersistFalse("alarmPushEnabled"), booleanFieldPersistFalse("alarmDeliverDsh"), booleanFieldPersistFalse("alarmDeliverIm"), textField("imTargets"), 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.refreshKbPreview(); this.syncKbRootFromSettings(); }); 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.toolsStatus !== null) { this.toolsStatus = null; changed = true; } this.presetCatalog = []; this.presetCatalogError = true; changed = true; if (this.kbStatus !== null || this.kbPreview !== null) { this.kbStatus = null; this.kbPreview = null; changed = true; } if (this.imDeliveryCatalog.options.length > 0 || this.imDeliveryCatalog.available !== true) { this.imDeliveryCatalog = { ...EMPTY_IM_DELIVERY_CATALOG }; changed = true; } if (this.sessionsExportStatus !== null) { this.sessionsExportStatus = null; changed = true; } if (changed) this.store.set(this.projection()); return; } this.startStatusPoll(); this.refreshAlarmPushStatus(); this.refreshToolsStatus(); this.refreshPresetCatalog(); this.reloadKbFromHost(); this.refreshKbPreview(); this.refreshImDeliveryCatalog(); this.refreshSessionsExportStatus(); } setDirectoryPicker(picker) { const ready = picker !== undefined; if (this.kbDirectoryPicker === picker && this.kbDirectoryPicker !== undefined === ready) { return; } this.kbDirectoryPicker = picker; this.store.set(this.projection()); } startStatusPoll() { if (this.pollTimer !== undefined) return; this.pollTimer = setInterval(() => { this.refreshAlarmPushStatus(); this.refreshToolsStatus(); this.refreshPresetCatalog(); this.refreshKbStatus(); this.refreshImDeliveryCatalog(); this.refreshSessionsExportStatus(); }, 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 refreshToolsStatus() { const call = this.rpcCall; if (call === undefined || this.toolsStatusInFlight) return; this.toolsStatusInFlight = true; try { const next = await fetchToolsStatus(call); const prev = this.toolsStatus; if (prev && prev.count === next.count && prev.version === next.version && prev.plane === next.plane && prev.error === next.error && prev.tokenConfigured === next.tokenConfigured && prev.apiUrl === next.apiUrl && JSON.stringify(prev) === JSON.stringify(next)) return; this.toolsStatus = next; this.store.set(this.projection()); } catch {} finally { this.toolsStatusInFlight = false; } } async refreshPresetCatalog() { const call = this.rpcCall; if (!call || this.presetCatalogInFlight) return; this.presetCatalogInFlight = true; try { const result = await call("/netxops", "presets.catalog", {}); if (call !== this.rpcCall) return; this.presetCatalogError = result?.ok !== true || !Array.isArray(result.value?.presets); if (!this.presetCatalogError) this.presetCatalog = result.value.presets; this.store.set(this.projection()); } catch { if (call !== this.rpcCall) return; this.presetCatalogError = true; this.store.set(this.projection()); } finally { this.presetCatalogInFlight = 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; } } async refreshSessionsExportStatus() { const call = this.rpcCall; if (call === undefined || this.exportStatusInFlight) return; this.exportStatusInFlight = true; try { const next = await fetchSessionsExportStatus(call); const prev = this.sessionsExportStatus; if (prev && prev.available === next.available && prev.sessionCount === next.sessionCount && prev.supportsRawArtifacts === next.supportsRawArtifacts && prev.reason === next.reason) return; this.sessionsExportStatus = next; this.store.set(this.projection()); } catch {} finally { this.exportStatusInFlight = false; } } async refreshKbStatus() { const call = this.rpcCall; if (call === undefined || this.kbStatusInFlight) return; this.kbStatusInFlight = true; try { const next = await fetchKbStatus(call); this.applyKbSnapshot(next); } catch {} finally { this.kbStatusInFlight = false; } } async reloadKbFromHost() { const call = this.rpcCall; if (call === undefined || this.kbReloadInFlight) return; this.kbReloadInFlight = true; try { const next = await reloadKbStatus(call); this.applyKbSnapshot(next); this.lastSyncedKbRoot = (this.scope.getSnapshot().value?.kbRoot ?? "").trim(); } catch { this.refreshKbStatus(); } finally { this.kbReloadInFlight = false; } } applyKbSnapshot(next) { const prev = this.kbStatus; if (prev && prev.status === next.status && prev.realRoot === next.realRoot && prev.operatorName === next.operatorName && prev.country === next.country && prev.version === next.version && prev.errorMessage === next.errorMessage) return; this.kbStatus = next; this.store.set(this.projection()); } async syncKbRootFromSettings() { if (this.rpcCall === undefined) return; const saved = (this.scope.getSnapshot().value?.kbRoot ?? "").trim(); if (saved === this.lastSyncedKbRoot) return; this.lastSyncedKbRoot = saved; await this.reloadKbFromHost(); } async refreshKbPreview() { const call = this.rpcCall; if (call === undefined) return; const draft = this.form.field("kbRoot").text.trim(); const saved = (this.scope.getSnapshot().value?.kbRoot ?? "").trim(); if (draft === saved) { if (this.kbPreview !== null) { this.kbPreview = null; this.store.set(this.projection()); } return; } try { const next = draft === "" ? unconfiguredKbSnapshot() : await resolveKbPath(call, draft); const prev = this.kbPreview; if (prev && prev.status === next.status && prev.realRoot === next.realRoot && prev.operatorName === next.operatorName && prev.country === next.country && prev.version === next.version && prev.errorMessage === next.errorMessage) return; this.kbPreview = next; this.store.set(this.projection()); } catch {} } async commitKbRoot(path) { const trimmed = path.trim(); this.kbUiError = null; this.store.set(this.projection()); await this.form.save(["kbRoot"]); const shell = this.form.shell(); const saved = (this.scope.getSnapshot().value?.kbRoot ?? "").trim(); if (shell.failed || saved !== trimmed) { this.kbUiError = saved === trimmed ? "kbRoot save failed" : `kbRoot save did not land (saved="${saved || "(empty)"}")`; this.store.set(this.projection()); const call = this.rpcCall; if (call !== undefined && trimmed !== "") { try { this.kbPreview = await resolveKbPath(call, trimmed); this.store.set(this.projection()); } catch {} } return; } this.kbPreview = null; this.lastSyncedKbRoot = undefined; await this.reloadKbFromHost(); this.refreshKbPreview(); } browseKbRoot() { if (this.kbBrowseInFlight) return; this.kbBrowseInFlight = true; this.kbUiError = null; this.store.set(this.projection()); const applyPath = (path) => { this.form.actions().edit("kbRoot", path); this.commitKbRoot(path); }; const fail = (error) => { this.kbUiError = error instanceof Error ? error.message : String(error); this.store.set(this.projection()); }; const acceptPickResult = (result) => { const path = unwrapDirectoryPickResult(result); if (path === null) return; if (path === undefined) { fail(`unexpected directoryPicker result: ${safeJson(result)}`); return; } applyPath(path); }; const picker = this.kbDirectoryPicker; if (picker !== undefined) { picker.pick().then(acceptPickResult).catch(fail).finally(() => { this.kbBrowseInFlight = false; this.store.set(this.projection()); }); return; } const call = this.rpcCall; if (call === undefined) { this.kbBrowseInFlight = false; this.kbUiError = "directory picker unavailable — paste an absolute folder path and Save"; this.store.set(this.projection()); return; } call("/api", "directoryPicker/pick", { args: {} }).then(acceptPickResult).catch(fail).finally(() => { this.kbBrowseInFlight = false; this.store.set(this.projection()); }); } exportAllSessions() { if (this.exportInFlight !== undefined || this.sessionsExportBusy) return; if (this.sessionsExportStatus?.available !== true) return; this.sessionsExportBusy = true; this.sessionsExportError = null; this.store.set(this.projection()); this.exportInFlight = downloadAllSessionsExport().then((result) => { this.sessionsExportLastFile = result.filename; this.sessionsExportError = null; }).catch((error) => { if (error instanceof SessionsExportDownloadError) { this.sessionsExportError = { code: error.code, status: error.status, detail: error.detail, fallback: error.message }; return; } this.sessionsExportError = { code: "http_failed", detail: "", fallback: error instanceof Error ? error.message : String(error) }; }).finally(() => { this.sessionsExportBusy = false; this.exportInFlight = undefined; this.store.set(this.projection()); this.refreshSessionsExportStatus(); }); } projection() { return { toolsEnabled: this.form.field("toolsEnabled"), groupOpsPresets: this.form.field("groupOpsPresets"), groupTopologyPresets: this.form.field("groupTopologyPresets"), groupBizMonitorPresets: this.form.field("groupBizMonitorPresets"), groupKbPresets: this.form.field("groupKbPresets"), presetCatalog: this.presetCatalog, presetCatalogError: this.presetCatalogError, ...this.form.shell(), apiUrl: this.form.field("apiUrl"), lang: this.form.field("lang"), thinkingLanguage: this.form.field("thinkingLanguage"), replyLanguage: this.form.field("replyLanguage"), nmsProvider: this.form.field("nmsProvider"), kbRoot: this.form.field("kbRoot"), groupOpsInPreset: this.form.field("groupOpsInPreset"), groupOpsPublic: this.form.field("groupOpsPublic"), groupTopologyInPreset: this.form.field("groupTopologyInPreset"), groupTopologyPublic: this.form.field("groupTopologyPublic"), groupBizMonitorInPreset: this.form.field("groupBizMonitorInPreset"), groupBizMonitorPublic: this.form.field("groupBizMonitorPublic"), groupKbInPreset: this.form.field("groupKbInPreset"), groupKbPublic: this.form.field("groupKbPublic"), alarmPushEnabled: this.form.field("alarmPushEnabled"), alarmDeliverDsh: this.form.field("alarmDeliverDsh"), alarmDeliverIm: this.form.field("alarmDeliverIm"), imTargets: this.form.field("imTargets"), 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, toolsStatus: this.toolsStatus, kbStatus: this.kbPreview ?? this.kbStatus, kbDirectoryPickerReady: this.kbDirectoryPicker !== undefined || this.rpcCall !== undefined, kbUiError: this.kbUiError, imDeliveryCatalog: this.imDeliveryCatalog, sessionsExportStatus: this.sessionsExportStatus, sessionsExportBusy: this.sessionsExportBusy, sessionsExportError: this.sessionsExportError, sessionsExportLastFile: this.sessionsExportLastFile }; } 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() { const actions = this.form.actions(); const capabilityFields = new Set([ "toolsEnabled", "groupOpsPresets", "groupTopologyPresets", "groupBizMonitorPresets", "groupKbPresets", "groupOpsInPreset", "groupOpsPublic", "groupTopologyInPreset", "groupTopologyPublic", "groupBizMonitorInPreset", "groupBizMonitorPublic", "groupKbInPreset", "groupKbPublic" ]); return { hooks: { netxopsCard: this.store }, ...actions, edit: (field, text) => { actions.edit(field, text); if (field === "kbRoot") this.refreshKbPreview(); if (capabilityFields.has(field)) this.form.save([field]); }, discard: () => { actions.discard(); this.refreshKbPreview(); }, resetField: (field) => { actions.resetField(field); if (field === "kbRoot") this.refreshKbPreview(); if (capabilityFields.has(field)) this.form.save([field]); }, exportAllSessions: () => { this.exportAllSessions(); }, browseKbRoot: () => { this.browseKbRoot(); } }; } 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; } function unwrapDirectoryPickResult(result) { if (result === null) return null; if (typeof result === "string") { const trimmed = result.trim(); return trimmed === "" ? null : trimmed; } if (result === null || typeof result !== "object" || Array.isArray(result)) return; const row = result; if (row.ok === false) { throw new Error(row.error?.message || "directoryPicker/pick failed"); } if ("value" in row) { if (row.value === null) return null; if (typeof row.value === "string") { const trimmed = row.value.trim(); return trimmed === "" ? null : trimmed; } return; } return; } function safeJson(value) { try { return JSON.stringify(value); } catch { return String(value); } } // src/client/locales.ts var en = { settingsUnavailable: "Connecting to Host settings…", toolsEnabled: "Enable tools and skills", toolsDisabled: "Disabled", selectedPresets: "Other presets", presetCatalogUnavailable: "Cannot load presets. Existing selections are retained.", presetUnavailable: "Preset unavailable", noOtherPresets: "No other presets available.", mountStatusHint: "Registered tools in active sessions; API access is verified when a tool runs.", mountDetails: "Diagnostics", title: "Netx Ops", sectionConnection: "Connection", sectionLanguage: "Model language", languageScope: "Applies to all presets", languageAuto: "Automatic", languageFollowUser: "Follow user", languageChinese: "Chinese", languageEnglish: "English", sectionCapabilities: "Tools and skills", sectionKnowledge: "Knowledge base", sectionAlarms: "Key-alarm delivery", sectionExport: "Session export", apiUrl: "API URL", apiUrlHint: "http://127.0.0.1:8890", apiUrlInvalid: "Enter an HTTP or HTTPS URL without credentials.", lang: "API / alarm language", thinkingLanguage: "Thinking language", replyLanguage: "Reply language", nmsProvider: "NMS provider", groupOps: "Alarms, inventory and CLI", groupTopology: "Topology and layout", groupBizMonitor: "Cutover monitoring", groupKb: "Knowledge skills", groupInPreset: "Netx Ops", groupPublic: "All presets", kbRoot: "Package root", kbRootHint: "Absolute path to a folder containing MANIFEST.json", kbBrowse: "Browse…", kbBrowseUnavailable: "Enter the folder path manually.", kbBrowseFailed: "Browse failed: {detail}", kbStatusConfigured: "{operator} ({country}) · v{version}", kbStatusUnconfigured: "Not configured", kbStatusError: "Load failed", alarmPushEnabled: "Enable push", alarmPushPhaseDisabled: "Off", alarmPushPhaseIdle: "Idle", alarmPushPhaseConnecting: "Connecting…", alarmPushPhaseAuthenticating: "Authenticating…", alarmPushPhaseConnected: "Connected", alarmPushPhaseReconnecting: "Reconnecting…", alarmPushPhaseAuthFailed: "Auth failed", alarmPushPhaseError: "Error", alarmDeliverDsh: "Deliver to DSH session", alarmDeliverIm: "Deliver to WhatsApp / IM", imTargetNone: "No targets. Add one in IM settings.", imTargetSelectedCount: "{count} selected", imCatalogUnavailable: "IM delivery unavailable", apiToken: "API token", apiTokenHint: "Leave blank to keep the current token", apiTokenUnavailable: "Credential service unavailable", apiTokenSet: "Configured", apiTokenUnset: "Not set", toolsStatusMounted: "{count} active tools", toolsStatusEmpty: "No active tools", toolsStatusError: "Publication failed", toolsStatusUnknown: "Status unavailable", sessionsExportCount: "{count} sessions", sessionsExportUnavailable: "Export unavailable", sessionsExportNoPersistence: "Session store unavailable.", sessionsExportNoRawArtifacts: "This session store does not support export.", sessionsExportListFailed: "Failed to list sessions: {detail}", sessionsExportRpcUnavailable: "Export service unavailable.", sessionsExportRpcFailed: "Cannot load export status.", sessionsExportHttpFailed: "Export failed: HTTP {status}{detail}", sessionsExportEmpty: "Export failed: empty ZIP body.", sessionsExportButton: "Download ZIP", sessionsExportBusy: "Preparing…", sessionsExportDone: "Downloaded: {file}", reset: "Reset", invalid: "Invalid", unsaved: "Unsaved changes", readOnly: "Read-only", saveFailed: "Save failed", discard: "Discard", save: "Save", saving: "Saving…" }; var zh = { settingsUnavailable: "正在连接 Host 设置…", toolsEnabled: "启用工具与技能", toolsDisabled: "已关闭", selectedPresets: "其他预设", presetCatalogUnavailable: "无法加载预设列表,已有选择已保留。", presetUnavailable: "预设不可用", noOtherPresets: "暂无其他预设。", mountStatusHint: "当前活跃会话中的工具注册情况;API 权限在调用时验证。", mountDetails: "诊断信息", title: "Netx Ops", sectionConnection: "连接", sectionLanguage: "模型语言", languageScope: "适用于所有预设", languageAuto: "自动", languageFollowUser: "跟随用户", languageChinese: "中文", languageEnglish: "English", sectionCapabilities: "工具与技能", sectionKnowledge: "知识库", sectionAlarms: "关键告警投递", sectionExport: "会话导出", apiUrl: "API 地址", apiUrlHint: "http://127.0.0.1:8890", apiUrlInvalid: "请输入不含账号密码的 HTTP 或 HTTPS 地址。", lang: "API / 告警语言", thinkingLanguage: "思考语言", replyLanguage: "回复语言", nmsProvider: "NMS 提供方", groupOps: "告警、资产与 CLI 运维", groupTopology: "拓扑与布局", groupBizMonitor: "割接业务监控", groupKb: "知识库技能", groupInPreset: "Netx Ops", groupPublic: "所有预设", kbRoot: "知识包根目录", kbRootHint: "含 MANIFEST.json 的知识包绝对路径", kbBrowse: "浏览…", kbBrowseUnavailable: "请手动输入目录路径。", kbBrowseFailed: "浏览失败:{detail}", kbStatusConfigured: "{operator}({country})· v{version}", kbStatusUnconfigured: "未配置", kbStatusError: "加载失败", alarmPushEnabled: "启用推送", alarmPushPhaseDisabled: "未开启", alarmPushPhaseIdle: "空闲", alarmPushPhaseConnecting: "连接中…", alarmPushPhaseAuthenticating: "鉴权中…", alarmPushPhaseConnected: "已连接", alarmPushPhaseReconnecting: "重连中…", alarmPushPhaseAuthFailed: "鉴权失败", alarmPushPhaseError: "异常", alarmDeliverDsh: "投递到 DSH 会话", alarmDeliverIm: "投递到 WhatsApp / IM", imTargetNone: "暂无目标,请在 IM 设置中添加。", imTargetSelectedCount: "已选 {count} 个", imCatalogUnavailable: "IM 投递不可用", apiToken: "API Token", apiTokenHint: "留空保留当前凭据", apiTokenUnavailable: "凭据服务不可用", apiTokenSet: "已配置", apiTokenUnset: "未设置", toolsStatusMounted: "{count} 个工具", toolsStatusEmpty: "暂无活跃工具", toolsStatusError: "发布异常", toolsStatusUnknown: "状态不可用", sessionsExportCount: "{count} 个会话", sessionsExportUnavailable: "无法导出", sessionsExportNoPersistence: "会话存储不可用。", sessionsExportNoRawArtifacts: "当前会话存储不支持导出。", sessionsExportListFailed: "列出会话失败:{detail}", sessionsExportRpcUnavailable: "导出服务不可用。", sessionsExportRpcFailed: "无法获取导出状态。", sessionsExportHttpFailed: "导出失败:HTTP {status}{detail}", sessionsExportEmpty: "导出失败:ZIP 为空。", sessionsExportButton: "下载 ZIP", sessionsExportBusy: "准备中…", sessionsExportDone: "已下载:{file}", reset: "重置", invalid: "无效", unsaved: "有未保存更改", readOnly: "只读", saveFailed: "保存失败", discard: "丢弃", save: "保存", saving: "保存中…" }; // src/client/settings-form.ts function createWaitingSettingsScope(initial) { const snapshot = { status: "loading", value: initial, writable: false }; return { getSnapshot: () => snapshot, subscribe: () => () => {}, set: async () => { throw new Error("settings_transport_unavailable"); }, unset: async () => { throw new Error("settings_transport_unavailable"); } }; } function findReadySettingsForm(forms, namespaces) { if (!forms.get) return; for (const ns of namespaces) { try { const scope = forms.get(ns); if (scope?.getSnapshot().status === "ready") return { ns, scope }; } catch {} } return; } // src/client/index.ts var LOCALE_NS = "settings.netxops"; var inject = [ "slots", "locale" ]; function namespaceCandidates(ctx) { const fiber = ctx.fiber?.entry; const raw = [ fiber?.options?.id, fiber?.id, fiber?.options?.name, NETXOPS_NS, "dsh-netxops" ]; const out = []; for (const item of raw) { if (typeof item !== "string" || !item.trim()) continue; const id = item.replace(/^:/, "").trim(); if (!id || out.includes(id)) continue; if (id === "dsh-netxops" && !out.includes(NETXOPS_NS)) out.push(NETXOPS_NS); out.push(id); } if (!out.includes(NETXOPS_NS)) out.push(NETXOPS_NS); return out; } function createMemoryScope(initial) { return createWaitingSettingsScope({ ...initial }); } function createDeferredScope(fallback) { let inner = fallback; const outerListeners = new Set; let innerOff; const relay = () => { for (const listener of outerListeners) listener(); }; const bindInner = () => { innerOff?.(); innerOff = inner.subscribe(relay); }; bindInner(); return { getSnapshot: () => inner.getSnapshot(), subscribe: (listener) => { outerListeners.add(listener); return () => { outerListeners.delete(listener); }; }, set: (field, next) => inner.set(field, next), unset: (field) => inner.unset(field), attach: (real) => { if (inner === real) return; inner = real; bindInner(); relay(); } }; } var MEMORY_DEFAULTS = { toolsEnabled: true, groupOpsPresets: "", groupTopologyPresets: "", groupBizMonitorPresets: "", groupKbPresets: "", apiUrl: "http://127.0.0.1:8890", lang: "zh", thinkingLanguage: "auto", replyLanguage: "follow-user", nmsProvider: "zte-ume", tokenCredentialRef: "NETX_API_TOKEN", alarmPushEnabled: false, alarmDeliverDsh: true, alarmDeliverIm: false, imBotId: "", imTargetId: "", imTargets: "", groupOpsInPreset: true, groupOpsPublic: false, groupTopologyInPreset: false, groupTopologyPublic: false, groupBizMonitorInPreset: true, groupBizMonitorPublic: false, kbRoot: "", groupKbInPreset: true, groupKbPublic: false }; function apply(ctx) { ctx.effect(() => { try { return ctx.locale.register(LOCALE_NS, { zh, en }); } catch { const offZh = ctx.locale.register(LOCALE_NS, "zh", zh); const offEn = ctx.locale.register(LOCALE_NS, "en", en); return () => { offZh?.(); offEn?.(); }; } }, "netxops: locales"); const t = ctx.locale.bind(LOCALE_NS); const deferred = createDeferredScope(createMemoryScope(MEMORY_DEFAULTS)); const card = new NetxopsCardController(deferred, ctx); let pluginsItemId; let wiredExtras = false; const ensureExtras = () => { if (wiredExtras) return; wiredExtras = true; try { wireOptionalServices(ctx, card); } catch (error) { ctx.logger?.warn?.("netxops: optional remotes skipped: %s", error); } }; try { ctx.slots.inject("settings.section", () => ctx.slots.register({ name: "settings.section", id: NETXOPS_SECTION_ID, order: 24, label: () => t("title"), locale: LOCALE_NS, inject: () => { ensureExtras(); return card.inject(); } }, NetxopsCard)); } catch (error) { ctx.logger?.warn?.("netxops: settings.section unavailable: %s", error); } const registerPluginsItem = (id) => { if (pluginsItemId === id) return () => {}; try { const off = ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({ name: "settings.plugin.item", key: id, locale: LOCALE_NS, inject: () => { ensureExtras(); return card.inject(); } }, NetxopsCard)); pluginsItemId = id; return () => { if (pluginsItemId === id) pluginsItemId = undefined; off(); }; } catch (error) { ctx.logger?.warn?.("netxops: plugins.item unavailable: %s", error); return () => {}; } }; const tryGetForm = (forms, candidates) => { return findReadySettingsForm(forms, candidates); }; const attachForm = (hit, source) => { ctx.logger?.info?.("netxops: attach form via %s (%s)", source, hit.ns); deferred.attach(hit.scope); ensureExtras(); return registerPluginsItem(hit.ns); }; let formsAttached = false; const tryAttachForms = () => { if (formsAttached) return true; try { const forms = ctx.get("configForms"); if (!forms || typeof forms.get !== "function") return false; const candidates = namespaceCandidates(ctx); const hit = tryGetForm(forms, candidates); if (!hit) return false; ctx.logger?.info?.("netxops: configForms candidates=%s", candidates.join(",")); ctx.effect(() => attachForm(hit, "poll"), "netxops: configForms poll attach"); if (typeof forms.whileServed === "function") { ctx.effect(() => forms.whileServed(candidates, (served) => { const ns = candidates.find((id) => served.has(id)); if (!ns) { ctx.logger?.warn?.("netxops: whileServed fired but none of %s are served (have=%s)", candidates.join(","), [...served].join(",") || "(empty)"); return () => {}; } const next = tryGetForm(forms, [ns, ...candidates]); if (!next) { ctx.logger?.warn?.("netxops: served %s but configForms.get failed", ns); return () => {}; } return attachForm(next, "whileServed"); }), "netxops: configForms whileServed"); } formsAttached = true; return true; } catch { return false; } }; const tryLegacySettingsScope = () => { if (formsAttached) return true; try { const getter = ctx.get; if (typeof getter !== "function") return false; const binder = getter.call(ctx, "settingsScope"); if (!binder || typeof binder.bind !== "function") return false; ctx.logger?.info?.("netxops: attach form via settingsScope (legacy one-shot)"); deferred.attach(binder.bind({ namespace: NETXOPS_NS })); ensureExtras(); formsAttached = true; return true; } catch (error) { ctx.logger?.warn?.("netxops: legacy settingsScope probe failed: %s", error); return false; } }; const startFormsPoll = () => { if (tryAttachForms() || tryLegacySettingsScope()) return; let ticks = 0; const timer = setInterval(() => { ticks += 1; if (tryAttachForms() || ticks >= 3 && tryLegacySettingsScope()) { clearInterval(timer); } }, 1000); try { ctx.effect(() => () => clearInterval(timer), "netxops: configForms poll"); } catch { clearInterval(timer); } }; setTimeout(startFormsPoll, 0); } function wireOptionalServices(ctx, card) { const soft = (deps, run, label) => { try { ctx.inject(deps, (inner) => { try { run(inner); } catch (error) { inner.logger?.warn?.("netxops: %s handler failed: %s", label, error); } }); } catch (error) { ctx.logger?.warn?.("netxops: soft-inject %s skipped: %s", label, error); } }; soft(["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"); }, "remote.credentials"); soft(["connection"], (connCtx) => { const connection = connCtx.get("connection"); const call = connection?.rpc?.call?.bind(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"); }, "connection"); const bindDirectoryPicker = (picker) => { if (!picker || typeof picker.pick !== "function") return; card.setDirectoryPicker({ pick: picker.pick.bind(picker) }); }; bindDirectoryPicker(ctx.get("remote.directoryPicker")); soft(["remote.directoryPicker"], (dpCtx) => { const viaGet = typeof dpCtx.get === "function" ? dpCtx.get("remote.directoryPicker") : undefined; const viaNested = dpCtx.remote?.directoryPicker; bindDirectoryPicker(viaGet ?? viaNested); dpCtx.effect(() => () => { card.setDirectoryPicker(undefined); }, "netxops: clear directory picker"); }, "remote.directoryPicker"); } return module.exports; } });