retold-facto 0.0.4

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 (92) hide show
  1. package/.claude/launch.json +11 -0
  2. package/.dockerignore +8 -0
  3. package/.quackage.json +19 -0
  4. package/Dockerfile +26 -0
  5. package/bin/retold-facto.js +909 -0
  6. package/examples/facto-government-data.sqlite +0 -0
  7. package/examples/government-data-catalog.json +137 -0
  8. package/examples/government-data-loader.js +1432 -0
  9. package/package.json +91 -0
  10. package/scripts/facto-download.js +425 -0
  11. package/source/Retold-Facto.js +1042 -0
  12. package/source/services/Retold-Facto-BeaconProvider.js +511 -0
  13. package/source/services/Retold-Facto-CatalogManager.js +1252 -0
  14. package/source/services/Retold-Facto-DataLakeService.js +1642 -0
  15. package/source/services/Retold-Facto-DatasetManager.js +417 -0
  16. package/source/services/Retold-Facto-IngestEngine.js +1315 -0
  17. package/source/services/Retold-Facto-ProjectionEngine.js +3960 -0
  18. package/source/services/Retold-Facto-RecordManager.js +360 -0
  19. package/source/services/Retold-Facto-SchemaManager.js +1110 -0
  20. package/source/services/Retold-Facto-SourceFolderScanner.js +2243 -0
  21. package/source/services/Retold-Facto-SourceManager.js +730 -0
  22. package/source/services/Retold-Facto-StoreConnectionManager.js +441 -0
  23. package/source/services/Retold-Facto-ThroughputMonitor.js +478 -0
  24. package/source/services/web-app/codemirror-entry.js +7 -0
  25. package/source/services/web-app/pict-app/Pict-Application-Facto-Configuration.json +9 -0
  26. package/source/services/web-app/pict-app/Pict-Application-Facto.js +70 -0
  27. package/source/services/web-app/pict-app/Pict-Facto-Bundle.js +11 -0
  28. package/source/services/web-app/pict-app/providers/Pict-Provider-Facto-UI.js +66 -0
  29. package/source/services/web-app/pict-app/providers/Pict-Provider-Facto.js +69 -0
  30. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Catalog.js +93 -0
  31. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Connections.js +42 -0
  32. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Datasets.js +605 -0
  33. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Projections.js +188 -0
  34. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Scanner.js +80 -0
  35. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Schema.js +116 -0
  36. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Sources.js +104 -0
  37. package/source/services/web-app/pict-app/views/PictView-Facto-Catalog.js +526 -0
  38. package/source/services/web-app/pict-app/views/PictView-Facto-Datasets.js +173 -0
  39. package/source/services/web-app/pict-app/views/PictView-Facto-Ingest.js +259 -0
  40. package/source/services/web-app/pict-app/views/PictView-Facto-Layout.js +191 -0
  41. package/source/services/web-app/pict-app/views/PictView-Facto-Projections.js +231 -0
  42. package/source/services/web-app/pict-app/views/PictView-Facto-Records.js +326 -0
  43. package/source/services/web-app/pict-app/views/PictView-Facto-Scanner.js +624 -0
  44. package/source/services/web-app/pict-app/views/PictView-Facto-Sources.js +201 -0
  45. package/source/services/web-app/pict-app/views/PictView-Facto-Throughput.js +456 -0
  46. package/source/services/web-app/pict-app-full/Pict-Application-Facto-Full-Configuration.json +14 -0
  47. package/source/services/web-app/pict-app-full/Pict-Application-Facto-Full.js +391 -0
  48. package/source/services/web-app/pict-app-full/providers/PictRouter-Facto-Configuration.json +56 -0
  49. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-BottomBar.js +68 -0
  50. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Connections.js +340 -0
  51. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Dashboard.js +149 -0
  52. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Dashboards.js +819 -0
  53. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Datasets.js +178 -0
  54. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-IngestJobs.js +99 -0
  55. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Layout.js +62 -0
  56. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-MappingEditor.js +158 -0
  57. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-ProjectionDetail.js +1120 -0
  58. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Projections.js +172 -0
  59. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-QueryPanel.js +119 -0
  60. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-RecordViewer.js +663 -0
  61. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Records.js +648 -0
  62. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Scanner.js +1017 -0
  63. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SchemaDetail.js +1404 -0
  64. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SchemaDocEditor.js +1036 -0
  65. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SchemaEditor.js +636 -0
  66. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SchemaResearch.js +357 -0
  67. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SourceDetail.js +822 -0
  68. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SourceEditor.js +1036 -0
  69. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SourceResearch.js +487 -0
  70. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Sources.js +165 -0
  71. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Throughput.js +439 -0
  72. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-TopBar.js +335 -0
  73. package/source/services/web-app/pict-app-full/views/projections/Facto-Projections-Constants.js +71 -0
  74. package/source/services/web-app/web/chart.min.js +20 -0
  75. package/source/services/web-app/web/codemirror-bundle.js +30099 -0
  76. package/source/services/web-app/web/css/facto-themes.css +467 -0
  77. package/source/services/web-app/web/css/facto.css +502 -0
  78. package/source/services/web-app/web/index.html +28 -0
  79. package/source/services/web-app/web/retold-facto.js +12138 -0
  80. package/source/services/web-app/web/retold-facto.js.map +1 -0
  81. package/source/services/web-app/web/retold-facto.min.js +2 -0
  82. package/source/services/web-app/web/retold-facto.min.js.map +1 -0
  83. package/source/services/web-app/web/simple/index.html +17 -0
  84. package/test/Facto_Browser_Integration_tests.js +798 -0
  85. package/test/RetoldFacto_tests.js +4117 -0
  86. package/test/fixtures/weather-readings.csv +17 -0
  87. package/test/fixtures/weather-stations.csv +9 -0
  88. package/test/model/MeadowModel-Extended.json +8497 -0
  89. package/test/model/MeadowModel-PICT.json +1 -0
  90. package/test/model/MeadowModel.json +1355 -0
  91. package/test/model/ddl/Facto.ddl +225 -0
  92. package/test/model/fable-configuration.json +14 -0
