@databrainhq/mcp-server 0.3.0-rc.10 → 0.3.0-rc.11

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,17 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  registerChartApps
4
- } from "./chunk-5IGP6A6T.js";
4
+ } from "./chunk-NBFUKVWP.js";
5
5
  import {
6
6
  enableChartApps,
7
7
  registerAllPrompts,
8
8
  registerAllResources,
9
9
  registerAllTools
10
- } from "./chunk-ERTAKB2H.js";
10
+ } from "./chunk-ES2LUP4U.js";
11
11
  import {
12
12
  DatabrainClient,
13
13
  MCP_SERVER_VERSION
14
- } from "./chunk-OCBTNAAO.js";
14
+ } from "./chunk-RHK62ZJ6.js";
15
15
 
16
16
  // src/apps/server.ts
17
17
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -31,4 +31,4 @@ function createChartAppServer(config, backend) {
31
31
  export {
32
32
  createChartAppServer
33
33
  };
34
- //# sourceMappingURL=chunk-N7X762VU.js.map
34
+ //# sourceMappingURL=chunk-IBYBK5ZE.js.map
@@ -3,11 +3,14 @@ import {
3
3
  APPLY_TO_PRODUCTION_CONFIRMATION,
4
4
  APP_MIME,
5
5
  CHART_URI,
6
+ CUSTOM_BUNDLE_CHUNK_CHARS,
6
7
  KPI_URI,
8
+ MAX_CUSTOM_BUNDLE_CHARS,
7
9
  READ_ONLY,
8
10
  RENDER_TYPES,
9
11
  WRITE_CREATE,
10
12
  appMeta,
13
+ bounded,
11
14
  chartResult,
12
15
  createServiceBundle,
13
16
  draftPage,
@@ -18,15 +21,87 @@ import {
18
21
  saveDraftAsDashboard,
19
22
  viewSchema,
20
23
  workspacesForDatamart
21
- } from "./chunk-ERTAKB2H.js";
24
+ } from "./chunk-ES2LUP4U.js";
22
25
  import {
26
+ CustomChartsApi,
23
27
  DatabrainApiError,
24
28
  DatabrainClient,
25
29
  toolContext
26
- } from "./chunk-OCBTNAAO.js";
30
+ } from "./chunk-RHK62ZJ6.js";
31
+ import {
32
+ contentHash,
33
+ readCustomChartBrowserRuntime,
34
+ validateBundle
35
+ } from "./chunk-Q5EYDKSA.js";
27
36
 
28
- // src/apps/registry.ts
37
+ // src/apps/custom-chart-assets.ts
29
38
  import { z } from "zod";
39
+ var assets = /* @__PURE__ */ new Map();
40
+ function registerCustomChartAssets(server, client) {
41
+ const runtime = server.tool(
42
+ "load_custom_chart_runtime",
43
+ "Load the trusted app/embed sandbox adapter for a custom chart tile. App-only; no company data or chart code.",
44
+ {},
45
+ READ_ONLY,
46
+ async () => bounded({
47
+ content: [{ type: "text", text: "Custom chart sandbox runtime." }],
48
+ _meta: { "databrain/custom-chart-runtime": readCustomChartBrowserRuntime() }
49
+ })
50
+ );
51
+ runtime.update({ _meta: appMeta(["app"]) });
52
+ const api = new CustomChartsApi(client);
53
+ const tool = server.tool(
54
+ "load_custom_chart_bundle",
55
+ "Load one bounded chunk of the company published custom chart bundle for the tile. No draft version, credentials, or query rows.",
56
+ {
57
+ key: z.string().regex(/^[a-z][a-z0-9_-]{0,63}$/),
58
+ offset: z.number().int().min(0).max(MAX_CUSTOM_BUNDLE_CHARS).default(0),
59
+ contentHash: z.string().regex(/^[a-f0-9]{64}$/).optional()
60
+ },
61
+ READ_ONLY,
62
+ async ({ key, offset, contentHash: expected }) => {
63
+ try {
64
+ const detail = await api.get(key);
65
+ if (detail.status !== "published" || !detail.publishedVersionId) throw new Error("not published");
66
+ const publishedHash = detail.versions?.find((version) => version.id === detail.publishedVersionId)?.contentHash;
67
+ const cached = publishedHash && /^[a-f0-9]{64}$/.test(publishedHash) ? assets.get(publishedHash) : void 0;
68
+ let hash = publishedHash, asset = cached && cached.expires > Date.now() ? cached.text : void 0;
69
+ if (asset === void 0) {
70
+ const raw = await api.bundle(key, detail.publishedVersionId);
71
+ const bundle = { ...raw, bundleCss: raw.bundleCss ?? void 0 };
72
+ if (validateBundle(bundle).length || bundle.manifest.id !== key) throw new Error("invalid bundle");
73
+ hash = contentHash(bundle);
74
+ if (publishedHash && publishedHash !== hash) throw new Error("published hash mismatch");
75
+ asset = JSON.stringify({ manifest: bundle.manifest, bundleJs: bundle.bundleJs, bundleCss: bundle.bundleCss ?? "", iconSvg: bundle.iconSvg });
76
+ if (assets.size >= 4) assets.delete(assets.keys().next().value);
77
+ assets.set(hash, { text: asset, expires: Date.now() + 6e4 });
78
+ }
79
+ if (expected && hash !== expected) throw new Error("published version changed");
80
+ if (offset > asset.length) throw new Error("offset");
81
+ const chunk = asset.slice(offset, offset + CUSTOM_BUNDLE_CHUNK_CHARS);
82
+ return bounded({
83
+ content: [{ type: "text", text: "Published custom chart asset chunk." }],
84
+ structuredContent: {
85
+ key,
86
+ contentHash: hash,
87
+ offset,
88
+ chunk,
89
+ nextOffset: offset + chunk.length < asset.length ? offset + chunk.length : null
90
+ }
91
+ });
92
+ } catch {
93
+ return { isError: true, content: [{
94
+ type: "text",
95
+ text: `Could not load published custom:${key}. It may be unpublished, changed, or unavailable to this sign-in.`
96
+ }] };
97
+ }
98
+ }
99
+ );
100
+ tool.update({ _meta: appMeta(["app"]) });
101
+ }
102
+
103
+ // src/apps/registry.ts
104
+ import { z as z2 } from "zod";
30
105
 
31
106
  // src/apps/backend.ts
32
107
  var RENDER_PATH = "/api/v2/dashboard/mcp-chart";
@@ -88,9 +163,10 @@ function createChartBackend(client) {
88
163
  }
89
164
 
90
165
  // src/apps/tile.ts
