@xpert-ai/plugin-live-dashboard 0.1.4 → 0.2.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 (49) hide show
  1. package/.xpertai-plugin/plugin.json +1 -1
  2. package/README.md +4 -4
  3. package/dist/lib/content-capability.d.ts +22 -0
  4. package/dist/lib/content-capability.js +33 -0
  5. package/dist/lib/content.provider.d.ts +23 -0
  6. package/dist/lib/content.provider.js +163 -0
  7. package/dist/lib/dashboard-navigation.d.ts +24 -0
  8. package/dist/lib/dashboard-navigation.js +1 -1
  9. package/dist/lib/dashboard-queries.d.ts +11 -0
  10. package/dist/lib/dashboard-queries.js +20 -0
  11. package/dist/lib/dashboard-search.d.ts +5 -0
  12. package/dist/lib/dashboard-search.js +12 -0
  13. package/dist/lib/dashboard.schema.js +4 -2
  14. package/dist/lib/dashboard.service.d.ts +8 -0
  15. package/dist/lib/dashboard.service.js +52 -15
  16. package/dist/lib/document-host.d.ts +4 -0
  17. package/dist/lib/document-host.js +13 -0
  18. package/dist/lib/document-network.d.ts +3 -0
  19. package/dist/lib/document-network.js +9 -0
  20. package/dist/lib/html-guidance.d.ts +1 -1
  21. package/dist/lib/html-guidance.js +1 -1
  22. package/dist/lib/middleware.js +47 -15
  23. package/dist/lib/plugin.js +3 -1
  24. package/dist/lib/presentation.provider.d.ts +5 -0
  25. package/dist/lib/presentation.provider.js +2 -2
  26. package/dist/lib/preview-retry.d.ts +7 -0
  27. package/dist/lib/preview-retry.js +52 -0
  28. package/dist/lib/renderer.js +5 -0
  29. package/dist/lib/save-evidence.d.ts +1 -1
  30. package/dist/lib/sharing.service.js +2 -0
  31. package/dist/lib/view.provider.d.ts +5 -0
  32. package/dist/lib/view.provider.js +4 -4
  33. package/dist/remote/app.css +1 -1
  34. package/dist/remote/app.js +36 -36
  35. package/dist/remote/build-manifest.json +34 -25
  36. package/dist/sandbox-actions/action.json +1 -1
  37. package/dist/sandbox-actions/bundle/host.js +2 -2
  38. package/dist/sandbox-actions/bundle/validate.mjs +24 -3
  39. package/dist/validation/host-entry.d.ts +3 -1
  40. package/dist/validation/host-entry.js +5 -2
  41. package/docs/drafts.mdx +1 -1
  42. package/docs/frontend-libraries.mdx +2 -2
  43. package/docs/html-mode.md +3 -3
  44. package/docs/team-sharing.mdx +1 -1
  45. package/docs/verification.mdx +3 -1
  46. package/package.json +6 -6
  47. package/skills/live-dashboard/SKILL.md +1 -1
  48. package/skills/live-dashboard/references/data-validation.md +4 -0
  49. package/skills/live-dashboard/references/html-runtime.md +1 -1
@@ -13,6 +13,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.DashboardService = exports.PLUGIN_NAME = void 0;
16
+ exports.projectId = projectId;
16
17
  const common_1 = require("@nestjs/common");
17
18
  const node_crypto_1 = require("node:crypto");
18
19
  const plugin_sdk_1 = require("@xpert-ai/plugin-sdk");
@@ -39,20 +40,24 @@ let DashboardService = class DashboardService {
39
40
  return dataset;
40
41
  }
41
42
  async list(context) {
42
- actor(context);
43
+ await this.authorizeProject(context);
43
44
  const result = await this.runtime.require(plugin_sdk_1.ArtifactsRuntimeCapability).listArtifacts({ pluginName: exports.PLUGIN_NAME, resourceType: RESOURCE_TYPE, resourceIdPrefix: hostPrefix(context), status: 'active', pageSize: 100, includeCurrentVersion: true });
44
- const items = result.items.filter((item) => item.resourceId.startsWith(hostPrefix(context)) && item.userId === context.userId);
45
+ const items = result.items.filter((item) => item.resourceId.startsWith(hostPrefix(context)) && (item.userId === context.userId || !item.userId && item.projectId === projectId(context)));
45
46
  const drafts = new Map((await this.runtime.require(plugin_sdk_1.ArtifactDraftsRuntimeCapability).list(items.map(item => item.id))).map(draft => [draft.artifactId, draft]));
46
47
  return items.map(item => ({ ...summary(item), ...(drafts.has(item.id) ? { title: drafts.get(item.id).title ?? item.title } : {}) }));
47
48
  }
48
49
  async save(context, manifest, expectedVersionId, expectedRevision = 0) {
50
+ await this.authorizeProject(context, 'write');
49
51
  const scopedActor = actor(context);
50
52
  const parsed = dashboard_schema_1.dashboardSchema.parse(manifest);
51
53
  const artifacts = this.runtime.require(plugin_sdk_1.ArtifactsRuntimeCapability);
52
54
  const source = { pluginName: exports.PLUGIN_NAME, resourceType: RESOURCE_TYPE, resourceId: `${hostPrefix(context)}${parsed.id}` };
53
55
  const existing = await artifacts.findArtifactBySource(source);
54
- if (existing && existing.userId !== scopedActor.userId)
55
- throw new common_1.ForbiddenException('DASHBOARD_ACCESS_DENIED');
56
+ if (existing) {
57
+ assertOwned(context, existing);
58
+ if (existing.status !== 'active')
59
+ throw new common_1.ForbiddenException('DASHBOARD_ARCHIVED');
60
+ }
56
61
  // A failed first upload can leave an Artifact with no saved content; revision 0 initializes it.
57
62
  const previous = existing && (existing.currentVersionId || expectedRevision > 0) ? await this.openDocument(context, existing.id) : null;
58
63
  const dependencies = await (0, document_libraries_1.resolveLibraryAssets)(parsed.presentation?.dependencies ?? [], previous?.dependencies);
@@ -70,14 +75,22 @@ let DashboardService = class DashboardService {
70
75
  await this.probeControls(context, binding, Object.fromEntries(parsed.controls.filter((control) => control.defaultValue !== undefined).map((control) => [control.controlId, control.defaultValue ?? null])));
71
76
  const htmlValidation = await this.htmlValidation.validate(context, parsed, preview, dependencies);
72
77
  const artifact = existing ?? await artifacts.createArtifact({ source, kind: 'file', title: parsed.title,
73
- scope: { ...scopedActor, ...(context.hostType === 'project' ? { projectId: context.hostId } : { xpertId: context.hostId }) } });
78
+ scope: { ...scopedActor, ...(projectId(context) ? { projectId: projectId(context), userId: null } : { xpertId: context.hostId }) } });
74
79
  const files = this.runtime.require(plugin_sdk_1.WorkspaceFilesRuntimeCapability);
