@miragon/pulse-analytics-connector 0.0.0-bootstrap.0 → 0.21.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.
Files changed (357) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +86 -3
  3. package/dist/bpmn-name-lookup.d.ts +72 -0
  4. package/dist/bpmn-name-lookup.d.ts.map +1 -0
  5. package/dist/bpmn-name-lookup.js +123 -0
  6. package/dist/bpmn-name-lookup.js.map +1 -0
  7. package/dist/bpmn-name-lookup.test.d.ts +2 -0
  8. package/dist/bpmn-name-lookup.test.d.ts.map +1 -0
  9. package/dist/bpmn-name-lookup.test.js +287 -0
  10. package/dist/bpmn-name-lookup.test.js.map +1 -0
  11. package/dist/bpmn-names.d.ts +38 -0
  12. package/dist/bpmn-names.d.ts.map +1 -0
  13. package/dist/bpmn-names.js +22 -0
  14. package/dist/bpmn-names.js.map +1 -0
  15. package/dist/brand-gates.test.d.ts +2 -0
  16. package/dist/brand-gates.test.d.ts.map +1 -0
  17. package/dist/brand-gates.test.js +58 -0
  18. package/dist/brand-gates.test.js.map +1 -0
  19. package/dist/builder-texts.d.ts +4 -0
  20. package/dist/builder-texts.d.ts.map +1 -0
  21. package/dist/builder-texts.js +14 -0
  22. package/dist/builder-texts.js.map +1 -0
  23. package/dist/builder-texts.test.d.ts +2 -0
  24. package/dist/builder-texts.test.d.ts.map +1 -0
  25. package/dist/builder-texts.test.js +32 -0
  26. package/dist/builder-texts.test.js.map +1 -0
  27. package/dist/definition.d.ts +3 -0
  28. package/dist/definition.d.ts.map +1 -0
  29. package/dist/definition.js +151 -0
  30. package/dist/definition.js.map +1 -0
  31. package/dist/engine-ids.d.ts +42 -0
  32. package/dist/engine-ids.d.ts.map +1 -0
  33. package/dist/engine-ids.js +50 -0
  34. package/dist/engine-ids.js.map +1 -0
  35. package/dist/engine-scope.test.d.ts +2 -0
  36. package/dist/engine-scope.test.d.ts.map +1 -0
  37. package/dist/engine-scope.test.js +260 -0
  38. package/dist/engine-scope.test.js.map +1 -0
  39. package/dist/heatmap-data.d.ts +15 -0
  40. package/dist/heatmap-data.d.ts.map +1 -0
  41. package/dist/heatmap-data.js +2 -0
  42. package/dist/heatmap-data.js.map +1 -0
  43. package/dist/index.d.ts +5 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +3 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/instructions.d.ts +8 -0
  48. package/dist/instructions.d.ts.map +1 -0
  49. package/dist/instructions.js +33 -0
  50. package/dist/instructions.js.map +1 -0
  51. package/dist/messages/de.ask-ai.d.ts +9 -0
  52. package/dist/messages/de.ask-ai.d.ts.map +1 -0
  53. package/dist/messages/de.ask-ai.js +18 -0
  54. package/dist/messages/de.ask-ai.js.map +1 -0
  55. package/dist/messages/de.builder.d.ts +12 -0
  56. package/dist/messages/de.builder.d.ts.map +1 -0
  57. package/dist/messages/de.builder.js +53 -0
  58. package/dist/messages/de.builder.js.map +1 -0
  59. package/dist/messages/de.server.d.ts +8 -0
  60. package/dist/messages/de.server.d.ts.map +1 -0
  61. package/dist/messages/de.server.js +26 -0
  62. package/dist/messages/de.server.js.map +1 -0
  63. package/dist/messages/de.sweep.d.ts +10 -0
  64. package/dist/messages/de.sweep.d.ts.map +1 -0
  65. package/dist/messages/de.sweep.js +203 -0
  66. package/dist/messages/de.sweep.js.map +1 -0
  67. package/dist/messages/en.ask-ai.d.ts +25 -0
  68. package/dist/messages/en.ask-ai.d.ts.map +1 -0
  69. package/dist/messages/en.ask-ai.js +24 -0
  70. package/dist/messages/en.ask-ai.js.map +1 -0
  71. package/dist/messages/en.builder.d.ts +12 -0
  72. package/dist/messages/en.builder.d.ts.map +1 -0
  73. package/dist/messages/en.builder.js +53 -0
  74. package/dist/messages/en.builder.js.map +1 -0
  75. package/dist/messages/en.server.d.ts +8 -0
  76. package/dist/messages/en.server.d.ts.map +1 -0
  77. package/dist/messages/en.server.js +26 -0
  78. package/dist/messages/en.server.js.map +1 -0
  79. package/dist/messages/en.sweep.d.ts +11 -0
  80. package/dist/messages/en.sweep.d.ts.map +1 -0
  81. package/dist/messages/en.sweep.js +204 -0
  82. package/dist/messages/en.sweep.js.map +1 -0
  83. package/dist/messages/glossary.d.ts +26 -0
  84. package/dist/messages/glossary.d.ts.map +1 -0
  85. package/dist/messages/glossary.js +37 -0
  86. package/dist/messages/glossary.js.map +1 -0
  87. package/dist/messages/glossary.test.d.ts +2 -0
  88. package/dist/messages/glossary.test.d.ts.map +1 -0
  89. package/dist/messages/glossary.test.js +109 -0
  90. package/dist/messages/glossary.test.js.map +1 -0
  91. package/dist/messages/index.d.ts +12 -0
  92. package/dist/messages/index.d.ts.map +1 -0
  93. package/dist/messages/index.js +23 -0
  94. package/dist/messages/index.js.map +1 -0
  95. package/dist/messages/plural.d.ts +10 -0
  96. package/dist/messages/plural.d.ts.map +1 -0
  97. package/dist/messages/plural.js +17 -0
  98. package/dist/messages/plural.js.map +1 -0
  99. package/dist/messages/use-t.d.ts +10 -0
  100. package/dist/messages/use-t.d.ts.map +1 -0
  101. package/dist/messages/use-t.js +10 -0
  102. package/dist/messages/use-t.js.map +1 -0
  103. package/dist/module.d.ts +48 -0
  104. package/dist/module.d.ts.map +1 -0
  105. package/dist/module.js +174 -0
  106. package/dist/module.js.map +1 -0
  107. package/dist/module.test.d.ts +2 -0
  108. package/dist/module.test.d.ts.map +1 -0
  109. package/dist/module.test.js +230 -0
  110. package/dist/module.test.js.map +1 -0
  111. package/dist/named-views.d.ts +27 -0
  112. package/dist/named-views.d.ts.map +1 -0
  113. package/dist/named-views.js +73 -0
  114. package/dist/named-views.js.map +1 -0
  115. package/dist/named-views.test.d.ts +2 -0
  116. package/dist/named-views.test.d.ts.map +1 -0
  117. package/dist/named-views.test.js +110 -0
  118. package/dist/named-views.test.js.map +1 -0
  119. package/dist/plugin.d.ts +44 -0
  120. package/dist/plugin.d.ts.map +1 -0
  121. package/dist/plugin.js +52 -0
  122. package/dist/plugin.js.map +1 -0
  123. package/dist/prometheus-wiring.test.d.ts +2 -0
  124. package/dist/prometheus-wiring.test.d.ts.map +1 -0
  125. package/dist/prometheus-wiring.test.js +142 -0
  126. package/dist/prometheus-wiring.test.js.map +1 -0
  127. package/dist/server-locale.d.ts +48 -0
  128. package/dist/server-locale.d.ts.map +1 -0
  129. package/dist/server-locale.js +33 -0
  130. package/dist/server-locale.js.map +1 -0
  131. package/dist/settings-tools.d.ts +23 -0
  132. package/dist/settings-tools.d.ts.map +1 -0
  133. package/dist/settings-tools.js +100 -0
  134. package/dist/settings-tools.js.map +1 -0
  135. package/dist/settings.d.ts +77 -0
  136. package/dist/settings.d.ts.map +1 -0
  137. package/dist/settings.js +99 -0
  138. package/dist/settings.js.map +1 -0
  139. package/dist/settings.test.d.ts +2 -0
  140. package/dist/settings.test.d.ts.map +1 -0
  141. package/dist/settings.test.js +299 -0
  142. package/dist/settings.test.js.map +1 -0
  143. package/dist/steps/app-config.d.ts +30 -0
  144. package/dist/steps/app-config.d.ts.map +1 -0
  145. package/dist/steps/app-config.js +46 -0
  146. package/dist/steps/app-config.js.map +1 -0
  147. package/dist/steps/dashboard.d.ts +12 -0
  148. package/dist/steps/dashboard.d.ts.map +1 -0
  149. package/dist/steps/dashboard.js +58 -0
  150. package/dist/steps/dashboard.js.map +1 -0
  151. package/dist/steps/failure-dashboard.d.ts +10 -0
  152. package/dist/steps/failure-dashboard.d.ts.map +1 -0
  153. package/dist/steps/failure-dashboard.js +32 -0
  154. package/dist/steps/failure-dashboard.js.map +1 -0
  155. package/dist/steps/index.d.ts +3 -0
  156. package/dist/steps/index.d.ts.map +1 -0
  157. package/dist/steps/index.js +3 -0
  158. package/dist/steps/index.js.map +1 -0
  159. package/dist/steps/steps.test.d.ts +2 -0
  160. package/dist/steps/steps.test.d.ts.map +1 -0
  161. package/dist/steps/steps.test.js +153 -0
  162. package/dist/steps/steps.test.js.map +1 -0
  163. package/dist/tool-names.d.ts +13 -0
  164. package/dist/tool-names.d.ts.map +1 -0
  165. package/dist/tool-names.js +27 -0
  166. package/dist/tool-names.js.map +1 -0
  167. package/dist/tools/cluster-compare.d.ts +8 -0
  168. package/dist/tools/cluster-compare.d.ts.map +1 -0
  169. package/dist/tools/cluster-compare.js +17 -0
  170. package/dist/tools/cluster-compare.js.map +1 -0
  171. package/dist/tools/element.d.ts +8 -0
  172. package/dist/tools/element.d.ts.map +1 -0
  173. package/dist/tools/element.js +25 -0
  174. package/dist/tools/element.js.map +1 -0
  175. package/dist/tools/engine-compare.d.ts +8 -0
  176. package/dist/tools/engine-compare.d.ts.map +1 -0
  177. package/dist/tools/engine-compare.js +18 -0
  178. package/dist/tools/engine-compare.js.map +1 -0
  179. package/dist/tools/engine-compare.test.d.ts +2 -0
  180. package/dist/tools/engine-compare.test.d.ts.map +1 -0
  181. package/dist/tools/engine-compare.test.js +166 -0
  182. package/dist/tools/engine-compare.test.js.map +1 -0
  183. package/dist/tools/engine-landscape.d.ts +7 -0
  184. package/dist/tools/engine-landscape.d.ts.map +1 -0
  185. package/dist/tools/engine-landscape.js +13 -0
  186. package/dist/tools/engine-landscape.js.map +1 -0
  187. package/dist/tools/engine-landscape.test.d.ts +2 -0
  188. package/dist/tools/engine-landscape.test.d.ts.map +1 -0
  189. package/dist/tools/engine-landscape.test.js +110 -0
  190. package/dist/tools/engine-landscape.test.js.map +1 -0
  191. package/dist/tools/failures.d.ts +7 -0
  192. package/dist/tools/failures.d.ts.map +1 -0
  193. package/dist/tools/failures.js +13 -0
  194. package/dist/tools/failures.js.map +1 -0
  195. package/dist/tools/health.d.ts +7 -0
  196. package/dist/tools/health.d.ts.map +1 -0
  197. package/dist/tools/health.js +23 -0
  198. package/dist/tools/health.js.map +1 -0
  199. package/dist/tools/health.test.d.ts +2 -0
  200. package/dist/tools/health.test.d.ts.map +1 -0
  201. package/dist/tools/health.test.js +119 -0
  202. package/dist/tools/health.test.js.map +1 -0
  203. package/dist/tools/index.d.ts +18 -0
  204. package/dist/tools/index.d.ts.map +1 -0
  205. package/dist/tools/index.js +35 -0
  206. package/dist/tools/index.js.map +1 -0
  207. package/dist/tools/performance.d.ts +8 -0
  208. package/dist/tools/performance.d.ts.map +1 -0
  209. package/dist/tools/performance.js +25 -0
  210. package/dist/tools/performance.js.map +1 -0
  211. package/dist/tools/performance.test.d.ts +2 -0
  212. package/dist/tools/performance.test.d.ts.map +1 -0
  213. package/dist/tools/performance.test.js +176 -0
  214. package/dist/tools/performance.test.js.map +1 -0
  215. package/dist/tools/version-compare.d.ts +8 -0
  216. package/dist/tools/version-compare.d.ts.map +1 -0
  217. package/dist/tools/version-compare.js +17 -0
  218. package/dist/tools/version-compare.js.map +1 -0
  219. package/dist/tools/version-compare.test.d.ts +2 -0
  220. package/dist/tools/version-compare.test.d.ts.map +1 -0
  221. package/dist/tools/version-compare.test.js +82 -0
  222. package/dist/tools/version-compare.test.js.map +1 -0
  223. package/dist/toolsets.d.ts +17 -0
  224. package/dist/toolsets.d.ts.map +1 -0
  225. package/dist/toolsets.js +47 -0
  226. package/dist/toolsets.js.map +1 -0
  227. package/dist/toolsets.test.d.ts +2 -0
  228. package/dist/toolsets.test.d.ts.map +1 -0
  229. package/dist/toolsets.test.js +91 -0
  230. package/dist/toolsets.test.js.map +1 -0
  231. package/dist/view-title.test.d.ts +2 -0
  232. package/dist/view-title.test.d.ts.map +1 -0
  233. package/dist/view-title.test.js +91 -0
  234. package/dist/view-title.test.js.map +1 -0
  235. package/dist/widget-tools/comparisons.d.ts +13 -0
  236. package/dist/widget-tools/comparisons.d.ts.map +1 -0
  237. package/dist/widget-tools/comparisons.js +177 -0
  238. package/dist/widget-tools/comparisons.js.map +1 -0
  239. package/dist/widget-tools/comparisons.test.d.ts +2 -0
  240. package/dist/widget-tools/comparisons.test.d.ts.map +1 -0
  241. package/dist/widget-tools/comparisons.test.js +123 -0
  242. package/dist/widget-tools/comparisons.test.js.map +1 -0
  243. package/dist/widget-tools/shared.d.ts +36 -0
  244. package/dist/widget-tools/shared.d.ts.map +1 -0
  245. package/dist/widget-tools/shared.js +39 -0
  246. package/dist/widget-tools/shared.js.map +1 -0
  247. package/dist/widget-tools.d.ts +25 -0
  248. package/dist/widget-tools.d.ts.map +1 -0
  249. package/dist/widget-tools.js +269 -0
  250. package/dist/widget-tools.js.map +1 -0
  251. package/dist/widget-tools.test.d.ts +2 -0
  252. package/dist/widget-tools.test.d.ts.map +1 -0
  253. package/dist/widget-tools.test.js +272 -0
  254. package/dist/widget-tools.test.js.map +1 -0
  255. package/package.json +91 -5
  256. package/src/bpmn-name-lookup.test.ts +333 -0
  257. package/src/bpmn-name-lookup.ts +197 -0
  258. package/src/bpmn-names.ts +51 -0
  259. package/src/brand-gates.test.ts +69 -0
  260. package/src/builder-texts.test.ts +40 -0
  261. package/src/builder-texts.ts +15 -0
  262. package/src/definition.ts +181 -0
  263. package/src/engine-ids.ts +85 -0
  264. package/src/engine-scope.test.ts +324 -0
  265. package/src/heatmap-data.ts +14 -0
  266. package/src/index.ts +4 -0
  267. package/src/instructions.ts +33 -0
  268. package/src/messages/GLOSSARY.md +88 -0
  269. package/src/messages/de.ask-ai.ts +28 -0
  270. package/src/messages/de.builder.ts +77 -0
  271. package/src/messages/de.server.ts +39 -0
  272. package/src/messages/de.sweep.ts +244 -0
  273. package/src/messages/en.ask-ai.ts +34 -0
  274. package/src/messages/en.builder.ts +76 -0
  275. package/src/messages/en.server.ts +39 -0
  276. package/src/messages/en.sweep.ts +240 -0
  277. package/src/messages/glossary.test.ts +122 -0
  278. package/src/messages/glossary.ts +62 -0
  279. package/src/messages/index.ts +24 -0
  280. package/src/messages/plural.ts +18 -0
  281. package/src/messages/use-t.ts +12 -0
  282. package/src/module.test.ts +319 -0
  283. package/src/module.ts +211 -0
  284. package/src/named-views.test.ts +141 -0
  285. package/src/named-views.ts +97 -0
  286. package/src/plugin.ts +94 -0
  287. package/src/prometheus-wiring.test.ts +174 -0
  288. package/src/server-locale.ts +59 -0
  289. package/src/settings-tools.ts +170 -0
  290. package/src/settings.test.ts +402 -0
  291. package/src/settings.ts +114 -0
  292. package/src/steps/app-config.ts +75 -0
  293. package/src/steps/dashboard.ts +73 -0
  294. package/src/steps/failure-dashboard.ts +38 -0
  295. package/src/steps/index.ts +2 -0
  296. package/src/steps/steps.test.ts +216 -0
  297. package/src/tool-names.ts +26 -0
  298. package/src/tools/cluster-compare.ts +28 -0
  299. package/src/tools/element.ts +35 -0
  300. package/src/tools/engine-compare.test.ts +210 -0
  301. package/src/tools/engine-compare.ts +30 -0
  302. package/src/tools/engine-landscape.test.ts +138 -0
  303. package/src/tools/engine-landscape.ts +21 -0
  304. package/src/tools/failures.ts +19 -0
  305. package/src/tools/health.test.ts +156 -0
  306. package/src/tools/health.ts +29 -0
  307. package/src/tools/index.ts +44 -0
  308. package/src/tools/performance.test.ts +210 -0
  309. package/src/tools/performance.ts +38 -0
  310. package/src/tools/version-compare.test.ts +97 -0
  311. package/src/tools/version-compare.ts +28 -0
  312. package/src/toolsets.test.ts +107 -0
  313. package/src/toolsets.ts +56 -0
  314. package/src/view-title.test.ts +123 -0
  315. package/src/widget-tools/comparisons.test.ts +170 -0
  316. package/src/widget-tools/comparisons.ts +229 -0
  317. package/src/widget-tools/shared.ts +67 -0
  318. package/src/widget-tools.test.ts +306 -0
  319. package/src/widget-tools.ts +375 -0
  320. package/src/widgets/analytics-dashboard/activity-bottleneck-table.tsx +179 -0
  321. package/src/widgets/analytics-dashboard/definition-breakdown.tsx +108 -0
  322. package/src/widgets/analytics-dashboard/execution-performance-kpi.tsx +94 -0
  323. package/src/widgets/analytics-dashboard/execution-summary-kpi.tsx +122 -0
  324. package/src/widgets/analytics-dashboard/lib.ts +43 -0
  325. package/src/widgets/bpmn-heatmap.test.tsx +75 -0
  326. package/src/widgets/bpmn-heatmap.tsx +127 -0
  327. package/src/widgets/catalogue-sync.test.ts +16 -0
  328. package/src/widgets/cluster-compare.tsx +148 -0
  329. package/src/widgets/comparison-shared.test.ts +237 -0
  330. package/src/widgets/comparison-shared.tsx +371 -0
  331. package/src/widgets/comparison-widgets.test.tsx +307 -0
  332. package/src/widgets/dashboard-widgets.test.tsx +397 -0
  333. package/src/widgets/engine-compare.tsx +87 -0
  334. package/src/widgets/engine-landscape.tsx +352 -0
  335. package/src/widgets/failure-dashboard/error-patterns-table.tsx +122 -0
  336. package/src/widgets/failure-dashboard/failure-rate-table.test.ts +80 -0
  337. package/src/widgets/failure-dashboard/failure-rate-table.tsx +170 -0
  338. package/src/widgets/failure-dashboard/lib.ts +32 -0
  339. package/src/widgets/failure-dashboard/summary-kpi.tsx +122 -0
  340. package/src/widgets/failure-landscape-widgets.test.tsx +393 -0
  341. package/src/widgets/figure.tsx +79 -0
  342. package/src/widgets/hand-off.test.ts +126 -0
  343. package/src/widgets/hand-off.ts +102 -0
  344. package/src/widgets/index.ts +124 -0
  345. package/src/widgets/model-descriptions.dashboard.test.ts +280 -0
  346. package/src/widgets/model-descriptions.test.ts +251 -0
  347. package/src/widgets/model-descriptions.ts +428 -0
  348. package/src/widgets/names.test.ts +32 -0
  349. package/src/widgets/names.ts +39 -0
  350. package/src/widgets/query-gate.test.tsx +57 -0
  351. package/src/widgets/query-gate.tsx +50 -0
  352. package/src/widgets/settings-section.test.tsx +174 -0
  353. package/src/widgets/settings-section.tsx +244 -0
  354. package/src/widgets/version-compare.test.ts +68 -0
  355. package/src/widgets/version-compare.tsx +96 -0
  356. package/src/widgets/view-meta.ts +18 -0
  357. package/src/widgets/widget-render.test-support.tsx +103 -0
