@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,402 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest"
2
+ import { parseToolResult } from "@miragon/mcp-toolkit-ui"
3
+ import type { MCPServer } from "mcp-use"
4
+ import {
5
+ createInMemoryProfileStore,
6
+ runWithMcpRequestInfo,
7
+ } from "@miragon/pulse-widget-shell/server"
8
+ import {
9
+ analyticsSettingsSaveInput,
10
+ analyticsSettingsSchema,
11
+ parseAnalyticsSettings,
12
+ settingsFor,
13
+ } from "./settings.js"
14
+ import { registerSettingsTools } from "./settings-tools.js"
15
+ import { ANALYTICS_SAVE_SETTINGS, ANALYTICS_SETTINGS_DATA } from "./tool-names.js"
16
+ import { localizeFor, type ProfileSource } from "./server-locale.js"
17
+ import type { AnalyticsToolset } from "./toolsets.js"
18
+
19
+ /** A feed's payload as the widget reads it (feeds answer in JSON text only). */
20
+ const feedData = <T = Record<string, unknown>>(result: unknown): T => parseToolResult<T>(result)
21
+
22
+ afterEach(() => {
23
+ vi.restoreAllMocks()
24
+ })
25
+
26
+ /** A signed-in caller's handler ctx (mcp-use's flattened `ctx.auth`). */
27
+ const CTX = { auth: { user: { id: "user-1" } } }
28
+
29
+ describe("analyticsSettingsSchema", () => {
30
+ it("fills every default from an empty object", () => {
31
+ expect(analyticsSettingsSchema.parse({})).toEqual({ defaultPeriod: "7d", minBucketSize: 10 })
32
+ })
33
+ })
34
+
35
+ describe("analyticsSettingsSaveInput", () => {
36
+ // Zod 4 re-applies `.default()`s through `.partial()` — the save input must
37
+ // stay default-free so a single-field save can't reset the other saved value
38
+ // at the tool boundary.
39
+ it("keeps omitted fields ABSENT instead of materializing defaults", () => {
40
+ expect(analyticsSettingsSaveInput.parse({ minBucketSize: 5 })).toEqual({ minBucketSize: 5 })
41
+ expect(analyticsSettingsSaveInput.parse({})).toEqual({})
42
+ })
43
+ })
44
+
45
+ describe("parseAnalyticsSettings", () => {
46
+ it("returns defaults when the slice is absent", () => {
47
+ expect(parseAnalyticsSettings(undefined)).toEqual({ defaultPeriod: "7d", minBucketSize: 10 })
48
+ expect(parseAnalyticsSettings({})).toEqual({ defaultPeriod: "7d", minBucketSize: 10 })
49
+ })
50
+
51
+ it("returns defaults for a garbage slice (fail-soft)", () => {
52
+ expect(parseAnalyticsSettings({ analytics: "nope" })).toEqual({
53
+ defaultPeriod: "7d",
54
+ minBucketSize: 10,
55
+ })
56
+ expect(parseAnalyticsSettings({ analytics: { defaultPeriod: "yesterday" } })).toEqual({
57
+ defaultPeriod: "7d",
58
+ minBucketSize: 10,
59
+ })
60
+ })
61
+
62
+ it("degrades per FIELD: one invalid value keeps the other saved preference", () => {
63
+ expect(
64
+ parseAnalyticsSettings({ analytics: { defaultPeriod: "yesterday", minBucketSize: 5 } }),
65
+ ).toEqual({ defaultPeriod: "7d", minBucketSize: 5 })
66
+ })
67
+
68
+ it("parses a valid slice and fills missing fields with defaults", () => {
69
+ expect(parseAnalyticsSettings({ analytics: { defaultPeriod: "30d" } })).toEqual({
70
+ defaultPeriod: "30d",
71
+ minBucketSize: 10,
72
+ })
73
+ })
74
+
75
+ it("ignores other modules' slices", () => {
76
+ expect(parseAnalyticsSettings({ other: { defaultPeriod: "1d" } })).toEqual({
77
+ defaultPeriod: "7d",
78
+ minBucketSize: 10,
79
+ })
80
+ })
81
+ })
82
+
83
+ describe("settingsFor", () => {
84
+ it("returns defaults without a store", async () => {
85
+ expect(await settingsFor(undefined)).toEqual({ defaultPeriod: "7d", minBucketSize: 10 })
86
+ })
87
+
88
+ it("reads the slice off the profile record", async () => {
89
+ const store: ProfileSource = {
90
+ get: () =>
91
+ Promise.resolve({ modules: { analytics: { defaultPeriod: "14d", minBucketSize: 3 } } }),
92
+ }
93
+ expect(await settingsFor(store, CTX)).toEqual({ defaultPeriod: "14d", minBucketSize: 3 })
94
+ // No caller identity → the defaults, never someone else's slice.
95
+ expect(await settingsFor(store)).toEqual({ defaultPeriod: "7d", minBucketSize: 10 })
96
+ })
97
+
98
+ it("falls back to defaults when the store throws (outage must not fail analytics reads)", async () => {
99
+ const store: ProfileSource = {
100
+ get: () => Promise.reject(new Error("connection refused")),
101
+ }
102
+ expect(await settingsFor(store, CTX)).toEqual({ defaultPeriod: "7d", minBucketSize: 10 })
103
+ })
104
+ })
105
+
106
+ describe("localizeFor", () => {
107
+ it("binds the translate to the profile language", async () => {
108
+ const store: ProfileSource = { get: () => Promise.resolve({ language: "de" }) }
109
+ const t = await localizeFor(store, CTX)
110
+ expect(t("aSettings.heading")).toBe("Analyse-Einstellungen")
111
+ })
112
+
113
+ it("falls back to English on a store OUTAGE, like settingsFor", async () => {
114
+ const store: ProfileSource = { get: () => Promise.reject(new Error("connection refused")) }
115
+ const t = await localizeFor(store, CTX)
116
+ expect(t("aSettings.heading")).toBe("Analytics settings")
117
+ })
118
+ })
119
+
120
+ describe("registerSettingsTools", () => {
121
+ // `toolset` is a plain string here on purpose: an untyped (JS) caller can
122
+ // hand the gate any name, and the gate must still resolve it fail-closed.
123
+ function registeredToolNames(store?: ProfileSource, toolset?: string): string[] {
124
+ const tool = vi.fn()
125
+ const server = { tool } as unknown as MCPServer
126
+ registerSettingsTools(server, store, toolset as AnalyticsToolset | undefined)
127
+ return tool.mock.calls.map((c) => (c[0] as { name: string }).name)
128
+ }
129
+
130
+ const writable: ProfileSource = {
131
+ get: () => Promise.resolve(undefined),
132
+ save: () => Promise.resolve({}),
133
+ }
134
+
135
+ it('registers the save tool with a writable store in the "standard" toolset', () => {
136
+ expect(registeredToolNames(writable, "standard")).toEqual([
137
+ "analytics_show_settings",
138
+ ANALYTICS_SETTINGS_DATA,
139
+ ANALYTICS_SAVE_SETTINGS,
140
+ ])
141
+ })
142
+
143
+ it("stays read-only without a writable store (no save tool), even in standard", () => {
144
+ const readOnly: ProfileSource = { get: () => Promise.resolve(undefined) }
145
+ expect(registeredToolNames(readOnly, "standard")).toEqual([
146
+ "analytics_show_settings",
147
+ ANALYTICS_SETTINGS_DATA,
148
+ ])
149
+ expect(registeredToolNames(undefined, "standard")).toEqual([
150
+ "analytics_show_settings",
151
+ ANALYTICS_SETTINGS_DATA,
152
+ ])
153
+ })
154
+
155
+ it("fails closed WITHOUT a toolset: a writable store alone registers no save tool", () => {
156
+ // A missing toolset is the read-only floor (silently — it is the
157
+ // documented default of a direct caller, not a misconfiguration).
158
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {})
159
+ expect(registeredToolNames(writable)).toEqual([
160
+ "analytics_show_settings",
161
+ ANALYTICS_SETTINGS_DATA,
162
+ ])
163
+ expect(warn).not.toHaveBeenCalled()
164
+ })
165
+
166
+ it('drops the durable save tool in the "read-only" toolset, fails closed on unknown names', () => {
167
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {})
168
+ expect(registeredToolNames(writable, "read-only")).toEqual([
169
+ "analytics_show_settings",
170
+ ANALYTICS_SETTINGS_DATA,
171
+ ])
172
+ expect(registeredToolNames(writable, "nonsense")).toEqual([
173
+ "analytics_show_settings",
174
+ ANALYTICS_SETTINGS_DATA,
175
+ ])
176
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining('Unknown toolset "nonsense"'))
177
+ })
178
+
179
+ it("a save-less section reports canSave false even for an identified caller", async () => {
180
+ const tool = vi.fn()
181
+ registerSettingsTools({ tool } as unknown as MCPServer, writable, "read-only")
182
+ const data = tool.mock.calls.find(
183
+ (c) => (c[0] as { name: string }).name === ANALYTICS_SETTINGS_DATA,
184
+ )![1] as () => Promise<unknown>
185
+ const view = await runWithMcpRequestInfo({ authUserId: "user-7" }, () => data())
186
+ expect(feedData(view).canSave).toBe(false)
187
+ })
188
+
189
+ it("the model summary points at the save tool only when it is registered", async () => {
190
+ type ShowResult = {
191
+ content: Array<{ text: string }>
192
+ structuredContent: Record<string, unknown>
193
+ }
194
+ const show = async (toolset: AnalyticsToolset): Promise<ShowResult> => {
195
+ const tool = vi.fn()
196
+ registerSettingsTools({ tool } as unknown as MCPServer, writable, toolset)
197
+ const handler = tool.mock.calls.find(
198
+ (c) => (c[0] as { name: string }).name === "analytics_show_settings",
199
+ )![1] as () => Promise<ShowResult>
200
+ return runWithMcpRequestInfo({ authUserId: "user-7" }, () => handler())
201
+ }
202
+ const defaults = { defaultPeriod: "7d", minBucketSize: 10 }
203
+
204
+ const standard = await show("standard")
205
+ expect(standard.content[0].text).toBe(
206
+ "Analytics settings: default period 7d, min bucket size 10. Change via analytics_save_settings.",
207
+ )
208
+ // A caller whose profile names no language follows the host: the server
209
+ // sets no view title (an English one would sit over a German view), and
210
+ // the section's own heading names the view (#322 U3).
211
+ expect(standard.structuredContent.title).toBeUndefined()
212
+ expect(standard.structuredContent).toMatchObject({
213
+ layout: [{ row: [{ widget: "analytics:settings" }] }],
214
+ context: {
215
+ stepData: {
216
+ result: {
217
+ data: { settings: defaults, canSave: true },
218
+ _app: "analytics",
219
+ _dataType: "analytics:settings",
220
+ },
221
+ },
222
+ },
223
+ })
224
+
225
+ // On the read-only floor the save tool does not exist — advertising it to
226
+ // the model would send it to an unknown tool.
227
+ const readOnly = await show("read-only")
228
+ expect(readOnly.content[0].text).toBe(
229
+ "Analytics settings: default period 7d, min bucket size 10.",
230
+ )
231
+ expect(readOnly.structuredContent).toMatchObject({
232
+ context: { stepData: { result: { data: { settings: defaults, canSave: false } } } },
233
+ })
234
+ })
235
+
236
+ it("declares honest metadata for the settings triple", () => {
237
+ const tool = vi.fn()
238
+ registerSettingsTools({ tool } as unknown as MCPServer, writable, "standard")
239
+ const definitions = tool.mock.calls.map(
240
+ (c) =>
241
+ c[0] as {
242
+ name: string
243
+ title?: string
244
+ description?: string
245
+ annotations?: Record<string, boolean>
246
+ },
247
+ )
248
+ const readOnlyRead = { readOnlyHint: true, idempotentHint: true, openWorldHint: true }
249
+ expect(definitions.map(({ name, annotations }) => [name, annotations])).toEqual([
250
+ ["analytics_show_settings", readOnlyRead],
251
+ [ANALYTICS_SETTINGS_DATA, readOnlyRead],
252
+ [
253
+ ANALYTICS_SAVE_SETTINGS,
254
+ { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
255
+ ],
256
+ ])
257
+ for (const { name, title, description } of definitions) {
258
+ expect(title, `${name} title`).toBeTruthy()
259
+ expect(description, `${name} description`).toBeTruthy()
260
+ }
261
+ })
262
+
263
+ it("round-trips a save through the caller's record", async () => {
264
+ const store = createInMemoryProfileStore()
265
+ const tool = vi.fn()
266
+ registerSettingsTools({ tool } as unknown as MCPServer, store, "standard")
267
+ type Handler = (
268
+ params: unknown,
269
+ ctx?: unknown,
270
+ ) => Promise<{
271
+ structuredContent?: Record<string, unknown>
272
+ content?: Array<{ type?: string; text?: string }>
273
+ }>
274
+ const handlerFor = (name: string): Handler => {
275
+ const call = tool.mock.calls.find((c) => (c[0] as { name: string }).name === name)
276
+ if (!call) throw new Error(`tool ${name} not registered`)
277
+ return call[1] as Handler
278
+ }
279
+
280
+ const saved = await handlerFor(ANALYTICS_SAVE_SETTINGS)({ defaultPeriod: "30d" }, CTX)
281
+ // The widget reads the EFFECTIVE slice back from structuredContent; the
282
+ // model gets the localized confirmation.
283
+ expect(saved.structuredContent).toEqual({ defaultPeriod: "30d", minBucketSize: 10 })
284
+ expect(saved.content).toEqual([
285
+ {
286
+ type: "text",
287
+ text: "Analytics settings saved: default period 30d, min bucket size 10.",
288
+ },
289
+ ])
290
+ // Only the provided field is persisted — no defaults materialized into
291
+ // storage, so a later default change applies to fields never set.
292
+ expect((await store.get("user-1"))?.modules).toEqual({
293
+ analytics: { defaultPeriod: "30d" },
294
+ })
295
+ const data = await handlerFor(ANALYTICS_SETTINGS_DATA)({}, CTX)
296
+ expect(feedData(data).settings).toEqual({ defaultPeriod: "30d", minBucketSize: 10 })
297
+ })
298
+
299
+ it("a partial save keeps the other saved value (the store merges the patch)", async () => {
300
+ const store = createInMemoryProfileStore()
301
+ const tool = vi.fn()
302
+ registerSettingsTools({ tool } as unknown as MCPServer, store, "standard")
303
+ const call = tool.mock.calls.find(
304
+ (c) => (c[0] as { name: string }).name === ANALYTICS_SAVE_SETTINGS,
305
+ )
306
+ const save = call![1] as (
307
+ params: unknown,
308
+ ctx: unknown,
309
+ ) => Promise<{ structuredContent?: unknown }>
310
+
311
+ await save({ defaultPeriod: "30d" }, CTX)
312
+ const saved = await save({ minBucketSize: 5 }, CTX)
313
+ expect((await store.get("user-1"))?.modules).toEqual({
314
+ analytics: { defaultPeriod: "30d", minBucketSize: 5 },
315
+ })
316
+ // The report is the SAVED slice, not just this call's patch.
317
+ expect(saved.structuredContent).toEqual({ defaultPeriod: "30d", minBucketSize: 5 })
318
+ })
319
+
320
+ it("concurrent saves of the two fields keep both, even when one already held a value", async () => {
321
+ // Writes land after a round-trip, like a database's: the second save runs
322
+ // while the first one's write is still in flight. A save that handed the
323
+ // store a slice pre-read outside its lock would write the stale period back.
324
+ const inner = createInMemoryProfileStore()
325
+ await inner.save("user-1", { modules: { analytics: { defaultPeriod: "7d" } } })
326
+ const store: ProfileSource = {
327
+ get: (key) => inner.get(key),
328
+ save: async (key, input, opts) => {
329
+ await new Promise((resolve) => setTimeout(resolve, 10))
330
+ return inner.save(key, input, opts)
331
+ },
332
+ }
333
+ const tool = vi.fn()
334
+ registerSettingsTools({ tool } as unknown as MCPServer, store, "standard")
335
+ const save = tool.mock.calls.find(
336
+ (c) => (c[0] as { name: string }).name === ANALYTICS_SAVE_SETTINGS,
337
+ )![1] as (params: unknown, ctx: unknown) => Promise<unknown>
338
+
339
+ const first = save({ defaultPeriod: "30d" }, CTX)
340
+ await new Promise((resolve) => setTimeout(resolve, 1))
341
+ await Promise.all([first, save({ minBucketSize: 25 }, CTX)])
342
+ expect((await inner.get("user-1"))?.modules).toEqual({
343
+ analytics: { defaultPeriod: "30d", minBucketSize: 25 },
344
+ })
345
+ })
346
+
347
+ // HTTP without OAuth: the ambient request info exists but carries no
348
+ // identity — the section must go read-only and the save tool must refuse
349
+ // with an actionable cause.
350
+ it("identity gating: canSave false + save refusal without identity, true with an auth user", async () => {
351
+ const tool = vi.fn()
352
+ registerSettingsTools(
353
+ { tool } as unknown as MCPServer,
354
+ { get: () => Promise.resolve(undefined), save: () => Promise.resolve({}) },
355
+ "standard",
356
+ )
357
+ type Handler = (
358
+ params: unknown,
359
+ ctx?: unknown,
360
+ ) => Promise<{
361
+ structuredContent?: Record<string, unknown>
362
+ content?: Array<{ text?: string }>
363
+ isError?: boolean
364
+ }>
365
+ const handlerFor = (name: string): Handler => {
366
+ const call = tool.mock.calls.find((c) => (c[0] as { name: string }).name === name)
367
+ if (!call) throw new Error(`tool ${name} not registered`)
368
+ return call[1] as Handler
369
+ }
370
+
371
+ const bare = await runWithMcpRequestInfo({}, () => handlerFor(ANALYTICS_SETTINGS_DATA)({}))
372
+ expect(feedData(bare).canSave).toBe(false)
373
+ const refused = await runWithMcpRequestInfo({}, () =>
374
+ handlerFor(ANALYTICS_SAVE_SETTINGS)({ defaultPeriod: "30d" }),
375
+ )
376
+ expect(refused.isError).toBe(true)
377
+ expect(refused.content?.[0]?.text).toContain("MCP_OAUTH")
378
+
379
+ const authed = await runWithMcpRequestInfo({ authUserId: "user-7" }, () =>
380
+ handlerFor(ANALYTICS_SETTINGS_DATA)({}),
381
+ )
382
+ expect(feedData(authed).canSave).toBe(true)
383
+ })
384
+
385
+ it("stamps the OAuth caller on the saved record as its owner", async () => {
386
+ const save = vi.fn<NonNullable<ProfileSource["save"]>>(() => Promise.resolve({}))
387
+ const tool = vi.fn()
388
+ registerSettingsTools(
389
+ { tool } as unknown as MCPServer,
390
+ { get: () => Promise.resolve(undefined), save },
391
+ "standard",
392
+ )
393
+ const handler = tool.mock.calls.find(
394
+ (c) => (c[0] as { name: string }).name === ANALYTICS_SAVE_SETTINGS,
395
+ )![1] as (params: unknown) => Promise<unknown>
396
+
397
+ await runWithMcpRequestInfo({ authUserId: "user-7" }, () => handler({ minBucketSize: 5 }))
398
+ expect(save).toHaveBeenCalledOnce()
399
+ expect(save.mock.calls[0][1]).toEqual({ modules: { analytics: { minBucketSize: 5 } } })
400
+ expect(save.mock.calls[0][2]).toEqual({ userId: "user-7" })
401
+ })
402
+ })
@@ -0,0 +1,114 @@
1
+ import { z } from "zod"
2
+ import { PERIODS } from "@miragon/pulse-analytics-client"
3
+ import { parseModuleSlice } from "@miragon/pulse-widget-shell/server"
4
+ import type { ProfileSource } from "./server-locale.js"
5
+ import { resolveSettingsKey } from "./server-locale.js"
6
+
7
+ /**
8
+ * The analytics module's OWN settings slice, persisted under
9
+ * `profile.modules.analytics` in the shared profile record (see the camunda7
10
+ * `modules` field — the profile transports the slice, this module owns its
11
+ * schema). Read fail-soft: a missing/foreign slice yields the defaults, so a
12
+ * stale or hand-edited record can never break analytics tools.
13
+ */
14
+ export const analyticsSettingsSchema = z.object({
15
+ defaultPeriod: z
16
+ .enum(PERIODS)
17
+ .default("7d")
18
+ .describe("Default look-back window applied when an analytics call omits `period`."),
19
+ minBucketSize: z
20
+ .number()
21
+ .int()
22
+ .min(1)
23
+ .default(10)
24
+ .describe(
25
+ "Default minimum per-window instance count before comparison results are flagged `suppressed`, applied when a compare call omits `minBucketSize`.",
26
+ ),
27
+ })
28
+
29
+ export type AnalyticsSettings = z.infer<typeof analyticsSettingsSchema>
30
+
31
+ /**
32
+ * The save tool's input — deliberately default-FREE: zod 4 re-applies
33
+ * `.default()`s through `.partial()` on parse, so a defaulted partial would
34
+ * materialize omitted fields at the tool boundary and a "single-field" save
35
+ * would silently reset the other saved value. With this shape, omitted keys
36
+ * stay absent and the handler's merge keeps them unchanged.
37
+ *
38
+ * Spelled out by hand because the save descriptions differ from the schema's
39
+ * ("Omitted → unchanged" is what the model needs at a write boundary). When the
40
+ * descriptions can be shared, derive it instead:
41
+ * `z.object(withoutDefaults(analyticsSettingsSchema.shape)).partial()`
42
+ * (`withoutDefaults` from `@miragon/pulse-widget-shell/server`) — which is what
43
+ * camunda7's `userProfileSaveInput` does.
44
+ */
45
+ export const analyticsSettingsSaveInput = z.object({
46
+ defaultPeriod: z
47
+ .enum(PERIODS)
48
+ .optional()
49
+ .describe("New default look-back period. Omitted → unchanged."),
50
+ minBucketSize: z
51
+ .number()
52
+ .int()
53
+ .min(1)
54
+ .optional()
55
+ .describe("New minimum comparison bucket size. Omitted → unchanged."),
56
+ })
57
+
58
+ /**
59
+ * `period` at the tool boundary is optional (no zod default) so an omitted
60
+ * value is distinguishable and can fall back to the user's saved analytics
61
+ * default before the schema default applies. Derived from PERIODS — never a
62
+ * copied enum. Shared by the widget tools and the registrar tools.
63
+ */
64
+ export const optionalPeriod = z
65
+ .enum(PERIODS)
66
+ .optional()
67
+ .describe("Analysis time period. Omitted → the user's saved analytics default (else 7d).")
68
+
69
+ /** Same optional-at-the-boundary treatment for `minBucketSize`. */
70
+ export const optionalMinBucketSize = z
71
+ .number()
72
+ .int()
73
+ .min(1)
74
+ .optional()
75
+ .describe(
76
+ "Minimum per-window instance count before results are flagged `suppressed`. Omitted → the user's saved analytics default (else 10).",
77
+ )
78
+
79
+ /** The module's slice key inside `profile.modules`. */
80
+ export const ANALYTICS_MODULE_KEY = "analytics"
81
+
82
+ /**
83
+ * The slice parsed leniently: garbage or absence yields the defaults —
84
+ * per FIELD (shared `parseModuleSlice`), so one invalid value keeps the
85
+ * other saved preference intact.
86
+ */
87
+ export function parseAnalyticsSettings(
88
+ modules: Record<string, unknown> | undefined,
89
+ ): AnalyticsSettings {
90
+ return parseModuleSlice(analyticsSettingsSchema, modules?.[ANALYTICS_MODULE_KEY])
91
+ }
92
+
93
+ /**
94
+ * Resolve the request's effective analytics settings from the shared profile
95
+ * store — the single place tools apply the "explicit arg > saved setting >
96
+ * schema default" precedence for `period`/`minBucketSize`. Fail-soft on every
97
+ * axis: no store, no resolvable key, or a store OUTAGE all yield the schema
98
+ * defaults — a profile-store hiccup must never fail an analytics read that
99
+ * only needs Prometheus.
100
+ */
101
+ export async function settingsFor(
102
+ store: ProfileSource | undefined,
103
+ ctx?: unknown,
104
+ ): Promise<AnalyticsSettings> {
105
+ if (!store) return analyticsSettingsSchema.parse({})
106
+ const key = resolveSettingsKey(ctx)
107
+ if (!key) return analyticsSettingsSchema.parse({})
108
+ try {
109
+ const profile = await store.get(key)
110
+ return parseAnalyticsSettings(profile?.modules)
111
+ } catch {
112
+ return analyticsSettingsSchema.parse({})
113
+ }
114
+ }
@@ -0,0 +1,75 @@
1
+ import { PERIODS, type PrometheusClient, type Period } from "@miragon/pulse-analytics-client"
2
+ import type { AnalyticsEngineScope } from "../engine-ids.js"
3
+ import type { ProfileSource } from "../server-locale.js"
4
+ import { settingsFor } from "../settings.js"
5
+ import type { NameLookup } from "../bpmn-name-lookup.js"
6
+
7
+ /** What the analytics plugin hands its pipeline steps (`appConfig` in `plugin.ts`). */
8
+ export interface AnalyticsAppConfig {
9
+ client: PrometheusClient
10
+ /** The server's configured engine ids — a step never reads an engine outside them. */
11
+ engineScope: AnalyticsEngineScope
12
+ /** The shared profile store — the source of the caller's saved analytics defaults. */
13
+ profileStore?: ProfileSource
14
+ /** The process names behind the ids, the same lookup the tools use (absent → ids). */
15
+ namesFor?: NameLookup
16
+ }
17
+
18
+ /*
19
+ * Step keys arrive unchecked: render-view and the builder accept them as a
20
+ * free-form record and pass them through untouched. So each reader below holds
21
+ * the line the tools' strict inputs hold — a malformed value is refused naming
22
+ * its key, never read as "omitted" (which would widen `analytics:engine` to the
23
+ * whole fleet, or swap in a default period nobody asked for).
24
+ */
25
+
26
+ /** The optional `analytics:engine` scoping key both steps read. */
27
+ export const ENGINE_KEY_DECLARATION = {
28
+ key: "analytics:engine",
29
+ description:
30
+ "Engine id, or a list of ids, to scope to (one of the server's configured engines). When omitted, every configured engine is aggregated — the result's `engines` names them.",
31
+ }
32
+
33
+ /** The step's `analytics:engine` key resolved to configured ids (omitted = all of them). */
34
+ export function stepEngines(
35
+ keys: Record<string, unknown>,
36
+ engineScope: AnalyticsEngineScope,
37
+ ): string[] {
38
+ const raw = keys[ENGINE_KEY_DECLARATION.key]
39
+ const wellFormed =
40
+ raw === undefined ||
41
+ typeof raw === "string" ||
42
+ (Array.isArray(raw) && raw.every((id) => typeof id === "string"))
43
+ if (!wellFormed) {
44
+ throw new Error(
45
+ `${ENGINE_KEY_DECLARATION.key} must be an engine id or a list of engine ids — omit it to cover every configured engine.`,
46
+ )
47
+ }
48
+ return engineScope.resolve(raw)
49
+ }
50
+
51
+ /**
52
+ * The step's `analytics:period`: the key when set, else the CALLER's saved
53
+ * analytics default (resolved from the ambient request — a step gets no
54
+ * handler ctx), else the module default — the tools' precedence.
55
+ */
56
+ export async function stepPeriod(
57
+ keys: Record<string, unknown>,
58
+ profileStore: ProfileSource | undefined,
59
+ ): Promise<Period> {
60
+ const raw = keys["analytics:period"]
61
+ if (raw === undefined) return (await settingsFor(profileStore)).defaultPeriod
62
+ if (PERIODS.includes(raw as Period)) return raw as Period
63
+ throw new Error(
64
+ `analytics:period must be one of ${PERIODS.join(", ")} — omit it for the caller's saved default.`,
65
+ )
66
+ }
67
+
68
+ /** The step's optional `analytics:processDefinitionKey` (omitted = every process). */
69
+ export function stepProcessKey(keys: Record<string, unknown>): string | undefined {
70
+ const raw = keys["analytics:processDefinitionKey"]
71
+ if (raw === undefined || typeof raw === "string") return raw
72
+ throw new Error(
73
+ "analytics:processDefinitionKey must be a process definition key (a string) — omit it to cover every process.",
74
+ )
75
+ }
@@ -0,0 +1,73 @@
1
+ import type { PipelineStepDefinition } from "@miragon/mcp-toolkit-core"
2
+ import { PERIODS, queries } from "@miragon/pulse-analytics-client"
3
+ import { analyticsSettingsSchema, settingsFor } from "../settings.js"
4
+ import { NO_NAMES } from "../bpmn-name-lookup.js"
5
+ import { namedDashboard } from "../named-views.js"
6
+ import {
7
+ ENGINE_KEY_DECLARATION,
8
+ stepEngines,
9
+ stepPeriod,
10
+ stepProcessKey,
11
+ type AnalyticsAppConfig,
12
+ } from "./app-config.js"
13
+ import { builderText } from "../builder-texts.js"
14
+
15
+ /** The period a caller without a saved default gets — the settings schema's default. */
16
+ const MODULE_DEFAULT_PERIOD = analyticsSettingsSchema.shape.defaultPeriod.parse(undefined)
17
+
18
+ /**
19
+ * Loads the dashboard figures from Prometheus — window flows plus the live
20
+ * state, the same `dashboardData` as `analytics_show_dashboard`. Consumed by
21
+ * `analytics:dashboard`. Reads optional filter keys:
22
+ * - `analytics:processDefinitionKey`
23
+ * - `analytics:period` (1d | 3d | 7d | 14d | 30d; omitted = the caller's saved default)
24
+ * - `analytics:engine` (configured engine id(s); omitted = all of them)
25
+ */
26
+ export const loadDashboardStep: PipelineStepDefinition<AnalyticsAppConfig> = {
27
+ id: "analytics:load-dashboard",
28
+ ...builderText("step.load-dashboard"),
29
+ description:
30
+ "Process metrics: what happened within a time window (instances started/completed, incidents created/resolved, durations of the instances that ended) plus the live state right now (instances running, incidents open), per process definition and per (process, activity). Powers the four dashboard widgets (execution-summary-kpi, execution-performance-kpi, process-definition-breakdown, activity-bottleneck-table).",
31
+ dataType: "analytics:dashboard",
32
+ requires: [],
33
+ optionalKeys: [
34
+ {
35
+ key: "analytics:processDefinitionKey",
36
+ description:
37
+ "Scope the dashboard to a single process definition (e.g. 'miraveloLeasing'). When omitted, all processes are aggregated.",
38
+ },
39
+ {
40
+ key: "analytics:period",
41
+ description: `Time window. When omitted, the caller's saved analytics default (else '${MODULE_DEFAULT_PERIOD}').`,
42
+ enum: [...PERIODS],
43
+ },
44
+ ENGINE_KEY_DECLARATION,
45
+ ],
46
+ produces: ["analytics:dashboardData"],
47
+ execute: async (context, appConfig) => {
48
+ // Every key is checked before the first query is sent.
49
+ const processDefinitionKey = stepProcessKey(context.keys)
50
+ const engine = stepEngines(context.keys, appConfig.engineScope)
51
+ const period = await stepPeriod(context.keys, appConfig.profileStore)
52
+ // The floor for judging the incident rate against the previous period:
53
+ // the caller's saved minBucketSize, like the tools'.
54
+ const { minBucketSize } = await settingsFor(appConfig.profileStore)
55
+
56
+ const data = await namedDashboard(
57
+ await queries.dashboardData(appConfig.client, {
58
+ processDefinitionKey,
59
+ period,
60
+ engine,
61
+ minBucketSize,
62
+ }),
63
+ appConfig.namesFor ?? NO_NAMES,
64
+ )
65
+
66
+ return {
67
+ data,
68
+ keys: { "analytics:dashboardData": data },
69
+ _app: "analytics",
70
+ _step: "load-dashboard",
71
+ }
72
+ },
73
+ }