75
- const scope = { tenantId: scopedActor.tenantId, userId: scopedActor.userId, catalog: 'users' };
76
- const file = await files.uploadBuffer({ ...scope, buffer, originalName: 'dashboard.json', fileName: `${digest}.json`, folder: `live-dashboards/${parsed.id}`, mimeType: MIME_TYPE });
80
+ const project = projectId(context);
81
+ // Shared definitions live in the Project volume; private legacy dashboards retain their user volume.
82
+ const privateScope = { tenantId: scopedActor.tenantId, userId: scopedActor.userId, catalog: 'users' };
83
+ const reference = project
84
+ ? (await files.writeRuntimeBuffer({ tenantId: scopedActor.tenantId, userId: scopedActor.userId,
85
+ catalog: 'projects', scopeId: project, projectId: project, buffer, originalName: 'dashboard.json',
86
+ fileName: `${digest}.json`, folder: `.xpert/live-dashboards/${parsed.id}`, mimeType: MIME_TYPE })).reference
87
+ : await files.uploadBuffer({ ...privateScope, buffer, originalName: 'dashboard.json', fileName: `${digest}.json`,
88
+ folder: `live-dashboards/${parsed.id}`, mimeType: MIME_TYPE }).then(file => ({
89
+ source: 'platform.workspace.files', ...privateScope, filePath: file.filePath, workspacePath: file.workspacePath
90
+ }));
77
91
  const result = await this.runtime.require(plugin_sdk_1.ArtifactDraftsRuntimeCapability).save({ artifactId: artifact.id, expectedRevision,
78
92
  expectedCurrentVersionId: expectedVersionId, mimeType: MIME_TYPE, title: parsed.title, fileName: 'dashboard.json', sha256: digest,
79
- ...(htmlValidation ? { metadata: { htmlValidation } } : {}),
80
- workspaceFileRef: { source: 'platform.workspace.files', ...scope, filePath: file.filePath, workspacePath: file.workspacePath } });
93
+ ...(htmlValidation ? { metadata: { htmlValidation } } : {}), workspaceFileRef: reference });
81
94
  return { artifactId: artifact.id, draftId: result.id, draftRevision: result.revision, baseVersionId: result.baseVersionId, status: 'draft', title: parsed.title };
82
95
  }
83
96
  async open(context, artifactId, versionId, draftRevision) {
@@ -86,10 +99,12 @@ let DashboardService = class DashboardService {
86
99
  }
87
100
  /** Trusted View transport only. Agent read tools receive locks, never megabytes of library code. */
88
101
  async openDocument(context, artifactId, versionId, draftRevision) {
89
- actor(context);
102
+ await this.authorizeProject(context);
90
103
  const artifacts = this.runtime.require(plugin_sdk_1.ArtifactsRuntimeCapability);
91
104
  const artifact = await artifacts.getArtifact(artifactId);
92
105
  assertOwned(context, artifact);
106
+ if (artifact.status !== 'active')
107
+ throw new common_1.ForbiddenException('DASHBOARD_ARCHIVED');
93
108
  const draft = !versionId || draftRevision !== undefined ? await this.runtime.require(plugin_sdk_1.ArtifactDraftsRuntimeCapability).get(artifactId) : null;
94
109
  if (draftRevision !== undefined && draft?.revision !== draftRevision)
95
110
  throw new common_1.ConflictException('DASHBOARD_DRAFT_CONFLICT');
@@ -109,10 +124,12 @@ let DashboardService = class DashboardService {
109
124
  isDraft: draft.revision !== draft.publishedRevision, title: draft.title, updatedAt: draft.updatedAt } : {}), manifest, dependencies };
110
125
  }
