netxops/lib/client.js

3406 lines
117 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
});