@feedmepos/mf-report 5.27.54 → 5.28.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 (39) hide show
  1. package/dist/{App-C6xpf-Q0.js → App-BDhSFQX_.js} +45 -44
  2. package/dist/DateRangeSelect-CRsKMi0U.js +437 -0
  3. package/dist/{Default.vue_vue_type_script_setup_true_lang-BwPZXC_d.js → Default.vue_vue_type_script_setup_true_lang-WGOOX2b8.js} +12 -11
  4. package/dist/{DynamicWidget.vue_vue_type_script_setup_true_lang-BpgXiPBY.js → DynamicWidget.vue_vue_type_script_setup_true_lang-CQM-l5Rk.js} +182 -181
  5. package/dist/{GlobalFilterSelect.vue_vue_type_script_setup_true_lang-CVShUWJC.js → GlobalFilterSelect.vue_vue_type_script_setup_true_lang-CMioHpn1.js} +5091 -5717
  6. package/dist/{InsightView-BYW66Wrv.js → InsightView-lnOobZAr.js} +17 -15
  7. package/dist/{Integration-DQPJBE-0.js → Integration-D5ssimUg.js} +27 -25
  8. package/dist/{Integrations-BW4H9lUS.js → Integrations-CaqGvkzI.js} +8 -7
  9. package/dist/{Layout-CMlh08aB.js → Layout-DS0XCT_E.js} +1 -1
  10. package/dist/LocationSelect.vue_vue_type_script_setup_true_lang-DC9QYJk-.js +8055 -0
  11. package/dist/{MenuTab.vue_vue_type_script_setup_true_lang-BUvD1_b0.js → MenuTab.vue_vue_type_script_setup_true_lang-nOukNXp3.js} +11 -10
  12. package/dist/{NavigationTab.vue_vue_type_script_setup_true_lang-TpeQ_Wm1.js → NavigationTab.vue_vue_type_script_setup_true_lang-DXNbt08D.js} +9636 -17677
  13. package/dist/{OverviewView-BtGpQkBY.js → OverviewView-BkIozvMs.js} +5 -5
  14. package/dist/{Report-CJ9myNLK.js → Report-HiSGRNhn.js} +27 -25
  15. package/dist/ReportCanvasView-ILVhB1KL.js +448 -0
  16. package/dist/{ReportEditor-DtnTBbY1.js → ReportEditor-B4tyVB8s.js} +30 -28
  17. package/dist/{ReportView-Ci8nFyI9.js → ReportView-CIQQHvWT.js} +14 -13
  18. package/dist/ReportViewRoute-B3KwVB0v.js +18 -0
  19. package/dist/{SelectComponent.vue_vue_type_script_setup_true_lang-Ckn2p74A.js → SelectComponent.vue_vue_type_script_setup_true_lang-DW7Dk7iQ.js} +1 -1
  20. package/dist/{Setting-QSiMSAQk.js → Setting-CvToMUKq.js} +5 -5
  21. package/dist/{Template-CAPlAQO9.js → Template-BBQEaHrM.js} +10 -9
  22. package/dist/{TileEditor-e7ZCWMU9.js → TileEditor-Cqy98LmJ.js} +19 -17
  23. package/dist/{TilePageStack.vue_vue_type_script_setup_true_lang-B4g3YPzM.js → TilePageStack.vue_vue_type_script_setup_true_lang-CzggUdfI.js} +3 -3
  24. package/dist/{TransitionFade-DqIazHHG.js → TransitionFade-D-8BYs6T.js} +36 -33
  25. package/dist/{_plugin-vue_export-helper-kkiiQoKl.js → _plugin-vue_export-helper-C6QDExPN.js} +276 -291
  26. package/dist/{app-BAgs-zHf.js → app-Bjt_JuJi.js} +670 -567
  27. package/dist/app.js +1 -1
  28. package/dist/assets/{formatChartData.worker-epD1fsTW.js → formatChartData.worker-BeBGCyYW.js} +1 -1
  29. package/dist/assets/{processTableData.worker-BJZ_Z45H.js → processTableData.worker-frTYV0ok.js} +39 -39
  30. package/dist/global-loader-BuxFfehu.js +19 -0
  31. package/dist/index-CtZE4sab.js +82 -0
  32. package/dist/report-DjGDWvm3.js +635 -0
  33. package/dist/report-canvas-frame.html +888 -0
  34. package/dist/report-canvas-libs/chart.js-4.js +14 -0
  35. package/dist/report-canvas-libs/fm-ui-1.js +904 -0
  36. package/dist/style.css +1 -1
  37. package/dist/{useRestaurantPermission-ChYMJfXG.js → useRestaurantPermission-Dkbh_cv8.js} +4 -4
  38. package/package.json +1 -1
  39. package/dist/DateRangeSelect-DYVOvSCA.js +0 -517