@@ -0,0 +1,439 @@
1
+ /**
2
+ * PictView-Facto-Full-Throughput
3
+ *
4
+ * Pipeline throughput visualization for the Full (hash-routed) Facto app.
5
+ * Shows live temporal histograms via pict-section-histogram instances.
6
+ * Each histogram has an inline time axis showing start / mid / end ticks.
7
+ */
8
+ const libPictView = require('pict-view');
9
+ const libPictSectionHistogram = require('pict-section-histogram');
10
+
11
+ // Histogram definitions — one per pipeline stage plus a combined total
12
+ const HISTOGRAM_DEFS =
13
+ [
14
+ { id: 'Facto-Full-Histogram-Extracted', stage: 'extracted', color: '#4a90d9', label: 'Extracted' },
15
+ { id: 'Facto-Full-Histogram-Transformed', stage: 'transformed', color: '#d09818', label: 'Transformed' },
16
+ { id: 'Facto-Full-Histogram-Written', stage: 'written', color: '#3a9468', label: 'Written' },
17
+ { id: 'Facto-Full-Histogram-Total', stage: 'total', color: '#6366f1', label: 'Total' },
18
+ ];
19
+
20
+ // Bar geometry — must match _ensureHistogramViews config.
21
+ // Width drives the histogram's pixel span: BAR_STRIDE × bucketCount should
22
+ // be close to the container width (~568px for a 2-col 1200px layout).
23
+ // 30 buckets × 18px = 540px ≈ fills the half-column container.
24
+ const BAR_THICKNESS = 16;
25
+ const BAR_GAP = 2;
26
+ const BAR_STRIDE = BAR_THICKNESS + BAR_GAP; // 18px per bucket
27
+
28
+ const POLL_INTERVAL_MS = 500;
29
+
30
+ class FactoFullThroughputView extends libPictView
31
+ {
32
+ constructor(pFable, pOptions, pServiceHash)
33
+ {
34
+ super(pFable, pOptions, pServiceHash);
35
+ this._pollTimer = null;
36
+ this._isPolling = false;
37
+ this._activeHistoricalRun = null;
38
+ this._activeDatasetFilter = null;
39
+ this._histogramsReady = false;
40
+ }
41
+
42
+ onAfterRender()
43
+ {
44
+ this._ensureHistogramViews();
45
+ // Render empty histograms immediately so containers aren't blank
46
+ this._renderHistograms({ buckets: [], interval: 1000 });
47
+ this.loadRunHistory();
48
+ }
49
+
50
+ // ─────────────────────────────────────────────
51
+ // Histogram view management
52
+ // ─────────────────────────────────────────────
53
+
54
+ _ensureHistogramViews()
55
+ {
56
+ if (this._histogramsReady) return;
57
+
58
+ for (let i = 0; i < HISTOGRAM_DEFS.length; i++)
59
+ {
60
+ let tmpDef = HISTOGRAM_DEFS[i];
61
+ // Container id is deterministic: stage name capitalised
62
+ let tmpContainerId = 'Facto-Full-Histogram-' + _capitalise(tmpDef.stage) + '-Container';
63
+
64
+ if (!this.pict.views[tmpDef.id])
65
+ {
66
+ this.pict.addView(tmpDef.id,
67
+ {
68
+ ViewIdentifier: tmpDef.id,
69
+ AutoRender: false,
70
+ RenderMode: 'browser',
71
+ Orientation: 'vertical',
72
+ TargetElementAddress: '#' + tmpContainerId,
73
+ DefaultDestinationAddress: '#' + tmpContainerId,
74
+ MaxBarSize: 100,
75
+ BarThickness: BAR_THICKNESS,
76
+ BarGap: BAR_GAP,
77
+ ShowValues: false,
78
+ ShowLabels: false,
79
+ BarColor: tmpDef.color,
80
+ Bins: [],
81
+ },
82
+ libPictSectionHistogram);
83
+ }
84
+ }
85
+
86
+ this._histogramsReady = true;
87
+ }
88
+
89
+ /**
90
+ * Convert a bucket array to a Bins array for pict-section-histogram.
91
+ * Labels are always empty strings — we draw our own time axis instead.
92
+ */
93
+ _bucketsToHistogramBins(pBuckets, pStage)
94
+ {
95
+ if (!pBuckets || pBuckets.length === 0) return [];
96
+ return pBuckets.map((pBucket) => ({ Label: '', Value: pBucket[pStage] || 0 }));
97
+ }
98
+
99
+ /**
100
+ * Render a per-histogram time axis: start | mid | end tick labels.
101
+ * The div is sized to match the histogram pixel width so labels align.
102
+ */
103
+ _renderTimeAxis(pAxisId, pBuckets)
104
+ {
105
+ let tmpAxisEl = document.getElementById(pAxisId);
106
+ if (!tmpAxisEl) return;
107
+
108
+ if (!pBuckets || pBuckets.length < 2)
109
+ {
110
+ tmpAxisEl.innerHTML = '';
111
+ return;
112
+ }
113
+
114
+ let tmpWidth = pBuckets.length * BAR_STRIDE;
115
+ tmpAxisEl.style.width = tmpWidth + 'px';
116
+
117
+ let tmpFirst = pBuckets[0].time;
118
+ let tmpMid = pBuckets[Math.floor((pBuckets.length - 1) / 2)].time;
119
+ let tmpLast = pBuckets[pBuckets.length - 1].time;
120
+
121
+ tmpAxisEl.innerHTML =
122
+ '<span>' + _hhmmss(new Date(tmpFirst)) + '</span>'
123
+ + '<span style="text-align:center;">' + _hhmmss(new Date(tmpMid)) + '</span>'
124
+ + '<span style="text-align:right;">' + _hhmmss(new Date(tmpLast)) + '</span>';
125
+ }
126
+
127
+ // ─────────────────────────────────────────────
128
+ // Live monitoring
129
+ // ─────────────────────────────────────────────
130
+
131
+ startMonitoring()
132
+ {
133
+ if (this._isPolling) return;
134
+ this._isPolling = true;
135
+ let tmpBtn = document.getElementById('facto-tp-toggle');
136
+ if (tmpBtn) { tmpBtn.textContent = 'Stop Monitoring'; tmpBtn.className = 'facto-btn facto-btn-danger'; }
137
+ this._poll();
138
+ }
139
+
140
+ stopMonitoring()
141
+ {
142
+ this._isPolling = false;
143
+ if (this._pollTimer) { clearTimeout(this._pollTimer); this._pollTimer = null; }
144
+ let tmpBtn = document.getElementById('facto-tp-toggle');
145
+ if (tmpBtn) { tmpBtn.textContent = 'Start Live Monitoring'; tmpBtn.className = 'facto-btn facto-btn-primary'; }
146
+ }
147
+
148
+ toggleMonitoring()
149
+ {
150
+ this._isPolling ? this.stopMonitoring() : this.startMonitoring();
151
+ }
152
+
153
+ _poll()
154
+ {
155
+ if (!this._isPolling) return;
156
+ fetch('/facto/throughput?duration=30')
157
+ .then((r) => r.json())
158
+ .then((d) => { this._renderHistograms(d); this._pollTimer = setTimeout(() => this._poll(), POLL_INTERVAL_MS); })
159
+ .catch(() => { this._pollTimer = setTimeout(() => this._poll(), POLL_INTERVAL_MS * 4); });
160
+ }
161
+
162
+ loadRunHistory()
163
+ {
164
+ fetch('/facto/throughput/runs?limit=10')
165
+ .then((r) => r.json())
166
+ .then((runs) => { this._renderRunHistory(runs); })
167
+ .catch(() => {});
168
+ }
169
+
170
+ loadHistoricalRun(pLabel)
171
+ {
172
+ this._activeHistoricalRun = pLabel;
173
+ this._activeDatasetFilter = null;
174
+ fetch('/facto/throughput/run/' + encodeURIComponent(pLabel))
175
+ .then((r) => r.json())
176
+ .then((d) => { d.historicalRun = pLabel; this._renderHistograms(d); this._loadDatasetBreakdown(pLabel); })
177
+ .catch(() => {});
178
+ }
179
+
180
+ filterByDataset(pDataset)
181
+ {
182
+ if (!this._activeHistoricalRun) return;
183
+ this._activeDatasetFilter = pDataset;
184
+ let tmpUrl = '/facto/throughput/run/' + encodeURIComponent(this._activeHistoricalRun);
185
+ if (pDataset) tmpUrl += '?dataset=' + encodeURIComponent(pDataset);
186
+ fetch(tmpUrl)
187
+ .then((r) => r.json())
188
+ .then((d) => { d.historicalRun = this._activeHistoricalRun; d.datasetFilter = pDataset; this._renderHistograms(d); })
189
+ .catch(() => {});
190
+ }
191
+
192
+ _loadDatasetBreakdown(pLabel)
193
+ {
194
+ fetch('/facto/throughput/run/' + encodeURIComponent(pLabel) + '/datasets')
195
+ .then((r) => r.json())
196
+ .then((ds) => { this._renderDatasetBreakdown(ds); })
197
+ .catch(() => {});
198
+ }
199
+
200
+ // ─────────────────────────────────────────────
201
+ // Rendering
202
+ // ─────────────────────────────────────────────
203
+
204
+ _renderHistograms(pData)
205
+ {
206
+ let tmpBuckets = pData.buckets || [];
207
+ let tmpInterval = pData.interval || 1000;
208
+
209
+ // Compute totals for header badges
210
+ let tmpTotals = { extracted: 0, transformed: 0, written: 0 };
211
+ for (let i = 0; i < tmpBuckets.length; i++)
212
+ {
213
+ tmpTotals.extracted += tmpBuckets[i].extracted || 0;
214
+ tmpTotals.transformed += tmpBuckets[i].transformed || 0;
215
+ tmpTotals.written += tmpBuckets[i].written || 0;
216
+ }
217
+
218
+ // Header badges + run/active info
219
+ let tmpHeaderEl = document.getElementById('facto-tp-header');
220
+ if (tmpHeaderEl)
221
+ {
222
+ let tmpHtml = '<div style="display:flex; gap:20px; flex-wrap:wrap; align-items:center;">';
223
+ tmpHtml += this._badge('Extracted', tmpTotals.extracted, '#4a90d9');
224
+ tmpHtml += this._badge('Transformed', tmpTotals.transformed, '#d09818');
225
+ tmpHtml += this._badge('Written', tmpTotals.written, '#3a9468');
226
+
227
+ if (pData.activeRun)
228
+ {
229
+ let tmpElapsed = ((Date.now() - pData.activeRun.startTime) / 1000).toFixed(1);
230
+ tmpHtml += '<span style="font-size:0.85em; color:var(--facto-text-secondary); display:flex; align-items:center; gap:6px;">'
231
+ + '<span style="width:8px; height:8px; border-radius:50%; background:var(--facto-success); animation:pulse 1s infinite;"></span>'
232
+ + pData.activeRun.label + ' (' + tmpElapsed + 's)</span>';
233
+ }
234
+ if (pData.historicalRun)
235
+ {
236
+ tmpHtml += '<span style="font-size:0.85em; color:var(--facto-text-secondary);">Viewing: ' + pData.historicalRun
237
+ + (pData.datasetFilter ? ' → ' + pData.datasetFilter : '') + '</span>';
238
+ }
239
+ tmpHtml += '</div>';
240
+ tmpHeaderEl.innerHTML = tmpHtml;
241
+ }
242
+
243
+ // Feed each histogram view + update its inline time axis
244
+ this._ensureHistogramViews();
245
+ for (let i = 0; i < HISTOGRAM_DEFS.length; i++)
246
+ {
247
+ let tmpDef = HISTOGRAM_DEFS[i];
248
+ let tmpView = this.pict.views[tmpDef.id];
249
+ if (!tmpView) continue;
250
+
251
+ let tmpBins = this._bucketsToHistogramBins(tmpBuckets, tmpDef.stage);
252
+ tmpView.setBins(tmpBins);
253
+ tmpView.renderHistogram();
254
+
255
+ this._renderTimeAxis('Facto-Full-Histogram-' + _capitalise(tmpDef.stage) + '-Axis', tmpBuckets);
256
+ }
257
+ }
258
+
259
+ _badge(pLabel, pCount, pColor)
260
+ {
261
+ return '<div style="display:flex; align-items:center; gap:6px;">'
262
+ + '<div style="width:10px; height:10px; border-radius:2px; background:' + pColor + ';"></div>'
263
+ + '<span style="font-weight:600; font-size:0.88em;">' + pLabel + ':</span>'
264
+ + '<span style="font-size:0.88em; color:var(--facto-text-secondary);">' + pCount.toLocaleString() + '</span>'
265
+ + '</div>';
266
+ }
267
+
268
+ _renderRunHistory(pRuns)
269
+ {
270
+ let tmpEl = document.getElementById('facto-tp-history');
271
+ if (!tmpEl) return;
272
+ if (!pRuns || pRuns.length === 0)
273
+ {
274
+ tmpEl.innerHTML = '<p style="color:var(--facto-text-tertiary); font-size:0.85em; font-style:italic;">No historical runs yet. Run a pipeline ingest first.</p>';
275
+ return;
276
+ }
277
+
278
+ let tmpHtml = '<div style="font-size:0.85em; font-weight:600; color:var(--facto-text-heading); margin-bottom:8px;">Run History</div>'
279
+ + '<div style="display:flex; flex-wrap:wrap; gap:8px;">';
280
+ for (let i = 0; i < pRuns.length; i++)
281
+ {
282
+ let tmpRun = pRuns[i];
283
+ let tmpDate = new Date(tmpRun.startTime).toLocaleString();
284
+ tmpHtml += '<button class="facto-btn facto-btn-secondary" style="font-size:0.8em;" '
285
+ + 'onclick="pict.views[\'Facto-Full-Throughput\'].loadHistoricalRun(\'' + tmpRun.label.replace(/'/g, "\\'") + '\')" '
286
+ + 'title="' + tmpDate + ' — ' + tmpRun.eventCount + ' events">'
287
+ + tmpRun.label.substring(0, 35) + (tmpRun.label.length > 35 ? '...' : '') + '</button>';
288
+ }
289
+ tmpHtml += '</div>';
290
+ tmpEl.innerHTML = tmpHtml;
291
+ }
292
+
293
+ _renderDatasetBreakdown(pDatasets)
294
+ {
295
+ let tmpEl = document.getElementById('facto-tp-datasets');
296
+ if (!tmpEl) return;
297
+ if (!pDatasets || pDatasets.length === 0) { tmpEl.innerHTML = ''; return; }
298
+
299
+ let tmpMaxTotal = 1;
300
+ for (let i = 0; i < pDatasets.length; i++)
301
+ {
302
+ if (pDatasets[i].total > tmpMaxTotal) tmpMaxTotal = pDatasets[i].total;
303
+ }
304
+
305
+ let tmpHtml = '<div style="font-size:0.85em; font-weight:600; color:var(--facto-text-heading); margin-bottom:8px; margin-top:16px;">Per-Dataset Breakdown</div>';
306
+ tmpHtml += '<div style="font-size:0.8em; margin-bottom:6px; color:var(--facto-text-tertiary);">Click a dataset to filter the histogram:</div>';
307
+ for (let i = 0; i < pDatasets.length; i++)
308
+ {
309
+ let tmpDS = pDatasets[i];
310
+ let tmpActive = this._activeDatasetFilter === tmpDS.dataset;
311
+ let tmpDatasetEsc = tmpDS.dataset.replace(/'/g, "\\'");
312
+ tmpHtml += '<div style="display:flex; align-items:center; gap:8px; margin-bottom:4px; cursor:pointer; padding:3px 8px; border-radius:4px; '
313
+ + (tmpActive ? 'background:var(--facto-brand-a15);' : '') + '" '
314
+ + 'onclick="pict.views[\'Facto-Full-Throughput\'].filterByDataset(' + (tmpActive ? 'null' : '\'' + tmpDatasetEsc + '\'') + ')">';
315
+ tmpHtml += '<span style="min-width:220px; font-family:monospace; font-size:0.9em; color:var(--facto-text);">' + tmpDS.dataset + '</span>';
316
+ tmpHtml += '<div style="flex:1; height:16px; display:flex; border-radius:3px; overflow:hidden; background:var(--facto-bg-elevated);">';
317
+ if (tmpDS.extracted > 0) tmpHtml += '<div style="width:' + ((tmpDS.extracted / tmpMaxTotal) * 100) + '%; background:#4a90d9;" title="Extracted: ' + tmpDS.extracted + '"></div>';
318
+ if (tmpDS.transformed > 0) tmpHtml += '<div style="width:' + ((tmpDS.transformed / tmpMaxTotal) * 100) + '%; background:#d09818;" title="Transformed: ' + tmpDS.transformed + '"></div>';
319
+ if (tmpDS.written > 0) tmpHtml += '<div style="width:' + ((tmpDS.written / tmpMaxTotal) * 100) + '%; background:#3a9468;" title="Written: ' + tmpDS.written + '"></div>';
320
+ tmpHtml += '</div>';
321
+ tmpHtml += '<span style="min-width:60px; text-align:right; font-size:0.85em; color:var(--facto-text-secondary);">' + tmpDS.total.toLocaleString() + '</span></div>';
322
+ }
323
+ tmpEl.innerHTML = tmpHtml;
324
+ }
325
+ }
326
+
327
+ // ─────────────────────────────────────────────
328
+ // Helpers
329
+ // ─────────────────────────────────────────────
330
+
331
+ function _hhmmss(pDate)
332
+ {
333
+ return String(pDate.getHours()).padStart(2, '0')
334
+ + ':' + String(pDate.getMinutes()).padStart(2, '0')
335
+ + ':' + String(pDate.getSeconds()).padStart(2, '0');
336
+ }
337
+
338
+ function _capitalise(pStr)
339
+ {
340
+ if (!pStr) return '';
341
+ return pStr.charAt(0).toUpperCase() + pStr.slice(1);
342
+ }
343
+
344
+ module.exports = FactoFullThroughputView;
345
+
346
+ module.exports.default_configuration =
347
+ {
348
+ ViewIdentifier: 'Facto-Full-Throughput',
349
+ DefaultRenderable: 'Facto-Full-Throughput-Content',
350
+ DefaultDestinationAddress: '#Facto-Full-Content-Container',
351
+ AutoRender: false,
352
+ Templates:
353
+ [
354
+ {
355
+ Hash: 'Facto-Full-Throughput-Template',
356
+ Template: /*html*/`
357
+ <div style="max-width:1200px; margin:0 auto;">
358
+ <h2 style="margin-bottom:4px;">Pipeline Throughput</h2>
359
+ <p style="color:var(--facto-text-secondary); margin-bottom:12px; font-size:0.9em;">
360
+ Temporal histograms showing record flow through extraction, transformation, and storage stages.
361
+ </p>
362
+
363
+ <div style="display:flex; gap:8px; margin-bottom:12px; flex-wrap:wrap;">
364
+ <button id="facto-tp-toggle" class="facto-btn facto-btn-primary" onclick="pict.views['Facto-Full-Throughput'].toggleMonitoring()">Start Live Monitoring</button>
365
+ <button class="facto-btn facto-btn-secondary" onclick="pict.views['Facto-Full-Throughput'].loadRunHistory()">Refresh History</button>
366
+ </div>
367
+
368
+ <div id="facto-tp-header" style="margin-bottom:12px;"></div>
369
+
370
+ <div id="facto-tp-history" style="margin-bottom:16px;"></div>
371
+
372
+ <div id="facto-tp-charts">
373
+ <div style="display:grid; grid-template-columns:1fr 1fr; gap:16px;">
374
+
375
+ <div style="background:rgba(74,144,217,0.1); border-radius:8px; padding:12px;">
376
+ <div style="font-size:0.85em; font-weight:600; color:#4a90d9; margin-bottom:6px;">Extracted</div>
377
+ <div style="overflow-x:auto;">
378
+ <div id="Facto-Full-Histogram-Extracted-Container"></div>
379
+ <div id="Facto-Full-Histogram-Extracted-Axis"
380
+ style="display:flex; justify-content:space-between; font-size:0.72em; font-family:monospace;
381
+ color:var(--facto-text-tertiary); margin-top:3px; padding-top:3px;
382
+ border-top:1px solid rgba(74,144,217,0.3);"></div>
383
+ </div>
384
+ </div>
385
+
386
+ <div style="background:rgba(208,152,24,0.1); border-radius:8px; padding:12px;">
387
+ <div style="font-size:0.85em; font-weight:600; color:#d09818; margin-bottom:6px;">Transformed</div>
388
+ <div style="overflow-x:auto;">
389
+ <div id="Facto-Full-Histogram-Transformed-Container"></div>
390
+ <div id="Facto-Full-Histogram-Transformed-Axis"
391
+ style="display:flex; justify-content:space-between; font-size:0.72em; font-family:monospace;
392
+ color:var(--facto-text-tertiary); margin-top:3px; padding-top:3px;
393
+ border-top:1px solid rgba(208,152,24,0.3);"></div>
394
+ </div>
395
+ </div>
396
+
397
+ <div style="background:rgba(58,148,104,0.1); border-radius:8px; padding:12px;">
398
+ <div style="font-size:0.85em; font-weight:600; color:#3a9468; margin-bottom:6px;">Written</div>
399
+ <div style="overflow-x:auto;">
400
+ <div id="Facto-Full-Histogram-Written-Container"></div>
401
+ <div id="Facto-Full-Histogram-Written-Axis"
402
+ style="display:flex; justify-content:space-between; font-size:0.72em; font-family:monospace;
403
+ color:var(--facto-text-tertiary); margin-top:3px; padding-top:3px;
404
+ border-top:1px solid rgba(58,148,104,0.3);"></div>
405
+ </div>
406
+ </div>
407
+
408
+ <div style="background:var(--facto-bg-elevated); border:1px solid var(--facto-border-subtle); border-radius:8px; padding:12px;">
409
+ <div style="font-size:0.85em; font-weight:600; color:#6366f1; margin-bottom:6px;">Total</div>
410
+ <div style="overflow-x:auto;">
411
+ <div id="Facto-Full-Histogram-Total-Container"></div>
412
+ <div id="Facto-Full-Histogram-Total-Axis"
413
+ style="display:flex; justify-content:space-between; font-size:0.72em; font-family:monospace;
414
+ color:var(--facto-text-tertiary); margin-top:3px; padding-top:3px;
415
+ border-top:1px solid rgba(99,102,241,0.3);"></div>
416
+ </div>
417
+ </div>
418
+
419
+ </div>
420
+ </div>
421
+
422
+ <div id="facto-tp-datasets"></div>
423
+
424
+ <style>
425
+ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
426
+ </style>
427
+ </div>
428
+ `
429
+ }
430
+ ],
431
+ Renderables:
432
+ [
433
+ {
434
+ RenderableHash: 'Facto-Full-Throughput-Content',
435
+ TemplateHash: 'Facto-Full-Throughput-Template',
436
+ DestinationAddress: '#Facto-Full-Content-Container'
437
+ }
438
+ ]
439
+ };