@usex/mikrotik-mcp 5.3.0 → 5.5.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
@@ -2,6 +2,8 @@
2
2
  // @bun
3
3
  import {
4
4
  AAA_ENTITIES,
5
+ ADVISORIES,
6
+ ADVISORY_DATASET_DATE,
5
7
  ALWAYS_ON_MODULES,
6
8
  AlertEngine,
7
9
  AlertRuleSchema,
@@ -40,6 +42,7 @@ import {
40
42
  blockDevice,
41
43
  buildChangePlan,
42
44
  buildChannelPlanCommands,
45
+ buildFabricMap,
43
46
  buildFtCommands,
44
47
  buildGuards,
45
48
  buildHaCommands,
@@ -54,6 +57,8 @@ import {
54
57
  closeAll,
55
58
  closeDevice,
56
59
  closeMemoryStore,
60
+ collectDeviceFacts,
61
+ collectFabric,
57
62
  commandUnsupported,
58
63
  configureRecorder,
59
64
  conversations,
@@ -89,6 +94,7 @@ import {
89
94
  fetchLatestRelease,
90
95
  flowStore,
91
96
  forgetJob,
97
+ getAccessPolicy,
92
98
  getAlertEngine,
93
99
  getCapabilities,
94
100
  getConfig,
@@ -129,6 +135,7 @@ import {
129
135
  logger,
130
136
  looksLikeError,
131
137
  makeDeviceStatic,
138
+ matchAdvisories,
132
139
  moduleCatalog,
133
140
  neverBlockSet,
134
141
  nextRun,
@@ -154,6 +161,7 @@ import {
154
161
  protocolMix,
155
162
  publishRollout,
156
163
  readBackup,
164
+ recentDenials,
157
165
  redact,
158
166
  redactChannels,
159
167
  registerTools,
@@ -230,7 +238,7 @@ import {
230
238
  validatePolicyText,
231
239
  worstSeverity,
232
240
  writeBackup
233
- } from "./shared/cli-54yv0kd3.js";
241
+ } from "./shared/cli-cvq8pxvn.js";
234
242
 
235
243
  // src/cli.ts
236
244
  import { existsSync as existsSync2 } from "fs";
@@ -1688,11 +1696,64 @@ async function driftRoutes(req, url) {
1688
1696
  return null;
1689
1697
  }
1690
1698
 
1691
- // src/observability/txn-routes.ts
1699
+ // src/observability/posture-routes.ts
1692
1700
  var JSON_HEADERS2 = { "content-type": "application/json; charset=utf-8" };
1693
1701
  function json2(body, status = 200) {
1694
1702
  return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS2 });
1695
1703
  }
1704
+ async function postureRoutes(req, url) {
1705
+ const p = url.pathname;
1706
+ if (req.method !== "GET")
1707
+ return null;
1708
+ if (p === "/api/fabric") {
1709
+ const device = url.searchParams.get("device") ?? undefined;
1710
+ const ctx = createContext(undefined, device);
1711
+ try {
1712
+ const map = buildFabricMap(await collectFabric(device, ctx));
1713
+ return json2(map);
1714
+ } catch (e) {
1715
+ return json2({ error: e instanceof Error ? e.message : String(e) }, 502);
1716
+ }
1717
+ }
1718
+ if (p === "/api/advisories") {
1719
+ const all = url.searchParams.get("all") === "1";
1720
+ const device = url.searchParams.get("device") ?? undefined;
1721
+ const targets = all ? listDevices().names : [device];
1722
+ try {
1723
+ const facts = await Promise.all(targets.map((d) => collectDeviceFacts(d)));
1724
+ const report = matchAdvisories(facts);
1725
+ return json2({
1726
+ ...report,
1727
+ datasetDate: ADVISORY_DATASET_DATE,
1728
+ datasetSize: ADVISORIES.length,
1729
+ devices: facts.map((f) => ({
1730
+ device: f.device,
1731
+ version: f.version,
1732
+ board: f.board,
1733
+ enabledServices: f.services.filter((s) => !s.disabled).length,
1734
+ unrestrictedServices: f.services.filter((s) => !s.disabled && s.allowedFrom.length === 0).length
1735
+ }))
1736
+ });
1737
+ } catch (e) {
1738
+ return json2({ error: e instanceof Error ? e.message : String(e) }, 502);
1739
+ }
1740
+ }
1741
+ if (p === "/api/access") {
1742
+ const policy = getAccessPolicy();
1743
+ return json2({
1744
+ enabled: policy.enabled,
1745
+ scope: policy.scope,
1746
+ denials: recentDenials(50)
1747
+ });
1748
+ }
1749
+ return null;
1750
+ }
1751
+
1752
+ // src/observability/txn-routes.ts
1753
+ var JSON_HEADERS3 = { "content-type": "application/json; charset=utf-8" };
1754
+ function json3(body, status = 200) {
1755
+ return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS3 });
1756
+ }
1696
1757
  function liveRecord(id) {
1697
1758
  const entry = getTxn(id);
1698
1759
  return entry ? { ...toRecord(entry.txn, entry.ts, entry.label), updated: Date.now() } : null;
@@ -1717,16 +1778,16 @@ async function txnRoutes(req, url) {
1717
1778
  for (const r of live)
1718
1779
  byId.set(r.id, r);
1719
1780
  const transactions = [...byId.values()].sort((a, b) => b.ts - a.ts).slice(0, limit);
1720
- return json2({ transactions, live: liveTxnUpdates(), error: storeError });
1781
+ return json3({ transactions, live: liveTxnUpdates(), error: storeError });
1721
1782
  }
1722
1783
  const parts = p.slice("/api/txn/".length).split("/").filter(Boolean);
1723
1784
  if (parts.length === 0 || parts.length > 2)
1724
- return json2({ error: "not found" }, 404);
1785
+ return json3({ error: "not found" }, 404);
1725
1786
  const id = decodeURIComponent(parts[0]);
1726
1787
  if (parts[1] === "abort" && req.method === "POST") {
1727
1788
  const entry = getTxn(id);
1728
1789
  if (!entry)
1729
- return json2({ error: `no open transaction '${id}'` }, 404);
1790
+ return json3({ error: `no open transaction '${id}'` }, 404);
1730
1791
  entry.txn = requestAbort(entry.txn, "aborted from the dashboard");
1731
1792
  const run = await runTransaction({
1732
1793
  txn: entry.txn,
@@ -1737,7 +1798,7 @@ async function txnRoutes(req, url) {
1737
1798
  await persistTxn(entry);
1738
1799
  if (run.state !== undefined)
1739
1800
  dropTxn(id);
1740
- return json2({
1801
+ return json3({
1741
1802
  state: run.state,
1742
1803
  summary: run.summary,
1743
1804
  transaction: toRecord(run.txn, entry.ts)
@@ -1746,16 +1807,16 @@ async function txnRoutes(req, url) {
1746
1807
  if (parts.length === 1 && req.method === "GET") {
1747
1808
  const record = liveRecord(id) ?? store?.get(id) ?? null;
1748
1809
  if (!record)
1749
- return json2({ error: `unknown transaction '${id}'` }, 404);
1750
- return json2({ transaction: record, events: store?.events(id) ?? [] });
1810
+ return json3({ error: `unknown transaction '${id}'` }, 404);
1811
+ return json3({ transaction: record, events: store?.events(id) ?? [] });
1751
1812
  }
1752
- return json2({ error: "method not allowed" }, 405);
1813
+ return json3({ error: "method not allowed" }, 405);
1753
1814
  }
1754
1815
 
1755
1816
  // src/observability/flow-routes.ts
1756
- var JSON_HEADERS3 = { "content-type": "application/json; charset=utf-8" };
1757
- function json3(body, status = 200) {
1758
- return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS3 });
1817
+ var JSON_HEADERS4 = { "content-type": "application/json; charset=utf-8" };
1818
+ function json4(body, status = 200) {
1819
+ return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS4 });
1759
1820
  }
1760
1821
  var WINDOWS = {
1761
1822
  "5m": 5 * 60000,
@@ -1775,14 +1836,14 @@ async function flowRoutes(req, url) {
1775
1836
  if (!p.startsWith("/api/flows"))
1776
1837
  return null;
1777
1838
  if (req.method !== "GET")
1778
- return json3({ error: "method not allowed" }, 405);
1839
+ return json4({ error: "method not allowed" }, 405);
1779
1840
  const collector = getFlowCollector();
1780
1841
  if (p === "/api/flows/health") {
1781
1842
  let store = null;
1782
1843
  try {
1783
1844
  store = (await flowStore()).stats();
1784
1845
  } catch {}
1785
- return json3({ collector: collector.stats(), store });
1846
+ return json4({ collector: collector.stats(), store });
1786
1847
  }
1787
1848
  collector.drain();
1788
1849
  let records;
@@ -1793,12 +1854,12 @@ async function flowRoutes(req, url) {
1793
1854
  try {
1794
1855
  records = (await flowStore()).query({ from, to, address, limit: 200000 });
1795
1856
  } catch (e) {
1796
- return json3({ error: e instanceof Error ? e.message : String(e), records: [] }, 200);
1857
+ return json4({ error: e instanceof Error ? e.message : String(e), records: [] }, 200);
1797
1858
  }
1798
1859
  if (p === "/api/flows/top") {
1799
1860
  const dimension = url.searchParams.get("dimension") ?? "source";
1800
1861
  const limit = Number(url.searchParams.get("limit") ?? 10);
1801
- return json3({
1862
+ return json4({
1802
1863
  window: { from, to },
1803
1864
  totals: summarize(records),
1804
1865
  top: topTalkers(records, dimension, limit, true),
@@ -1808,7 +1869,7 @@ async function flowRoutes(req, url) {
1808
1869
  }
1809
1870
  if (p === "/api/flows/conversations") {
1810
1871
  const limit = Number(url.searchParams.get("limit") ?? 20);
1811
- return json3({
1872
+ return json4({
1812
1873
  window: { from, to },
1813
1874
  conversations: conversations(records, limit)
1814
1875
  });
@@ -1817,20 +1878,20 @@ async function flowRoutes(req, url) {
1817
1878
  const dimension = url.searchParams.get("dimension") ?? "source";
1818
1879
  const topN = Number(url.searchParams.get("topN") ?? 5);
1819
1880
  const bucket = bucketFor(span);
1820
- return json3({
1881
+ return json4({
1821
1882
  window: { from, to },
1822
1883
  bucketMs: bucket,
1823
1884
  keys: topTalkers(records, dimension, topN).map((t) => t.key),
1824
1885
  buckets: timeline(records, from, to, bucket, dimension, topN)
1825
1886
  });
1826
1887
  }
1827
- return json3({ error: "not found" }, 404);
1888
+ return json4({ error: "not found" }, 404);
1828
1889
  }
1829
1890
 
1830
1891
  // src/observability/rollout-routes.ts
1831
- var JSON_HEADERS4 = { "content-type": "application/json; charset=utf-8" };
1832
- function json4(body, status = 200) {
1833
- return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS4 });
1892
+ var JSON_HEADERS5 = { "content-type": "application/json; charset=utf-8" };
1893
+ function json5(body, status = 200) {
1894
+ return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS5 });
1834
1895
  }
1835
1896
  function liveRecord2(id) {
1836
1897
  const entry = getRollout(id);
@@ -1859,17 +1920,17 @@ async function rolloutRoutes(req, url) {
1859
1920
  for (const r of live)
1860
1921
  byId.set(r.id, r);
1861
1922
  const rollouts = [...byId.values()].sort((a, b) => b.ts - a.ts).slice(0, limit);
1862
- return json4({ rollouts, live: liveRolloutUpdates(), error: storeError });
1923
+ return json5({ rollouts, live: liveRolloutUpdates(), error: storeError });
1863
1924
  }
1864
1925
  const parts = p.slice("/api/rollout/".length).split("/").filter(Boolean);
1865
1926
  if (parts.length === 0 || parts.length > 2)
1866
- return json4({ error: "not found" }, 404);
1927
+ return json5({ error: "not found" }, 404);
1867
1928
  const id = decodeURIComponent(parts[0]);
1868
1929
  const verb = parts[1];
1869
1930
  if (verb && req.method === "POST") {
1870
1931
  const entry = getRollout(id);
1871
1932
  if (!entry)
1872
- return json4({ error: `no active rollout '${id}'` }, 404);
1933
+ return json5({ error: `no active rollout '${id}'` }, 404);
1873
1934
  switch (verb) {
1874
1935
  case "hold": {
1875
1936
  entry.state = requestHold(entry.state);
@@ -1884,7 +1945,7 @@ async function rolloutRoutes(req, url) {
1884
1945
  devices: entry.state.devices,
1885
1946
  gates: entry.state.gates
1886
1947
  });
1887
- return json4({ held: true, summary: summarize2(entry.state) });
1948
+ return json5({ held: true, summary: summarize2(entry.state) });
1888
1949
  }
1889
1950
  case "resume": {
1890
1951
  entry.state = resume(entry.state);
@@ -1912,7 +1973,7 @@ async function rolloutRoutes(req, url) {
1912
1973
  await persistRollout(entry);
1913
1974
  if (run.outcome)
1914
1975
  dropRollout(id);
1915
- return json4({ outcome: run.outcome, summary: run.summary });
1976
+ return json5({ outcome: run.outcome, summary: run.summary });
1916
1977
  }
1917
1978
  case "abort": {
1918
1979
  entry.state = requestAbort2(entry.state, "aborted from the dashboard");
@@ -1925,19 +1986,19 @@ async function rolloutRoutes(req, url) {
1925
1986
  await persistRollout(entry);
1926
1987
  if (run.outcome)
1927
1988
  dropRollout(id);
1928
- return json4({ outcome: run.outcome, summary: run.summary });
1989
+ return json5({ outcome: run.outcome, summary: run.summary });
1929
1990
  }
1930
1991
  default:
1931
- return json4({ error: `unknown action '${verb}'` }, 404);
1992
+ return json5({ error: `unknown action '${verb}'` }, 404);
1932
1993
  }
1933
1994
  }
1934
1995
  if (parts.length === 1 && req.method === "GET") {
1935
1996
  const record = liveRecord2(id) ?? store?.get(id) ?? null;
1936
1997
  if (!record)
1937
- return json4({ error: `unknown rollout '${id}'` }, 404);
1938
- return json4({ rollout: record, events: store?.events(id) ?? [] });
1998
+ return json5({ error: `unknown rollout '${id}'` }, 404);
1999
+ return json5({ rollout: record, events: store?.events(id) ?? [] });
1939
2000
  }
1940
- return json4({ error: "method not allowed" }, 405);
2001
+ return json5({ error: "method not allowed" }, 405);
1941
2002
  }
1942
2003
 
1943
2004
  // src/policy/results.ts
@@ -2026,9 +2087,9 @@ async function policyResults(device, limit = 50) {
2026
2087
  }
2027
2088
 
2028
2089
  // src/observability/policy-routes.ts
2029
- var JSON_HEADERS5 = { "content-type": "application/json; charset=utf-8" };
2030
- function json5(body, status = 200) {
2031
- return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS5 });
2090
+ var JSON_HEADERS6 = { "content-type": "application/json; charset=utf-8" };
2091
+ function json6(body, status = 200) {
2092
+ return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS6 });
2032
2093
  }
2033
2094
  async function bodyJson2(req) {
2034
2095
  return await req.json().catch(() => ({}));
@@ -2039,7 +2100,7 @@ async function policyRoutes(req, url) {
2039
2100
  return null;
2040
2101
  if (p === "/api/policies" && req.method === "GET") {
2041
2102
  const set = currentPolicySet();
2042
- return json5({
2103
+ return json6({
2043
2104
  files: set.files.map((f) => ({
2044
2105
  path: f.path,
2045
2106
  name: f.name,
@@ -2060,21 +2121,21 @@ async function policyRoutes(req, url) {
2060
2121
  try {
2061
2122
  results = await policyResults(device, limit);
2062
2123
  } catch (e) {
2063
- return json5({ results: [], error: e instanceof Error ? e.message : String(e) });
2124
+ return json6({ results: [], error: e instanceof Error ? e.message : String(e) });
2064
2125
  }
2065
- return json5({ results });
2126
+ return json6({ results });
2066
2127
  }
2067
2128
  if (p === "/api/policies/validate" && req.method === "POST") {
2068
2129
  const body = await bodyJson2(req);
2069
2130
  if (typeof body.content !== "string")
2070
- return json5({ error: "content is required" }, 400);
2071
- return json5(validatePolicyText(body.content));
2131
+ return json6({ error: "content is required" }, 400);
2132
+ return json6(validatePolicyText(body.content));
2072
2133
  }
2073
2134
  if (p === "/api/policies/run" && req.method === "POST") {
2074
2135
  const body = await bodyJson2(req);
2075
2136
  const set = currentPolicySet();
2076
2137
  if (set.policies.length === 0) {
2077
- return json5({ error: "no policy rules are loaded", reports: [] });
2138
+ return json6({ error: "no policy rules are loaded", reports: [] });
2078
2139
  }
2079
2140
  const cfg = getConfig();
2080
2141
  const devices = body.devices && body.devices.length > 0 ? body.devices : Object.entries(cfg.devices).filter(([, d]) => !d.disabled).map(([name]) => name);
@@ -2103,9 +2164,9 @@ async function policyRoutes(req, url) {
2103
2164
  reports.push({ device, error: e instanceof Error ? e.message : String(e) });
2104
2165
  }
2105
2166
  }
2106
- return json5({ reports });
2167
+ return json6({ reports });
2107
2168
  }
2108
- return json5({ error: "not found" }, 404);
2169
+ return json6({ error: "not found" }, 404);
2109
2170
  }
2110
2171
 
2111
2172
  // src/sim/suites.ts
@@ -2198,9 +2259,9 @@ async function saveSuite(input) {
2198
2259
  }
2199
2260
 
2200
2261
  // src/observability/sim-routes.ts
2201
- var JSON_HEADERS6 = { "content-type": "application/json; charset=utf-8" };
2202
- function json6(body, status = 200) {
2203
- return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS6 });
2262
+ var JSON_HEADERS7 = { "content-type": "application/json; charset=utf-8" };
2263
+ function json7(body, status = 200) {
2264
+ return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS7 });
2204
2265
  }
2205
2266
  async function bodyJson3(req) {
2206
2267
  return await req.json().catch(() => ({}));
@@ -2232,28 +2293,28 @@ async function simRoutes(req, url) {
2232
2293
  if (!p.startsWith("/api/sim"))
2233
2294
  return null;
2234
2295
  if (p === "/api/sim/suites" && req.method === "GET") {
2235
- return json6({ suites: await listSuites() });
2296
+ return json7({ suites: await listSuites() });
2236
2297
  }
2237
2298
  if (p === "/api/sim/suites" && req.method === "POST") {
2238
2299
  const body = await bodyJson3(req);
2239
2300
  if (!body.name || !Array.isArray(body.packets) || body.packets.length === 0) {
2240
- return json6({ error: "name and a non-empty packets array are required" }, 400);
2301
+ return json7({ error: "name and a non-empty packets array are required" }, 400);
2241
2302
  }
2242
2303
  const saved = await saveSuite({
2243
2304
  id: body.id,
2244
2305
  name: body.name,
2245
2306
  packets: body.packets
2246
2307
  });
2247
- return json6({ suite: saved });
2308
+ return json7({ suite: saved });
2248
2309
  }
2249
2310
  if (p === "/api/sim/packet" && req.method === "POST") {
2250
2311
  const body = await bodyJson3(req);
2251
2312
  const config = await configFor(body);
2252
2313
  if ("error" in config)
2253
- return json6(config, 400);
2314
+ return json7(config, 400);
2254
2315
  const model = buildModel(config.text);
2255
2316
  const result = tracePacket({ model, packet: body.packet });
2256
- return json6({
2317
+ return json7({
2257
2318
  source: config.source,
2258
2319
  result,
2259
2320
  coverage: {
@@ -2267,16 +2328,16 @@ async function simRoutes(req, url) {
2267
2328
  const body = await bodyJson3(req);
2268
2329
  const config = await configFor(body);
2269
2330
  if ("error" in config)
2270
- return json6(config, 400);
2331
+ return json7(config, 400);
2271
2332
  if (!body.changes || body.changes.trim() === "") {
2272
- return json6({ error: "changes are required" }, 400);
2333
+ return json7({ error: "changes are required" }, 400);
2273
2334
  }
2274
2335
  const before = tracePacket({ model: buildModel(config.text), packet: body.packet });
2275
2336
  const afterModel = buildModel(`${config.text}
2276
2337
  ${body.changes}
2277
2338
  `);
2278
2339
  const after = tracePacket({ model: afterModel, packet: body.packet });
2279
- return json6({
2340
+ return json7({
2280
2341
  source: config.source,
2281
2342
  before,
2282
2343
  after,
@@ -2291,10 +2352,10 @@ ${body.changes}
2291
2352
  packet: {}
2292
2353
  });
2293
2354
  if ("error" in config)
2294
- return json6(config, 400);
2355
+ return json7(config, 400);
2295
2356
  const model = buildModel(config.text);
2296
2357
  const dead = unreachableRules(model.filter);
2297
- return json6({
2358
+ return json7({
2298
2359
  source: config.source,
2299
2360
  rules: model.filter.map((r) => ({
2300
2361
  chain: r.chain,
@@ -2315,11 +2376,11 @@ ${body.changes}
2315
2376
  const id = decodeURIComponent(p.slice("/api/sim/suite/".length, -"/run".length));
2316
2377
  const suite = await getSuite(id);
2317
2378
  if (!suite)
2318
- return json6({ error: `no suite '${id}'` }, 404);
2379
+ return json7({ error: `no suite '${id}'` }, 404);
2319
2380
  const body = await bodyJson3(req);
2320
2381
  const config = await configFor({ ...body, packet: {} });
2321
2382
  if ("error" in config)
2322
- return json6(config, 400);
2383
+ return json7(config, 400);
2323
2384
  const model = buildModel(config.text);
2324
2385
  const results = suite.packets.map((entry) => {
2325
2386
  const result = tracePacket({ model, packet: entry.packet });
@@ -2331,7 +2392,7 @@ ${body.changes}
2331
2392
  summary: result.summary
2332
2393
  };
2333
2394
  });
2334
- return json6({
2395
+ return json7({
2335
2396
  source: config.source,
2336
2397
  suite: { id: suite.id, name: suite.name },
2337
2398
  results,
@@ -2339,13 +2400,13 @@ ${body.changes}
2339
2400
  total: results.length
2340
2401
  });
2341
2402
  }
2342
- return json6({ error: "not found" }, 404);
2403
+ return json7({ error: "not found" }, 404);
2343
2404
  }
2344
2405
 
2345
2406
  // src/observability/schedule-routes.ts
2346
- var JSON_HEADERS7 = { "content-type": "application/json; charset=utf-8" };
2347
- function json7(body, status = 200) {
2348
- return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS7 });
2407
+ var JSON_HEADERS8 = { "content-type": "application/json; charset=utf-8" };
2408
+ function json8(body, status = 200) {
2409
+ return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS8 });
2349
2410
  }
2350
2411
  function latestPerDevice(runs) {
2351
2412
  const latest = new Map;
@@ -2421,12 +2482,12 @@ async function scheduleRoutes(req, url) {
2421
2482
  try {
2422
2483
  store3 = await scheduleStore();
2423
2484
  } catch (e) {
2424
- return json7({ error: e instanceof Error ? e.message : String(e), jobs: [] }, 503);
2485
+ return json8({ error: e instanceof Error ? e.message : String(e), jobs: [] }, 503);
2425
2486
  }
2426
2487
  const now = Date.now();
2427
2488
  if (p === "/api/schedules" && req.method === "GET") {
2428
2489
  const jobs = store3.listJobs().map((job) => jobRow(job, store3.runs(job.id, 500), now));
2429
- return json7({
2490
+ return json8({
2430
2491
  jobs,
2431
2492
  schedulable: schedulableTools().map((a) => ({ tool: a.tool, summary: a.summary }))
2432
2493
  });
@@ -2434,20 +2495,20 @@ async function scheduleRoutes(req, url) {
2434
2495
  if (p === "/api/schedules" && req.method === "POST") {
2435
2496
  const body = await req.json().catch(() => null);
2436
2497
  if (!body)
2437
- return json7({ error: "invalid JSON body" }, 400);
2498
+ return json8({ error: "invalid JSON body" }, 400);
2438
2499
  const parsed = JobSchema.safeParse(body);
2439
2500
  if (!parsed.success) {
2440
- return json7({
2501
+ return json8({
2441
2502
  error: parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")
2442
2503
  }, 400);
2443
2504
  }
2444
2505
  if (!auditAdapter(parsed.data.tool)) {
2445
- return json7({ error: `'${parsed.data.tool}' is not a schedulable auditor` }, 400);
2506
+ return json8({ error: `'${parsed.data.tool}' is not a schedulable auditor` }, 400);
2446
2507
  }
2447
2508
  await primeRiskIndex();
2448
2509
  const risk = liveExecutor().riskOf(parsed.data.tool);
2449
2510
  if (risk !== "READ") {
2450
- return json7({ error: `'${parsed.data.tool}' is annotated ${risk}; only READ may be scheduled` }, 400);
2511
+ return json8({ error: `'${parsed.data.tool}' is annotated ${risk}; only READ may be scheduled` }, 400);
2451
2512
  }
2452
2513
  const existing = store3.getJob(parsed.data.id);
2453
2514
  const job = { ...parsed.data, createdAt: existing?.createdAt ?? now };
@@ -2456,7 +2517,7 @@ async function scheduleRoutes(req, url) {
2456
2517
  armJob(job, now);
2457
2518
  else
2458
2519
  forgetJob(job.id);
2459
- return json7({ job: jobRow(job, store3.runs(job.id, 500), now), replaced: existing !== null });
2520
+ return json8({ job: jobRow(job, store3.runs(job.id, 500), now), replaced: existing !== null });
2460
2521
  }
2461
2522
  if (p === "/api/schedules/timeline" && req.method === "GET") {
2462
2523
  const jobId = url.searchParams.get("job") ?? undefined;
@@ -2464,7 +2525,7 @@ async function scheduleRoutes(req, url) {
2464
2525
  const days = Number(url.searchParams.get("days") ?? 30);
2465
2526
  const since = now - days * 86400000;
2466
2527
  const runs = store3.runs(jobId, 2000).filter((r) => r.startedAt >= since && (!device || r.device === device));
2467
- return json7({
2528
+ return json8({
2468
2529
  points: runs.map((r) => ({
2469
2530
  at: r.startedAt,
2470
2531
  jobId: r.jobId,
@@ -2484,30 +2545,30 @@ async function scheduleRoutes(req, url) {
2484
2545
  const limit = Number(url.searchParams.get("limit") ?? 50);
2485
2546
  const jobs = jobId ? [jobId] : store3.listJobs().map((j) => j.id);
2486
2547
  const items = jobs.flatMap((id2) => regressionsFor(store3.runs(id2, 500), id2)).sort((a, b) => b.at - a.at).slice(0, limit);
2487
- return json7({ regressions: items });
2548
+ return json8({ regressions: items });
2488
2549
  }
2489
2550
  const parts = p.slice("/api/schedules/".length).split("/").filter(Boolean);
2490
2551
  if (parts.length === 0 || parts.length > 2)
2491
- return json7({ error: "not found" }, 404);
2552
+ return json8({ error: "not found" }, 404);
2492
2553
  const id = decodeURIComponent(parts[0]);
2493
2554
  const verb = parts[1];
2494
2555
  if (!verb && req.method === "DELETE") {
2495
2556
  const job = store3.getJob(id);
2496
2557
  if (!job)
2497
- return json7({ error: `no schedule '${id}'` }, 404);
2558
+ return json8({ error: `no schedule '${id}'` }, 404);
2498
2559
  const runCount = store3.runs(id, 1e4).length;
2499
2560
  store3.removeJob(id);
2500
2561
  forgetJob(id);
2501
- return json7({ removed: id, runsDiscarded: runCount });
2562
+ return json8({ removed: id, runsDiscarded: runCount });
2502
2563
  }
2503
2564
  if (verb === "run" && req.method === "POST") {
2504
2565
  const job = store3.getJob(id);
2505
2566
  if (!job)
2506
- return json7({ error: `no schedule '${id}'` }, 404);
2567
+ return json8({ error: `no schedule '${id}'` }, 404);
2507
2568
  await primeRiskIndex();
2508
2569
  logger.info(`schedule '${id}' run requested from the dashboard`);
2509
2570
  const result = await runJob(job, liveExecutor(), runnerOptions());
2510
- return json7({
2571
+ return json8({
2511
2572
  ...result,
2512
2573
  job: jobRow(job, store3.runs(id, 500), Date.now())
2513
2574
  });
@@ -2515,17 +2576,17 @@ async function scheduleRoutes(req, url) {
2515
2576
  if (!verb && req.method === "GET") {
2516
2577
  const job = store3.getJob(id);
2517
2578
  if (!job)
2518
- return json7({ error: `no schedule '${id}'` }, 404);
2579
+ return json8({ error: `no schedule '${id}'` }, 404);
2519
2580
  const runs = store3.runs(id, 500);
2520
- return json7({ job: jobRow(job, runs, now), runs, regressions: regressionsFor(runs, id) });
2581
+ return json8({ job: jobRow(job, runs, now), runs, regressions: regressionsFor(runs, id) });
2521
2582
  }
2522
- return json7({ error: "not found" }, 404);
2583
+ return json8({ error: "not found" }, 404);
2523
2584
  }
2524
2585
 
2525
2586
  // src/observability/explain-routes.ts
2526
- var JSON_HEADERS8 = { "content-type": "application/json; charset=utf-8" };
2527
- function json8(body, status = 200) {
2528
- return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS8 });
2587
+ var JSON_HEADERS9 = { "content-type": "application/json; charset=utf-8" };
2588
+ function json9(body, status = 200) {
2589
+ return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS9 });
2529
2590
  }
2530
2591
  var storePromise5 = null;
2531
2592
  function snapshots2() {
@@ -2565,53 +2626,53 @@ async function explainRoutes(req, url) {
2565
2626
  if (p === "/api/explain/diff" && req.method === "POST") {
2566
2627
  const body = await req.json().catch(() => null);
2567
2628
  if (!body)
2568
- return json8({ error: "invalid JSON body" }, 400);
2629
+ return json9({ error: "invalid JSON body" }, 400);
2569
2630
  if (!body.before && !body.beforeText) {
2570
- return json8({ error: "a diff needs a baseline: pass `before` (a snapshot id) or `beforeText`" }, 400);
2631
+ return json9({ error: "a diff needs a baseline: pass `before` (a snapshot id) or `beforeText`" }, 400);
2571
2632
  }
2572
2633
  const device2 = body.device ?? "";
2573
2634
  const before = body.beforeText ? { text: body.beforeText, device: device2, source: "supplied text" } : await configFor2(device2, body.before ?? null);
2574
2635
  if ("error" in before)
2575
- return json8({ error: `before: ${before.error}` }, before.status);
2636
+ return json9({ error: `before: ${before.error}` }, before.status);
2576
2637
  const after = body.afterText ? { text: body.afterText, device: device2, source: "supplied text" } : await configFor2(device2, body.after ?? null);
2577
2638
  if ("error" in after)
2578
- return json8({ error: `after: ${after.error}` }, after.status);
2639
+ return json9({ error: `after: ${after.error}` }, after.status);
2579
2640
  const diff = diffNarratives(analyzeDevice(before.text, before.device), analyzeDevice(after.text, after.device));
2580
- return json8({ diff, markdown: renderDiff(diff), before: before.source, after: after.source });
2641
+ return json9({ diff, markdown: renderDiff(diff), before: before.source, after: after.source });
2581
2642
  }
2582
2643
  const rest = p.slice("/api/explain/".length).split("/").filter(Boolean);
2583
2644
  if (rest.length === 0)
2584
- return json8({ error: "not found" }, 404);
2645
+ return json9({ error: "not found" }, 404);
2585
2646
  if (req.method !== "GET")
2586
- return json8({ error: "not found" }, 404);
2647
+ return json9({ error: "not found" }, 404);
2587
2648
  const device = decodeURIComponent(rest[0]);
2588
2649
  const snapshotId = url.searchParams.get("snapshot");
2589
2650
  let config;
2590
2651
  try {
2591
2652
  config = await configFor2(device, snapshotId);
2592
2653
  } catch (e) {
2593
- return json8({ error: e instanceof Error ? e.message : String(e) }, 502);
2654
+ return json9({ error: e instanceof Error ? e.message : String(e) }, 502);
2594
2655
  }
2595
2656
  if ("error" in config)
2596
- return json8({ error: config.error }, config.status);
2657
+ return json9({ error: config.error }, config.status);
2597
2658
  const narrative = { ...analyzeDevice(config.text, config.device), generatedAt: Date.now() };
2598
2659
  if (rest.length === 1) {
2599
- return json8(payload(narrative, config.source, renderNarrative(narrative)));
2660
+ return json9(payload(narrative, config.source, renderNarrative(narrative)));
2600
2661
  }
2601
2662
  if (rest[1] === "section" && rest[2]) {
2602
2663
  const section = decodeURIComponent(rest[2]);
2603
2664
  if (!NARRATIVE_SECTIONS.includes(section)) {
2604
- return json8({ error: `unknown section '${section}'. Known: ${NARRATIVE_SECTIONS.join(", ")}` }, 400);
2665
+ return json9({ error: `unknown section '${section}'. Known: ${NARRATIVE_SECTIONS.join(", ")}` }, 400);
2605
2666
  }
2606
- return json8(payload(narrative, config.source, renderNarrative(narrative, { sections: [section] })));
2667
+ return json9(payload(narrative, config.source, renderNarrative(narrative, { sections: [section] })));
2607
2668
  }
2608
- return json8({ error: "not found" }, 404);
2669
+ return json9({ error: "not found" }, 404);
2609
2670
  }
2610
2671
 
2611
2672
  // src/observability/attack-routes.ts
2612
- var JSON_HEADERS9 = { "content-type": "application/json; charset=utf-8" };
2613
- function json9(body, status = 200) {
2614
- return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS9 });
2673
+ var JSON_HEADERS10 = { "content-type": "application/json; charset=utf-8" };
2674
+ function json10(body, status = 200) {
2675
+ return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS10 });
2615
2676
  }
2616
2677
  async function attackRoutes(req, url) {
2617
2678
  const p = url.pathname;
@@ -2621,7 +2682,7 @@ async function attackRoutes(req, url) {
2621
2682
  try {
2622
2683
  store3 = await attackStore();
2623
2684
  } catch (e) {
2624
- return json9({ error: e instanceof Error ? e.message : String(e), incidents: [] }, 503);
2685
+ return json10({ error: e instanceof Error ? e.message : String(e), incidents: [] }, 503);
2625
2686
  }
2626
2687
  const cfg = getConfig().attacks;
2627
2688
  const now = Date.now();
@@ -2633,7 +2694,7 @@ async function attackRoutes(req, url) {
2633
2694
  });
2634
2695
  const responses = store3.listResponses({ active: true, limit: 500 });
2635
2696
  const blocked = new Set(responses.map((r) => r.source));
2636
- return json9({
2697
+ return json10({
2637
2698
  incidents: incidents.map((i) => ({ ...i, blocked: blocked.has(i.source) })),
2638
2699
  responses,
2639
2700
  posture: {
@@ -2673,7 +2734,7 @@ async function attackRoutes(req, url) {
2673
2734
  }
2674
2735
  }
2675
2736
  const active = new Set(store3.listResponses({ active: true, limit: 500 }).map((r) => r.source));
2676
- return json9({
2737
+ return json10({
2677
2738
  sources: [...bySource.values()].map((s) => ({
2678
2739
  source: s.source,
2679
2740
  devices: [...s.devices].sort(),
@@ -2691,14 +2752,14 @@ async function attackRoutes(req, url) {
2691
2752
  const guards = buildGuards();
2692
2753
  const never = [...neverBlockSet(guards)].sort();
2693
2754
  const check = url.searchParams.get("check");
2694
- return json9({
2755
+ return json10({
2695
2756
  config: cfg,
2696
2757
  neverBlock: never,
2697
2758
  check: check ? { address: check, protected: isNeverBlock(check, new Set(never)) } : undefined
2698
2759
  });
2699
2760
  }
2700
2761
  if (p === "/api/attacks/config" && req.method === "POST") {
2701
- return json9({
2762
+ return json10({
2702
2763
  error: "edit the `attacks` block in Config \u2014 the policy has to survive a restart, so it is not held in memory here"
2703
2764
  }, 501);
2704
2765
  }
@@ -2711,7 +2772,7 @@ async function attackRoutes(req, url) {
2711
2772
  onlineOnly: body.onlineOnly,
2712
2773
  respond: false
2713
2774
  });
2714
- return json9({
2775
+ return json10({
2715
2776
  incidents: result.incidents,
2716
2777
  unavailable: result.unavailable,
2717
2778
  devices: result.devices,
@@ -2719,7 +2780,7 @@ async function attackRoutes(req, url) {
2719
2780
  });
2720
2781
  }
2721
2782
  if (p === "/api/attacks/devices" && req.method === "GET") {
2722
- return json9({
2783
+ return json10({
2723
2784
  devices: listDevices().names.map((name) => ({
2724
2785
  name,
2725
2786
  reachable: getDeviceStatus(name).reachable
@@ -2732,24 +2793,24 @@ async function attackRoutes(req, url) {
2732
2793
  const response = store3.responseFor(address);
2733
2794
  const devices = response?.devices ?? [];
2734
2795
  if (devices.length === 0)
2735
- return json9({ error: `no recorded block for ${address}` }, 404);
2796
+ return json10({ error: `no recorded block for ${address}` }, 404);
2736
2797
  const results = await revokeBlock(address, devices, response?.list);
2737
2798
  store3.revokeResponse(address, now);
2738
- return json9({ address, results });
2799
+ return json10({ address, results });
2739
2800
  }
2740
2801
  if (rest.length === 1 && req.method === "GET") {
2741
2802
  const incident = store3.getIncident(decodeURIComponent(rest[0]));
2742
2803
  if (!incident)
2743
- return json9({ error: "not found" }, 404);
2744
- return json9({ incident, response: store3.responseFor(incident.source) });
2804
+ return json10({ error: "not found" }, 404);
2805
+ return json10({ incident, response: store3.responseFor(incident.source) });
2745
2806
  }
2746
2807
  if (rest.length === 2 && rest[1] === "respond" && req.method === "POST") {
2747
2808
  const incident = store3.getIncident(decodeURIComponent(rest[0]));
2748
2809
  if (!incident)
2749
- return json9({ error: "not found" }, 404);
2810
+ return json10({ error: "not found" }, 404);
2750
2811
  const body = await req.json().catch(() => ({}));
2751
2812
  if (body.action === "dismiss") {
2752
- return json9({ dismissed: incident.id });
2813
+ return json10({ dismissed: incident.id });
2753
2814
  }
2754
2815
  const decision = decide({
2755
2816
  incident,
@@ -2761,13 +2822,13 @@ async function attackRoutes(req, url) {
2761
2822
  timeout: body.timeout
2762
2823
  });
2763
2824
  if (!isPlan(decision)) {
2764
- return json9({ refused: true, guard: decision.guard, reason: decision.reason }, 200);
2825
+ return json10({ refused: true, guard: decision.guard, reason: decision.reason }, 200);
2765
2826
  }
2766
2827
  if (decision.action === "escalate") {
2767
- return json9({ escalated: true, reason: decision.reason });
2828
+ return json10({ escalated: true, reason: decision.reason });
2768
2829
  }
2769
2830
  if (!body.confirm) {
2770
- return json9({ dryRun: true, plan: decision });
2831
+ return json10({ dryRun: true, plan: decision });
2771
2832
  }
2772
2833
  const applied = await executePlan(decision);
2773
2834
  const ok = applied.some((r) => r.ok);
@@ -2783,15 +2844,15 @@ async function attackRoutes(req, url) {
2783
2844
  ok,
2784
2845
  error: ok ? undefined : applied.map((r) => r.detail).join("; ")
2785
2846
  });
2786
- return json9({ applied, ok, plan: decision });
2847
+ return json10({ applied, ok, plan: decision });
2787
2848
  }
2788
- return json9({ error: "not found" }, 404);
2849
+ return json10({ error: "not found" }, 404);
2789
2850
  }
2790
2851
 
2791
2852
  // src/observability/alert-routes.ts
2792
- var JSON_HEADERS10 = { "content-type": "application/json; charset=utf-8" };
2793
- function json10(body, status = 200) {
2794
- return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS10 });
2853
+ var JSON_HEADERS11 = { "content-type": "application/json; charset=utf-8" };
2854
+ function json11(body, status = 200) {
2855
+ return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS11 });
2795
2856
  }
2796
2857
  function ruleRow(rule, subject, status, since) {
2797
2858
  return {
@@ -2845,11 +2906,11 @@ async function alertRoutes(req, url, db) {
2845
2906
  const channels = getConfig().alerts?.channels;
2846
2907
  if (p === "/api/alerts" && req.method === "GET") {
2847
2908
  if (!engine) {
2848
- return json10({ configured: false, rules: [], active: [], channels: {} });
2909
+ return json11({ configured: false, rules: [], active: [], channels: {} });
2849
2910
  }
2850
2911
  const now = Date.now();
2851
2912
  const rows = engine.snapshot().map(({ rule, subject, state }) => ruleRow(rule, subject, isMuted(rule, now) ? "muted" : !rule.enabled ? "disabled" : state.status, state.since));
2852
- return json10({
2913
+ return json11({
2853
2914
  configured: true,
2854
2915
  rules: rows,
2855
2916
  active: rows.filter((r) => r.status === "firing"),
@@ -2858,41 +2919,41 @@ async function alertRoutes(req, url, db) {
2858
2919
  }
2859
2920
  if (p === "/api/alerts/history" && req.method === "GET") {
2860
2921
  if (!engine)
2861
- return json10({ history: [] });
2922
+ return json11({ history: [] });
2862
2923
  const hours = Number(url.searchParams.get("hours") ?? 24);
2863
2924
  const history = await engine.history({
2864
2925
  sinceMs: Date.now() - hours * 3600000,
2865
2926
  ruleId: url.searchParams.get("rule") ?? undefined,
2866
2927
  limit: Number(url.searchParams.get("limit") ?? 200)
2867
2928
  });
2868
- return json10({ history });
2929
+ return json11({ history });
2869
2930
  }
2870
2931
  if (p === "/api/alerts/rules" && req.method === "POST") {
2871
2932
  if (!engine)
2872
- return json10({ error: "alerting is not configured" }, 400);
2933
+ return json11({ error: "alerting is not configured" }, 400);
2873
2934
  const parsed = AlertRuleSchema.safeParse(await req.json());
2874
2935
  if (!parsed.success) {
2875
- return json10({ error: parsed.error.issues.map((i) => i.message).join("; ") }, 400);
2936
+ return json11({ error: parsed.error.issues.map((i) => i.message).join("; ") }, 400);
2876
2937
  }
2877
2938
  const rules = engine.configuredRules();
2878
2939
  if (rules.some((r) => r.id === parsed.data.id)) {
2879
- return json10({ error: `rule '${parsed.data.id}' already exists` }, 409);
2940
+ return json11({ error: `rule '${parsed.data.id}' already exists` }, 409);
2880
2941
  }
2881
2942
  engine.setRules([...rules, parsed.data]);
2882
- return json10({ ok: true, id: parsed.data.id });
2943
+ return json11({ ok: true, id: parsed.data.id });
2883
2944
  }
2884
2945
  const idMatch = p.match(/^\/api\/alerts\/rules\/([^/]+)$/);
2885
2946
  if (idMatch) {
2886
2947
  if (!engine)
2887
- return json10({ error: "alerting is not configured" }, 400);
2948
+ return json11({ error: "alerting is not configured" }, 400);
2888
2949
  const id = decodeURIComponent(idMatch[1]);
2889
2950
  const rules = engine.configuredRules();
2890
2951
  const existing = rules.find((r) => r.id === id);
2891
2952
  if (!existing)
2892
- return json10({ error: `unknown rule: ${id}` }, 404);
2953
+ return json11({ error: `unknown rule: ${id}` }, 404);
2893
2954
  if (req.method === "DELETE") {
2894
2955
  engine.setRules(rules.filter((r) => r.id !== id));
2895
- return json10({ ok: true });
2956
+ return json11({ ok: true });
2896
2957
  }
2897
2958
  if (req.method === "PATCH") {
2898
2959
  const body = await req.json();
@@ -2900,22 +2961,22 @@ async function alertRoutes(req, url, db) {
2900
2961
  if (typeof body.mute === "string") {
2901
2962
  const ms = parseDuration(body.mute);
2902
2963
  if (ms === null)
2903
- return json10({ error: `invalid duration: ${body.mute}` }, 400);
2964
+ return json11({ error: `invalid duration: ${body.mute}` }, 400);
2904
2965
  mutedUntil = ms === 0 ? undefined : Date.now() + ms;
2905
2966
  delete body.mute;
2906
2967
  }
2907
2968
  const merged = AlertRuleSchema.safeParse({ ...existing, ...body, mutedUntil });
2908
2969
  if (!merged.success) {
2909
- return json10({ error: merged.error.issues.map((i) => i.message).join("; ") }, 400);
2970
+ return json11({ error: merged.error.issues.map((i) => i.message).join("; ") }, 400);
2910
2971
  }
2911
2972
  engine.setRules(rules.map((r) => r.id === id ? merged.data : r));
2912
- return json10({ ok: true, rule: merged.data });
2973
+ return json11({ ok: true, rule: merged.data });
2913
2974
  }
2914
2975
  }
2915
2976
  if (p === "/api/alerts/test" && req.method === "POST") {
2916
2977
  const body = await req.json();
2917
2978
  if (!channels || !body?.channel)
2918
- return json10({ error: "channel is required" }, 400);
2979
+ return json11({ error: "channel is required" }, 400);
2919
2980
  try {
2920
2981
  const result = await deliver(body.channel, channels, {
2921
2982
  ruleId: "test",
@@ -2925,20 +2986,20 @@ async function alertRoutes(req, url, db) {
2925
2986
  body: "If you can read this, this channel is configured correctly.",
2926
2987
  at: Date.now()
2927
2988
  });
2928
- return json10(result);
2989
+ return json11(result);
2929
2990
  } catch (e) {
2930
2991
  logger.error(`Alert channel test failed: ${logError(e)}`);
2931
- return json10({ error: clientError(e) }, 502);
2992
+ return json11({ error: clientError(e) }, 502);
2932
2993
  }
2933
2994
  }
2934
2995
  if (p === "/api/alerts/preview" && req.method === "POST") {
2935
2996
  const parsed = AlertRuleSchema.safeParse(await req.json());
2936
2997
  if (!parsed.success) {
2937
- return json10({ error: parsed.error.issues.map((i) => i.message).join("; ") }, 400);
2998
+ return json11({ error: parsed.error.issues.map((i) => i.message).join("; ") }, 400);
2938
2999
  }
2939
3000
  const hours = Number(url.searchParams.get("hours") ?? 24);
2940
3001
  if (!db) {
2941
- return json10({ hours, sampled: 0, fires: 0, resolves: 0, matched: 0, noHistory: true });
3002
+ return json11({ hours, sampled: 0, fires: 0, resolves: 0, matched: 0, noHistory: true });
2942
3003
  }
2943
3004
  const events = db.query({ since: Date.now() - hours * 3600000, limit: 5000 }).map((e) => ({
2944
3005
  ts: e.ts,
@@ -2947,15 +3008,15 @@ async function alertRoutes(req, url, db) {
2947
3008
  risk: e.risk,
2948
3009
  isError: e.isError
2949
3010
  }));
2950
- return json10({ hours, sampled: events.length, ...previewRule(parsed.data, events) });
3011
+ return json11({ hours, sampled: events.length, ...previewRule(parsed.data, events) });
2951
3012
  }
2952
3013
  return null;
2953
3014
  }
2954
3015
 
2955
3016
  // src/observability/memory-routes.ts
2956
- var JSON_HEADERS11 = { "content-type": "application/json; charset=utf-8" };
2957
- function json11(body, status = 200) {
2958
- return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS11 });
3017
+ var JSON_HEADERS12 = { "content-type": "application/json; charset=utf-8" };
3018
+ function json12(body, status = 200) {
3019
+ return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS12 });
2959
3020
  }
2960
3021
  async function bodyJson4(req) {
2961
3022
  return await req.json();
@@ -2966,7 +3027,7 @@ async function memoryRoutes(req, url) {
2966
3027
  return null;
2967
3028
  const cfg = getConfig();
2968
3029
  if (!cfg.memory.enabled) {
2969
- return json11({ error: "Knowledge-graph memory is disabled" }, 503);
3030
+ return json12({ error: "Knowledge-graph memory is disabled" }, 503);
2970
3031
  }
2971
3032
  if (p === "/api/memory/config" && req.method === "GET") {
2972
3033
  let stats = null;
@@ -2974,7 +3035,7 @@ async function memoryRoutes(req, url) {
2974
3035
  const store4 = await getMemoryStore();
2975
3036
  stats = store4.stats();
2976
3037
  } catch {}
2977
- return json11({
3038
+ return json12({
2978
3039
  enabled: cfg.memory.enabled,
2979
3040
  dbPath: cfg.memory.dbPath,
2980
3041
  stats
@@ -2988,7 +3049,7 @@ async function memoryRoutes(req, url) {
2988
3049
  try {
2989
3050
  await reopenMemoryStore(body.dbPath);
2990
3051
  } catch (e) {
2991
- return json11({
3052
+ return json12({
2992
3053
  error: `Failed to open memory DB at ${body.dbPath}: ${e instanceof Error ? e.message : String(e)}`
2993
3054
  }, 400);
2994
3055
  }
@@ -3010,7 +3071,7 @@ async function memoryRoutes(req, url) {
3010
3071
  const store4 = await getMemoryStore();
3011
3072
  stats = store4.stats();
3012
3073
  } catch {}
3013
- return json11({
3074
+ return json12({
3014
3075
  ok: true,
3015
3076
  enabled: getConfig().memory.enabled,
3016
3077
  dbPath: getConfig().memory.dbPath,
@@ -3019,67 +3080,67 @@ async function memoryRoutes(req, url) {
3019
3080
  }
3020
3081
  const store3 = await getMemoryStore();
3021
3082
  if (p === "/api/memory/graph" && req.method === "GET") {
3022
- return json11(store3.readGraph());
3083
+ return json12(store3.readGraph());
3023
3084
  }
3024
3085
  if (p === "/api/memory/stats" && req.method === "GET") {
3025
- return json11(store3.stats());
3086
+ return json12(store3.stats());
3026
3087
  }
3027
3088
  if (p === "/api/memory/search" && req.method === "GET") {
3028
3089
  const q = url.searchParams.get("q") || "";
3029
3090
  const limit = Number(url.searchParams.get("limit") ?? 50);
3030
- return json11(store3.searchNodes(q, limit));
3091
+ return json12(store3.searchNodes(q, limit));
3031
3092
  }
3032
3093
  if (p === "/api/memory/activity" && req.method === "GET") {
3033
3094
  const limit = Number(url.searchParams.get("limit") ?? 50);
3034
3095
  const since = url.searchParams.get("since");
3035
- return json11(store3.activity(limit, since ? Number(since) : undefined));
3096
+ return json12(store3.activity(limit, since ? Number(since) : undefined));
3036
3097
  }
3037
3098
  const entityMatch = p.match(/^\/api\/memory\/entity\/(.+)$/);
3038
3099
  if (entityMatch && req.method === "GET") {
3039
3100
  const name = decodeURIComponent(entityMatch[1]);
3040
3101
  const graph = store3.openNodes([name]);
3041
3102
  if (graph.entities.length === 0)
3042
- return json11({ error: "Entity not found" }, 404);
3043
- return json11({ entity: graph.entities[0], relations: graph.relations });
3103
+ return json12({ error: "Entity not found" }, 404);
3104
+ return json12({ entity: graph.entities[0], relations: graph.relations });
3044
3105
  }
3045
3106
  if (p === "/api/memory/entities" && req.method === "POST") {
3046
3107
  const body = await bodyJson4(req);
3047
3108
  const created = store3.createEntities(body.entities ?? []);
3048
- return json11({ created, count: created.length });
3109
+ return json12({ created, count: created.length });
3049
3110
  }
3050
3111
  if (p === "/api/memory/relations" && req.method === "POST") {
3051
3112
  const body = await bodyJson4(req);
3052
3113
  const created = store3.createRelations(body.relations ?? []);
3053
- return json11({ created, count: created.length });
3114
+ return json12({ created, count: created.length });
3054
3115
  }
3055
3116
  if (p === "/api/memory/observations" && req.method === "POST") {
3056
3117
  const body = await bodyJson4(req);
3057
3118
  const results = store3.addObservations(body.observations ?? []);
3058
- return json11({ results });
3119
+ return json12({ results });
3059
3120
  }
3060
3121
  if (p === "/api/memory/entities" && req.method === "DELETE") {
3061
3122
  const body = await bodyJson4(req);
3062
3123
  const removed = store3.deleteEntities(body.names ?? []);
3063
- return json11({ removed });
3124
+ return json12({ removed });
3064
3125
  }
3065
3126
  if (p === "/api/memory/relations" && req.method === "DELETE") {
3066
3127
  const body = await bodyJson4(req);
3067
3128
  const removed = store3.deleteRelations(body.relations ?? []);
3068
- return json11({ removed });
3129
+ return json12({ removed });
3069
3130
  }
3070
3131
  if (p === "/api/memory/observations" && req.method === "DELETE") {
3071
3132
  const body = await bodyJson4(req);
3072
3133
  const removed = store3.deleteObservations(body.deletions ?? []);
3073
- return json11({ removed });
3134
+ return json12({ removed });
3074
3135
  }
3075
3136
  return null;
3076
3137
  }
3077
3138
 
3078
3139
  // src/observability/dashboard.ts
3079
3140
  var SERVER_TAG3 = "mikrotik-mcp";
3080
- var JSON_HEADERS12 = { "content-type": "application/json; charset=utf-8" };
3081
- function json12(body, status = 200) {
3082
- return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS12 });
3141
+ var JSON_HEADERS13 = { "content-type": "application/json; charset=utf-8" };
3142
+ function json13(body, status = 200) {
3143
+ return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS13 });
3083
3144
  }
3084
3145
  function restartProcess() {
3085
3146
  let ok = false;
@@ -3379,11 +3440,11 @@ function issues(error) {
3379
3440
  async function configRoutes(req, url, admin) {
3380
3441
  const p = url.pathname;
3381
3442
  if (p === "/api/config-schema" && req.method === "GET") {
3382
- return json12(configSchemaJson());
3443
+ return json13(configSchemaJson());
3383
3444
  }
3384
3445
  if (p === "/api/config/validate" && req.method === "POST") {
3385
3446
  const merged = mergeSecrets(await readJson(req), getConfig());
3386
- return json12(validateConfig(merged));
3447
+ return json13(validateConfig(merged));
3387
3448
  }
3388
3449
  if (p === "/api/config/test-device" && req.method === "POST") {
3389
3450
  const body = await readJson(req);
@@ -3392,22 +3453,22 @@ async function configRoutes(req, url, admin) {
3392
3453
  const merged = mergeSecrets(body?.config, currentDc);
3393
3454
  const parsed = DeviceConfigSchema.safeParse(merged);
3394
3455
  if (!parsed.success)
3395
- return json12({ ok: false, errors: issues(parsed.error) }, 400);
3456
+ return json13({ ok: false, errors: issues(parsed.error) }, 400);
3396
3457
  const status = await probeDevice(`config-test:${name}`, parsed.data);
3397
- return json12({ ok: true, status });
3458
+ return json13({ ok: true, status });
3398
3459
  }
3399
3460
  if (p === "/api/config/preview" && req.method === "POST") {
3400
3461
  const before = JSON.stringify(redact(getConfig()), null, 2);
3401
3462
  const after = JSON.stringify(await readJson(req) ?? {}, null, 2);
3402
3463
  const d = diffLines(before, after, { fromLabel: "current", toLabel: "edited" });
3403
- return json12({ summary: d.summary, unified: d.unified });
3464
+ return json13({ summary: d.summary, unified: d.unified });
3404
3465
  }
3405
3466
  if (p === "/api/config" && req.method === "POST") {
3406
3467
  const body = await readJson(req);
3407
3468
  const merged = mergeSecrets(body?.config, getConfig());
3408
3469
  const v = validateConfig(merged);
3409
3470
  if (!v.ok || !v.value)
3410
- return json12({ ok: false, errors: v.errors }, 400);
3471
+ return json13({ ok: false, errors: v.errors }, 400);
3411
3472
  const prev = getConfig();
3412
3473
  const before = JSON.stringify(redact(prev), null, 2);
3413
3474
  const after = JSON.stringify(redact(v.value), null, 2);
@@ -3416,25 +3477,25 @@ async function configRoutes(req, url, admin) {
3416
3477
  const nextDevs = Object.keys(v.value.devices);
3417
3478
  const devicesChanged = prevDevs.length !== nextDevs.length || prevDevs.some((d) => !nextDevs.includes(d));
3418
3479
  const res = admin.applyConfig(v.value, clampRollback(body?.rollbackMs));
3419
- return json12({ ok: true, ...res, devicesChanged, summary: diff.summary, unified: diff.unified });
3480
+ return json13({ ok: true, ...res, devicesChanged, summary: diff.summary, unified: diff.unified });
3420
3481
  }
3421
3482
  if (p === "/api/config/keep" && req.method === "POST") {
3422
3483
  const body = await readJson(req);
3423
3484
  const kept = admin.keepConfig(String(body?.pendingId ?? ""));
3424
3485
  if (kept)
3425
3486
  recordVersion(getConfig(), "auto", Date.now());
3426
- return json12({ kept });
3487
+ return json13({ kept });
3427
3488
  }
3428
3489
  if (p === "/api/config/rollback" && req.method === "POST") {
3429
3490
  const body = await readJson(req);
3430
3491
  const rolledBack = admin.rollback(String(body?.pendingId ?? ""));
3431
- return json12({ rolledBack, config: redact(getConfig()) });
3492
+ return json13({ rolledBack, config: redact(getConfig()) });
3432
3493
  }
3433
3494
  if (p === "/api/reload" && req.method === "POST") {
3434
3495
  const body = await readJson(req);
3435
3496
  if (body?.hard === true) {
3436
3497
  const relaunched = restartProcess();
3437
- return json12({
3498
+ return json13({
3438
3499
  ok: true,
3439
3500
  mode: "restart",
3440
3501
  note: relaunched ? "Restarting now \u2014 a fresh server process was launched and will bind the same port(s) in ~1.5s. Reconnect shortly." : "Could not self-relaunch; the process is exiting. It only comes back if a supervisor respawns it."
@@ -3445,10 +3506,10 @@ async function configRoutes(req, url, admin) {
3445
3506
  setConfig(cfg);
3446
3507
  const devices = Object.keys(cfg.devices);
3447
3508
  logger.info(`Config reloaded from source \u2014 ${devices.length} device(s): ${devices.join(", ")}`);
3448
- return json12({ ok: true, mode: "config", count: devices.length, devices });
3509
+ return json13({ ok: true, mode: "config", count: devices.length, devices });
3449
3510
  } catch (e) {
3450
3511
  logger.error(`Config reload failed: ${logError(e)}`);
3451
- return json12({ ok: false, error: clientError(e) }, 500);
3512
+ return json13({ ok: false, error: clientError(e) }, 500);
3452
3513
  }
3453
3514
  }
3454
3515
  if (p === "/api/config/history" && req.method === "GET") {
@@ -3464,55 +3525,55 @@ async function configRoutes(req, url, admin) {
3464
3525
  } catch {}
3465
3526
  return { ...v, drift: { added, removed } };
3466
3527
  });
3467
- return json12({ versions, bytes: historyBytes(), retention: AUTO_RETENTION });
3528
+ return json13({ versions, bytes: historyBytes(), retention: AUTO_RETENTION });
3468
3529
  }
3469
3530
  if (p === "/api/config/history/get" && req.method === "GET") {
3470
3531
  const id = url.searchParams.get("id");
3471
3532
  if (!id)
3472
- return json12({ error: "id required" }, 400);
3533
+ return json13({ error: "id required" }, 400);
3473
3534
  try {
3474
3535
  const v = readVersion(id);
3475
- return json12({
3536
+ return json13({
3476
3537
  ts: v.ts,
3477
3538
  kind: v.kind,
3478
3539
  label: v.label,
3479
3540
  config: JSON.stringify(redact(v.config), null, 2)
3480
3541
  });
3481
3542
  } catch {
3482
- return json12({ error: "not found" }, 404);
3543
+ return json13({ error: "not found" }, 404);
3483
3544
  }
3484
3545
  }
3485
3546
  if (p === "/api/config/history/diff" && req.method === "GET") {
3486
3547
  const id = url.searchParams.get("id");
3487
3548
  if (!id)
3488
- return json12({ error: "id required" }, 400);
3549
+ return json13({ error: "id required" }, 400);
3489
3550
  try {
3490
3551
  const before = JSON.stringify(redact(readVersion(id).config), null, 2);
3491
3552
  const after = JSON.stringify(redact(getConfig()), null, 2);
3492
3553
  const d = diffLines(before, after, { fromLabel: "this version", toLabel: "current" });
3493
- return json12({ summary: d.summary, unified: d.unified });
3554
+ return json13({ summary: d.summary, unified: d.unified });
3494
3555
  } catch {
3495
- return json12({ error: "not found" }, 404);
3556
+ return json13({ error: "not found" }, 404);
3496
3557
  }
3497
3558
  }
3498
3559
  if (p === "/api/config/history/checkpoint" && req.method === "POST") {
3499
3560
  const b = await readJson(req);
3500
3561
  const label = b?.label?.trim() || "checkpoint";
3501
- return json12({ ok: true, version: recordVersion(getConfig(), "checkpoint", Date.now(), label) });
3562
+ return json13({ ok: true, version: recordVersion(getConfig(), "checkpoint", Date.now(), label) });
3502
3563
  }
3503
3564
  if (p === "/api/config/history/restore" && req.method === "POST") {
3504
3565
  const b = await readJson(req);
3505
3566
  if (!b?.id)
3506
- return json12({ error: "id required" }, 400);
3567
+ return json13({ error: "id required" }, 400);
3507
3568
  let target;
3508
3569
  try {
3509
3570
  target = readVersion(b.id);
3510
3571
  } catch {
3511
- return json12({ error: "not found" }, 404);
3572
+ return json13({ error: "not found" }, 404);
3512
3573
  }
3513
3574
  const v = validateConfig(target.config);
3514
3575
  if (!v.ok || !v.value)
3515
- return json12({ ok: false, errors: v.errors }, 400);
3576
+ return json13({ ok: false, errors: v.errors }, 400);
3516
3577
  recordVersion(getConfig(), "auto", Date.now(), "before restore");
3517
3578
  setConfig(v.value);
3518
3579
  let persisted = true;
@@ -3523,13 +3584,13 @@ async function configRoutes(req, url, admin) {
3523
3584
  }
3524
3585
  const label = target.label ? `restored "${target.label}"` : `restored ${b.id}`;
3525
3586
  recordVersion(getConfig(), "auto", Date.now(), label);
3526
- return json12({ ok: true, persisted, restored: b.id, config: redact(getConfig()) });
3587
+ return json13({ ok: true, persisted, restored: b.id, config: redact(getConfig()) });
3527
3588
  }
3528
3589
  if (p === "/api/config/history/delete" && req.method === "POST") {
3529
3590
  const b = await readJson(req);
3530
3591
  if (!b?.id)
3531
- return json12({ error: "id required" }, 400);
3532
- return deleteVersion(b.id) ? json12({ ok: true }) : json12({ error: "not found" }, 404);
3592
+ return json13({ error: "id required" }, 400);
3593
+ return deleteVersion(b.id) ? json13({ ok: true }) : json13({ error: "not found" }, 404);
3533
3594
  }
3534
3595
  return null;
3535
3596
  }
@@ -3540,7 +3601,7 @@ async function modulesRoutes(req, url) {
3540
3601
  if (p === "/api/modules" && req.method === "GET") {
3541
3602
  const cfg = getConfig();
3542
3603
  const src = getConfigSource();
3543
- return json12({
3604
+ return json13({
3544
3605
  ...moduleSurface(cfg.tools),
3545
3606
  filter: cfg.tools,
3546
3607
  source: src,
@@ -3550,11 +3611,11 @@ async function modulesRoutes(req, url) {
3550
3611
  if (p === "/api/modules/toggle" && req.method === "POST") {
3551
3612
  const b = await readJson(req);
3552
3613
  if (typeof b?.slug !== "string" || typeof b?.enabled !== "boolean") {
3553
- return json12({ error: "slug (string) and enabled (boolean) are required" }, 400);
3614
+ return json13({ error: "slug (string) and enabled (boolean) are required" }, 400);
3554
3615
  }
3555
3616
  const mod = moduleCatalog.find((m) => m.slug.toLowerCase() === b.slug.toLowerCase());
3556
3617
  if (!mod)
3557
- return json12({ error: `unknown module: ${b.slug}` }, 404);
3618
+ return json13({ error: `unknown module: ${b.slug}` }, 404);
3558
3619
  const cfg = getConfig();
3559
3620
  const nextTools = ToolFilterSchema.parse(applyModuleToggle(cfg.tools, mod.slug, b.enabled));
3560
3621
  const next = { ...cfg, tools: nextTools };
@@ -3570,7 +3631,7 @@ async function modulesRoutes(req, url) {
3570
3631
  if (persisted) {
3571
3632
  recordVersion(getConfig(), "auto", Date.now(), `module ${b.enabled ? "enabled" : "disabled"}: ${mod.slug}`);
3572
3633
  }
3573
- return json12({
3634
+ return json13({
3574
3635
  ok: true,
3575
3636
  persisted,
3576
3637
  requiresReconnect: true,
@@ -3585,7 +3646,7 @@ async function modulesRoutes(req, url) {
3585
3646
  if (p === "/api/modules/app-views" && req.method === "POST") {
3586
3647
  const b = await readJson(req);
3587
3648
  if (typeof b?.enabled !== "boolean") {
3588
- return json12({ error: "enabled (boolean) is required" }, 400);
3649
+ return json13({ error: "enabled (boolean) is required" }, 400);
3589
3650
  }
3590
3651
  const cfg = getConfig();
3591
3652
  const next = { ...cfg, mcp: { ...cfg.mcp, appViews: b.enabled } };
@@ -3601,7 +3662,7 @@ async function modulesRoutes(req, url) {
3601
3662
  if (persisted) {
3602
3663
  recordVersion(getConfig(), "auto", Date.now(), `app views ${b.enabled ? "enabled" : "disabled"}`);
3603
3664
  }
3604
- return json12({
3665
+ return json13({
3605
3666
  ok: true,
3606
3667
  persisted,
3607
3668
  requiresReconnect: true,
@@ -3615,11 +3676,11 @@ async function modulesRoutes(req, url) {
3615
3676
  async function captureRoutes(req, url) {
3616
3677
  const p = url.pathname;
3617
3678
  if (p === "/api/capture/status" && req.method === "GET") {
3618
- return json12(capture.stats());
3679
+ return json13(capture.stats());
3619
3680
  }
3620
3681
  if (p === "/api/capture/packets" && req.method === "GET") {
3621
3682
  const limit = Number(url.searchParams.get("limit") ?? 200) || 200;
3622
- return json12({ packets: capture.recent(limit), stats: capture.stats() });
3683
+ return json13({ packets: capture.recent(limit), stats: capture.stats() });
3623
3684
  }
3624
3685
  if (p === "/api/capture/pcap" && req.method === "GET") {
3625
3686
  return new Response(capture.pcap(), {
@@ -3631,11 +3692,11 @@ async function captureRoutes(req, url) {
3631
3692
  }
3632
3693
  if (p === "/api/capture/start" && req.method === "POST") {
3633
3694
  const body = await readJson(req);
3634
- return json12(await capture.start(typeof body?.port === "number" ? body.port : DEFAULT_TZSP_PORT));
3695
+ return json13(await capture.start(typeof body?.port === "number" ? body.port : DEFAULT_TZSP_PORT));
3635
3696
  }
3636
3697
  if (p === "/api/capture/stop" && req.method === "POST") {
3637
3698
  capture.stop();
3638
- return json12({ ok: true, ...capture.stats() });
3699
+ return json13({ ok: true, ...capture.stats() });
3639
3700
  }
3640
3701
  return null;
3641
3702
  }
@@ -3647,24 +3708,24 @@ async function capsmanRoutes(req, url) {
3647
3708
  if (req.method === "GET") {
3648
3709
  const ctx = createContext(undefined, url.searchParams.get("device") ?? undefined);
3649
3710
  if (p === "/api/capsman/overview") {
3650
- return json12(capsmanOverview(await fetchCapsmanState(ctx)));
3711
+ return json13(capsmanOverview(await fetchCapsmanState(ctx)));
3651
3712
  }
3652
3713
  if (p === "/api/capsman/clients") {
3653
3714
  const state = await fetchCapsmanState(ctx);
3654
3715
  const weakDbm = Number.parseInt(url.searchParams.get("weak_dbm") ?? "", 10);
3655
3716
  const weak = reportWeakClients(state, Number.isFinite(weakDbm) ? weakDbm : undefined);
3656
- return json12({ clients: state.clients, weak });
3717
+ return json13({ clients: state.clients, weak });
3657
3718
  }
3658
3719
  if (p === "/api/capsman/audit") {
3659
- return json12(runCapsmanAudit(await fetchCapsmanState(ctx)));
3720
+ return json13(runCapsmanAudit(await fetchCapsmanState(ctx)));
3660
3721
  }
3661
3722
  if (p === "/api/capsman/trends") {
3662
3723
  if (!capsmanStore)
3663
- return json12({ error: "capsman store not active" }, 503);
3724
+ return json13({ error: "capsman store not active" }, 503);
3664
3725
  const device = resolveDeviceName(url.searchParams.get("device") ?? undefined);
3665
3726
  const days = daysParam(url, 7, 30);
3666
3727
  const series = capsmanStore.radioSeries(device, Date.now() - days * 86400000);
3667
- return json12({ series, days });
3728
+ return json13({ series, days });
3668
3729
  }
3669
3730
  return null;
3670
3731
  }
@@ -3675,45 +3736,45 @@ async function capsmanRoutes(req, url) {
3675
3736
  if (p === "/api/capsman/apply/steer") {
3676
3737
  const client = state.clients.find((c) => c.mac.toLowerCase() === (body.mac ?? "").toLowerCase());
3677
3738
  if (!client)
3678
- return json12({ ok: false, error: "client not associated" }, 400);
3739
+ return json13({ ok: false, error: "client not associated" }, 400);
3679
3740
  if (steerAlreadyPresent(state, body.mac ?? "")) {
3680
- return json12({ ok: true, message: "already steered (no-op)" });
3741
+ return json13({ ok: true, message: "already steered (no-op)" });
3681
3742
  }
3682
3743
  const mode = body.mode === "soft" ? "soft" : "hard";
3683
3744
  const commands = buildSteerCommands(state, body.mac ?? "", client.radioId, mode);
3684
3745
  if (!body.confirm)
3685
- return json12({ ok: true, preview: commands });
3686
- return json12(await applyCapsmanWrites(ctx, commands, `pre-steer-${body.mac}`));
3746
+ return json13({ ok: true, preview: commands });
3747
+ return json13(await applyCapsmanWrites(ctx, commands, `pre-steer-${body.mac}`));
3687
3748
  }
3688
3749
  if (p === "/api/capsman/apply/load-balance") {
3689
3750
  const plan = loadBalancePlan(state);
3690
3751
  const commands = buildLoadBalanceCommands(state, plan);
3691
3752
  if (!body.confirm)
3692
- return json12({ ok: true, preview: commands, plan });
3693
- return json12(await applyCapsmanWrites(ctx, commands, "pre-load-balance"));
3753
+ return json13({ ok: true, preview: commands, plan });
3754
+ return json13(await applyCapsmanWrites(ctx, commands, "pre-load-balance"));
3694
3755
  }
3695
3756
  if (p === "/api/capsman/apply/channel-plan") {
3696
3757
  if (state.path === "/caps-man") {
3697
- return json12({ ok: false, error: "channel-plan apply is v7 /interface wifi only" }, 400);
3758
+ return json13({ ok: false, error: "channel-plan apply is v7 /interface wifi only" }, 400);
3698
3759
  }
3699
3760
  const commands = buildChannelPlanCommands(state);
3700
3761
  if (!body.confirm)
3701
- return json12({ ok: true, preview: commands });
3702
- return json12(await applyCapsmanWrites(ctx, commands, "pre-channel-plan"));
3762
+ return json13({ ok: true, preview: commands });
3763
+ return json13(await applyCapsmanWrites(ctx, commands, "pre-channel-plan"));
3703
3764
  }
3704
3765
  if (p === "/api/capsman/apply/ft") {
3705
3766
  const commands = buildFtCommands(state);
3706
3767
  if (!body.confirm)
3707
- return json12({ ok: true, preview: commands });
3708
- return json12(await applyCapsmanWrites(ctx, commands, "pre-ft"));
3768
+ return json13({ ok: true, preview: commands });
3769
+ return json13(await applyCapsmanWrites(ctx, commands, "pre-ft"));
3709
3770
  }
3710
3771
  if (p === "/api/capsman/apply/ha") {
3711
3772
  const commands = buildHaCommands(state);
3712
3773
  const guidance = haGuidance(state);
3713
3774
  if (!body.confirm)
3714
- return json12({ ok: true, preview: commands, guidance });
3775
+ return json13({ ok: true, preview: commands, guidance });
3715
3776
  const res = await applyCapsmanWrites(ctx, commands, "pre-ha");
3716
- return json12({ ...res, guidance });
3777
+ return json13({ ...res, guidance });
3717
3778
  }
3718
3779
  }
3719
3780
  return null;
@@ -3739,32 +3800,32 @@ async function clientsRoutes(req, url) {
3739
3800
  const deviceFromQuery = () => url.searchParams.get("device") ?? undefined;
3740
3801
  if (p === "/api/clients" && req.method === "GET") {
3741
3802
  const ctx = createContext(undefined, deviceFromQuery());
3742
- return json12(devicesView(await fetchDevices(ctx)));
3803
+ return json13(devicesView(await fetchDevices(ctx)));
3743
3804
  }
3744
3805
  if (p === "/api/clients/traffic" && req.method === "GET") {
3745
3806
  const ip = url.searchParams.get("ip");
3746
3807
  if (!ip)
3747
- return json12({ error: "ip required" }, 400);
3808
+ return json13({ error: "ip required" }, 400);
3748
3809
  const ctx = createContext(undefined, deviceFromQuery());
3749
- return json12(await sampleDeviceTraffic(ctx, ip));
3810
+ return json13(await sampleDeviceTraffic(ctx, ip));
3750
3811
  }
3751
3812
  if (p === "/api/clients/traffic-bulk" && req.method === "GET") {
3752
3813
  const ctx = createContext(undefined, deviceFromQuery());
3753
- return json12(await sampleAllTraffic(ctx));
3814
+ return json13(await sampleAllTraffic(ctx));
3754
3815
  }
3755
3816
  if (req.method === "POST") {
3756
3817
  const b = await readJson(req);
3757
3818
  const ctx = createContext(undefined, b?.device);
3758
3819
  if (p === "/api/clients/limits") {
3759
3820
  if (!b?.ip)
3760
- return json12({ error: "ip required" }, 400);
3761
- return json12(await setDeviceLimits(ctx, b.ip, { download: b.download, upload: b.upload }));
3821
+ return json13({ error: "ip required" }, 400);
3822
+ return json13(await setDeviceLimits(ctx, b.ip, { download: b.download, upload: b.upload }));
3762
3823
  }
3763
3824
  if (!b?.mac)
3764
- return json12({ error: "mac required" }, 400);
3825
+ return json13({ error: "mac required" }, 400);
3765
3826
  const run = async (op) => {
3766
3827
  const r = await op;
3767
- return json12({ ...r, view: r.ok ? devicesView(await fetchDevices(ctx)) : undefined });
3828
+ return json13({ ...r, view: r.ok ? devicesView(await fetchDevices(ctx)) : undefined });
3768
3829
  };
3769
3830
  if (p === "/api/clients/block")
3770
3831
  return run(blockDevice(ctx, b.mac, b.comment));
@@ -3776,12 +3837,12 @@ async function clientsRoutes(req, url) {
3776
3837
  return run(removeDeviceLease(ctx, b.mac));
3777
3838
  if (p === "/api/clients/set-ip") {
3778
3839
  if (!b.ip)
3779
- return json12({ error: "ip required" }, 400);
3840
+ return json13({ error: "ip required" }, 400);
3780
3841
  return run(setDeviceIp(ctx, b.mac, b.ip));
3781
3842
  }
3782
3843
  if (p === "/api/clients/label") {
3783
3844
  if (typeof b.label !== "string")
3784
- return json12({ error: "label required" }, 400);
3845
+ return json13({ error: "label required" }, 400);
3785
3846
  return run(setDeviceLabel(ctx, b.mac, b.label));
3786
3847
  }
3787
3848
  }
@@ -3794,20 +3855,20 @@ async function aaaRoutes(req, url) {
3794
3855
  const deviceFromQuery = () => url.searchParams.get("device") ?? undefined;
3795
3856
  if (req.method === "GET") {
3796
3857
  if (p === "/api/aaa/entities") {
3797
- return json12({ entities: AAA_ENTITIES });
3858
+ return json13({ entities: AAA_ENTITIES });
3798
3859
  }
3799
3860
  if (p === "/api/aaa/radius-incoming") {
3800
- return json12(await getRadiusIncoming(createContext(undefined, deviceFromQuery())));
3861
+ return json13(await getRadiusIncoming(createContext(undefined, deviceFromQuery())));
3801
3862
  }
3802
3863
  if (p === "/api/aaa/um-settings") {
3803
- return json12(await getUmSettings(createContext(undefined, deviceFromQuery())));
3864
+ return json13(await getUmSettings(createContext(undefined, deviceFromQuery())));
3804
3865
  }
3805
3866
  const listMatch = p.match(/^\/api\/aaa\/list\/([\w-]+)$/);
3806
3867
  if (listMatch) {
3807
3868
  const slug = listMatch[1];
3808
3869
  if (!AAA_ENTITIES[slug])
3809
- return json12({ error: "unknown entity" }, 404);
3810
- return json12(await listAaaEntity(createContext(undefined, deviceFromQuery()), slug));
3870
+ return json13({ error: "unknown entity" }, 404);
3871
+ return json13(await listAaaEntity(createContext(undefined, deviceFromQuery()), slug));
3811
3872
  }
3812
3873
  return null;
3813
3874
  }
@@ -3815,30 +3876,30 @@ async function aaaRoutes(req, url) {
3815
3876
  const body = await readJson(req);
3816
3877
  const ctx = createContext(undefined, body?.device);
3817
3878
  if (p === "/api/aaa/radius-incoming")
3818
- return json12(await setRadiusIncoming(ctx, body?.fields ?? {}));
3879
+ return json13(await setRadiusIncoming(ctx, body?.fields ?? {}));
3819
3880
  if (p === "/api/aaa/um-settings")
3820
- return json12(await setUmSettings(ctx, body?.fields ?? {}));
3881
+ return json13(await setUmSettings(ctx, body?.fields ?? {}));
3821
3882
  if (p === "/api/aaa/radius-reset-counters")
3822
- return json12(await resetRadiusCounters(ctx));
3883
+ return json13(await resetRadiusCounters(ctx));
3823
3884
  const slug = body?.slug ?? "";
3824
3885
  if (!AAA_ENTITIES[slug])
3825
- return json12({ error: "unknown entity" }, 404);
3886
+ return json13({ error: "unknown entity" }, 404);
3826
3887
  if (p === "/api/aaa/add")
3827
- return json12(await addAaaEntity(ctx, slug, body?.fields ?? {}));
3888
+ return json13(await addAaaEntity(ctx, slug, body?.fields ?? {}));
3828
3889
  if (p === "/api/aaa/update") {
3829
3890
  if (!body?.id)
3830
- return json12({ error: "id required" }, 400);
3831
- return json12(await updateAaaEntity(ctx, slug, body.id, body?.fields ?? {}));
3891
+ return json13({ error: "id required" }, 400);
3892
+ return json13(await updateAaaEntity(ctx, slug, body.id, body?.fields ?? {}));
3832
3893
  }
3833
3894
  if (p === "/api/aaa/remove") {
3834
3895
  if (!body?.id)
3835
- return json12({ error: "id required" }, 400);
3836
- return json12(await removeAaaEntity(ctx, slug, body.id));
3896
+ return json13({ error: "id required" }, 400);
3897
+ return json13(await removeAaaEntity(ctx, slug, body.id));
3837
3898
  }
3838
3899
  if (p === "/api/aaa/toggle") {
3839
3900
  if (!body?.id)
3840
- return json12({ error: "id required" }, 400);
3841
- return json12(await toggleAaaEntity(ctx, slug, body.id, body?.enable === true));
3901
+ return json13({ error: "id required" }, 400);
3902
+ return json13(await toggleAaaEntity(ctx, slug, body.id, body?.enable === true));
3842
3903
  }
3843
3904
  }
3844
3905
  return null;
@@ -3854,43 +3915,43 @@ async function usageRoutes(req, url) {
3854
3915
  return null;
3855
3916
  if (p === "/api/usage/sampler") {
3856
3917
  if (req.method === "GET")
3857
- return json12({ intervalMs: getUsageSamplerInterval() });
3918
+ return json13({ intervalMs: getUsageSamplerInterval() });
3858
3919
  if (req.method === "POST") {
3859
3920
  const b = await readJson(req);
3860
3921
  const applied = setUsageSamplerInterval(Number(b?.intervalMs));
3861
- return json12({ intervalMs: applied });
3922
+ return json13({ intervalMs: applied });
3862
3923
  }
3863
3924
  return null;
3864
3925
  }
3865
3926
  if (req.method !== "GET")
3866
3927
  return null;
3867
3928
  if (!usageStore)
3868
- return json12({ error: "usage store not active" }, 503);
3929
+ return json13({ error: "usage store not active" }, 503);
3869
3930
  const device = resolveDeviceName(url.searchParams.get("device") ?? undefined);
3870
3931
  const sinceTs = (days) => Date.now() - days * 86400000;
3871
3932
  if (p === "/api/usage/um-users") {
3872
- return json12({ users: usageStore.umUsers(device) });
3933
+ return json13({ users: usageStore.umUsers(device) });
3873
3934
  }
3874
3935
  if (p === "/api/usage/client") {
3875
3936
  const ip = url.searchParams.get("ip");
3876
3937
  if (!ip)
3877
- return json12({ error: "ip required" }, 400);
3938
+ return json13({ error: "ip required" }, 400);
3878
3939
  const series = usageStore.clientDailyUsage(device, ip, sinceTs(daysParam(url, 90, 400)));
3879
- return json12(withTotals(series));
3940
+ return json13(withTotals(series));
3880
3941
  }
3881
3942
  if (p === "/api/usage/um-user") {
3882
3943
  const user = url.searchParams.get("user");
3883
3944
  if (!user)
3884
- return json12({ error: "user required" }, 400);
3945
+ return json13({ error: "user required" }, 400);
3885
3946
  const series = usageStore.umUserDailyUsage(device, user, sinceTs(daysParam(url, 90, 400)));
3886
- return json12(withTotals(series));
3947
+ return json13(withTotals(series));
3887
3948
  }
3888
3949
  if (p === "/api/usage/heatmap") {
3889
3950
  const user = url.searchParams.get("user");
3890
3951
  const days = usageStore.heatmap(device, user || null, sinceTs(daysParam(url, 371, 400)));
3891
3952
  const total = days.reduce((s, d) => s + d.count, 0);
3892
3953
  const max = days.reduce((m, d) => Math.max(m, d.count), 0);
3893
- return json12({ days, total, max });
3954
+ return json13({ days, total, max });
3894
3955
  }
3895
3956
  return null;
3896
3957
  }
@@ -3914,13 +3975,13 @@ async function featureRoutes(req, url) {
3914
3975
  const cfg = getConfig();
3915
3976
  const all = Object.keys(cfg.devices).flatMap((d) => store3.list(d, 200, false));
3916
3977
  all.sort((a, b) => b.ts - a.ts);
3917
- return json12({ snapshots: all });
3978
+ return json13({ snapshots: all });
3918
3979
  }
3919
3980
  const snapMatch = p.match(/^\/api\/snapshot\/(.+)$/);
3920
3981
  if (snapMatch && req.method === "GET") {
3921
3982
  const store3 = await snapStore();
3922
3983
  const s = store3.get(decodeURIComponent(snapMatch[1]));
3923
- return s ? json12(s) : json12({ error: "not found" }, 404);
3984
+ return s ? json13(s) : json13({ error: "not found" }, 404);
3924
3985
  }
3925
3986
  if (p === "/api/snapshots/diff" && req.method === "POST") {
3926
3987
  const store3 = await snapStore();
@@ -3928,13 +3989,13 @@ async function featureRoutes(req, url) {
3928
3989
  const from = b?.from ? store3.get(b.from) : null;
3929
3990
  const to = b?.to ? store3.get(b.to) : null;
3930
3991
  if (!from || !to) {
3931
- return json12({ error: "both 'from' and 'to' snapshot ids are required" }, 400);
3992
+ return json13({ error: "both 'from' and 'to' snapshot ids are required" }, 400);
3932
3993
  }
3933
3994
  const diff = diffLines(normalizeExport(from.body), normalizeExport(to.body), {
3934
3995
  fromLabel: from.label ?? from.id,
3935
3996
  toLabel: to.label ?? to.id
3936
3997
  });
3937
- return json12({
3998
+ return json13({
3938
3999
  from: { id: from.id, label: from.label, ts: from.ts, device: from.device },
3939
4000
  to: { id: to.id, label: to.label, ts: to.ts, device: to.device },
3940
4001
  summary: diff.summary,
@@ -3948,16 +4009,16 @@ async function featureRoutes(req, url) {
3948
4009
  ...b?.script ? splitCommands(b.script) : []
3949
4010
  ];
3950
4011
  if (commands.length === 0)
3951
- return json12({ error: "no commands provided" }, 400);
4012
+ return json13({ error: "no commands provided" }, 400);
3952
4013
  const plan = buildChangePlan(commands);
3953
- return json12({ plan, text: renderPlan(plan) });
4014
+ return json13({ plan, text: renderPlan(plan) });
3954
4015
  }
3955
4016
  if (p === "/api/s3" && req.method === "GET") {
3956
- return json12({ configured: isS3Configured(), target: isS3Configured() ? s3Target() : null });
4017
+ return json13({ configured: isS3Configured(), target: isS3Configured() ? s3Target() : null });
3957
4018
  }
3958
4019
  if (p === "/api/s3/list" && req.method === "GET") {
3959
4020
  if (!isS3Configured())
3960
- return json12({ configured: false, objects: [] });
4021
+ return json13({ configured: false, objects: [] });
3961
4022
  const prefix = url.searchParams.get("prefix") ?? "";
3962
4023
  try {
3963
4024
  const res = await getS3Client().list({ prefix: prefix || undefined, maxKeys: 1000 });
@@ -3966,42 +4027,42 @@ async function featureRoutes(req, url) {
3966
4027
  size: o.size ?? 0,
3967
4028
  lastModified: o.lastModified ?? null
3968
4029
  }));
3969
- return json12({ configured: true, target: s3Target(), objects, truncated: !!res.isTruncated });
4030
+ return json13({ configured: true, target: s3Target(), objects, truncated: !!res.isTruncated });
3970
4031
  } catch (e) {
3971
- return json12({ error: clientError(e) }, 502);
4032
+ return json13({ error: clientError(e) }, 502);
3972
4033
  }
3973
4034
  }
3974
4035
  if (p === "/api/s3/presign" && req.method === "GET") {
3975
4036
  if (!isS3Configured())
3976
- return json12({ error: "S3 not configured" }, 400);
4037
+ return json13({ error: "S3 not configured" }, 400);
3977
4038
  const key = url.searchParams.get("key");
3978
4039
  if (!key)
3979
- return json12({ error: "key required" }, 400);
4040
+ return json13({ error: "key required" }, 400);
3980
4041
  try {
3981
4042
  const link = getS3Client().presign(key, { expiresIn: presignExpiresIn(), method: "GET" });
3982
- return json12({ url: link });
4043
+ return json13({ url: link });
3983
4044
  } catch (e) {
3984
- return json12({ error: clientError(e) }, 502);
4045
+ return json13({ error: clientError(e) }, 502);
3985
4046
  }
3986
4047
  }
3987
4048
  if (p === "/api/s3/delete" && req.method === "POST") {
3988
4049
  if (!isS3Configured())
3989
- return json12({ error: "S3 not configured" }, 400);
4050
+ return json13({ error: "S3 not configured" }, 400);
3990
4051
  const b = await readJson(req);
3991
4052
  if (!b?.key)
3992
- return json12({ error: "key required" }, 400);
4053
+ return json13({ error: "key required" }, 400);
3993
4054
  try {
3994
4055
  const client = getS3Client();
3995
4056
  if (!await client.exists(b.key))
3996
- return json12({ error: "not found" }, 404);
4057
+ return json13({ error: "not found" }, 404);
3997
4058
  await client.delete(b.key);
3998
- return json12({ ok: true, key: b.key });
4059
+ return json13({ ok: true, key: b.key });
3999
4060
  } catch (e) {
4000
- return json12({ error: clientError(e) }, 502);
4061
+ return json13({ error: clientError(e) }, 502);
4001
4062
  }
4002
4063
  }
4003
4064
  if (p === "/api/backups" && req.method === "GET") {
4004
- return json12({
4065
+ return json13({
4005
4066
  dir: backupDir(),
4006
4067
  devices: Object.keys(getConfig().devices),
4007
4068
  backups: listBackups()
@@ -4011,14 +4072,14 @@ async function featureRoutes(req, url) {
4011
4072
  const b = await readJson(req);
4012
4073
  const raw = b?.dir?.trim();
4013
4074
  if (!raw)
4014
- return json12({ error: "dir required" }, 400);
4075
+ return json13({ error: "dir required" }, 400);
4015
4076
  const dir = raw === "~" || raw.startsWith("~/") ? join4(homedir3(), raw.slice(1)) : raw;
4016
4077
  const next = { ...getConfig(), backupDir: dir };
4017
4078
  setConfig(next);
4018
4079
  try {
4019
4080
  atomicWrite(getConfigSource().path, serializeConfig(next));
4020
4081
  } catch (e) {
4021
- return json12({
4082
+ return json13({
4022
4083
  ok: true,
4023
4084
  dir: backupDir(),
4024
4085
  persisted: false,
@@ -4026,16 +4087,16 @@ async function featureRoutes(req, url) {
4026
4087
  });
4027
4088
  }
4028
4089
  recordVersion(getConfig(), "auto", Date.now(), "backup path changed");
4029
- return json12({ ok: true, dir: backupDir(), persisted: true });
4090
+ return json13({ ok: true, dir: backupDir(), persisted: true });
4030
4091
  }
4031
4092
  if (p === "/api/backups/get" && req.method === "GET") {
4032
4093
  const name = url.searchParams.get("name");
4033
4094
  if (!name)
4034
- return json12({ error: "name required" }, 400);
4095
+ return json13({ error: "name required" }, 400);
4035
4096
  try {
4036
- return json12({ name, content: readBackup(name) });
4097
+ return json13({ name, content: readBackup(name) });
4037
4098
  } catch {
4038
- return json12({ error: "not found" }, 404);
4099
+ return json13({ error: "not found" }, 404);
4039
4100
  }
4040
4101
  }
4041
4102
  if (p === "/api/backups/raw" && req.method === "GET") {
@@ -4056,41 +4117,41 @@ async function featureRoutes(req, url) {
4056
4117
  if (p === "/api/backups/upload" && req.method === "POST") {
4057
4118
  const b = await readJson(req);
4058
4119
  if (!b?.name || typeof b.content !== "string") {
4059
- return json12({ error: "name and content are required" }, 400);
4120
+ return json13({ error: "name and content are required" }, 400);
4060
4121
  }
4061
4122
  try {
4062
4123
  const safe = b.name.endsWith(".rsc") ? b.name : `${b.name}.rsc`;
4063
- return json12({ ok: true, name: writeBackup(safe, b.content) });
4124
+ return json13({ ok: true, name: writeBackup(safe, b.content) });
4064
4125
  } catch (e) {
4065
- return json12({ error: clientError(e) }, 400);
4126
+ return json13({ error: clientError(e) }, 400);
4066
4127
  }
4067
4128
  }
4068
4129
  if (p === "/api/backups/rename" && req.method === "POST") {
4069
4130
  const b = await readJson(req);
4070
4131
  if (!b?.name || !b?.new_name)
4071
- return json12({ error: "name and new_name are required" }, 400);
4132
+ return json13({ error: "name and new_name are required" }, 400);
4072
4133
  try {
4073
- return json12({ ok: true, name: renameBackup(b.name, b.new_name) });
4134
+ return json13({ ok: true, name: renameBackup(b.name, b.new_name) });
4074
4135
  } catch (e) {
4075
- return json12({ error: clientError(e) }, 400);
4136
+ return json13({ error: clientError(e) }, 400);
4076
4137
  }
4077
4138
  }
4078
4139
  if (p === "/api/backups/delete" && req.method === "POST") {
4079
4140
  const b = await readJson(req);
4080
4141
  if (!b?.name)
4081
- return json12({ error: "name required" }, 400);
4142
+ return json13({ error: "name required" }, 400);
4082
4143
  try {
4083
- return deleteBackup(b.name) ? json12({ ok: true }) : json12({ error: "not found" }, 404);
4144
+ return deleteBackup(b.name) ? json13({ ok: true }) : json13({ error: "not found" }, 404);
4084
4145
  } catch (e) {
4085
- return json12({ error: clientError(e) }, 400);
4146
+ return json13({ error: clientError(e) }, 400);
4086
4147
  }
4087
4148
  }
4088
4149
  if (p === "/api/backups/restore" && req.method === "POST") {
4089
4150
  const b = await readJson(req);
4090
4151
  if (!b?.name)
4091
- return json12({ error: "name required" }, 400);
4152
+ return json13({ error: "name required" }, 400);
4092
4153
  const device = resolveDeviceName(b.device);
4093
- return json12(await restoreLocalBackup(device, b.name, b.confirm === true));
4154
+ return json13(await restoreLocalBackup(device, b.name, b.confirm === true));
4094
4155
  }
4095
4156
  if (p === "/api/backups/create" && req.method === "POST") {
4096
4157
  const b = await readJson(req);
@@ -4102,7 +4163,7 @@ async function featureRoutes(req, url) {
4102
4163
  compact: b?.compact === true,
4103
4164
  terse: b?.terse === true
4104
4165
  });
4105
- return r.ok ? json12(r) : json12({ error: r.error ?? "export failed" }, 502);
4166
+ return r.ok ? json13(r) : json13({ error: r.error ?? "export failed" }, 502);
4106
4167
  }
4107
4168
  return null;
4108
4169
  }
@@ -4201,6 +4262,9 @@ async function runDashboard(cfg, transportLabel) {
4201
4262
  const driftResp = await driftRoutes(req, url);
4202
4263
  if (driftResp)
4203
4264
  return driftResp;
4265
+ const postureResp = await postureRoutes(req, url);
4266
+ if (postureResp)
4267
+ return postureResp;
4204
4268
  const txnResp = await txnRoutes(req, url);
4205
4269
  if (txnResp)
4206
4270
  return txnResp;
@@ -4233,7 +4297,7 @@ async function runDashboard(cfg, transportLabel) {
4233
4297
  return memoryResp;
4234
4298
  const db = getEventStore();
4235
4299
  if (!db)
4236
- return json12({ error: "recorder not active" }, 503);
4300
+ return json13({ error: "recorder not active" }, 503);
4237
4301
  if (url.pathname === "/api/events" && req.method === "DELETE") {
4238
4302
  let body = {};
4239
4303
  try {
@@ -4241,7 +4305,7 @@ async function runDashboard(cfg, transportLabel) {
4241
4305
  } catch {}
4242
4306
  const ids = Array.isArray(body.ids) ? body.ids.filter((x) => typeof x === "string") : [];
4243
4307
  const removed = body.all === true ? db.clear() : db.delete(ids);
4244
- return json12({ removed, total: db.total() });
4308
+ return json13({ removed, total: db.total() });
4245
4309
  }
4246
4310
  if (url.pathname === "/api/ssh-pool") {
4247
4311
  const cfg2 = getConfig();
@@ -4250,7 +4314,7 @@ async function runDashboard(cfg, transportLabel) {
4250
4314
  const totalInflight = ps.reduce((s, p) => s + p.inflight, 0);
4251
4315
  const totalIdle = ps.filter((p) => p.idle).length;
4252
4316
  const totalBusy = ps.filter((p) => p.inflight > 0).length;
4253
- return json12({
4317
+ return json13({
4254
4318
  enabled,
4255
4319
  config: {
4256
4320
  keepAlive: cfg2.ssh.keepAlive,
@@ -4279,39 +4343,39 @@ async function runDashboard(cfg, transportLabel) {
4279
4343
  });
4280
4344
  }
4281
4345
  if (url.pathname === "/api/devices") {
4282
- return json12(devicesPayload(db));
4346
+ return json13(devicesPayload(db));
4283
4347
  }
4284
4348
  if (url.pathname === "/api/capabilities") {
4285
4349
  const devices = Object.keys(getConfig().devices).map((name) => ({
4286
4350
  device: name,
4287
4351
  capabilities: serializeCapabilities(peekCapabilities(name))
4288
4352
  }));
4289
- return json12({ devices });
4353
+ return json13({ devices });
4290
4354
  }
4291
4355
  if (url.pathname === "/api/capabilities/refresh" && req.method === "POST") {
4292
4356
  const b = await readJson(req);
4293
4357
  if (typeof b?.device !== "string")
4294
- return json12({ error: "device (string) is required" }, 400);
4358
+ return json13({ error: "device (string) is required" }, 400);
4295
4359
  if (!(b.device in getConfig().devices)) {
4296
- return json12({ error: `unknown device: ${b.device}` }, 404);
4360
+ return json13({ error: `unknown device: ${b.device}` }, 404);
4297
4361
  }
4298
4362
  invalidateCapabilities(b.device);
4299
4363
  try {
4300
4364
  const caps = await getCapabilities(b.device);
4301
- return json12({ device: b.device, capabilities: serializeCapabilities(caps) });
4365
+ return json13({ device: b.device, capabilities: serializeCapabilities(caps) });
4302
4366
  } catch (e) {
4303
4367
  logger.error(`Capability probe failed for '${b.device}': ${logError(e)}`);
4304
- return json12({ error: clientError(e) }, 502);
4368
+ return json13({ error: clientError(e) }, 502);
4305
4369
  }
4306
4370
  }
4307
4371
  if (url.pathname === "/api/devices/toggle" && req.method === "POST") {
4308
4372
  const b = await readJson(req);
4309
4373
  if (typeof b?.device !== "string" || typeof b?.disabled !== "boolean") {
4310
- return json12({ error: "device (string) and disabled (boolean) are required" }, 400);
4374
+ return json13({ error: "device (string) and disabled (boolean) are required" }, 400);
4311
4375
  }
4312
4376
  const cfg2 = getConfig();
4313
4377
  if (!(b.device in cfg2.devices))
4314
- return json12({ error: `unknown device: ${b.device}` }, 404);
4378
+ return json13({ error: `unknown device: ${b.device}` }, 404);
4315
4379
  const dc = cfg2.devices[b.device];
4316
4380
  const next = {
4317
4381
  ...cfg2,
@@ -4332,7 +4396,7 @@ async function runDashboard(cfg, transportLabel) {
4332
4396
  if (persisted) {
4333
4397
  recordVersion(getConfig(), "auto", Date.now(), `device ${b.disabled ? "disabled" : "enabled"}: ${b.device}`);
4334
4398
  }
4335
- return json12({
4399
+ return json13({
4336
4400
  ok: true,
4337
4401
  persisted,
4338
4402
  requiresReconnect: true,
@@ -4345,17 +4409,17 @@ async function runDashboard(cfg, transportLabel) {
4345
4409
  const name = typeof b?.device === "string" ? b.device : "";
4346
4410
  const cfg2 = getConfig();
4347
4411
  if (!(name in cfg2.devices))
4348
- return json12({ error: `unknown device: ${name}` }, 404);
4412
+ return json13({ error: `unknown device: ${name}` }, 404);
4349
4413
  if (url.pathname === "/api/devices/reconnect")
4350
4414
  closeDevice(name);
4351
4415
  const status = await probeDevice(name, cfg2.devices[name]);
4352
- return json12({ ok: true, status, ...devicesPayload(db) });
4416
+ return json13({ ok: true, status, ...devicesPayload(db) });
4353
4417
  }
4354
4418
  if (url.pathname === "/api/topology") {
4355
- return json12(topologyPayload());
4419
+ return json13(topologyPayload());
4356
4420
  }
4357
4421
  if (url.pathname === "/api/config") {
4358
- return json12(configPayload());
4422
+ return json13(configPayload());
4359
4423
  }
4360
4424
  if (url.pathname === "/" || url.pathname === "/index.html") {
4361
4425
  return new Response(dashboardHtml(), {
@@ -4364,9 +4428,9 @@ async function runDashboard(cfg, transportLabel) {
4364
4428
  }
4365
4429
  if (url.pathname === "/api/releases/latest") {
4366
4430
  try {
4367
- return json12(await fetchLatestRelease());
4431
+ return json13(await fetchLatestRelease());
4368
4432
  } catch (e) {
4369
- return json12({ error: clientError(e, "fetch failed") }, 502);
4433
+ return json13({ error: clientError(e, "fetch failed") }, 502);
4370
4434
  }
4371
4435
  }
4372
4436
  if (url.pathname === "/api/catalog" && req.method === "GET") {
@@ -4385,7 +4449,7 @@ async function runDashboard(cfg, transportLabel) {
4385
4449
  }));
4386
4450
  const prompts = listPrompts();
4387
4451
  const groups = [...new Set(modules.map((m) => m.group))].sort();
4388
- return json12({
4452
+ return json13({
4389
4453
  modules,
4390
4454
  prompts,
4391
4455
  groups,
@@ -4400,25 +4464,25 @@ async function runDashboard(cfg, transportLabel) {
4400
4464
  }
4401
4465
  if (url.pathname === "/api/releases" && req.method === "GET") {
4402
4466
  try {
4403
- return json12(await fetchAllReleases());
4467
+ return json13(await fetchAllReleases());
4404
4468
  } catch (e) {
4405
- return json12({ error: clientError(e, "fetch failed") }, 502);
4469
+ return json13({ error: clientError(e, "fetch failed") }, 502);
4406
4470
  }
4407
4471
  }
4408
4472
  if (url.pathname === "/api/upgrade" && req.method === "POST") {
4409
4473
  const body = await readJson(req);
4410
4474
  const version = String(body?.version ?? "latest");
4411
4475
  if (version !== "latest" && !/^\d+\.\d+\.\d+$/.test(version)) {
4412
- return json12({ ok: false, error: `Invalid version "${version}" (use "latest" or x.y.z).` }, 400);
4476
+ return json13({ ok: false, error: `Invalid version "${version}" (use "latest" or x.y.z).` }, 400);
4413
4477
  }
4414
4478
  const spec = `@usex/mikrotik-mcp@${version}`;
4415
4479
  logger.warn(`Dashboard requested upgrade: bun i -g ${spec}`);
4416
4480
  const { ok, log } = await runUpgrade(spec);
4417
4481
  if (!ok)
4418
- return json12({ ok: false, version, log }, 500);
4482
+ return json13({ ok: false, version, log }, 500);
4419
4483
  const willRestart = body?.restart !== false;
4420
4484
  const relaunched = willRestart ? restartProcess() : false;
4421
- return json12({
4485
+ return json13({
4422
4486
  ok: true,
4423
4487
  version,
4424
4488
  log,
@@ -4428,7 +4492,7 @@ async function runDashboard(cfg, transportLabel) {
4428
4492
  }
4429
4493
  if (url.pathname === "/api/meta") {
4430
4494
  const f = facets(db);
4431
- return json12({
4495
+ return json13({
4432
4496
  ...f,
4433
4497
  version: VERSION,
4434
4498
  risks: ["READ", "WRITE", "WRITE_IDEMPOTENT", "DESTRUCTIVE", "DANGEROUS"],
@@ -4439,19 +4503,19 @@ async function runDashboard(cfg, transportLabel) {
4439
4503
  }
4440
4504
  if (url.pathname === "/api/events") {
4441
4505
  const filter = filterFromQuery(url);
4442
- return json12({ events: db.query(filter), total: db.total() });
4506
+ return json13({ events: db.query(filter), total: db.total() });
4443
4507
  }
4444
4508
  const eventMatch = url.pathname.match(/^\/api\/event\/(.+)$/);
4445
4509
  if (eventMatch) {
4446
4510
  const e = db.get(decodeURIComponent(eventMatch[1]));
4447
- return e ? json12(e) : json12({ error: "not found" }, 404);
4511
+ return e ? json13(e) : json13({ error: "not found" }, 404);
4448
4512
  }
4449
4513
  if (url.pathname === "/api/stats") {
4450
4514
  const now = Date.now();
4451
4515
  const windowMs2 = Number(url.searchParams.get("window") ?? 3600000);
4452
4516
  const buckets = Number(url.searchParams.get("buckets") ?? 60);
4453
4517
  const events = db.query({ since: now - windowMs2, limit: 5000 });
4454
- return json12(computeStats(events, { now, windowMs: windowMs2, buckets }));
4518
+ return json13(computeStats(events, { now, windowMs: windowMs2, buckets }));
4455
4519
  }
4456
4520
  return new Response("Not Found", { status: 404 });
4457
4521
  }
@@ -4470,7 +4534,7 @@ async function runDashboard(cfg, transportLabel) {
4470
4534
  return await dashboardRoute(req, url, srv, configAdmin, transportLabel);
4471
4535
  } catch (e) {
4472
4536
  logger.error(`Dashboard request failed (${url.pathname}): ${logError(e)}`);
4473
- return json12({ error: clientError(e) }, 502);
4537
+ return json13({ error: clientError(e) }, 502);
4474
4538
  }
4475
4539
  },
4476
4540
  websocket: {