@datarails-ai/dr-cli 0.0.1-locked → 1.6.70

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 (58) hide show
  1. package/README.md +608 -0
  2. package/dist/chunk-DWMCBGQH.js +242 -0
  3. package/dist/cli.js +64468 -0
  4. package/dist/kb-6UWI7Q32.js +21 -0
  5. package/dist/scopes-map.json +4412 -0
  6. package/dist/skills/connector-sandbox/SKILL.md +95 -0
  7. package/dist/skills/datarails-connect-workato/SKILL.md +140 -0
  8. package/dist/skills/datarails-connectors-onboard-connector/SKILL.md +813 -0
  9. package/dist/skills/dr-cli/SKILL.md +659 -0
  10. package/dist/skills/dr-cli/reference/admin.md +796 -0
  11. package/dist/skills/dr-cli/reference/budget-builder.md +151 -0
  12. package/dist/skills/dr-cli/reference/calc-vals.md +73 -0
  13. package/dist/skills/dr-cli/reference/calcmodels.md +76 -0
  14. package/dist/skills/dr-cli/reference/cash.md +61 -0
  15. package/dist/skills/dr-cli/reference/dashboards-widgets.md +85 -0
  16. package/dist/skills/dr-cli/reference/datamappers.md +150 -0
  17. package/dist/skills/dr-cli/reference/desk.md +472 -0
  18. package/dist/skills/dr-cli/reference/filebox-documents.md +373 -0
  19. package/dist/skills/dr-cli/reference/forecast.md +51 -0
  20. package/dist/skills/dr-cli/reference/integrations.md +57 -0
  21. package/dist/skills/dr-cli/reference/lookups.md +168 -0
  22. package/dist/skills/dr-cli/reference/metrics.md +512 -0
  23. package/dist/skills/dr-cli/reference/multi-product.md +180 -0
  24. package/dist/skills/dr-cli/reference/reconciliation-mec.md +76 -0
  25. package/dist/skills/dr-cli/reference/rh-config.md +84 -0
  26. package/dist/skills/dr-cli/reference/safety.md +143 -0
  27. package/dist/skills/dr-cli/reference/spend-control.md +206 -0
  28. package/dist/skills/dr-cli/reference/templates.md +163 -0
  29. package/dist/skills/dr-cli/reference/workflows.md +137 -0
  30. package/dist/skills/dr-error-enrichment/SKILL.md +80 -0
  31. package/dist/skills/dr-error-enrichment/reference/core.md +257 -0
  32. package/dist/skills/dr-error-enrichment/reference/groundcover.md +84 -0
  33. package/dist/skills/dr-error-enrichment/reference/jira-bugs.md +219 -0
  34. package/dist/skills/dr-error-enrichment/reference/safety-backup.md +122 -0
  35. package/dist/skills/dr-error-enrichment-author/SKILL.md +219 -0
  36. package/dist/skills/dr-error-enrichment-improve/SKILL.md +218 -0
  37. package/dist/skills/dr-error-enrichment-maintain/SKILL.md +237 -0
  38. package/dist/skills/dr-error-enrichment-reconcile/SKILL.md +167 -0
  39. package/dist/skills/dr-filebox-api-setup/SKILL.md +170 -0
  40. package/dist/skills/dr-switch-user/SKILL.md +395 -0
  41. package/dist/skills/dr-validate/SKILL.md +392 -0
  42. package/dist/skills/dr-validate/report-template.html +950 -0
  43. package/dist/skills/fx-conversion-matrix/SKILL.md +763 -0
  44. package/dist/skills/fx-conversion-matrix/scripts/_args.mjs +52 -0
  45. package/dist/skills/fx-conversion-matrix/scripts/_xlsx.mjs +241 -0
  46. package/dist/skills/fx-conversion-matrix/scripts/build-matrix.mjs +366 -0
  47. package/dist/skills/fx-conversion-matrix/scripts/gen-mappers.mjs +171 -0
  48. package/dist/skills/fx-conversion-matrix/scripts/rates-to-xlsx.mjs +172 -0
  49. package/dist/skills/fx-onboard/SKILL.md +325 -0
  50. package/dist/skills/mec-table-validation/SKILL.md +383 -0
  51. package/dist/skills/plugins.json +24 -0
  52. package/dist/skills/validate-workato-migration/SKILL.md +167 -0
  53. package/dist/skills/validate-workato-migration/reference/gotchas.md +16 -0
  54. package/dist/skills/validate-workato-migration/reference/staging-orgs.md +26 -0
  55. package/dist/skills/validate-workato-migration/scripts/compare_files.py +255 -0
  56. package/dist/zendeskKb-JPSHVOPH.js +243 -0
  57. package/package.json +56 -9
  58. package/scripts/postinstall.mjs +91 -0
