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,478 @@
1
+ /**
2
+ * Retold-Facto-ThroughputMonitor
3
+ *
4
+ * In-memory ring buffer that collects timestamped throughput events
5
+ * from pipeline stages (extracted, transformed, written).
6
+ *
7
+ * Events are bucketed into configurable time intervals (default 1s)
8
+ * and exposed via a REST endpoint for the temporal histogram UI.
9
+ *
10
+ * Usage:
11
+ * fable.ThroughputMonitor.recordEvent('extracted', 50);
12
+ * fable.ThroughputMonitor.recordEvent('transformed', 48);
13
+ * fable.ThroughputMonitor.recordEvent('written', 45);
14
+ *
15
+ * let buckets = fable.ThroughputMonitor.getBuckets(60); // last 60 seconds
16
+ */
17
+
18
+ 'use strict';
19
+
20
+ const libFableServiceProviderBase = require('fable-serviceproviderbase');
21
+
22
+ const STAGES = ['extracted', 'transformed', 'written'];
23
+
24
+ class RetoldFactoThroughputMonitor extends libFableServiceProviderBase
25
+ {
26
+ constructor(pFable, pOptions, pServiceHash)
27
+ {
28
+ super(pFable, pOptions, pServiceHash);
29
+
30
+ this.serviceType = 'ThroughputMonitor';
31
+
32
+ // Configurable bucket interval in milliseconds (default 1 second)
33
+ this._BucketIntervalMs = (pOptions && pOptions.BucketIntervalMs) || 1000;
34
+
35
+ // Ring buffer of events: { timestamp, stage, count }
36
+ this._Events = [];
37
+
38
+ // Maximum events to keep (prevents unbounded memory growth)
39
+ this._MaxEvents = (pOptions && pOptions.MaxEvents) || 100000;
40
+
41
+ // Pipeline run tracking
42
+ this._ActiveRun = null;
43
+ this._RunHistory = [];
44
+ }
45
+
46
+ // ─────────────────────────────────────────────
47
+ // Event recording
48
+ // ─────────────────────────────────────────────
49
+
50
+ /**
51
+ * Record a throughput event.
52
+ *
53
+ * @param {string} pStage — 'extracted', 'transformed', or 'written'
54
+ * @param {number} pCount — number of records in this event
55
+ * @param {string} [pDatasetName] — optional dataset label
56
+ */
57
+ recordEvent(pStage, pCount, pDatasetName)
58
+ {
59
+ if (STAGES.indexOf(pStage) < 0)
60
+ {
61
+ this.log.warn(`ThroughputMonitor: unknown stage "${pStage}"`);
62
+ return;
63
+ }
64
+
65
+ this._Events.push(
66
+ {
67
+ timestamp: Date.now(),
68
+ stage: pStage,
69
+ count: pCount || 0,
70
+ dataset: pDatasetName || '',
71
+ });
72
+
73
+ // Trim ring buffer
74
+ if (this._Events.length > this._MaxEvents)
75
+ {
76
+ this._Events = this._Events.slice(this._Events.length - this._MaxEvents);
77
+ }
78
+ }
79
+
80
+ // ─────────────────────────────────────────────
81
+ // Run lifecycle
82
+ // ─────────────────────────────────────────────
83
+
84
+ /**
85
+ * Mark the start of a pipeline run. Clears previous events.
86
+ */
87
+ startRun(pRunLabel)
88
+ {
89
+ if (this._ActiveRun)
90
+ {
91
+ this.endRun();
92
+ }
93
+
94
+ this._Events = [];
95
+ this._ActiveRun =
96
+ {
97
+ label: pRunLabel || 'run-' + Date.now(),
98
+ startTime: Date.now(),
99
+ endTime: null,
100
+ };
101
+ }
102
+
103
+ /**
104
+ * Mark the end of a pipeline run and flush events to the database.
105
+ */
106
+ endRun()
107
+ {
108
+ if (this._ActiveRun)
109
+ {
110
+ this._ActiveRun.endTime = Date.now();
111
+ this._RunHistory.push(this._ActiveRun);
112
+ if (this._RunHistory.length > 10)
113
+ {
114
+ this._RunHistory.shift();
115
+ }
116
+
117
+ // Persist events to the ThroughputEvent table
118
+ this._flushToDatabase(this._ActiveRun);
119
+ }
120
+ this._ActiveRun = null;
121
+ }
122
+
123
+ /**
124
+ * Batch-write all buffered events to the ThroughputEvent table.
125
+ * Uses the MeadowSQLiteProvider's better-sqlite3 database handle.
126
+ */
127
+ _flushToDatabase(pRun)
128
+ {
129
+ if (!this._Events || this._Events.length === 0)
130
+ {
131
+ return;
132
+ }
133
+
134
+ let tmpDB = this._getDatabase();
135
+ if (!tmpDB)
136
+ {
137
+ this.log.warn('ThroughputMonitor: no database available for persistence');
138
+ return;
139
+ }
140
+
141
+ let tmpLabel = pRun.label || '';
142
+ let tmpStartTime = pRun.startTime || 0;
143
+
144
+ try
145
+ {
146
+ let tmpStmt = tmpDB.prepare(
147
+ 'INSERT INTO ThroughputEvent (RunLabel, RunStartTime, Timestamp, Stage, Count, Dataset) VALUES (?, ?, ?, ?, ?, ?)');
148
+
149
+ let tmpInsertMany = tmpDB.transaction(
150
+ (pEvents) =>
151
+ {
152
+ for (let i = 0; i < pEvents.length; i++)
153
+ {
154
+ let tmpEvent = pEvents[i];
155
+ tmpStmt.run(tmpLabel, tmpStartTime, tmpEvent.timestamp, tmpEvent.stage, tmpEvent.count, tmpEvent.dataset || '');
156
+ }
157
+ });
158
+
159
+ tmpInsertMany(this._Events);
160
+ this.log.info(`ThroughputMonitor: flushed ${this._Events.length} events for run "${tmpLabel}"`);
161
+ }
162
+ catch (pError)
163
+ {
164
+ this.log.warn(`ThroughputMonitor: flush error: ${pError.message}`);
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Get the better-sqlite3 database handle from the MeadowSQLiteProvider.
170
+ */
171
+ _getDatabase()
172
+ {
173
+ if (!this.fable.servicesMap || !this.fable.servicesMap.MeadowSQLiteProvider)
174
+ {
175
+ return null;
176
+ }
177
+ let tmpProvider = Object.values(this.fable.servicesMap.MeadowSQLiteProvider)[0];
178
+ return tmpProvider && tmpProvider._database ? tmpProvider._database : null;
179
+ }
180
+
181
+ /**
182
+ * Query persisted runs from the database.
183
+ *
184
+ * @param {number} [pLimit=20] — max runs to return
185
+ * @returns {Array} — [{ label, startTime, eventCount, datasets }]
186
+ */
187
+ getPersistedRuns(pLimit)
188
+ {
189
+ let tmpDB = this._getDatabase();
190
+ if (!tmpDB)
191
+ {
192
+ return [];
193
+ }
194
+
195
+ let tmpLimit = pLimit || 20;
196
+ try
197
+ {
198
+ let tmpRows = tmpDB.prepare(
199
+ `SELECT RunLabel, RunStartTime, COUNT(*) as EventCount, GROUP_CONCAT(DISTINCT Dataset) as Datasets
200
+ FROM ThroughputEvent
201
+ GROUP BY RunLabel, RunStartTime
202
+ ORDER BY RunStartTime DESC
203
+ LIMIT ?`).all(tmpLimit);
204
+
205
+ return tmpRows.map(
206
+ (pRow) =>
207
+ ({
208
+ label: pRow.RunLabel,
209
+ startTime: pRow.RunStartTime,
210
+ eventCount: pRow.EventCount,
211
+ datasets: (pRow.Datasets || '').split(',').filter((pD) => pD.length > 0),
212
+ }));
213
+ }
214
+ catch (pError)
215
+ {
216
+ this.log.warn(`ThroughputMonitor: query error: ${pError.message}`);
217
+ return [];
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Load events for a specific historical run, optionally filtered by dataset.
223
+ *
224
+ * @param {string} pRunLabel — run label to load
225
+ * @param {string} [pDataset] — optional dataset filter
226
+ * @returns {object} — same format as getBuckets()
227
+ */
228
+ getPersistedRunBuckets(pRunLabel, pDataset)
229
+ {
230
+ let tmpDB = this._getDatabase();
231
+ if (!tmpDB)
232
+ {
233
+ return { buckets: [], stages: STAGES, interval: this._BucketIntervalMs, maxValue: 0, datasets: [] };
234
+ }
235
+
236
+ try
237
+ {
238
+ let tmpParams = [pRunLabel || ''];
239
+ let tmpSql = 'SELECT Timestamp, Stage, Count, Dataset FROM ThroughputEvent WHERE RunLabel = ?';
240
+ if (pDataset)
241
+ {
242
+ tmpSql += ' AND Dataset = ?';
243
+ tmpParams.push(pDataset);
244
+ }
245
+ tmpSql += ' ORDER BY Timestamp';
246
+
247
+ let tmpRows = tmpDB.prepare(tmpSql).all(...tmpParams);
248
+
249
+ if (!tmpRows || tmpRows.length === 0)
250
+ {
251
+ return { buckets: [], stages: STAGES, interval: this._BucketIntervalMs, maxValue: 0, datasets: [] };
252
+ }
253
+
254
+ // Reconstruct events
255
+ let tmpEvents = tmpRows.map(
256
+ (pRow) => ({ timestamp: pRow.Timestamp, stage: pRow.Stage, count: pRow.Count, dataset: pRow.Dataset }));
257
+
258
+ // Find time range
259
+ let tmpMinTime = tmpEvents[0].timestamp;
260
+ let tmpMaxTime = tmpEvents[tmpEvents.length - 1].timestamp;
261
+ let tmpDuration = tmpMaxTime - tmpMinTime + this._BucketIntervalMs;
262
+ let tmpInterval = this._BucketIntervalMs;
263
+ let tmpBucketCount = Math.ceil(tmpDuration / tmpInterval);
264
+
265
+ let tmpBuckets = [];
266
+ for (let i = 0; i < tmpBucketCount; i++)
267
+ {
268
+ tmpBuckets.push(
269
+ {
270
+ time: tmpMinTime + (i * tmpInterval),
271
+ extracted: 0,
272
+ transformed: 0,
273
+ written: 0,
274
+ total: 0,
275
+ });
276
+ }
277
+
278
+ // Collect unique datasets
279
+ let tmpDatasetSet = {};
280
+
281
+ for (let i = 0; i < tmpEvents.length; i++)
282
+ {
283
+ let tmpEvent = tmpEvents[i];
284
+ let tmpBucketIndex = Math.floor((tmpEvent.timestamp - tmpMinTime) / tmpInterval);
285
+ if (tmpBucketIndex >= 0 && tmpBucketIndex < tmpBuckets.length)
286
+ {
287
+ tmpBuckets[tmpBucketIndex][tmpEvent.stage] += tmpEvent.count;
288
+ tmpBuckets[tmpBucketIndex].total += tmpEvent.count;
289
+ }
290
+ if (tmpEvent.dataset)
291
+ {
292
+ tmpDatasetSet[tmpEvent.dataset] = true;
293
+ }
294
+ }
295
+
296
+ let tmpMaxValue = 0;
297
+ for (let i = 0; i < tmpBuckets.length; i++)
298
+ {
299
+ tmpMaxValue = Math.max(tmpMaxValue, tmpBuckets[i].extracted, tmpBuckets[i].transformed, tmpBuckets[i].written);
300
+ }
301
+
302
+ return {
303
+ buckets: tmpBuckets,
304
+ stages: STAGES,
305
+ interval: tmpInterval,
306
+ maxValue: tmpMaxValue,
307
+ datasets: Object.keys(tmpDatasetSet),
308
+ };
309
+ }
310
+ catch (pError)
311
+ {
312
+ this.log.warn(`ThroughputMonitor: query error: ${pError.message}`);
313
+ return { buckets: [], stages: STAGES, interval: this._BucketIntervalMs, maxValue: 0, datasets: [] };
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Get per-dataset breakdown for a run.
319
+ *
320
+ * @param {string} pRunLabel — run label
321
+ * @returns {Array} — [{ dataset, extracted, transformed, written, total }]
322
+ */
323
+ getPersistedRunDatasetBreakdown(pRunLabel)
324
+ {
325
+ let tmpDB = this._getDatabase();
326
+ if (!tmpDB)
327
+ {
328
+ return [];
329
+ }
330
+
331
+ try
332
+ {
333
+ let tmpRows = tmpDB.prepare(
334
+ `SELECT Dataset, Stage, SUM(Count) as Total
335
+ FROM ThroughputEvent
336
+ WHERE RunLabel = ? AND Dataset != ''
337
+ GROUP BY Dataset, Stage
338
+ ORDER BY Dataset, Stage`).all(pRunLabel || '');
339
+
340
+ if (!tmpRows || tmpRows.length === 0) return [];
341
+
342
+ // Pivot: rows are (Dataset, Stage, Total) → objects are { dataset, extracted, transformed, written }
343
+ let tmpMap = {};
344
+ for (let i = 0; i < tmpRows.length; i++)
345
+ {
346
+ let tmpRow = tmpRows[i];
347
+ let tmpDataset = tmpRow.Dataset;
348
+ let tmpStage = tmpRow.Stage;
349
+ let tmpTotal = tmpRow.Total;
350
+
351
+ if (!tmpMap[tmpDataset])
352
+ {
353
+ tmpMap[tmpDataset] = { dataset: tmpDataset, extracted: 0, transformed: 0, written: 0, total: 0 };
354
+ }
355
+ tmpMap[tmpDataset][tmpStage] = tmpTotal;
356
+ tmpMap[tmpDataset].total += tmpTotal;
357
+ }
358
+
359
+ return Object.values(tmpMap);
360
+ }
361
+ catch (pError)
362
+ {
363
+ this.log.warn(`ThroughputMonitor: breakdown error: ${pError.message}`);
364
+ return [];
365
+ }
366
+ }
367
+
368
+ // ─────────────────────────────────────────────
369
+ // Bucketed query
370
+ // ─────────────────────────────────────────────
371
+
372
+ /**
373
+ * Get time-bucketed throughput data.
374
+ *
375
+ * @param {number} [pDurationSeconds=60] — how far back to look
376
+ * @returns {object} — { buckets: [...], stages, interval, activeRun }
377
+ */
378
+ getBuckets(pDurationSeconds)
379
+ {
380
+ let tmpDuration = (pDurationSeconds || 60) * 1000;
381
+ let tmpNow = Date.now();
382
+ let tmpInterval = this._BucketIntervalMs;
383
+
384
+ // Determine the right-edge anchor for the window:
385
+ // Active run → anchor to "now" so live events always appear at the right edge.
386
+ // No active run → anchor to the last recorded event so bars stay right-aligned
387
+ // and don't float into dead space on the right as time passes.
388
+ let tmpAnchor = tmpNow;
389
+ if (!this._ActiveRun && this._Events.length > 0)
390
+ {
391
+ tmpAnchor = this._Events[this._Events.length - 1].timestamp;
392
+ }
393
+
394
+ // Align to a clock-boundary so bucket indices are stable across consecutive
395
+ // polls — the same event always lands in the same bucket, no rebucketing jitter.
396
+ let tmpAlignedAnchor = Math.floor(tmpAnchor / tmpInterval) * tmpInterval;
397
+ let tmpBucketCount = Math.ceil(tmpDuration / tmpInterval);
398
+ let tmpStart = tmpAlignedAnchor - (tmpBucketCount - 1) * tmpInterval;
399
+ let tmpBuckets = [];
400
+
401
+ for (let i = 0; i < tmpBucketCount; i++)
402
+ {
403
+ let tmpBucketStart = tmpStart + (i * tmpInterval);
404
+ tmpBuckets.push(
405
+ {
406
+ time: tmpBucketStart,
407
+ extracted: 0,
408
+ transformed: 0,
409
+ written: 0,
410
+ });
411
+ }
412
+
413
+ // Fill buckets from events
414
+ for (let i = 0; i < this._Events.length; i++)
415
+ {
416
+ let tmpEvent = this._Events[i];
417
+ if (tmpEvent.timestamp < tmpStart)
418
+ {
419
+ continue;
420
+ }
421
+
422
+ let tmpBucketIndex = Math.min(Math.floor((tmpEvent.timestamp - tmpStart) / tmpInterval), tmpBuckets.length - 1);
423
+ if (tmpBucketIndex >= 0 && tmpBucketIndex < tmpBuckets.length)
424
+ {
425
+ tmpBuckets[tmpBucketIndex][tmpEvent.stage] += tmpEvent.count;
426
+ }
427
+ }
428
+
429
+ // Find max value for scaling
430
+ let tmpMaxValue = 0;
431
+ for (let i = 0; i < tmpBuckets.length; i++)
432
+ {
433
+ let tmpBucket = tmpBuckets[i];
434
+ tmpMaxValue = Math.max(tmpMaxValue, tmpBucket.extracted, tmpBucket.transformed, tmpBucket.written);
435
+ // Also track stacked total
436
+ tmpBucket.total = tmpBucket.extracted + tmpBucket.transformed + tmpBucket.written;
437
+ }
438
+
439
+ return {
440
+ buckets: tmpBuckets,
441
+ stages: STAGES,
442
+ interval: tmpInterval,
443
+ maxValue: tmpMaxValue,
444
+ activeRun: this._ActiveRun,
445
+ totalEvents: this._Events.length,
446
+ };
447
+ }
448
+
449
+ /**
450
+ * Get cumulative totals per stage.
451
+ */
452
+ getTotals()
453
+ {
454
+ let tmpTotals = { extracted: 0, transformed: 0, written: 0 };
455
+
456
+ for (let i = 0; i < this._Events.length; i++)
457
+ {
458
+ let tmpEvent = this._Events[i];
459
+ if (tmpTotals.hasOwnProperty(tmpEvent.stage))
460
+ {
461
+ tmpTotals[tmpEvent.stage] += tmpEvent.count;
462
+ }
463
+ }
464
+
465
+ return tmpTotals;
466
+ }
467
+
468
+ /**
469
+ * Clear all events.
470
+ */
471
+ reset()
472
+ {
473
+ this._Events = [];
474
+ this._ActiveRun = null;
475
+ }
476
+ }
477
+
478
+ module.exports = RetoldFactoThroughputMonitor;
@@ -0,0 +1,7 @@
1
+ import { EditorView, basicSetup } from 'codemirror';
2
+ import { EditorState } from '@codemirror/state';
3
+ import { Decoration, ViewPlugin, WidgetType } from '@codemirror/view';
4
+ import { markdown } from '@codemirror/lang-markdown';
5
+
6
+ export { EditorView, EditorState, Decoration, ViewPlugin, WidgetType, basicSetup, markdown };
7
+ export const extensions = [basicSetup, markdown()];
@@ -0,0 +1,9 @@
1
+ {
2
+ "Name": "Retold Facto",
3
+ "Hash": "Facto",
4
+ "MainViewportViewIdentifier": "Facto-Layout",
5
+ "MainViewportDestinationAddress": "#Facto-Application-Container",
6
+ "MainViewportDefaultDataAddress": "AppData.Facto",
7
+ "pict_configuration": { "Product": "Facto" },
8
+ "AutoRenderMainViewportViewAfterInitialize": false
9
+ }
@@ -0,0 +1,70 @@
1
+ const libPictApplication = require('pict-application');
2
+
3
+ const libPictSectionModal = require('pict-section-modal');
4
+ const libProvider = require('./providers/Pict-Provider-Facto.js');
5
+
6
+ const libViewLayout = require('./views/PictView-Facto-Layout.js');
7
+ const libViewSources = require('./views/PictView-Facto-Sources.js');
8
+ const libViewRecords = require('./views/PictView-Facto-Records.js');
9
+ const libViewDatasets = require('./views/PictView-Facto-Datasets.js');
10
+ const libViewIngest = require('./views/PictView-Facto-Ingest.js');
11
+ const libViewProjections = require('./views/PictView-Facto-Projections.js');
12
+ const libViewCatalog = require('./views/PictView-Facto-Catalog.js');
13
+ const libViewScanner = require('./views/PictView-Facto-Scanner.js');
14
+ const libViewThroughput = require('./views/PictView-Facto-Throughput.js');
15
+
16
+ class FactoApplication extends libPictApplication
17
+ {
18
+ constructor(pFable, pOptions, pServiceHash)
19
+ {
20
+ super(pFable, pOptions, pServiceHash);
21
+
22
+ // Register modal/notification service
23
+ this.pict.addView('Pict-Section-Modal', libPictSectionModal.default_configuration, libPictSectionModal);
24
+
25
+ // Register provider
26
+ this.pict.addProvider('Facto', libProvider.default_configuration, libProvider);
27
+
28
+ // Register views
29
+ this.pict.addView('Facto-Layout', libViewLayout.default_configuration, libViewLayout);
30
+ this.pict.addView('Facto-Sources', libViewSources.default_configuration, libViewSources);
31
+ this.pict.addView('Facto-Records', libViewRecords.default_configuration, libViewRecords);
32
+ this.pict.addView('Facto-Datasets', libViewDatasets.default_configuration, libViewDatasets);
33
+ this.pict.addView('Facto-Ingest', libViewIngest.default_configuration, libViewIngest);
34
+ this.pict.addView('Facto-Projections', libViewProjections.default_configuration, libViewProjections);
35
+ this.pict.addView('Facto-Catalog', libViewCatalog.default_configuration, libViewCatalog);
36
+ this.pict.addView('Facto-Scanner', libViewScanner.default_configuration, libViewScanner);
37
+ this.pict.addView('Facto-Throughput', libViewThroughput.default_configuration, libViewThroughput);
38
+ }
39
+
40
+ onAfterInitializeAsync(fCallback)
41
+ {
42
+ // Centralized application state
43
+ this.pict.AppData.Facto =
44
+ {
45
+ CatalogEntries: [],
46
+ Sources: [],
47
+ Datasets: [],
48
+ Records: [],
49
+ IngestJobs: [],
50
+ SelectedSource: null,
51
+ SelectedDataset: null,
52
+ RecordPage: 0,
53
+ RecordPageSize: 50,
54
+ ScannerPaths: [],
55
+ ScannerDatasets: []
56
+ };
57
+
58
+ // Make pict available for inline onclick handlers
59
+ window.pict = this.pict;
60
+
61
+ // Render layout (which cascades child view renders)
62
+ this.pict.views['Facto-Layout'].render();
63
+
64
+ return fCallback();
65
+ }
66
+ }
67
+
68
+ module.exports = FactoApplication;
69
+
70
+ module.exports.default_configuration = require('./Pict-Application-Facto-Configuration.json');
@@ -0,0 +1,11 @@
1
+ module.exports =
2
+ {
3
+ FactoApplication: require('./Pict-Application-Facto.js'),
4
+ FactoFullApplication: require('../pict-app-full/Pict-Application-Facto-Full.js')
5
+ };
6
+
7
+ if (typeof(window) !== 'undefined')
8
+ {
9
+ window.FactoApplication = module.exports.FactoApplication;
10
+ window.FactoFullApplication = module.exports.FactoFullApplication;
11
+ }
@@ -0,0 +1,66 @@
1
+ const libPictProvider = require('pict-provider');
2
+
3
+ const _ProviderConfiguration =
4
+ {
5
+ ProviderIdentifier: 'FactoUI',
6
+ AutoInitialize: true,
7
+ AutoInitializeOrdinal: 0
8
+ };
9
+
10
+ class FactoUIProvider extends libPictProvider
11
+ {
12
+ constructor(pFable, pOptions, pServiceHash)
13
+ {
14
+ super(pFable, pOptions, pServiceHash);
15
+ }
16
+
17
+ /**
18
+ * Read a form field value by element ID using pict's ContentAssignment.
19
+ * Replaces the (document.getElementById(id) || {}).value || '' pattern.
20
+ * @param {string} pID - The element ID (without the leading #)
21
+ * @returns {string} The element's value, or empty string if not found
22
+ */
23
+ getVal(pID)
24
+ {
25
+ return this.pict.ContentAssignment.readContent('#' + pID) || '';
26
+ }
27
+
28
+ /**
29
+ * Reload and refresh sibling views after data-mutating operations.
30
+ * Handles both pict-app ('Facto-X') and pict-app-full ('Facto-Full-X') identifiers.
31
+ * @param {string[]} [pWhich=['sources','datasets']] - Which views to refresh
32
+ */
33
+ refreshDataViews(pWhich)
34
+ {
35
+ let tmpLoad = pWhich || ['sources', 'datasets'];
36
+
37
+ if (tmpLoad.indexOf('sources') > -1)
38
+ {
39
+ let tmpView = this.pict.views['Facto-Sources'] || this.pict.views['Facto-Full-Sources'];
40
+ if (tmpView)
41
+ {
42
+ this.pict.providers.Facto.loadSources().then(() => { tmpView.refreshList(); });
43
+ }
44
+ }
45
+ if (tmpLoad.indexOf('datasets') > -1)
46
+ {
47
+ let tmpView = this.pict.views['Facto-Datasets'] || this.pict.views['Facto-Full-Datasets'];
48
+ if (tmpView)
49
+ {
50
+ this.pict.providers.Facto.loadDatasets().then(() => { tmpView.refreshList(); });
51
+ }
52
+ }
53
+ if (tmpLoad.indexOf('records') > -1)
54
+ {
55
+ let tmpView = this.pict.views['Facto-Records'] || this.pict.views['Facto-Full-Records'];
56
+ if (tmpView)
57
+ {
58
+ this.pict.providers.Facto.loadRecords().then(() => { tmpView.refreshList(); });
59
+ }
60
+ }
61
+ }
62
+ }
63
+
64
+ module.exports = FactoUIProvider;
65
+
66
+ module.exports.default_configuration = _ProviderConfiguration;