@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.
- package/LICENSE +21 -0
- package/README.md +86 -3
- package/dist/bpmn-name-lookup.d.ts +72 -0
- package/dist/bpmn-name-lookup.d.ts.map +1 -0
- package/dist/bpmn-name-lookup.js +123 -0
- package/dist/bpmn-name-lookup.js.map +1 -0
- package/dist/bpmn-name-lookup.test.d.ts +2 -0
- package/dist/bpmn-name-lookup.test.d.ts.map +1 -0
- package/dist/bpmn-name-lookup.test.js +287 -0
- package/dist/bpmn-name-lookup.test.js.map +1 -0
- package/dist/bpmn-names.d.ts +38 -0
- package/dist/bpmn-names.d.ts.map +1 -0
- package/dist/bpmn-names.js +22 -0
- package/dist/bpmn-names.js.map +1 -0
- package/dist/brand-gates.test.d.ts +2 -0
- package/dist/brand-gates.test.d.ts.map +1 -0
- package/dist/brand-gates.test.js +58 -0
- package/dist/brand-gates.test.js.map +1 -0
- package/dist/builder-texts.d.ts +4 -0
- package/dist/builder-texts.d.ts.map +1 -0
- package/dist/builder-texts.js +14 -0
- package/dist/builder-texts.js.map +1 -0
- package/dist/builder-texts.test.d.ts +2 -0
- package/dist/builder-texts.test.d.ts.map +1 -0
- package/dist/builder-texts.test.js +32 -0
- package/dist/builder-texts.test.js.map +1 -0
- package/dist/definition.d.ts +3 -0
- package/dist/definition.d.ts.map +1 -0
- package/dist/definition.js +151 -0
- package/dist/definition.js.map +1 -0
- package/dist/engine-ids.d.ts +42 -0
- package/dist/engine-ids.d.ts.map +1 -0
- package/dist/engine-ids.js +50 -0
- package/dist/engine-ids.js.map +1 -0
- package/dist/engine-scope.test.d.ts +2 -0
- package/dist/engine-scope.test.d.ts.map +1 -0
- package/dist/engine-scope.test.js +260 -0
- package/dist/engine-scope.test.js.map +1 -0
- package/dist/heatmap-data.d.ts +15 -0
- package/dist/heatmap-data.d.ts.map +1 -0
- package/dist/heatmap-data.js +2 -0
- package/dist/heatmap-data.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/instructions.d.ts +8 -0
- package/dist/instructions.d.ts.map +1 -0
- package/dist/instructions.js +33 -0
- package/dist/instructions.js.map +1 -0
- package/dist/messages/de.ask-ai.d.ts +9 -0
- package/dist/messages/de.ask-ai.d.ts.map +1 -0
- package/dist/messages/de.ask-ai.js +18 -0
- package/dist/messages/de.ask-ai.js.map +1 -0
- package/dist/messages/de.builder.d.ts +12 -0
- package/dist/messages/de.builder.d.ts.map +1 -0
- package/dist/messages/de.builder.js +53 -0
- package/dist/messages/de.builder.js.map +1 -0
- package/dist/messages/de.server.d.ts +8 -0
- package/dist/messages/de.server.d.ts.map +1 -0
- package/dist/messages/de.server.js +26 -0
- package/dist/messages/de.server.js.map +1 -0
- package/dist/messages/de.sweep.d.ts +10 -0
- package/dist/messages/de.sweep.d.ts.map +1 -0
- package/dist/messages/de.sweep.js +203 -0
- package/dist/messages/de.sweep.js.map +1 -0
- package/dist/messages/en.ask-ai.d.ts +25 -0
- package/dist/messages/en.ask-ai.d.ts.map +1 -0
- package/dist/messages/en.ask-ai.js +24 -0
- package/dist/messages/en.ask-ai.js.map +1 -0
- package/dist/messages/en.builder.d.ts +12 -0
- package/dist/messages/en.builder.d.ts.map +1 -0
- package/dist/messages/en.builder.js +53 -0
- package/dist/messages/en.builder.js.map +1 -0
- package/dist/messages/en.server.d.ts +8 -0
- package/dist/messages/en.server.d.ts.map +1 -0
- package/dist/messages/en.server.js +26 -0
- package/dist/messages/en.server.js.map +1 -0
- package/dist/messages/en.sweep.d.ts +11 -0
- package/dist/messages/en.sweep.d.ts.map +1 -0
- package/dist/messages/en.sweep.js +204 -0
- package/dist/messages/en.sweep.js.map +1 -0
- package/dist/messages/glossary.d.ts +26 -0
- package/dist/messages/glossary.d.ts.map +1 -0
- package/dist/messages/glossary.js +37 -0
- package/dist/messages/glossary.js.map +1 -0
- package/dist/messages/glossary.test.d.ts +2 -0
- package/dist/messages/glossary.test.d.ts.map +1 -0
- package/dist/messages/glossary.test.js +109 -0
- package/dist/messages/glossary.test.js.map +1 -0
- package/dist/messages/index.d.ts +12 -0
- package/dist/messages/index.d.ts.map +1 -0
- package/dist/messages/index.js +23 -0
- package/dist/messages/index.js.map +1 -0
- package/dist/messages/plural.d.ts +10 -0
- package/dist/messages/plural.d.ts.map +1 -0
- package/dist/messages/plural.js +17 -0
- package/dist/messages/plural.js.map +1 -0
- package/dist/messages/use-t.d.ts +10 -0
- package/dist/messages/use-t.d.ts.map +1 -0
- package/dist/messages/use-t.js +10 -0
- package/dist/messages/use-t.js.map +1 -0
- package/dist/module.d.ts +48 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +174 -0
- package/dist/module.js.map +1 -0
- package/dist/module.test.d.ts +2 -0
- package/dist/module.test.d.ts.map +1 -0
- package/dist/module.test.js +230 -0
- package/dist/module.test.js.map +1 -0
- package/dist/named-views.d.ts +27 -0
- package/dist/named-views.d.ts.map +1 -0
- package/dist/named-views.js +73 -0
- package/dist/named-views.js.map +1 -0
- package/dist/named-views.test.d.ts +2 -0
- package/dist/named-views.test.d.ts.map +1 -0
- package/dist/named-views.test.js +110 -0
- package/dist/named-views.test.js.map +1 -0
- package/dist/plugin.d.ts +44 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +52 -0
- package/dist/plugin.js.map +1 -0
- package/dist/prometheus-wiring.test.d.ts +2 -0
- package/dist/prometheus-wiring.test.d.ts.map +1 -0
- package/dist/prometheus-wiring.test.js +142 -0
- package/dist/prometheus-wiring.test.js.map +1 -0
- package/dist/server-locale.d.ts +48 -0
- package/dist/server-locale.d.ts.map +1 -0
- package/dist/server-locale.js +33 -0
- package/dist/server-locale.js.map +1 -0
- package/dist/settings-tools.d.ts +23 -0
- package/dist/settings-tools.d.ts.map +1 -0
- package/dist/settings-tools.js +100 -0
- package/dist/settings-tools.js.map +1 -0
- package/dist/settings.d.ts +77 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +99 -0
- package/dist/settings.js.map +1 -0
- package/dist/settings.test.d.ts +2 -0
- package/dist/settings.test.d.ts.map +1 -0
- package/dist/settings.test.js +299 -0
- package/dist/settings.test.js.map +1 -0
- package/dist/steps/app-config.d.ts +30 -0
- package/dist/steps/app-config.d.ts.map +1 -0
- package/dist/steps/app-config.js +46 -0
- package/dist/steps/app-config.js.map +1 -0
- package/dist/steps/dashboard.d.ts +12 -0
- package/dist/steps/dashboard.d.ts.map +1 -0
- package/dist/steps/dashboard.js +58 -0
- package/dist/steps/dashboard.js.map +1 -0
- package/dist/steps/failure-dashboard.d.ts +10 -0
- package/dist/steps/failure-dashboard.d.ts.map +1 -0
- package/dist/steps/failure-dashboard.js +32 -0
- package/dist/steps/failure-dashboard.js.map +1 -0
- package/dist/steps/index.d.ts +3 -0
- package/dist/steps/index.d.ts.map +1 -0
- package/dist/steps/index.js +3 -0
- package/dist/steps/index.js.map +1 -0
- package/dist/steps/steps.test.d.ts +2 -0
- package/dist/steps/steps.test.d.ts.map +1 -0
- package/dist/steps/steps.test.js +153 -0
- package/dist/steps/steps.test.js.map +1 -0
- package/dist/tool-names.d.ts +13 -0
- package/dist/tool-names.d.ts.map +1 -0
- package/dist/tool-names.js +27 -0
- package/dist/tool-names.js.map +1 -0
- package/dist/tools/cluster-compare.d.ts +8 -0
- package/dist/tools/cluster-compare.d.ts.map +1 -0
- package/dist/tools/cluster-compare.js +17 -0
- package/dist/tools/cluster-compare.js.map +1 -0
- package/dist/tools/element.d.ts +8 -0
- package/dist/tools/element.d.ts.map +1 -0
- package/dist/tools/element.js +25 -0
- package/dist/tools/element.js.map +1 -0
- package/dist/tools/engine-compare.d.ts +8 -0
- package/dist/tools/engine-compare.d.ts.map +1 -0
- package/dist/tools/engine-compare.js +18 -0
- package/dist/tools/engine-compare.js.map +1 -0
- package/dist/tools/engine-compare.test.d.ts +2 -0
- package/dist/tools/engine-compare.test.d.ts.map +1 -0
- package/dist/tools/engine-compare.test.js +166 -0
- package/dist/tools/engine-compare.test.js.map +1 -0
- package/dist/tools/engine-landscape.d.ts +7 -0
- package/dist/tools/engine-landscape.d.ts.map +1 -0
- package/dist/tools/engine-landscape.js +13 -0
- package/dist/tools/engine-landscape.js.map +1 -0
- package/dist/tools/engine-landscape.test.d.ts +2 -0
- package/dist/tools/engine-landscape.test.d.ts.map +1 -0
- package/dist/tools/engine-landscape.test.js +110 -0
- package/dist/tools/engine-landscape.test.js.map +1 -0
- package/dist/tools/failures.d.ts +7 -0
- package/dist/tools/failures.d.ts.map +1 -0
- package/dist/tools/failures.js +13 -0
- package/dist/tools/failures.js.map +1 -0
- package/dist/tools/health.d.ts +7 -0
- package/dist/tools/health.d.ts.map +1 -0
- package/dist/tools/health.js +23 -0
- package/dist/tools/health.js.map +1 -0
- package/dist/tools/health.test.d.ts +2 -0
- package/dist/tools/health.test.d.ts.map +1 -0
- package/dist/tools/health.test.js +119 -0
- package/dist/tools/health.test.js.map +1 -0
- package/dist/tools/index.d.ts +18 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +35 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/performance.d.ts +8 -0
- package/dist/tools/performance.d.ts.map +1 -0
- package/dist/tools/performance.js +25 -0
- package/dist/tools/performance.js.map +1 -0
- package/dist/tools/performance.test.d.ts +2 -0
- package/dist/tools/performance.test.d.ts.map +1 -0
- package/dist/tools/performance.test.js +176 -0
- package/dist/tools/performance.test.js.map +1 -0
- package/dist/tools/version-compare.d.ts +8 -0
- package/dist/tools/version-compare.d.ts.map +1 -0
- package/dist/tools/version-compare.js +17 -0
- package/dist/tools/version-compare.js.map +1 -0
- package/dist/tools/version-compare.test.d.ts +2 -0
- package/dist/tools/version-compare.test.d.ts.map +1 -0
- package/dist/tools/version-compare.test.js +82 -0
- package/dist/tools/version-compare.test.js.map +1 -0
- package/dist/toolsets.d.ts +17 -0
- package/dist/toolsets.d.ts.map +1 -0
- package/dist/toolsets.js +47 -0
- package/dist/toolsets.js.map +1 -0
- package/dist/toolsets.test.d.ts +2 -0
- package/dist/toolsets.test.d.ts.map +1 -0
- package/dist/toolsets.test.js +91 -0
- package/dist/toolsets.test.js.map +1 -0
- package/dist/view-title.test.d.ts +2 -0
- package/dist/view-title.test.d.ts.map +1 -0
- package/dist/view-title.test.js +91 -0
- package/dist/view-title.test.js.map +1 -0
- package/dist/widget-tools/comparisons.d.ts +13 -0
- package/dist/widget-tools/comparisons.d.ts.map +1 -0
- package/dist/widget-tools/comparisons.js +177 -0
- package/dist/widget-tools/comparisons.js.map +1 -0
- package/dist/widget-tools/comparisons.test.d.ts +2 -0
- package/dist/widget-tools/comparisons.test.d.ts.map +1 -0
- package/dist/widget-tools/comparisons.test.js +123 -0
- package/dist/widget-tools/comparisons.test.js.map +1 -0
- package/dist/widget-tools/shared.d.ts +36 -0
- package/dist/widget-tools/shared.d.ts.map +1 -0
- package/dist/widget-tools/shared.js +39 -0
- package/dist/widget-tools/shared.js.map +1 -0
- package/dist/widget-tools.d.ts +25 -0
- package/dist/widget-tools.d.ts.map +1 -0
- package/dist/widget-tools.js +269 -0
- package/dist/widget-tools.js.map +1 -0
- package/dist/widget-tools.test.d.ts +2 -0
- package/dist/widget-tools.test.d.ts.map +1 -0
- package/dist/widget-tools.test.js +272 -0
- package/dist/widget-tools.test.js.map +1 -0
- package/package.json +91 -5
- package/src/bpmn-name-lookup.test.ts +333 -0
- package/src/bpmn-name-lookup.ts +197 -0
- package/src/bpmn-names.ts +51 -0
- package/src/brand-gates.test.ts +69 -0
- package/src/builder-texts.test.ts +40 -0
- package/src/builder-texts.ts +15 -0
- package/src/definition.ts +181 -0
- package/src/engine-ids.ts +85 -0
- package/src/engine-scope.test.ts +324 -0
- package/src/heatmap-data.ts +14 -0
- package/src/index.ts +4 -0
- package/src/instructions.ts +33 -0
- package/src/messages/GLOSSARY.md +88 -0
- package/src/messages/de.ask-ai.ts +28 -0
- package/src/messages/de.builder.ts +77 -0
- package/src/messages/de.server.ts +39 -0
- package/src/messages/de.sweep.ts +244 -0
- package/src/messages/en.ask-ai.ts +34 -0
- package/src/messages/en.builder.ts +76 -0
- package/src/messages/en.server.ts +39 -0
- package/src/messages/en.sweep.ts +240 -0
- package/src/messages/glossary.test.ts +122 -0
- package/src/messages/glossary.ts +62 -0
- package/src/messages/index.ts +24 -0
- package/src/messages/plural.ts +18 -0
- package/src/messages/use-t.ts +12 -0
- package/src/module.test.ts +319 -0
- package/src/module.ts +211 -0
- package/src/named-views.test.ts +141 -0
- package/src/named-views.ts +97 -0
- package/src/plugin.ts +94 -0
- package/src/prometheus-wiring.test.ts +174 -0
- package/src/server-locale.ts +59 -0
- package/src/settings-tools.ts +170 -0
- package/src/settings.test.ts +402 -0
- package/src/settings.ts +114 -0
- package/src/steps/app-config.ts +75 -0
- package/src/steps/dashboard.ts +73 -0
- package/src/steps/failure-dashboard.ts +38 -0
- package/src/steps/index.ts +2 -0
- package/src/steps/steps.test.ts +216 -0
- package/src/tool-names.ts +26 -0
- package/src/tools/cluster-compare.ts +28 -0
- package/src/tools/element.ts +35 -0
- package/src/tools/engine-compare.test.ts +210 -0
- package/src/tools/engine-compare.ts +30 -0
- package/src/tools/engine-landscape.test.ts +138 -0
- package/src/tools/engine-landscape.ts +21 -0
- package/src/tools/failures.ts +19 -0
- package/src/tools/health.test.ts +156 -0
- package/src/tools/health.ts +29 -0
- package/src/tools/index.ts +44 -0
- package/src/tools/performance.test.ts +210 -0
- package/src/tools/performance.ts +38 -0
- package/src/tools/version-compare.test.ts +97 -0
- package/src/tools/version-compare.ts +28 -0
- package/src/toolsets.test.ts +107 -0
- package/src/toolsets.ts +56 -0
- package/src/view-title.test.ts +123 -0
- package/src/widget-tools/comparisons.test.ts +170 -0
- package/src/widget-tools/comparisons.ts +229 -0
- package/src/widget-tools/shared.ts +67 -0
- package/src/widget-tools.test.ts +306 -0
- package/src/widget-tools.ts +375 -0
- package/src/widgets/analytics-dashboard/activity-bottleneck-table.tsx +179 -0
- package/src/widgets/analytics-dashboard/definition-breakdown.tsx +108 -0
- package/src/widgets/analytics-dashboard/execution-performance-kpi.tsx +94 -0
- package/src/widgets/analytics-dashboard/execution-summary-kpi.tsx +122 -0
- package/src/widgets/analytics-dashboard/lib.ts +43 -0
- package/src/widgets/bpmn-heatmap.test.tsx +75 -0
- package/src/widgets/bpmn-heatmap.tsx +127 -0
- package/src/widgets/catalogue-sync.test.ts +16 -0
- package/src/widgets/cluster-compare.tsx +148 -0
- package/src/widgets/comparison-shared.test.ts +237 -0
- package/src/widgets/comparison-shared.tsx +371 -0
- package/src/widgets/comparison-widgets.test.tsx +307 -0
- package/src/widgets/dashboard-widgets.test.tsx +397 -0
- package/src/widgets/engine-compare.tsx +87 -0
- package/src/widgets/engine-landscape.tsx +352 -0
- package/src/widgets/failure-dashboard/error-patterns-table.tsx +122 -0
- package/src/widgets/failure-dashboard/failure-rate-table.test.ts +80 -0
- package/src/widgets/failure-dashboard/failure-rate-table.tsx +170 -0
- package/src/widgets/failure-dashboard/lib.ts +32 -0
- package/src/widgets/failure-dashboard/summary-kpi.tsx +122 -0
- package/src/widgets/failure-landscape-widgets.test.tsx +393 -0
- package/src/widgets/figure.tsx +79 -0
- package/src/widgets/hand-off.test.ts +126 -0
- package/src/widgets/hand-off.ts +102 -0
- package/src/widgets/index.ts +124 -0
- package/src/widgets/model-descriptions.dashboard.test.ts +280 -0
- package/src/widgets/model-descriptions.test.ts +251 -0
- package/src/widgets/model-descriptions.ts +428 -0
- package/src/widgets/names.test.ts +32 -0
- package/src/widgets/names.ts +39 -0
- package/src/widgets/query-gate.test.tsx +57 -0
- package/src/widgets/query-gate.tsx +50 -0
- package/src/widgets/settings-section.test.tsx +174 -0
- package/src/widgets/settings-section.tsx +244 -0
- package/src/widgets/version-compare.test.ts +68 -0
- package/src/widgets/version-compare.tsx +96 -0
- package/src/widgets/view-meta.ts +18 -0
- 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
|
+
})
|
package/src/settings.ts
ADDED
|
@@ -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
|
+
}
|