@@ -0,0 +1,950 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>Datarails Onboarding Validation</title>
6
+ <link rel="preconnect" href="https://fonts.googleapis.com">
7
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
8
+ <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
9
+ <style>
10
+ /* ===========================================================
11
+ Datarails / FinanceOS brand tokens
12
+ =========================================================== */
13
+ :root {
14
+ /* Brand */
15
+ --brand-pink: #f93576;
16
+ --brand-pink-light: #ffdbe5;
17
+ --brand-orange: #ffa310;
18
+ --brand-orange-light: #fff8ee;
19
+
20
+ /* Ink */
21
+ --navy: #1a1a2e;
22
+ --dark: #0c142b;
23
+ --gray: #5a5a6e;
24
+ --gray-light: #6a6e80;
25
+ --gray-soft: #8a8aa0;
26
+ --text-dark: #2e3348;
27
+
28
+ /* Surfaces */
29
+ --bg-warm: #fffbf5;
30
+ --bg-white: #ffffff;
31
+ --surface-tinted: #f6f3ed;
32
+
33
+ /* Borders */
34
+ --border-card: #e8e4dd;
35
+ --border-soft: #efeae2;
36
+
37
+ /* === Aliases the existing JS/markup expects === */
38
+ --bg: var(--bg-warm);
39
+ --bg-soft: var(--surface-tinted);
40
+ --panel: var(--bg-white);
41
+ --panel-2: var(--surface-tinted);
42
+ --border: var(--border-card);
43
+ --border-strong: #d8d1c6;
44
+ --text: var(--navy);
45
+ --text-soft: var(--text-dark);
46
+ --muted: var(--gray);
47
+ --muted-2: var(--gray-soft);
48
+
49
+ /* Status palette */
50
+ --ok: #1f8f4a;
51
+ --ok-bg: #e8f9ee;
52
+ --ok-border: #b5e6c7;
53
+
54
+ --warn: #c47a05;
55
+ --warn-bg: var(--brand-orange-light);
56
+ --warn-border: #f5d99a;
57
+
58
+ --bad: #d11d4f;
59
+ --bad-bg: #fde7ee;
60
+ --bad-border: #f6b8cb;
61
+
62
+ --info: #4a4a66;
63
+ --info-bg: #f0eee9;
64
+ --info-border: #ddd6cb;
65
+
66
+ --accent: var(--brand-pink);
67
+ --accent-strong: var(--navy);
68
+ --accent-bg: #fff3f7;
69
+
70
+ /* Shadow system from FinanceOS */
71
+ --shadow: 0 2px 7px rgba(0, 0, 0, 0.04);
72
+ --shadow-md: 0 8px 24px rgba(26, 26, 46, 0.08);
73
+ --shadow-dashboard: 0 27px 54px -13px rgba(255, 163, 16, 0.18);
74
+ --shadow-cta: 0 6px 20px rgba(249, 53, 118, 0.3);
75
+
76
+ /* Easing */
77
+ --ease: cubic-bezier(0.16, 1, 0.3, 1);
78
+
79
+ /* Type */
80
+ --font-sans: 'Poppins', system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
81
+ --font-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
82
+ }
83
+
84
+ * { box-sizing: border-box; }
85
+ body {
86
+ margin: 0;
87
+ font-family: var(--font-sans);
88
+ background: var(--bg-warm);
89
+ color: var(--navy);
90
+ line-height: 1.6;
91
+ font-weight: 400;
92
+ -webkit-font-smoothing: antialiased;
93
+ -moz-osx-font-smoothing: grayscale;
94
+ }
95
+ .container { max-width: 1240px; margin: 0 auto; padding: 32px 28px; }
96
+
97
+ /* Top bar */
98
+ .topbar {
99
+ background: var(--dark);
100
+ color: #ffffff;
101
+ padding: 16px 32px;
102
+ display: flex; align-items: center; gap: 14px;
103
+ font-family: var(--font-sans);
104
+ font-weight: 500; font-size: 14px;
105
+ letter-spacing: -0.005em;
106
+ border-bottom: 1px solid rgba(255, 255, 255, 0.06);
107
+ }
108
+ .topbar .logo { width: 24px; height: 28px; display: flex; align-items: center; justify-content: center; }
109
+ .topbar .logo svg { width: 100%; height: 100%; }
110
+ .topbar .brand { font-weight: 600; font-size: 15px; color: #ffffff; letter-spacing: -0.01em; }
111
+ .topbar .product { font-weight: 400; font-size: 13px; color: rgba(255, 255, 255, 0.72); letter-spacing: 0.01em; }
112
+ .topbar .divider { width: 1px; height: 18px; background: rgba(255,255,255,0.18); }
113
+ .topbar .registered { font-size: 10px; vertical-align: super; opacity: 0.7; margin-left: 1px; }
114
+
115
+ /* Header */
116
+ header { margin-bottom: 36px; }
117
+ h1 {
118
+ margin: 0 0 8px;
119
+ font-family: var(--font-sans);
120
+ font-size: 28px; font-weight: 600;
121
+ line-height: 1.25;
122
+ letter-spacing: -0.4px;
123
+ color: var(--navy);
124
+ }
125
+ .subtitle { color: var(--gray); font-size: 14.5px; font-weight: 400; }
126
+ .subtitle a {
127
+ color: var(--brand-pink); text-decoration: none; font-weight: 500;
128
+ border-bottom: 1px solid transparent; transition: border-color 200ms var(--ease);
129
+ }
130
+ .subtitle a:hover { border-bottom-color: var(--brand-pink); }
131
+
132
+ .meta {
133
+ display: grid; grid-template-columns: repeat(4, 1fr); gap: 1px;
134
+ margin-top: 24px;
135
+ background: var(--border-soft);
136
+ border: 1px solid var(--border-soft);
137
+ border-radius: 12px;
138
+ overflow: hidden;
139
+ box-shadow: none;
140
+ }
141
+ .meta > div { background: var(--bg-white); padding: 16px 18px; font-size: 13px; }
142
+ .meta .k {
143
+ color: var(--gray-light); display: block; margin-bottom: 6px;
144
+ font-family: var(--font-sans); font-size: 11px;
145
+ text-transform: uppercase; letter-spacing: 1.2px; font-weight: 600;
146
+ }
147
+ .meta .v {
148
+ font-family: var(--font-sans); font-size: 13.5px; color: var(--navy);
149
+ word-break: break-word; font-weight: 500; line-height: 1.4;
150
+ }
151
+ .meta .v-sub { display: block; font-size: 12px; color: var(--gray); font-weight: 400; margin-top: 4px; }
152
+
153
+ /* Summary stat cards */
154
+ .summary {
155
+ display: grid; grid-template-columns: 1fr 1.6fr; gap: 14px;
156
+ margin-bottom: 36px;
157
+ }
158
+ @media (max-width: 980px) { .summary { grid-template-columns: 1fr; } }
159
+
160
+ .stat.action-items { padding: 18px 22px; }
161
+ .action-title {
162
+ font-family: var(--font-sans); font-size: 14px; font-weight: 600;
163
+ color: var(--navy); margin: 0 0 12px; letter-spacing: -0.01em;
164
+ }
165
+ .action-list { list-style: none; padding: 0; margin: 0; }
166
+ .action-list li {
167
+ display: flex; align-items: center; gap: 12px;
168
+ padding: 9px 0;
169
+ border-bottom: 1px solid var(--border-soft);
170
+ font-size: 13px;
171
+ }
172
+ .action-list li:last-child { border-bottom: none; }
173
+ .action-list .rank {
174
+ color: var(--gray-light); font-weight: 500; width: 16px; text-align: right;
175
+ font-family: var(--font-mono); font-size: 12px; flex-shrink: 0;
176
+ }
177
+ .action-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
178
+ .action-dot.sev-high { background: var(--brand-pink); }
179
+ .action-dot.sev-medium { background: var(--brand-orange); }
180
+ .action-dot.sev-low { background: var(--gray-soft); }
181
+ .action-list .action-main { flex: 1; min-width: 0; }
182
+ .action-list .action-line {
183
+ font-weight: 500; color: var(--navy);
184
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
185
+ letter-spacing: -0.01em;
186
+ }
187
+ .action-list .action-type { color: var(--gray); font-size: 11.5px; margin-top: 1px; }
188
+ .action-list .action-value {
189
+ font-family: var(--font-mono); font-variant-numeric: tabular-nums;
190
+ font-weight: 600; color: var(--navy); white-space: nowrap; font-size: 13px;
191
+ }
192
+ .action-list .action-value.muted { color: var(--gray-light); font-weight: 500; }
193
+ .action-list .action-value.done { color: var(--ok); }
194
+ .stat {
195
+ background: var(--bg-white);
196
+ border: 1px solid var(--border-soft);
197
+ border-radius: 12px;
198
+ padding: 16px 20px 14px;
199
+ box-shadow: none;
200
+ }
201
+
202
+ /* Table intro row */
203
+ .table-intro {
204
+ display: flex; align-items: center; justify-content: space-between;
205
+ gap: 16px; margin: -6px 0 16px;
206
+ }
207
+ .table-intro p { font-size: 13px; color: var(--muted); margin: 0; }
208
+ .toggle-matches {
209
+ background: var(--bg-white);
210
+ border: 1.1px solid var(--border-card);
211
+ border-radius: 999px;
212
+ padding: 8px 18px;
213
+ font-family: var(--font-sans);
214
+ font-size: 12.5px; font-weight: 500; color: var(--navy);
215
+ cursor: pointer; white-space: nowrap;
216
+ transition: border-color 200ms var(--ease), color 200ms var(--ease), background 200ms var(--ease);
217
+ }
218
+ .toggle-matches:hover { border-color: var(--brand-pink); color: var(--brand-pink); background: var(--accent-bg); }
219
+ body.hide-matches tr.is-match { display: none; }
220
+
221
+ /* Filter pills */
222
+ .filter-pills { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 16px; }
223
+ .filter-pill {
224
+ background: var(--bg-white);
225
+ border: 1.1px solid var(--border-card);
226
+ border-radius: 999px;
227
+ padding: 7px 16px;
228
+ font-family: var(--font-sans);
229
+ font-size: 12.5px; font-weight: 500; color: var(--gray);
230
+ cursor: pointer; white-space: nowrap;
231
+ transition: border-color 200ms var(--ease), color 200ms var(--ease), background 200ms var(--ease);
232
+ }
233
+ .filter-pill .count { color: var(--gray-light); font-weight: 400; margin-left: 4px; font-variant-numeric: tabular-nums; }
234
+ .filter-pill:hover { border-color: var(--brand-pink); color: var(--brand-pink); }
235
+ .filter-pill:hover .count { color: var(--brand-pink); opacity: 0.8; }
236
+ .filter-pill.active { background: var(--navy); border-color: var(--navy); color: #ffffff; }
237
+ .filter-pill.active .count { color: rgba(255, 255, 255, 0.7); }
238
+
239
+ /* Section eyebrow headers (h2) */
240
+ h2 {
241
+ font-family: var(--font-sans);
242
+ font-size: 12px;
243
+ text-transform: uppercase;
244
+ letter-spacing: 1.4px;
245
+ color: var(--gray);
246
+ margin: 40px 0 16px;
247
+ font-weight: 600;
248
+ }
249
+
250
+ /* Comparison table */
251
+ table {
252
+ width: 100%;
253
+ border-collapse: collapse;
254
+ background: var(--bg-white);
255
+ border: 1px solid var(--border-soft);
256
+ border-radius: 14px;
257
+ overflow: hidden;
258
+ box-shadow: none;
259
+ }
260
+ thead { background: var(--surface-tinted); }
261
+ th, td { padding: 13px 18px; text-align: left; font-size: 14px; vertical-align: top; }
262
+ th {
263
+ font-family: var(--font-mono);
264
+ font-weight: 700; color: var(--gray-light);
265
+ font-size: 10px; text-transform: uppercase; letter-spacing: 1.5px;
266
+ border-bottom: 1px solid var(--border-card);
267
+ padding-top: 16px; padding-bottom: 14px;
268
+ }
269
+ tbody tr { border-top: 1px solid var(--border-soft); }
270
+ tbody tr:first-child { border-top: 0; }
271
+ tbody tr.clickable { cursor: pointer; transition: background 200ms var(--ease); }
272
+ tbody tr.clickable:hover { background: var(--bg-warm); }
273
+ td.num {
274
+ font-family: var(--font-mono); font-size: 13px; text-align: right;
275
+ font-variant-numeric: tabular-nums; color: var(--text-dark); font-weight: 400;
276
+ }
277
+ td.gap-num {
278
+ font-family: var(--font-mono); font-size: 13px; text-align: right;
279
+ font-variant-numeric: tabular-nums; font-weight: 700;
280
+ }
281
+ td.line { font-family: var(--font-sans); font-weight: 500; color: var(--navy); font-size: 14px; }
282
+ td.status { text-align: center; width: 100px; }
283
+
284
+ tr.section-row td {
285
+ background: var(--bg-warm);
286
+ color: var(--navy);
287
+ font-family: var(--font-mono);
288
+ font-size: 10px; font-weight: 700;
289
+ text-transform: uppercase; letter-spacing: 1.5px;
290
+ padding: 18px 18px 10px;
291
+ border-top: 1px solid var(--border-card);
292
+ }
293
+ tr.section-row td::before {
294
+ content: "";
295
+ display: inline-block;
296
+ width: 6px; height: 6px;
297
+ border-radius: 50%;
298
+ background: var(--brand-pink);
299
+ margin-right: 10px;
300
+ vertical-align: middle;
301
+ }
302
+
303
+ /* Status & severity badges */
304
+ .badge {
305
+ display: inline-block;
306
+ padding: 4px 11px;
307
+ border-radius: 999px;
308
+ font-family: var(--font-sans);
309
+ font-size: 11px; font-weight: 600;
310
+ letter-spacing: -0.005em; white-space: nowrap; line-height: 1.4;
311
+ }
312
+ .badge.ok { background: var(--ok-bg); color: var(--ok); border: 1px solid var(--ok-border); }
313
+ .badge.warn { background: var(--warn-bg); color: var(--warn); border: 1px solid var(--warn-border); }
314
+ .badge.bad { background: var(--bad-bg); color: var(--bad); border: 1px solid var(--bad-border); }
315
+ .badge.info { background: var(--info-bg); color: var(--info); border: 1px solid var(--info-border); }
316
+ .badge.muted { background: var(--surface-tinted); color: var(--gray-light); border: 1px solid var(--border-card); }
317
+ .badge.accent { background: var(--accent-bg); color: var(--brand-pink); border: 1px solid var(--brand-pink-light); }
318
+
319
+ .sev {
320
+ display: inline-flex; align-items: center; gap: 7px;
321
+ padding: 4px 11px 4px 10px;
322
+ border-radius: 999px;
323
+ font-family: var(--font-sans);
324
+ font-size: 11px; font-weight: 600; line-height: 1.4;
325
+ }
326
+ .sev::before {
327
+ content: "";
328
+ display: inline-block;
329
+ width: 6px; height: 6px;
330
+ border-radius: 50%;
331
+ background: currentColor;
332
+ }
333
+ .sev.high { color: var(--bad); background: var(--bad-bg); border: 1px solid var(--bad-border); }
334
+ .sev.medium { color: var(--brand-orange); background: var(--brand-orange-light); border: 1px solid var(--warn-border); }
335
+ .sev.low { color: var(--info); background: var(--info-bg); border: 1px solid var(--info-border); }
336
+ .sev.none { color: var(--gray-soft); background: transparent; border: 1px dashed var(--border-card); }
337
+ .sev.none::before { background: var(--gray-soft); opacity: 0.5; }
338
+
339
+ /* Grouped-row bracket (N DR rows → 1 Client row) */
340
+ td.group-first, td.group-mid, td.group-last { position: relative; }
341
+ td.group-first::before, td.group-mid::before, td.group-last::before {
342
+ content: ""; position: absolute;
343
+ left: 8px; width: 2.5px;
344
+ background: var(--brand-pink); opacity: 0.55; border-radius: 2px;
345
+ }
346
+ td.group-first::before { top: 50%; bottom: 0; }
347
+ td.group-mid::before { top: 0; bottom: 0; }
348
+ td.group-last::before { top: 0; bottom: 50%; }
349
+ td.group-first::after, td.group-mid::after, td.group-last::after {
350
+ content: ""; position: absolute;
351
+ left: 8px; width: 9px; height: 2.5px;
352
+ background: var(--brand-pink); opacity: 0.55;
353
+ top: 50%; transform: translateY(-1px);
354
+ border-radius: 2px;
355
+ }
356
+ td.line.in-group { padding-left: 28px; }
357
+
358
+ td.merged-cell {
359
+ background: var(--brand-pink-light);
360
+ border-left: 1px solid #f6b8cb;
361
+ border-right: 1px solid #f6b8cb;
362
+ }
363
+ td.merged-cell .merged-label {
364
+ font-family: var(--font-sans); font-weight: 600; color: var(--navy);
365
+ display: flex; align-items: center; gap: 8px; font-size: 14px;
366
+ }
367
+ td.merged-cell .merged-hint {
368
+ display: block;
369
+ font-family: var(--font-mono);
370
+ color: var(--bad);
371
+ font-size: 10px; font-weight: 400; letter-spacing: 0.5px;
372
+ margin-top: 6px; text-transform: uppercase;
373
+ }
374
+
375
+ tr.has-variance td { background: transparent; }
376
+ tr.has-variance:hover td { background: var(--bg-soft); }
377
+
378
+ /* Detail panel */
379
+ tr.detail-row { display: none; }
380
+ tr.detail-row.open { display: table-row; }
381
+ tr.detail-row > td {
382
+ background: var(--bg-warm);
383
+ padding: 32px 36px;
384
+ border-top: 1px solid var(--border-card);
385
+ border-left: 4px solid var(--brand-pink);
386
+ position: relative;
387
+ }
388
+ .detail-grid { display: grid; grid-template-columns: 2fr 2fr 2fr; gap: 32px; }
389
+ @media (max-width: 900px) { .detail-grid { grid-template-columns: 1fr; } }
390
+ .detail-block h3 {
391
+ margin: 0 0 14px;
392
+ font-family: var(--font-sans);
393
+ font-size: 12px; text-transform: uppercase; letter-spacing: 1.15px;
394
+ color: var(--brand-pink); font-weight: 600;
395
+ }
396
+ .detail-block p {
397
+ margin: 0 0 12px;
398
+ font-family: var(--font-sans);
399
+ font-size: 14px; color: var(--text-dark); line-height: 1.7;
400
+ }
401
+ .detail-block ul, .detail-block ol {
402
+ margin: 0; padding-left: 20px;
403
+ font-family: var(--font-sans);
404
+ font-size: 14px; color: var(--text-dark);
405
+ }
406
+ .detail-block li { margin-bottom: 8px; line-height: 1.65; }
407
+ .detail-block .strong { color: var(--navy); font-weight: 600; }
408
+ .detail-block code {
409
+ background: var(--bg-white);
410
+ border: 1px solid var(--border-card);
411
+ border-radius: 5px;
412
+ padding: 1px 7px;
413
+ font-size: 12px;
414
+ font-family: var(--font-mono);
415
+ color: var(--brand-pink);
416
+ font-weight: 500;
417
+ }
418
+
419
+ .chev {
420
+ color: var(--gray-soft);
421
+ margin-right: 10px;
422
+ transition: transform 250ms var(--ease), color 200ms var(--ease);
423
+ display: inline-block;
424
+ width: 10px;
425
+ font-size: 9px;
426
+ }
427
+ tr.clickable:hover .chev { color: var(--brand-pink); }
428
+ tr.clickable.open .chev { transform: rotate(90deg); color: var(--brand-pink); }
429
+
430
+ footer {
431
+ margin-top: 64px;
432
+ color: var(--gray-light);
433
+ font-size: 12px;
434
+ font-family: var(--font-sans);
435
+ text-align: center;
436
+ padding-top: 28px;
437
+ border-top: 1px solid var(--border-card);
438
+ }
439
+ footer code {
440
+ background: var(--bg-white);
441
+ border: 1px solid var(--border-card);
442
+ border-radius: 4px;
443
+ padding: 2px 7px;
444
+ font-family: var(--font-mono);
445
+ font-size: 11px;
446
+ color: var(--brand-pink);
447
+ }
448
+
449
+ /* ===== Tie-out summary headline ===== */
450
+ .stat.tieout { display:block; }
451
+ .tieout-head { display:flex; align-items:center; justify-content:space-between; margin-bottom:8px; }
452
+ .tieout-title { font-size:15px; font-weight:600; color:var(--navy); }
453
+ .tieout-verdict { font-size:13px; font-weight:600; }
454
+ .tieout-verdict.warn { color:var(--warn); }
455
+ .tieout-verdict.bad { color:var(--bad); }
456
+ .tieout-banner { background:var(--bad-bg); border-left:3px solid var(--bad-border); color:var(--bad);
457
+ border-radius:0 6px 6px 0; padding:9px 11px; font-size:12.5px; line-height:1.45; margin-bottom:12px; }
458
+ .tieout-tbl { width:100%; border-collapse:collapse; }
459
+ .tieout-tbl td { padding:7px 4px; border-top:1px solid var(--border-soft); font-size:13.5px; }
460
+ .tieout-tbl tr.tieout-ch td { border-top:none; color:var(--muted); font-size:10.5px;
461
+ text-transform:uppercase; letter-spacing:.04em; padding-bottom:1px; }
462
+ .tieout-tbl .to-l { font-weight:500; color:var(--text-dark); }
463
+ .tieout-tbl .to-n { text-align:right; font-family:ui-monospace,Menlo,Consolas,monospace;
464
+ color:var(--muted); font-variant-numeric:tabular-nums; }
465
+ .tieout-tbl .to-s { text-align:right; width:118px; }
466
+ .to-pill { display:inline-block; padding:2px 8px; border-radius:999px; font-size:12px; font-weight:600; white-space:nowrap; }
467
+ .to-pill.ok { background:var(--ok-bg); color:var(--ok); border:1px solid var(--ok-border); }
468
+ .to-pill.bad { background:var(--bad-bg); color:var(--bad); border:1px solid var(--bad-border); }
469
+
470
+ /* BS accounting-identity block (Assets = Liabilities + Equity, per side) */
471
+ .tieout-identity { margin:2px 0 12px; padding:10px 12px; background:var(--surface-tinted); border-radius:8px; }
472
+ .id-title { font-family:var(--font-mono); font-size:10px; text-transform:uppercase; letter-spacing:1.2px; color:var(--gray-light); margin-bottom:8px; }
473
+ .id-row { display:flex; align-items:center; gap:10px; padding:4px 0; font-size:13px; }
474
+ .id-side { font-weight:600; color:var(--navy); width:78px; flex-shrink:0; }
475
+ .id-eq { flex:1; font-family:var(--font-mono); font-variant-numeric:tabular-nums; color:var(--text-dark); font-size:12px; }
476
+ .id-st { flex-shrink:0; }
477
+ </style>
478
+ <!--__DATA_BLOCK__-->
479
+ </head>
480
+ <body>
481
+
482
+ <div class="topbar">
483
+ <div class="logo" aria-label="Datarails">
484
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 54" fill="none" aria-hidden="true">
485
+ <path d="M41.3837 0C39.8951 0 38.6874 1.21185 38.6874 2.69625C38.6874 4.18066 39.8993 5.3925 41.3837 5.3925C42.8723 5.3925 44.0799 4.18066 44.0799 2.69625C44.0799 1.21185 42.8681 0 41.3837 0Z" fill="#F93576"/>
486
+ <path d="M13.2558 18.5211C6.74367 20.039 1.72856 25.0164 0.164485 31.5201L0.0512695 31.9856H4.67641L4.76027 31.7214C6.05598 27.5869 9.30154 24.3707 13.4487 23.1169L13.717 23.0372V18.4163L13.2516 18.5253L13.2558 18.5211Z" fill="#F9AB10"/>
487
+ <path d="M30.3674 23.8385L29.273 22.8154C27.017 20.702 24.2495 19.2176 21.2723 18.5215L20.8068 18.4125V23.0334L21.0752 23.1131C26.6312 24.7946 30.3674 29.8307 30.3674 35.6467C30.3674 42.8716 24.4885 48.7505 17.2636 48.7505C14.2822 48.7505 11.3637 47.719 9.04062 45.8404C6.98174 44.1757 5.43863 41.8694 4.69223 39.3409L4.61256 39.0684H0L0.104831 39.5296C0.926705 43.1526 2.93527 46.4946 5.75731 48.9392C8.95256 51.7068 13.0368 53.2331 17.2636 53.2331C21.7335 53.2331 25.9981 51.5432 29.273 48.478L30.3674 47.4548V52.7425H34.8542V9.27124H30.3674V23.8343V23.8385Z" fill="#FFFFFF"/>
488
+ </svg>
489
+ </div>
490
+ <span class="brand">Datarails<span class="registered">®</span></span>
491
+ <div class="divider"></div>
492
+ <span class="product">AI Validation</span>
493
+ </div>
494
+
495
+ <div class="container">
496
+
497
+ <header>
498
+ <h1 id="rpt-title"></h1>
499
+ <div class="subtitle" id="rpt-subtitle"></div>
500
+ <div class="meta" id="rpt-meta"></div>
501
+ </header>
502
+
503
+ <!-- Summary: Tie-out + Top Action Items -->
504
+ <div class="summary">
505
+ <div class="stat tieout" id="rpt-tieout" style="display:none;">
506
+ <div class="tieout-head">
507
+ <span class="tieout-title">Tie-out summary</span>
508
+ <span class="tieout-verdict" id="rpt-tieout-verdict"></span>
509
+ </div>
510
+ <div class="tieout-banner" id="rpt-tieout-banner"></div>
511
+ <div class="tieout-identity" id="rpt-tieout-identity" style="display:none;"></div>
512
+ <table class="tieout-tbl" id="rpt-tieout-tbl"></table>
513
+ </div>
514
+
515
+ <!-- Top Action Items -->
516
+ <div class="stat action-items">
517
+ <div class="action-title">Top Action Items</div>
518
+ <ol class="action-list" id="actionList"><!-- populated by JS --></ol>
519
+ </div>
520
+ </div>
521
+
522
+ <!-- Comparison table -->
523
+ <h2>Line-by-line comparison</h2>
524
+ <div class="table-intro">
525
+ <p>
526
+ Click any row marked with <span class="chev" style="color:var(--accent)">▶</span> to expand the root-cause investigation and recommended actions.
527
+ </p>
528
+ <button type="button" class="toggle-matches" id="toggleMatches">Hide matched lines</button>
529
+ </div>
530
+ <div class="filter-pills" id="filterPills"><!-- populated by JS --></div>
531
+ <table>
532
+ <thead>
533
+ <tr>
534
+ <th style="width:26%;">Datarails line</th>
535
+ <th style="width:11%; text-align:right;">DR value</th>
536
+ <th style="width:26%;">Client line</th>
537
+ <th style="width:11%; text-align:right;">Client value</th>
538
+ <th style="width:8%; text-align:right;">Gap</th>
539
+ <th style="width:9%; text-align:center;">Severity</th>
540
+ <th style="width:9%; text-align:center;">Status</th>
541
+ </tr>
542
+ </thead>
543
+ <tbody id="cmp"></tbody>
544
+ </table>
545
+
546
+ <footer id="rpt-footer"></footer>
547
+
548
+ </div>
549
+
550
+ <script>
551
+ // =============================================================================
552
+ // Data model — injected by the CLI as window.__DATA__.rows (see
553
+ // src/types/validateReport.ts). Row shapes:
554
+ // { section: "..." } → section divider
555
+ // { type: "match"|"variance"|"subtotal", drLine, clLine,
556
+ // dr, client, status, severity, detail? } → 1:1 (incl. one-sided)
557
+ // { type: "group", side:"dr"|"client", memberNames:[...],
558
+ // mergedName, dr, client, status, severity, detail? } → N:1 grouping
559
+ // dr/client are the authoritative side totals; grouped member per-line
560
+ // amounts are not available from the service (shown as "—").
561
+ // =============================================================================
562
+ const ROWS = (window.__DATA__ && window.__DATA__.rows) || [];
563
+
564
+ // -------------------- helpers --------------------
565
+ const TOL_ABS = 0.01, TOL_REL = 1e-6;
566
+ const fmt = (n) => {
567
+ if (n === null || n === undefined) return "—";
568
+ if (typeof n !== "number") return n;
569
+ if (Math.abs(n) < 0.0001) return "0";
570
+ if (Math.abs(n) < 100 && !Number.isInteger(n)) return n.toFixed(2);
571
+ return n.toLocaleString("en-US", { minimumFractionDigits: 0, maximumFractionDigits: 2 });
572
+ };
573
+ const gapClass = (g) => Math.abs(g) > 100 ? "var(--bad)" : "var(--warn)";
574
+ const escapeHtml = (s) => String(s).replace(/[&<>]/g, c => ({ "&":"&amp;", "<":"&lt;", ">":"&gt;" })[c]);
575
+
576
+ const statusBadge = (s) => {
577
+ switch (s) {
578
+ case "match": return '<span class="badge ok">match</span>';
579
+ case "variance": return '<span class="badge bad">variance</span>';
580
+ case "sign-flip": return '<span class="badge accent">sign flip</span>';
581
+ case "grouped": return '<span class="badge info">different grouping</span>';
582
+ case "reclass": return '<span class="badge warn">reclass</span>';
583
+ case "dr-only": return '<span class="badge warn">DR only</span>';
584
+ case "client-only": return '<span class="badge warn">Client only</span>';
585
+ default: return '<span class="badge muted">' + (s || "—") + '</span>';
586
+ }
587
+ };
588
+ const severityBadge = (s) => {
589
+ if (!s || s === "none") return '<span class="sev none">—</span>';
590
+ return '<span class="sev ' + s + '">' + s.charAt(0).toUpperCase() + s.slice(1) + '</span>';
591
+ };
592
+
593
+ const tbody = document.getElementById("cmp");
594
+ let detailIdx = 0;
595
+
596
+ function buildDetailRow(detail) {
597
+ return `<td colspan="7">
598
+ <div class="detail-grid">
599
+ <div class="detail-block">
600
+ <h3>1 · What is the gap?</h3>
601
+ <p>${detail.what}</p>
602
+ </div>
603
+ <div class="detail-block">
604
+ <h3>2 · Where it comes from</h3>
605
+ <p>${detail.cause}</p>
606
+ </div>
607
+ <div class="detail-block">
608
+ <h3>3 · Actions to fix</h3>
609
+ <ul>${detail.actions.map(a => `<li>${a}</li>`).join("")}</ul>
610
+ </div>
611
+ </div>
612
+ </td>`;
613
+ }
614
+
615
+ function appendRow(html, opts = {}) {
616
+ const tr = document.createElement("tr");
617
+ if (opts.cls) tr.className = opts.cls;
618
+ if (opts.targetId) tr.dataset.target = opts.targetId;
619
+ if (opts.status) tr.dataset.status = opts.status;
620
+ tr.innerHTML = html;
621
+ tbody.appendChild(tr);
622
+ return tr;
623
+ }
624
+
625
+ function appendDetail(detail, id) {
626
+ const tr = document.createElement("tr");
627
+ tr.className = "detail-row";
628
+ tr.id = id;
629
+ tr.innerHTML = buildDetailRow(detail);
630
+ tbody.appendChild(tr);
631
+ }
632
+
633
+ function renderTable() {
634
+ tbody.innerHTML = "";
635
+ detailIdx = 0;
636
+
637
+ ROWS.forEach((r) => {
638
+ // SECTION
639
+ if (r.section) {
640
+ appendRow(`<td colspan="7">${r.section}</td>`, { cls: "section-row" });
641
+ return;
642
+ }
643
+
644
+ // GROUP (N lines on one side → 1 on the other). Per-line member amounts are
645
+ // unavailable from the service, so members show names + "—" and the
646
+ // multi-side aggregate is shown in the merged-cell hint.
647
+ if (r.type === "group") {
648
+ const did = "det-" + (++detailIdx);
649
+ const isDrSide = (r.side || "dr") === "dr";
650
+ const members = r.memberNames || [];
651
+ const n = members.length;
652
+ const multiTotal = isDrSide ? r.dr : r.client;
653
+ const gap = (r.dr || 0) - (r.client || 0);
654
+ const hasGap = Math.abs(gap) > TOL_ABS;
655
+ const gapDisplay = hasGap ? fmt(gap) : "0";
656
+ const gapColor = hasGap ? gapClass(gap) : "var(--muted)";
657
+ const dash = '<td class="num" style="color:var(--muted-2);">—</td>';
658
+
659
+ members.forEach((name, i) => {
660
+ const isFirst = (i === 0), isLast = (i === n - 1);
661
+ const groupCls = isFirst ? "group-first" : (isLast ? "group-last" : "group-mid");
662
+ const memberNameCell = `<td class="line in-group ${groupCls}">` +
663
+ (isFirst ? '<span class="chev">▶</span>' : '<span class="chev"></span>') + escapeHtml(name) + `</td>`;
664
+ const mergedLabelCell = isFirst
665
+ ? `<td class="merged-cell" rowspan="${n}"><div class="merged-label">${escapeHtml(r.mergedName || "—")}</div>` +
666
+ `<span class="merged-hint">${n} ${isDrSide ? "Datarails" : "Client"} lines (total ${fmt(multiTotal)}) combined into 1 ${isDrSide ? "Client" : "Datarails"} line</span></td>`
667
+ : "";
668
+ const gapSevStatus = isFirst
669
+ ? `<td class="gap-num merged-cell" rowspan="${n}" style="color:${gapColor}; font-weight:600;">${gapDisplay}</td>` +
670
+ `<td class="status merged-cell" rowspan="${n}" style="text-align:center;">${severityBadge(r.severity)}</td>` +
671
+ `<td class="status merged-cell" rowspan="${n}" style="text-align:center;">${statusBadge(r.status)}</td>`
672
+ : "";
673
+
674
+ let html;
675
+ if (isDrSide) {
676
+ const mergedClientVal = isFirst ? `<td class="num merged-cell" rowspan="${n}">${fmt(r.client)}</td>` : "";
677
+ html = memberNameCell + dash + mergedLabelCell + mergedClientVal + gapSevStatus;
678
+ } else {
679
+ const mergedDrVal = isFirst ? `<td class="num merged-cell" rowspan="${n}">${fmt(r.dr)}</td>` : "";
680
+ html = mergedLabelCell + mergedDrVal + memberNameCell + dash + gapSevStatus;
681
+ }
682
+ const clickable = isFirst ? "clickable has-variance" : "has-variance";
683
+ appendRow(html, { cls: clickable, targetId: (isFirst && r.detail) ? did : undefined, status: r.status });
684
+ });
685
+
686
+ if (r.detail) appendDetail(r.detail, did);
687
+ return;
688
+ }
689
+
690
+ // MATCH / VARIANCE / SUBTOTAL (1:1, incl. one-sided DR-only / Client-only)
691
+ const dr = r.dr, cl = r.client;
692
+ const oneSided = (dr === null || dr === undefined || cl === null || cl === undefined);
693
+ const status = r.status || "match";
694
+ // Sign-convention lines agree in magnitude; gap is |dr|-|cl| (≈0), never dr-cl (~2x).
695
+ const gap = oneSided ? 0 : (status === "sign-flip" ? (Math.abs(dr) - Math.abs(cl)) : (dr - cl));
696
+ const tol = Math.max(TOL_ABS, TOL_REL * Math.max(Math.abs(dr || 0), Math.abs(cl || 0)));
697
+ const hasGap = !oneSided && Math.abs(gap) > tol;
698
+ const severity = r.severity || (hasGap ? "high" : "none");
699
+ const hasDetail = !!r.detail;
700
+ const did = hasDetail ? "det-" + (++detailIdx) : null;
701
+ const isSubtotal = r.type === "subtotal";
702
+ const variantCls = (status !== "match") ? "has-variance" : "";
703
+
704
+ const gapColor = hasGap ? gapClass(gap) : "var(--muted)";
705
+ const gapCell = oneSided ? '<td class="gap-num" style="color:var(--muted-2);">—</td>'
706
+ : `<td class="gap-num" style="color:${gapColor}">${hasGap ? fmt(gap) : "0"}</td>`;
707
+
708
+ const drLineCell = `<td class="line">` +
709
+ (hasDetail ? '<span class="chev">▶</span>' : '<span class="chev"></span>') +
710
+ (r.drLine ? escapeHtml(r.drLine) : '<span style="color:var(--muted-2);">—</span>') +
711
+ (isSubtotal ? '<span style="color:var(--muted); font-weight:400;"> (subtotal)</span>' : '') +
712
+ `</td>`;
713
+
714
+ const html =
715
+ drLineCell +
716
+ `<td class="num">${fmt(dr)}</td>` +
717
+ `<td class="line">${r.clLine ? escapeHtml(r.clLine) : '<span style="color:var(--muted-2);">—</span>'}</td>` +
718
+ `<td class="num">${fmt(cl)}</td>` +
719
+ gapCell +
720
+ `<td class="status">${severityBadge(severity)}</td>` +
721
+ `<td class="status">${statusBadge(status)}</td>`;
722
+
723
+ const isMatchRow = (status === "match");
724
+ const matchCls = isMatchRow ? "is-match " : "";
725
+
726
+ appendRow(html, {
727
+ cls: matchCls + (hasDetail ? "clickable " : "") + variantCls,
728
+ targetId: did,
729
+ status: status
730
+ });
731
+
732
+ if (hasDetail) appendDetail(r.detail, did);
733
+ });
734
+ } // end renderTable
735
+
736
+ renderTable();
737
+
738
+ // -------------------- Populate Top Action Items --------------------
739
+ function populateActionItems() {
740
+ const list = document.getElementById("actionList");
741
+ if (!list) return;
742
+
743
+ const sevRank = { high: 0, medium: 1, low: 2, none: 3 };
744
+ const typeLabel = {
745
+ "variance": "amount mismatch",
746
+ "sign-flip": "sign convention",
747
+ "grouped": "grouping difference",
748
+ "reclass": "reclassification",
749
+ "dr-only": "extra DR data",
750
+ "client-only": "extra Client data"
751
+ };
752
+
753
+ const candidates = ROWS
754
+ .filter(r => !r.section && r.severity && r.severity !== "none" && r.severity !== "low")
755
+ .map(r => {
756
+ const line = (r.type === "group") ? (r.mergedName || (r.memberNames || []).join(" + ")) : (r.drLine || r.clLine);
757
+ const value = (r.dr || 0) - (r.client || 0);
758
+ return { line: line || "—", type: r.status || "variance", severity: r.severity, value, magnitude: Math.abs(value) };
759
+ })
760
+ .sort((a, b) => sevRank[a.severity] - sevRank[b.severity] || b.magnitude - a.magnitude)
761
+ .slice(0, 5);
762
+
763
+ const matched = ROWS.filter(r => !r.section && r.status === "match").length;
764
+ const items = candidates.map((c, i) => {
765
+ const label = typeLabel[c.type] || c.type;
766
+ const v = c.value;
767
+ const valueDisplay = v < 0 ? "(" + fmt(Math.abs(v)) + ")" : fmt(v);
768
+ return `
769
+ <li>
770
+ <span class="rank">${i + 1}.</span>
771
+ <span class="action-dot sev-${c.severity}"></span>
772
+ <div class="action-main">
773
+ <div class="action-line">${escapeHtml(c.line)}</div>
774
+ <div class="action-type">${label}</div>
775
+ </div>
776
+ <div class="action-value">${valueDisplay}</div>
777
+ </li>
778
+ `;
779
+ }).join("");
780
+ const tail = candidates.length
781
+ ? `
782
+ <li>
783
+ <span class="rank">·</span>
784
+ <span class="action-dot sev-low"></span>
785
+ <div class="action-main">
786
+ <div class="action-line">Everything else</div>
787
+ <div class="action-type">${matched} matched line${matched === 1 ? "" : "s"} reconcile — no action</div>
788
+ </div>
789
+ <div class="action-value muted">reconciles</div>
790
+ </li>`
791
+ : `
792
+ <li>
793
+ <span class="rank">·</span>
794
+ <span class="action-dot sev-low"></span>
795
+ <div class="action-main">
796
+ <div class="action-line">No action items flagged</div>
797
+ <div class="action-type">${matched} matched line${matched === 1 ? "" : "s"} — nothing needs attention</div>
798
+ </div>
799
+ <div class="action-value done">✓ clean</div>
800
+ </li>`;
801
+ list.innerHTML = items + tail;
802
+ }
803
+ populateActionItems();
804
+
805
+ // -------------------- Filter pills --------------------
806
+ function setupFilterPills() {
807
+ const pillsContainer = document.getElementById("filterPills");
808
+ if (!pillsContainer) return;
809
+
810
+ const STATUS_LABEL = {
811
+ "match": "Match",
812
+ "variance": "Variance",
813
+ "sign-flip": "Sign flip",
814
+ "grouped": "Different grouping",
815
+ "reclass": "Reclass",
816
+ "dr-only": "DR only",
817
+ "client-only": "Client only"
818
+ };
819
+ const STATUS_ORDER = ["match", "variance", "sign-flip", "grouped", "reclass", "dr-only", "client-only"];
820
+
821
+ const counts = {};
822
+ document.querySelectorAll("#cmp tr[data-status]").forEach(tr => {
823
+ const s = tr.dataset.status;
824
+ counts[s] = (counts[s] || 0) + 1;
825
+ });
826
+ const totalRows = Object.values(counts).reduce((a, b) => a + b, 0);
827
+
828
+ const presentStatuses = STATUS_ORDER.filter(s => counts[s] && counts[s] > 0);
829
+ const pills = [
830
+ `<button class="filter-pill active" data-filter="all">All <span class="count">${totalRows}</span></button>`,
831
+ ...presentStatuses.map(s =>
832
+ `<button class="filter-pill" data-filter="${s}">${STATUS_LABEL[s]} <span class="count">${counts[s]}</span></button>`
833
+ )
834
+ ];
835
+ pillsContainer.innerHTML = pills.join("");
836
+
837
+ function applyFilter(filterStatus) {
838
+ let lastNonDetailVisible = true;
839
+ document.querySelectorAll("#cmp tr").forEach(tr => {
840
+ if (tr.classList.contains("section-row")) {
841
+ tr.style.display = "";
842
+ return;
843
+ }
844
+ if (tr.classList.contains("detail-row")) {
845
+ tr.style.display = lastNonDetailVisible ? "" : "none";
846
+ if (!lastNonDetailVisible) tr.classList.remove("open");
847
+ return;
848
+ }
849
+ const rowStatus = tr.dataset.status;
850
+ const visible = (filterStatus === "all" || rowStatus === filterStatus);
851
+ tr.style.display = visible ? "" : "none";
852
+ lastNonDetailVisible = visible;
853
+ });
854
+ }
855
+
856
+ pillsContainer.onclick = e => {
857
+ const pill = e.target.closest(".filter-pill");
858
+ if (!pill) return;
859
+ pillsContainer.querySelectorAll(".filter-pill").forEach(p => p.classList.remove("active"));
860
+ pill.classList.add("active");
861
+ applyFilter(pill.dataset.filter);
862
+ };
863
+ }
864
+ setupFilterPills();
865
+
866
+ // Click-to-expand
867
+ tbody.addEventListener("click", (e) => {
868
+ const tr = e.target.closest("tr.clickable");
869
+ if (!tr || !tr.dataset.target) return;
870
+ const target = document.getElementById(tr.dataset.target);
871
+ if (!target) return;
872
+ const isOpen = target.classList.toggle("open");
873
+ tr.classList.toggle("open", isOpen);
874
+ });
875
+
876
+ // Hide/show matched lines
877
+ (function setupToggleMatches() {
878
+ const btn = document.getElementById("toggleMatches");
879
+ if (!btn) return;
880
+ btn.addEventListener("click", () => {
881
+ const hidden = document.body.classList.toggle("hide-matches");
882
+ btn.textContent = hidden ? "Show matched lines" : "Hide matched lines";
883
+ });
884
+ })();
885
+
886
+ // -------------------- Bootstrap: header / tie-out / footer from __DATA__ --------------------
887
+ (function bootstrap() {
888
+ const DATA = (typeof window !== "undefined" && window.__DATA__) ? window.__DATA__ : { meta: {}, tieout: null, rows: [] };
889
+ const m = DATA.meta || {};
890
+ const fmtDollar = (n) => (n === null || n === undefined) ? "—" : "$" + fmt(n);
891
+ const setText = (id, v) => { const el = document.getElementById(id); if (el) el.textContent = v || ""; };
892
+ setText("rpt-title", m.title);
893
+ setText("rpt-subtitle", m.subtitle);
894
+
895
+ const metaEl = document.getElementById("rpt-meta");
896
+ if (metaEl) {
897
+ const cards = [
898
+ ["Organization", m.org, m.orgId ? ("Server: " + (m.server || "") + " · Org " + m.orgId) : (m.server ? ("Server: " + m.server) : "")],
899
+ ["Period", m.period, ""],
900
+ ["Client report", m.clientReportName, m.clientReportRef],
901
+ ["Datarails report", m.drReportName, m.drReportRef]
902
+ ];
903
+ metaEl.innerHTML = cards.map((c) =>
904
+ '<div><span class="k">' + escapeHtml(c[0]) + '</span><span class="v">' + escapeHtml(c[1] || "—") +
905
+ (c[2] ? '<span class="v-sub">' + escapeHtml(c[2]) + '</span>' : "") + '</span></div>').join("");
906
+ }
907
+
908
+ const t = DATA.tieout;
909
+ if (t) {
910
+ const card = document.getElementById("rpt-tieout");
911
+ if (card) card.style.display = "";
912
+ const verdict = document.getElementById("rpt-tieout-verdict");
913
+ if (verdict) { verdict.textContent = t.verdict.label; verdict.className = "tieout-verdict " + (t.verdict.balanced ? "" : "bad"); }
914
+ const banner = document.getElementById("rpt-tieout-banner");
915
+ if (banner) banner.innerHTML = t.banner || ""; // CLI-sanitized (allowlisted tags only)
916
+ const idn = t.identity;
917
+ const idEl = document.getElementById("rpt-tieout-identity");
918
+ if (idEl && idn && (idn.datarails || idn.client)) {
919
+ idEl.style.display = "";
920
+ // Always 2 decimals for the identity equation (cleaner than fmt's variable dp).
921
+ const money2 = (n) => (n === null || n === undefined) ? "—" : "$" + Number(n).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
922
+ const eqRow = (label, e) => {
923
+ if (!e) return "";
924
+ const st = e.balanced
925
+ ? '<span class="to-pill ok">✓ balanced</span>'
926
+ : '<span class="to-pill bad">✗ off ' + money2(Math.abs(e.difference || 0)) + '</span>';
927
+ return '<div class="id-row"><span class="id-side">' + escapeHtml(label) + '</span>' +
928
+ '<span class="id-eq">Assets ' + money2(e.assets) + ' = Liabilities + Equity ' + money2(e.liabilitiesAndEquity) + '</span>' +
929
+ '<span class="id-st">' + st + '</span></div>';
930
+ };
931
+ idEl.innerHTML = '<div class="id-title">Accounting identity · Assets = Liabilities + Equity</div>' +
932
+ eqRow("Datarails", idn.datarails) + eqRow("Client", idn.client);
933
+ }
934
+ const tbl = document.getElementById("rpt-tieout-tbl");
935
+ if (tbl) {
936
+ tbl.innerHTML = '<tr class="tieout-ch"><td>Total</td><td class="to-n">Datarails</td><td class="to-n">Client</td><td class="to-s">Status</td></tr>' +
937
+ (t.rows || []).map((r) =>
938
+ '<tr><td class="to-l">' + escapeHtml(r.label) + '</td><td class="to-n">' + fmtDollar(r.dr) + '</td><td class="to-n">' + fmtDollar(r.client) + '</td>' +
939
+ '<td class="to-s"><span class="to-pill ' + (r.tied ? "ok" : "bad") + '">' + (r.tied ? "✓ Tied" : "✗ Off " + fmtDollar(Math.abs(r.gap))) + '</span></td></tr>').join("");
940
+ }
941
+ }
942
+
943
+ const footer = document.getElementById("rpt-footer");
944
+ if (footer) footer.innerHTML = "Generated " + escapeHtml(m.generatedAt || "") + " · validation session <code>" + escapeHtml(m.sessionId || "") + "</code>" +
945
+ (m.tenant ? (" on tenant <code>" + escapeHtml(m.tenant) + "</code>") : "");
946
+ })();
947
+ </script>
948
+
949
+ </body>
950
+ </html>