@usex/mikrotik-mcp 3.46.0 → 3.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -89,7 +89,7 @@ import {
89
89
  toggleAaaEntity,
90
90
  updateAaaEntity,
91
91
  writeBackup
92
- } from "./shared/cli-k2ghtqd0.js";
92
+ } from "./shared/cli-rcs47w19.js";
93
93
 
94
94
  // src/cli.ts
95
95
  import { existsSync as existsSync2 } from "fs";
@@ -652,7 +652,8 @@ function rowToEvent(r) {
652
652
  output: r.output,
653
653
  outputBytes: r.output_bytes,
654
654
  hasStructured: r.has_structured === 1,
655
- truncated: r.truncated === 1
655
+ truncated: r.truncated === 1,
656
+ reason: r.reason ?? undefined
656
657
  };
657
658
  }
658
659
  var SCHEMA_STATEMENTS = [
@@ -671,12 +672,14 @@ var SCHEMA_STATEMENTS = [
671
672
  output TEXT NOT NULL,
672
673
  output_bytes INTEGER NOT NULL,
673
674
  has_structured INTEGER NOT NULL,
674
- truncated INTEGER NOT NULL
675
+ truncated INTEGER NOT NULL,
676
+ reason TEXT
675
677
  )`,
676
678
  "CREATE INDEX IF NOT EXISTS idx_events_ts ON events(ts)",
677
679
  "CREATE INDEX IF NOT EXISTS idx_events_tool ON events(tool)",
678
680
  "CREATE INDEX IF NOT EXISTS idx_events_error ON events(is_error)"
679
681
  ];
682
+ var MIGRATIONS = ["ALTER TABLE events ADD COLUMN reason TEXT"];
680
683
 
681
684
  class SqliteEventStore {
682
685
  db;
@@ -686,13 +689,18 @@ class SqliteEventStore {
686
689
  db.run("PRAGMA synchronous = NORMAL");
687
690
  for (const stmt of SCHEMA_STATEMENTS)
688
691
  db.run(stmt);
692
+ for (const m of MIGRATIONS) {
693
+ try {
694
+ db.run(m);
695
+ } catch {}
696
+ }
689
697
  }
690
698
  insert(e) {
691
699
  this.db.query(`INSERT OR REPLACE INTO events
692
700
  (id, ts, tool, title, risk, device, transport, duration_ms, is_error, error,
693
- input, output, output_bytes, has_structured, truncated)
701
+ input, output, output_bytes, has_structured, truncated, reason)
694
702
  VALUES ($id,$ts,$tool,$title,$risk,$device,$transport,$dur,$err,$errmsg,
695
- $input,$output,$obytes,$structured,$trunc)`).run({
703
+ $input,$output,$obytes,$structured,$trunc,$reason)`).run({
696
704
  $id: e.id,
697
705
  $ts: e.ts,
698
706
  $tool: e.tool,
@@ -707,7 +715,8 @@ class SqliteEventStore {
707
715
  $output: e.output,
708
716
  $obytes: e.outputBytes,
709
717
  $structured: e.hasStructured ? 1 : 0,
710
- $trunc: e.truncated ? 1 : 0
718
+ $trunc: e.truncated ? 1 : 0,
719
+ $reason: e.reason ?? null
711
720
  });
712
721
  }
