@xpert-ai/plugin-live-dashboard 0.5.0 → 0.6.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 (58) hide show
  1. package/.xpertai-plugin/plugin.json +1 -1
  2. package/README.md +4 -2
  3. package/dist/index.js +3 -1
  4. package/dist/lib/agent.schema.d.ts +9 -9
  5. package/dist/lib/agent.schema.js +2 -1
  6. package/dist/lib/archive.service.d.ts +26 -2
  7. package/dist/lib/archive.service.js +17 -3
  8. package/dist/lib/bounded-map.d.ts +2 -0
  9. package/dist/lib/bounded-map.js +25 -0
  10. package/dist/lib/capacity.d.ts +14 -0
  11. package/dist/lib/capacity.js +16 -0
  12. package/dist/lib/dashboard-archive.d.ts +1 -1
  13. package/dist/lib/dashboard-queries.d.ts +1 -1
  14. package/dist/lib/dashboard-queries.js +11 -6
  15. package/dist/lib/dashboard-receipt.js +1 -1
  16. package/dist/lib/dashboard-sources.js +3 -2
  17. package/dist/lib/dashboard-view.schema.d.ts +20 -20
  18. package/dist/lib/dashboard.schema.d.ts +18 -18
  19. package/dist/lib/dashboard.schema.js +2 -1
  20. package/dist/lib/dashboard.service.d.ts +24 -5
  21. package/dist/lib/dashboard.service.js +58 -8
  22. package/dist/lib/definition-tools.d.ts +4 -4
  23. package/dist/lib/document-host.js +1 -1
  24. package/dist/lib/document-package.d.ts +1 -1
  25. package/dist/lib/editing-patch.js +2 -1
  26. package/dist/lib/editing.d.ts +11 -10
  27. package/dist/lib/editing.js +5 -1
  28. package/dist/lib/filter-evidence.d.ts +28 -0
  29. package/dist/lib/filter-evidence.js +3 -2
  30. package/dist/lib/html-guidance.d.ts +1 -0
  31. package/dist/lib/html-guidance.js +2 -1
  32. package/dist/lib/html-validation.js +1 -1
  33. package/dist/lib/middleware.d.ts +3 -1
  34. package/dist/lib/middleware.js +89 -11
  35. package/dist/lib/plugin.js +2 -1
  36. package/dist/lib/presentation.provider.d.ts +1 -1
  37. package/dist/lib/preview-retry.d.ts +4 -3
  38. package/dist/lib/preview-retry.js +13 -7
  39. package/dist/lib/prompt-workflows.js +1 -1
  40. package/dist/lib/query-session.d.ts +9 -0
  41. package/dist/lib/query-session.js +64 -0
  42. package/dist/lib/save-requests.d.ts +118 -0
  43. package/dist/lib/save-requests.js +199 -0
  44. package/dist/lib/source-executions.d.ts +243 -2
  45. package/dist/lib/source-executions.js +29 -5
  46. package/dist/lib/view.provider.d.ts +38 -3
  47. package/dist/lib/view.provider.js +20 -4
  48. package/dist/remote/app.js +114 -65
  49. package/dist/remote/build-manifest.json +37 -32
  50. package/dist/sandbox-actions/action.json +1 -1
  51. package/dist/sandbox-actions/bundle/host.js +63 -14
  52. package/dist/sandbox-actions/bundle/validate.mjs +2 -2
  53. package/dist/validation/host-entry.d.ts +5 -1
  54. package/dist/validation/host-entry.js +21 -4
  55. package/docs/docs.json +2 -0
  56. package/docs/large-dashboards-acceptance.mdx +74 -0
  57. package/docs/large-dashboards.mdx +80 -0
  58. package/package.json +5 -5