@@ -0,0 +1,888 @@
1
+ <meta charset="utf-8" />
2
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
3
+ <!--
4
+ Runtime shell for AI-written report sections. Loaded into ONE iframe per report with
5
+ sandbox="allow-scripts" and no allow-same-origin, so this document runs on an opaque
6
+ origin and cannot reach the host. Section code needs inline+eval by design; the sandbox
7
+ attribute is the boundary, and this CSP's job is to keep the frame off the network.
8
+ -->
9
+ <meta
10
+ http-equiv="Content-Security-Policy"
11
+ content="default-src 'none'; script-src 'unsafe-inline' 'unsafe-eval'; style-src 'unsafe-inline'; img-src data: blob:; font-src data:; connect-src 'none'; form-action 'none'; base-uri 'none'"
12
+ />
13
+ <title>Report</title>
14
+ <style>
15
+ /* Design tokens, from the B2B_POS working file's Report page. A report reads these;
16
+ a merchant asking for a different look is one redefinition here, not a rewrite. */
17
+ :root {
18
+ color-scheme: light;
19
+ --fm-fg: #1c1c1e;
20
+ --fm-body: #2e2e2e;
21
+ --fm-muted: #6b6c70;
22
+ --fm-faint: #aeaeb2;
23
+ --fm-line: #f0f0f0;
24
+ --fm-card: #ffffff;
25
+ --fm-page: #f3f3f6;
26
+ --fm-signature: #ff7823;
27
+ --fm-signature-tint: #fff3e6;
28
+ --fm-signature-dark: #6b3100;
29
+ --fm-danger: #99231d;
30
+ --fm-danger-bg: #ffebea;
31
+ --fm-aqua-500: #0a5384;
32
+ --fm-aqua-300: #3ab0ff;
33
+ --fm-aqua-100: #b7e2ff;
34
+ --fm-purple-500: #512191;
35
+ --fm-purple-300: #8f61e8;
36
+ --fm-purple-100: #eddfff;
37
+ --fm-green-500: #306a01;
38
+ --fm-green-300: #aeef4b;
39
+ --fm-green-100: #f4fde6;
40
+ --fm-plum-500: #8c146a;
41
+ --fm-plum-300: #ffc4c3;
42
+ --fm-plum-100: #fff7fc;
43
+ --fm-mustard-500: #903e1f;
44
+ --fm-mustard-300: #ffc746;
45
+ --fm-mustard-100: #fef5ea;
46
+ --fm-berry-500: #5f090a;
47
+ --fm-berry-300: #e94850;
48
+ --fm-berry-100: #fee4dc;
49
+ --fm-success: #1f7735;
50
+ --fm-success-bg: #ebf9ee;
51
+ --fm-warning: #995900;
52
+ --fm-warning-bg: #fff4e6;
53
+ --fm-error: #99231d;
54
+ --fm-error-bg: #ffebea;
55
+ --fm-info: #004999;
56
+ --fm-info-bg: #e6f2ff;
57
+ --fm-radius: 12px;
58
+ --fm-radius-sm: 8px;
59
+ --fm-gap: 12px;
60
+ }
61
+ * {
62
+ box-sizing: border-box;
63
+ }
64
+ html,
65
+ body {
66
+ margin: 0;
67
+ padding: 0;
68
+ height: 100%;
69
+ }
70
+ body {
71
+ font: 14px/1.5 Inter, "Helvetica Neue", Arial, sans-serif;
72
+ color: var(--fm-fg);
73
+ background: #fff;
74
+ overflow-y: auto;
75
+ }
76
+ body.fmr-page {
77
+ background: var(--fm-page);
78
+ }
79
+ /* The board: 12 columns, ragged bottoms. A section spans all 12 unless it says otherwise. */
80
+ #report {
81
+ display: grid;
82
+ grid-template-columns: repeat(12, minmax(0, 1fr));
83
+ align-items: start;
84
+ gap: var(--fm-gap);
85
+ padding: 12px 16px 24px;
86
+ }
87
+ #report > section {
88
+ grid-column: 1 / -1;
89
+ min-width: 0;
90
+ }
91
+ /* ---- components ------------------------------------------------------
92
+ The FeedMe report component set. A report is composed from these; each one
93
+ is already the design system, so nothing here needs restyling to look right. */
94
+ .fmr-card {
95
+ background: var(--fm-card);
96
+ border: 1px solid var(--fm-line);
97
+ border-radius: var(--fm-radius);
98
+ }
99
+ .fmr-tile {
100
+ display: flex;
101
+ flex-direction: column;
102
+ overflow: hidden;
103
+ }
104
+ .fmr-tile-head {
105
+ display: flex;
106
+ align-items: flex-start;
107
+ justify-content: space-between;
108
+ gap: 16px;
109
+ flex-wrap: wrap;
110
+ padding: 16px 20px 12px;
111
+ }
112
+ .fmr-tile-title {
113
+ margin: 0;
114
+ font-size: 16px;
115
+ font-weight: 600;
116
+ color: var(--fm-fg);
117
+ }
118
+ /* One factual line. Not a place to explain the report. */
119
+ .fmr-tile-note {
120
+ margin: 4px 0 0;
121
+ font-size: 12px;
122
+ color: var(--fm-muted);
123
+ }
124
+ .fmr-tile-body {
125
+ padding: 0 20px 16px;
126
+ }
127
+ .fmr-tile-foot {
128
+ display: flex;
129
+ align-items: center;
130
+ justify-content: space-between;
131
+ gap: 12px;
132
+ flex-wrap: wrap;
133
+ border-top: 1px solid var(--fm-line);
134
+ padding: 10px 20px 12px;
135
+ font-size: 12px;
136
+ color: var(--fm-muted);
137
+ }
138
+
139
+ /* Scorecards: label, value, optional trend. */
140
+ .fmr-kpis {
141
+ display: grid;
142
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
143
+ gap: var(--fm-gap);
144
+ }
145
+ .fmr-kpi {
146
+ padding: 14px 16px;
147
+ min-width: 0;
148
+ }
149
+ .fmr-kpi-label {
150
+ font-size: 14px;
151
+ color: var(--fm-muted);
152
+ }
153
+ .fmr-kpi-value {
154
+ display: flex;
155
+ align-items: baseline;
156
+ gap: 6px;
157
+ margin-top: 5px;
158
+ font-size: 24px;
159
+ font-weight: 700;
160
+ color: var(--fm-fg);
161
+ font-variant-numeric: tabular-nums;
162
+ }
163
+ .fmr-kpi-cur {
164
+ font-size: 14px;
165
+ font-weight: 600;
166
+ }
167
+ .fmr-kpi-note {
168
+ margin-top: 6px;
169
+ font-size: 12px;
170
+ color: var(--fm-muted);
171
+ }
172
+ .fmr-trend {
173
+ display: inline-flex;
174
+ align-items: center;
175
+ gap: 4px;
176
+ border-radius: 999px;
177
+ padding: 3px 8px;
178
+ font-size: 12px;
179
+ font-weight: 600;
180
+ }
181
+ .fmr-trend.up { color: var(--fm-success); background: var(--fm-green-100); }
182
+ .fmr-trend.down { color: var(--fm-error); background: var(--fm-berry-100); }
183
+ .fmr-trend.flat { color: var(--fm-muted); background: var(--fm-line); }
184
+
185
+ /* Breakdown: a share row over one stacked bar. */
186
+ .fmr-breakdown {
187
+ display: grid;
188
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
189
+ gap: 12px 24px;
190
+ }
191
+ .fmr-breakdown-label {
192
+ display: flex;
193
+ align-items: center;
194
+ gap: 6px;
195
+ font-size: 14px;
196
+ color: var(--fm-muted);
197
+ }
198
+ .fmr-breakdown-value {
199
+ margin-top: 2px;
200
+ font-size: 14px;
201
+ font-weight: 600;
202
+ color: var(--fm-fg);
203
+ font-variant-numeric: tabular-nums;
204
+ }
205
+ .fmr-stack {
206
+ display: flex;
207
+ gap: 2px;
208
+ height: 12px;
209
+ margin-top: 14px;
210
+ }
211
+ .fmr-stack > span {
212
+ border-radius: 4px;
213
+ min-width: 2px;
214
+ }
215
+
216
+ /* Legend — you draw it in HTML; chart.js legends stay off. */
217
+ .fmr-legend {
218
+ display: flex;
219
+ align-items: center;
220
+ flex-wrap: wrap;
221
+ gap: 6px 20px;
222
+ }
223
+ .fmr-legend-item {
224
+ display: inline-flex;
225
+ align-items: center;
226
+ gap: 6px;
227
+ font-size: 12px;
228
+ color: var(--fm-muted);
229
+ }
230
+ .fmr-swatch {
231
+ width: 10px;
232
+ height: 10px;
233
+ border-radius: 2px;
234
+ flex: none;
235
+ }
236
+ .fmr-swatch.line {
237
+ width: 12px;
238
+ height: 2px;
239
+ border-radius: 0;
240
+ }
241
+
242
+ /* Table: no zebra, no vertical rules. */
243
+ table.fmr-table {
244
+ width: 100%;
245
+ border-collapse: collapse;
246
+ }
247
+ table.fmr-table th {
248
+ padding: 0 12px 10px;
249
+ text-align: left;
250
+ font-size: 14px;
251
+ font-weight: 400;
252
+ color: var(--fm-muted);
253
+ white-space: nowrap;
254
+ }
255
+ table.fmr-table td {
256
+ height: 52px;
257
+ padding: 7px 12px;
258
+ border-top: 1px solid var(--fm-line);
259
+ font-size: 14px;
260
+ color: var(--fm-fg);
261
+ vertical-align: middle;
262
+ }
263
+ table.fmr-table th:first-child,
264
+ table.fmr-table td:first-child {
265
+ padding-left: 20px;
266
+ }
267
+ table.fmr-table th:last-child,
268
+ table.fmr-table td:last-child {
269
+ padding-right: 20px;
270
+ }
271
+ table.fmr-table td:first-child {
272
+ font-weight: 600;
273
+ }
274
+ .fmr-num {
275
+ text-align: right;
276
+ font-variant-numeric: tabular-nums;
277
+ }
278
+ .fmr-dim {
279
+ color: var(--fm-muted);
280
+ }
281
+ /* the th/td rules above outrank a bare class, so the table needs its own. */
282
+ table.fmr-table th.fmr-num,
283
+ table.fmr-table td.fmr-num {
284
+ text-align: right;
285
+ }
286
+ table.fmr-table td.fmr-dim {
287
+ color: var(--fm-muted);
288
+ }
289
+ table.fmr-table td.fmr-empty {
290
+ font-weight: 400;
291
+ color: var(--fm-muted);
292
+ }
293
+ .fmr-scroll-x {
294
+ overflow-x: auto;
295
+ }
296
+
297
+ /* Pager: a table's footer. fmr-tile-foot gives it the frame, this adds the two buttons. */
298
+ .fmr-pager[hidden] {
299
+ display: none;
300
+ }
301
+ .fmr-pager-btns {
302
+ display: flex;
303
+ align-items: center;
304
+ gap: 16px;
305
+ }
306
+ .fmr-pager-btns .fmr-link[disabled] {
307
+ color: var(--fm-faint);
308
+ cursor: default;
309
+ text-decoration: none;
310
+ }
311
+
312
+ /* Tabs — orange marks the active one, the only place a report uses the signature. */
313
+ .fmr-tabs {
314
+ display: flex;
315
+ gap: 4px;
316
+ border-bottom: 1px solid var(--fm-line);
317
+ padding: 0 20px;
318
+ }
319
+ .fmr-tab {
320
+ border: 0;
321
+ background: none;
322
+ padding: 12px 16px;
323
+ font: inherit;
324
+ font-size: 14px;
325
+ font-weight: 600;
326
+ color: var(--fm-muted);
327
+ cursor: pointer;
328
+ border-bottom: 2px solid transparent;
329
+ margin-bottom: -1px;
330
+ }
331
+ .fmr-tab[aria-selected="true"] {
332
+ color: var(--fm-signature);
333
+ border-bottom-color: var(--fm-signature);
334
+ }
335
+
336
+ /* Status pairs — the dark tone on its own pale ground, never a brand tint. */
337
+ .fmr-badge {
338
+ display: inline-block;
339
+ border-radius: 999px;
340
+ padding: 3px 8px;
341
+ font-size: 12px;
342
+ font-weight: 600;
343
+ }
344
+ .fmr-badge.success { color: var(--fm-success); background: var(--fm-success-bg); }
345
+ .fmr-badge.warning { color: var(--fm-warning); background: var(--fm-warning-bg); }
346
+ .fmr-badge.error { color: var(--fm-error); background: var(--fm-error-bg); }
347
+ .fmr-badge.info { color: var(--fm-info); background: var(--fm-info-bg); }
348
+
349
+ .fmr-link {
350
+ border: 0;
351
+ background: none;
352
+ padding: 0;
353
+ font: inherit;
354
+ font-weight: 600;
355
+ color: var(--fm-info);
356
+ cursor: pointer;
357
+ text-decoration: underline;
358
+ text-decoration-color: transparent;
359
+ text-underline-offset: 2px;
360
+ }
361
+ .fmr-link:hover,
362
+ .fmr-link:focus-visible {
363
+ text-decoration-color: currentColor;
364
+ }
365
+
366
+ /* A chart canvas needs a height its parent does not give it. */
367
+ .fmr-chart {
368
+ position: relative;
369
+ height: 240px;
370
+ }
371
+ .fmr-chart.sm { height: 180px; }
372
+ .fmr-chart.lg { height: 320px; }
373
+
374
+ /* The two states the shell does not paint. */
375
+ .fmr-note {
376
+ margin: 0 20px 16px;
377
+ padding: 8px 0;
378
+ font-size: 14px;
379
+ color: var(--fm-muted);
380
+ }
381
+ .fmr-note.error {
382
+ border-radius: var(--fm-radius-sm);
383
+ padding: 11px 12px;
384
+ color: var(--fm-error);
385
+ background: var(--fm-error-bg);
386
+ }
387
+ .fm-section-error {
388
+ border: 1px solid var(--fm-line);
389
+ border-left: 3px solid var(--fm-danger);
390
+ background: var(--fm-danger-bg);
391
+ border-radius: 6px;
392
+ padding: 12px 14px;
393
+ }
394
+ .fm-section-error b {
395
+ color: var(--fm-danger);
396
+ }
397
+ .fm-section-error pre {
398
+ margin: 6px 0 0;
399
+ white-space: pre-wrap;
400
+ font-size: 12px;
401
+ color: var(--fm-muted);
402
+ }
403
+ .fm-placeholder {
404
+ color: var(--fm-muted);
405
+ padding: 24px 16px;
406
+ }
407
+ /* Loading belongs to the shell: a section declares its queries and never draws a skeleton. */
408
+ #fm-progress {
409
+ position: fixed;
410
+ inset: 0 0 auto;
411
+ z-index: 30;
412
+ height: 2px;
413
+ overflow: hidden;
414
+ background: var(--fm-signature-tint);
415
+ opacity: 0;
416
+ transition: opacity 0.15s linear;
417
+ pointer-events: none;
418
+ }
419
+ #fm-progress[data-on] {
420
+ opacity: 1;
421
+ }
422
+ #fm-progress i {
423
+ display: block;
424
+ width: 32%;
425
+ height: 100%;
426
+ background: var(--fm-signature);
427
+ }
428
+ section[data-fm-busy] {
429
+ position: relative;
430
+ min-height: 48px;
431
+ }
432
+ /* A scrim, not a replacement: the designed panel stays put and stale values stay clickable. */
433
+ section[data-fm-busy]::after {
434
+ content: "";
435
+ position: absolute;
436
+ inset: -4px;
437
+ border-radius: 10px;
438
+ background: rgba(255, 255, 255, 0.62);
439
+ pointer-events: none;
440
+ }
441
+ @media (prefers-reduced-motion: no-preference) {
442
+ #fm-progress i {
443
+ animation: fm-slide 1.1s linear infinite;
444
+ }
445
+ }
446
+ @keyframes fm-slide {
447
+ from {
448
+ transform: translateX(-100%);
449
+ }
450
+ to {
451
+ transform: translateX(320%);
452
+ }
453
+ }
454
+ .fm-text h1,
455
+ .fm-text h2,
456
+ .fm-text h3 {
457
+ margin: 0 0 8px;
458
+ line-height: 1.3;
459
+ }
460
+ .fm-text p {
461
+ margin: 0 0 8px;
462
+ }
463
+ .fm-text code {
464
+ background: #f0f0f0;
465
+ border-radius: 3px;
466
+ padding: 1px 4px;
467
+ font-size: 12px;
468
+ }
469
+ </style>
470
+
471
+ <div id="fm-progress" data-on aria-hidden="true"><i></i></div>
472
+ <div id="report" aria-busy="true"></div>
473
+
474
+ <script>
475
+ ;(function () {
476
+ "use strict"
477
+
478
+ var root = document.getElementById("report")
479
+
480
+ var env = {}
481
+ var params = {}
482
+ var results = Object.create(null) // queryId -> { rows, status, runId, truncated, error }
483
+ var subs = Object.create(null) // queryId -> [cb]
484
+ var loadedLibs = Object.create(null)
485
+ var mounted = Object.create(null) // sectionId -> container element
486
+ var versions = Object.create(null) // sectionId -> mounted source version
487
+ var failed = Object.create(null) // sectionId -> true once this mount reported an error
488
+ var order = [] // section ids, render order
489
+ var mounting = null // section id currently executing, for window.onerror attribution
490
+ var declared = Object.create(null) // sectionId -> the query ids that section reads
491
+ var progress = document.getElementById("fm-progress")
492
+
493
+ /**
494
+ * Section code shares this window and can call parent.postMessage itself, so a render
495
+ * outcome carries a nonce only this closure holds — never on `window`, never in the DOM.
496
+ * The host learns it from `ready` and drops any ack without it.
497
+ */
498
+ var NONCE = (function () {
499
+ try {
500
+ var buffer = new Uint32Array(4)
501
+ crypto.getRandomValues(buffer)
502
+ return Array.prototype.map
503
+ .call(buffer, function (value) {
504
+ return value.toString(36)
505
+ })
506
+ .join("")
507
+ } catch (err) {
508
+ return String(Math.random()).slice(2) + String(new Date().getTime())
509
+ }
510
+ })()
511
+
512
+ function post(message) {
513
+ // Opaque origins cannot be named, so "*" is the only usable target. The host
514
+ // authenticates us by comparing event.source to its own iframe.contentWindow.
515
+ parent.postMessage(message, "*")
516
+ }
517
+
518
+ /** Only the shell may claim a section rendered or threw. */
519
+ function postOutcome(message) {
520
+ message.nonce = NONCE
521
+ post(message)
522
+ }
523
+
524
+ function reportError(sectionId, err) {
525
+ var message = err && err.message ? String(err.message) : String(err)
526
+ var stack = err && err.stack ? String(err.stack).slice(0, 4000) : undefined
527
+ failed[sectionId] = true
528
+ renderError(sectionId, message)
529
+ postOutcome({
530
+ type: "error",
531
+ sectionId: sectionId,
532
+ version: versions[sectionId],
533
+ message: message,
534
+ stack: stack
535
+ })
536
+ }
537
+
538
+ function renderError(sectionId, message) {
539
+ var el = mounted[sectionId]
540
+ if (!el) return
541
+ el.innerHTML = ""
542
+ var box = document.createElement("div")
543
+ box.className = "fm-section-error"
544
+ var title = document.createElement("b")
545
+ title.textContent = "This part of the report could not be shown"
546
+ var detail = document.createElement("pre")
547
+ detail.textContent = message
548
+ box.appendChild(title)
549
+ box.appendChild(detail)
550
+ el.appendChild(box)
551
+ }
552
+
553
+ // --- markdown (text sections): escape first, then a small safe subset
554
+
555
+ function escapeHtml(text) {
556
+ return text
557
+ .replace(/&/g, "&amp;")
558
+ .replace(/</g, "&lt;")
559
+ .replace(/>/g, "&gt;")
560
+ .replace(/"/g, "&quot;")
561
+ }
562
+
563
+ function renderMarkdown(source) {
564
+ var lines = escapeHtml(String(source || "")).split(/\r?\n/)
565
+ var out = []
566
+ var list = null
567
+ function closeList() {
568
+ if (list) {
569
+ out.push("<" + list.tag + ">" + list.items.join("") + "</" + list.tag + ">")
570
+ list = null
571
+ }
572
+ }
573
+ for (var i = 0; i < lines.length; i++) {
574
+ var line = lines[i]
575
+ var heading = /^(#{1,3})\s+(.*)$/.exec(line)
576
+ var bullet = /^\s*[-*]\s+(.*)$/.exec(line)
577
+ var numbered = /^\s*\d+\.\s+(.*)$/.exec(line)
578
+ if (heading) {
579
+ closeList()
580
+ var level = heading[1].length
581
+ out.push("<h" + level + ">" + inline(heading[2]) + "</h" + level + ">")
582
+ } else if (bullet || numbered) {
583
+ var tag = bullet ? "ul" : "ol"
584
+ if (!list || list.tag !== tag) {
585
+ closeList()
586
+ list = { tag: tag, items: [] }
587
+ }
588
+ list.items.push("<li>" + inline((bullet || numbered)[1]) + "</li>")
589
+ } else if (!line.trim()) {
590
+ closeList()
591
+ } else {
592
+ closeList()
593
+ out.push("<p>" + inline(line) + "</p>")
594
+ }
595
+ }
596
+ closeList()
597
+ return out.join("")
598
+ }
599
+
600
+ function inline(text) {
601
+ return text
602
+ .replace(/`([^`]+)`/g, "<code>$1</code>")
603
+ .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
604
+ .replace(/(^|[^*])\*([^*]+)\*/g, "$1<em>$2</em>")
605
+ .replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+|mailto:[^)\s]+)\)/g, function (_all, label, href) {
606
+ return '<a href="' + href + '" target="_blank" rel="noreferrer noopener">' + label + "</a>"
607
+ })
608
+ }
609
+
610
+ // --- libs
611
+
612
+ function installLibs(bundles) {
613
+ if (!bundles) return
614
+ Object.keys(bundles).forEach(function (key) {
615
+ if (loadedLibs[key]) return
616
+ var script = document.createElement("script")
617
+ script.textContent = bundles[key]
618
+ document.head.appendChild(script)
619
+ loadedLibs[key] = true
620
+ })
621
+ }
622
+
623
+ // --- sections
624
+
625
+ function container(sectionId) {
626
+ var el = mounted[sectionId]
627
+ if (el) {
628
+ el.innerHTML = ""
629
+ return el
630
+ }
631
+ el = document.createElement("section")
632
+ el.id = sectionId
633
+ el.dataset.sectionId = sectionId
634
+ mounted[sectionId] = el
635
+ return el
636
+ }
637
+
638
+ /**
639
+ * Sections share one DOM and one CSS cascade on purpose, but top-level JS is wrapped
640
+ * so two sections cannot collide on a `const` — they share state through `window`.
641
+ * Always attempts the persisted source, even one saved as `status: "error"`: that is
642
+ * what "restore last good source" means, and a genuinely broken one throws and re-acks.
643
+ */
644
+ function mountSection(section) {
645
+ var el = container(section.id)
646
+ versions[section.id] = section.version
647
+ failed[section.id] = false
648
+ declared[section.id] = (section.queryIds || []).slice()
649
+ mounting = section.id
650
+ try {
651
+ if (section.kind === "text") {
652
+ var text = document.createElement("div")
653
+ text.className = "fm-text"
654
+ text.innerHTML = renderMarkdown(section.source)
655
+ el.appendChild(text)
656
+ return
657
+ }
658
+ var doc = new DOMParser().parseFromString(
659
+ "<body>" + String(section.source || "") + "</body>",
660
+ "text/html"
661
+ )
662
+ var scripts = []
663
+ Array.prototype.slice.call(doc.body.childNodes).forEach(function (node) {
664
+ if (node.tagName === "SCRIPT") {
665
+ scripts.push(node.textContent || "")
666
+ } else {
667
+ el.appendChild(document.importNode(node, true))
668
+ }
669
+ })
670
+ // Inline scripts throw past the caller's try/catch, so wrap the source itself.
671
+ scripts.forEach(function (code, index) {
672
+ var tag = document.createElement("script")
673
+ tag.textContent = "try{(function(){\n" + code + "\n})()}catch(e){window.FM.__err(e)}"
674
+ el.appendChild(tag)
675
+ void index
676
+ })
677
+ } catch (err) {
678
+ reportError(section.id, err)
679
+ } finally {
680
+ mounting = null
681
+ if (!failed[section.id]) {
682
+ postOutcome({ type: "rendered", sectionId: section.id, version: section.version })
683
+ }
684
+ }
685
+ }
686
+
687
+ function unmountSection(sectionId) {
688
+ var el = mounted[sectionId]
689
+ if (el && el.parentNode) el.parentNode.removeChild(el)
690
+ delete mounted[sectionId]
691
+ delete versions[sectionId]
692
+ delete failed[sectionId]
693
+ delete declared[sectionId]
694
+ Object.keys(subs).forEach(function (queryId) {
695
+ subs[queryId] = subs[queryId].filter(function (entry) {
696
+ return entry.sectionId !== sectionId
697
+ })
698
+ })
699
+ }
700
+
701
+ function renderAll(sections) {
702
+ root.innerHTML = ""
703
+ mounted = Object.create(null)
704
+ versions = Object.create(null)
705
+ failed = Object.create(null)
706
+ declared = Object.create(null)
707
+ subs = Object.create(null)
708
+ order = sections.map(function (section) {
709
+ return section.id
710
+ })
711
+ sections.forEach(function (section) {
712
+ var el = container(section.id)
713
+ root.appendChild(el)
714
+ mountSection(section)
715
+ })
716
+ if (sections.length === 0) {
717
+ var empty = document.createElement("div")
718
+ empty.className = "fm-placeholder"
719
+ empty.textContent = "This report is empty. Ask in the chat to build it."
720
+ root.appendChild(empty)
721
+ }
722
+ root.removeAttribute("aria-busy")
723
+ paintBusy()
724
+ }
725
+
726
+ /** Hot-swap: replace only the named sections, appending any that are new. */
727
+ function swapSections(sections) {
728
+ sections.forEach(function (section) {
729
+ var existing = mounted[section.id]
730
+ if (existing) {
731
+ unmountSection(section.id)
732
+ var replacement = container(section.id)
733
+ root.insertBefore(replacement, positionAfter(section.id))
734
+ mountSection(section)
735
+ return
736
+ }
737
+ order.push(section.id)
738
+ var el = container(section.id)
739
+ root.appendChild(el)
740
+ mountSection(section)
741
+ })
742
+ paintBusy()
743
+ }
744
+
745
+ function positionAfter(sectionId) {
746
+ var index = order.indexOf(sectionId)
747
+ for (var i = index + 1; i < order.length; i++) {
748
+ if (mounted[order[i]]) return mounted[order[i]]
749
+ }
750
+ return null
751
+ }
752
+
753
+ function notify(queryId) {
754
+ var list = subs[queryId] || []
755
+ list.slice().forEach(function (entry) {
756
+ try {
757
+ entry.cb(results[queryId] ? results[queryId].rows : [])
758
+ } catch (err) {
759
+ reportError(entry.sectionId, err)
760
+ }
761
+ })
762
+ }
763
+
764
+ // --- loading
765
+
766
+ /**
767
+ * Every report gets the same loading affordance, so no section hand-rolls one: a scrim
768
+ * over each section whose declared query is in flight, plus one bar for the report.
769
+ * Only `loading` counts — an idle lazy query keeps the empty state it was designed with,
770
+ * and a query the shell has not heard about yet is not assumed to be running.
771
+ */
772
+ function paintBusy() {
773
+ Object.keys(mounted).forEach(function (sectionId) {
774
+ var el = mounted[sectionId]
775
+ var busy = !failed[sectionId] && (declared[sectionId] || []).some(isRunning)
776
+ if (busy) {
777
+ el.setAttribute("data-fm-busy", "")
778
+ el.setAttribute("aria-busy", "true")
779
+ } else {
780
+ el.removeAttribute("data-fm-busy")
781
+ el.removeAttribute("aria-busy")
782
+ }
783
+ })
784
+ if (progress) {
785
+ if (Object.keys(results).some(isRunning)) progress.setAttribute("data-on", "")
786
+ else progress.removeAttribute("data-on")
787
+ }
788
+ }
789
+
790
+ function isRunning(queryId) {
791
+ var result = results[queryId]
792
+ return !!result && result.status === "loading"
793
+ }
794
+
795
+ // --- FM: the only API section code may use (see the agent system prompt)
796
+
797
+ window.env = env
798
+ window.FM = {
799
+ rows: function (queryId) {
800
+ var result = results[queryId]
801
+ return result && result.rows ? result.rows : []
802
+ },
803
+ status: function (queryId) {
804
+ var result = results[queryId]
805
+ return result ? result.status : "idle"
806
+ },
807
+ onRows: function (queryId, cb) {
808
+ if (typeof cb !== "function") return
809
+ if (!subs[queryId]) subs[queryId] = []
810
+ subs[queryId].push({ sectionId: mounting, cb: cb })
811
+ // fire immediately so a section renders its current state without waiting
812
+ try {
813
+ cb(window.FM.rows(queryId))
814
+ } catch (err) {
815
+ reportError(mounting, err)
816
+ }
817
+ },
818
+ setParams: function (values) {
819
+ if (!values || typeof values !== "object") return
820
+ post({ type: "setParams", values: values })
821
+ },
822
+ params: function () {
823
+ return JSON.parse(JSON.stringify(params))
824
+ },
825
+ /** Takes no section id on purpose: the shell attributes the failure to what it is mounting. */
826
+ __err: function (err) {
827
+ if (mounting) reportError(mounting, err)
828
+ }
829
+ }
830
+
831
+ window.addEventListener("error", function (event) {
832
+ if (!mounting) return
833
+ reportError(mounting, event.error || new Error(event.message))
834
+ })
835
+ window.addEventListener("unhandledrejection", function (event) {
836
+ if (!mounting) return
837
+ reportError(mounting, event.reason || new Error("unhandled rejection"))
838
+ })
839
+
840
+ // --- host channel
841
+
842
+ window.addEventListener("message", function (event) {
843
+ // the host is the only window that can reach us; anything else is not ours
844
+ if (event.source !== window.parent) return
845
+ var data = event.data
846
+ if (!data || typeof data !== "object") return
847
+ switch (data.type) {
848
+ case "init":
849
+ env = data.env || {}
850
+ window.env = env
851
+ params = data.params || {}
852
+ installLibs(data.libs)
853
+ renderAll(data.sections || [])
854
+ break
855
+ case "sections":
856
+ installLibs(data.libs)
857
+ swapSections(data.sections || [])
858
+ break
859
+ case "params":
860
+ params = data.values || {}
861
+ Object.keys(subs).forEach(notify)
862
+ break
863
+ // Mutated in place, never replaced: sections captured `env` at mount and a new
864
+ // object would leave every one of them reading the empty first snapshot.
865
+ case "env":
866
+ Object.keys(env).forEach(function (key) { delete env[key] })
867
+ Object.keys(data.env || {}).forEach(function (key) { env[key] = data.env[key] })
868
+ Object.keys(subs).forEach(notify)
869
+ break
870
+ case "rows":
871
+ var previous = results[data.queryId]
872
+ if (previous && previous.runId > data.runId) return // superseded run
873
+ results[data.queryId] = {
874
+ rows: data.rows || [],
875
+ status: data.status,
876
+ runId: data.runId,
877
+ truncated: !!data.truncated,
878
+ error: data.error
879
+ }
880
+ notify(data.queryId)
881
+ paintBusy()
882
+ break
883
+ }
884
+ })
885
+
886
+ post({ type: "ready", nonce: NONCE })
887
+ })()
888
+ </script>