713
722
  query(filter) {
@@ -738,7 +747,7 @@ class SqliteEventStore {
738
747
  params.$until = filter.until;
739
748
  }
740
749
  if (filter.q) {
741
- where.push("(tool LIKE $q OR title LIKE $q OR input LIKE $q OR output LIKE $q OR error LIKE $q)");
750
+ where.push("(tool LIKE $q OR title LIKE $q OR input LIKE $q OR output LIKE $q OR error LIKE $q OR reason LIKE $q)");
742
751
  params.$q = `%${filter.q}%`;
743
752
  }
744
753
  const clause = where.length ? `WHERE ${where.join(" AND ")}` : "";
@@ -1134,7 +1143,7 @@ function computeStats(events, opts) {
1134
1143
  // package.json
1135
1144
  var package_default = {
1136
1145
  name: "@usex/mikrotik-mcp",
1137
- version: "3.46.0",
1146
+ version: "3.48.0",
1138
1147
  description: "MCP server for MikroTik RouterOS \u2014 660+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
1139
1148
  keywords: [
1140
1149
  "ai",
@@ -1210,6 +1219,8 @@ var package_default = {
1210
1219
  "@modelcontextprotocol/ext-apps": "^1.7.4",
1211
1220
  "@modelcontextprotocol/sdk": "^1.29.0",
1212
1221
  "@tikoci/centrs": "^0.1.0",
1222
+ figlet: "^1.11.0",
1223
+ "gradient-string": "^3.0.0",
1213
1224
  "ipaddr.js": "^2.4.0",
1214
1225
  ssh2: "^1.17.0",
1215
1226
  zod: "^4.4.3"
@@ -1218,7 +1229,8 @@ var package_default = {
1218
1229
  "@resvg/resvg-js": "^2.6.2",
1219
1230
  "@tailwindcss/vite": "^4.3.2",
1220
1231
  "@types/bun": "latest",
1221
- "@types/node": "^26.0.1",
1232
+ "@types/figlet": "^1.7.0",
1233
+ "@types/node": "^26.1.0",
1222
1234
  "@types/react": "^19.2.17",
1223
1235
  "@types/react-dom": "^19.2.3",
1224
1236
  "@types/ssh2": "^1.15.5",
@@ -1230,12 +1242,12 @@ var package_default = {
1230
1242
  prettier: "^3.9.4",
1231
1243
  react: "^19.2.7",
1232
1244
  "react-dom": "^19.2.7",
1233
- recharts: "^3.9.0",
1245
+ recharts: "^3.9.1",
1234
1246
  "release-it": "^20.2.1",
1235
1247
  "tailwind-merge": "^3.6.0",
1236
1248
  tailwindcss: "^4.3.2",
1237
1249
  vite: "npm:@voidzero-dev/vite-plus-core@latest",
1238
- "vite-plus": "latest"
1250
+ "vite-plus": "^0.2.2"
1239
1251
  },
1240
1252
  peerDependencies: {
1241
1253
  typescript: "^6.0.3"
@@ -2479,6 +2491,19 @@ interfaces, IP addressing, DHCP, DNS, firewall (filter + NAT), routing, VLANs,
2479
2491
  wireless, WireGuard, queues/QoS, users, logs, backups, PoE, system, network
2480
2492
  tools, bridges, address-lists, scheduler/scripts and certificates.
2481
2493
 
2494
+ Tool discovery \u2014 MANDATORY workflow:
2495
+ This server has several hundred dedicated tools, but the host only surfaces a
2496
+ small subset at a time. You MUST follow this order:
2497
+ 1. ALWAYS call \`find_tools\` FIRST \u2014 describe what you want to do and it
2498
+ searches the full catalog for the best-matching dedicated tool.
2499
+ 2. (Optional) Call \`describe_tool\` to get the exact parameter schema.
2500
+ 3. Call the tool directly if it is listed, or via \`invoke_tool\` if not.
2501
+ 4. ONLY if \`find_tools\` returned ZERO results, fall back to
2502
+ \`run_routeros_command\` as a last resort.
2503
+ Dedicated tools have schema validation, structured output, and correct risk
2504
+ annotations \u2014 \`run_routeros_command\` has none of these. Always prefer the
2505
+ dedicated tool path.
2506
+
2482
2507
  Safety model \u2014 tools are annotated by risk:
2483
2508
  \u2022 readOnlyHint \u2192 inspection only, no changes
2484
2509
  \u2022 destructiveHint \u2192 removes or replaces configuration
@@ -2669,6 +2694,59 @@ async function runStdio() {
2669
2694
  logger.info(`MCP server ready on stdio \u2014 ${toolCount} tools, ${promptCount} prompts, ${uiViewCount} app views${readOnly ? " [READ-ONLY]" : ""}`);
2670
2695
  }
2671
2696
 
2697
+ // src/cli-logo.ts
2698
+ import figlet from "figlet";
2699
+ import gradient from "gradient-string";
2700
+ var BRAND = gradient(["#9d7bff", "#3291ff", "#2dd4bf"]);
2701
+ var ESC = "\x1B[";
2702
+ var R = `${ESC}0m`;
2703
+ var DIM = `${ESC}90m`;
2704
+ var BOLD = `${ESC}1m`;
2705
+ var ITALIC = `${ESC}3m`;
2706
+ var rgb = (r, g, b) => `${ESC}38;2;${r};${g};${b}m`;
2707
+ var CV = rgb(157, 123, 255);
2708
+ var CS = rgb(50, 145, 255);
2709
+ var CT = rgb(45, 212, 191);
2710
+ var CA = rgb(245, 166, 35);
2711
+ async function printBanner() {
2712
+ if (!process.stderr.isTTY)
2713
+ return;
2714
+ try {
2715
+ const art = figlet.textSync("MikroTik MCP", { font: "Small" });
2716
+ const colored = BRAND.multiline(art);
2717
+ process.stderr.write(`${colored}
2718
+ `);
2719
+ const author = typeof package_default.author === "object" ? package_default.author : { name: String(package_default.author) };
2720
+ const site = (package_default.homepage ?? "").replace(/^https?:\/\//, "");
2721
+ const w = 56;
2722
+ const hr = "\u2500".repeat(w);
2723
+ const pad = (s, vis) => `${s}${" ".repeat(Math.max(0, w - 2 - vis))}`;
2724
+ const r1Label = `\u25CF v${VERSION}`;
2725
+ const r1Mid = "RouterOS over SSH";
2726
+ const r1End = "MCP Protocol";
2727
+ const r1Text = ` ${CV}\u25CF${R} ${BOLD}v${VERSION}${R}${DIM} \u2502 ${CS}${r1Mid}${R}${DIM} \u2502 ${CT}${r1End}${R}`;
2728
+ const r1Vis = r1Label.length + 2 + r1Mid.length + 4 + r1End.length + 4;
2729
+ const r2Name = author.name ?? "Unknown";
2730
+ const r2License = package_default.license ?? "MIT";
2731
+ const r2Text = ` ${CA}${r2Name}${R}${DIM} \xB7 ${ITALIC}${r2License}${R}${DIM} \xB7 ${CS}${site}${R}`;
2732
+ const r2Vis = r2Name.length + 2 + r2License.length + 4 + site.length + 4;
2733
+ const box = [
2734
+ `${DIM} \u250C${hr}\u2510${R}`,
2735
+ `${DIM} \u2502${R}${pad(r1Text, r1Vis)}${DIM}\u2502${R}`,
2736
+ `${DIM} \u251C${hr}\u2524${R}`,
2737
+ `${DIM} \u2502${R}${pad(r2Text, r2Vis)}${DIM}\u2502${R}`,
2738
+ `${DIM} \u2514${hr}\u2518${R}`
2739
+ ].join(`
2740
+ `);
2741
+ process.stderr.write(`${box}
2742
+
2743
+ `);
2744
+ } catch {
2745
+ process.stderr.write(`${DIM}MikroTik MCP v${VERSION}${R}
2746
+ `);
2747
+ }
2748
+ }
2749
+
2672
2750
  // src/cli.ts
2673
2751
  var HELP = `${SERVER_NAME} v${VERSION} \u2014 MikroTik RouterOS MCP server
2674
2752
 
@@ -2811,6 +2889,7 @@ async function main() {
2811
2889
  }
2812
2890
  const cfg = loadConfig();
2813
2891
  setConfig(cfg);
2892
+ await printBanner();
2814
2893
  warnIfPlaintextPasswordInContainer(Object.values(cfg.devices).some((d) => !!d.password));
2815
2894
  const deviceNames = Object.keys(cfg.devices);
2816
2895
  logger.info(`Starting ${SERVER_NAME} v${VERSION} (transport=${cfg.mcp.transport}, ` + `devices=${deviceNames.length === 1 ? deviceNames[0] : deviceNames.join("/")}, ` + `ssh-pool=${cfg.ssh.keepAlive ? "on" : "off"})`);
package/dist/index.js CHANGED
@@ -21,7 +21,7 @@ import {
21
21
  resolveDeviceName,
22
22
  selectToolModules,
23
23
  setConfig
24
- } from "./shared/library-nb4wss02.js";
24
+ } from "./shared/library-esjkmrg2.js";
25
25
  // src/server.ts
26
26
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
27
27
  import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
@@ -131,7 +131,7 @@ function registerPrompts(server) {
131
131
  // package.json
132
132
  var package_default = {
133
133
  name: "@usex/mikrotik-mcp",
134
- version: "3.46.0",
134
+ version: "3.48.0",
135
135
  description: "MCP server for MikroTik RouterOS \u2014 660+ tools over SSH for firewall, NAT, routing, DHCP, DNS, WireGuard, wireless, QoS and more.",
136
136
  keywords: [
137
137
  "ai",
@@ -207,6 +207,8 @@ var package_default = {
207
207
  "@modelcontextprotocol/ext-apps": "^1.7.4",
208
208
  "@modelcontextprotocol/sdk": "^1.29.0",
209
209
  "@tikoci/centrs": "^0.1.0",
210
+ figlet: "^1.11.0",
211
+ "gradient-string": "^3.0.0",
210
212
  "ipaddr.js": "^2.4.0",
211
213
  ssh2: "^1.17.0",
212
214
  zod: "^4.4.3"
@@ -215,7 +217,8 @@ var package_default = {
215
217
  "@resvg/resvg-js": "^2.6.2",
216
218
  "@tailwindcss/vite": "^4.3.2",
217
219
  "@types/bun": "latest",
218
- "@types/node": "^26.0.1",
220
+ "@types/figlet": "^1.7.0",
221
+ "@types/node": "^26.1.0",
219
222
  "@types/react": "^19.2.17",
220
223
  "@types/react-dom": "^19.2.3",
221
224
  "@types/ssh2": "^1.15.5",
@@ -227,12 +230,12 @@ var package_default = {
227
230
  prettier: "^3.9.4",
228
231
  react: "^19.2.7",
229
232
  "react-dom": "^19.2.7",
230
- recharts: "^3.9.0",
233
+ recharts: "^3.9.1",
231
234
  "release-it": "^20.2.1",
232
235
  "tailwind-merge": "^3.6.0",
233
236
  tailwindcss: "^4.3.2",
234
237
  vite: "npm:@voidzero-dev/vite-plus-core@latest",
235
- "vite-plus": "latest"
238
+ "vite-plus": "^0.2.2"
236
239
  },
237
240
  peerDependencies: {
238
241
  typescript: "^6.0.3"
@@ -277,6 +280,19 @@ interfaces, IP addressing, DHCP, DNS, firewall (filter + NAT), routing, VLANs,
277
280
  wireless, WireGuard, queues/QoS, users, logs, backups, PoE, system, network
278
281
  tools, bridges, address-lists, scheduler/scripts and certificates.
279
282
 
283
+ Tool discovery \u2014 MANDATORY workflow:
284
+ This server has several hundred dedicated tools, but the host only surfaces a
285
+ small subset at a time. You MUST follow this order:
286
+ 1. ALWAYS call \`find_tools\` FIRST \u2014 describe what you want to do and it
287
+ searches the full catalog for the best-matching dedicated tool.
288
+ 2. (Optional) Call \`describe_tool\` to get the exact parameter schema.
289
+ 3. Call the tool directly if it is listed, or via \`invoke_tool\` if not.
290
+ 4. ONLY if \`find_tools\` returned ZERO results, fall back to
291
+ \`run_routeros_command\` as a last resort.
292
+ Dedicated tools have schema validation, structured output, and correct risk
293
+ annotations \u2014 \`run_routeros_command\` has none of these. Always prefer the
294
+ dedicated tool path.
295
+
280
296
  Safety model \u2014 tools are annotated by risk:
281
297
  \u2022 readOnlyHint \u2192 inspection only, no changes
282
298
  \u2022 destructiveHint \u2192 removes or replaces configuration
@@ -4,7 +4,7 @@ import {
4
4
  allToolModules,
5
5
  moduleCatalog,
6
6
  selectToolModules
7
- } from "./cli-k2ghtqd0.js";
7
+ } from "./cli-rcs47w19.js";
8
8
  export {
9
9
  selectToolModules,
10
10
  moduleCatalog,
@@ -1990,7 +1990,8 @@ function buildEvent(raw, id, opts) {
1990
1990
  output,
1991
1991
  outputBytes,
1992
1992
  hasStructured: raw.hasStructured,
1993
- truncated
1993
+ truncated,
1994
+ reason: raw.reason
1994
1995
  };
1995
1996
  }
1996
1997
 
@@ -2102,13 +2103,20 @@ function defineTool(def) {
2102
2103
  const multiDevice = !!deviceNames && deviceNames.length > 1;
2103
2104
  const { ui, auto } = appViews === false ? { ui: undefined, auto: false } : effectiveUi(def);
2104
2105
  const selectorNames = multiDevice && deviceNames ? [...new Set([...deviceNames, ...deviceAliases ?? []])] : [];
2105
- const inputSchema = multiDevice ? {
2106
+ let inputSchema = multiDevice ? {
2106
2107
  ...def.inputSchema,
2107
2108
  device: z2.enum(selectorNames).optional().describe(deviceSelectorDescription(selectorNames, deviceDirectory2))
2108
2109
  } : def.inputSchema;
2109
2110
  const risk = riskOf(def.annotations);
2111
+ const requiresReason = risk === "DANGEROUS";
2112
+ if (requiresReason) {
2113
+ inputSchema = {
2114
+ ...inputSchema,
2115
+ reason: z2.string().describe("Your rationale for performing this high-impact action \u2014 explain in one concise " + "sentence why this operation is necessary. Logged for audit and accountability.")
2116
+ };
2117
+ }
2110
2118
  const callback = async (args) => {
2111
- const { device, ...rest } = args;
2119
+ const { device, reason, ...rest } = args;
2112
2120
  const deviceName = typeof device === "string" ? device : undefined;
2113
2121
  const ctx = createContext(sendLog, deviceName);
2114
2122
  const deviceStamp = multiDevice && risk !== "READ" ? (() => {
@@ -2174,7 +2182,8 @@ function defineTool(def) {
2174
2182
  error: errMsg,
2175
2183
  args: rest,
2176
2184
  output: outText,
2177
- hasStructured
2185
+ hasStructured,
2186
+ reason: typeof reason === "string" ? reason : undefined
2178
2187
  });
2179
2188
  }
2180
2189
  }
@@ -6179,7 +6188,9 @@ var rawCommandTools = [
6179
6188
  name: "run_routeros_command",
6180
6189
  title: "Run a RouterOS Command (raw CLI)",
6181
6190
  annotations: WRITE,
6182
- description: "Execute ANY single RouterOS / MikroTik CLI command on the device and return its console output \u2014 the " + "universal primitive to read or change anything when no dedicated tool fits, for uncommon or bulk " + "operations, or simply when you can't quickly find the specific tool among the catalog. " + "Pass the command EXACTLY as typed in the RouterOS terminal, e.g. " + "`/system identity set name=Router1`, " + "`/ip dns set servers=1.1.1.1,8.8.8.8 allow-remote-requests=yes`, " + "`/ip address print`, `/ip firewall filter print`, `/interface print`, `/ip service print`, " + "`/system resource print`. " + "Works for reads (`\u2026 print`) and writes (`\u2026 set` / `add` / `remove`). Runs one command per call \u2014 " + "call again with a `\u2026 print` to verify a change. Prefer a dedicated tool when you know its name " + "(better validation and structured output); use this whenever discovery is the bottleneck. " + "WARNING: the command is run as-is and is NOT validated \u2014 it can be destructive (e.g. " + "`/system reset-configuration`, removing the management address), so review it before running. " + "For a stored multi-command script use add_script then run_script.",
6191
+ description: "\u26A0\uFE0F LAST RESORT \u2014 do NOT call this tool directly. ALWAYS call `find_tools` first to locate the " + "dedicated tool for your task, then run it via `invoke_tool`. Dedicated tools provide schema " + "validation, structured output, and accurate risk annotations that this raw command lacks. " + "Only use this tool when `find_tools` returned zero results for your query AND you are certain " + `no dedicated tool exists.
6192
+
6193
+ ` + "When you do use it: pass one RouterOS CLI command exactly as typed in the terminal. " + "The command is executed as-is with NO validation \u2014 it can be destructive, so review carefully. " + "Runs one command per call; call again with `\u2026 print` to verify a change. " + "For multi-command scripts use add_script + run_script instead.",
6183
6194
  inputSchema: {
6184
6195
  command: z17.string().describe("Full RouterOS CLI command exactly as typed, e.g. '/ip address print'")
6185
6196
  },
@@ -6310,7 +6321,7 @@ var cache = null;
6310
6321
  async function gateway() {
6311
6322
  if (cache)
6312
6323
  return cache;
6313
- const { moduleCatalog } = await import("./cli-1e5sj65w.js");
6324
+ const { moduleCatalog } = await import("./cli-frtfsqcq.js");
6314
6325
  const forIndex = [];
6315
6326
  const byName = new Map;
6316
6327
  for (const mod of moduleCatalog) {
@@ -6348,7 +6359,9 @@ var toolGatewayTools = [
6348
6359
  name: "find_tools",
6349
6360
  title: "Find Tools (catalog search)",
6350
6361
  annotations: READ,
6351
- description: "Search the FULL MikroTik tool catalog by intent and get back the tools that best match \u2014 " + "use this FIRST whenever you can't immediately see the specific tool you need among the listed tools (the host only surfaces a subset of the several-hundred-tool catalog). Describe the task in natural language or keywords, e.g. 'block a LAN client by MAC', 'add an IPv4 firewall filter rule', 'import a TLS certificate', 'list DHCP leases', 'create a WireGuard peer'. Returns each match's exact tool name, what it does, and its " + "parameter names \u2014 then call it directly if it's already available, or run it via " + "`invoke_tool`. For IPv4 vs IPv6, say which: 'ipv4'/'ipv6' disambiguates the result.",
6362
+ description: "\uD83D\uDD0D START HERE \u2014 this is the PRIMARY entry point for ANY MikroTik / RouterOS task. " + "ALWAYS call this tool FIRST before attempting any other tool. This server has several hundred dedicated tools with full validation and structured output, but the host only surfaces a small " + `subset \u2014 this tool searches the FULL catalog and finds the best match for your intent.
6363
+
6364
+ ` + "Describe what you want to do in natural language or keywords, e.g. 'block a LAN client by MAC', 'add an IPv4 firewall filter rule', 'import a TLS certificate', 'list DHCP leases', " + "'create a WireGuard peer'. Returns each match's exact tool name, description, and parameters \u2014 " + "then call it directly if available, or via `invoke_tool`.\n\n" + "Workflow: find_tools \u2192 (optional) describe_tool \u2192 invoke_tool. " + "Only fall back to run_routeros_command if this search returns zero results. For IPv4 vs IPv6, include 'ipv4' or 'ipv6' to disambiguate.",
6352
6365
  inputSchema: {
6353
6366
  query: z18.string().min(1).describe("What you want to do, in words or keywords (e.g. 'add ipv4 nat masquerade rule')."),
6354
6367
  limit: z18.coerce.number().int().min(1).max(25).optional().describe("Max results to return (default 8).")
@@ -24769,7 +24782,7 @@ ${result}`;
24769
24782
  const interfaceType = await detectWirelessInterfaceType(ctx);
24770
24783
  if (!interfaceType)
24771
24784
  return "Error: No wireless interface support detected on this device.";
24772
- const scanCmd = new Cmd(`${interfaceType} scan`).raw(a.interface).set("duration", a.duration).build();
24785
+ const scanCmd = new Cmd(`${interfaceType} scan`).raw(quoteValue(a.interface)).set("duration", a.duration).build();
24773
24786
  const result = await executeMikrotikCommand(scanCmd, ctx);
24774
24787
  if (looksLikeError(result))
24775
24788
  return `Failed to scan wireless networks: ${result}`;
@@ -1971,7 +1971,8 @@ function buildEvent(raw, id, opts) {
1971
1971
  output,
1972
1972
  outputBytes,
1973
1973
  hasStructured: raw.hasStructured,
1974
- truncated
1974
+ truncated,
1975
+ reason: raw.reason
1975
1976
  };
1976
1977
  }
1977
1978
 
@@ -2065,13 +2066,20 @@ function defineTool(def) {
2065
2066
  const multiDevice = !!deviceNames && deviceNames.length > 1;
2066
2067
  const { ui, auto } = appViews === false ? { ui: undefined, auto: false } : effectiveUi(def);
2067
2068
  const selectorNames = multiDevice && deviceNames ? [...new Set([...deviceNames, ...deviceAliases ?? []])] : [];
2068
- const inputSchema = multiDevice ? {
2069
+ let inputSchema = multiDevice ? {
2069
2070
  ...def.inputSchema,
2070
2071
  device: z2.enum(selectorNames).optional().describe(deviceSelectorDescription(selectorNames, deviceDirectory2))
2071
2072
  } : def.inputSchema;
2072
2073
  const risk = riskOf(def.annotations);
2074
+ const requiresReason = risk === "DANGEROUS";
2075
+ if (requiresReason) {
2076
+ inputSchema = {
2077
+ ...inputSchema,
2078
+ reason: z2.string().describe("Your rationale for performing this high-impact action \u2014 explain in one concise " + "sentence why this operation is necessary. Logged for audit and accountability.")
2079
+ };
2080
+ }
2073
2081
  const callback = async (args) => {
2074
- const { device, ...rest } = args;
2082
+ const { device, reason, ...rest } = args;
2075
2083
  const deviceName = typeof device === "string" ? device : undefined;
2076
2084
  const ctx = createContext(sendLog, deviceName);
2077
2085
  const deviceStamp = multiDevice && risk !== "READ" ? (() => {
@@ -2137,7 +2145,8 @@ function defineTool(def) {
2137
2145
  error: errMsg,
2138
2146
  args: rest,
2139
2147
  output: outText,
2140
- hasStructured
2148
+ hasStructured,
2149
+ reason: typeof reason === "string" ? reason : undefined
2141
2150
  });
2142
2151
  }
2143
2152
  }
@@ -6120,7 +6129,9 @@ var rawCommandTools = [
6120
6129
  name: "run_routeros_command",
6121
6130
  title: "Run a RouterOS Command (raw CLI)",
6122
6131
  annotations: WRITE,
6123
- description: "Execute ANY single RouterOS / MikroTik CLI command on the device and return its console output \u2014 the " + "universal primitive to read or change anything when no dedicated tool fits, for uncommon or bulk " + "operations, or simply when you can't quickly find the specific tool among the catalog. " + "Pass the command EXACTLY as typed in the RouterOS terminal, e.g. " + "`/system identity set name=Router1`, " + "`/ip dns set servers=1.1.1.1,8.8.8.8 allow-remote-requests=yes`, " + "`/ip address print`, `/ip firewall filter print`, `/interface print`, `/ip service print`, " + "`/system resource print`. " + "Works for reads (`\u2026 print`) and writes (`\u2026 set` / `add` / `remove`). Runs one command per call \u2014 " + "call again with a `\u2026 print` to verify a change. Prefer a dedicated tool when you know its name " + "(better validation and structured output); use this whenever discovery is the bottleneck. " + "WARNING: the command is run as-is and is NOT validated \u2014 it can be destructive (e.g. " + "`/system reset-configuration`, removing the management address), so review it before running. " + "For a stored multi-command script use add_script then run_script.",
6132
+ description: "\u26A0\uFE0F LAST RESORT \u2014 do NOT call this tool directly. ALWAYS call `find_tools` first to locate the " + "dedicated tool for your task, then run it via `invoke_tool`. Dedicated tools provide schema " + "validation, structured output, and accurate risk annotations that this raw command lacks. " + "Only use this tool when `find_tools` returned zero results for your query AND you are certain " + `no dedicated tool exists.
6133
+
6134
+ ` + "When you do use it: pass one RouterOS CLI command exactly as typed in the terminal. " + "The command is executed as-is with NO validation \u2014 it can be destructive, so review carefully. " + "Runs one command per call; call again with `\u2026 print` to verify a change. " + "For multi-command scripts use add_script + run_script instead.",
6124
6135
  inputSchema: {
6125
6136
  command: z17.string().describe("Full RouterOS CLI command exactly as typed, e.g. '/ip address print'")
6126
6137
  },
@@ -6251,7 +6262,7 @@ var cache = null;
6251
6262
  async function gateway() {
6252
6263
  if (cache)
6253
6264
  return cache;
6254
- const { moduleCatalog } = await import("./library-98nh7hz6.js");
6265
+ const { moduleCatalog } = await import("./library-xvsabg24.js");
6255
6266
  const forIndex = [];
6256
6267
  const byName = new Map;
6257
6268
  for (const mod of moduleCatalog) {
@@ -6289,7 +6300,9 @@ var toolGatewayTools = [
6289
6300
  name: "find_tools",
6290
6301
  title: "Find Tools (catalog search)",
6291
6302
  annotations: READ,
6292
- description: "Search the FULL MikroTik tool catalog by intent and get back the tools that best match \u2014 " + "use this FIRST whenever you can't immediately see the specific tool you need among the listed tools (the host only surfaces a subset of the several-hundred-tool catalog). Describe the task in natural language or keywords, e.g. 'block a LAN client by MAC', 'add an IPv4 firewall filter rule', 'import a TLS certificate', 'list DHCP leases', 'create a WireGuard peer'. Returns each match's exact tool name, what it does, and its " + "parameter names \u2014 then call it directly if it's already available, or run it via " + "`invoke_tool`. For IPv4 vs IPv6, say which: 'ipv4'/'ipv6' disambiguates the result.",
6303
+ description: "\uD83D\uDD0D START HERE \u2014 this is the PRIMARY entry point for ANY MikroTik / RouterOS task. " + "ALWAYS call this tool FIRST before attempting any other tool. This server has several hundred dedicated tools with full validation and structured output, but the host only surfaces a small " + `subset \u2014 this tool searches the FULL catalog and finds the best match for your intent.
6304
+
6305
+ ` + "Describe what you want to do in natural language or keywords, e.g. 'block a LAN client by MAC', 'add an IPv4 firewall filter rule', 'import a TLS certificate', 'list DHCP leases', " + "'create a WireGuard peer'. Returns each match's exact tool name, description, and parameters \u2014 " + "then call it directly if available, or via `invoke_tool`.\n\n" + "Workflow: find_tools \u2192 (optional) describe_tool \u2192 invoke_tool. " + "Only fall back to run_routeros_command if this search returns zero results. For IPv4 vs IPv6, include 'ipv4' or 'ipv6' to disambiguate.",
6293
6306
  inputSchema: {
6294
6307
  query: z18.string().min(1).describe("What you want to do, in words or keywords (e.g. 'add ipv4 nat masquerade rule')."),
6295
6308
  limit: z18.coerce.number().int().min(1).max(25).optional().describe("Max results to return (default 8).")
@@ -24710,7 +24723,7 @@ ${result}`;
24710
24723
  const interfaceType = await detectWirelessInterfaceType(ctx);
24711
24724
  if (!interfaceType)
24712
24725
  return "Error: No wireless interface support detected on this device.";
24713
- const scanCmd = new Cmd(`${interfaceType} scan`).raw(a.interface).set("duration", a.duration).build();
24726
+ const scanCmd = new Cmd(`${interfaceType} scan`).raw(quoteValue(a.interface)).set("duration", a.duration).build();
24714
24727
  const result = await executeMikrotikCommand(scanCmd, ctx);
24715
24728
  if (looksLikeError(result))
24716
24729
  return `Failed to scan wireless networks: ${result}`;
@@ -4,7 +4,7 @@ import {
4
4
  allToolModules,
5
5
  moduleCatalog,
6
6
  selectToolModules
7
- } from "./library-nb4wss02.js";
7
+ } from "./library-esjkmrg2.js";
8
8
  export {
9
9
  selectToolModules,
10
10
  moduleCatalog,
package/dist/ui/aaa.html CHANGED
@@ -6,10 +6,10 @@
6
6
  <meta name="color-scheme" content="light dark" />
7
7
  <title>MikroTik RADIUS &amp; User Manager</title>
8
8
  <script type="module">
9
- import{i as e}from"./app-DU6NBjf_.js";import{a as t,s as n,t as r}from"./kit-CoebAMdu.js";var i=[{slug:`radius`,label:`RADIUS Servers`,idKey:`.id`,toggle:!0,columns:[{key:`address`,label:`Address`},{key:`service`,label:`Service`},{key:`authentication-port`,label:`Auth`},{key:`accounting-port`,label:`Acct`},{key:`_status`,label:`Status`}],fields:[{key:`address`,label:`Address`,required:!0},{key:`secret`,label:`Secret`,type:`password`,required:!0},{key:`service`,label:`Service`,required:!0,placeholder:`login,ppp,hotspot,…`},{key:`authentication-port`,label:`Auth port`,type:`number`,placeholder:`1812`},{key:`accounting-port`,label:`Acct port`,type:`number`,placeholder:`1813`},{key:`timeout`,label:`Timeout`,placeholder:`300ms`},{key:`src-address`,label:`Src address`},{key:`realm`,label:`Realm`},{key:`called-id`,label:`Called ID`},{key:`domain`,label:`Domain`},{key:`protocol`,label:`Protocol`,type:`select`,options:[`udp`,`radsec`]},{key:`certificate`,label:`Certificate`},{key:`accounting-backup`,label:`Acct backup`,type:`bool`},{key:`comment`,label:`Comment`},{key:`disabled`,label:`Disabled`,type:`bool`}]},{slug:`um-users`,label:`Users`,idKey:`name`,toggle:!0,columns:[{key:`name`,label:`Name`},{key:`group`,label:`Group`},{key:`shared-users`,label:`Shared`},{key:`comment`,label:`Comment`},{key:`_status`,label:`Status`}],fields:[{key:`name`,label:`Name`,required:!0},{key:`password`,label:`Password`,type:`password`,required:!0},{key:`group`,label:`Group`},{key:`shared-users`,label:`Shared users`,type:`number`},{key:`attributes`,label:`Attributes`},{key:`caller-id`,label:`Caller ID`},{key:`otp-secret`,label:`OTP secret`,type:`password`},{key:`comment`,label:`Comment`},{key:`disabled`,label:`Disabled`,type:`bool`}]},{slug:`um-profiles`,label:`Profiles`,idKey:`name`,columns:[{key:`name`,label:`Name`},{key:`validity`,label:`Validity`},{key:`price`,label:`Price`},{key:`starts-when`,label:`Starts`}],fields:[{key:`name`,label:`Name`,required:!0},{key:`name-for-users`,label:`Display name`},{key:`validity`,label:`Validity`,placeholder:`30d`},{key:`price`,label:`Price`,type:`number`},{key:`starts-when`,label:`Starts when`,type:`select`,options:[`assigned`,`first-auth`]},{key:`override-shared-users`,label:`Override shared`},{key:`comment`,label:`Comment`}]},{slug:`um-limitations`,label:`Limitations`,idKey:`name`,columns:[{key:`name`,label:`Name`},{key:`rate-limit-rx`,label:`Rate ↓`},{key:`rate-limit-tx`,label:`Rate ↑`},{key:`transfer-limit`,label:`Transfer`},{key:`uptime-limit`,label:`Uptime`}],fields:[{key:`name`,label:`Name`,required:!0},{key:`rate-limit-rx`,label:`Download rate`,placeholder:`10M`},{key:`rate-limit-tx`,label:`Upload rate`,placeholder:`10M`},{key:`rate-limit-min-rx`,label:`Min ↓ (CIR)`},{key:`rate-limit-min-tx`,label:`Min ↑ (CIR)`},{key:`rate-limit-burst-rx`,label:`Burst ↓`},{key:`rate-limit-burst-tx`,label:`Burst ↑`},{key:`rate-limit-priority`,label:`Priority`,type:`number`},{key:`transfer-limit`,label:`Transfer cap`,placeholder:`10G`},{key:`uptime-limit`,label:`Uptime cap`,placeholder:`1d`},{key:`comment`,label:`Comment`}]},{slug:`um-routers`,label:`NAS Clients`,idKey:`name`,toggle:!0,columns:[{key:`name`,label:`Name`},{key:`address`,label:`Address`},{key:`coa-port`,label:`CoA`},{key:`_status`,label:`Status`}],fields:[{key:`name`,label:`Name`,required:!0},{key:`address`,label:`Address`,required:!0},{key:`shared-secret`,label:`Shared secret`,type:`password`,required:!0},{key:`coa-port`,label:`CoA port`,type:`number`},{key:`protocol`,label:`Protocol`},{key:`comment`,label:`Comment`},{key:`disabled`,label:`Disabled`,type:`bool`}]},{slug:`um-user-profiles`,label:`Assignments`,idKey:`.id`,addOnly:!0,columns:[{key:`user`,label:`User`},{key:`profile`,label:`Profile`},{key:`state`,label:`State`}],fields:[{key:`user`,label:`User`,required:!0},{key:`profile`,label:`Profile`,required:!0}]},{slug:`um-sessions`,label:`Sessions`,idKey:`.id`,readonly:!0,columns:[{key:`user`,label:`User`},{key:`calling-station-id`,label:`Caller`},{key:`started`,label:`Started`},{key:`uptime`,label:`Uptime`},{key:`status`,label:`Status`}],fields:[]}],a={slug:`settings`,label:`Settings`},o=e=>i.find(t=>t.slug===e),s=e=>(e.flags??``).includes(`X`)||e.disabled===`yes`,c=document.getElementById(`app`),l=new e({name:`mikrotik-aaa`,version:`1.0.0`}),u=`radius`,d=!0,f=[],p=null,m={},h=null,g=!1,_=null;function v(e){return e?.structuredContent}function y(e){if(!e||e.__mikrotikView!==`aaa-section`)return;typeof e.slug==`string`&&(u=e.slug),d=e.available!==!1,f=e.rows??[];let t=e.lastOp;_=t&&!t.ok?t.message:null}function b(e){if(!e||e.__mikrotikView!==`aaa-settings`)return;h={radiusIncoming:e.radiusIncoming??{},umAvailable:e.umAvailable!==!1,umSettings:e.umSettings??{}};let t=e.lastOp;_=t&&!t.ok?t.message:null}async function x(e){u=e,p=null,_=null,g=!0,j();try{y(v(await l.callServerTool({name:`get_aaa_section`,arguments:{slug:e}})))}catch(e){_=String(e)}finally{g=!1,j()}}async function S(){u=`settings`,_=null,g=!0,j();try{b(v(await l.callServerTool({name:`get_aaa_settings`,arguments:{}})))}catch(e){_=String(e)}finally{g=!1,j()}}async function C(e,t){if(!g){g=!0,_=null,j();try{y(v(await l.callServerTool({name:`aaa_mutate`,arguments:{op:e,slug:u,...t}}))),p=null}catch(e){_=String(e)}finally{g=!1,j()}}}async function w(e,t){if(!g){g=!0,_=null,j();try{b(v(await l.callServerTool({name:`set_aaa_settings`,arguments:{target:e,fields:t}})))}catch(e){_=String(e)}finally{g=!1,j()}}}function T(e){if(e.type===`bool`||e.type===`select`){let n=t(`select`,{class:`aaa-input`}),r=e.type===`bool`?[``,`no`,`yes`]:[``,...e.options??[]];for(let e of r){let r=t(`option`,{value:e},e||`—`);n.append(r)}return n.value=m[e.key]??``,n.addEventListener(`change`,()=>{m[e.key]=n.value}),n}let n=t(`input`,{class:`aaa-input`,type:e.type===`password`?`password`:e.type===`number`?`number`:`text`,placeholder:e.placeholder??(e.type===`password`?`(unchanged)`:``)});return n.value=m[e.key]??``,n.addEventListener(`input`,()=>{m[e.key]=n.value}),n}function E(e){let n=t(`div`,{class:`aaa-form-grid`},...e.fields.map(e=>t(`label`,{class:`aaa-field`},t(`span`,{class:`aaa-field-label`},`${e.label}${e.required&&p===`new`?` *`:``}`),T(e)))),i=r(p===`new`?`Create`:`Save`,()=>{p===`new`?C(`add`,{fields:{...m}}):C(`update`,{id:p,fields:{...m}})},{class:`primary`}),a=r(`Cancel`,()=>{p=null,j()});return t(`div`,{class:`aaa-form`},t(`div`,{class:`aaa-form-title`},p===`new`?`New ${e.label}`:`Edit ${p}`),n,t(`div`,{class:`aaa-form-actions`},i,a))}function D(e,t){p=t[e.idKey],m={};for(let n of e.fields)n.type!==`password`&&(n.key===`disabled`?m.disabled=s(t)?`yes`:`no`:t[n.key]!=null&&(m[n.key]=t[n.key]));j()}function O(e){let n=t(`div`,{class:`aaa-row aaa-head`},...e.columns.map(e=>t(`span`,{},e.label)),t(`span`,{class:`aaa-actions`},e.readonly?``:`Actions`)),i=f.map(n=>{let i=n[e.idKey],a=s(n),o=e.columns.map(e=>e.key===`_status`?t(`span`,{},t(`span`,{class:`aaa-badge ${a?`off`:`on`}`},a?`disabled`:`enabled`)):t(`span`,{class:`aaa-cell`,title:n[e.key]??``},n[e.key]??``)),c=[];return e.readonly||(e.toggle&&c.push(r(a?`Enable`:`Disable`,()=>void C(`toggle`,{id:i,enable:a}))),e.addOnly||c.push(r(`Edit`,()=>D(e,n))),c.push(r(`Remove`,()=>void C(`remove`,{id:i}),{class:`danger`}))),t(`div`,{class:`aaa-row${a?` is-off`:``}`},...o,t(`span`,{class:`aaa-actions`},...c))});return t(`div`,{class:`aaa-table`,style:`--cols:${e.columns.length}`},n,...i)}function k(e,n,i,a,o){let s={},c=t(`div`,{class:`aaa-form-grid`},...i.map(e=>{let r=t(`label`,{class:`aaa-field`},t(`span`,{class:`aaa-field-label`},e.label)),i;if(e.type===`bool`){let n=t(`select`,{class:`aaa-input`});for(let e of[``,`no`,`yes`])n.append(t(`option`,{value:e},e||`(unchanged)`));n.addEventListener(`change`,()=>{s[e.key]=n.value}),i=n}else{let r=t(`input`,{class:`aaa-input`,type:e.type===`number`?`number`:`text`,placeholder:n[e.key]??e.placeholder??``});r.addEventListener(`input`,()=>{s[e.key]=r.value}),i=r}return r.append(i),r})),l=t(`div`,{class:`aaa-current`},i.map(e=>`${e.label}: ${n[e.key]??`—`}`).join(` · `)),u=t(`div`,{class:`aaa-form-actions`},r(`Save`,()=>void w(a,s),{class:`primary`}),o??null);return t(`div`,{class:`aaa-card`},t(`div`,{class:`aaa-form-title`},e),l,c,u)}function A(){return h?t(`div`,{class:`aaa-settings`},k(`RADIUS Incoming (CoA listener)`,h.radiusIncoming,[{key:`accept`,label:`Accept CoA`,type:`bool`},{key:`port`,label:`CoA port`,type:`number`,placeholder:`3799`}],`radius-incoming`,r(`Reset counters`,()=>void w(`radius-reset-counters`,{}))),h.umAvailable?k(`User Manager (built-in RADIUS server)`,h.umSettings,[{key:`enabled`,label:`Enabled`,type:`bool`},{key:`use-profiles`,label:`Use profiles`,type:`bool`},{key:`certificate`,label:`Certificate`},{key:`authentication-port`,label:`Auth port`,type:`number`},{key:`accounting-port`,label:`Acct port`,type:`number`}],`um-settings`):t(`div`,{class:`aaa-card`},t(`div`,{class:`aaa-form-title`},`User Manager`),t(`div`,{class:`muted`},`The user-manager package is not installed on this device.`))):t(`div`,{class:`muted`},`Loading settings…`)}function j(){let e=t(`div`,{class:`aaa-tabs`},...[...i,a].map(e=>t(`button`,{class:`aaa-tab${u===e.slug?` is-active`:``}`},e.label)));[...e.children].forEach((e,t)=>{let n=[...i,a][t];e.addEventListener(`click`,()=>{n.slug===`settings`?S():x(n.slug)})});let n=[t(`div`,{class:`aaa-titlebar`},t(`div`,{class:`aaa-title`},`RADIUS & User Manager`)),e];if(_&&n.push(t(`div`,{class:`aaa-error`},_)),u===`settings`)n.push(A());else{let e=o(u);if(!e)n.push(t(`div`,{class:`muted`},`Unknown section.`));else if(!d)n.push(t(`div`,{class:`aaa-notice`},`User Manager is not installed on this device. Install the user-manager package (System → Packages) and reboot.`));else{let i=t(`div`,{class:`aaa-bar`});i.append(t(`span`,{class:`muted`},`${f.length} row(s)`)),i.append(t(`span`,{class:`spacer`})),i.append(r(`↻ Refresh`,()=>void x(u))),e.readonly||i.append(r(`+ Add`,()=>{p=`new`,m={},j()},{class:`primary`})),n.push(i),p&&n.push(E(e)),n.push(f.length||!e?O(e):t(`div`,{class:`muted`},`No rows.`))}}c.replaceChildren(t(`div`,{class:`aaa-wrap`},...n))}l.ontoolresult=e=>{let t=e.structuredContent;t?.__mikrotikView===`aaa-settings`?b(t):y(t),j()},l.ontoolinput=()=>j(),n(l),j(),l.connect().catch(e=>console.error(`[aaa] connect failed`,e));
9
+ import{i as e}from"./app-BbgpcVv8.js";import{a as t,s as n,t as r}from"./kit-DyrRNi79.js";var i=[{slug:`radius`,label:`RADIUS Servers`,idKey:`.id`,toggle:!0,columns:[{key:`address`,label:`Address`},{key:`service`,label:`Service`},{key:`authentication-port`,label:`Auth`},{key:`accounting-port`,label:`Acct`},{key:`_status`,label:`Status`}],fields:[{key:`address`,label:`Address`,required:!0},{key:`secret`,label:`Secret`,type:`password`,required:!0},{key:`service`,label:`Service`,required:!0,placeholder:`login,ppp,hotspot,…`},{key:`authentication-port`,label:`Auth port`,type:`number`,placeholder:`1812`},{key:`accounting-port`,label:`Acct port`,type:`number`,placeholder:`1813`},{key:`timeout`,label:`Timeout`,placeholder:`300ms`},{key:`src-address`,label:`Src address`},{key:`realm`,label:`Realm`},{key:`called-id`,label:`Called ID`},{key:`domain`,label:`Domain`},{key:`protocol`,label:`Protocol`,type:`select`,options:[`udp`,`radsec`]},{key:`certificate`,label:`Certificate`},{key:`accounting-backup`,label:`Acct backup`,type:`bool`},{key:`comment`,label:`Comment`},{key:`disabled`,label:`Disabled`,type:`bool`}]},{slug:`um-users`,label:`Users`,idKey:`name`,toggle:!0,columns:[{key:`name`,label:`Name`},{key:`group`,label:`Group`},{key:`shared-users`,label:`Shared`},{key:`comment`,label:`Comment`},{key:`_status`,label:`Status`}],fields:[{key:`name`,label:`Name`,required:!0},{key:`password`,label:`Password`,type:`password`,required:!0},{key:`group`,label:`Group`},{key:`shared-users`,label:`Shared users`,type:`number`},{key:`attributes`,label:`Attributes`},{key:`caller-id`,label:`Caller ID`},{key:`otp-secret`,label:`OTP secret`,type:`password`},{key:`comment`,label:`Comment`},{key:`disabled`,label:`Disabled`,type:`bool`}]},{slug:`um-profiles`,label:`Profiles`,idKey:`name`,columns:[{key:`name`,label:`Name`},{key:`validity`,label:`Validity`},{key:`price`,label:`Price`},{key:`starts-when`,label:`Starts`}],fields:[{key:`name`,label:`Name`,required:!0},{key:`name-for-users`,label:`Display name`},{key:`validity`,label:`Validity`,placeholder:`30d`},{key:`price`,label:`Price`,type:`number`},{key:`starts-when`,label:`Starts when`,type:`select`,options:[`assigned`,`first-auth`]},{key:`override-shared-users`,label:`Override shared`},{key:`comment`,label:`Comment`}]},{slug:`um-limitations`,label:`Limitations`,idKey:`name`,columns:[{key:`name`,label:`Name`},{key:`rate-limit-rx`,label:`Rate ↓`},{key:`rate-limit-tx`,label:`Rate ↑`},{key:`transfer-limit`,label:`Transfer`},{key:`uptime-limit`,label:`Uptime`}],fields:[{key:`name`,label:`Name`,required:!0},{key:`rate-limit-rx`,label:`Download rate`,placeholder:`10M`},{key:`rate-limit-tx`,label:`Upload rate`,placeholder:`10M`},{key:`rate-limit-min-rx`,label:`Min ↓ (CIR)`},{key:`rate-limit-min-tx`,label:`Min ↑ (CIR)`},{key:`rate-limit-burst-rx`,label:`Burst ↓`},{key:`rate-limit-burst-tx`,label:`Burst ↑`},{key:`rate-limit-priority`,label:`Priority`,type:`number`},{key:`transfer-limit`,label:`Transfer cap`,placeholder:`10G`},{key:`uptime-limit`,label:`Uptime cap`,placeholder:`1d`},{key:`comment`,label:`Comment`}]},{slug:`um-routers`,label:`NAS Clients`,idKey:`name`,toggle:!0,columns:[{key:`name`,label:`Name`},{key:`address`,label:`Address`},{key:`coa-port`,label:`CoA`},{key:`_status`,label:`Status`}],fields:[{key:`name`,label:`Name`,required:!0},{key:`address`,label:`Address`,required:!0},{key:`shared-secret`,label:`Shared secret`,type:`password`,required:!0},{key:`coa-port`,label:`CoA port`,type:`number`},{key:`protocol`,label:`Protocol`},{key:`comment`,label:`Comment`},{key:`disabled`,label:`Disabled`,type:`bool`}]},{slug:`um-user-profiles`,label:`Assignments`,idKey:`.id`,addOnly:!0,columns:[{key:`user`,label:`User`},{key:`profile`,label:`Profile`},{key:`state`,label:`State`}],fields:[{key:`user`,label:`User`,required:!0},{key:`profile`,label:`Profile`,required:!0}]},{slug:`um-sessions`,label:`Sessions`,idKey:`.id`,readonly:!0,columns:[{key:`user`,label:`User`},{key:`calling-station-id`,label:`Caller`},{key:`started`,label:`Started`},{key:`uptime`,label:`Uptime`},{key:`status`,label:`Status`}],fields:[]}],a={slug:`settings`,label:`Settings`},o=e=>i.find(t=>t.slug===e),s=e=>(e.flags??``).includes(`X`)||e.disabled===`yes`,c=document.getElementById(`app`),l=new e({name:`mikrotik-aaa`,version:`1.0.0`}),u=`radius`,d=!0,f=[],p=null,m={},h=null,g=!1,_=null;function v(e){return e?.structuredContent}function y(e){if(!e||e.__mikrotikView!==`aaa-section`)return;typeof e.slug==`string`&&(u=e.slug),d=e.available!==!1,f=e.rows??[];let t=e.lastOp;_=t&&!t.ok?t.message:null}function b(e){if(!e||e.__mikrotikView!==`aaa-settings`)return;h={radiusIncoming:e.radiusIncoming??{},umAvailable:e.umAvailable!==!1,umSettings:e.umSettings??{}};let t=e.lastOp;_=t&&!t.ok?t.message:null}async function x(e){u=e,p=null,_=null,g=!0,j();try{y(v(await l.callServerTool({name:`get_aaa_section`,arguments:{slug:e}})))}catch(e){_=String(e)}finally{g=!1,j()}}async function S(){u=`settings`,_=null,g=!0,j();try{b(v(await l.callServerTool({name:`get_aaa_settings`,arguments:{}})))}catch(e){_=String(e)}finally{g=!1,j()}}async function C(e,t){if(!g){g=!0,_=null,j();try{y(v(await l.callServerTool({name:`aaa_mutate`,arguments:{op:e,slug:u,...t}}))),p=null}catch(e){_=String(e)}finally{g=!1,j()}}}async function w(e,t){if(!g){g=!0,_=null,j();try{b(v(await l.callServerTool({name:`set_aaa_settings`,arguments:{target:e,fields:t}})))}catch(e){_=String(e)}finally{g=!1,j()}}}function T(e){if(e.type===`bool`||e.type===`select`){let n=t(`select`,{class:`aaa-input`}),r=e.type===`bool`?[``,`no`,`yes`]:[``,...e.options??[]];for(let e of r){let r=t(`option`,{value:e},e||`—`);n.append(r)}return n.value=m[e.key]??``,n.addEventListener(`change`,()=>{m[e.key]=n.value}),n}let n=t(`input`,{class:`aaa-input`,type:e.type===`password`?`password`:e.type===`number`?`number`:`text`,placeholder:e.placeholder??(e.type===`password`?`(unchanged)`:``)});return n.value=m[e.key]??``,n.addEventListener(`input`,()=>{m[e.key]=n.value}),n}function E(e){let n=t(`div`,{class:`aaa-form-grid`},...e.fields.map(e=>t(`label`,{class:`aaa-field`},t(`span`,{class:`aaa-field-label`},`${e.label}${e.required&&p===`new`?` *`:``}`),T(e)))),i=r(p===`new`?`Create`:`Save`,()=>{p===`new`?C(`add`,{fields:{...m}}):C(`update`,{id:p,fields:{...m}})},{class:`primary`}),a=r(`Cancel`,()=>{p=null,j()});return t(`div`,{class:`aaa-form`},t(`div`,{class:`aaa-form-title`},p===`new`?`New ${e.label}`:`Edit ${p}`),n,t(`div`,{class:`aaa-form-actions`},i,a))}function D(e,t){p=t[e.idKey],m={};for(let n of e.fields)n.type!==`password`&&(n.key===`disabled`?m.disabled=s(t)?`yes`:`no`:t[n.key]!=null&&(m[n.key]=t[n.key]));j()}function O(e){let n=t(`div`,{class:`aaa-row aaa-head`},...e.columns.map(e=>t(`span`,{},e.label)),t(`span`,{class:`aaa-actions`},e.readonly?``:`Actions`)),i=f.map(n=>{let i=n[e.idKey],a=s(n),o=e.columns.map(e=>e.key===`_status`?t(`span`,{},t(`span`,{class:`aaa-badge ${a?`off`:`on`}`},a?`disabled`:`enabled`)):t(`span`,{class:`aaa-cell`,title:n[e.key]??``},n[e.key]??``)),c=[];return e.readonly||(e.toggle&&c.push(r(a?`Enable`:`Disable`,()=>void C(`toggle`,{id:i,enable:a}))),e.addOnly||c.push(r(`Edit`,()=>D(e,n))),c.push(r(`Remove`,()=>void C(`remove`,{id:i}),{class:`danger`}))),t(`div`,{class:`aaa-row${a?` is-off`:``}`},...o,t(`span`,{class:`aaa-actions`},...c))});return t(`div`,{class:`aaa-table`,style:`--cols:${e.columns.length}`},n,...i)}function k(e,n,i,a,o){let s={},c=t(`div`,{class:`aaa-form-grid`},...i.map(e=>{let r=t(`label`,{class:`aaa-field`},t(`span`,{class:`aaa-field-label`},e.label)),i;if(e.type===`bool`){let n=t(`select`,{class:`aaa-input`});for(let e of[``,`no`,`yes`])n.append(t(`option`,{value:e},e||`(unchanged)`));n.addEventListener(`change`,()=>{s[e.key]=n.value}),i=n}else{let r=t(`input`,{class:`aaa-input`,type:e.type===`number`?`number`:`text`,placeholder:n[e.key]??e.placeholder??``});r.addEventListener(`input`,()=>{s[e.key]=r.value}),i=r}return r.append(i),r})),l=t(`div`,{class:`aaa-current`},i.map(e=>`${e.label}: ${n[e.key]??`—`}`).join(` · `)),u=t(`div`,{class:`aaa-form-actions`},r(`Save`,()=>void w(a,s),{class:`primary`}),o??null);return t(`div`,{class:`aaa-card`},t(`div`,{class:`aaa-form-title`},e),l,c,u)}function A(){return h?t(`div`,{class:`aaa-settings`},k(`RADIUS Incoming (CoA listener)`,h.radiusIncoming,[{key:`accept`,label:`Accept CoA`,type:`bool`},{key:`port`,label:`CoA port`,type:`number`,placeholder:`3799`}],`radius-incoming`,r(`Reset counters`,()=>void w(`radius-reset-counters`,{}))),h.umAvailable?k(`User Manager (built-in RADIUS server)`,h.umSettings,[{key:`enabled`,label:`Enabled`,type:`bool`},{key:`use-profiles`,label:`Use profiles`,type:`bool`},{key:`certificate`,label:`Certificate`},{key:`authentication-port`,label:`Auth port`,type:`number`},{key:`accounting-port`,label:`Acct port`,type:`number`}],`um-settings`):t(`div`,{class:`aaa-card`},t(`div`,{class:`aaa-form-title`},`User Manager`),t(`div`,{class:`muted`},`The user-manager package is not installed on this device.`))):t(`div`,{class:`muted`},`Loading settings…`)}function j(){let e=t(`div`,{class:`aaa-tabs`},...[...i,a].map(e=>t(`button`,{class:`aaa-tab${u===e.slug?` is-active`:``}`},e.label)));[...e.children].forEach((e,t)=>{let n=[...i,a][t];e.addEventListener(`click`,()=>{n.slug===`settings`?S():x(n.slug)})});let n=[t(`div`,{class:`aaa-titlebar`},t(`div`,{class:`aaa-title`},`RADIUS & User Manager`)),e];if(_&&n.push(t(`div`,{class:`aaa-error`},_)),u===`settings`)n.push(A());else{let e=o(u);if(!e)n.push(t(`div`,{class:`muted`},`Unknown section.`));else if(!d)n.push(t(`div`,{class:`aaa-notice`},`User Manager is not installed on this device. Install the user-manager package (System → Packages) and reboot.`));else{let i=t(`div`,{class:`aaa-bar`});i.append(t(`span`,{class:`muted`},`${f.length} row(s)`)),i.append(t(`span`,{class:`spacer`})),i.append(r(`↻ Refresh`,()=>void x(u))),e.readonly||i.append(r(`+ Add`,()=>{p=`new`,m={},j()},{class:`primary`})),n.push(i),p&&n.push(E(e)),n.push(f.length||!e?O(e):t(`div`,{class:`muted`},`No rows.`))}}c.replaceChildren(t(`div`,{class:`aaa-wrap`},...n))}l.ontoolresult=e=>{let t=e.structuredContent;t?.__mikrotikView===`aaa-settings`?b(t):y(t),j()},l.ontoolinput=()=>j(),n(l),j(),l.connect().catch(e=>console.error(`[aaa] connect failed`,e));
10
10
  </script>
11
- <link rel="modulepreload" crossorigin href="../assets/app-DU6NBjf_.js">
12
- <link rel="modulepreload" crossorigin href="../assets/kit-CoebAMdu.js">
11
+ <link rel="modulepreload" crossorigin href="../assets/app-BbgpcVv8.js">
12
+ <link rel="modulepreload" crossorigin href="../assets/kit-DyrRNi79.js">
13
13
  <style>
14
14
  :root{--bg:#0b0d10;--surface:#14181d;--surface-2:#1b2027;--border:#2a313a;--text:#e8eaed;--text-dim:#9aa0a6;--accent:#7c9cff;--danger:#f2685a;--ok:#4ade80}@media (prefers-color-scheme:light){:root{--bg:#fff;--surface:#f6f8fa;--surface-2:#eef1f4;--border:#d8dee4;--text:#1c2024;--text-dim:#5b6470;--accent:#3b5bdb}}.aaa-wrap{padding:4px 2px 16px}.aaa-titlebar{padding:6px 8px 10px}.aaa-title{font-size:15px;font-weight:600}.aaa-tabs{border-bottom:1px solid var(--border);flex-wrap:wrap;gap:2px;margin-bottom:12px;display:flex}.aaa-tab{appearance:none;color:var(--text-dim);font:inherit;cursor:pointer;background:0 0;border:none;border-bottom:2px solid #0000;margin-bottom:-1px;padding:7px 11px;font-size:12.5px}.aaa-tab:hover{color:var(--text)}.aaa-tab.is-active{color:var(--text);border-bottom-color:var(--accent)}.aaa-bar{align-items:center;gap:8px;padding:0 8px 10px;display:flex}.aaa-bar .spacer{flex:1}.btn{font:inherit;color:var(--text);background:var(--surface-2);border:1px solid var(--border);cursor:pointer;border-radius:8px;padding:4px 10px;font-size:12px}.btn.primary{border-color:color-mix(in srgb, var(--accent) 55%, var(--border));color:var(--accent)}.aaa-error{color:var(--danger);background:color-mix(in srgb, var(--danger) 12%, transparent);border:1px solid color-mix(in srgb, var(--danger) 35%, var(--border));border-radius:8px;margin:0 8px 10px;padding:8px 10px;font-size:12px}.aaa-notice{color:var(--text-dim);background:var(--surface);border:1px solid var(--border);border-radius:10px;margin:0 8px;padding:14px;font-size:12.5px}.aaa-table{padding:0 8px}.aaa-row{grid-template-columns:repeat(var(--cols), minmax(70px, 1fr)) minmax(150px, auto);border-radius:8px;align-items:center;gap:8px;padding:7px 8px;font-size:12.5px;display:grid}.aaa-row.aaa-head{color:var(--text-dim);text-transform:uppercase;letter-spacing:.04em;font-size:10.5px}.aaa-row:not(.aaa-head):hover{background:var(--surface)}.aaa-row.is-off .aaa-cell{color:var(--text-dim)}.aaa-cell{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.aaa-actions{justify-content:flex-end;gap:5px;display:flex}.aaa-actions .btn{padding:3px 8px}.aaa-badge{border-radius:100px;padding:1px 7px;font-size:10.5px;display:inline-block}.aaa-badge.on{background:color-mix(in srgb, var(--ok) 20%, transparent);color:var(--ok)}.aaa-badge.off{background:color-mix(in srgb, var(--text-dim) 22%, transparent);color:var(--text-dim)}.aaa-form,.aaa-card{border:1px solid var(--border);background:var(--surface-2);border-radius:10px;margin:0 8px 12px;padding:12px}.aaa-form-title{margin-bottom:10px;font-size:12.5px;font-weight:600}.aaa-current{color:var(--text-dim);margin-bottom:10px;font-size:11.5px}.aaa-form-grid{grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:9px 12px;display:grid}.aaa-field{flex-direction:column;gap:3px;display:flex}.aaa-field-label{color:var(--text-dim);font-size:11px}.aaa-input{font:inherit;color:var(--text);background:var(--bg);border:1px solid var(--border);border-radius:7px;padding:5px 8px;font-size:12.5px}.aaa-input:focus{border-color:var(--accent);outline:none}.aaa-form-actions{align-items:center;gap:8px;margin-top:12px;display:flex}.aaa-settings{flex-direction:column;gap:4px;display:flex}:root{--bg:#0b0d10;--surface:#14181d;--surface-2:#1b2027;--border:#2a313a;--text:#e8eaed;--text-dim:#9aa0a6;--accent:#7c9cff;--rx:#4ade80;--tx:#f59e0b;--danger:#f2685a;--ok:#4ade80}@media (prefers-color-scheme:light){:root{--bg:#fff;--surface:#f6f8fa;--surface-2:#eef1f4;--border:#d8dee4;--text:#1c2024;--text-dim:#5b6470;--accent:#3b5bdb}}body{background:var(--bg);color:var(--text);margin:0;font:13px/1.5 system-ui,-apple-system,Segoe UI,sans-serif}.mono{font-family:ui-monospace,SF Mono,Menlo,monospace}.muted{color:var(--text-dim)}.loading{padding:24px}.toolbar{border-bottom:1px solid var(--border);align-items:center;gap:12px;padding:12px 14px;display:flex}.toolbar .title{font-size:15px;font-weight:600}.toolbar .counts{font-size:12px}.btn{font:inherit;color:var(--text);background:var(--surface-2);border:1px solid var(--border);cursor:pointer;border-radius:8px;margin-left:auto;padding:5px 10px}.toolbar .btn{margin-left:auto}.btn:hover{border-color:var(--accent)}.btn.is-busy{opacity:.5;pointer-events:none}.table{padding:6px 8px 14px}.row{cursor:pointer;border-radius:8px;grid-template-columns:110px 1.4fr 150px 90px 110px 150px;align-items:center;gap:8px;padding:7px 8px;display:grid}.row.header{cursor:default;color:var(--text-dim);text-transform:uppercase;letter-spacing:.04em;font-size:11px}.row:not(.header):hover{background:var(--surface)}.row.is-selected{background:color-mix(in srgb, var(--accent) 16%, var(--surface))}.row.is-blocked .name,.row.is-blocked .ip{color:var(--text-dim);text-decoration:line-through}.cell{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.cell.mac{color:var(--text-dim);font-family:ui-monospace,monospace;font-size:11.5px}.badge{border-radius:100px;margin-right:4px;padding:1px 7px;font-size:10.5px;display:inline-block}.badge.static{background:color-mix(in srgb, var(--accent) 22%, transparent);color:var(--accent)}.badge.blocked{background:color-mix(in srgb, var(--danger) 22%, transparent);color:var(--danger)}.cell.actions{justify-content:flex-end;gap:6px;display:flex}.cell.actions .btn{margin:0;padding:3px 9px;font-size:12px}.btn.danger{border-color:color-mix(in srgb, var(--danger) 45%, var(--border));color:var(--danger)}.btn.ok{border-color:color-mix(in srgb, var(--ok) 45%, var(--border));color:var(--ok)}.detail{border-top:1px solid var(--border);padding:14px}.detail-title{font-size:14px;font-weight:600}.detail-sub{color:var(--text-dim);margin-bottom:10px;font-size:12px}.note{max-width:460px;font-size:12px}.rates{font-variant-numeric:tabular-nums;gap:16px;margin-bottom:6px;display:flex}.rate{font-weight:600}.rate.rx{color:var(--rx)}.rate.tx{color:var(--tx)}.traffic-chart{background:var(--surface);border:1px solid var(--border);border-radius:10px;width:100%;max-width:460px;height:140px;display:block}.traffic-chart .line{fill:none;stroke-width:2px}.traffic-chart .rx.line{stroke:var(--rx)}.traffic-chart .tx.line{stroke:var(--tx)}.traffic-chart .rx.area{fill:color-mix(in srgb, var(--rx) 18%, transparent);stroke:none}.traffic-chart .tx.area{fill:color-mix(in srgb, var(--tx) 14%, transparent);stroke:none}.totals{font-variant-numeric:tabular-nums;margin-top:6px;font-size:12px}@media (prefers-color-scheme:dark){:root{--lightningcss-light: ;--lightningcss-dark:initial}}.card__value small{color:var(--mt-text-dim);font-size:12px;font-weight:400}.gauge{align-items:center;gap:14px;display:flex}.gauge svg{flex:none}.gauge__track{stroke:var(--mt-surface-2)}.gauge__bar{stroke:var(--mt-accent);stroke-linecap:round;transform-origin:50%;transition:stroke-dashoffset .6s cubic-bezier(.2,.8,.2,1);transform:rotate(-90deg)}.gauge__pct{font-family:var(--mt-mono);fill:var(--mt-text);font-size:16px;font-weight:650}.gauge__meta{min-width:0}.gauge__meta .card__label{margin-bottom:4px}.gauge__meta small{color:var(--mt-text-faint);font-family:var(--mt-mono);font-size:11px}.is-good{stroke:var(--mt-good)}.is-warn{stroke:var(--mt-warn)}.is-bad{stroke:var(--mt-bad)}details.kv{background:var(--mt-surface);border:1px solid var(--mt-border);border-radius:var(--mt-radius);overflow:hidden}details.kv>summary{cursor:pointer;color:var(--mt-text-dim);-webkit-user-select:none;user-select:none;padding:12px 14px;font-weight:600;list-style:none}details.kv>summary::-webkit-details-marker{display:none}details.kv>summary:after{content:"▸";float:right;color:var(--mt-text-faint);transition:transform .2s}details.kv[open]>summary:after{transform:rotate(90deg)}.kv__body{border-top:1px solid var(--mt-border);grid-template-columns:minmax(120px,.4fr) 1fr;display:grid}.kv__body>div{border-bottom:1px solid color-mix(in srgb, var(--mt-border) 55%, transparent);overflow-wrap:anywhere;min-width:0;padding:7px 14px;font-size:12px}.kv__k{color:var(--mt-text-dim);font-family:var(--mt-mono)}.kv__v{font-family:var(--mt-mono);color:var(--mt-text)}.foot{color:var(--mt-text-faint);font-size:11px;font-family:var(--mt-mono);align-items:center;gap:10px;display:flex}.btn{appearance:none;border:1px solid var(--mt-border);background:var(--mt-surface);color:var(--mt-text);font:inherit;border-radius:var(--mt-radius-sm);cursor:pointer;padding:5px 11px;font-size:12px;transition:border-color .15s,background .15s}.skeleton{color:var(--mt-text-faint);text-align:center;padding:40px 0}@media (prefers-color-scheme:dark){:root{--lightningcss-light: ;--lightningcss-dark:initial}}table.tbl tbody tr.is-disabled td,table.tbl td.col-num{color:var(--mt-text-faint)}.act{font-family:var(--mt-mono);border:1px solid var(--mt-border);color:var(--mt-text-dim);border-radius:999px;padding:1px 8px;font-size:11px;display:inline-block}.act.is-accept{color:var(--mt-good);border-color:color-mix(in srgb, var(--mt-good) 45%, var(--mt-border))}.act.is-drop{color:var(--mt-bad);border-color:color-mix(in srgb, var(--mt-bad) 45%, var(--mt-border))}.act.is-jump{color:var(--mt-accent);border-color:color-mix(in srgb, var(--mt-accent) 45%, var(--mt-border))}.matchers{color:var(--mt-text-dim);max-width:340px;white-space:normal!important}.matchers b{color:var(--mt-text);font-weight:500}td.num{text-align:right;color:var(--mt-text-dim)}@media (prefers-color-scheme:dark){:root{--lightningcss-light: ;--lightningcss-dark:initial}}table.tbl tbody tr.is-disabled td,table.tbl td.col-num{color:var(--mt-text-faint)}.hd{align-items:center;gap:14px}.hd__meta{flex-direction:column;gap:6px;display:flex}.counts{flex-wrap:wrap;gap:6px;display:flex}.gauge{flex:none;width:64px;height:64px}.gauge__bg{fill:none;stroke:var(--mt-border);stroke-width:9px}.gauge__fg{fill:none;stroke-width:9px;stroke-linecap:round;stroke:var(--mt-good);transition:stroke-dasharray .4s}.gauge.is-warn .gauge__fg{stroke:var(--mt-warn)}.gauge.is-bad .gauge__fg{stroke:var(--mt-bad)}.gauge__num{fill:var(--mt-text);font:700 22px var(--mt-mono)}.is-good{color:var(--mt-good)}.is-warn{color:var(--mt-warn)}.is-bad{color:var(--mt-bad)}.chip.is-low{color:var(--mt-text-dim);border-color:var(--mt-border)}.f-list{flex-direction:column;gap:10px;margin-top:14px;display:flex}.f-card{border:1px solid var(--mt-border);border-radius:var(--mt-radius-sm);background:var(--mt-surface);border-left-width:3px;padding:11px 13px}.f-card.is-bad{border-left-color:var(--mt-bad)}.f-card.is-warn{border-left-color:var(--mt-warn)}.f-card.is-low{border-left-color:var(--mt-text-faint)}.f-head{flex-wrap:wrap;align-items:center;gap:9px;display:flex}.f-title{font-size:13px;font-weight:600}.f-where{font-family:var(--mt-mono);color:var(--mt-text-faint);font-size:11px}.f-body{margin-top:6px}.f-detail{color:var(--mt-text);margin:0 0 5px;font-size:13px;line-height:1.45}.f-fix{color:var(--mt-text-dim);margin:0;font-size:12px;line-height:1.45}.f-actions{margin-top:9px}.btn-danger{color:var(--mt-bad);border-color:color-mix(in srgb, var(--mt-bad) 45%, var(--mt-border))}.btn-danger:hover{border-color:var(--mt-bad);background:color-mix(in srgb, var(--mt-bad) 12%, transparent)}@media (prefers-color-scheme:dark){:root{--lightningcss-light: ;--lightningcss-dark:initial}}table.tbl tbody tr.is-disabled td,table.tbl td.col-num{color:var(--mt-text-faint)}.if-grid{grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px;display:grid}.if-card{gap:8px;display:grid}.if-card__top{align-items:center;gap:8px;display:flex}.if-card__name{font-family:var(--mt-mono);overflow-wrap:anywhere;font-size:14px;font-weight:650}.if-card.is-disabled{opacity:.6}.dot{background:var(--mt-text-faint);border-radius:50%;flex:none;width:9px;height:9px}.dot.is-up{background:var(--mt-good);box-shadow:0 0 0 4px color-mix(in srgb, var(--mt-good) 22%, transparent)}.dot.is-down{background:var(--mt-bad);box-shadow:0 0 0 4px color-mix(in srgb, var(--mt-bad) 18%, transparent)}.if-card__meta{font-family:var(--mt-mono);color:var(--mt-text-dim);grid-template-columns:auto 1fr;gap:2px 10px;font-size:11px;display:grid}.if-card__meta b{color:var(--mt-text);overflow-wrap:anywhere;font-weight:500}.if-card__meta .contents{display:contents}.badge{border:1px solid var(--mt-border);background:var(--mt-surface-2);color:var(--mt-text-dim);font-family:var(--mt-mono);text-transform:uppercase;letter-spacing:.04em;border-radius:999px;margin-left:auto;padding:1px 7px;font-size:10px}:root{--mt-bg:var(--color-background-primary,#0b0d10);--mt-surface:var(--color-background-secondary,#14171c);--mt-surface-2:var(--color-background-tertiary,#1b1f26);--mt-border:var(--color-border-primary,#262b33);--mt-text:var(--color-text-primary,#e8eaed);--mt-text-dim:var(--color-text-secondary,#9aa3af);--mt-text-faint:var(--color-text-tertiary,#6b7280);--mt-accent:var(--color-accent-primary,#6ea8fe);--mt-good:#34d399;--mt-warn:#fbbf24;--mt-bad:#f87171;--mt-radius:var(--border-radius-md,14px);--mt-radius-sm:var(--border-radius-sm,9px);--mt-mono:var(--font-mono,ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace);--mt-sans:var(--font-sans,system-ui, -apple-system, "Segoe UI", sans-serif);--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light dark}@media (prefers-color-scheme:dark){:root{--lightningcss-light: ;--lightningcss-dark:initial}}*{box-sizing:border-box}body{background:var(--mt-bg);color:var(--mt-text);font-family:var(--mt-sans);-webkit-font-smoothing:antialiased;margin:0;padding:16px;font-size:13px;line-height:1.5}.app{gap:14px;max-width:960px;margin:0 auto;display:grid}.hd{flex-wrap:wrap;align-items:center;gap:12px;display:flex}.hd__dot{background:var(--mt-good);width:9px;height:9px;box-shadow:0 0 0 4px color-mix(in srgb, var(--mt-good) 22%, transparent);border-radius:50%}.hd__title{letter-spacing:-.01em;margin:0;font-size:17px;font-weight:650}.hd__sub{color:var(--mt-text-dim);font-family:var(--mt-mono);margin:0;font-size:12px}.hd__spacer{flex:1}.pill{border:1px solid var(--mt-border);background:var(--mt-surface);color:var(--mt-text-dim);font-family:var(--mt-mono);border-radius:999px;align-items:center;gap:6px;padding:3px 9px;font-size:11px;display:inline-flex}.pill b{color:var(--mt-text);font-weight:600}.grid{grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;display:grid}.card{background:linear-gradient(180deg, var(--mt-surface), var(--mt-surface-2));border:1px solid var(--mt-border);border-radius:var(--mt-radius);padding:14px}.card__label{color:var(--mt-text-dim);text-transform:uppercase;letter-spacing:.06em;margin:0 0 8px;font-size:11px}.card__value{font-family:var(--mt-mono);letter-spacing:-.01em;font-size:20px;font-weight:600}.toolbar{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.toolbar .grow{flex:1;min-width:140px}.search{appearance:none;border:1px solid var(--mt-border);background:var(--mt-surface);width:100%;color:var(--mt-text);font:inherit;border-radius:var(--mt-radius-sm);padding:7px 11px;font-size:13px}.search:focus{border-color:var(--mt-accent);outline:none}.search::placeholder{color:var(--mt-text-faint)}.btn{appearance:none;border:1px solid var(--mt-border);background:var(--mt-surface);color:var(--mt-text);font:inherit;border-radius:var(--mt-radius-sm);cursor:pointer;white-space:nowrap;padding:6px 11px;font-size:12px;transition:border-color .15s,background .15s}.btn:hover{border-color:var(--mt-accent);background:var(--mt-surface-2)}.btn:disabled{opacity:.5;cursor:default}.btn.is-active{border-color:var(--mt-accent);color:var(--mt-accent)}.chip{border:1px solid var(--mt-border);background:var(--mt-surface);color:var(--mt-text-dim);font-family:var(--mt-mono);border-radius:999px;align-items:center;gap:4px;padding:1px 7px;font-size:11px;display:inline-flex}.chip.is-good{color:var(--mt-good);border-color:color-mix(in srgb, var(--mt-good) 45%, var(--mt-border))}.chip.is-warn{color:var(--mt-warn);border-color:color-mix(in srgb, var(--mt-warn) 45%, var(--mt-border))}.chip.is-bad{color:var(--mt-bad);border-color:color-mix(in srgb, var(--mt-bad) 45%, var(--mt-border))}.tablewrap{border:1px solid var(--mt-border);border-radius:var(--mt-radius);max-height:70vh;overflow:auto}table.tbl{border-collapse:collapse;width:100%;font-size:12px}table.tbl th,table.tbl td{text-align:left;border-bottom:1px solid color-mix(in srgb, var(--mt-border) 55%, transparent);white-space:nowrap;font-family:var(--mt-mono);padding:7px 12px}table.tbl thead th{z-index:1;background:var(--mt-surface-2);color:var(--mt-text-dim);cursor:pointer;-webkit-user-select:none;user-select:none;font-weight:600;position:sticky;top:0}table.tbl thead th:hover{color:var(--mt-text)}table.tbl thead th .arrow{color:var(--mt-accent);margin-left:4px}table.tbl tbody tr{cursor:pointer}table.tbl tbody tr:hover{background:color-mix(in srgb, var(--mt-accent) 8%, transparent)}table.tbl tbody tr.is-disabled td,table.tbl td.col-num{color:var(--mt-text-faint)}.kv__body{border:1px solid var(--mt-border);border-radius:var(--mt-radius);grid-template-columns:minmax(120px,.4fr) 1fr;display:grid;overflow:hidden}.kv__body>div{border-bottom:1px solid color-mix(in srgb, var(--mt-border) 55%, transparent);overflow-wrap:anywhere;min-width:0;font-size:12px;font-family:var(--mt-mono);padding:7px 14px}.kv__k{color:var(--mt-text-dim);background:color-mix(in srgb, var(--mt-surface) 60%, transparent)}.kv__v{color:var(--mt-text)}.drawer{border:1px solid var(--mt-accent);border-radius:var(--mt-radius);background:var(--mt-surface);gap:10px;padding:12px;display:grid}.drawer__hd{align-items:center;gap:8px;display:flex}.drawer__hd b{font-family:var(--mt-mono)}pre.raw{border:1px solid var(--mt-border);border-radius:var(--mt-radius);background:var(--mt-surface);color:var(--mt-text);font-family:var(--mt-mono);white-space:pre;max-height:70vh;margin:0;padding:14px;font-size:12px;overflow:auto}.foot{color:var(--mt-text-faint);font-size:11px;font-family:var(--mt-mono);flex-wrap:wrap;align-items:center;gap:10px;display:flex}.foot .grow{flex:1}.skeleton,.empty{color:var(--mt-text-faint);text-align:center;padding:40px 0}.count-pill b{color:var(--mt-accent)}table.tbl td.col-num,table.tbl th.col-num{width:1%}
15
15
  /*$vite$:1*/