@@ -128,16 +128,16 @@ export declare const dashboardSchema: z.ZodEffects<z.ZodObject<{
128
128
  y: z.ZodString;
129
129
  color: z.ZodOptional<z.ZodString>;
130
130
  }, "strict", z.ZodTypeAny, {
131
- title: string;
132
131
  type: "bar" | "line" | "area" | "pie" | "scatter";
132
+ title: string;
133
133
  id: string;
134
134
  datasetId: string;
135
135
  x: string;
136
136
  y: string;
137
137
  color?: string | undefined;
138
138
  }, {
139
- title: string;
140
139
  type: "bar" | "line" | "area" | "pie" | "scatter";
140
+ title: string;
141
141
  id: string;
142
142
  datasetId: string;
143
143
  x: string;
@@ -173,8 +173,8 @@ export declare const dashboardSchema: z.ZodEffects<z.ZodObject<{
173
173
  allowedValues?: (string | number | boolean | null)[] | undefined;
174
174
  }[];
175
175
  charts: {
176
- title: string;
177
176
  type: "bar" | "line" | "area" | "pie" | "scatter";
177
+ title: string;
178
178
  id: string;
179
179
  datasetId: string;
180
180
  x: string;
@@ -220,8 +220,8 @@ export declare const dashboardSchema: z.ZodEffects<z.ZodObject<{
220
220
  allowedValues?: (string | number | boolean | null)[] | undefined;
221
221
  }[];
222
222
  charts: {
223
- title: string;
224
223
  type: "bar" | "line" | "area" | "pie" | "scatter";
224
+ title: string;
225
225
  id: string;
226
226
  datasetId: string;
227
227
  x: string;
@@ -273,8 +273,8 @@ export declare const dashboardSchema: z.ZodEffects<z.ZodObject<{
273
273
  allowedValues?: (string | number | boolean | null)[] | undefined;
274
274
  }[];
275
275
  charts: {
276
- title: string;
277
276
  type: "bar" | "line" | "area" | "pie" | "scatter";
277
+ title: string;
278
278
  id: string;
279
279
  datasetId: string;
280
280
  x: string;
@@ -320,8 +320,8 @@ export declare const dashboardSchema: z.ZodEffects<z.ZodObject<{
320
320
  allowedValues?: (string | number | boolean | null)[] | undefined;
321
321
  }[];
322
322
  charts: {
323
- title: string;
324
323
  type: "bar" | "line" | "area" | "pie" | "scatter";
324
+ title: string;
325
325
  id: string;
326
326
  datasetId: string;
327
327
  x: string;
@@ -493,16 +493,16 @@ export declare const dashboardChangesSchema: z.ZodObject<{
493
493
  y: z.ZodString;
494
494
  color: z.ZodOptional<z.ZodString>;
495
495
  }, "strict", z.ZodTypeAny, {
496
- title: string;
497
496
  type: "bar" | "line" | "area" | "pie" | "scatter";
497
+ title: string;
498
498
  id: string;
499
499
  datasetId: string;
500
500
  x: string;
501
501
  y: string;
502
502
  color?: string | undefined;
503
503
  }, {
504
- title: string;
505
504
  type: "bar" | "line" | "area" | "pie" | "scatter";
505
+ title: string;
506
506
  id: string;
507
507
  datasetId: string;
508
508
  x: string;
@@ -556,8 +556,8 @@ export declare const dashboardChangesSchema: z.ZodObject<{
556
556
  allowedValues?: (string | number | boolean | null)[] | undefined;
557
557
  }[] | undefined;
558
558
  charts?: {
559
- title: string;
560
559
  type: "bar" | "line" | "area" | "pie" | "scatter";
560
+ title: string;
561
561
  id: string;
562
562
  datasetId: string;
563
563
  x: string;
@@ -607,8 +607,8 @@ export declare const dashboardChangesSchema: z.ZodObject<{
607
607
  allowedValues?: (string | number | boolean | null)[] | undefined;
608
608
  }[] | undefined;
609
609
  charts?: {
610
- title: string;
611
610
  type: "bar" | "line" | "area" | "pie" | "scatter";
611
+ title: string;
612
612
  id: string;
613
613
  datasetId: string;
614
614
  x: string;
@@ -754,16 +754,16 @@ export declare const saveSchema: z.ZodObject<{
754
754
  y: z.ZodString;
755
755
  color: z.ZodOptional<z.ZodString>;
756
756
  }, "strict", z.ZodTypeAny, {
757
- title: string;
758
757
  type: "bar" | "line" | "area" | "pie" | "scatter";
758
+ title: string;
759
759
  id: string;
760
760
  datasetId: string;
761
761
  x: string;
762
762
  y: string;
763
763
  color?: string | undefined;
764
764
  }, {
765
- title: string;
766
765
  type: "bar" | "line" | "area" | "pie" | "scatter";
766
+ title: string;
767
767
  id: string;
768
768
  datasetId: string;
769
769
  x: string;
@@ -799,8 +799,8 @@ export declare const saveSchema: z.ZodObject<{
799
799
  allowedValues?: (string | number | boolean | null)[] | undefined;
800
800
  }[];
801
801
  charts: {
802
- title: string;
803
802
  type: "bar" | "line" | "area" | "pie" | "scatter";
803
+ title: string;
804
804
  id: string;
805
805
  datasetId: string;
806
806
  x: string;
@@ -846,8 +846,8 @@ export declare const saveSchema: z.ZodObject<{
846
846
  allowedValues?: (string | number | boolean | null)[] | undefined;
847
847
  }[];
848
848
  charts: {
849
- title: string;
850
849
  type: "bar" | "line" | "area" | "pie" | "scatter";
850
+ title: string;
851
851
  id: string;
852
852
  datasetId: string;
853
853
  x: string;
@@ -899,8 +899,8 @@ export declare const saveSchema: z.ZodObject<{
899
899
  allowedValues?: (string | number | boolean | null)[] | undefined;
900
900
  }[];
901
901
  charts: {
902
- title: string;
903
902
  type: "bar" | "line" | "area" | "pie" | "scatter";
903
+ title: string;
904
904
  id: string;
905
905
  datasetId: string;
906
906
  x: string;
@@ -946,8 +946,8 @@ export declare const saveSchema: z.ZodObject<{
946
946
  allowedValues?: (string | number | boolean | null)[] | undefined;
947
947
  }[];
948
948
  charts: {
949
- title: string;
950
949
  type: "bar" | "line" | "area" | "pie" | "scatter";
950
+ title: string;
951
951
  id: string;
952
952
  datasetId: string;
953
953
  x: string;
@@ -1003,8 +1003,8 @@ export declare const saveSchema: z.ZodObject<{
1003
1003
  allowedValues?: (string | number | boolean | null)[] | undefined;
1004
1004
  }[];
1005
1005
  charts: {
1006
- title: string;
1007
1006
  type: "bar" | "line" | "area" | "pie" | "scatter";
1007
+ title: string;
1008
1008
  id: string;
1009
1009
  datasetId: string;
1010
1010
  x: string;
@@ -1054,8 +1054,8 @@ export declare const saveSchema: z.ZodObject<{
1054
1054
  allowedValues?: (string | number | boolean | null)[] | undefined;
1055
1055
  }[];
1056
1056
  charts: {
1057
- title: string;
1058
1057
  type: "bar" | "line" | "area" | "pie" | "scatter";
1058
+ title: string;
1059
1059
  id: string;
1060
1060
  datasetId: string;
1061
1061
  x: string;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.queryBindingSchema = exports.querySchema = exports.saveSchema = exports.controlsSchema = exports.dashboardChangesSchema = exports.dashboardSchema = void 0;
4
+ const capacity_1 = require("./capacity");
4
5
  const v3_1 = require("zod/v3");
5
6
  const analytics_1 = require("@xpert-ai/plugin-sdk/analytics");
6
7
  const artifact_document_1 = require("@xpert-ai/plugin-sdk/artifact-document");
@@ -8,7 +9,7 @@ const dashboard_sources_1 = require("./dashboard-sources");
8
9
  const id = v3_1.z.string().regex(/^[A-Za-z][A-Za-z0-9_.-]{0,79}$/);
9
10
  exports.dashboardSchema = v3_1.z.object({
10
11
  schema: v3_1.z.literal('xpert.live-dashboard'), id: v3_1.z.string().uuid(), title: v3_1.z.string().min(1).max(160),
11
- bindings: v3_1.z.array(analytics_1.analyticsBindingSchema).max(8),
12
+ bindings: v3_1.z.array(analytics_1.analyticsBindingSchema).max(capacity_1.MAX_DASHBOARD_BINDINGS),
12
13
  sourceMetadata: dashboard_sources_1.sourceMetadataSchema.optional(),
13
14
  components: dashboard_sources_1.dashboardComponentsSchema.optional(),
14
15
  presentation: v3_1.z.object({ type: v3_1.z.literal('html'), html: artifact_document_1.artifactDocumentHtml, dependencies: artifact_document_1.artifactDocumentDependencyLocks.optional() }).strict().optional(),
@@ -1,3 +1,4 @@
1
+ import { type DashboardConfig } from './capacity';
1
2
  import type { AnalyticsValue, AnalyticsDiscoveryRequest, AnalyticsQueryRequest, XpertResolvedViewHostContext } from '@xpert-ai/contracts';
2
3
  import { type RuntimeCapabilityRegistry } from '@xpert-ai/plugin-sdk';
3
4
  import { type DashboardManifest, type DashboardChanges } from './dashboard.schema';
@@ -5,10 +6,18 @@ import { type ArtifactDocumentDependencyAsset } from '@xpert-ai/plugin-sdk/artif
5
6
  import { HtmlDashboardValidation } from './html-validation';
6
7
  import type { DashboardAccess, DashboardMode } from './project';
7
8
  export declare const PLUGIN_NAME = "@xpert-ai/plugin-live-dashboard";
9
+ export type SaveProgress = (stage: string, completed: number, total: number) => Promise<void>;
10
+ export type SaveOptions = {
11
+ requestId: string;
12
+ progress: SaveProgress;
13
+ };
8
14
  export declare class DashboardService {
9
15
  private readonly runtime;
10
16
  private readonly htmlValidation;
11
- constructor(runtime: RuntimeCapabilityRegistry, htmlValidation: HtmlDashboardValidation);
17
+ private readonly config?;
18
+ constructor(runtime: RuntimeCapabilityRegistry, htmlValidation: HtmlDashboardValidation, config?: DashboardConfig | undefined);
19
+ get maxBindings(): number;
20
+ assertCapacity(count: number, previousCount?: number): void;
12
21
  sources(context: XpertResolvedViewHostContext): Promise<import("@xpert-ai/contracts").AnalyticsSourceSummary[]>;
13
22
  discover(context: XpertResolvedViewHostContext, input: AnalyticsDiscoveryRequest): Promise<import("@xpert-ai/contracts").AnalyticsDiscoveryResult>;
14
23
  preview(context: XpertResolvedViewHostContext, input: AnalyticsQueryRequest): Promise<import("@xpert-ai/contracts").AnalyticalDataset>;
@@ -64,7 +73,17 @@ export declare class DashboardService {
64
73
  trash(context: XpertResolvedViewHostContext, artifactId: string, expectedRevision: number): Promise<{
65
74
  artifactId: string;
66
75
  }>;
67
- save(context: XpertResolvedViewHostContext, manifest: DashboardManifest, expectedVersionId: string | null, expectedRevision?: number, importedDependencies?: ArtifactDocumentDependencyAsset[]): Promise<{
76
+ previousBindingCount(context: XpertResolvedViewHostContext, manifest: DashboardManifest): Promise<number>;
77
+ /** Read-only reconciliation of a completed CAS after a worker/status-store interruption. */
78
+ savedRequestReceipt(context: XpertResolvedViewHostContext, manifestId: string, requestId: string, expectedRevision: number): Promise<{
79
+ artifactId: string;
80
+ draftId: string;
81
+ draftRevision: number;
82
+ baseVersionId: string | null;
83
+ status: "draft";
84
+ title: string;
85
+ } | null>;
86
+ save(context: XpertResolvedViewHostContext, manifest: DashboardManifest, expectedVersionId: string | null, expectedRevision?: number, importedDependencies?: ArtifactDocumentDependencyAsset[], task?: SaveOptions): Promise<{
68
87
  artifactId: string;
69
88
  draftId: string;
70
89
  draftRevision: number;
@@ -86,8 +105,8 @@ export declare class DashboardService {
86
105
  allowedValues?: (string | number | boolean | null)[] | undefined;
87
106
  }[];
88
107
  charts: {
89
- title: string;
90
108
  type: "bar" | "line" | "area" | "pie" | "scatter";
109
+ title: string;
91
110
  id: string;
92
111
  datasetId: string;
93
112
  x: string;
@@ -147,8 +166,8 @@ export declare class DashboardService {
147
166
  allowedValues?: (string | number | boolean | null)[] | undefined;
148
167
  }[];
149
168
  charts: {
150
- title: string;
151
169
  type: "bar" | "line" | "area" | "pie" | "scatter";
170
+ title: string;
152
171
  id: string;
153
172
  datasetId: string;
154
173
  x: string;
@@ -293,8 +312,8 @@ export declare class DashboardService {
293
312
  allowedValues?: (string | number | boolean | null)[] | undefined;
294
313
  }[];
295
314
  charts: {
296
- title: string;
297
315
  type: "bar" | "line" | "area" | "pie" | "scatter";
316
+ title: string;
298
317
  id: string;
299
318
  datasetId: string;
300
319
  x: string;
@@ -14,6 +14,8 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.DashboardService = exports.PLUGIN_NAME = void 0;
16
16
  exports.projectId = projectId;
17
+ const capacity_1 = require("./capacity");
18
+ const bounded_map_1 = require("./bounded-map");
17
19
  const common_1 = require("@nestjs/common");
18
20
  const node_crypto_1 = require("node:crypto");
19
21
  const plugin_sdk_1 = require("@xpert-ai/plugin-sdk");
@@ -30,9 +32,19 @@ exports.PLUGIN_NAME = '@xpert-ai/plugin-live-dashboard';
30
32
  const RESOURCE_TYPE = 'live-dashboard';
31
33
  const MIME_TYPE = 'application/json';
32
34
  let DashboardService = class DashboardService {
33
- constructor(runtime, htmlValidation) {
35
+ constructor(runtime, htmlValidation, config) {
34
36
  this.runtime = runtime;
35
37
  this.htmlValidation = htmlValidation;
38
+ this.config = config;
39
+ }
40
+ get maxBindings() { return this.config?.maxBindings ?? capacity_1.DEFAULT_DASHBOARD_BINDINGS; }
41
+ assertCapacity(count, previousCount = 0) {
42
+ try {
43
+ (0, capacity_1.assertBindingCapacity)(count, previousCount, this.maxBindings);
44
+ }
45
+ catch (error) {
46
+ throw new common_1.BadRequestException(error instanceof Error ? error.message : 'DASHBOARD_BINDING_CAPACITY');
47
+ }
36
48
  }
37
49
  async sources(context) { await this.authorizeProject(context); return this.runtime.require(plugin_sdk_1.AnalyticsRuntimeCapability).list(actor(context)); }
38
50
  async discover(context, input) { await this.authorizeProject(context); return this.runtime.require(plugin_sdk_1.AnalyticsRuntimeCapability).discover(actor(context), input); }
@@ -100,7 +112,31 @@ let DashboardService = class DashboardService {
100
112
  await this.runtime.require(plugin_sdk_1.ArtifactDraftsRuntimeCapability).setRecycled({ artifactId, expectedRevision, recycled: true });
101
113
  return { artifactId };
102
114
  }
103
- async save(context, manifest, expectedVersionId, expectedRevision = 0, importedDependencies) {
115
+ async previousBindingCount(context, manifest) {
116
+ await this.authorizeProject(context, 'write');
117
+ const existing = await this.runtime.require(plugin_sdk_1.ArtifactsRuntimeCapability).findArtifactBySource({ pluginName: exports.PLUGIN_NAME, resourceType: RESOURCE_TYPE, resourceId: `${hostPrefix(context)}${manifest.id}` });
118
+ if (!existing)
119
+ return 0;
120
+ assertOwned(context, existing);
121
+ const draft = (await this.runtime.require(plugin_sdk_1.ArtifactDraftsRuntimeCapability).list([existing.id]))[0];
122
+ return existing.currentVersionId || draft ? (await this.open(context, existing.id)).manifest.bindings.length : 0;
123
+ }
124
+ /** Read-only reconciliation of a completed CAS after a worker/status-store interruption. */
125
+ async savedRequestReceipt(context, manifestId, requestId, expectedRevision) {
126
+ await this.authorizeProject(context, 'write');
127
+ const artifact = await this.runtime.require(plugin_sdk_1.ArtifactsRuntimeCapability).findArtifactBySource({ pluginName: exports.PLUGIN_NAME, resourceType: RESOURCE_TYPE, resourceId: `${hostPrefix(context)}${manifestId}` });
128
+ if (!artifact)
129
+ return null;
130
+ assertOwned(context, artifact);
131
+ if (artifact.status !== 'active')
132
+ throw new common_1.ForbiddenException('DASHBOARD_ARCHIVED');
133
+ const draft = await this.runtime.require(plugin_sdk_1.ArtifactDraftsRuntimeCapability).get(artifact.id);
134
+ if (draft?.metadata?.saveRequestId !== requestId || draft.revision !== expectedRevision + 1)
135
+ return null;
136
+ return { artifactId: artifact.id, draftId: draft.id, draftRevision: draft.revision, baseVersionId: draft.baseVersionId, status: 'draft', title: draft.title ?? artifact.title ?? '' };
137
+ }
138
+ async save(context, manifest, expectedVersionId, expectedRevision = 0, importedDependencies, task) {
139
+ await task?.progress('authorization', 0, manifest.bindings.length);
104
140
  await this.authorizeProject(context, 'write');
105
141
  const scopedActor = actor(context);
106
142
  const parsed = dashboard_schema_1.dashboardSchema.parse(manifest);
@@ -115,6 +151,7 @@ let DashboardService = class DashboardService {
115
151
  }
116
152
  // A failed first upload can leave an Artifact with no saved content; revision 0 initializes it.
117
153
  const previous = existing && (existing.currentVersionId || expectedRevision > 0) ? await this.openDocument(context, existing.id) : null;
154
+ this.assertCapacity(parsed.bindings.length, previous?.manifest.bindings.length ?? 0);
118
155
  if (importedDependencies && existing)
119
156
  throw new common_1.BadRequestException('DASHBOARD_IMPORT_NEW_ID_REQUIRED');
120
157
  const dependencies = importedDependencies ? (0, artifact_document_1.verifyArtifactDocumentDependencies)(importedDependencies, parsed.presentation?.dependencies ?? [])
@@ -123,21 +160,31 @@ let DashboardService = class DashboardService {
123
160
  if (buffer.byteLength > document_package_1.MAX_DASHBOARD_PACKAGE_BYTES)
124
161
  throw new common_1.BadRequestException('DASHBOARD_MANIFEST_TOO_LARGE');
125
162
  const digest = (0, node_crypto_1.createHash)('sha256').update(buffer).digest('hex');
163
+ if (existing && task) {
164
+ const draft = await this.runtime.require(plugin_sdk_1.ArtifactDraftsRuntimeCapability).get(existing.id);
165
+ if (draft?.metadata?.saveRequestId === task.requestId && draft.sha256 === digest && draft.revision === expectedRevision + 1)
166
+ return { artifactId: existing.id, draftId: draft.id, draftRevision: draft.revision, baseVersionId: draft.baseVersionId, status: 'draft', title: parsed.title };
167
+ }
126
168
  if ((existing?.currentVersionId ?? null) !== expectedVersionId)
127
169
  throw new common_1.ConflictException('DASHBOARD_VERSION_CONFLICT');
128
170
  // Recheck current data access even for an idempotent retry; the platform enforces the final CAS under a row lock.
129
- const preview = await this.execute(context, parsed, Object.fromEntries(parsed.controls.filter((control) => control.defaultValue !== undefined).map((control) => [control.controlId, control.defaultValue ?? null])));
171
+ const preview = await (0, dashboard_queries_1.queryDashboard)(this.runtime, actor(context), parsed, Object.fromEntries(parsed.controls.filter((control) => control.defaultValue !== undefined).map((control) => [control.controlId, control.defaultValue ?? null])), task ? completed => task.progress('queries', completed, parsed.bindings.length) : undefined);
130
172
  if (!parsed.presentation)
131
173
  (0, renderer_1.toDataAnalyticsPayload)(parsed, preview);
132
- for (const binding of parsed.bindings)
133
- await this.probeControls(context, binding, Object.fromEntries(parsed.controls.filter((control) => control.defaultValue !== undefined).map((control) => [control.controlId, control.defaultValue ?? null])));
174
+ let completedFilters = 0;
175
+ await (0, bounded_map_1.boundedMap)(parsed.bindings, async (binding) => {
176
+ await this.probeControls(context, binding, Object.fromEntries(parsed.controls.filter((control) => control.defaultValue !== undefined).map((control) => [control.controlId, control.defaultValue ?? null])), task ? () => task.progress('filters', completedFilters, parsed.bindings.length) : undefined);
177
+ await task?.progress('filters', ++completedFilters, parsed.bindings.length);
178
+ });
179
+ await task?.progress('html', 0, 1);
134
180
  const htmlValidation = await this.htmlValidation.validate(context, parsed, preview, dependencies);
181
+ await task?.progress('save', 0, 1);
135
182
  const artifact = existing ?? await artifacts.createArtifact({ source, type: { pluginName: exports.PLUGIN_NAME, resourceType: RESOURCE_TYPE }, kind: 'file', title: parsed.title,
136
183
  scope: { ...scopedActor, ...(projectId(context) ? { projectId: projectId(context), userId: null } : { xpertId: context.hostId }) } });
137
184
  const reference = await this.runtime.require(plugin_sdk_1.ArtifactContentRuntimeCapability).write({ artifactId: artifact.id, buffer });
138
185
  const result = await this.runtime.require(plugin_sdk_1.ArtifactDraftsRuntimeCapability).save({ artifactId: artifact.id, expectedRevision,
139
186
  expectedCurrentVersionId: expectedVersionId, mimeType: MIME_TYPE, title: parsed.title, fileName: 'dashboard.json', sha256: digest,
140
- ...(htmlValidation ? { metadata: { htmlValidation } } : {}), workspaceFileRef: reference });
187
+ ...((htmlValidation || task) ? { metadata: { ...(htmlValidation ? { htmlValidation } : {}), ...(task ? { saveRequestId: task.requestId } : {}) } } : {}), workspaceFileRef: reference });
141
188
  return { artifactId: artifact.id, draftId: result.id, draftRevision: result.revision, baseVersionId: result.baseVersionId, status: 'draft', title: parsed.title };
142
189
  }
143
190
  async open(context, artifactId, versionId, draftRevision, mode = 'draft') {
@@ -242,7 +289,7 @@ let DashboardService = class DashboardService {
242
289
  execute(context, manifest, input) {
243
290
  return (0, dashboard_queries_1.queryDashboard)(this.runtime, actor(context), manifest, input);
244
291
  }
245
- async probeControls(context, binding, input) {
292
+ async probeControls(context, binding, input, checkpoint) {
246
293
  const controls = Object.fromEntries(binding.parameters.map((parameter) => [parameter.controlId,
247
294
  Object.hasOwn(input, parameter.controlId) ? input[parameter.controlId] : parameter.defaultValue ?? null]));
248
295
  // A null default can bypass a native predicate entirely. Verify a real selection too,
@@ -253,6 +300,7 @@ let DashboardService = class DashboardService {
253
300
  const samples = [...(alternative !== undefined ? [alternative] : []),
254
301
  ...(current !== null && parameter.allowedValues?.includes(null) ? [null] : [])];
255
302
  for (const value of samples) {
303
+ await checkpoint?.();
256
304
  // Analytics already redacts provider failures. Preserve its actionable validation
257
305
  // errors: a duplicate grain is not a malformed filter and needs a different repair.
258
306
  await this.runtime.require(plugin_sdk_1.AnalyticsRuntimeCapability).query(actor(context), { binding, controls: { ...controls, [parameter.controlId]: value } });
@@ -264,7 +312,9 @@ exports.DashboardService = DashboardService;
264
312
  exports.DashboardService = DashboardService = __decorate([
265
313
  (0, common_1.Injectable)(),
266
314
  __param(0, (0, common_1.Inject)(plugin_sdk_1.XPERT_RUNTIME_CAPABILITIES_TOKEN)),
267
- __metadata("design:paramtypes", [Object, html_validation_1.HtmlDashboardValidation])
315
+ __param(2, (0, common_1.Optional)()),
316
+ __param(2, (0, common_1.Inject)(capacity_1.DASHBOARD_CONFIG)),
317
+ __metadata("design:paramtypes", [Object, html_validation_1.HtmlDashboardValidation, Object])
268
318
  ], DashboardService);
269
319
  function actor(context) {
270
320
  if (!context.tenantId || !context.organizationId || !context.userId || !['agent', 'project'].includes(context.hostType))
@@ -31,26 +31,26 @@ export declare function createDefinitionTools(context: IAgentMiddlewareContext,
31
31
  definitionJson: z.ZodString;
32
32
  }, "strict", z.ZodTypeAny, {
33
33
  name: string;
34
+ requestId: string;
34
35
  providerId: string;
35
36
  connectionKey: string;
36
- requestId: string;
37
37
  definitionJson: string;
38
38
  }, {
39
39
  name: string;
40
+ requestId: string;
40
41
  providerId: string;
41
42
  connectionKey: string;
42
- requestId: string;
43
43
  definitionJson: string;
44
44
  }>, {
45
45
  name: string;
46
+ requestId: string;
46
47
  providerId: string;
47
48
  connectionKey: string;
48
- requestId: string;
49
49
  definitionJson: string;
50
50
  }, {
51
51
  name: string;
52
+ requestId: string;
52
53
  providerId: string;
53
54
  connectionKey: string;
54
- requestId: string;
55
55
  definitionJson: string;
56
56
  }, string>)[];
@@ -6,7 +6,7 @@ const artifact_document_1 = require("@xpert-ai/plugin-sdk/artifact-document");
6
6
  const document_network_1 = require("./document-network");
7
7
  /** The HTML never supplies or overrides its network permissions. */
8
8
  function mountDashboardDocument(frame, options) {
9
- return (0, artifact_document_1.mountArtifactDocument)(frame, { ...options, networkPolicy: document_network_1.DASHBOARD_NETWORK_POLICY });
9
+ return (0, artifact_document_1.mountArtifactDocument)(frame, { ...options, dispatchesPerMinute: 60, networkPolicy: document_network_1.DASHBOARD_NETWORK_POLICY });
10
10
  }
11
11
  function isDashboardResourceAllowed(resourceType, url) {
12
12
  return (0, artifact_document_1.isArtifactDocumentResourceAllowed)(document_network_1.DASHBOARD_NETWORK_POLICY, resourceType, url);
@@ -17,8 +17,8 @@ export declare function decodeDashboardPackage(bytes: Buffer): {
17
17
  allowedValues?: (string | number | boolean | null)[] | undefined;
18
18
  }[];
19
19
  charts: {
20
- title: string;
21
20
  type: "bar" | "line" | "area" | "pie" | "scatter";
21
+ title: string;
22
22
  id: string;
23
23
  datasetId: string;
24
24
  x: string;
@@ -4,10 +4,11 @@ exports.removalsSchema = void 0;
4
4
  exports.upsertComponents = upsertComponents;
5
5
  exports.htmlElementIds = htmlElementIds;
6
6
  exports.assertHtmlPreserved = assertHtmlPreserved;
7
+ const capacity_1 = require("./capacity");
7
8
  const parse5_1 = require("parse5");
8
9
  const v3_1 = require("zod/v3");
9
10
  exports.removalsSchema = v3_1.z.object({
10
- bindingIds: v3_1.z.array(v3_1.z.string()).max(8).default([]), controlIds: v3_1.z.array(v3_1.z.string()).max(24).default([]),
11
+ bindingIds: v3_1.z.array(v3_1.z.string()).max(capacity_1.MAX_DASHBOARD_BINDINGS).default([]), controlIds: v3_1.z.array(v3_1.z.string()).max(24).default([]),
11
12
  viewIds: v3_1.z.array(v3_1.z.string()).max(40).default([]), htmlElementIds: v3_1.z.array(v3_1.z.string()).max(500).default([]),
12
13
  componentIds: v3_1.z.array(v3_1.z.string()).max(160).default([])
13
14
  }).strict().optional().describe('Only explicitly requested removals. Arrays of changed components otherwise upsert by stable ID and preserve omitted components.');
@@ -1,3 +1,4 @@
1
+ import type { DashboardSaveRequests } from './save-requests';
1
2
  import { z } from 'zod/v3';
2
3
  import type { AnalyticsBinding, XpertResolvedViewHostContext } from '@xpert-ai/contracts';
3
4
  import { type IAgentMiddlewareContext } from '@xpert-ai/plugin-sdk';
@@ -101,24 +102,24 @@ export declare const updateToolSchema: z.ZodObject<{
101
102
  y: z.ZodString;
102
103
  color: z.ZodOptional<z.ZodString>;
103
104
  }, "strict", z.ZodTypeAny, {
104
- title: string;
105
105
  type: "bar" | "line" | "area" | "pie" | "scatter";
106
+ title: string;
106
107
  id: string;
107
108
  datasetId: string;
108
109
  x: string;
109
110
  y: string;
110
111
  color?: string | undefined;
111
112
  }, {
112
- title: string;
113
113
  type: "bar" | "line" | "area" | "pie" | "scatter";
114
+ title: string;
114
115
  id: string;
115
116
  datasetId: string;
116
117
  x: string;
117
118
  y: string;
118
119
  color?: string | undefined;
119
120
  }>, "many">, {
120
- title: string;
121
121
  type: "bar" | "line" | "area" | "pie" | "scatter";
122
+ title: string;
122
123
  id: string;
123
124
  datasetId: string;
124
125
  x: string;
@@ -202,8 +203,8 @@ export declare const updateToolSchema: z.ZodObject<{
202
203
  allowedValues?: (string | number | boolean | null)[] | undefined;
203
204
  }[] | undefined;
204
205
  charts?: {
205
- title: string;
206
206
  type: "bar" | "line" | "area" | "pie" | "scatter";
207
+ title: string;
207
208
  id: string;
208
209
  datasetId: string;
209
210
  x: string;
@@ -251,7 +252,7 @@ export declare const updateToolSchema: z.ZodObject<{
251
252
  }>;
252
253
  export declare const EDITING_GUIDANCE = "Opening or reading a dashboard does not modify it. A selection_required result is normal: show its message to the user and wait for an explicit selection; do not retry, infer a target from history, or create a replacement. Do not save an unchanged editing copy or reopen/update merely to confirm a successful save; use the mutation receipt. For existing dashboards, call dashboard_open first and inspect presentation.type. For standard dashboards, update definition fields directly: a rename needs only title, editId, expectedRevision and expectedCurrentVersionId. No HTML file exists; omit htmlPath and libraries. For HTML dashboards only, open prepares a unique editing copy at htmlPath. Read with sandbox_read_file. If a long HTML line is truncated, do not repeat the same read: use dashboard_read_html with editId and follow nextOffset until null to inspect the saved source, then apply exact replacements to htmlPath. Never replace a file with a truncated preview. Edit with sandbox_edit_file/multi_edit_file (sandbox_write_file is create-only), then call dashboard_update with that exact htmlPath and read revisions. If the user requests a rename, include the new title in this same update; editing the HTML heading alone does not rename the Artifact. Save each batch automatically without human confirmation. Files are not dashboard saves. Preserve Binding semantics, stable component IDs and dependency locks unless requested. To add filters to an existing Binding, copy its complete dashboard_open.bindings[] entry, retain binding.id and existing parameters[].controlId, and call dashboard_preview_binding after changing payload/parameters/defaults. Binding ID is independent of output.datasetId; simplified dashboard_preview derives the ID from datasetId and can accidentally add a new Binding instead of updating the old one. Use the stable Binding ID in HTML query/subscribe calls. Submit every intended changed/new Binding previewId and matching controls with htmlPath in the same dashboard_update. Update arrays upsert by ID, preserving omitted components; removals must name explicitly requested deletions. On preflight failure, compare expected/requested Binding IDs, repair the HTML or previews, and retain all intended previewIds, controls, title and htmlPath when retrying. Dropping previewIds does not register new HTML queries. Do not remove an existing Binding merely to suppress validation. HTML updates retain existing element IDs. For new HTML, assign stable IDs to every KPI, chart, detail table and filter. Read and patch the current file instead of rebuilding unrelated sections. On conflict retain the editing file, open the latest draft and reconcile; never silently overwrite. Historical versions are read-only references. All Agent saves are drafts; only the Workbench human publish action creates formal versions. Read the live-dashboard skill for the complete workflow.";
253
254
  /** Run-local read receipts carry planning versions; authorization and CAS remain in the domain service. */
254
- export declare function createEditingTools(service: DashboardService, scope: XpertResolvedViewHostContext, context: IAgentMiddlewareContext, resolvePreviews: (ids: string[]) => AnalyticsBinding[], validateFinal: (manifest: DashboardManifest, requirePreviews: boolean, changedBindingIds?: string[]) => void, onOpen?: (path: string, artifactId: string) => void, currentTarget?: (config: unknown) => DashboardSelection | null | undefined): (import("@langchain/core/tools").DynamicStructuredTool<z.ZodObject<{
255
+ export declare function createEditingTools(service: DashboardService, scope: XpertResolvedViewHostContext, context: IAgentMiddlewareContext, resolvePreviews: (ids: string[]) => AnalyticsBinding[], validateFinal: (manifest: DashboardManifest, requirePreviews: boolean, changedBindingIds?: string[]) => void, onOpen?: (path: string, artifactId: string) => void, currentTarget?: (config: unknown) => DashboardSelection | null | undefined, saveRequests?: DashboardSaveRequests): (import("@langchain/core/tools").DynamicStructuredTool<z.ZodObject<{
255
256
  page: z.ZodDefault<z.ZodNumber>;
256
257
  }, "strict", z.ZodTypeAny, {
257
258
  page: number;
@@ -394,24 +395,24 @@ export declare function createEditingTools(service: DashboardService, scope: Xpe
394
395
  y: z.ZodString;
395
396
  color: z.ZodOptional<z.ZodString>;
396
397
  }, "strict", z.ZodTypeAny, {
397
- title: string;
398
398
  type: "bar" | "line" | "area" | "pie" | "scatter";
399
+ title: string;
399
400
  id: string;
400
401
  datasetId: string;
401
402
  x: string;
402
403
  y: string;
403
404
  color?: string | undefined;
404
405
  }, {
405
- title: string;
406
406
  type: "bar" | "line" | "area" | "pie" | "scatter";
407
+ title: string;
407
408
  id: string;
408
409
  datasetId: string;
409
410
  x: string;
410
411
  y: string;
411
412
  color?: string | undefined;
412
413
  }>, "many">, {
413
- title: string;
414
414
  type: "bar" | "line" | "area" | "pie" | "scatter";
415
+ title: string;
415
416
  id: string;
416
417
  datasetId: string;
417
418
  x: string;
@@ -495,8 +496,8 @@ export declare function createEditingTools(service: DashboardService, scope: Xpe
495
496
  allowedValues?: (string | number | boolean | null)[] | undefined;
496
497
  }[] | undefined;
497
498
  charts?: {
498
- title: string;
499
499
  type: "bar" | "line" | "area" | "pie" | "scatter";
500
+ title: string;
500
501
  id: string;
501
502
  datasetId: string;
502
503
  x: string;
@@ -569,8 +570,8 @@ export declare function createEditingTools(service: DashboardService, scope: Xpe
569
570
  allowedValues?: (string | number | boolean | null)[] | undefined;
570
571
  }[] | undefined;
571
572
  charts?: {
572
- title: string;
573
573
  type: "bar" | "line" | "area" | "pie" | "scatter";
574
+ title: string;
574
575
  id: string;
575
576
  datasetId: string;
576
577
  x: string;
@@ -25,7 +25,7 @@ const readHtmlSchema = v3_1.z.object({ editId: v3_1.z.string().uuid(), versionId
25
25
  length: v3_1.z.number().int().min(1).max(24000).default(16000) }).strict();
26
26
  exports.EDITING_GUIDANCE = `Opening or reading a dashboard does not modify it. A selection_required result is normal: show its message to the user and wait for an explicit selection; do not retry, infer a target from history, or create a replacement. Do not save an unchanged editing copy or reopen/update merely to confirm a successful save; use the mutation receipt. For existing dashboards, call dashboard_open first and inspect presentation.type. For standard dashboards, update definition fields directly: a rename needs only title, editId, expectedRevision and expectedCurrentVersionId. No HTML file exists; omit htmlPath and libraries. For HTML dashboards only, open prepares a unique editing copy at htmlPath. Read with sandbox_read_file. If a long HTML line is truncated, do not repeat the same read: use dashboard_read_html with editId and follow nextOffset until null to inspect the saved source, then apply exact replacements to htmlPath. Never replace a file with a truncated preview. Edit with sandbox_edit_file/multi_edit_file (sandbox_write_file is create-only), then call dashboard_update with that exact htmlPath and read revisions. If the user requests a rename, include the new title in this same update; editing the HTML heading alone does not rename the Artifact. Save each batch automatically without human confirmation. Files are not dashboard saves. Preserve Binding semantics, stable component IDs and dependency locks unless requested. To add filters to an existing Binding, copy its complete dashboard_open.bindings[] entry, retain binding.id and existing parameters[].controlId, and call dashboard_preview_binding after changing payload/parameters/defaults. Binding ID is independent of output.datasetId; simplified dashboard_preview derives the ID from datasetId and can accidentally add a new Binding instead of updating the old one. Use the stable Binding ID in HTML query/subscribe calls. Submit every intended changed/new Binding previewId and matching controls with htmlPath in the same dashboard_update. Update arrays upsert by ID, preserving omitted components; removals must name explicitly requested deletions. On preflight failure, compare expected/requested Binding IDs, repair the HTML or previews, and retain all intended previewIds, controls, title and htmlPath when retrying. Dropping previewIds does not register new HTML queries. Do not remove an existing Binding merely to suppress validation. HTML updates retain existing element IDs. For new HTML, assign stable IDs to every KPI, chart, detail table and filter. Read and patch the current file instead of rebuilding unrelated sections. On conflict retain the editing file, open the latest draft and reconcile; never silently overwrite. Historical versions are read-only references. All Agent saves are drafts; only the Workbench human publish action creates formal versions. Read the live-dashboard skill for the complete workflow.`;
27
27
  /** Run-local read receipts carry planning versions; authorization and CAS remain in the domain service. */
28
- function createEditingTools(service, scope, context, resolvePreviews, validateFinal, onOpen = () => undefined, currentTarget = editing_session_1.selectionFromConfig) {
28
+ function createEditingTools(service, scope, context, resolvePreviews, validateFinal, onOpen = () => undefined, currentTarget = editing_session_1.selectionFromConfig, saveRequests) {
29
29
  const target = (input, config) => {
30
30
  const selected = currentTarget(config);
31
31
  if (input.artifactId)
@@ -119,6 +119,10 @@ function createEditingTools(service, scope, context, resolvePreviews, validateFi
119
119
  ...(sourceMetadata.length || saved.manifest.sourceMetadata ? { sourceMetadata } : {}), ...(components.length || saved.manifest.components ? { components } : {}),
120
120
  ...(presentation ? { presentation } : {}) });
121
121
  validateFinal(manifest, previewIds !== undefined, changedBindings?.map(item => item.id));
122
+ if (saveRequests && manifest.bindings.length > 8) {
123
+ const key = (0, node_crypto_1.createHash)('sha256').update(JSON.stringify([editId, expectedRevision, expectedCurrentVersionId, manifest, sourceFile])).digest('hex');
124
+ return JSON.stringify(await saveRequests.submit(scope, manifest, expectedCurrentVersionId, expectedRevision, key, sourceFile));
125
+ }
122
126
  const { schema, id, ...update } = manifest;
123
127
  const result = await service.update(scope, editId, expectedRevision, update, expectedCurrentVersionId);
124
128
  const resourceCard = await (0, resource_card_1.emitDashboardCard)(scope, { ...result, state: 'draft', revision: result.draftRevision }, config);
@@ -1,6 +1,34 @@
1
1
  import { z } from 'zod/v3';
2
2
  import type { AnalyticsBinding, AnalyticsValue, XpertResolvedViewHostContext } from '@xpert-ai/contracts';
3
3
  import type { DashboardService } from './dashboard.service';
4
+ export declare const filterTestSchema: z.ZodObject<{
5
+ previewId: z.ZodString;
6
+ controls: z.ZodEffects<z.ZodRecord<z.ZodString, z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>>, Record<string, string | number | boolean | null>, unknown>;
7
+ expectations: z.ZodEffects<z.ZodArray<z.ZodObject<{
8
+ fieldId: z.ZodString;
9
+ oneOf: z.ZodArray<z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>, "many">;
10
+ }, "strict", z.ZodTypeAny, {
11
+ fieldId: string;
12
+ oneOf: (string | number | boolean | null)[];
13
+ }, {
14
+ fieldId: string;
15
+ oneOf: (string | number | boolean | null)[];
16
+ }>, "many">, {
17
+ fieldId: string;
18
+ oneOf: (string | number | boolean | null)[];
19
+ }[], unknown>;
20
+ }, "strict", z.ZodTypeAny, {
21
+ controls: Record<string, string | number | boolean | null>;
22
+ previewId: string;
23
+ expectations: {
24
+ fieldId: string;
25
+ oneOf: (string | number | boolean | null)[];
26
+ }[];
27
+ }, {
28
+ previewId: string;
29
+ controls?: unknown;
30
+ expectations?: unknown;
31
+ }>;
4
32
  /** Model quality evidence, not authorization: every query still goes through Analytics. */
5
33
  export declare function createFilterEvidence(service: DashboardService, scope: XpertResolvedViewHostContext, previews: Map<string, AnalyticsBinding>): {
6
34
  requirements(binding: AnalyticsBinding, previewId: string, rows: Record<string, AnalyticsValue>[]): {