@xpert-ai/plugin-live-dashboard 0.2.0 → 0.3.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.
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "sources": {
3
- ".xpertai-plugin/plugin.json": "8b4dae57dad788202e1df7d3201b0fed111f2a3546859cce5f1a9fbcc77caf56",
4
- "package.json": "0ff6c6f2749692dd7cfaf449987a741ccd2fea2c481b194beeaeb942781df570",
5
- "scripts/build-action.mjs": "283f26ef3c1c2ddf7f3f2bfc7a7780229b4d7061f1e3f1b0f222d012b78583de",
3
+ ".xpertai-plugin/plugin.json": "e83bedba6954e48497f37638f59eae5c7214c23ca16c9c18806b02ccf74b1500",
4
+ "package.json": "9543695d680f899db6082ae376c1653c613ac99f0eb61ddaba2e968cd6ecd8ea",
5
+ "scripts/build-action.mjs": "3a3de4d7fd05d81f5fdeb9eacbd04235dcc6e5000c6fff2d07f0e4bdc7b40c30",
6
6
  "scripts/build-libraries.mjs": "20bd34ca3f8bb6e87df2637a3cca85496962688c8ea4ceff5bc294ebd2de704d",
7
7
  "scripts/build-remote.mjs": "492550ac5c95eb371253d863567723d8853522013a0bcdfa0e264720206fda16",
8
8
  "scripts/dist-manifest.mjs": "8dcca9f1e74c520171649e848f9b5570ca9cc83fb5e8d2f265be3d2e5ceb970c",
@@ -24,9 +24,9 @@
24
24
  "src/lib/dashboard-receipt.ts": "d02b7e4e1be0a075b7f27e887ebb555684f17036ebb9442982e6725ab37a53ab",
25
25
  "src/lib/dashboard-search.ts": "0cd14c0004fb078b54c095faeab3195fb7e03a20c3400b5e67aec7cb4b2442e2",
26
26
  "src/lib/dashboard-sharing.ts": "61199f135cc53b6b9f0bef0f75f14c4d40bff6a3986e62e2823ab80c564534d3",
27
- "src/lib/dashboard-view.schema.ts": "11562ce17c573611eedd5ab5d0db378c6ae676955c1b3f8606d3e60b367ed05e",
27
+ "src/lib/dashboard-view.schema.ts": "4db0f043e836a1e050e8621074e5370e00507f5c6862ab9e2d624001c0452f26",
28
28
  "src/lib/dashboard.schema.ts": "982e15591e70102ea506951cd10de9ec4864281157b0b95a7b591a8fff3da813",
29
- "src/lib/dashboard.service.ts": "2a404e66ec95877ab37b55ce05dd024049c9d556e52a1a780bedcc45c425ac31",
29
+ "src/lib/dashboard.service.ts": "8b61384e2e655821dcbd427f3e393c51b90d5d023ee50e078f1038617ef26dcb",
30
30
  "src/lib/definition-tools.ts": "ba48842396e57499331a52ca7cf5e4e1eb72e1e519b5dabfff2743fddf15947d",
31
31
  "src/lib/document-host.ts": "3543ddb443006b90cd4689b65446b0e3b3305362d3d24d93209c094967921ffe",
32
32
  "src/lib/document-libraries.ts": "9c228995c509dc7f606cdc70d972ddb22df0164df4e0070583d0ab901f6605d0",
@@ -36,24 +36,25 @@
36
36
  "src/lib/filter-evidence.ts": "594ea9065e5c8d32c6bd4fc055e78c2df7ad2418c2019106136917c89c8ac331",
37
37
  "src/lib/html-guidance.ts": "0bd0b830b33045357a070c2079b2aedaadb57807ba366be56b767b7d55120325",
38
38
  "src/lib/html-validation.ts": "9f342f6716d457ba1a46c9d228f3f7858dcae2c0f64a3c98bc1d28d35a8f43ac",
39
- "src/lib/middleware.ts": "d1173e3b058778fb6e569f861ddec1ffc3c36a4fb46df71b4652bfc8fc74115a",
39
+ "src/lib/middleware.ts": "962c6c40e41995899099aaf0977787a85533c9071be20d91d52041e676961a07",
40
40
  "src/lib/plugin.ts": "c2788944df7697802350cde7fcc292a49b433b5176c7f8eec6a9809fbdf4ced7",
41
41
  "src/lib/presentation.provider.ts": "3b37387636d9e7317ee6e36c9e607fadb20002c08c24d54e424342e913b2405f",
42
42
  "src/lib/preview-retry.ts": "55fa28cc61f11adac4dce5a2066cbdd6d529ffd5ffd3cca7dcb552c6b7299500",
43
+ "src/lib/prompt-workflows.ts": "2782167c3e28fbda9be7a7312a68621a98bc120e652f0f985f57160df8110ce6",
43
44
  "src/lib/remote-entry.ts": "4c760963ff099b4d9a8961c933b1a586c0ac57d85a88c745c2578e2f66bf1539",
44
45
  "src/lib/renderer.ts": "418ac7cc5f37f9d638c0b2dd3a5361464674f21dbfb801b4414a9d68f1c9edc8",
45
46
  "src/lib/resource-card.ts": "d64f91e640a3db220b941adef4a582e011a7aea253d57cc5d70a5bdcdacd8f71",
46
47
  "src/lib/save-evidence.ts": "458f8fe2ef74fd4809fb9543d8c31bb42e0e16b10550b2a7808ca92ec3662bda",
47
48
  "src/lib/sharing.service.ts": "e52c98d967f27a2f3bc80614f31da15e373dd4450b80aea250156efe24c77f3b",
48
- "src/lib/view.provider.ts": "ba4087458334cbf71a256232695f1b56858926200f9430d79e95d5ac71fe1a26",
49
- "src/remote/bridge.ts": "c191bc1876e11b44397b0fb41f4a0ecd7842b9a43dbc5eeddfb05ee21df72421",
50
- "src/remote/dashboard-picker.tsx": "c84c147aeaa0774bd3f2bcd8b9502be4a78111167b01867e167c9cd72d263800",
49
+ "src/lib/view.provider.ts": "5bafa599cb1d618fc8256db0eb3360533f888b2cd63cd012cdd3ae45edc4da41",
50
+ "src/remote/bridge.ts": "6c213ac74daf8efee87100a94396fb13728027f23cfc189e65c524985235aea7",
51
+ "src/remote/dashboard-picker.tsx": "ffd5ddf54469a439bcdbabe974c0764de967a1f2716ea1304625fb6c54f67461",
51
52
  "src/remote/html-dashboard.tsx": "3e7eba0f8f760cd5334735a790278eea51beaa33ad9ff36a31b560bf7a0ed20d",
52
53
  "src/remote/i18n.ts": "65bb7941903e0a0e6445836cdf25f22e30d875d709fb07d60ac2d88d591b7218",
53
- "src/remote/locales/en.json": "f5090ccb4f8117082d79bec2da7d508b50602151da89f5867eb22008a419a9bf",
54
- "src/remote/locales/zh-Hans.json": "8df4134a625b2f1fa07c73834bc208f451dab40b0a73200bffaf025b55c51b41",
55
- "src/remote/locales/zh-Hant.json": "290c2f16c915a2c648ef58b5817d4cd2b007a88ab8ce809b11dd1bb2580807d9",
56
- "src/remote/main.tsx": "4c9e9ac143e8508558f5721fd9f5574a7fb6ecdda5b5b4469702d3a95b90f669",
54
+ "src/remote/locales/en.json": "8564f0f61fa0e4739e7ee2d55a23b7ff23cbfd40a8a18eeb2c32bdf15abe9a2e",
55
+ "src/remote/locales/zh-Hans.json": "5060899f01c9da602d0de4a84548de0115246c75f2a1e2f690beefa0f44f9913",
56
+ "src/remote/locales/zh-Hant.json": "123c66ae7f1b4ff4cb0001e225164193b0eb3a5069d0768879284ee96a926855",
57
+ "src/remote/main.tsx": "33ec783a2087a37126aca5792bdbb95bd656cecd034674e7c5bf29175d218125",
57
58
  "src/remote/share-dialog.tsx": "f4a78a3bbf99d49c84e10631be9a9bcd3dc11b42130ce79e00010e53fbc2af84",
58
59
  "src/remote/style.css": "8491fc393bacecd73cff2295526769cf73495d7842ca1ce57fb1abd0b7b5b60f",
59
60
  "src/remote/viewer-controls.tsx": "b55e27a8a7932575a994fe30645b535c3dd5ba7e27df1f18a1f86462a6354ca3",
@@ -61,8 +62,8 @@
61
62
  "tsconfig.remote.json": "e0b173f885c7439dfce09d411ff97fb7511bd77f6f1d7fdd66b5adb477a1f411"
62
63
  },
