@xpert-ai/plugin-live-dashboard 0.4.1 → 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 (79) hide show
  1. package/.xpertai-plugin/plugin.json +1 -1
  2. package/README.md +8 -2
  3. package/dist/index.js +3 -1
  4. package/dist/lib/agent.schema.d.ts +177 -23
  5. package/dist/lib/agent.schema.js +11 -9
  6. package/dist/lib/archive.service.d.ts +169 -0
  7. package/dist/lib/archive.service.js +147 -0
  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/content.provider.d.ts +1 -1
  13. package/dist/lib/dashboard-archive.d.ts +70 -0
  14. package/dist/lib/dashboard-archive.js +118 -0
  15. package/dist/lib/dashboard-queries.d.ts +1 -1
  16. package/dist/lib/dashboard-queries.js +11 -6
  17. package/dist/lib/dashboard-receipt.js +1 -1
  18. package/dist/lib/dashboard-sources.d.ts +69 -0
  19. package/dist/lib/dashboard-sources.js +38 -0
  20. package/dist/lib/dashboard-view.schema.d.ts +422 -88
  21. package/dist/lib/dashboard.schema.d.ts +379 -58
  22. package/dist/lib/dashboard.schema.js +20 -2
  23. package/dist/lib/dashboard.service.d.ts +80 -15
  24. package/dist/lib/dashboard.service.js +65 -9
  25. package/dist/lib/definition-tools.d.ts +4 -4
  26. package/dist/lib/document-host.js +1 -1
  27. package/dist/lib/document-package.d.ts +19 -4
  28. package/dist/lib/editing-patch.d.ts +3 -0
  29. package/dist/lib/editing-patch.js +4 -2
  30. package/dist/lib/editing-session.d.ts +4 -4
  31. package/dist/lib/editing.d.ts +204 -20
  32. package/dist/lib/editing.js +10 -3
  33. package/dist/lib/filter-evidence.d.ts +28 -0
  34. package/dist/lib/filter-evidence.js +3 -2
  35. package/dist/lib/html-guidance.d.ts +1 -0
  36. package/dist/lib/html-guidance.js +2 -1
  37. package/dist/lib/html-validation-feedback.d.ts +7 -0
  38. package/dist/lib/html-validation-feedback.js +19 -0
  39. package/dist/lib/html-validation.js +3 -2
  40. package/dist/lib/inspection-host.d.ts +7 -0
  41. package/dist/lib/inspection-host.js +48 -0
  42. package/dist/lib/inspection-runtime.d.ts +2 -0
  43. package/dist/lib/inspection-runtime.js +77 -0
  44. package/dist/lib/json-input.d.ts +4 -0
  45. package/dist/lib/json-input.js +21 -0
  46. package/dist/lib/middleware.d.ts +3 -1
  47. package/dist/lib/middleware.js +92 -13
  48. package/dist/lib/plugin.js +3 -1
  49. package/dist/lib/presentation.provider.d.ts +24 -9
  50. package/dist/lib/presentation.provider.js +2 -1
  51. package/dist/lib/preview-retry.d.ts +4 -3
  52. package/dist/lib/preview-retry.js +13 -7
  53. package/dist/lib/prompt-workflows.js +1 -1
  54. package/dist/lib/query-session.d.ts +9 -0
  55. package/dist/lib/query-session.js +64 -0
  56. package/dist/lib/save-requests.d.ts +118 -0
  57. package/dist/lib/save-requests.js +199 -0
  58. package/dist/lib/source-executions.d.ts +437 -0
  59. package/dist/lib/source-executions.js +49 -0
  60. package/dist/lib/source-validation.d.ts +2 -0
  61. package/dist/lib/source-validation.js +21 -0
  62. package/dist/lib/view.provider.d.ts +213 -8
  63. package/dist/lib/view.provider.js +47 -5
  64. package/dist/remote/app.css +1 -1
  65. package/dist/remote/app.js +185 -63
  66. package/dist/remote/build-manifest.json +52 -33
  67. package/dist/sandbox-actions/action.json +1 -1
  68. package/dist/sandbox-actions/bundle/host.js +63 -14
  69. package/dist/sandbox-actions/bundle/validate.mjs +2 -2
  70. package/dist/validation/host-entry.d.ts +5 -1
  71. package/dist/validation/host-entry.js +21 -4
  72. package/docs/docs.json +5 -1
  73. package/docs/large-dashboards-acceptance.mdx +74 -0
  74. package/docs/large-dashboards.mdx +80 -0
  75. package/docs/sources-inspect-portability-acceptance.mdx +67 -0
  76. package/docs/sources-inspect-portability.mdx +115 -0
  77. package/package.json +8 -7
  78. package/skills/live-dashboard/SKILL.md +5 -4
  79. package/skills/live-dashboard/references/source-inspection.md +17 -0