166
+ import { transformSync } from "esbuild";
91
167
  var TILE_SCRIPT = String.raw`(()=>{
92
168
  const $=id=>document.getElementById(id),root=document.documentElement,pending=new Map();
93
- let seq=0,origin='*',ready=false,closed=false,ref=null,view={},next=null,context={},busy=false,revision=0,rawSvg=null,dialect=null,shown=false,draft=null,rows=[],saved=null,attempt='',workspace=null;
169
+ let seq=0,origin='*',ready=false,closed=false,ref=null,view={},next=null,context={},busy=false,revision=0,rawSvg=null,dialect=null,shown=false,draft=null,rows=[],saved=null,attempt='',workspace=null,custom=null,customMode=false;
94
170
  /* One lifecycle, two sets of names. Hosts ship either or both, so notifications go out
95
171
  under both and arrive under either. Same order in both rows: result, input, context. */
96
172
  const DASH=['ui/notifications/tool-result','ui/notifications/tool-input','ui/notifications/host-context-changed'],
@@ -128,7 +204,7 @@ img.src='data:image/svg+xml,'+encodeURIComponent(rawSvg.replace(/<\/svg>\s*$/i,'
128
204
  +'[stroke="#dbdee4"],[stroke="#f1f1f2" i]{stroke:'+mix(13)+'!important}[stroke="#54555a"],[stroke="#d4d4d8" i]{stroke:'+mix(30)+'!important}'
129
205
  +'</style></svg>'))}
130
206
  function controls(){document.querySelectorAll('button,input,select').forEach(e=>{e.disabled=busy});
131
- $('refresh').hidden=!ref;$('next').hidden=!next;$('range').hidden=!ref;$('plot').toggleAttribute('data-busy',busy);
207
+ $('refresh').hidden=!ref;$('next').hidden=!next;$('range').hidden=!ref||customMode;$('plot').toggleAttribute('data-busy',busy);
132
208
  $('save').hidden=!draft||!!saved||!$('form').hidden;
133
209
  $('expand').hidden=!shown||root.dataset.kind==='dashboard'||!context.availableDisplayModes?.includes('fullscreen');
134
210
  root.toggleAttribute('data-deep',context.displayMode==='fullscreen'||$('expand').hidden)}
@@ -139,11 +215,12 @@ const face=change.styles?.css?.fonts;if(typeof face==='string'&&face.length<4000
139
215
  const insets=change.safeAreaInsets;if(insets)for(const edge of ['top','right','bottom','left']){
140
216
  const n=insets[edge];if(Number.isFinite(n))root.style.setProperty('--safe-'+edge,Math.max(0,Math.min(n,200))+'px')}
141
217
  root.dataset.mode=context.displayMode==='fullscreen'?'full':'inline';
142
- $('expand').textContent=context.displayMode==='fullscreen'?'Back to conversation':'Expand';paint();controls()}
218
+ $('expand').textContent=context.displayMode==='fullscreen'?'Back to conversation':'Expand';paint();custom?.update();controls()}
143
219
  function scope(p,n){const bits=[];
144
220
  if(p.kind==='chart'||p.kind==='table')bits.push(p.rowsTotal>n?'first '+n+' of '+p.rowsTotal+' rows':n?(n===1?'1 row':n+' rows'):'No rows in this view');
145
221
  if(view.dateRange)bits.push(view.dateRange.from+' to '+view.dateRange.to);
146
222
  if(next)bits.push('more pages available');
223
+ if(p.customChart&&n)bits.push('calculations use this page');
147
224
  $('scope').textContent=bits.join(' · ')}
148
225
  function resize(){if(ready)notify(SIZE,{width:document.body.scrollWidth,height:document.body.scrollHeight})}
149
226
  function table(rows,columns){$('rows').replaceChildren();$('data').hidden=!rows?.length;if(!rows?.length)return;
@@ -175,7 +252,7 @@ if(shown&&$('plot').firstChild){stale();controls();return}shown=false;controls()
175
252
  status(ref?'Chart unavailable. Check your access to this dashboard, then Refresh.'
176
253
  :'Chart unavailable. Ask again to retry.',1);return}
177
254
  const p=result?.structuredContent;if(!p||typeof p!=='object'){shown=false;controls();status('No chart data came back for this result.',1);return}
178
- revision++;shown=true;rawSvg=null;$('plot').replaceChildren();$('plot').dataset.state='ready';$('choices').replaceChildren();delete $('rail').dataset.value;
255
+ revision++;custom?.dispose();custom=null;customMode=!!p.customChart;shown=true;rawSvg=null;$('plot').replaceChildren();$('plot').dataset.state='ready';$('choices').replaceChildren();delete $('rail').dataset.value;
179
256
  ref=p.reference||null;view=p.view||{};next=p.nextCursor||null;draft=p.draft||null;rows=p.rows||[];saved=null;$('form').hidden=true;
180
257
  $('title').textContent=p.title||'Analytics';$('description').textContent=answer(p);root.dataset.kind=p.kind;$('choices').toggleAttribute('data-grid',p.kind==='dashboard');
181
258
  if(p.kind==='dashboard'){$('plot').dataset.state='off';
@@ -183,6 +260,11 @@ for(const item of p.widgets||[]){const b=document.createElement('button');b.text
183
260
  status(p.widgets?.length?'':'No charts on this dashboard yet. Add one in Databrain.');
184
261
  }else if(p.kind==='kpi'){
185
262
  const number=document.createElement('p');number.className='kpi';number.textContent=fmt(p.value);exact(number,p.value);$('plot').append(number);status('');
263
+ }else if(p.customChart){
264
+ if(!rows.length){$('plot').dataset.state='off';status('No rows in this view.');}
265
+ else{status('Loading custom chart…');const rev=revision;
266
+ const box=document.createElement('div');box.style.width='100%';$('plot').append(box);
267
+ loadCustom(result,p,box,rev);}
186
268
  }else if(p.svg){
187
269
  const box=document.createElement('div');box.className='chart';box.style.aspectRatio=p.width+'/'+p.height;
188
270
  const img=document.createElement('img');img.id='chart-image';rawSvg=p.svg;img.alt=p.title||'Chart';img.onerror=()=>{status('The chart could not be drawn. The rows are below.',1);$('data').open=$('data').toggleAttribute('data-only',true)};box.append(img);
@@ -201,7 +283,31 @@ if(draft&&p.previewTypes?.length){for(const t of p.previewTypes){const b=documen
201
283
  b.onclick=()=>run('preview_chart',{draft,rows,chartType:t});$('choices').append(b)}$('choices').dataset.chips='1'}else delete $('choices').dataset.chips;
202
284
  if(p.previewError)status('Cannot draw this as '+(p.draft?.chartType||'a chart')+' ('+p.previewError+'). Showing the rows.',1);
203
285
  paint();table(p.rows||[],p.columns||Object.keys(p.rows?.[0]||{}));scope(p,p.rows?.length||0);
204
- const only=!p.svg&&p.value===undefined&&p.kind!=='dashboard';$('data').open=only;$('data').toggleAttribute('data-only',only);controls();resize()}
286
+ const only=!p.svg&&!p.customChart&&p.value===undefined&&p.kind!=='dashboard';$('data').open=only;$('data').toggleAttribute('data-only',only);controls();resize()}
287
+ async function loadCustom(result,p,box,rev){try{
288
+ if(!globalThis.DbnCustomChartRuntime){
289
+ const runtime=await rpc('tools/call',{name:'load_custom_chart_runtime',arguments:{context:'tile'}});
290
+ if(rev!==revision)return;
291
+ const code=runtime._meta?.['databrain/custom-chart-runtime'];
292
+ if(runtime.isError||typeof code!=='string'||code.length>64000)throw Error('runtime unavailable');
293
+ const script=document.createElement('script');script.textContent=code;document.head.append(script);script.remove();
294
+ if(!globalThis.DbnCustomChartRuntime)throw Error('runtime unavailable');
295
+ }
296
+ let bundle=result._meta?.['databrain/custom-chart-bundle'];
297
+ if(!bundle){let text='',offset=0,hash;
298
+ for(let chunks=0;chunks<${Math.ceil(MAX_CUSTOM_BUNDLE_CHARS / CUSTOM_BUNDLE_CHUNK_CHARS)};chunks++){
299
+ const r=await rpc('tools/call',{name:'load_custom_chart_bundle',arguments:{context:'tile',key:p.customChart.key,offset,...(hash?{contentHash:hash}:{})}});
300
+ if(rev!==revision)return;
301
+ if(r.isError)throw Error('unavailable');const part=r.structuredContent;
302
+ if(!part||part.key!==p.customChart.key||part.offset!==offset||typeof part.chunk!=='string'||part.chunk.length>${CUSTOM_BUNDLE_CHUNK_CHARS}||!part.chunk.length)throw Error('invalid chunk');
303
+ if(hash&&hash!==part.contentHash)throw Error('changed');hash=part.contentHash;text+=part.chunk;
304
+ if(text.length>${MAX_CUSTOM_BUNDLE_CHARS})throw Error('too large');if(part.nextOffset===null){bundle=JSON.parse(text);break}
305
+ if(part.nextOffset!==offset+part.chunk.length)throw Error('invalid offset');offset=part.nextOffset;
306
+ }
307
+ }
308
+ if(rev!==revision)return;if(!bundle||bundle.manifest.id!==p.customChart.key)throw Error('invalid bundle');
309
+ custom=DbnCustomChartRuntime.mount(box,bundle,p.customChart,p.rows||[],message=>{if(rev!==revision)return;status(message,!!message);if(message){$('data').open=true;$('data').setAttribute('data-only','')}resize()});
310
+ }catch{if(rev===revision){status(result._meta?.['databrain/custom-chart-bundle']?'Draft preview could not open. Check the bindings and sandbox support. The rows are below.':'Published custom chart unavailable, or this host cannot open its sandbox. The rows are below.',1);$('data').open=true;$('data').setAttribute('data-only','');resize()}}}
205
311
  $('refresh').onclick=()=>call('refresh_widget',{cursor:undefined});$('next').onclick=()=>call('render_chart',{cursor:next});
206
312
  function choose(names){const w=$('ws');w.replaceChildren();for(const n of names){const o=document.createElement('option');o.value=n;o.textContent=n;w.append(o)}$('wsrow').hidden=names.length<2;$('wsone').textContent=names.length===1?'in workspace '+names[0]:'';$('form').hidden=false;$('dtitle').focus();controls();resize()}
207
313
  $('save').onclick=()=>{attempt=crypto.randomUUID();$('dtitle').value=draft?.title||'';choose(draft?.workspaces||[])};
@@ -216,7 +322,7 @@ if(!m.method&&pending.has(m.id)&&('result'in m||'error'in m)){if(origin==='*'&&e
216
322
  const p=pending.get(m.id);pending.delete(m.id);clearTimeout(p.timer);
217
323
  if(m.error){const error=Error('host error');error.code=m.error.code;p.reject(error)}else p.resolve(m.result);return}
218
324
  if(m.method==='ping'&&m.id!==undefined){send({id:m.id,result:{}});return}
219
- if(m.method==='ui/resource-teardown'){send({id:m.id,result:{}});closed=true;revision++;for(const p of pending.values()){clearTimeout(p.timer);p.reject(Error('closed'))}pending.clear();observer.disconnect();removeEventListener('message',receive);return}
325
+ if(m.method==='ui/resource-teardown'){send({id:m.id,result:{}});closed=true;revision++;custom?.dispose();custom=null;for(const p of pending.values()){clearTimeout(p.timer);p.reject(Error('closed'))}pending.clear();observer.disconnect();removeEventListener('message',receive);return}
220
326
  if(!ready||m.id!==undefined)return;
221
327
  let slot=DASH.indexOf(m.method),kind='dashed';
222
328
  if(slot<0){slot=SNAKE.indexOf(m.method);kind='snake'}
@@ -229,16 +335,16 @@ if(slot===0)render(kind==='snake'?m.params&&m.params.result:m.params);
229
335
  else if(slot===1)wait();
230
336
  else{host(m.params||{});resize()}
231
337
  return}
232
- if(m.method==='ui/notifications/tool-cancelled'){revision++;busy=false;$('plot').dataset.state='off';status('Request cancelled.');controls()}}
338
+ if(m.method==='ui/notifications/tool-cancelled'){revision++;custom?.dispose();custom=null;busy=false;$('plot').dataset.state='off';status('Request cancelled.');controls()}}
233
339
  addEventListener('message',receive);const observer=new ResizeObserver(resize);observer.observe(document.body);controls();
234
340
  rpc('ui/initialize',{protocolVersion:'2026-01-26',appInfo:{name:'Databrain tile',version:'1.0.0'},appCapabilities:{availableDisplayModes:['inline','fullscreen']}})
235
341
  .then(r=>{if(r.protocolVersion!=='2026-01-26')throw Error('version');host(r.hostContext||{});ready=true;notify(INIT,{});resize()})
236
342
  .catch(()=>{trouble('initialize-failed');status('This chart needs an MCP Apps host.',1)});
237
343
  })();`;