63
64
  "outputs": {
64
- "app.js": "fef3c4052f17a6629c6c5c57bef6daab106172e29677c4c4fb9333966406b5d6",
65
- "app.css": "418aaf0742026be83a2abb385204341b9726db63a1944c936d2c8d94263f0a0b",
65
+ "app.js": "06a6ef10aa3a96cb333c6a7772a8cbf03571e1edf0aba34b92ec8024420d7cfa",
66
+ "app.css": "3584fe18b974747f90b1adefcb9fe4519870f2d04e194723fb83a15dff36ce11",
66
67
  "renderer.html": "30abe65cd0d32a7596e02c5e94f2e20879fa73644507fefa35c7119a018ff7d2"
67
68
  },
68
69
  "libraries": {
@@ -6,5 +6,5 @@
6
6
  "playwrightVersion": "1.61.0",
7
7
  "bundle": "./bundle",
8
8
  "entrypoint": "runner.mjs",
9
- "bundleSha256": "de79ec597b5b7984c7f6cbd84832881bd5fccc32d4dc84e012eae6c090e8f539"
9
+ "bundleSha256": "81ed423dae0f11964c5c48ddec4cd64b1a72c202fbef9c19507a13cffff0e870"
10
10
  }
@@ -0,0 +1,39 @@
1
+ // Reuse successful static resource bytes only within one validation. Every request
2
+ // still passes the document resource policy; redirects are never fetched out of band.
3
+ export function createResourceCache() {
4
+ const admitted = new WeakSet(), entries = new Map()
5
+ const maxResourceBytes = 8 * 1024 * 1024, maxTotalBytes = 32 * 1024 * 1024
6
+ let totalBytes = 0
7
+ const key = request => `${request.resourceType()}:${request.url().split('#')[0]}`
8
+ return {
9
+ async route(page, route) {
10
+ const request = route.request()
11
+ const allowed = request.method() === 'GET' && await page.evaluate(({ type, url }) =>
12
+ DocumentHost.isDashboardResourceAllowed(type, url), { type: request.resourceType(), url: request.url() })
13
+ if (!allowed) return route.abort('blockedbyclient')
14
+ admitted.add(request)
15
+ const cached = await entries.get(key(request))
16
+ if (cached) return route.fulfill(cached)
17
+ return route.continue()
18
+ },
19
+ capture(response) {
20
+ const request = response.request(), id = key(request), headers = response.headers()
21
+ if (!admitted.has(request) || response.status() !== 200 || entries.has(id) || entries.size >= 128 ||
22
+ headers['set-cookie'] || /(?:^|,)\s*no-store(?:,|$)/i.test(headers['cache-control'] ?? '') ||
23
+ headers.vary === '*' || Number(headers['content-length']) > maxResourceBytes) return
24
+ const pending = (async () => {
25
+ try {
26
+ const body = await response.body()
27
+ if (body.length > maxResourceBytes || totalBytes + body.length > maxTotalBytes) return
28
+ totalBytes += body.length
29
+ // Playwright exposes decoded bytes; do not replay wire compression/length.
30
+ const replayHeaders = { ...headers }
31
+ for (const name of ['content-encoding', 'content-length', 'transfer-encoding']) delete replayHeaders[name]
32
+ return { status: 200, headers: replayHeaders, body }
33
+ } catch { return undefined } // A failed load is reported by the page, never cached.
34
+ })()
35
+ entries.set(id, pending)
36
+ },
37
+ async settle() { await Promise.all(entries.values()) }
38
+ }
39
+ }
@@ -1,18 +1,58 @@
1
- export async function validateDocument(input, browser, hostPath) {
2
- const errors = [], addError = value => { const message = String(value).slice(0, 300); if (errors.length < 16 && !errors.includes(message)) errors.push(message) }
3
- const loadedResources = new Set()
1
+ // Invariants: the SDK's per-document rate/concurrency limits also apply in preflight.
2
+ // Independent synthetic interactions need fresh mounts, not a larger production quota.
3
+ // Each scenario still validates initial load, its interaction sequence and host refresh.
4
+ import { createResourceCache } from './resource-cache.mjs'
5
+
6
+ export async function validateDocument(input, browser, hostPath, { timeoutMs = 55000 } = {}) {
4
7
  const context = await browser.newContext({ viewport: { width: 1280, height: 900 }, serviceWorkers: 'block' })
8
+ const resources = createResourceCache()
9
+ let timer
10
+ const validation = validateScenarios(input, context, hostPath, resources)
5
11
  try {
12
+ // Leave time for browser/report cleanup within the host's 60-second Action budget.
13
+ return await Promise.race([validation, new Promise(resolve => {
14
+ timer = setTimeout(() => resolve({ ok: false, errors: ['DOCUMENT_PREFLIGHT_TIMEOUT'],
15
+ requestedBindings: [], renderedTextLength: 0 }), timeoutMs)
16
+ })])
17
+ } finally {
18
+ clearTimeout(timer)
19
+ await context.close()
20
+ await validation.catch(() => {}) // Closing the context cancels outstanding Playwright operations.
21
+ }
22
+ }
23
+
24
+ async function validateScenarios(input, context, hostPath, resources) {
25
+ const initial = await validateScenario(input, context, hostPath, resources, { type: 'refresh' })
26
+ if (!initial.report.ok) return initial.report
27
+ const scenarios = [
28
+ ...Array.from({ length: input.parameters ? Math.min(initial.selectCount, 24) : 0 }, (_, index) => ({ type: 'select', index })),
29
+ ...Array.from({ length: Math.min(initial.buttonCount, 12) }, (_, index) => ({ type: 'button', index }))
30
+ ]
31
+ const requestedBindings = new Set(initial.report.requestedBindings)
32
+ // Bound live pages while keeping the full control/button sweep within the Action deadline.
33
+ for (let offset = 0; offset < scenarios.length; offset += 2) {
34
+ const results = await Promise.allSettled(scenarios.slice(offset, offset + 2).map(scenario =>
35
+ validateScenario(input, context, hostPath, resources, scenario)))
36
+ // Settle both pages before closing the context, including on infrastructure errors.
37
+ for (const result of results) {
38
+ if (result.status === 'rejected') throw result.reason
39
+ const { report } = result.value
40
+ report.requestedBindings.forEach(id => requestedBindings.add(id))
41
+ if (!report.ok) return { ...report, requestedBindings: [...requestedBindings] }
42
+ }
43
+ }
44
+ return { ...initial.report, requestedBindings: [...requestedBindings] }
45
+ }
46
+
47
+ async function validateScenario(input, context, hostPath, resources, scenario) {
48
+ const errors = [], addError = value => { const message = String(value).slice(0, 300); if (errors.length < 16 && !errors.includes(message)) errors.push(message) }
49
+ const loadedResources = new Set()
6
50
  const page = await context.newPage()
7
- await page.route('**/*', async route => {
8
- const request = route.request()
9
- const allowed = request.method() === 'GET' && await page.evaluate(({ type, url }) =>
10
- DocumentHost.isDashboardResourceAllowed(type, url), { type: request.resourceType(), url: request.url() })
11
- if (allowed) await route.continue()
12
- else await route.abort('blockedbyclient')
13
- })
51
+ try {
52
+ await page.route('**/*', route => resources.route(page, route))
14
53
  page.on('requestfailed', request => addError(`DOCUMENT_RESOURCE_LOAD_FAILED: ${request.resourceType()} ${request.url().split('?')[0]} (${request.failure()?.errorText ?? 'request failed'})`))
15
54
  page.on('response', response => {
55
+ resources.capture(response)
16
56
  if (response.status() >= 400) addError(`DOCUMENT_RESOURCE_HTTP_ERROR: ${response.status()} ${response.url().split('?')[0]}`)
17
57
  else if (response.status() < 300 || response.status() === 304) {
18
58
  for (let request = response.request(); request; request = request.redirectedFrom()) loadedResources.add(request.url().split('#')[0])
@@ -79,9 +119,10 @@ const errors = [], addError = value => { const message = String(value).slice(0,
79
119
  }
80
120
  // Exercise bounded native selects against the saved parameter contract; data remains the real default preview.
81
121
  // This detects invalid filter wiring, not the upstream effect of a filter or arbitrary custom controls.
82
- if (frame && input.parameters) {
122
+ if (frame && input.parameters && scenario.type === 'select') {
83
123
  const selects = frame.locator('select')
84
- for (let index = 0; index < Math.min(await selects.count(), 24); index++) {
124
+ if (scenario.index >= await selects.count()) addError('DOCUMENT_SELECT_INTERACTION_FAILED')
125
+ for (let index = scenario.index; index < Math.min(await selects.count(), scenario.index + 1); index++) {
85
126
  const select = selects.nth(index)
86
127
  if (!(await select.isVisible()) || !(await select.isEnabled())) continue
87
128
  const original = await select.inputValue()
@@ -138,14 +179,8 @@ const errors = [], addError = value => { const message = String(value).slice(0,
138
179
  }
139
180
  // Page buttons are untrusted executable UI too. Exercise a bounded set inside
140
181
  // the isolated preview; do not guess which button is refresh from its label.
141
- if (frame) {
142
- const buttons = frame.locator('button')
143
- for (let index = 0; index < Math.min(await buttons.count(), 12); index++) {
144
- const button = buttons.nth(index)
145
- if (!(await button.isVisible()) || !(await button.isEnabled())) continue
146
- await button.click({ timeout: 1000 }).catch(() => addError('DOCUMENT_BUTTON_INTERACTION_FAILED'))
147
- await page.waitForTimeout(200)
148
- }
182
+ if (frame && scenario.type === 'button') {
183
+ await exerciseButtonPath(frame, page, scenario.index, addError)
149
184
  }
150
185
  // Exercise the same refresh event as the Workbench. A renderer that fails on its
151
186
  // second Dataset must not pass merely because the first query painted a page.
@@ -156,6 +191,39 @@ const errors = [], addError = value => { const message = String(value).slice(0,
156
191
  const renderedTextLength = frame ? (await frame.locator('body').innerText()).trim().length : 0
157
192
  if (!renderedTextLength) addError('DOCUMENT_EMPTY_PAGE')
158
193
  if (!state.requestedBindings.length) addError('DOCUMENT_INITIAL_QUERY_MISSING')
159
- return { ok: !errors.length, errors, requestedBindings: [...new Set(state.requestedBindings)], renderedTextLength }
160
- } finally { await context.close() }
194
+ await resources.settle()
195
+ return {
196
+ report: { ok: !errors.length, errors, requestedBindings: [...new Set(state.requestedBindings)], renderedTextLength },
197
+ selectCount: frame ? await frame.locator('select').count() : 0,
198
+ buttonCount: frame ? await frame.locator('button').count() : 0
199
+ }
200
+ } finally { await page.close() }
201
+ }
202
+
203
+ async function actionableButtons(frame) {
204
+ const buttons = (await frame.locator('button').elementHandles()).slice(0, 12)
205
+ const actionable = []
206
+ for (const button of buttons)
207
+ if (await button.isVisible() && await button.isEnabled()) actionable.push(button)
208
+ return actionable
209
+ }
210
+
211
+ async function exerciseButtonPath(frame, page, index, addError) {
212
+ const root = (await frame.locator('button').elementHandles())[index]
213
+ if (!root) { addError('DOCUMENT_BUTTON_INTERACTION_FAILED'); return }
214
+ const pending = [root], clicked = []
215
+ while (pending.length) {
216
+ const button = pending.shift()
217
+ if (!(await button.isVisible()) || !(await button.isEnabled()) ||
218
+ await button.evaluate((element, previous) => previous.includes(element), clicked)) continue
219
+ if (clicked.length >= 12) { addError('DOCUMENT_BUTTON_PATH_LIMIT'); return }
220
+ const before = await actionableButtons(frame)
221
+ await button.click({ timeout: 1000 }).catch(() => addError('DOCUMENT_BUTTON_INTERACTION_FAILED'))
222
+ clicked.push(button)
223
+ await page.waitForTimeout(200)
224
+ // Follow newly inserted, revealed or enabled controls in this same document.
225
+ // Compare DOM identity, not labels or indexes that can shift after a click.
226
+ for (const next of await actionableButtons(frame))
227
+ if (!await next.evaluate((element, previous) => previous.includes(element), before)) pending.push(next)
228
+ }
161
229
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xpert-ai/plugin-live-dashboard",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Versioned live dashboards using platform analytics and Data Analytics renderer",
5
5
  "main": "./index.cjs",
6
6
  "types": "./dist/index.d.ts",