@@ -6,7 +6,7 @@ const context = `Work in the current authorized Assistant/project scope. Use the
6
6
  Resolve source/provider pairs, Artifact IDs, fields and file paths from current tools, not names or previous conversation claims. Use only real query results; no invented business data, targets, thresholds, comparison periods or activity feeds. Keep queries read-only and bounded. Report actual tool failures and diagnosticId when supplied. A missing capability or runtime binding is a blocker, not permission to bypass validation or switch presentation modes.`;
7
7
  const metrics = `Confirm metric definitions, units, time range, grain and join keys from metadata and small real samples. Compute ratios from aggregated numerators and denominators, not sums/averages of percentages. Keep unknown/zero denominators as unavailable, not zero. Check join cardinality and avoid duplicate totals. Use chronological date ordering and comparable complete periods. State freshness and gaps; historical data must not be described as current operations. Avoid exposing unnecessary personal or sensitive row-level fields.`;
8
8
  const discover = `Call dashboard_list_sources first, then dashboard_discover for exact authorized source/provider pairs. Follow returned inspectRequest/nextRequest and query contracts; do not scan unrelated schemas or guess SQL fields. Prefer existing governed definitions. If the requested source is missing, use analytics_definition_connections and analytics_definition_inspect; register a missing definition with analytics_definition_register only when needed for this requested dashboard, then refresh sources. Never duplicate an existing definition or ask the user to supply internal IDs.`;
9
- const preview = `Before saving new or changed Bindings, call dashboard_preview or dashboard_preview_binding with the exact final queries and typed default controls in this run. Execute every preview.validation.checks[].suggestedCall with dashboard_test_filter; use observed non-default values and the all-values/null case when applicable. Follow each tool's schema, serialize JSON-string fields correctly, and copy returned bindingJson rather than nesting a Binding inside SQL. Changed queries/defaults need fresh evidence. Use at most eight final Bindings, sharing a query where its metric, grain and filters permit it. Never reuse historical previewIds or hard-code preview rows into the page.`;
9
+ const preview = `Before saving new or changed Bindings, call dashboard_preview or dashboard_preview_binding with the exact final queries and typed default controls in this run. Execute every preview.validation.checks[].suggestedCall with dashboard_test_filter; use observed non-default values and the all-values/null case when applicable. Follow each tool's schema, serialize JSON-string fields correctly, and copy returned bindingJson rather than nesting a Binding inside SQL. Changed queries/defaults need fresh evidence. Respect the installation authoring limit (32 Bindings by default, at most 64), sharing a query where its metric, grain and filters permit it. Never reuse historical previewIds or hard-code preview rows into the page.`;
10
10
  const receipt = `Save a working draft and require a successful domain receipt with dashboard.artifactId, status "draft" and draftRevision. File writes, an existing resource card or an older published version are not proof of this save. Open the exact saved Artifact to inspect it; do not update an unchanged copy just to verify persistence. Verify real filter changes and refresh where available, distinguishing tool checks from unperformed viewer checks. Return the saved receipt, sources, metric definitions and a short manual acceptance checklist. Formal publication remains a separate human Workbench action.`;
11
11
  const newDashboard = `Create a NEW independent dashboard. Do not modify, rename or replace an existing Artifact, including the currently selected dashboard. Choose a distinct business title. In HTML mode choose an unused workspace-relative named .html path; never reuse an old page just because it has a similar title. Reuse verified source definitions, not an old dashboard's identity. If this run already saved the requested dashboard, keep its Artifact rather than creating a duplicate.`;