@@ -0,0 +1,174 @@
1
+ import http from "node:http"
2
+ import type { AddressInfo } from "node:net"
3
+ import { afterAll, beforeAll, describe, expect, it } from "vitest"
4
+ import type { MCPServer } from "mcp-use"
5
+ import { analyticsModule } from "./module.js"
6
+ import { ANALYTICS_ENGINE_LANDSCAPE_DATA } from "./tool-names.js"
7
+
8
+ /**
9
+ * #325 through the module: env → config → plugin → Prometheus client → tool.
10
+ * A real local HTTP server plays Prometheus, so the auth headers, the
11
+ * deadline and the caller's cancellation are observed on the wire.
12
+ */
13
+
14
+ type ToolCallback = (
15
+ args: Record<string, unknown>,
16
+ ctx?: unknown,
17
+ ) => Promise<{ isError?: boolean; content: Array<{ type: string; text: string }> }>
18
+
19
+ const seenAuth: Array<string | undefined> = []
20
+ const seenTenant: Array<string | string[] | undefined> = []
21
+ let hang = false
22
+
23
+ const prometheus = http.createServer((req, res) => {
24
+ seenAuth.push(req.headers.authorization)
25
+ seenTenant.push(req.headers["x-scope-orgid"])
26
+ if (hang) return
27
+ res.writeHead(200, { "Content-Type": "application/json" })
28
+ res.end(JSON.stringify({ status: "success", data: { resultType: "vector", result: [] } }))
29
+ })
30
+
31
+ let url = ""
32
+
33
+ beforeAll(async () => {
34
+ await new Promise<void>((resolve) => prometheus.listen(0, "127.0.0.1", resolve))
35
+ url = `http://127.0.0.1:${(prometheus.address() as AddressInfo).port}`
36
+ })
37
+
38
+ afterAll(async () => {
39
+ prometheus.closeAllConnections()
40
+ await new Promise<void>((resolve) => prometheus.close(() => resolve()))
41
+ })
42
+
43
+ /** A standalone analytics boot names the engines it may read (`engine-ids.ts`). */
44
+ const ENGINES = { ANALYTICS_ENGINE_IDS: "prod-a,prod-b" }
45
+
46
+ /** Boots the module from env the way the composition root does; returns its tools. */
47
+ function boot(env: NodeJS.ProcessEnv) {
48
+ const tools = new Map<string, ToolCallback>()
49
+ const server = {
50
+ tool: (definition: { name: string }, callback: ToolCallback) => {
51
+ tools.set(definition.name, callback)
52
+ },
53
+ use: () => {},
54
+ } as unknown as MCPServer
55
+ const plugin = analyticsModule.createPlugin(
56
+ {
57
+ ...analyticsModule.configFromEnv({ PROMETHEUS_URL: url, ...ENGINES, ...env }),
58
+ toolset: "read-only",
59
+ },
60
+ {},
61
+ )
62
+ plugin.registerTools?.(server)
63
+ plugin.registerWidgetTools?.(server)
64
+ return tools
65
+ }
66
+
67
+ describe("Prometheus config reaches the wire", () => {
68
+ it("PROMETHEUS_BEARER_TOKEN authenticates every query of a registrar tool", async () => {
69
+ hang = false
70
+ seenAuth.length = 0
71
+ const tools = boot({ PROMETHEUS_BEARER_TOKEN: "tok-123" })
72
+ const result = await tools.get("analytics_engine_landscape")!({})
73
+ expect(result.isError).toBeFalsy()
74
+ expect(seenAuth.length).toBeGreaterThan(0)
75
+ expect(seenAuth.every((auth) => auth === "Bearer tok-123")).toBe(true)
76
+ })
77
+
78
+ it("PROMETHEUS_HEADERS and PROMETHEUS_USERNAME/PROMETHEUS_PASSWORD reach every query", async () => {
79
+ hang = false
80
+ seenAuth.length = 0
81
+ seenTenant.length = 0
82
+ const tools = boot({
83
+ PROMETHEUS_HEADERS: '{"X-Scope-OrgID":"tenant-a"}',
84
+ PROMETHEUS_USERNAME: "grafana",
85
+ PROMETHEUS_PASSWORD: "hunter2-pass",
86
+ })
87
+ const result = await tools.get("analytics_engine_landscape")!({})
88
+ expect(result.isError).toBeFalsy()
89
+ expect(seenTenant.length).toBeGreaterThan(0)
90
+ expect(seenTenant.every((tenant) => tenant === "tenant-a")).toBe(true)
91
+ const basic = `Basic ${Buffer.from("grafana:hunter2-pass").toString("base64")}`
92
+ expect(seenAuth.every((auth) => auth === basic)).toBe(true)
93
+ })
94
+
95
+ it("PROMETHEUS_TIMEOUT_MS bounds a hung Prometheus", async () => {
96
+ hang = true
97
+ const tools = boot({ PROMETHEUS_TIMEOUT_MS: "60" })
98
+ const result = await tools.get("analytics_engine_landscape")!({})
99
+ expect(result.isError).toBe(true)
100
+ expect(result.content[0].text).toBe("Prometheus did not respond within 60 ms (timeout)")
101
+ })
102
+
103
+ it("EVERY widget tool and feed hands ctx.signal to its queries", async () => {
104
+ hang = false
105
+ const tools = new Map<string, ToolCallback>()
106
+ const server = {
107
+ tool: (definition: { name: string }, callback: ToolCallback) => {
108
+ tools.set(definition.name, callback)
109
+ },
110
+ } as unknown as MCPServer
111
+ analyticsModule
112
+ .createPlugin(
113
+ {
114
+ ...analyticsModule.configFromEnv({ PROMETHEUS_URL: url, ...ENGINES }),
115
+ toolset: "read-only",
116
+ },
117
+ {},
118
+ )
119
+ .registerWidgetTools?.(server)
120
+ // Each handler runs twice with args every tool accepts: unsignalled it
121
+ // must reach Prometheus, with an already-aborted ctx.signal it must not —
122
+ // a handler that drops ctx would still query. Tools that never query are
123
+ // named.
124
+ const args = {
125
+ processDefinitionKey: "order",
126
+ // Handlers called directly get no schema defaults: the compare windows.
127
+ windowDays: 7,
128
+ windowBeforeDays: 3,
129
+ windowAfterDays: 3,
130
+ // Relative to now: compare windows are held inside the retention.
131
+ deploymentTimestamp: new Date(Date.now() - 3 * 86_400_000).toISOString(),
132
+ engineA: "prod-a",
133
+ engineB: "prod-b",
134
+ }
135
+ const queriesOf = async (name: string, ctx: { signal?: AbortSignal }) => {
136
+ const before = seenAuth.length
137
+ await tools.get(name)!(args, ctx)
138
+ return seenAuth.length - before
139
+ }
140
+ const neverQuery: string[] = []
141
+ for (const name of tools.keys()) {
142
+ if ((await queriesOf(name, {})) === 0) {
143
+ neverQuery.push(name)
144
+ continue
145
+ }
146
+ expect({ name, queries: await queriesOf(name, { signal: AbortSignal.abort() }) }).toEqual({
147
+ name,
148
+ queries: 0,
149
+ })
150
+ }
151
+ // The settings and the heatmap's diagram (read from the engine) never ask Prometheus.
152
+ expect(neverQuery.sort()).toEqual([
153
+ "analytics_bpmn_xml_data",
154
+ "analytics_settings_data",
155
+ "analytics_show_settings",
156
+ ])
157
+ expect(tools.size - neverQuery.length).toBeGreaterThanOrEqual(11)
158
+ })
159
+
160
+ it("a widget feed's ctx.signal cancels its queries before the deadline", async () => {
161
+ hang = true
162
+ const tools = boot({ PROMETHEUS_TIMEOUT_MS: "10000" })
163
+ const controller = new AbortController()
164
+ setTimeout(() => controller.abort(), 30)
165
+ const started = Date.now()
166
+ const result = await tools.get(ANALYTICS_ENGINE_LANDSCAPE_DATA)!(
167
+ {},
168
+ { signal: controller.signal },
169
+ )
170
+ expect(Date.now() - started).toBeLessThan(5_000)
171
+ expect(result.isError).toBe(true)
172
+ expect(result.content[0].text).toBe("Prometheus query cancelled by the caller")
173
+ })
174
+ })
@@ -0,0 +1,59 @@
1
+ import {
2
+ createLocalizeFor,
3
+ createLocalizeViewFor,
4
+ resolveAuthUserId,
5
+ resolveProfileKey,
6
+ } from "@miragon/pulse-widget-shell/server"
7
+ import { translator } from "./messages/index.js"
8
+
9
+ /**
10
+ * The profile port + key resolution come from the shared server kit, NOT from a
11
+ * module-local copy: every module reads and writes the same profile record, so
12
+ * a divergent key precedence (or a differently-spelled anonymous fallback)
13
+ * would silently split a user's settings across two records. Re-exported under
14
+ * this module's names so the call sites read in analytics vocabulary.
15
+ *
16
+ * `ProfileSource` is the minimal read/write view a module needs — the locale
17
+ * plus its own slice under `modules.analytics`. camunda7's `ProfileStore`
18
+ * satisfies it structurally, without analytics depending on the camunda7
19
+ * module; `save` merges per module key on the store side, so writing
20
+ * `modules.analytics` never touches other modules' slices.
21
+ */
22
+ export type { ProfileSlice, ProfileSource } from "@miragon/pulse-widget-shell/server"
23
+
24
+ /**
25
+ * Resolve the profile key for the in-flight request: the OAuth caller (off the
26
+ * tool-handler `ctx`), else `anonymous` for an explicitly declared local
27
+ * caller (stdio, tests). A request without OAuth resolves `undefined` — reads
28
+ * fall back to defaults, saves fail visibly, so unrelated callers never
29
+ * cross-share one record.
30
+ */
31
+ export const resolveSettingsKey = resolveProfileKey
32
+
33
+ /**
34
+ * Just the OAuth half of {@link resolveSettingsKey} — the save path stamps it
35
+ * onto the record as its owner (`opts.userId`).
36
+ */
37
+ export const resolveSettingsAuthUserId = resolveAuthUserId
38
+
39
+ /** A locale-bound translate for summaries; a show tool's summary translate + view title. */
40
+ export type { ServerT, ViewLocale } from "@miragon/pulse-widget-shell/server"
41
+
42
+ /**
43
+ * Resolve the request locale and return a translate bound to it + the analytics
44
+ * catalogs — `const t = await localizeFor(store, ctx); … summary: t("key", { … })`.
45
+ * Pass the tool-handler `ctx` so an auth user id resolves the same record the
46
+ * save path writes. Locale resolution + the fail-soft rules (no store, no key,
47
+ * store OUTAGE → English) come from the shared `createLocalizeFor`.
48
+ */
49
+ export const localizeFor = createLocalizeFor(translator)
50
+
51
+ /**
52
+ * {@link localizeFor} plus the view title, from ONE profile read — the kit's
53
+ * one title rule for every module's show tools: a view is titled only in a
54
+ * language the profile names; with "system" (the default), no caller or a
55
+ * store outage it carries no title, and the widget's own heading names it in
56
+ * the host's language (#322 U3). Summaries stay `t`:
57
+ * `const { t, title } = await localizeViewFor(store, ctx); … title: title("key")`.
58
+ */
59
+ export const localizeViewFor = createLocalizeViewFor(translator)
@@ -0,0 +1,170 @@
1
+ import type { MCPServer } from "mcp-use"
2
+ import {
3
+ appOnly,
4
+ buildDataFeedResult,
5
+ buildSingleWidgetView,
6
+ requireProfileKey,
7
+ saveModuleSlice,
8
+ showToolBinding,
9
+ withToolErrors,
10
+ strictToolInput,
11
+ } from "@miragon/pulse-widget-shell/server"
12
+ import { ANALYTICS_SAVE_SETTINGS, ANALYTICS_SETTINGS_DATA } from "./tool-names.js"
13
+ import {
14
+ localizeFor,
15
+ localizeViewFor,
16
+ resolveSettingsAuthUserId,
17
+ resolveSettingsKey,
18
+ type ProfileSource,
19
+ type ServerT,
20
+ } from "./server-locale.js"
21
+ import {
22
+ ANALYTICS_MODULE_KEY,
23
+ analyticsSettingsSaveInput,
24
+ parseAnalyticsSettings,
25
+ settingsFor,
26
+ type AnalyticsSettings,
27
+ } from "./settings.js"
28
+ import { allowsDurableWrites, type AnalyticsToolset } from "./toolsets.js"
29
+
30
+ /**
31
+ * The analytics module's settings section — its own show/data/save tool triple
32
+ * (the three render paths), so the module plugs into the settings page without
33
+ * camunda7 owning any analytics vocabulary. The slice persists under
34
+ * `modules.analytics` in the shared profile record; the shared store merges
35
+ * per module key, so this never touches other modules' slices.
36
+ */
37
+
38
+ /** What the settings widget renders: the effective slice + whether saving is wired. */
39
+ export interface AnalyticsSettingsView {
40
+ settings: AnalyticsSettings
41
+ /**
42
+ * False without a writable store, in the `read-only` toolset (also what a
43
+ * missing toolset resolves to), or when the request carries no caller
44
+ * identity (no OAuth — see `resolveProfileKey`) — the widget hides Save.
45
+ */
46
+ canSave: boolean
47
+ }
48
+
49
+ export function registerSettingsTools(
50
+ server: MCPServer,
51
+ profileStore?: ProfileSource,
52
+ toolset?: AnalyticsToolset,
53
+ ): void {
54
+ // The save tool is a durable write registered OUTSIDE the tool registrar, so
55
+ // it gates itself against the module's toolset (see `allowsDurableWrites`):
56
+ // only `standard` saves. A missing toolset resolves to the `read-only`
57
+ // floor, and an unknown name from an untyped caller warns and degrades to it.
58
+ const store = profileStore
59
+ const save = allowsDurableWrites(toolset) ? store?.save?.bind(store) : undefined
60
+
61
+ const loadView = async (ctx: unknown): Promise<AnalyticsSettingsView> => ({
62
+ settings: await settingsFor(store, ctx),
63
+ // Store/toolset gate AND per-request identity: without a profile key the
64
+ // save tool refuses, so the section must render read-only instead of a
65
+ // Save button whose click errors.
66
+ canSave: Boolean(save) && resolveSettingsKey(ctx) !== undefined,
67
+ })
68
+
69
+ const summarize = (t: ServerT, view: AnalyticsSettingsView): string =>
70
+ t("aSum.settings", {
71
+ period: view.settings.defaultPeriod,
72
+ minBucketSize: view.settings.minBucketSize,
73
+ changeHint: view.canSave ? t("aSum.settingsChangeHint") : "",
74
+ })
75
+
76
+ server.tool(
77
+ {
78
+ name: "analytics_show_settings",
79
+ title: "Analytics Settings",
80
+ description:
81
+ "Open the analytics settings section: the default look-back period and the minimum comparison bucket size applied when analytics calls omit them.",
82
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
83
+ inputSchema: strictToolInput({}),
84
+ ...showToolBinding("analytics_show_settings", "Analytics Settings"),
85
+ },
86
+ withToolErrors(async (_params, ctx) => {
87
+ const view = await loadView(ctx)
88
+ const { t, title } = await localizeViewFor(store, ctx)
89
+ return buildSingleWidgetView({
90
+ widget: "analytics:settings",
91
+ app: "analytics",
92
+ dataType: "analytics:settings",
93
+ data: { ...view },
94
+ // The view title in the language the profile names (the section
95
+ // heading's own key); none with "system", where the heading names it.
96
+ title: title("aSettings.heading"),
97
+ summary: summarize(t, view),
98
+ })
99
+ }),
100
+ )
101
+
102
+ server.tool(
103
+ {
104
+ name: ANALYTICS_SETTINGS_DATA,
105
+ title: "Analytics settings data (internal)",
106
+ description:
107
+ "Internal JSON feed (no UI) for the analytics settings section's self-fetch. Prefer analytics_show_settings.",
108
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
109
+ inputSchema: strictToolInput({}),
110
+ ...appOnly,
111
+ },
112
+ withToolErrors(async (_params, ctx) => buildDataFeedResult({ ...(await loadView(ctx)) })),
113
+ )
114
+
115
+ // Without a writable store (or on the `read-only` floor) there is nothing to
116
+ // save into — the section stays read-only (canSave: false) and the tool
117
+ // surface honestly reflects that.
118
+ if (!save || !store) return
119
+
120
+ server.tool(
121
+ {
122
+ name: ANALYTICS_SAVE_SETTINGS,
123
+ title: "Save analytics settings",
124
+ description:
125
+ "Update the caller's analytics defaults. Only the provided fields change; omitted fields keep their value. The saved defaults apply whenever an analytics call omits `period` or `minBucketSize`.",
126
+ // The module's only non-read-only tool, and explicitly NOT destructive
127
+ // (MCP presumes a write destructive unless told otherwise): it merges
128
+ // the caller's own slice, nothing is deleted or overwritten wholesale.
129
+ annotations: {
130
+ readOnlyHint: false,
131
+ destructiveHint: false,
132
+ idempotentHint: true,
133
+ openWorldHint: false,
134
+ },
135
+ inputSchema: strictToolInput(analyticsSettingsSaveInput.shape),
136
+ // No view binding / app visibility: a normal model-visible tool; the
137
+ // settings widget also calls it and reads the saved slice back from
138
+ // structuredContent.
139
+ },
140
+ withToolErrors(async (params, ctx) => {
141
+ // Keyless refusal + a patch-only slice save are the shared slice-write
142
+ // contract (`@miragon/pulse-widget-shell/server`) — every module's save
143
+ // tool uses the same pair. The store merges the patch under its per-key
144
+ // lock, so a concurrent save of the other field is never reverted.
145
+ const key = requireProfileKey(ctx)
146
+ // The OAuth caller is stamped as the record's owner.
147
+ const savedSlice = await saveModuleSlice(
148
+ { get: (k) => store.get(k), save },
149
+ key,
150
+ ANALYTICS_MODULE_KEY,
151
+ params,
152
+ { userId: resolveSettingsAuthUserId(ctx) },
153
+ )
154
+ const effective = parseAnalyticsSettings({ [ANALYTICS_MODULE_KEY]: savedSlice })
155
+ const t = await localizeFor(store, ctx)
156
+ return {
157
+ content: [
158
+ {
159
+ type: "text" as const,
160
+ text: t("aSum.settingsSaved", {
161
+ period: effective.defaultPeriod,
162
+ minBucketSize: effective.minBucketSize,
163
+ }),
164
+ },
165
+ ],
166
+ structuredContent: effective as unknown as Record<string, unknown>,
167
+ }
168
+ }),
169
+ )
170
+ }