238
- var SERVED_SCRIPT = TILE_SCRIPT.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "").replace(/\n{2,}/g, "\n");
344
+ var SERVED_SCRIPT = transformSync(TILE_SCRIPT, { minify: true, target: "es2020" }).code;
239
345
  function tileHtml(kind = "chart") {
240
346
  return strip(`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
241
- <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; font-src data:; connect-src 'none'; base-uri 'none'; form-action 'none'">
347
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; font-src data:; connect-src 'none'; frame-src blob:; base-uri 'none'; form-action 'none'">
242
348
  <title>Databrain ${kind} tile</title><style id="hostfont"></style><style>
243
349
  /* Host tokens are the Claude MCP Apps style-variable names. Every one carries a fallback:
244
350
  hosts may ship any subset, so nothing here may depend on a token existing. */
@@ -382,7 +488,7 @@ function registerChartApps(server, client, backend = createChartBackend(client))
382
488
  const tool = server.tool(name, description, {
383
489
  reference: referenceSchema,
384
490
  view: viewSchema.default({}),
385
- context: z.string().max(200).optional().describe("Chart interaction context")
491
+ context: z2.string().max(200).optional().describe("Chart interaction context")
386
492
  }, READ_ONLY, async ({ reference, view }) => {
387
493
  try {
388
494
  if (action === "drill_down" && !view.drillId || action === "change_date_range" && !view.dateRange) throw new Error("Missing view selector");
@@ -393,7 +499,7 @@ function registerChartApps(server, client, backend = createChartBackend(client))
393
499
  if (page.svg && (!/^\s*<svg\b/i.test(page.svg) || /<\s*(?:script|foreignObject|iframe|image|animate\w*|set)\b|<!DOCTYPE|<!ENTITY|\bon\w+\s*=|@(?:import|keyframes)|\banimation\s*:|(?:href\s*=\s*["'](?!#)|url\s*\(\s*["']?(?!#))/i.test(page.svg))) {
394
500
  throw new Error("Unsafe SVG");
395
501
  }
396
- return chartResult(`${page.kind === "kpi" ? "KPI" : "Chart"} ready. ${page.rows.length} rows in this page.`, {
502
+ return chartResult(`${page.kind === "kpi" ? "KPI" : "Chart"} ready. ${page.rows.length} rows in this page.${page.fallback ? " " + page.fallback : ""}`, {
397
503
  ...page,
398
504
  reference,
399
505
  view
@@ -408,9 +514,10 @@ function registerChartApps(server, client, backend = createChartBackend(client))
408
514
  tool.update({ _meta: appMeta(["app"]) });
409
515
  }
410
516
  registerDraftTools(server, client);
517
+ registerCustomChartAssets(server, client);
411
518
  for (const [uri, kind] of [[CHART_URI, "chart"], [KPI_URI, "kpi"]]) {
412
519
  server.resource(`Databrain ${kind} tile`, uri, { mimeType: APP_MIME, description: `Interactive ${kind} using host theme and scoped server tools` }, async () => ({
413
- contents: [{ uri, mimeType: APP_MIME, text: tileHtml(kind), _meta: { ui: { prefersBorder: true } } }]
520
+ contents: [{ uri, mimeType: APP_MIME, text: tileHtml(kind), _meta: { ui: { prefersBorder: true, csp: { resourceDomains: ["data:", "blob:"], frameDomains: ["blob:"], connectDomains: [] } } } }]
414
521
  }));
415
522
  }
416
523
  }
@@ -421,8 +528,8 @@ function registerDraftTools(server, client) {
421
528
  {
422
529
  draft: draftSchema,
423
530
  rows: rowsSchema,
424
- chartType: z.enum(RENDER_TYPES),
425
- context: z.string().max(200).optional().describe("Chart interaction context")
531
+ chartType: z2.enum(RENDER_TYPES),
532
+ context: z2.string().max(200).optional().describe("Chart interaction context")
426
533
  },
427
534
  READ_ONLY,
428
535
  async ({ draft, rows, chartType }) => {
@@ -440,12 +547,12 @@ function registerDraftTools(server, client) {
440
547
  `Save an ask_question answer: creates a workspace metric from its SQL and a new dashboard holding it. Production write; requires confirm: "${APPLY_TO_PRODUCTION_CONFIRMATION}". A workspaceName is needed when the datamart backs more than one workspace.`,
441
548
  {
442
549
  draft: draftSchema,
443
- chartType: z.enum(RENDER_TYPES),
444
- dashboardTitle: z.string().trim().min(1).max(200),
445
- workspaceName: z.string().trim().min(1).max(256).optional(),
446
- attemptId: z.string().trim().min(1).max(64).optional().describe("Identifies one save attempt. Reuse it to retry safely; omit or change it to save again on purpose."),
447
- confirm: z.string().optional(),
448
- context: z.string().max(200).optional().describe("Chart interaction context")
550
+ chartType: z2.enum(RENDER_TYPES),
551
+ dashboardTitle: z2.string().trim().min(1).max(200),
552
+ workspaceName: z2.string().trim().min(1).max(256).optional(),
553
+ attemptId: z2.string().trim().min(1).max(64).optional().describe("Identifies one save attempt. Reuse it to retry safely; omit or change it to save again on purpose."),
554
+ confirm: z2.string().optional(),
555
+ context: z2.string().max(200).optional().describe("Chart interaction context")
449
556
  },
450
557
  WRITE_CREATE,
451
558
  async ({ draft, chartType, dashboardTitle, workspaceName, attemptId, confirm }) => {
@@ -493,4 +600,4 @@ function registerDraftTools(server, client) {
493
600
  export {
494
601
  registerChartApps
495
602
  };
496
- //# sourceMappingURL=chunk-5IGP6A6T.js.map
603
+ //# sourceMappingURL=chunk-NBFUKVWP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/apps/custom-chart-assets.ts","../src/apps/registry.ts","../src/apps/backend.ts","../src/apps/tile.ts"],"sourcesContent":["import { z } from 'zod';\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { DatabrainClient } from '../client/databrain-client.js';\nimport { CustomChartsApi } from '../api/custom-charts-api.js';\nimport { contentHash, validateBundle } from '../custom-chart/bundle.js';\nimport { READ_ONLY } from '../tools/annotations.js';\nimport { appMeta } from './feature.js';\nimport { bounded, MAX_CUSTOM_BUNDLE_CHARS, CUSTOM_BUNDLE_CHUNK_CHARS } from './contract.js';\nimport { readCustomChartBrowserRuntime } from '../custom-chart/assets.js';\n\n// Content-addressed immutable artifacts only. Authorization and the published pointer/hash\n// are fetched live before every cache lookup; no principal or API response is cached.\nconst assets = new Map<string, { text: string; expires: number }>();\n\n/** Chunk large code assets; every chunk rechecks the live published pointer and caller. */\nexport function registerCustomChartAssets(server: McpServer, client: DatabrainClient): void {\n const runtime = server.tool('load_custom_chart_runtime',\n 'Load the trusted app/embed sandbox adapter for a custom chart tile. App-only; no company data or chart code.',\n {}, READ_ONLY, async () => bounded({ content: [{ type: 'text' as const, text: 'Custom chart sandbox runtime.' }],\n _meta: { 'databrain/custom-chart-runtime': readCustomChartBrowserRuntime() } }));\n runtime.update({ _meta: appMeta(['app']) });\n const api = new CustomChartsApi(client);\n const tool = server.tool('load_custom_chart_bundle',\n 'Load one bounded chunk of the company published custom chart bundle for the tile. No draft version, credentials, or query rows.',\n { key: z.string().regex(/^[a-z][a-z0-9_-]{0,63}$/), offset: z.number().int().min(0).max(MAX_CUSTOM_BUNDLE_CHARS).default(0),\n contentHash: z.string().regex(/^[a-f0-9]{64}$/).optional() }, READ_ONLY,\n async ({ key, offset, contentHash: expected }) => {\n try {\n const detail = await api.get(key);\n if (detail.status !== 'published' || !detail.publishedVersionId) throw new Error('not published');\n const publishedHash = detail.versions?.find(version => version.id === detail.publishedVersionId)?.contentHash;\n const cached = publishedHash && /^[a-f0-9]{64}$/.test(publishedHash) ? assets.get(publishedHash) : undefined;\n let hash = publishedHash, asset = cached && cached.expires > Date.now() ? cached.text : undefined;\n if (asset === undefined) {\n const raw = await api.bundle(key, detail.publishedVersionId);\n const bundle = { ...raw, bundleCss: raw.bundleCss ?? undefined };\n if (validateBundle(bundle).length || bundle.manifest.id !== key) throw new Error('invalid bundle');\n hash = contentHash(bundle);\n if (publishedHash && publishedHash !== hash) throw new Error('published hash mismatch');\n // Only runtime artifacts cross the bridge; never any API envelope or extra fields.\n asset = JSON.stringify({ manifest: bundle.manifest, bundleJs: bundle.bundleJs, bundleCss: bundle.bundleCss ?? '', iconSvg: bundle.iconSvg });\n if (assets.size >= 4) assets.delete(assets.keys().next().value!);\n assets.set(hash, { text: asset, expires: Date.now() + 60_000 });\n }\n if (expected && hash !== expected) throw new Error('published version changed');\n if (offset > asset.length) throw new Error('offset');\n const chunk = asset.slice(offset, offset + CUSTOM_BUNDLE_CHUNK_CHARS);\n return bounded({ content: [{ type: 'text' as const, text: 'Published custom chart asset chunk.' }],\n structuredContent: { key, contentHash: hash, offset, chunk,\n nextOffset: offset + chunk.length < asset.length ? offset + chunk.length : null } });\n } catch {\n return { isError: true, content: [{ type: 'text' as const,\n text: `Could not load published custom:${key}. It may be unpublished, changed, or unavailable to this sign-in.` }] };\n }\n });\n tool.update({ _meta: appMeta(['app']) });\n}\n","import { registerCustomChartAssets } from './custom-chart-assets.js';\nimport { z } from 'zod';\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { DatabrainClient } from '../client/databrain-client.js';\nimport { DatabrainApiError } from '../utils/errors.js';\nimport { READ_ONLY } from '../tools/annotations.js';\nimport { toolContext } from '../utils/tool-context.js';\nimport { createChartBackend, type ChartBackend, type ChartAction } from './backend.js';\nimport { APP_MIME, CHART_URI, KPI_URI, pageSchema, referenceSchema, viewSchema } from './contract.js';\nimport { appMeta, chartResult } from './feature.js';\nimport { tileHtml } from './tile.js';\nimport { draftSchema, draftPage, RENDER_TYPES, saveDraftAsDashboard, workspacesForDatamart } from './preview.js';\nimport { rowsSchema } from './contract.js';\nimport { WRITE_CREATE } from '../tools/annotations.js';\nimport { APPLY_TO_PRODUCTION_CONFIRMATION } from '../tools/widgets/production-guards.js';\n\nconst actions: Record<ChartAction, string> = {\n render_chart: 'Render one authorized chart page as SVG with point metadata. Cursor pagination stays within the same guest scope.',\n refresh_widget: 'Refresh a chart in the current guest scope. Revalidates access before returning data.',\n drill_down: 'Explore a server-issued drill identifier within the current chart and guest scope.',\n change_date_range: 'Render a chart for an ordered date range within the current guest scope.',\n};\n\n/** Call after registerAllTools so existing registration policy/instrumentation remains active. */\nexport function registerChartApps(server: McpServer, client: DatabrainClient, backend: ChartBackend = createChartBackend(client)): void {\n for (const [name, description] of Object.entries(actions)) {\n const action = name as ChartAction;\n const tool = server.tool(name, description, {\n reference: referenceSchema, view: viewSchema.default({}),\n context: z.string().max(200).optional().describe('Chart interaction context'),\n }, READ_ONLY, async ({ reference, view }) => {\n try {\n if ((action === 'drill_down' && !view.drillId) || (action === 'change_date_range' && !view.dateRange)) throw new Error('Missing view selector');\n const page = pageSchema.parse(await toolContext.run({ tool: name, category: 'embedding', workflowIntent: 'chart_app', inputMode: 'app' },\n () => backend.execute(action, reference, view)));\n // SVG is rendered as an isolated image, never inserted into the HTML DOM.\n // Reject active/external/animated constructs as a further contract check.\n if (page.svg && (!/^\\s*<svg\\b/i.test(page.svg) ||\n /<\\s*(?:script|foreignObject|iframe|image|animate\\w*|set)\\b|<!DOCTYPE|<!ENTITY|\\bon\\w+\\s*=|@(?:import|keyframes)|\\banimation\\s*:|(?:href\\s*=\\s*[\"'](?!#)|url\\s*\\(\\s*[\"']?(?!#))/i.test(page.svg))) {\n throw new Error('Unsafe SVG');\n }\n return chartResult(`${page.kind === 'kpi' ? 'KPI' : 'Chart'} ready. ${page.rows.length} rows in this page.${page.fallback ? ' ' + page.fallback : ''}`, {\n ...page, reference, view,\n });\n } catch {\n return { isError: true, content: [{ type: 'text' as const,\n text: 'Chart unavailable. Check dashboard access and the chart renderer integration, then retry.' }] };\n }\n });\n tool.update({ _meta: appMeta(['app']) });\n }\n registerDraftTools(server, client);\n registerCustomChartAssets(server, client);\n for (const [uri, kind] of [[CHART_URI, 'chart'], [KPI_URI, 'kpi']] as const) {\n server.resource(`Databrain ${kind} tile`, uri, { mimeType: APP_MIME, description: `Interactive ${kind} using host theme and scoped server tools` }, async () => ({\n contents: [{ uri, mimeType: APP_MIME, text: tileHtml(kind), _meta: { ui: { prefersBorder: true, csp: { resourceDomains: [\"data:\", \"blob:\"], frameDomains: [\"blob:\"], connectDomains: [] } } } }],\n }));\n }\n}\n\n/** The tile's controls over an unsaved answer. `preview_chart` is the \"show as\" row and\n * is app-only: the model already has ask_question. `save_chart_as_dashboard` is the commit,\n * visible to both, and gated the way every production write here is gated. */\nfunction registerDraftTools(server: McpServer, client: DatabrainClient): void {\n const preview = server.tool('preview_chart',\n 'Redraw an unsaved ask_question answer as another chart type, from the rows already on screen. No query runs.',\n { draft: draftSchema, rows: rowsSchema, chartType: z.enum(RENDER_TYPES),\n context: z.string().max(200).optional().describe('Chart interaction context') },\n READ_ONLY, async ({ draft, rows, chartType }) => {\n try {\n const page = await draftPage(client, draft, rows, chartType);\n return chartResult(`${page.kind === 'table' ? 'Table' : 'Chart'} ready as ${chartType}.`, page);\n } catch {\n return { isError: true, content: [{ type: 'text' as const, text: 'Chart preview unavailable. Ask the question again to retry.' }] };\n }\n });\n preview.update({ _meta: appMeta(['app']) });\n\n const save = server.tool('save_chart_as_dashboard',\n 'Save an ask_question answer: creates a workspace metric from its SQL and a new dashboard holding it. ' +\n `Production write; requires confirm: \"${APPLY_TO_PRODUCTION_CONFIRMATION}\". ` +\n 'A workspaceName is needed when the datamart backs more than one workspace.',\n { draft: draftSchema, chartType: z.enum(RENDER_TYPES),\n dashboardTitle: z.string().trim().min(1).max(200),\n workspaceName: z.string().trim().min(1).max(256).optional(),\n attemptId: z.string().trim().min(1).max(64).optional()\n .describe('Identifies one save attempt. Reuse it to retry safely; omit or change it to save again on purpose.'),\n confirm: z.string().optional(),\n context: z.string().max(200).optional().describe('Chart interaction context') },\n WRITE_CREATE, async ({ draft, chartType, dashboardTitle, workspaceName, attemptId, confirm }) => {\n if (confirm !== APPLY_TO_PRODUCTION_CONFIRMATION) {\n return { isError: true, content: [{ type: 'text' as const,\n text: `Saving writes to production. Retry with confirm: \"${APPLY_TO_PRODUCTION_CONFIRMATION}\".` }] };\n }\n // The draft's workspace list is a hint for the tile. The caller wrote it, so the\n // set a save may target is recomputed here from the datamart, on the server's own credential.\n const choices = await workspacesForDatamart(client, draft.datamartName, 'authorize');\n const refuse = (text: string) => ({ isError: true, content: [{ type: 'text' as const, text }],\n structuredContent: { kind: 'save', ok: false, choices } });\n // A named workspace must be one the draft's datamart can serve, spelled the draft's way.\n const target = workspaceName\n ? choices.find(w => w.trim().toLowerCase() === workspaceName.toLowerCase())\n : choices.length === 1 ? choices[0] : undefined;\n if (workspaceName && !target) {\n return refuse(`Workspace \"${workspaceName}\" cannot run this datamart's SQL. Choose one of: ${choices.join(', ')}.`);\n }\n if (!target) {\n return refuse(choices.length\n ? `Choose a workspace: ${choices.join(', ')}.`\n : `No workspace is built on datamart \"${draft.datamartName}\". Create one in Databrain, then save again.`);\n }\n try {\n const saved = await saveDraftAsDashboard(client, draft, { chartType, dashboardTitle, workspaceName: target, attemptId });\n return chartResult(`Saved \"${saved.title}\" to workspace ${saved.workspaceName} (dashboard ${saved.dashboardId}, metric ${saved.metricId}).`,\n { kind: 'save', ok: true, ...saved, draft: { ...draft, chartType } });\n } catch (error) {\n const code = error instanceof DatabrainApiError ? error.code : 'SAVE_FAILED';\n return { isError: true, content: [{ type: 'text' as const,\n text: `Could not save (${code}). Check the workspace name and your access, then try again.` }],\n structuredContent: { kind: 'save', ok: false, code } };\n }\n });\n save.update({ _meta: appMeta(['model', 'app']) });\n}\n","import { DatabrainClient } from '../client/databrain-client.js';\nimport type { RequestOptions } from '../client/types.js';\nimport { createServiceBundle } from '../services/factory.js';\nimport { pageSchema, referenceSchema, viewSchema, type ChartPage, type ChartReference, type ChartView } from './contract.js';\n\nexport const RENDER_PATH = '/api/v2/dashboard/mcp-chart';\nexport type ChartAction = 'render_chart' | 'refresh_widget' | 'drill_down' | 'change_date_range';\nexport interface ChartBackend {\n execute(action: ChartAction, reference: ChartReference, view: ChartView): Promise<ChartPage>;\n}\n\nclass UncachedChartClient extends DatabrainClient {\n constructor(private readonly delegate: DatabrainClient) {\n super({ apiUrl: delegate.getApiUrl(), demoDomain: delegate.getDemoDomain() });\n }\n override request<T>(options: RequestOptions): Promise<T> {\n return this.delegate.request<T>({ ...options, cacheKey: undefined, cacheTtlMs: undefined });\n }\n}\n\n/** Proposed backend route; no local renderer, ambient service fallback, or UI credentials. */\nexport function createChartBackend(client: DatabrainClient): ChartBackend {\n // The general embed lookup caches for five minutes. Authorization must not.\n const services = createServiceBundle(new UncachedChartClient(client));\n return {\n async execute(action, rawReference, rawView) {\n const reference = referenceSchema.parse(rawReference);\n const view = viewSchema.parse(rawView);\n // Every page/refresh/drill re-reads the embed live, so a revoked tenant stops working at once.\n const app = await services.widgetService.resolveDataAppName(undefined, reference.embedId);\n if (!app.ok) throw new Error('Chart scope unavailable');\n const scope = await services.widgetService.ensureReadableDashboardScope({\n embedId: reference.embedId, clientId: reference.clientId, dataAppName: app.value });\n if (!scope.ok) throw new Error('Chart scope denied');\n const { token } = await services.guestTokenService.generateToken({\n clientId: reference.clientId, dataAppName: app.value, allowedEmbeds: [reference.embedId],\n permissions: { isDisableMetricCreation: true, isEnableManageMetrics: false,\n isEnableMetricUpdation: false, isEnableArchiveMetrics: false },\n });\n if (!token) throw new Error('Guest scope unavailable');\n const page = await client.request<unknown>({\n method: 'POST', path: RENDER_PATH, auth: 'none', token,\n body: { action, embedId: reference.embedId, widgetId: reference.widgetId, ...view,\n pageSize: 50, width: 600, height: 320, animation: false },\n skipDataUnwrap: true,\n });\n return pageSchema.parse(page);\n },\n };\n}\n","import { transformSync } from 'esbuild';\nimport { MAX_CUSTOM_BUNDLE_CHARS, CUSTOM_BUNDLE_CHUNK_CHARS } from './contract.js';\n\n/** No iframe imports. All requests use the host bridge. */\nexport const TILE_SCRIPT = String.raw`(()=>{\nconst $=id=>document.getElementById(id),root=document.documentElement,pending=new Map();\nlet seq=0,origin='*',ready=false,closed=false,ref=null,view={},next=null,context={},busy=false,revision=0,rawSvg=null,dialect=null,shown=false,draft=null,rows=[],saved=null,attempt='',workspace=null,custom=null,customMode=false;\n/* One lifecycle, two sets of names. Hosts ship either or both, so notifications go out\n under both and arrive under either. Same order in both rows: result, input, context. */\nconst DASH=['ui/notifications/tool-result','ui/notifications/tool-input','ui/notifications/host-context-changed'],\nSNAKE=['notifications/tools/call/result','notifications/tools/call/input','notifications/host_context/changed'],\nINIT=['ui/notifications/initialized','notifications/initialized'],\nSIZE=['ui/notifications/size-changed','notifications/size_changed'],\nDISPLAY=['ui/request-display-mode','requestDisplayMode'];\nfunction send(message){if(!closed)parent.postMessage({jsonrpc:'2.0',...message},origin)}\nfunction notify(names,params){for(const method of names)send({method,params})}\nfunction rpc(method,params){return new Promise((resolve,reject)=>{const id=++seq;\nconst timer=setTimeout(()=>{pending.delete(id);reject(Error('timeout'))},65000);\npending.set(id,{resolve,reject,timer});send({id,method,params})})}\n/* Try each alias while the host says it never heard of it; any other error stands. */\nasync function ask(names,params){let last;\nfor(const method of names){try{return await rpc(method,params)}catch(error){last=error;if(error.code!==-32601)throw error}}\nthrow last}\n/* Fixed codes only; never log customer rows. */\nfunction trouble(code){send({method:'notifications/error',params:{code}})}\n/* One formatter for rail, table, KPI and answer line. Compact past 10K, grouped past 100,\n two decimals below; null is an absence, so it prints as a dash and never as 0. */\nfunction fmt(v){if(typeof v!=='number')return v==null||v===''?'\\u2014':String(v);const a=Math.abs(v);\nreturn new Intl.NumberFormat(navigator.language||'en',a>=1e4?{notation:'compact',maximumFractionDigits:1}:a>=100?{maximumFractionDigits:0}:{maximumFractionDigits:2}).format(v)}\nfunction exact(el,v){el.title=typeof v==='number'?String(v):''}\n/* The answer line: what this shows, and the headline value. Gated on carrying a number or\n a count, so it never renders as a second title or an empty reserved slot. */\nfunction answer(p){if(p.description)return p.description;const bits=[];\nif(p.kind==='dashboard'){const n=p.widgets?.length||0;return n?n+(n===1?' chart':' charts')+' on this dashboard':''}\nconst [dim,measure]=p.columns||[];const pts=(p.points||[]).filter(q=>typeof q.value==='number');\nif(pts.length<2)return '';if(dim&&measure&&!p.draft)bits.push(measure+' by '+dim);\nconst top=pts.reduce((a,b)=>b.value>a.value?b:a);bits.push(top.label.split(' \\u2014 ')[0]+' highest at '+fmt(top.value));return bits.join(' \\u00b7 ')}\nfunction status(text,bad,ok){const s=$('status');s.textContent=text;s.toggleAttribute('data-error',!!bad);s.toggleAttribute('data-ok',!!ok);if(bad)$('plot').dataset.state='off'}\nfunction paint(){const img=$('chart-image');if(!img||!rawSvg)return;\nconst c=getComputedStyle(document.body).color,mix=pc=>'color-mix(in srgb,'+c+' '+pc+'%,transparent)';\nimg.src='data:image/svg+xml,'+encodeURIComponent(rawSvg.replace(/<\\/svg>\\s*$/i,'<style>text{fill:'+mix(72)+'!important}'\n+'[stroke=\"#dbdee4\"],[stroke=\"#f1f1f2\" i]{stroke:'+mix(13)+'!important}[stroke=\"#54555a\"],[stroke=\"#d4d4d8\" i]{stroke:'+mix(30)+'!important}'\n+'</style></svg>'))}\nfunction controls(){document.querySelectorAll('button,input,select').forEach(e=>{e.disabled=busy});\n$('refresh').hidden=!ref;$('next').hidden=!next;$('range').hidden=!ref||customMode;$('plot').toggleAttribute('data-busy',busy);\n$('save').hidden=!draft||!!saved||!$('form').hidden;\n$('expand').hidden=!shown||root.dataset.kind==='dashboard'||!context.availableDisplayModes?.includes('fullscreen');\nroot.toggleAttribute('data-deep',context.displayMode==='fullscreen'||$('expand').hidden)}\nfunction host(change){context={...context,...change};if(change.theme==='dark'||change.theme==='light')root.style.colorScheme=change.theme;\nconst vars=change.styles?.variables||{};for(const [key,value] of Object.entries(vars)){\nif(/^--(?:color-|font-|border-|shadow-)/.test(key)&&typeof value==='string'&&!/url\\s*\\(|@import/i.test(value))root.style.setProperty(key,value)}\nconst face=change.styles?.css?.fonts;if(typeof face==='string'&&face.length<4000&&!/@import|javascript:/i.test(face))$('hostfont').textContent=face;\nconst insets=change.safeAreaInsets;if(insets)for(const edge of ['top','right','bottom','left']){\nconst n=insets[edge];if(Number.isFinite(n))root.style.setProperty('--safe-'+edge,Math.max(0,Math.min(n,200))+'px')}\nroot.dataset.mode=context.displayMode==='fullscreen'?'full':'inline';\n$('expand').textContent=context.displayMode==='fullscreen'?'Back to conversation':'Expand';paint();custom?.update();controls()}\nfunction scope(p,n){const bits=[];\nif(p.kind==='chart'||p.kind==='table')bits.push(p.rowsTotal>n?'first '+n+' of '+p.rowsTotal+' rows':n?(n===1?'1 row':n+' rows'):'No rows in this view');\nif(view.dateRange)bits.push(view.dateRange.from+' to '+view.dateRange.to);\nif(next)bits.push('more pages available');\nif(p.customChart&&n)bits.push('calculations use this page');\n$('scope').textContent=bits.join(' · ')}\nfunction resize(){if(ready)notify(SIZE,{width:document.body.scrollWidth,height:document.body.scrollHeight})}\nfunction table(rows,columns){$('rows').replaceChildren();$('data').hidden=!rows?.length;if(!rows?.length)return;\n$('count').textContent=rows.length===1?'1 row behind this':rows.length+' rows behind this';\nconst t=document.createElement('table');\nconst num=name=>rows.some(r=>typeof r[name]==='number');\nconst header=document.createElement('tr');for(const name of columns){const th=document.createElement('th');th.scope='col';th.textContent=name;if(num(name))th.className='n';header.append(th)}\nconst head=document.createElement('thead');head.append(header);t.append(head);const body=document.createElement('tbody');\nfor(const row of rows.slice(0,50)){const tr=document.createElement('tr');for(const key of columns){const td=document.createElement('td');td.textContent=fmt(row[key]);exact(td,row[key]);if(num(key))td.className='n';tr.append(td)}body.append(tr)}t.append(body);$('rows').append(t)}\nasync function display(mode){try{const answer=await ask(DISPLAY,{mode});\nhost({displayMode:answer.mode||context.displayMode});if(root.dataset.mode==='full'&&!$('data').hidden)$('data').open=true;resize()}\ncatch{status('Expand is unavailable in this host.',1)}}\nfunction wait(){busy=true;if(!$('plot').firstChild)$('plot').dataset.state='wait';status('Loading chart…');controls()}\nasync function call(name,extra={}){if(!ready||!ref||busy)return;run(name,{reference:ref,view:{...view,...extra}})}\n/* The draft tools take the draft and the rows on screen instead of a reference. */\nasync function run(name,args){if(!ready||busy)return;wait();const rev=++revision;\nconst selection=workspace?(name==='save_chart_as_dashboard'?{workspaceName:args.workspaceName||workspace.name}:{workspace:workspace.id}):{};\ntry{const result=await rpc('tools/call',{name,arguments:{...args,...selection,context:'tile'}});\nif(rev===revision)render(result)}catch{if(rev===revision){trouble('tool-call-failed');if(name==='save_chart_as_dashboard'){$('plot').dataset.state='ready';status('Could not save. Try again.',1)}else if(name==='preview_chart'){status('Could not draw that. Pick another type.',1);$('plot').dataset.state='ready'}else stale()}}finally{busy=false;controls()}}\nfunction stale(){if(shown&&$('plot').firstChild){status('Could not refresh. Showing the last chart that loaded. Select Refresh to try again.',1);$('plot').dataset.state='stale'}else status('Could not load the chart. Select Refresh to try again.',1)}\nfunction render(result){busy=false;controls();\nif(result?._meta?.['databrain/workspace'])workspace=result._meta['databrain/workspace'];\nconst sp=result?.structuredContent;if(sp&&sp.kind==='save'){$('plot').dataset.state='ready';if(sp.ok){saved=sp;$('form').hidden=true;status('Saved \"'+sp.title+'\" to workspace '+sp.workspaceName+'.',0,1)}\nelse if(sp.choices?.length){choose(sp.choices);status('Choose a workspace, then save again.',1)}\nelse status((result.content?.[0]?.text)||'Could not save. Try again.',1);controls();resize();return}\nif(result?.isError&&draft&&$('form').hidden===false){$('plot').dataset.state='ready';status((result.content?.[0]?.text)||'Could not save. Try again.',1);controls();return}\nif(result?.isError){trouble('tool-returned-error');\nif(shown&&$('plot').firstChild){stale();controls();return}shown=false;controls();\nstatus(ref?'Chart unavailable. Check your access to this dashboard, then Refresh.'\n:'Chart unavailable. Ask again to retry.',1);return}\nconst p=result?.structuredContent;if(!p||typeof p!=='object'){shown=false;controls();status('No chart data came back for this result.',1);return}\nrevision++;custom?.dispose();custom=null;customMode=!!p.customChart;shown=true;rawSvg=null;$('plot').replaceChildren();$('plot').dataset.state='ready';$('choices').replaceChildren();delete $('rail').dataset.value;\nref=p.reference||null;view=p.view||{};next=p.nextCursor||null;draft=p.draft||null;rows=p.rows||[];saved=null;$('form').hidden=true;\n$('title').textContent=p.title||'Analytics';$('description').textContent=answer(p);root.dataset.kind=p.kind;$('choices').toggleAttribute('data-grid',p.kind==='dashboard');\nif(p.kind==='dashboard'){$('plot').dataset.state='off';\nfor(const item of p.widgets||[]){const b=document.createElement('button');b.textContent=item.name||'Untitled widget';b.onclick=()=>{ref=item.reference;view={};call('render_chart')};$('choices').append(b)}\nstatus(p.widgets?.length?'':'No charts on this dashboard yet. Add one in Databrain.');\n}else if(p.kind==='kpi'){\nconst number=document.createElement('p');number.className='kpi';number.textContent=fmt(p.value);exact(number,p.value);$('plot').append(number);status('');\n}else if(p.customChart){\nif(!rows.length){$('plot').dataset.state='off';status('No rows in this view.');}\nelse{status('Loading custom chart…');const rev=revision;\nconst box=document.createElement('div');box.style.width='100%';$('plot').append(box);\nloadCustom(result,p,box,rev);}\n}else if(p.svg){\nconst box=document.createElement('div');box.className='chart';box.style.aspectRatio=p.width+'/'+p.height;\nconst img=document.createElement('img');img.id='chart-image';rawSvg=p.svg;img.alt=p.title||'Chart';img.onerror=()=>{status('The chart could not be drawn. The rows are below.',1);$('data').open=$('data').toggleAttribute('data-only',true)};box.append(img);\nfor(const point of p.points||[]){const b=document.createElement('button');b.className='point';b.setAttribute('aria-label',point.label+': '+fmt(point.value));b.setAttribute('aria-pressed','false');\nb.style.left=100*point.x/p.width+'%';b.style.top=100*point.y/p.height+'%';b.style.width=100*point.width/p.width+'%';b.style.height=100*point.height/p.height+'%';\nconst hover=()=>{$('vlabel').textContent=point.label;$('vnum').textContent=fmt(point.value);exact($('vnum'),point.value);$('rail').dataset.value='1'};b.onmouseenter=hover;b.onfocus=hover;\nb.onclick=()=>{box.querySelectorAll('.point').forEach(e=>e.setAttribute('aria-pressed','false'));b.setAttribute('aria-pressed','true');hover();\n$('choices').replaceChildren();if(point.drillId){const drill=document.createElement('button');drill.className='primary';drill.textContent='Explore selected data';drill.onclick=()=>call('drill_down',{drillId:point.drillId,cursor:undefined});$('choices').append(drill)}};box.append(b)}$('plot').append(box);\nstatus('');\n}else if(p.kind==='table'){\n// ask_question returns a table by design; calling that a missing chart called success a failure.\n$('plot').dataset.state='off';status('');\n}else{$('plot').dataset.state='off';\nstatus('Nothing to show. Try a wider date range, then Refresh.',1)}\nif(draft&&p.previewTypes?.length){for(const t of p.previewTypes){const b=document.createElement('button');b.textContent=t[0].toUpperCase()+t.slice(1);b.className='chip';b.setAttribute('aria-pressed',String(t===draft.chartType));\nb.onclick=()=>run('preview_chart',{draft,rows,chartType:t});$('choices').append(b)}$('choices').dataset.chips='1'}else delete $('choices').dataset.chips;\nif(p.previewError)status('Cannot draw this as '+(p.draft?.chartType||'a chart')+' ('+p.previewError+'). Showing the rows.',1);\npaint();table(p.rows||[],p.columns||Object.keys(p.rows?.[0]||{}));scope(p,p.rows?.length||0);\nconst only=!p.svg&&!p.customChart&&p.value===undefined&&p.kind!=='dashboard';$('data').open=only;$('data').toggleAttribute('data-only',only);controls();resize()}\nasync function loadCustom(result,p,box,rev){try{\nif(!globalThis.DbnCustomChartRuntime){\nconst runtime=await rpc('tools/call',{name:'load_custom_chart_runtime',arguments:{context:'tile'}});\nif(rev!==revision)return;\nconst code=runtime._meta?.['databrain/custom-chart-runtime'];\nif(runtime.isError||typeof code!=='string'||code.length>64000)throw Error('runtime unavailable');\nconst script=document.createElement('script');script.textContent=code;document.head.append(script);script.remove();\nif(!globalThis.DbnCustomChartRuntime)throw Error('runtime unavailable');\n}\nlet bundle=result._meta?.['databrain/custom-chart-bundle'];\nif(!bundle){let text='',offset=0,hash;\nfor(let chunks=0;chunks<${Math.ceil(MAX_CUSTOM_BUNDLE_CHARS / CUSTOM_BUNDLE_CHUNK_CHARS)};chunks++){\nconst r=await rpc('tools/call',{name:'load_custom_chart_bundle',arguments:{context:'tile',key:p.customChart.key,offset,...(hash?{contentHash:hash}:{})}});\nif(rev!==revision)return;\nif(r.isError)throw Error('unavailable');const part=r.structuredContent;\nif(!part||part.key!==p.customChart.key||part.offset!==offset||typeof part.chunk!=='string'||part.chunk.length>${CUSTOM_BUNDLE_CHUNK_CHARS}||!part.chunk.length)throw Error('invalid chunk');\nif(hash&&hash!==part.contentHash)throw Error('changed');hash=part.contentHash;text+=part.chunk;\nif(text.length>${MAX_CUSTOM_BUNDLE_CHARS})throw Error('too large');if(part.nextOffset===null){bundle=JSON.parse(text);break}\nif(part.nextOffset!==offset+part.chunk.length)throw Error('invalid offset');offset=part.nextOffset;\n}\n}\nif(rev!==revision)return;if(!bundle||bundle.manifest.id!==p.customChart.key)throw Error('invalid bundle');\ncustom=DbnCustomChartRuntime.mount(box,bundle,p.customChart,p.rows||[],message=>{if(rev!==revision)return;status(message,!!message);if(message){$('data').open=true;$('data').setAttribute('data-only','')}resize()});\n}catch{if(rev===revision){status(result._meta?.['databrain/custom-chart-bundle']?'Draft preview could not open. Check the bindings and sandbox support. The rows are below.':'Published custom chart unavailable, or this host cannot open its sandbox. The rows are below.',1);$('data').open=true;$('data').setAttribute('data-only','');resize()}}}\n$('refresh').onclick=()=>call('refresh_widget',{cursor:undefined});$('next').onclick=()=>call('render_chart',{cursor:next});\nfunction choose(names){const w=$('ws');w.replaceChildren();for(const n of names){const o=document.createElement('option');o.value=n;o.textContent=n;w.append(o)}$('wsrow').hidden=names.length<2;$('wsone').textContent=names.length===1?'in workspace '+names[0]:'';$('form').hidden=false;$('dtitle').focus();controls();resize()}\n$('save').onclick=()=>{attempt=crypto.randomUUID();$('dtitle').value=draft?.title||'';choose(draft?.workspaces||[])};\n$('cancel').onclick=()=>{$('form').hidden=true;status('');controls();resize()};\n$('commit').onclick=()=>{const t=$('dtitle').value.trim();if(!t){status('Name the dashboard.',1);return}\nrun('save_chart_as_dashboard',{draft,chartType:draft.chartType,dashboardTitle:t,workspaceName:$('wsrow').hidden?undefined:$('ws').value,attemptId:attempt,confirm:'APPLY_TO_PRODUCTION'})};\n$('expand').onclick=()=>display(context.displayMode==='fullscreen'?'inline':'fullscreen');\n$('dates').onclick=()=>{const from=$('from').value,to=$('to').value;if(!from||!to||from>to){status('Start date must not be after the end date.',1);return}call('change_date_range',{dateRange:{from,to},cursor:undefined,drillId:undefined})};\nfunction receive(event){if(event.source!==parent||closed)return;const m=event.data;\nif(!m||Array.isArray(m)||m.jsonrpc!=='2.0'||(origin!=='*'&&event.origin!==origin))return;\nif(!m.method&&pending.has(m.id)&&('result'in m||'error'in m)){if(origin==='*'&&event.origin!=='null')origin=event.origin;\nconst p=pending.get(m.id);pending.delete(m.id);clearTimeout(p.timer);\nif(m.error){const error=Error('host error');error.code=m.error.code;p.reject(error)}else p.resolve(m.result);return}\nif(m.method==='ping'&&m.id!==undefined){send({id:m.id,result:{}});return}\nif(m.method==='ui/resource-teardown'){send({id:m.id,result:{}});closed=true;revision++;custom?.dispose();custom=null;for(const p of pending.values()){clearTimeout(p.timer);p.reject(Error('closed'))}pending.clear();observer.disconnect();removeEventListener('message',receive);return}\nif(!ready||m.id!==undefined)return;\nlet slot=DASH.indexOf(m.method),kind='dashed';\nif(slot<0){slot=SNAKE.indexOf(m.method);kind='snake'}\nif(slot>=0){\n/* A dual host posts each notification twice. Lock to the first name and drop its twin:\n same payload, and a second render is a visible flicker. */\nif(!dialect)dialect=kind;if(dialect!==kind)return;\n// The snake result nests one level deeper.\nif(slot===0)render(kind==='snake'?m.params&&m.params.result:m.params);\nelse if(slot===1)wait();\nelse{host(m.params||{});resize()}\nreturn}\nif(m.method==='ui/notifications/tool-cancelled'){revision++;custom?.dispose();custom=null;busy=false;$('plot').dataset.state='off';status('Request cancelled.');controls()}}\naddEventListener('message',receive);const observer=new ResizeObserver(resize);observer.observe(document.body);controls();\nrpc('ui/initialize',{protocolVersion:'2026-01-26',appInfo:{name:'Databrain tile',version:'1.0.0'},appCapabilities:{availableDisplayModes:['inline','fullscreen']}})\n.then(r=>{if(r.protocolVersion!=='2026-01-26')throw Error('version');host(r.hostContext||{});ready=true;notify(INIT,{});resize()})\n.catch(()=>{trouble('initialize-failed');status('This chart needs an MCP Apps host.',1)});\n})();`;\n\n/** Compact the trusted bridge once, preserving the existing built-in result budget. */\nconst SERVED_SCRIPT = transformSync(TILE_SCRIPT, { minify: true, target: 'es2020' }).code;\nexport function tileHtml(kind: 'chart' | 'kpi' = 'chart'): string {\n return strip(`<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; font-src data:; connect-src 'none'; frame-src blob:; base-uri 'none'; form-action 'none'\">\n<title>Databrain ${kind} tile</title><style id=\"hostfont\"></style><style>\n/* Host tokens are the Claude MCP Apps style-variable names. Every one carries a fallback:\n hosts may ship any subset, so nothing here may depend on a token existing. */\n:root{color-scheme:light dark;\n--dbn-bg:var(--color-background-primary,Canvas);--dbn-text:var(--color-text-primary,CanvasText);\n--dbn-mute:var(--color-text-secondary,GrayText);--dbn-faint:var(--color-text-tertiary,GrayText);\n--dbn-edge:var(--color-border-secondary,color-mix(in srgb,currentColor 20%,transparent));\n--dbn-line:var(--color-border-tertiary,color-mix(in srgb,currentColor 9%,transparent));\n--dbn-accent:var(--color-text-info,LinkText);--dbn-ring:var(--color-ring-primary,var(--dbn-accent));\n--dbn-bad:var(--color-text-danger,var(--dbn-text));\n--dbn-quiet:color-mix(in srgb,currentColor 6%,transparent);\n--bw:var(--border-width-regular,1px);--r-s:var(--border-radius-sm,6px);--r-m:var(--border-radius-md,8px);\n--med:var(--font-weight-medium,500);--semi:var(--font-weight-semibold,600);--xs:var(--font-text-xs-size,12px);\n/* One curve. Quick / standard / display-mode. Corporate: no overshoot. */\n--e:cubic-bezier(.2,0,0,1);--q:120ms;--s:300ms;--l:400ms}\n*{box-sizing:border-box}\nbody{margin:0;padding:var(--safe-top,0px) var(--safe-right,0px) var(--safe-bottom,0px) var(--safe-left,0px);background:var(--dbn-bg);color:var(--dbn-text);\nfont:var(--font-text-sm-size,14px)/var(--font-text-sm-line-height,1.4) var(--font-sans,system-ui)}\nmain{padding:20px;max-width:1200px;margin:auto;transition:padding var(--l) var(--e)}\nheader{display:flex;align-items:start;justify-content:space-between;gap:12px}header>div{min-width:0}\n.acts{display:flex;gap:6px;flex:none;align-items:center}\nh1{font-size:var(--font-heading-md-size,18px);line-height:var(--font-heading-md-line-height,1.4);margin:0 0 6px;font-weight:var(--semi);letter-spacing:-.01em;overflow-wrap:anywhere;transition:font-size var(--l) var(--e)}\np{margin:6px 0;color:var(--dbn-mute);overflow-wrap:anywhere}\n#description{font-size:var(--xs);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;margin-top:2px}#description:empty{display:none}\nbutton,input,select{font:inherit;color:inherit;background:var(--dbn-bg);border:var(--bw) solid var(--dbn-edge);border-radius:var(--r-s);padding:7px 10px}\nbutton{cursor:pointer;transition:background-color var(--q) var(--e),border-color var(--q) var(--e)}\ninput{max-width:160px;min-width:0}button:hover:not(:disabled){background:var(--dbn-quiet)}button:disabled{color:var(--dbn-faint);cursor:default}\nbutton:focus-visible,input:focus-visible,summary:focus-visible{outline:2px solid var(--dbn-ring);outline-offset:2px}\n/* The picker is a scannable grid of quiet chips: 26 charts fit in nine rows, not a wall. */\n#choices[data-grid]{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:6px;margin-top:12px}\n#choices[data-grid] button{text-align:left;border-color:var(--dbn-line);padding:8px 10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}\n#choices[data-grid] button:hover:not(:disabled){background:var(--dbn-quiet);border-color:var(--dbn-edge)}\n/* One primary per state: the drill pill, and Next page when there is a next page. */\n#choices button.primary,#next{flex:none;text-align:center;border:var(--bw) solid var(--dbn-accent);\nborder-radius:var(--r-s);padding:7px 10px;font-weight:var(--med);color:var(--dbn-accent)}\n#choices button.primary{align-self:flex-start;margin-top:10px}\n/* Refresh is a re-fetch, not a choice. Quiet it so it stops reading as a fifth chart. */\n#refresh{border-color:transparent;color:var(--dbn-mute);padding:7px 8px}\n#refresh:hover:not(:disabled){color:var(--dbn-text);background:none;text-decoration:underline}\n#status:empty{margin:0}\n#status[data-ok],#status[data-error]{font-weight:var(--med);border-left:2px solid;border-radius:0 var(--r-m) var(--r-m) 0;padding:10px 12px;margin:10px 0;background:color-mix(in srgb,currentColor 5%,transparent)}\n#status[data-ok]{color:var(--color-text-success,var(--dbn-text));border-color:var(--color-border-success,color-mix(in srgb,currentColor 40%,transparent))}\n/* \"Show as\": one row of quiet chips, the current type pressed. Never a second title. */\n#choices[data-chips]{flex-direction:row;flex-wrap:wrap;gap:6px;margin-top:10px}\n.chip{border-color:var(--dbn-line);border-radius:999px;padding:5px 11px;font-size:var(--xs);color:var(--dbn-mute)}\n.chip:hover:not(:disabled){color:var(--dbn-text);border-color:var(--dbn-edge)}\n.chip[aria-pressed=true]{color:var(--dbn-text);border-color:var(--dbn-text);background:var(--dbn-quiet)}\n/* Save is the one commit on a draft, so it is the one filled button in the tile. */\n#save,#commit{border-color:var(--dbn-accent);background:var(--dbn-accent);color:var(--dbn-bg);font-weight:var(--med)}\n#save:hover:not(:disabled),#commit:hover:not(:disabled){background:color-mix(in srgb,var(--dbn-accent) 85%,var(--dbn-text))}\n#form{margin-top:12px;padding:12px;border:var(--bw) solid var(--dbn-line);border-radius:var(--r-m);display:flex;flex-direction:column;gap:8px}\n[hidden]{display:none!important}#form label{flex-direction:column;align-items:stretch;gap:4px;font-size:var(--xs)}\n#form input,#form select{max-width:none;width:100%;background:color-mix(in srgb,currentColor 4%,var(--dbn-bg))}\n#wsone{color:var(--dbn-mute);font-size:var(--xs)}#wsone:empty{display:none}\n#status[data-error]{color:var(--dbn-bad);border-color:var(--color-border-danger,color-mix(in srgb,currentColor 40%,transparent));background:var(--color-background-danger,color-mix(in srgb,currentColor 5%,transparent))}\n/* Reserved plot. The skeleton is a static bar silhouette matching the 15:8 backend render:\n Claude's guidance asks for a skeleton that matches the final layout, and bans nothing here,\n but shimmer is banned by our own motion budget - so this paints once and never repaints. */\n#plot{position:relative;transition:opacity var(--q) var(--e)}#plot[data-busy],#plot[data-state=stale]{opacity:.55}\n#plot[data-state=wait]{aspect-ratio:15/8}\n#plot[data-state=wait]::before,#plot[data-state=ready]::before{content:'';position:absolute;pointer-events:none;inset:16% 6% 13%;\nborder-bottom:var(--bw) solid var(--dbn-line);transition:opacity var(--s) var(--e);\n--g:linear-gradient(var(--dbn-quiet),var(--dbn-quiet));background:var(--g) 0 100%/11% 44% no-repeat,var(--g) 22% 100%/11% 71% no-repeat,var(--g) 44% 100%/11% 57% no-repeat,var(--g) 66% 100%/11% 86% no-repeat,var(--g) 88% 100%/11% 33% no-repeat}\n#plot[data-state=ready]::before{opacity:0}\n.chart{position:relative;width:100%;margin:12px 0}.chart img{width:100%;height:100%;display:block}\n/* A point's geometry comes from the renderer and can be 8px; the hit area must not.\n ::after pads it to at least 24px; ::before is the marker that says \"you can pick this\". */\n.point{position:absolute;padding:0;border:0;background:transparent;border-radius:50%}\n.point::after{content:'';position:absolute;inset:-8px}\n.point::before{content:'';position:absolute;left:50%;top:50%;width:8px;height:8px;margin:-4px;border-radius:50%;\nbackground:var(--dbn-accent);box-shadow:0 0 0 2px var(--dbn-bg);opacity:0;transform:scale(.6);transition:opacity var(--q) var(--e),transform var(--q) var(--e)}\n.point:hover::before,.point:focus-visible::before{opacity:.7;transform:none}\n.point[aria-pressed=true]::before{opacity:1;transform:none;box-shadow:0 0 0 2px var(--dbn-bg),0 0 0 4px color-mix(in srgb,var(--dbn-accent) 35%,transparent)}\n.kpi{font-size:clamp(34px,10vw,44px);line-height:1.1;font-weight:var(--semi);letter-spacing:-.025em;font-variant-numeric:tabular-nums;color:var(--dbn-text);margin:14px 0 6px;transition:font-size var(--l) var(--e)}\n/* The readout rail. At rest it states the scope of what is on screen; on selection it\n becomes the value. One slot, two jobs, so selecting a bar never reflows the tile. */\n#rail{display:flex;align-items:center;min-height:2.5em;margin:2px 0 0}\n#scope,#value{margin:0;display:none;overflow-wrap:anywhere}\n#rail:not([data-value]) #scope{display:block;color:var(--dbn-mute);font-size:var(--xs)}\n#rail:not([data-value]):has(#scope:empty){display:none}\n#rail[data-value] #value{display:flex;flex-direction:column;gap:1px;line-height:1.15}\n#vlabel{color:var(--dbn-mute);font-size:var(--xs)}\n#vnum{font-size:var(--font-heading-md-size,18px);font-weight:var(--semi);letter-spacing:-.015em;font-variant-numeric:tabular-nums}\n[data-mode=full] #vnum{font-size:var(--font-heading-xl-size,24px)}\nfooter,.dates{display:flex;gap:8px;flex-wrap:wrap;align-items:center}\n#choices{display:flex;flex-direction:column;align-items:stretch;margin:10px 0 0}footer{margin-top:12px}footer:not(:has(button:not([hidden]))){display:none}.dates{margin:8px 0 2px}\nlabel{display:flex;align-items:center;gap:6px;color:var(--dbn-mute)}\n#range{margin-top:10px}#data{margin-top:2px}\nhtml:not([data-deep]) #range,html:not([data-deep]) #data:not([data-only]){display:none}\nsummary{cursor:pointer;color:var(--dbn-mute);font-size:var(--xs);padding:5px 0;width:fit-content;transition:color var(--q) var(--e)}summary:hover{color:var(--dbn-text)}\n#rows{overflow:auto;max-height:260px;border-radius:var(--r-s)}\ntable{border-collapse:collapse;width:100%;font-size:var(--xs)}\nth,td{text-align:left;padding:8px;border-bottom:var(--bw) solid var(--dbn-line);overflow-wrap:anywhere}\ntd.n,th.n{text-align:right;font-variant-numeric:tabular-nums}\nth{font-weight:var(--semi);position:sticky;top:0;background:var(--dbn-bg);color:var(--dbn-mute);\nfont-size:var(--xs);letter-spacing:.02em;text-transform:uppercase}\n/* Inline to fullscreen is a layout morph, not a new screen: main widens over 400ms on the\n one curve and every dependent box follows it. Fullscreen is the detailed view, so the\n table loses its inline height cap. */\n/* The SVG is rendered at 600x320 and scaled by width, so an unbounded fullscreen column\n makes the chart's own axis type larger than the tile's heading. Hold a measure. */\n[data-mode=full] main{padding:28px 32px;max-width:900px}\n[data-mode=full] h1{font-size:var(--font-heading-xl-size,24px);line-height:var(--font-heading-xl-line-height,1.25)}\n[data-mode=full] .kpi{font-size:clamp(44px,7vw,72px)}\n[data-mode=full] #rows{max-height:none}\n/* Touch: the host gives vertical pan to the conversation, so a nested scroller is a trap. */\n@media(pointer:coarse){button,input{min-height:44px}summary{padding:12px 0}#rows{max-height:none;overflow:visible}}\n@media(max-width:480px){main{padding:14px}}\n/* Entrance. Content first, chrome 60ms later: 300+60 = 360ms, inside the 400ms budget.\n No stagger past two groups - a chat tile that cascades reads as lag, not luxury. */\n@keyframes rise{from{opacity:0;transform:translateY(6px)}}\n@keyframes land{from{opacity:0;transform:translateY(4px) scale(.995)}}\nheader,#status,#plot{animation:rise var(--s) var(--e) both}\n#rail,#choices,footer,#range,#data{animation:rise var(--s) var(--e) 60ms both}\n.chart,.kpi{animation:land var(--s) var(--e) both}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important}}\n</style></head><body><main id=\"tile\"><header><div><h1 id=\"title\">Analytics</h1><p id=\"description\"></p></div><div class=\"acts\"><button id=\"refresh\" hidden>Refresh</button><button id=\"expand\" hidden>Expand</button></div></header>\n<p id=\"status\" role=\"status\">Loading chart…</p><div id=\"plot\" data-state=\"wait\"></div>\n<div id=\"rail\"><p id=\"scope\"></p><p id=\"value\"><span id=\"vlabel\"></span><b id=\"vnum\"></b></p></div><div id=\"choices\"></div>\n<footer><button id=\"save\" hidden>Save as dashboard</button><button id=\"next\" hidden>Next page</button></footer>\n<form id=\"form\" hidden onsubmit=\"return false\"><label>Dashboard name<input id=\"dtitle\" type=\"text\" maxlength=\"200\" autocomplete=\"off\"></label>\n<label id=\"wsrow\" hidden>Workspace<select id=\"ws\"></select></label><p id=\"wsone\"></p>\n<div class=\"dates\"><button id=\"commit\" type=\"button\">Save to Databrain</button><button id=\"cancel\" type=\"button\">Cancel</button></div></form>\n<details id=\"range\" hidden><summary>Change the date range</summary><div class=\"dates\"><label>From <input id=\"from\" type=\"date\"></label><label>To <input id=\"to\" type=\"date\"></label><button id=\"dates\" type=\"button\">Apply dates</button></div></details>\n<details id=\"data\" hidden><summary id=\"count\">Show the data behind this</summary><div id=\"rows\" tabindex=\"0\" role=\"region\" aria-label=\"Chart data\"></div></details></main><script>${SERVED_SCRIPT}</script></body></html>`);\n}\nfunction strip(html: string): string {\n return html.replace(/\\/\\*[\\s\\S]*?\\*\\//g, '').replace(/\\n{2,}/g, '\\n')\n .replace(/<style>([\\s\\S]*?)<\\/style><\\/head>/, (_m, css: string) => `<style>${css.replace(/;}/g, '}').replace(/\\n/g, '')}</style></head>`);\n}\n"],"mappings":"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","names":["z","z"]}