12
12
  const automaticTheme = `Choose one useful business theme from the authorized data, prioritizing an operational decision such as delivery delays, inventory backlog, supplier performance or production quality. Compare only a few plausible sources by completeness, freshness, metric clarity and actionable detail; then select the strongest one without repeatedly asking the user to choose. Explain who will use it, which decision it supports and which records require attention. If no source supports a defensible theme, report the missing data instead of making a decorative demo.`;
@@ -0,0 +1,9 @@
1
+ import type { AnalyticalDataset } from '@xpert-ai/contracts';
2
+ import { type ArtifactQueryInput, type ArtifactQueryPriority } from '@xpert-ai/plugin-sdk/artifact-document';
3
+ import type { DashboardManifest } from './dashboard.schema';
4
+ import { type SourceEvent } from './source-executions';
5
+ /** One saved definition and actor. No cache survives a navigation or a version change. */
6
+ export declare function createDashboardQuerySession(manifest: DashboardManifest, execute: (input: ArtifactQueryInput) => Promise<AnalyticalDataset>, changed: (datasets: AnalyticalDataset[], failures: string[]) => void, event: (event: SourceEvent) => void, busy: (value: boolean) => void): {
7
+ query(values: ArtifactQueryInput["controls"], priority?: ArtifactQueryPriority, only?: string[]): Promise<void>;
8
+ dispose(): void;
9
+ };
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createDashboardQuerySession = createDashboardQuerySession;
4
+ const artifact_document_1 = require("@xpert-ai/plugin-sdk/artifact-document");
5
+ const source_executions_1 = require("./source-executions");
6
+ /** One saved definition and actor. No cache survives a navigation or a version change. */
7
+ function createDashboardQuerySession(manifest, execute, changed, event, busy) {
8
+ const results = new Map(), failures = new Set();
9
+ const keys = new Map(), requests = new Map(), pending = new Set();
10
+ let sequence = 0, disposed = false;
11
+ const publish = () => { if (!disposed)
12
+ changed([...results.values()], [...failures]); };
13
+ const scheduler = (0, artifact_document_1.createArtifactQueryScheduler)({ execute, onBusy: busy, onState: state => {
14
+ if (disposed || !['queued', 'running'].includes(state.state))
15
+ return;
16
+ event({ type: state.state === 'queued' ? 'queue' : 'start', bindingId: state.bindingId,
17
+ requestId: requests.get(state.bindingId), controls: state.controls, at: new Date(state.state === 'queued' ? state.queuedAt : state.startedAt).toISOString() });
18
+ } });
19
+ return {
20
+ async query(values, priority = 'interactive', only) {
21
+ const affected = manifest.bindings.flatMap(binding => {
22
+ if (only && !only.includes(binding.id))
23
+ return [];
24
+ const controls = Object.fromEntries(binding.parameters.map(parameter => [parameter.controlId,
25
+ Object.hasOwn(values, parameter.controlId) ? values[parameter.controlId] : parameter.defaultValue ?? null]));
26
+ const key = JSON.stringify(controls);
27
+ if (priority === 'interactive' && keys.get(binding.id) === key && !failures.has(binding.output.datasetId))
28
+ return [];
29
+ // A repeated refresh shares both the physical query and its Sources lifecycle.
30
+ const requestId = keys.get(binding.id) === key && pending.has(binding.id) ? requests.get(binding.id) : String(++sequence);
31
+ keys.set(binding.id, key);
32
+ pending.add(binding.id);
33
+ requests.set(binding.id, requestId);
34
+ results.delete(binding.output.datasetId);
35
+ failures.delete(binding.output.datasetId);
36
+ return [{ binding, controls, requestId }];
37
+ });
38
+ // Remove every affected old-parameter dataset before publishing any new result.
39
+ publish();
40
+ await Promise.all(affected.map(async ({ binding, controls, requestId }) => {
41
+ try {
42
+ const result = await scheduler.query({ bindingId: binding.id, controls }, priority);
43
+ if (disposed || requests.get(binding.id) !== requestId)
44
+ return;
45
+ results.set(binding.output.datasetId, result);
46
+ if (new TextEncoder().encode(JSON.stringify([...results.values()])).byteLength > 3_000_000) {
47
+ results.delete(binding.output.datasetId);
48
+ throw new Error('DASHBOARD_RESULT_TOO_LARGE');
49
+ }
50
+ event({ type: 'success', bindingId: binding.id, requestId, result: source_executions_1.sourceDatasetSchema.parse(result), at: new Date().toISOString() });
51
+ }
52
+ catch {
53
+ if (disposed || requests.get(binding.id) !== requestId)
54
+ return;
55
+ failures.add(binding.output.datasetId);
56
+ event({ type: 'failure', bindingId: binding.id, requestId, at: new Date().toISOString() });
57
+ }
58
+ pending.delete(binding.id);
59
+ publish();
60
+ }));
61
+ },
62
+ dispose() { disposed = true; scheduler.dispose(); results.clear(); requests.clear(); keys.clear(); pending.clear(); }
63
+ };
64
+ }
@@ -0,0 +1,118 @@
1
+ import { z } from 'zod/v3';
2
+ import type { XpertResolvedViewHostContext } from '@xpert-ai/contracts';
3
+ import { type ManagedQueueJob, type ManagedQueueJobContext, type ManagedQueueService, type RuntimeCapabilityRegistry } from '@xpert-ai/plugin-sdk';
4
+ import { type ArtifactDocumentDependencyAsset } from '@xpert-ai/plugin-sdk/artifact-document';
5
+ import { DashboardService } from './dashboard.service';
6
+ import { type DashboardManifest } from './dashboard.schema';
7
+ declare const sourceFileSchema: z.ZodObject<{
8
+ path: z.ZodString;
9
+ sha256: z.ZodString;
10
+ }, "strict", z.ZodTypeAny, {
11
+ path: string;
12
+ sha256: string;
13
+ }, {
14
+ path: string;
15
+ sha256: string;
16
+ }>;
17
+ export declare const saveRequestSchema: z.ZodObject<{
18
+ requestId: z.ZodString;
19
+ }, "strict", z.ZodTypeAny, {
20
+ requestId: string;
21
+ }, {
22
+ requestId: string;
23
+ }>;
24
+ export declare const waitSaveRequestSchema: z.ZodObject<{
25
+ requestId: z.ZodString;
26
+ } & {
27
+ cursor: z.ZodString;
28
+ }, "strict", z.ZodTypeAny, {
29
+ requestId: string;
30
+ cursor: string;
31
+ }, {
32
+ requestId: string;
33
+ cursor: string;
34
+ }>;
35
+ /** Durable save orchestration; domain validation and CAS remain in DashboardService. */
36
+ export declare class DashboardSaveRequests {
37
+ private readonly service;
38
+ private readonly runtime;
39
+ private readonly queue;
40
+ constructor(service: DashboardService, runtime: RuntimeCapabilityRegistry, queue: ManagedQueueService);
41
+ submit(context: XpertResolvedViewHostContext, manifest: DashboardManifest, expectedVersionId: string | null, expectedRevision: number, idempotencyKey: string, sourceFile?: z.infer<typeof sourceFileSchema>, dependencies?: ArtifactDocumentDependencyAsset[]): Promise<{
42
+ cursor: string;
43
+ nextAction: string;
44
+ error?: string | undefined;
45
+ sourceFile?: {
46
+ path: string;
47
+ sha256: string;
48
+ } | undefined;
49
+ dashboard?: {
50
+ status: "draft";
51
+ title: string;
52
+ artifactId: string;
53
+ draftRevision: number;
54
+ draftId: string;
55
+ baseVersionId: string | null;
56
+ } | undefined;
57
+ requestId: string;
58
+ state: "failed" | "running" | "succeeded" | "queued";
59
+ stage: string;
60
+ completed: number;
61
+ total: number;
62
+ terminal: boolean;
63
+ }>;
64
+ private reference;
65
+ private enqueue;
66
+ private read;
67
+ private snapshot;
68
+ get(context: XpertResolvedViewHostContext, requestId: string): Promise<{
69
+ cursor: string;
70
+ nextAction: string;
71
+ error?: string | undefined;
72
+ sourceFile?: {
73
+ path: string;
74
+ sha256: string;
75
+ } | undefined;
76
+ dashboard?: {
77
+ status: "draft";
78
+ title: string;
79
+ artifactId: string;
80
+ draftRevision: number;
81
+ draftId: string;
82
+ baseVersionId: string | null;
83
+ } | undefined;
84
+ requestId: string;
85
+ state: "failed" | "running" | "succeeded" | "queued";
86
+ stage: string;
87
+ completed: number;
88
+ total: number;
89
+ terminal: boolean;
90
+ }>;
91
+ wait(context: XpertResolvedViewHostContext, requestId: string, cursor: string, signal?: AbortSignal): Promise<{
92
+ cursor: string;
93
+ nextAction: string;
94
+ error?: string | undefined;
95
+ sourceFile?: {
96
+ path: string;
97
+ sha256: string;
98
+ } | undefined;
99
+ dashboard?: {
100
+ status: "draft";
101
+ title: string;
102
+ artifactId: string;
103
+ draftRevision: number;
104
+ draftId: string;
105
+ baseVersionId: string | null;
106
+ } | undefined;
107
+ requestId: string;
108
+ state: "failed" | "running" | "succeeded" | "queued";
109
+ stage: string;
110
+ completed: number;
111
+ total: number;
112
+ terminal: boolean;
113
+ }>;
114
+ handle(job: ManagedQueueJob<{
115
+ requestId: string;
116
+ }>, identity: ManagedQueueJobContext): Promise<void>;
117
+ }
118
+ export {};
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.DashboardSaveRequests = exports.waitSaveRequestSchema = exports.saveRequestSchema = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const node_crypto_1 = require("node:crypto");
18
+ const promises_1 = require("node:timers/promises");
19
+ const v3_1 = require("zod/v3");
20
+ const plugin_sdk_1 = require("@xpert-ai/plugin-sdk");
21
+ const artifact_document_1 = require("@xpert-ai/plugin-sdk/artifact-document");
22
+ const dashboard_service_1 = require("./dashboard.service");
23
+ const dashboard_schema_1 = require("./dashboard.schema");
24
+ const TTL = 86400, MAX_RUNTIME = 10 * 60_000, WAIT_MS = 20_000;
25
+ const hash = (value) => (0, node_crypto_1.createHash)('sha256').update(value).digest('hex');
26
+ const key = (scope, id) => {
27
+ if (!scope.tenantId || !scope.organizationId)
28
+ throw new Error('DASHBOARD_SAVE_REQUEST_DENIED');
29
+ return `live_dashboard:save:${scope.tenantId}:${scope.organizationId}:${id}`;
30
+ };
31
+ const contextSchema = v3_1.z.object({ tenantId: v3_1.z.string().uuid(), organizationId: v3_1.z.string().uuid(), userId: v3_1.z.string().uuid(),
32
+ hostType: v3_1.z.enum(['agent', 'project']), hostId: v3_1.z.string().uuid(), projectId: v3_1.z.string().uuid().optional() }).strict();
33
+ const fileSchema = v3_1.z.object({ filePath: v3_1.z.string(), workspacePath: v3_1.z.string(), sha256: v3_1.z.string(), size: v3_1.z.number() }).strict();
34
+ const receiptSchema = v3_1.z.object({ artifactId: v3_1.z.string().uuid(), draftId: v3_1.z.string(), draftRevision: v3_1.z.number(), baseVersionId: v3_1.z.string().nullable(), status: v3_1.z.literal('draft'), title: v3_1.z.string() });
35
+ const sourceFileSchema = v3_1.z.object({ path: v3_1.z.string().max(300), sha256: v3_1.z.string().regex(/^[a-f0-9]{64}$/) }).strict();
36
+ const recordSchema = v3_1.z.object({ requestId: v3_1.z.string().uuid(), jobId: v3_1.z.string(), context: contextSchema, file: fileSchema,
37
+ manifestId: v3_1.z.string().uuid(), expectedRevision: v3_1.z.number(), state: v3_1.z.enum(['queued', 'running', 'succeeded', 'failed']), stage: v3_1.z.string(), completed: v3_1.z.number(), total: v3_1.z.number(),
38
+ fingerprint: v3_1.z.string(), createdAt: v3_1.z.number(), deadline: v3_1.z.number(), error: v3_1.z.string().optional(), dashboard: receiptSchema.optional(), sourceFile: sourceFileSchema.optional() }).strict();
39
+ const inputSchema = v3_1.z.object({ manifest: dashboard_schema_1.dashboardSchema, expectedVersionId: v3_1.z.string().uuid().nullable(), expectedRevision: v3_1.z.number().int().nonnegative(),
40
+ dependencies: artifact_document_1.artifactDocumentDependencyAssets.optional() }).strict();
41
+ exports.saveRequestSchema = v3_1.z.object({ requestId: v3_1.z.string().uuid() }).strict();
42
+ exports.waitSaveRequestSchema = exports.saveRequestSchema.extend({ cursor: v3_1.z.string().min(1).max(100) }).strict();
43
+ function trustedScope(context) {
44
+ return contextSchema.parse({ tenantId: context.tenantId, organizationId: context.organizationId, userId: context.userId,
45
+ hostType: context.hostType, hostId: context.hostId,
46
+ ...(context.hostType === 'project' ? { projectId: context.hostId } : context.runtimeScope?.projectId ? { projectId: context.runtimeScope.projectId } : {}) });
47
+ }
48
+ function restoredScope(scope) {
49
+ return { ...scope, slots: [], ...(scope.projectId ? { runtimeScope: { projectId: scope.projectId, conversationId: null,
50
+ dataScopeKey: `project:${scope.projectId}`, workspaceFiles: { catalog: 'projects', scopeId: scope.projectId, projectId: scope.projectId } } } : {}) };
51
+ }
52
+ /** Durable save orchestration; domain validation and CAS remain in DashboardService. */
53
+ let DashboardSaveRequests = class DashboardSaveRequests {
54
+ constructor(service, runtime, queue) {
55
+ this.service = service;
56
+ this.runtime = runtime;
57
+ this.queue = queue;
58
+ }
59
+ async submit(context, manifest, expectedVersionId, expectedRevision, idempotencyKey, sourceFile, dependencies) {
60
+ await this.service.authorizeProject(context, 'write');
61
+ const scope = trustedScope(context), parsed = inputSchema.parse({ manifest, expectedVersionId, expectedRevision, dependencies });
62
+ const fingerprint = hash(JSON.stringify({ ...parsed, manifest: { ...parsed.manifest, ...(expectedRevision === 0 ? { id: 'new' } : {}) }, sourceFile }));
63
+ const previousCount = await this.service.previousBindingCount(context, parsed.manifest);
64
+ this.service.assertCapacity(parsed.manifest.bindings.length, previousCount);
65
+ const redis = await this.queue.getRedis(), files = this.runtime.require(plugin_sdk_1.WorkspaceFilesRuntimeCapability);
66
+ const dedupe = `${key(scope, 'submission')}:${hash(JSON.stringify([scope, idempotencyKey]))}`;
67
+ const previous = await redis.get(dedupe);
68
+ if (previous) {
69
+ if ((await this.read(context, previous)).fingerprint !== fingerprint)
70
+ throw new Error('DASHBOARD_SAVE_IDEMPOTENCY_CONFLICT');
71
+ return this.get(context, previous);
72
+ }
73
+ const requestId = (0, node_crypto_1.randomUUID)(), buffer = Buffer.from(JSON.stringify(parsed));
74
+ if (buffer.length > 12_000_000)
75
+ throw new Error('DASHBOARD_MANIFEST_TOO_LARGE');
76
+ const stored = await files.uploadBuffer({ tenantId: scope.tenantId, userId: scope.userId, catalog: 'users', buffer,
77
+ originalName: 'save.json', fileName: 'save.json', folder: `live-dashboard-save/${requestId}`, mimeType: 'application/json' });
78
+ const record = { requestId, jobId: `dashboard-save-${requestId}`, context: scope,
79
+ file: { filePath: stored.filePath, workspacePath: stored.workspacePath, sha256: hash(buffer), size: buffer.length },
80
+ fingerprint, manifestId: manifest.id, expectedRevision, state: 'queued', stage: 'queued', completed: 0, total: manifest.bindings.length, createdAt: Date.now(), deadline: Date.now() + MAX_RUNTIME,
81
+ ...(sourceFile ? { sourceFile: sourceFileSchema.parse(sourceFile) } : {}) };
82
+ const winner = await redis.eval("local id=redis.call('GET',KEYS[1]); if id then return id end; redis.call('SET',KEYS[2],ARGV[2],'EX',ARGV[3]); redis.call('SET',KEYS[1],ARGV[1],'EX',ARGV[3]); return ARGV[1]", 2, dedupe, key(scope, requestId), requestId, JSON.stringify(record), TTL);
83
+ if (winner !== requestId) {
84
+ await files.deleteFile(this.reference(record));
85
+ if ((await this.read(context, String(winner))).fingerprint !== fingerprint)
86
+ throw new Error('DASHBOARD_SAVE_IDEMPOTENCY_CONFLICT');
87
+ return this.get(context, String(winner));
88
+ }
89
+ // Recovery can enqueue the same deterministic job if the process stops at this boundary.
90
+ await this.enqueue(record);
91
+ return this.snapshot(record);
92
+ }
93
+ reference(record) {
94
+ return { source: 'platform.workspace.files', tenantId: record.context.tenantId, userId: record.context.userId, catalog: 'users',
95
+ filePath: record.file.filePath, workspacePath: record.file.workspacePath };
96
+ }
97
+ async enqueue(record) {
98
+ await this.queue.enqueue({ pluginName: dashboard_service_1.PLUGIN_NAME, queueName: 'save-validation', jobName: 'save', payload: { requestId: record.requestId },
99
+ tenantId: record.context.tenantId, organizationId: record.context.organizationId, userId: record.context.userId,
100
+ scopeKey: 'system:global', jobId: record.jobId, attempts: 1, removeOnComplete: { age: TTL, count: 1000 }, removeOnFail: { age: TTL, count: 1000 } });
101
+ }
102
+ async read(context, requestId) {
103
+ v3_1.z.string().uuid().parse(requestId);
104
+ const redis = await this.queue.getRedis(), value = await redis.get(key(context, requestId));
105
+ if (!value)
106
+ throw new Error('DASHBOARD_SAVE_REQUEST_UNAVAILABLE');
107
+ const record = recordSchema.parse(JSON.parse(value));
108
+ if (JSON.stringify(trustedScope(context)) !== JSON.stringify(record.context))
109
+ throw new Error('DASHBOARD_SAVE_REQUEST_DENIED');
110
+ return record;
111
+ }
112
+ snapshot(record) {
113
+ const terminal = record.state === 'succeeded' || record.state === 'failed';
114
+ const result = { requestId: record.requestId, state: record.state, stage: record.stage, completed: record.completed, total: record.total,
115
+ terminal, ...(record.dashboard ? { dashboard: record.dashboard, ...(record.sourceFile ? { sourceFile: record.sourceFile } : {}) } : {}),
116
+ ...(record.error ? { error: record.error } : {}), nextAction: terminal ? 'report_result' : 'wait' };
117
+ return { ...result, cursor: hash(JSON.stringify(result)) };
118
+ }
119
+ async get(context, requestId) {
120
+ await this.service.authorizeProject(context);
121
+ let record = await this.read(context, requestId);
122
+ if (record.state === 'queued' || record.state === 'running') {
123
+ const job = await this.queue.getJob({ jobId: record.jobId });
124
+ if (!job && record.deadline > Date.now())
125
+ await this.enqueue(record);
126
+ else if (job?.state === 'failed' || !job && record.deadline <= Date.now()) {
127
+ // A completed domain receipt always wins a racing observation.
128
+ record = await this.read(context, requestId);
129
+ if (record.state !== 'succeeded') {
130
+ const receipt = await this.service.savedRequestReceipt(context, record.manifestId, record.requestId, record.expectedRevision);
131
+ record = receipt ? { ...record, state: 'succeeded', stage: 'saved', completed: record.total, dashboard: receipt }
132
+ : { ...record, state: 'failed', error: 'DASHBOARD_SAVE_JOB_FAILED: retrieve the current draft before retrying; no new save receipt was confirmed.' };
133
+ await (await this.queue.getRedis()).set(key(record.context, requestId), JSON.stringify(record), 'EX', TTL);
134
+ await this.runtime.require(plugin_sdk_1.WorkspaceFilesRuntimeCapability).deleteFile(this.reference(record)).catch(() => undefined);
135
+ }
136
+ }
137
+ }
138
+ return this.snapshot(record);
139
+ }
140
+ async wait(context, requestId, cursor, signal) {
141
+ const until = Date.now() + WAIT_MS;
142
+ let current = await this.get(context, requestId);
143
+ while (!current.terminal && current.cursor === cursor && Date.now() < until) {
144
+ await (0, promises_1.setTimeout)(Math.min(2000, until - Date.now()), undefined, { signal });
145
+ current = this.snapshot(await this.read(context, requestId));
146
+ }
147
+ return current.terminal || current.cursor !== cursor ? current : this.get(context, requestId);
148
+ }
149
+ async handle(job, identity) {
150
+ const requestId = v3_1.z.string().uuid().parse(job.data.requestId), redis = await this.queue.getRedis();
151
+ let record = recordSchema.parse(JSON.parse(await redis.get(key(identity, requestId)) ?? 'null'));
152
+ if (record.context.tenantId !== identity.tenantId || record.context.organizationId !== identity.organizationId || record.context.userId !== identity.userId)
153
+ throw new Error('DASHBOARD_SAVE_REQUEST_DENIED');
154
+ if (record.state === 'succeeded')
155
+ return;
156
+ let committed = false, terminalPersisted = false;
157
+ const persist = async () => { await redis.set(key(record.context, requestId), JSON.stringify(record), 'EX', TTL); };
158
+ const files = this.runtime.require(plugin_sdk_1.WorkspaceFilesRuntimeCapability);
159
+ try {
160
+ const file = await files.readBuffer(this.reference(record));
161
+ if (file.buffer.length !== record.file.size || hash(file.buffer) !== record.file.sha256)
162
+ throw new Error('DASHBOARD_SAVE_INPUT_CHANGED');
163
+ const input = inputSchema.parse(JSON.parse(file.buffer.toString('utf8')));
164
+ const dashboard = await this.service.save(restoredScope(record.context), input.manifest, input.expectedVersionId, input.expectedRevision, input.dependencies, { requestId, progress: async (stage, completed, total) => {
165
+ if (Date.now() >= record.deadline)
166
+ throw new Error('DASHBOARD_SAVE_DEADLINE');
167
+ record = { ...record, state: 'running', stage, completed, total };
168
+ await persist();
169
+ } });
170
+ committed = true;
171
+ record = { ...record, state: 'succeeded', stage: 'saved', completed: record.total, dashboard };
172
+ await persist();
173
+ terminalPersisted = true;
174
+ }
175
+ catch (error) {
176
+ // Keep frozen input for recovery when the Artifact CAS succeeded but status persistence did not.
177
+ if (committed)
178
+ throw new Error('DASHBOARD_SAVE_COMMIT_UNCONFIRMED');
179
+ // Do not persist native provider messages or private query configuration.
180
+ const code = error instanceof Error ? error.message.match(/^DASHBOARD_[A-Z_]+/)?.[0] : undefined;
181
+ record = { ...record, state: 'failed', error: code ?? 'DASHBOARD_SAVE_VALIDATION_FAILED' };
182
+ await persist();
183
+ terminalPersisted = true;
184
+ throw new Error(record.error);
185
+ }
186
+ finally {
187
+ if (terminalPersisted)
188
+ await files.deleteFile(this.reference(record)).catch(() => undefined);
189
+ }
190
+ }
191
+ };
192
+ exports.DashboardSaveRequests = DashboardSaveRequests;
193
+ exports.DashboardSaveRequests = DashboardSaveRequests = __decorate([
194
+ (0, common_1.Injectable)(),
195
+ (0, plugin_sdk_1.PluginJobProcessor)({ pluginName: dashboard_service_1.PLUGIN_NAME, queueName: 'save-validation', jobName: 'save', concurrency: 2 }),
196
+ __param(1, (0, common_1.Inject)(plugin_sdk_1.XPERT_RUNTIME_CAPABILITIES_TOKEN)),
197
+ __param(2, (0, common_1.Inject)(plugin_sdk_1.MANAGED_QUEUE_SERVICE_TOKEN)),
198
+ __metadata("design:paramtypes", [dashboard_service_1.DashboardService, Object, Object])
199
+ ], DashboardSaveRequests);