111
126
  async history(context, artifactId) {
112
- actor(context);
127
+ await this.authorizeProject(context);
113
128
  const artifacts = this.runtime.require(plugin_sdk_1.ArtifactsRuntimeCapability);
114
129
  const artifact = await artifacts.getArtifact(artifactId);
115
130
  assertOwned(context, artifact);
131
+ if (artifact.status !== 'active')
132
+ throw new common_1.ForbiddenException('DASHBOARD_ARCHIVED');
116
133
  const versions = await artifacts.listArtifactVersions({ artifactId, status: 'active' });
117
134
  return versions.map(version => ({ ...summary(artifact, version), current: version.id === artifact.currentVersionId }))
118
135
  .sort((a, b) => (b.versionNumber ?? 0) - (a.versionNumber ?? 0));
@@ -124,7 +141,10 @@ let DashboardService = class DashboardService {
124
141
  return this.save(context, dashboard_schema_1.dashboardSchema.parse({ ...saved.manifest, ...dashboard_schema_1.dashboardChangesSchema.parse(changes) }), saved.baseVersionId ?? saved.versionId ?? null, expectedRevision);
125
142
  }
126
143
  async publish(context, artifactId, expectedRevision, expectedCurrentVersionId) {
144
+ await this.authorizeProject(context, 'manage');
127
145
  const saved = await this.open(context, artifactId, undefined, expectedRevision);
146
+ if (!saved.manifest.bindings.length)
147
+ throw new common_1.BadRequestException('DASHBOARD_CONFIGURE_BEFORE_PUBLISHING');
128
148
  const share = await this.runtime.require(plugin_sdk_1.ArtifactsRuntimeCapability).getArtifactShare({ artifactId, shareKey: dashboard_sharing_1.DASHBOARD_SHARE_KEY });
129
149
  if (share?.status === 'active' && share.versionMode === 'latest' && (!share.expiresAt || new Date(share.expiresAt).getTime() > Date.now())) {
130
150
  const issues = (0, dashboard_sharing_1.dashboardSharingIssues)(saved.manifest, await this.sources(context));
@@ -138,13 +158,29 @@ let DashboardService = class DashboardService {
138
158
  }
139
159
  async query(context, artifactId, versionId, controls, draftRevision) {
140
160
  const saved = await this.open(context, artifactId, versionId, draftRevision);
141
- const datasets = await this.execute(context, saved.manifest, controls);
142
- return { ...saved, datasets, ...(saved.manifest.presentation ? {} : { payload: (0, renderer_1.toDataAnalyticsPayload)(saved.manifest, datasets) }) };
161
+ const { datasets, failures } = await (0, dashboard_queries_1.queryDashboardPartial)(this.runtime, actor(context), saved.manifest, controls);
162
+ return { ...saved, datasets, failures, ...(saved.manifest.presentation ? {} : { payload: (0, renderer_1.toDataAnalyticsPayload)(saved.manifest, datasets) }) };
143
163
  }
144
164
  async queryBinding(context, artifactId, versionId, bindingId, input, draftRevision) {
145
165
  const saved = await this.open(context, artifactId, versionId, draftRevision);
146
166
  return (0, dashboard_queries_1.queryDashboardBinding)(this.runtime, actor(context), saved.manifest, bindingId, input);
147
167
  }
168
+ /** Every operation uses the current human's Project role; data queries still use actor(context). */
169
+ async authorizeProject(context, operation = 'read') {
170
+ const scopedActor = actor(context), project = projectId(context);
171
+ if (!project)
172
+ return;
173
+ const access = this.runtime.require(plugin_sdk_1.ProjectAccessRuntimeCapability);
174
+ const input = { actor: scopedActor, projectId: project };
175
+ if (context.hostType === 'agent')
176
+ await access.assertAssistant({ ...input, assistantId: context.hostId });
177
+ if (operation === 'write')
178
+ await access.assertEdit(input);
179
+ else if (operation === 'manage')
180
+ await access.assertManage(input);
181
+ else if (!(await access.listReadable({ actor: scopedActor, projectIds: [project] })).length)
182
+ throw new common_1.ForbiddenException('DASHBOARD_ACCESS_DENIED');
183
+ }
148
184
  execute(context, manifest, input) {
149
185
  return (0, dashboard_queries_1.queryDashboard)(this.runtime, actor(context), manifest, input);
150
186
  }
@@ -177,10 +213,11 @@ function actor(context) {
177
213
  throw new common_1.ForbiddenException('DASHBOARD_ACCESS_DENIED');
178
214
  return { tenantId: context.tenantId, organizationId: context.organizationId, userId: context.userId };
179
215
  }
180
- function hostPrefix(context) { return `${context.hostType}/${context.hostId}/`; }
216
+ function projectId(context) { return context.hostType === 'project' ? context.hostId : context.runtimeScope?.projectId; }
217
+ function hostPrefix(context) { return projectId(context) ? `project/${projectId(context)}/` : `${context.hostType}/${context.hostId}/`; }
181
218
  function assertOwned(context, artifact) {
182
219
  if (artifact.pluginName !== exports.PLUGIN_NAME || artifact.resourceType !== RESOURCE_TYPE || !artifact.resourceId.startsWith(hostPrefix(context)) ||
183
- artifact.tenantId !== context.tenantId || artifact.organizationId !== context.organizationId || artifact.userId !== context.userId)
220
+ artifact.tenantId !== context.tenantId || artifact.organizationId !== context.organizationId || (artifact.userId ? artifact.userId !== context.userId : !projectId(context) || artifact.projectId !== projectId(context)))
184
221
  throw new common_1.ForbiddenException('DASHBOARD_ACCESS_DENIED');
185
222
  }
186
223
  function summary(artifact, version = artifact.currentVersion) {
@@ -0,0 +1,4 @@
1
+ import { type ArtifactDocumentHostOptions } from '@xpert-ai/plugin-sdk/artifact-document';
2
+ /** The HTML never supplies or overrides its network permissions. */
3
+ export declare function mountDashboardDocument<T>(frame: HTMLIFrameElement, options: ArtifactDocumentHostOptions<T>): import("@xpert-ai/plugin-sdk/artifact-document").ArtifactDocumentHost;
4
+ export declare function isDashboardResourceAllowed(resourceType: string, url: string): boolean;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mountDashboardDocument = mountDashboardDocument;
4
+ exports.isDashboardResourceAllowed = isDashboardResourceAllowed;
5
+ const artifact_document_1 = require("@xpert-ai/plugin-sdk/artifact-document");
6
+ const document_network_1 = require("./document-network");
7
+ /** The HTML never supplies or overrides its network permissions. */
8
+ function mountDashboardDocument(frame, options) {
9
+ return (0, artifact_document_1.mountArtifactDocument)(frame, { ...options, networkPolicy: document_network_1.DASHBOARD_NETWORK_POLICY });
10
+ }
11
+ function isDashboardResourceAllowed(resourceType, url) {
12
+ return (0, artifact_document_1.isArtifactDocumentResourceAllowed)(document_network_1.DASHBOARD_NETWORK_POLICY, resourceType, url);
13
+ }
@@ -0,0 +1,3 @@
1
+ import type { ArtifactDocumentNetworkPolicy } from '@xpert-ai/plugin-sdk/artifact-document';
2
+ /** Trusted plugin policy, shared by the displayed document and browser preflight. */
3
+ export declare const DASHBOARD_NETWORK_POLICY: ArtifactDocumentNetworkPolicy;
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DASHBOARD_NETWORK_POLICY = void 0;
4
+ /** Trusted plugin policy, shared by the displayed document and browser preflight. */
5
+ exports.DASHBOARD_NETWORK_POLICY = {
6
+ scripts: ['https://cdn.tailwindcss.com', 'https://cdn.jsdelivr.net', 'https://cdnjs.cloudflare.com', 'https://unpkg.com'],
7
+ styles: ['https://cdn.jsdelivr.net', 'https://cdnjs.cloudflare.com', 'https://unpkg.com', 'https://fonts.googleapis.com'],
8
+ fonts: ['https://cdn.jsdelivr.net', 'https://cdnjs.cloudflare.com', 'https://unpkg.com', 'https://fonts.gstatic.com']
9
+ };
@@ -1 +1 @@
1
- export declare const HTML_DASHBOARD_GUIDANCE = "For free-layout HTML dashboards, read the installed live-dashboard skill's references/html-runtime.md through read_skill_file, relative to its SKILL.md path in the skill catalog. Create/edit the HTML with SandboxFile; then dashboard_create_html or dashboard_update validates and saves it. Do not use generic present_files to save or preview a live dashboard. Its preview belongs to the Live dashboards Workbench. ECharts/Three.js are available via dashboard_libraries; all data access remains declarative Binding queries through window.xpertLiveArtifact. The page MUST declare const live = window.xpertLiveArtifact in its script (live is not a host global), register live.subscribe(bindingId, render), await live.ready(), and invoke live.query(bindingId, typedControls) for initial data and filter changes. window.__datasets and window.__refresh are not platform APIs and are never populated by the host. Read the runtime reference and its html-lifecycle.md example before writing HTML; preserve that lifecycle while freely changing the layout. Use one complete Dataset for KPI, trend and details when they share the same metric and filters; do not create a redundant unfiltered total Binding. Never let filtered and unfiltered subscriptions both write the same KPI: a refresh would race and overwrite the selected-region total. Distinct global totals need distinct, clearly labeled elements. Native data selects require data-dashboard-control=\"<controlId>\"; purely visual selects use data-dashboard-presentation. A data filter must query, not only filter cached browser rows. bindSelect controls ONE binding only. For a shared dimension filter across multiple bindings, follow the skill custom-select example and query every affected binding with the same typed controls; matching control IDs do not broadcast automatically. bindSelect derives options from Dataset fields and is only for dimension filters. Fixed query enums (days, page size, granularity) need options from parameter.allowedValues and a typed change listener calling live.query; never use date rows as numeric day-count options.";
1
+ export declare const HTML_DASHBOARD_GUIDANCE = "For free-layout HTML dashboards, read the installed live-dashboard skill's references/html-runtime.md through read_skill_file, relative to its SKILL.md path in the skill catalog. Create/edit the HTML with SandboxFile; then dashboard_create_html or dashboard_update validates and saves it. Do not use generic present_files to save or preview a live dashboard. Its preview belongs to the Live dashboards Workbench. Call dashboard_libraries for packaged ECharts/Three.js and the allowed HTTPS origins for online scripts, stylesheets and fonts. Tailwind CDN is supported through script src; pin a version when possible. Online resource failures must be repaired before saving. Do not add online URLs to the packaged libraries array; all data access remains declarative Binding queries through window.xpertLiveArtifact. The page MUST declare const live = window.xpertLiveArtifact in its script (live is not a host global), register live.subscribe(bindingId, render), await live.ready(), and invoke live.query(bindingId, typedControls) for initial data and filter changes. window.__datasets and window.__refresh are not platform APIs and are never populated by the host. Read the runtime reference and its html-lifecycle.md example before writing HTML; preserve that lifecycle while freely changing the layout. Use one complete Dataset for KPI, trend and details when they share the same metric and filters; do not create a redundant unfiltered total Binding. Never let filtered and unfiltered subscriptions both write the same KPI: a refresh would race and overwrite the selected-region total. Distinct global totals need distinct, clearly labeled elements. Native data selects require data-dashboard-control=\"<controlId>\"; purely visual selects use data-dashboard-presentation. A data filter must query, not only filter cached browser rows. bindSelect controls ONE binding only. For a shared dimension filter across multiple bindings, follow the skill custom-select example and query every affected binding with the same typed controls; matching control IDs do not broadcast automatically. bindSelect derives options from Dataset fields and is only for dimension filters. Fixed query enums (days, page size, granularity) need options from parameter.allowedValues and a typed change listener calling live.query; never use date rows as numeric day-count options.";
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.HTML_DASHBOARD_GUIDANCE = void 0;
4
- exports.HTML_DASHBOARD_GUIDANCE = `For free-layout HTML dashboards, read the installed live-dashboard skill's references/html-runtime.md through read_skill_file, relative to its SKILL.md path in the skill catalog. Create/edit the HTML with SandboxFile; then dashboard_create_html or dashboard_update validates and saves it. Do not use generic present_files to save or preview a live dashboard. Its preview belongs to the Live dashboards Workbench. ECharts/Three.js are available via dashboard_libraries; all data access remains declarative Binding queries through window.xpertLiveArtifact. The page MUST declare const live = window.xpertLiveArtifact in its script (live is not a host global), register live.subscribe(bindingId, render), await live.ready(), and invoke live.query(bindingId, typedControls) for initial data and filter changes. window.__datasets and window.__refresh are not platform APIs and are never populated by the host. Read the runtime reference and its html-lifecycle.md example before writing HTML; preserve that lifecycle while freely changing the layout. Use one complete Dataset for KPI, trend and details when they share the same metric and filters; do not create a redundant unfiltered total Binding. Never let filtered and unfiltered subscriptions both write the same KPI: a refresh would race and overwrite the selected-region total. Distinct global totals need distinct, clearly labeled elements. Native data selects require data-dashboard-control="<controlId>"; purely visual selects use data-dashboard-presentation. A data filter must query, not only filter cached browser rows. bindSelect controls ONE binding only. For a shared dimension filter across multiple bindings, follow the skill custom-select example and query every affected binding with the same typed controls; matching control IDs do not broadcast automatically. bindSelect derives options from Dataset fields and is only for dimension filters. Fixed query enums (days, page size, granularity) need options from parameter.allowedValues and a typed change listener calling live.query; never use date rows as numeric day-count options.`;
4
+ exports.HTML_DASHBOARD_GUIDANCE = `For free-layout HTML dashboards, read the installed live-dashboard skill's references/html-runtime.md through read_skill_file, relative to its SKILL.md path in the skill catalog. Create/edit the HTML with SandboxFile; then dashboard_create_html or dashboard_update validates and saves it. Do not use generic present_files to save or preview a live dashboard. Its preview belongs to the Live dashboards Workbench. Call dashboard_libraries for packaged ECharts/Three.js and the allowed HTTPS origins for online scripts, stylesheets and fonts. Tailwind CDN is supported through script src; pin a version when possible. Online resource failures must be repaired before saving. Do not add online URLs to the packaged libraries array; all data access remains declarative Binding queries through window.xpertLiveArtifact. The page MUST declare const live = window.xpertLiveArtifact in its script (live is not a host global), register live.subscribe(bindingId, render), await live.ready(), and invoke live.query(bindingId, typedControls) for initial data and filter changes. window.__datasets and window.__refresh are not platform APIs and are never populated by the host. Read the runtime reference and its html-lifecycle.md example before writing HTML; preserve that lifecycle while freely changing the layout. Use one complete Dataset for KPI, trend and details when they share the same metric and filters; do not create a redundant unfiltered total Binding. Never let filtered and unfiltered subscriptions both write the same KPI: a refresh would race and overwrite the selected-region total. Distinct global totals need distinct, clearly labeled elements. Native data selects require data-dashboard-control="<controlId>"; purely visual selects use data-dashboard-presentation. A data filter must query, not only filter cached browser rows. bindSelect controls ONE binding only. For a shared dimension filter across multiple bindings, follow the skill custom-select example and query every affected binding with the same typed controls; matching control IDs do not broadcast automatically. bindSelect derives options from Dataset fields and is only for dimension filters. Fixed query enums (days, page size, granularity) need options from parameter.allowedValues and a typed change listener calling live.query; never use date rows as numeric day-count options.`;
@@ -20,6 +20,7 @@ const v3_1 = require("zod/v3");
20
20
  const plugin_sdk_1 = require("@xpert-ai/plugin-sdk");
21
21
  const artifact_document_1 = require("@xpert-ai/plugin-sdk/artifact-document");
22
22
  const document_libraries_1 = require("./document-libraries");
23
+ const document_network_1 = require("./document-network");
23
24
  const agent_schema_1 = require("./agent.schema");
24
25
  const html_guidance_1 = require("./html-guidance");
25
26
  const dashboard_service_1 = require("./dashboard.service");
@@ -31,6 +32,7 @@ const filter_evidence_1 = require("./filter-evidence");
31
32
  const save_evidence_1 = require("./save-evidence");
32
33
  const resource_card_1 = require("./resource-card");
33
34
  const dashboard_icon_1 = require("./dashboard-icon");
35
+ const preview_retry_1 = require("./preview-retry");
34
36
  const middlewareOptionsSchema = v3_1.z.object({ allowedFinalProviders: v3_1.z.array(v3_1.z.string().min(1)).min(1).max(20).optional() }).strict().default({});
35
37
  exports.DASHBOARD_GUIDANCE = `For a new independent dashboard, use a new unused HTML file path. An existing filename or matching title is not authorization to edit another Artifact; do not switch from creation to dashboard_open/update because a file already exists. For a new dashboard, call dashboard_list_sources before searching raw files, reading old scripts or creating transformations. Named governed sources may already provide the required query; inspect their provider contracts first. File-backed sources do not imply that a script must be written. For creating or editing live dashboards, first read the installed live-dashboard skill using read_skill_file and follow only the references relevant to the task. Use SandboxFile tools for all source-file authoring; HTML alone is not a saved dashboard. Follow preview.validation.checks immediately before authoring HTML; each suggestedCall must actually succeed. Business definitions, real data previews and nullable-filter tests are required before saving new or changed Bindings. Do not invent business data or substitute a different metric without agreement. Report completion only from a successful dashboard_create/dashboard_create_html/dashboard_update draft receipt. Human publication is separate. Successful create, update and open tools emit a chat resource card automatically; resourceCard.status reports emission. Its Open button selects the exact dashboard in Workbench. Never fabricate a link, call present_files for dashboard HTML, or output card JSON as text. If card delivery is unavailable, report the saved receipt and use the Workbench picker.`;
36
38
  let DashboardMiddleware = class DashboardMiddleware {
@@ -56,13 +58,37 @@ let DashboardMiddleware = class DashboardMiddleware {
56
58
  };
57
59
  // Evidence belongs to this run and actor, never a process-wide cache of business data.
58
60
  const discovered = new Map(), previews = new Map();
61
+ const discovering = new Map();
59
62
  let listedSources;
63
+ const assertSourceSelection = (input) => {
64
+ if (listedSources && !listedSources.some(source => (0, analytics_source_1.sameAnalyticsResource)(source.source, input.source) && source.providers.some(provider => provider.id === input.providerId)))
65
+ throw new Error(`DASHBOARD_SOURCE_SELECTION_INVALID: copy an exact source/providerId pair from dashboard_list_sources; do not invent or combine IDs. Known pairs: ${JSON.stringify(listedSources.map(source => ({ source: source.source, name: source.name, providerIds: source.providers.map(provider => provider.id) })))}. Refresh the list if the source changed. Initial requestJson is "{}"; then follow inspectRequest/nextRequest.`);
66
+ };
67
+ const discover = async (input) => {
68
+ assertSourceSelection(input);
69
+ const result = await this.service.discover(scope, input);
70
+ discovered.set(`${(0, analytics_1.analyticsSourceKey)(input.source)}/${input.providerId}`, result.queryContract);
71
+ return result;
72
+ };
73
+ const ensureContract = async (input) => {
74
+ assertSourceSelection(input);
75
+ const key = `${(0, analytics_1.analyticsSourceKey)(input.source)}/${input.providerId}`;
76
+ const cached = discovered.get(key);
77
+ if (cached)
78
+ return cached;
79
+ // Parallel previews share discovery, but failures are retryable and never become evidence.
80
+ let pending = discovering.get(key);
81
+ if (!pending) {
82
+ pending = discover({ source: input.source, providerId: input.providerId, request: {} })
83
+ .then(result => result.queryContract).finally(() => discovering.delete(key));
84
+ discovering.set(key, pending);
85
+ }
86
+ return pending;
87
+ };
60
88
  const filterEvidence = (0, filter_evidence_1.createFilterEvidence)(this.service, scope, previews);
61
89
  const created = new Map();
62
90
  const preview = async (binding) => {
63
- const contract = discovered.get(`${(0, analytics_1.analyticsSourceKey)(binding.source)}/${binding.providerId}`);
64
- if (!contract)
65
- throw new Error('DASHBOARD_DISCOVER_REQUIRED');
91
+ const contract = await ensureContract(binding);
66
92
  (0, plugin_sdk_1.validateAnalyticsPayload)(binding.payload, contract.payloadSchema);
67
93
  const controls = Object.fromEntries(binding.parameters.map((parameter) => [parameter.controlId, parameter.defaultValue ?? null]));
68
94
  const dataset = await this.service.preview(scope, { binding, controls });
@@ -124,6 +150,7 @@ let DashboardMiddleware = class DashboardMiddleware {
124
150
  throw new Error('DASHBOARD_WORKSPACE_REQUIRED');
125
151
  return files;
126
152
  });
153
+ const previewRetries = (0, preview_retry_1.createPreviewRetryGuard)();
127
154
  let modelCalls = 0;
128
155
  return { name: 'LiveDashboard', contextSchema: editing_1.dashboardContextSchema,
129
156
  wrapToolCall: async (request, handler) => {
@@ -134,11 +161,20 @@ let DashboardMiddleware = class DashboardMiddleware {
134
161
  : name === 'dashboard_create' ? agent_schema_1.createToolSchema.safeParse(args) : undefined;
135
162
  const save = dashboard_receipt_1.DASHBOARD_MUTATION_TOOLS.includes(name) ? evidence.beginSave(creation?.success ? creation.data.supersedesPaths : undefined, name === 'dashboard_create' ? 'draft' : 'source') : undefined;
136
163
  const result = path && (evidence.has(path) || previews.size > 0 && agent_schema_1.htmlPathSchema.safeParse(path).success)
137
- ? await evidence.trackWrite(path, async () => handler(request)) : await handler(request);
164
+ ? await evidence.trackWrite(path, async () => handler(request)) : await previewRetries.run(request, handler);
138
165
  if (save && 'content' in result && (!('status' in result) || result.status !== 'error'))
139
166
  await evidence.accept(save, result.content);
140
167
  return result;
141
168
  }, wrapModelCall: async (request, handler) => {
169
+ if (previewRetries.exhausted) {
170
+ const summary = previewRetries.summary + (evidence.pending ? ` Unsaved Workspace files: ${JSON.stringify(evidence.pendingPaths)}.` : '');
171
+ const result = await handler({ ...request, tools: [], toolChoice: 'none', systemMessage: new messages_1.SystemMessage({ content: [
172
+ ...(typeof request.systemMessage?.content === 'string' ? [{ type: 'text', text: request.systemMessage.content }] : request.systemMessage?.content ?? []),
173
+ { type: 'text', text: summary }
174
+ ] }) });
175
+ // A noncompliant model must not restart the failed loop even after tools were removed.
176
+ return result?.tool_calls?.length ? new messages_1.AIMessage(summary) : result;
177
+ }
142
178
  const exhausted = ++modelCalls > 24;
143
179
  if (exhausted && evidence.pending)
144
180
  throw new Error('DASHBOARD_SAVE_NOT_CONFIRMED: no successful draft save receipt; Workspace changes have not been saved to the dashboard.');
@@ -153,6 +189,8 @@ let DashboardMiddleware = class DashboardMiddleware {
153
189
  filterEvidence.tool,
154
190
  ...(0, definition_tools_1.createDefinitionTools)(context, () => { listedSources = undefined; }),
155
191
  (0, tools_1.tool)(async () => JSON.stringify({ libraries: await (0, document_libraries_1.libraryLocks)(['echarts', 'three']),
192
+ online: { ...document_network_1.DASHBOARD_NETWORK_POLICY,
193
+ usage: 'HTML may load scripts, stylesheets and fonts directly from these exact HTTPS origins. Use script src and link rel="stylesheet"; pin exact versions where available. Tailwind CDN https://cdn.tailwindcss.com is supported. Online resources require connectivity when saving and viewing; loading failures fail preflight. Keep all business data queries on window.xpertLiveArtifact; fetch/XHR, external images, workers, iframes and eval remain unavailable. Online resources do not belong in the packaged libraries array.' },
156
194
  pageRuntime: {
157
195
  global: 'window.xpertLiveArtifact',
158
196
  bindingIds: [...new Set([...previews.values()].map(binding => binding.id))],
@@ -162,7 +200,7 @@ let DashboardMiddleware = class DashboardMiddleware {
162
200
  },
163
201
  usage: { echarts: 'window.echarts: init(element, null, {renderer:"svg"}) then setOption(completeOption, {notMerge:true}). Canvas is also available. Derive active series/legend dimensions from each received Dataset, including filtered results; never retain excluded groups as zero-valued series. Aggregate repeated categories using all grain dimensions, clear empty results, and redraw KPI, chart and details together in subscribe. Use ResizeObserver to resize and reuse the instance on filter/refresh.',
164
202
  three: 'window.THREE: bundled core including WebGLRenderer. Requires WebGL2; catch unavailable GPU contexts and show a clear fallback. External textures/models, loaders/addons, WebGPU and workers are not bundled.' },
165
- lifecycle: 'Pass libraries IDs to dashboard_create_html or dashboard_update. Actual version and bytes are pinned on save; omitted libraries on update preserves saved locks. No CDN, imports or network access.' }), { name: 'dashboard_libraries', description: 'Discover packaged frontend libraries and exact versions available to HTML dashboards. Read before using ECharts/Three.js. Returns metadata, never library source.',
203
+ lifecycle: 'Pass packaged library IDs to dashboard_create_html or dashboard_update. Actual version and bytes are pinned on save; omitted libraries on update preserves saved locks. Online resources are listed separately under online, are not pinned in the Artifact, and must load successfully when saving and viewing.' }), { name: 'dashboard_libraries', description: 'Discover packaged frontend libraries, exact versions and allowed HTTPS origins for online scripts, stylesheets and fonts. Read before using Tailwind CDN or ECharts/Three.js. Returns metadata, never library source.',
166
204
  schema: v3_1.z.object({}).strict(), verboseParsingErrors: true, metadata: { toolName: { en_US: 'Discover dashboard libraries', zh_Hans: '查看看板前端图库' } } }),
167
205
  ...(0, editing_1.createEditingTools)(this.service, scope, context, resolvePreviews, validateFinal, (path, artifactId) => evidence.opened(path, artifactId)),
168
206
  (0, tools_1.tool)(async () => {
@@ -173,24 +211,18 @@ let DashboardMiddleware = class DashboardMiddleware {
173
211
  schema: v3_1.z.object({}).strict(), verboseParsingErrors: true, metadata: { toolName: { en_US: 'List analytics sources', zh_Hans: '发现分析数据源' } }
174
212
  }),
175
213
  (0, tools_1.tool)(async (input) => {
176
- if (listedSources && !listedSources.some(source => (0, analytics_source_1.sameAnalyticsResource)(source.source, input.source) && source.providers.some(provider => provider.id === input.providerId)))
177
- throw new Error(`DASHBOARD_SOURCE_SELECTION_INVALID: copy an exact source/providerId pair from dashboard_list_sources; do not invent or combine IDs. Known pairs: ${JSON.stringify(listedSources.map(source => ({ source: source.source, name: source.name, providerIds: source.providers.map(provider => provider.id) })))}. Refresh the list if the source changed. Initial requestJson is "{}"; then follow inspectRequest/nextRequest.`);
178
214
  const parsed = { source: input.source, providerId: input.providerId, request: (0, agent_schema_1.parseObjectJson)(input.requestJson) };
179
- const result = await this.service.discover(scope, parsed);
180
- discovered.set(`${(0, analytics_1.analyticsSourceKey)(parsed.source)}/${parsed.providerId}`, result.queryContract);
181
- return JSON.stringify(result);
215
+ return JSON.stringify(await discover(parsed));
182
216
  }, { name: 'dashboard_discover', description: 'Discover provider-owned business metadata. Start requestJson="{}" then serialize inspectRequest/nextRequest. Inspect dataset fields, comments, measures and native query format before preview.',
183
217
  schema: agent_schema_1.discoverToolSchema, verboseParsingErrors: true, metadata: { toolName: { en_US: 'Inspect business data', zh_Hans: '理解业务数据与指标' } } }),
184
218
  (0, tools_1.tool)(async (input) => {
185
- const contract = discovered.get(`${(0, analytics_1.analyticsSourceKey)(input.binding.source)}/${input.binding.providerId}`);
186
- if (!contract)
187
- throw new Error('DASHBOARD_DISCOVER_REQUIRED: discover the source before preview');
219
+ const contract = await ensureContract(input.binding);
188
220
  const binding = (0, agent_schema_1.fromAgentBinding)(input.binding, contract);
189
221
  return preview(binding);
190
- }, { name: 'dashboard_preview', description: 'Run one read-only binding using parameters[].defaultValue. Returns validated Dataset fields, first 20 rows, full query completeness and audit. Required before create; changes to binding/defaults require another preview.',
222
+ }, { name: 'dashboard_preview', description: 'Run one read-only binding using parameters[].defaultValue. Automatically discovers the source/provider query contract when absent in this run; use dashboard_discover to inspect business metadata or repair invalid queries. Returns validated Dataset fields, first 20 rows, full query completeness and audit. Required before create; changes to binding/defaults require another preview.',
191
223
  schema: agent_schema_1.previewToolSchema, verboseParsingErrors: true, metadata: { toolName: { en_US: 'Verify dashboard data', zh_Hans: '预览并验证看板数据' } } }),
192
224
  (0, tools_1.tool)(async ({ bindingJson }) => preview(plugin_sdk_1.analyticsBindingSchema.parse(JSON.parse(bindingJson))), {
193
- name: 'dashboard_preview_binding', description: 'Preview a complete canonical AnalyticsBinding returned by another tool. Serialize the complete canonical Binding (for example discovery items[].metadata.binding), including parameters[].controlId. bindingPayload is only the payload, not a Binding. If adding a parameter, include name, controlId, type, required and defaultValue. Do not convert it to the simplified dashboard_preview input. Discovery of the source is still required. Returns the same previewId receipt for dashboard_create.',
225
+ name: 'dashboard_preview_binding', description: 'Preview a complete canonical AnalyticsBinding returned by another tool. Serialize the complete canonical Binding (for example discovery items[].metadata.binding), including parameters[].controlId. bindingPayload is only the payload, not a Binding. If adding a parameter, include name, controlId, type, required and defaultValue. Do not convert it to the simplified dashboard_preview input. Automatically discovers the source/provider query contract when absent in this run; use dashboard_discover for business metadata. Returns the same previewId receipt for dashboard_create.',
194
226
  schema: v3_1.z.object({ bindingJson: v3_1.z.string().min(2).max(200000) }).strict(), verboseParsingErrors: true,
195
227
  metadata: { toolName: { en_US: 'Verify registered dataset', zh_Hans: '验证已注册数据集' } }
196
228
  }),
@@ -7,6 +7,8 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  };
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.LiveDashboardPlugin = void 0;
10
+ const content_capability_1 = require("./content-capability");
11
+ const content_provider_1 = require("./content.provider");
10
12
  const plugin_sdk_1 = require("@xpert-ai/plugin-sdk");
11
13
  const dashboard_service_1 = require("./dashboard.service");
12
14
  const view_provider_1 = require("./view.provider");
@@ -18,5 +20,5 @@ let LiveDashboardPlugin = class LiveDashboardPlugin {
18
20
  };
19
21
  exports.LiveDashboardPlugin = LiveDashboardPlugin;
20
22
  exports.LiveDashboardPlugin = LiveDashboardPlugin = __decorate([
21
- (0, plugin_sdk_1.XpertServerPlugin)({ providers: [dashboard_service_1.DashboardService, view_provider_1.DashboardViewProvider, middleware_1.DashboardMiddleware, html_validation_1.HtmlDashboardValidation, presentation_provider_1.DashboardPresentationProvider, sharing_service_1.DashboardSharingService] })
23
+ (0, plugin_sdk_1.XpertServerPlugin)({ providers: [content_capability_1.DashboardCapability, content_provider_1.DashboardContentProvider, content_provider_1.DashboardContentEditor, dashboard_service_1.DashboardService, view_provider_1.DashboardViewProvider, middleware_1.DashboardMiddleware, html_validation_1.HtmlDashboardValidation, presentation_provider_1.DashboardPresentationProvider, sharing_service_1.DashboardSharingService] })
22
24
  ], LiveDashboardPlugin);
@@ -117,6 +117,11 @@ export declare class DashboardPresentationProvider implements IArtifactPresentat
117
117
  };
118
118
  } | undefined;
119
119
  datasets: import("@xpert-ai/contracts").AnalyticalDataset[];
120
+ failures: {
121
+ bindingId: string;
122
+ datasetId: string;
123
+ reason: "unavailable";
124
+ }[];
120
125
  }>;
121
126
  private open;
122
127
  }
@@ -44,8 +44,8 @@ let DashboardPresentationProvider = class DashboardPresentationProvider {
44
44
  return (0, dashboard_queries_1.queryDashboardBinding)(this.runtime, context.actor, saved.manifest, input.bindingId, input.controls);
45
45
  if (request.operation !== 'query')
46
46
  throw new common_1.ForbiddenException('DASHBOARD_ACTION_UNSUPPORTED');
47
- const datasets = await (0, dashboard_queries_1.queryDashboard)(this.runtime, context.actor, saved.manifest, input.controls);
48
- return { datasets, ...(saved.manifest.presentation ? {} : { payload: (0, renderer_1.toDataAnalyticsPayload)(saved.manifest, datasets) }) };
47
+ const { datasets, failures } = await (0, dashboard_queries_1.queryDashboardPartial)(this.runtime, context.actor, saved.manifest, input.controls);
48
+ return { datasets, failures, ...(saved.manifest.presentation ? {} : { payload: (0, renderer_1.toDataAnalyticsPayload)(saved.manifest, datasets) }) };
49
49
  }
50
50
  open(context) {
51
51
  const { artifact, version, content } = context;
@@ -0,0 +1,7 @@
1
+ import type { AgentBuiltInState, ToolCallHandler, ToolCallRequest } from '@xpert-ai/plugin-sdk';
2
+ /** Run-local bounds include tool-schema failures, before any discovery or query executes. */
3
+ export declare function createPreviewRetryGuard(): {
4
+ readonly exhausted: boolean;
5
+ readonly summary: string;
6
+ run(request: ToolCallRequest<AgentBuiltInState>, handler: ToolCallHandler): Promise<import("@langchain/core/messages").ToolMessage | import("@langchain/langgraph").Command<unknown, Record<string, unknown>, string>>;
7
+ };
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createPreviewRetryGuard = createPreviewRetryGuard;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const PREVIEW_TOOLS = new Set(['dashboard_preview', 'dashboard_preview_binding', 'dashboard_test_filter']);
6
+ /** Run-local bounds include tool-schema failures, before any discovery or query executes. */
7
+ function createPreviewRetryGuard() {
8
+ const failures = new Map(), pending = new Set();
9
+ let consecutiveFailures = 0, lastError = '';
10
+ const recordFailure = (key, message) => {
11
+ failures.set(key, { count: (failures.get(key)?.count ?? 0) + 1, message: message.slice(0, 2000) });
12
+ consecutiveFailures++;
13
+ lastError = message.slice(0, 2000);
14
+ };
15
+ return {
16
+ get exhausted() { return consecutiveFailures >= 6; },
17
+ get summary() { return `DASHBOARD_PREVIEW_RETRY_LIMIT: preview validation repeatedly failed. This run has stopped making tool calls. Report the last error and the required repair; do not claim an unconfirmed save. Last error: ${lastError}`; },
18
+ async run(request, handler) {
19
+ if (!PREVIEW_TOOLS.has(request.toolCall.name))
20
+ return handler(request);
21
+ const key = (0, node_crypto_1.createHash)('sha256').update(JSON.stringify({ name: request.toolCall.name, args: request.toolCall.args }, (_key, value) => value && typeof value === 'object' && !Array.isArray(value) ? Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b))) : value)).digest('hex');
22
+ if (consecutiveFailures >= 6)
23
+ throw new Error(this.summary);
24
+ if ((failures.get(key)?.count ?? 0) >= 2) {
25
+ consecutiveFailures++;
26
+ throw new Error(`DASHBOARD_PREVIEW_REPEAT_BLOCKED: this exact tool input already failed twice; no query was executed. Correct the input using the previous diagnostic, or stop and report the blocker. Last error: ${failures.get(key).message}`);
27
+ }
28
+ // Concurrent duplicate calls must not multiply queries or bypass the per-input bound.
29
+ if (pending.has(key))
30
+ throw new Error('DASHBOARD_PREVIEW_IN_PROGRESS: wait for the identical preview already in progress; do not submit duplicates.');
31
+ pending.add(key);
32
+ try {
33
+ const result = await handler(request);
34
+ if ('status' in result && result.status === 'error' && 'content' in result) {
35
+ recordFailure(key, typeof result.content === 'string' ? result.content : 'Preview tool returned an error');
36
+ }
37
+ else {
38
+ failures.delete(key);
39
+ consecutiveFailures = 0;
40
+ }
41
+ return result;
42
+ }
43
+ catch (error) {
44
+ recordFailure(key, error instanceof Error ? error.message : 'Preview tool failed');
45
+ throw error;
46
+ }
47
+ finally {
48
+ pending.delete(key);
49
+ }
50
+ }
51
+ };
52
+ }
@@ -3,6 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.toDataAnalyticsPayload = toDataAnalyticsPayload;
4
4
  /** The only Data Analytics v1 translation. Providers never see chart encoding syntax. */
5
5
  function toDataAnalyticsPayload(manifest, datasets) {
6
+ // Missing datasets are failures, not empty/zero business results. The caller renders
7
+ // their failure state separately; successful empty datasets still retain their views.
8
+ const available = new Set(datasets.map(dataset => dataset.datasetId));
9
+ manifest = { ...manifest, charts: manifest.charts.filter(chart => available.has(chart.datasetId)),
10
+ tables: manifest.tables.filter(table => available.has(table.datasetId)) };
6
11
  const snapshot = Object.fromEntries(datasets.flatMap((dataset) => [
7
12
  [dataset.datasetId, dataset.rows],
8
13
  ...(!manifest.charts.some((chart) => chart.datasetId === dataset.datasetId) ? [] : [[`__plot.${dataset.datasetId}`, dataset.rows.map((row) => Object.fromEntries(dataset.fields.map((field) => [field.id, manifest.charts.some((chart) => chart.datasetId === dataset.datasetId && [chart.x, chart.y].includes(field.id)) ? renderValue(row[field.id], field.logicalType) : row[field.id]])))]])
@@ -15,7 +15,7 @@ export declare function dashboardSaveEvidence(files: () => WorkspaceFilesApi): {
15
15
  token: number;
16
16
  generations: Map<string, number>;
17
17
  supersedesPaths: string[];
18
- confirmation: "source" | "draft";
18
+ confirmation: "draft" | "source";
19
19
  };
20
20
  accept(save: SaveAttempt, output: unknown): Promise<void>;
21
21
  readonly pending: boolean;
@@ -41,6 +41,7 @@ let DashboardSharingService = class DashboardSharingService {
41
41
  versionId: link.artifactVersionId, url: link.presentationUrl, expiresAt: link.expiresAt } : null };
42
42
  }
43
43
  async share(context, input) {
44
+ await this.dashboards.authorizeProject(context, 'manage');
44
45
  const state = await this.inspect(context, input.artifactId, input.versionMode === 'version' ? input.versionId : undefined);
45
46
  if (state.issues.length || !state.published?.versionId)
46
47
  throw new common_1.BadRequestException({ code: 'DASHBOARD_SHARE_BLOCKED', issues: state.issues });
@@ -52,6 +53,7 @@ let DashboardSharingService = class DashboardSharingService {
52
53
  return this.inspect(context, input.artifactId, input.versionMode === 'version' ? input.versionId : undefined);
53
54
  }
54
55
  async revoke(context, artifactId) {
56
+ await this.dashboards.authorizeProject(context, 'manage');
55
57
  await this.dashboards.history(context, artifactId);
56
58
  await this.runtime.require(plugin_sdk_1.ArtifactsRuntimeCapability).revokeArtifactShare({ artifactId, shareKey: dashboard_sharing_1.DASHBOARD_SHARE_KEY });
57
59
  return this.inspect(context, artifactId);
@@ -191,6 +191,11 @@ export declare class DashboardViewProvider implements IXpertViewExtensionProvide
191
191
  };
192
192
  } | undefined;
193
193
  datasets: import("@xpert-ai/contracts").AnalyticalDataset[];
194
+ failures: {
195
+ bindingId: string;
196
+ datasetId: string;
197
+ reason: "unavailable";
198
+ }[];
194
199
  manifest: {
195
200
  schema: "xpert.live-dashboard";
196
201
  id: string;
@@ -32,15 +32,15 @@ let DashboardViewProvider = class DashboardViewProvider {
32
32
  }
33
33
  supports(context) { return context.hostType === 'agent' || context.hostType === 'project'; }
34
34
  getViewManifests(context, slot) {
35
- if (!(context.hostType === 'agent' && slot === 'agent.workbench' || context.hostType === 'project' && slot === 'detail.sections'))
35
+ if (!(context.hostType === 'agent' && ['agent.workbench', 'agent.workbench.fixed', 'agent.workbench.main'].includes(slot) || context.hostType === 'project' && slot === 'detail.sections'))
36
36
  return [];
37
37
  return [{ key: exports.VIEW_KEY, title: text('Live dashboards', '实时看板'), hostType: context.hostType, slot, order: 35,
38
- icon: dashboard_icon_1.dashboardIcon, refreshable: true, activation: { requiredFeatures: [exports.FEATURE] },
38
+ icon: dashboard_icon_1.dashboardIcon, refreshable: true, activation: { projectResource: true, requiredFeatures: [exports.FEATURE] },
39
39
  source: { provider: exports.VIEW_PROVIDER, plugin: dashboard_service_1.PLUGIN_NAME },
40
- workbench: { fixed: true, menu: { enabled: true, label: text('Live dashboards', '实时看板'), icon: dashboard_icon_1.dashboardIcon, order: 35 } },
40
+ workbench: { fixed: true, openMode: 'on-demand', menu: { enabled: true, label: text('Live dashboards', '实时看板'), icon: dashboard_icon_1.dashboardIcon, order: 35 } },
41
41
  view: { type: 'remote_component', runtime: 'react', protocolVersion: 1, component: { isolation: 'iframe', entry: exports.VIEW_KEY }, dataSource: { mode: 'platform' } },
42
42
  dataSource: { mode: 'platform', querySchema: { supportsSelection: true, supportsParameters: true }, cache: { enabled: false } },
43
- clientCommands: [{ key: contracts_1.ASSISTANT_CONTEXT_SET_COMMAND, label: text('Select dashboard for Assistant', '设置助理当前看板') },
43
+ clientCommands: [{ key: 'workbench.content.status' }, { key: 'workbench.content.resolve' }, { key: contracts_1.ASSISTANT_CONTEXT_SET_COMMAND, label: text('Select dashboard for Assistant', '设置助理当前看板') },
44
44
  { key: contracts_1.ARTIFACT_PRESENTATION_OPEN_COMMAND, label: text('Open shared page', '打开独立页面') },
45
45
  { key: contracts_1.WORKBENCH_NAVIGATION_OPEN_COMMAND, label: text('Open dashboard', '打开看板') }],
46
46
  hostEvents: { subscriptions: [{ key: 'dashboard-created', event: 'assistant.tool.completed',