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,663 @@
1
+ const libPictView = require('pict-view');
2
+ const libPictSectionObjectEditor = require('pict-section-objecteditor');
3
+
4
+ const _ViewConfiguration =
5
+ {
6
+ ViewIdentifier: "Facto-Full-RecordViewer",
7
+
8
+ DefaultRenderable: "Facto-Full-RecordViewer-Content",
9
+ DefaultDestinationAddress: "#Facto-Full-Content-Container",
10
+
11
+ AutoRender: false,
12
+
13
+ CSS: /*css*/`
14
+ .facto-record-viewer-back {
15
+ display: inline-flex;
16
+ align-items: center;
17
+ gap: 0.35em;
18
+ color: var(--facto-text-secondary);
19
+ cursor: pointer;
20
+ font-size: 0.85em;
21
+ margin-bottom: 0.75em;
22
+ transition: color 0.15s;
23
+ }
24
+ .facto-record-viewer-back:hover {
25
+ color: var(--facto-brand);
26
+ }
27
+ .facto-record-meta {
28
+ display: grid;
29
+ grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
30
+ gap: 1em;
31
+ margin-bottom: 1.5em;
32
+ }
33
+ .facto-record-meta-card {
34
+ background: var(--facto-bg-elevated, #1a1e2a);
35
+ border: 1px solid var(--facto-border-subtle, #2a2e3a);
36
+ border-radius: 8px;
37
+ padding: 1em;
38
+ }
39
+ .facto-record-meta-card h3 {
40
+ margin: 0 0 0.5em;
41
+ font-size: 0.75em;
42
+ text-transform: uppercase;
43
+ letter-spacing: 0.05em;
44
+ color: var(--facto-text-tertiary, #888);
45
+ }
46
+ .facto-record-meta-row {
47
+ display: flex;
48
+ justify-content: space-between;
49
+ align-items: baseline;
50
+ padding: 0.2em 0;
51
+ font-size: 0.85em;
52
+ }
53
+ .facto-record-meta-label {
54
+ color: var(--facto-text-secondary, #aaa);
55
+ flex-shrink: 0;
56
+ margin-right: 0.75em;
57
+ }
58
+ .facto-record-meta-value {
59
+ color: var(--facto-text-heading, #eee);
60
+ text-align: right;
61
+ overflow: hidden;
62
+ text-overflow: ellipsis;
63
+ white-space: nowrap;
64
+ font-family: 'SF Mono', Consolas, monospace;
65
+ font-size: 0.9em;
66
+ }
67
+ .facto-record-meta-value.facto-hash-value {
68
+ color: var(--facto-brand, #4a90d9);
69
+ }
70
+ .facto-record-certainty-bar {
71
+ height: 6px;
72
+ background: var(--facto-border-subtle, #2a2e3a);
73
+ border-radius: 3px;
74
+ margin-top: 0.35em;
75
+ overflow: hidden;
76
+ }
77
+ .facto-record-certainty-fill {
78
+ height: 100%;
79
+ border-radius: 3px;
80
+ transition: width 0.3s;
81
+ }
82
+ .facto-record-content-section {
83
+ margin-top: 1.5em;
84
+ }
85
+ .facto-record-content-section h2 {
86
+ font-size: 1em;
87
+ margin: 0 0 0.75em;
88
+ color: var(--facto-text-secondary, #aaa);
89
+ text-transform: uppercase;
90
+ letter-spacing: 0.05em;
91
+ }
92
+ /* Override ObjectEditor styles for dark theme compatibility */
93
+ .facto-record-content-section .pict-objecteditor {
94
+ background: var(--facto-bg-elevated, #1a1e2a);
95
+ border-color: var(--facto-border-subtle, #2a2e3a);
96
+ color: var(--facto-text-heading, #eee);
97
+ }
98
+ .facto-record-content-section .pict-oe-row:hover {
99
+ background: var(--facto-table-row-hover, #222738);
100
+ }
101
+ .facto-record-content-section .pict-oe-key {
102
+ color: var(--facto-brand, #4a90d9);
103
+ }
104
+ .facto-record-content-section .pict-oe-separator {
105
+ color: var(--facto-text-tertiary, #888);
106
+ }
107
+ .facto-record-content-section .pict-oe-value-string {
108
+ color: #055d40;
109
+ }
110
+ .facto-record-content-section .pict-oe-value-string::before,
111
+ .facto-record-content-section .pict-oe-value-string::after {
112
+ color: #1a8a66;
113
+ }
114
+ .facto-record-content-section .pict-oe-value-number {
115
+ color: #d4a0ff;
116
+ }
117
+ .facto-record-content-section .pict-oe-value-boolean {
118
+ color: #ffb347;
119
+ }
120
+ .facto-record-content-section .pict-oe-value-null {
121
+ color: var(--facto-text-tertiary, #666);
122
+ }
123
+ .facto-record-content-section .pict-oe-summary {
124
+ color: var(--facto-text-tertiary, #666);
125
+ }
126
+ .facto-record-content-section .pict-oe-toggle {
127
+ color: var(--facto-text-secondary, #aaa);
128
+ }
129
+ .facto-record-content-section .pict-oe-toggle:hover {
130
+ color: var(--facto-brand, #4a90d9);
131
+ background: var(--facto-table-row-hover, #222738);
132
+ }
133
+ .facto-record-content-section .pict-oe-type-badge {
134
+ background: var(--facto-table-row-hover, #222738);
135
+ color: var(--facto-text-tertiary, #888);
136
+ }
137
+ .facto-record-content-section .pict-oe-empty {
138
+ color: var(--facto-text-tertiary, #666);
139
+ }
140
+ `,
141
+
142
+ Templates:
143
+ [
144
+ {
145
+ Hash: "Facto-Full-RecordViewer-Template",
146
+ Template: /*html*/`
147
+ <div class="facto-content">
148
+ <div class="facto-record-viewer-back" onclick="{~P~}.views['Facto-Full-RecordViewer'].goBack()">
149
+ &#8592; Back to Records
150
+ </div>
151
+
152
+ <div class="facto-content-header">
153
+ <h1 id="Facto-RecordViewer-Title">Record</h1>
154
+ </div>
155
+
156
+ <div id="Facto-RecordViewer-Loading" style="color:var(--facto-text-secondary);">Loading record...</div>
157
+ <div id="Facto-RecordViewer-Error" class="facto-status facto-status-error" style="display:none;"></div>
158
+
159
+ <div id="Facto-RecordViewer-MetaContainer" style="display:none;">
160
+ <div class="facto-record-meta" id="Facto-RecordViewer-Meta"></div>
161
+
162
+ <div class="facto-record-content-section">
163
+ <h2>Record Content</h2>
164
+ <div id="Facto-RecordViewer-ObjectEditor-Container"></div>
165
+ </div>
166
+ </div>
167
+ </div>
168
+ `
169
+ }
170
+ ],
171
+
172
+ Renderables:
173
+ [
174
+ {
175
+ RenderableHash: "Facto-Full-RecordViewer-Content",
176
+ TemplateHash: "Facto-Full-RecordViewer-Template",
177
+ DestinationAddress: "#Facto-Full-Content-Container",
178
+ RenderMethod: "replace"
179
+ }
180
+ ]
181
+ };
182
+
183
+ class FactoFullRecordViewerView extends libPictView
184
+ {
185
+ constructor(pFable, pOptions, pServiceHash)
186
+ {
187
+ super(pFable, pOptions, pServiceHash);
188
+
189
+ this._CurrentIDRecord = null;
190
+ this._CurrentProjectionName = null;
191
+ this._ObjectEditorView = null;
192
+ }
193
+
194
+ onBeforeInitialize()
195
+ {
196
+ super.onBeforeInitialize();
197
+
198
+ // Register the ObjectEditor view type if it isn't already present
199
+ if (!this.fable.servicesMap.hasOwnProperty('PictViewObjectEditor'))
200
+ {
201
+ this.fable.addServiceType('PictViewObjectEditor', libPictSectionObjectEditor);
202
+ }
203
+
204
+ return true;
205
+ }
206
+
207
+ /**
208
+ * Navigate to a specific record by ID.
209
+ */
210
+ loadRecord(pIDRecord)
211
+ {
212
+ this._CurrentIDRecord = pIDRecord;
213
+ this._CurrentProjectionName = null;
214
+ this.render();
215
+ }
216
+
217
+ loadProjectionRecord(pEntityName, pIDRecord)
218
+ {
219
+ this._CurrentIDRecord = pIDRecord;
220
+ this._CurrentProjectionName = pEntityName;
221
+ this.render();
222
+ }
223
+
224
+ onAfterRender()
225
+ {
226
+ super.onAfterRender();
227
+
228
+ if (!this._CurrentIDRecord)
229
+ {
230
+ let tmpLoading = document.getElementById('Facto-RecordViewer-Loading');
231
+ if (tmpLoading)
232
+ {
233
+ tmpLoading.textContent = 'No record selected.';
234
+ }
235
+ return;
236
+ }
237
+
238
+ this._fetchAndDisplayRecord(this._CurrentIDRecord);
239
+ }
240
+
241
+ _fetchAndDisplayRecord(pIDRecord)
242
+ {
243
+ let tmpProvider = this.pict.providers.Facto;
244
+ let tmpLoadingEl = document.getElementById('Facto-RecordViewer-Loading');
245
+ let tmpErrorEl = document.getElementById('Facto-RecordViewer-Error');
246
+ let tmpMetaContainer = document.getElementById('Facto-RecordViewer-MetaContainer');
247
+
248
+ // Check if viewing a projection record (set via loadProjectionRecord)
249
+ if (this._CurrentProjectionName)
250
+ {
251
+ return this._fetchAndDisplayProjectionRecord(pIDRecord, this._CurrentProjectionName);
252
+ }
253
+
254
+ // Fetch the record, its source, its dataset, and certainty in parallel
255
+ let tmpRecord = null;
256
+ let tmpSource = null;
257
+ let tmpDataset = null;
258
+ let tmpCertainty = null;
259
+ let tmpIngestJob = null;
260
+
261
+ let tmpRecordPromise = tmpProvider.api('GET', '/1.0/Record/' + pIDRecord);
262
+ let tmpCertaintyPromise = tmpProvider.api('GET', '/facto/record/' + pIDRecord + '/certainty');
263
+
264
+ tmpRecordPromise.then(
265
+ (pRecordResponse) =>
266
+ {
267
+ if (pRecordResponse && pRecordResponse.Error)
268
+ {
269
+ if (tmpLoadingEl) tmpLoadingEl.style.display = 'none';
270
+ if (tmpErrorEl)
271
+ {
272
+ tmpErrorEl.textContent = 'Error loading record: ' + pRecordResponse.Error;
273
+ tmpErrorEl.style.display = 'block';
274
+ }
275
+ return;
276
+ }
277
+
278
+ tmpRecord = pRecordResponse;
279
+
280
+ // Now fetch the related source, dataset, and ingest job
281
+ let tmpFetches = [];
282
+
283
+ if (tmpRecord.IDSource)
284
+ {
285
+ tmpFetches.push(
286
+ tmpProvider.api('GET', '/1.0/Source/' + tmpRecord.IDSource).then(
287
+ (pResp) => { tmpSource = pResp; }));
288
+ }
289
+ if (tmpRecord.IDDataset)
290
+ {
291
+ tmpFetches.push(
292
+ tmpProvider.api('GET', '/1.0/Dataset/' + tmpRecord.IDDataset).then(
293
+ (pResp) => { tmpDataset = pResp; }));
294
+ }
295
+ if (tmpRecord.IDIngestJob)
296
+ {
297
+ tmpFetches.push(
298
+ tmpProvider.api('GET', '/facto/ingest/job/' + tmpRecord.IDIngestJob).then(
299
+ (pResp) => { tmpIngestJob = pResp; }));
300
+ }
301
+
302
+ return Promise.all(tmpFetches).then(
303
+ () =>
304
+ {
305
+ return tmpCertaintyPromise;
306
+ }).then(
307
+ (pCertaintyResponse) =>
308
+ {
309
+ tmpCertainty = pCertaintyResponse;
310
+ this._renderRecordDetail(tmpRecord, tmpSource, tmpDataset, tmpCertainty, tmpIngestJob);
311
+ });
312
+ }).catch(
313
+ (pError) =>
314
+ {
315
+ if (tmpLoadingEl) tmpLoadingEl.style.display = 'none';
316
+ if (tmpErrorEl)
317
+ {
318
+ tmpErrorEl.textContent = 'Error loading record: ' + (pError.message || pError);
319
+ tmpErrorEl.style.display = 'block';
320
+ }
321
+ });
322
+ }
323
+
324
+ _renderRecordDetail(pRecord, pSource, pDataset, pCertainty, pIngestJob)
325
+ {
326
+ let tmpLoadingEl = document.getElementById('Facto-RecordViewer-Loading');
327
+ let tmpMetaContainer = document.getElementById('Facto-RecordViewer-MetaContainer');
328
+ let tmpTitleEl = document.getElementById('Facto-RecordViewer-Title');
329
+
330
+ if (tmpLoadingEl) tmpLoadingEl.style.display = 'none';
331
+ if (tmpMetaContainer) tmpMetaContainer.style.display = 'block';
332
+
333
+ // Title
334
+ if (tmpTitleEl)
335
+ {
336
+ let tmpTitle = 'Record #' + pRecord.IDRecord;
337
+ if (pDataset && pDataset.Hash)
338
+ {
339
+ tmpTitle += ' \u2014 ' + pDataset.Hash;
340
+ }
341
+ tmpTitleEl.textContent = tmpTitle;
342
+ }
343
+
344
+ // Build metadata cards
345
+ let tmpMetaEl = document.getElementById('Facto-RecordViewer-Meta');
346
+ if (tmpMetaEl)
347
+ {
348
+ tmpMetaEl.innerHTML = this._buildMetaCards(pRecord, pSource, pDataset, pCertainty, pIngestJob);
349
+ }
350
+
351
+ // Parse the Content JSON and render via ObjectEditor
352
+ let tmpContentData = {};
353
+ if (pRecord.Content)
354
+ {
355
+ try
356
+ {
357
+ tmpContentData = JSON.parse(pRecord.Content);
358
+ }
359
+ catch (e)
360
+ {
361
+ tmpContentData = { '_raw': pRecord.Content };
362
+ }
363
+ }
364
+
365
+ // Store the parsed content in AppData so the ObjectEditor can find it
366
+ this.pict.AppData.Facto.CurrentRecordContent = tmpContentData;
367
+
368
+ // Create or re-render the ObjectEditor
369
+ this._renderObjectEditor();
370
+ }
371
+
372
+ _renderObjectEditor()
373
+ {
374
+ let tmpEditorContainerId = 'Facto-RecordViewer-ObjectEditor-Container';
375
+ let tmpViewHash = 'Facto-RecordViewer-ObjectEditor';
376
+
377
+ // If we already created this view, just re-render
378
+ if (this.pict.views[tmpViewHash])
379
+ {
380
+ this.pict.views[tmpViewHash].render();
381
+ return;
382
+ }
383
+
384
+ // Create a new ObjectEditor view instance
385
+ let tmpEditorConfig =
386
+ {
387
+ ViewIdentifier: tmpViewHash,
388
+ DefaultDestinationAddress: '#' + tmpEditorContainerId,
389
+ ObjectDataAddress: 'AppData.Facto.CurrentRecordContent',
390
+ InitialExpandDepth: 2,
391
+ Editable: false,
392
+ ShowTypeIndicators: true,
393
+ IndentPixels: 20,
394
+ Renderables:
395
+ [
396
+ {
397
+ RenderableHash: 'ObjectEditor-Container',
398
+ TemplateHash: 'ObjectEditor-Container-Template',
399
+ DestinationAddress: '#' + tmpEditorContainerId,
400
+ RenderMethod: 'replace'
401
+ }
402
+ ]
403
+ };
404
+
405
+ this.pict.addView(tmpViewHash, tmpEditorConfig, libPictSectionObjectEditor);
406
+
407
+ // The ObjectEditor registers node renderers in onBeforeInitialize.
408
+ // Since this view is created dynamically (after the app init cycle),
409
+ // we must explicitly trigger initialization before the first render.
410
+ let tmpEditorView = this.pict.views[tmpViewHash];
411
+ tmpEditorView.onBeforeInitialize();
412
+ tmpEditorView.render();
413
+ }
414
+
415
+ _buildMetaCards(pRecord, pSource, pDataset, pCertainty, pIngestJob)
416
+ {
417
+ let tmpHtml = '';
418
+
419
+ // Record Identity card
420
+ tmpHtml += '<div class="facto-record-meta-card">';
421
+ tmpHtml += '<h3>Record Identity</h3>';
422
+ tmpHtml += this._metaRow('ID', pRecord.IDRecord);
423
+ tmpHtml += this._metaRow('GUID', pRecord.GUIDRecord, true);
424
+ tmpHtml += this._metaRow('Type', pRecord.Type || '\u2014');
425
+ tmpHtml += this._metaRow('Version', pRecord.Version || 1);
426
+ tmpHtml += '</div>';
427
+
428
+ // Source card
429
+ tmpHtml += '<div class="facto-record-meta-card">';
430
+ tmpHtml += '<h3>Source</h3>';
431
+ if (pSource && !pSource.Error)
432
+ {
433
+ tmpHtml += this._metaRow('Name', pSource.Name || '\u2014');
434
+ tmpHtml += this._metaRow('Hash', pSource.Hash || '\u2014', false, true);
435
+ tmpHtml += this._metaRow('Type', pSource.Type || '\u2014');
436
+ if (pSource.URL)
437
+ {
438
+ tmpHtml += this._metaRow('URL', pSource.URL);
439
+ }
440
+ }
441
+ else
442
+ {
443
+ tmpHtml += this._metaRow('ID', pRecord.IDSource || 0);
444
+ }
445
+ tmpHtml += '</div>';
446
+
447
+ // Dataset card
448
+ tmpHtml += '<div class="facto-record-meta-card">';
449
+ tmpHtml += '<h3>Dataset</h3>';
450
+ if (pDataset && !pDataset.Error)
451
+ {
452
+ tmpHtml += this._metaRow('Name', pDataset.Name || '\u2014');
453
+ tmpHtml += this._metaRow('Hash', pDataset.Hash || '\u2014', false, true);
454
+ tmpHtml += this._metaRow('Type', pDataset.Type || '\u2014');
455
+ tmpHtml += this._metaRow('Version Policy', pDataset.VersionPolicy || '\u2014');
456
+ }
457
+ else
458
+ {
459
+ tmpHtml += this._metaRow('ID', pRecord.IDDataset || 0);
460
+ }
461
+ tmpHtml += '</div>';
462
+
463
+ // Ingest Metadata card
464
+ tmpHtml += '<div class="facto-record-meta-card">';
465
+ tmpHtml += '<h3>Ingest Metadata</h3>';
466
+ tmpHtml += this._metaRow('Ingest Date', this._formatDate(pRecord.IngestDate));
467
+ tmpHtml += this._metaRow('Ingest Job', pRecord.IDIngestJob || '\u2014');
468
+ if (pIngestJob && pIngestJob.Job)
469
+ {
470
+ tmpHtml += this._metaRow('Job Status', pIngestJob.Job.Status || '\u2014');
471
+ }
472
+ tmpHtml += this._metaRow('Created', this._formatDate(pRecord.CreateDate));
473
+ if (pRecord.OriginCreateDate)
474
+ {
475
+ tmpHtml += this._metaRow('Origin Date', this._formatDate(pRecord.OriginCreateDate));
476
+ }
477
+ tmpHtml += '</div>';
478
+
479
+ // Schema card
480
+ tmpHtml += '<div class="facto-record-meta-card">';
481
+ tmpHtml += '<h3>Schema</h3>';
482
+ tmpHtml += this._metaRow('Schema Hash', pRecord.SchemaHash || '\u2014');
483
+ tmpHtml += this._metaRow('Schema Version', pRecord.SchemaVersion || 0);
484
+ tmpHtml += '</div>';
485
+
486
+ // Certainty card
487
+ tmpHtml += '<div class="facto-record-meta-card">';
488
+ tmpHtml += '<h3>Certainty</h3>';
489
+ if (pCertainty && pCertainty.CertaintyIndices && pCertainty.CertaintyIndices.length > 0)
490
+ {
491
+ for (let i = 0; i < pCertainty.CertaintyIndices.length; i++)
492
+ {
493
+ let tmpCI = pCertainty.CertaintyIndices[i];
494
+ let tmpPct = Math.round((tmpCI.CertaintyValue || 0) * 100);
495
+ let tmpBarColor = tmpPct >= 70 ? '#28a745' : tmpPct >= 40 ? '#ffc107' : '#dc3545';
496
+ tmpHtml += this._metaRow(tmpCI.Dimension || 'overall', tmpPct + '%');
497
+ tmpHtml += '<div class="facto-record-certainty-bar"><div class="facto-record-certainty-fill" style="width:' + tmpPct + '%; background:' + tmpBarColor + ';"></div></div>';
498
+ if (tmpCI.Justification)
499
+ {
500
+ tmpHtml += '<div style="font-size:0.75em; color:var(--facto-text-tertiary); margin-top:0.15em;">' + this._escapeHtml(tmpCI.Justification) + '</div>';
501
+ }
502
+ }
503
+ }
504
+ else
505
+ {
506
+ tmpHtml += '<div style="color:var(--facto-text-tertiary); font-size:0.85em;">No certainty data</div>';
507
+ }
508
+ tmpHtml += '</div>';
509
+
510
+ return tmpHtml;
511
+ }
512
+
513
+ _metaRow(pLabel, pValue, pIsGuid, pIsHash)
514
+ {
515
+ let tmpDisplayValue = this._escapeHtml(String(pValue || ''));
516
+ if (pIsGuid && tmpDisplayValue.length > 16)
517
+ {
518
+ tmpDisplayValue = '<span title="' + tmpDisplayValue + '">' + tmpDisplayValue.substring(0, 8) + '\u2026' + tmpDisplayValue.substring(tmpDisplayValue.length - 4) + '</span>';
519
+ }
520
+ let tmpValueClass = 'facto-record-meta-value';
521
+ if (pIsHash)
522
+ {
523
+ tmpValueClass += ' facto-hash-value';
524
+ }
525
+ return '<div class="facto-record-meta-row"><span class="facto-record-meta-label">' + this._escapeHtml(pLabel) + '</span><span class="' + tmpValueClass + '">' + tmpDisplayValue + '</span></div>';
526
+ }
527
+
528
+ _formatDate(pDateStr)
529
+ {
530
+ if (!pDateStr) return '\u2014';
531
+ try
532
+ {
533
+ // SQLite datetimes (e.g. "2026-03-15 19:07:43") lack a timezone
534
+ // indicator, so new Date() would treat them as local time.
535
+ // Append 'Z' to interpret them as UTC, matching ISO timestamps.
536
+ let tmpNormalized = pDateStr;
537
+ if (typeof tmpNormalized === 'string' && !tmpNormalized.endsWith('Z') && !tmpNormalized.match(/[+-]\d{2}:\d{2}$/))
538
+ {
539
+ tmpNormalized = tmpNormalized.replace(' ', 'T') + 'Z';
540
+ }
541
+ let tmpDate = new Date(tmpNormalized);
542
+ return tmpDate.toLocaleString();
543
+ }
544
+ catch (e)
545
+ {
546
+ return pDateStr;
547
+ }
548
+ }
549
+
550
+ _escapeHtml(pStr)
551
+ {
552
+ return pStr.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
553
+ }
554
+
555
+ _fetchAndDisplayProjectionRecord(pIDRecord, pProjectionName)
556
+ {
557
+ let tmpProvider = this.pict.providers.Facto;
558
+ let tmpLoadingEl = document.getElementById('Facto-RecordViewer-Loading');
559
+ let tmpErrorEl = document.getElementById('Facto-RecordViewer-Error');
560
+ let tmpMetaContainer = document.getElementById('Facto-RecordViewer-MetaContainer');
561
+
562
+ tmpProvider.api('GET', '/1.0/' + pProjectionName + '/' + pIDRecord).then(
563
+ (pRecord) =>
564
+ {
565
+ if (!pRecord || pRecord.Error)
566
+ {
567
+ if (tmpLoadingEl) tmpLoadingEl.style.display = 'none';
568
+ if (tmpErrorEl)
569
+ {
570
+ tmpErrorEl.textContent = 'Error loading projection record: ' + (pRecord ? pRecord.Error : 'Not found');
571
+ tmpErrorEl.style.display = 'block';
572
+ }
573
+ return;
574
+ }
575
+
576
+ if (tmpLoadingEl) tmpLoadingEl.style.display = 'none';
577
+ if (tmpMetaContainer) tmpMetaContainer.style.display = 'block';
578
+
579
+ // Find the dataset for context
580
+ let tmpDatasets = this.pict.AppData.Facto.Datasets || [];
581
+ let tmpDataset = tmpDatasets.find(function(d) { return d.Name === pProjectionName; });
582
+
583
+ // Build title
584
+ let tmpTitleEl = document.getElementById('Facto-RecordViewer-Title');
585
+ if (tmpTitleEl)
586
+ {
587
+ tmpTitleEl.textContent = pProjectionName + ' #' + pIDRecord;
588
+ }
589
+
590
+ // Build metadata cards into the inner Meta div using the same
591
+ // _metaRow helper and CSS classes as the regular record view
592
+ let tmpMetaEl = document.getElementById('Facto-RecordViewer-Meta');
593
+ if (tmpMetaEl)
594
+ {
595
+ let tmpHtml = '';
596
+
597
+ // Projection Identity card
598
+ tmpHtml += '<div class="facto-record-meta-card">';
599
+ tmpHtml += '<h3>Projection Identity</h3>';
600
+ tmpHtml += this._metaRow('ID', pIDRecord);
601
+ tmpHtml += this._metaRow('GUID', pRecord['GUID' + pProjectionName] || '\u2014', true);
602
+ tmpHtml += this._metaRow('Entity', pProjectionName);
603
+ tmpHtml += this._metaRow('Type', '<span class="facto-badge" style="background:var(--facto-brand-a20); color:var(--facto-brand);">Projection</span>');
604
+ tmpHtml += '</div>';
605
+
606
+ // Dataset card
607
+ if (tmpDataset)
608
+ {
609
+ tmpHtml += '<div class="facto-record-meta-card">';
610
+ tmpHtml += '<h3>Dataset</h3>';
611
+ tmpHtml += this._metaRow('Name', tmpDataset.Name || '\u2014');
612
+ tmpHtml += this._metaRow('Type', tmpDataset.Type || '\u2014');
613
+ if (tmpDataset.Hash)
614
+ {
615
+ tmpHtml += this._metaRow('Hash', tmpDataset.Hash, false, true);
616
+ }
617
+ tmpHtml += '</div>';
618
+ }
619
+
620
+ // Timestamps card (if available)
621
+ if (pRecord.CreateDate || pRecord.UpdateDate)
622
+ {
623
+ tmpHtml += '<div class="facto-record-meta-card">';
624
+ tmpHtml += '<h3>Timestamps</h3>';
625
+ if (pRecord.CreateDate)
626
+ {
627
+ tmpHtml += this._metaRow('Created', this._formatDate(pRecord.CreateDate));
628
+ }
629
+ if (pRecord.UpdateDate)
630
+ {
631
+ tmpHtml += this._metaRow('Updated', this._formatDate(pRecord.UpdateDate));
632
+ }
633
+ tmpHtml += '</div>';
634
+ }
635
+
636
+ tmpMetaEl.innerHTML = tmpHtml;
637
+ }
638
+
639
+ // Display record content — store in AppData and use
640
+ // the same ObjectEditor as the regular record view
641
+ let tmpDisplayRecord = Object.assign({}, pRecord);
642
+ delete tmpDisplayRecord.CreateDate;
643
+ delete tmpDisplayRecord.UpdateDate;
644
+ delete tmpDisplayRecord.Deleted;
645
+ delete tmpDisplayRecord.DeleteDate;
646
+ delete tmpDisplayRecord.CreatingIDUser;
647
+ delete tmpDisplayRecord.UpdatingIDUser;
648
+ delete tmpDisplayRecord.DeletingIDUser;
649
+
650
+ this.pict.AppData.Facto.CurrentRecordContent = tmpDisplayRecord;
651
+ this._renderObjectEditor();
652
+ });
653
+ }
654
+
655
+ goBack()
656
+ {
657
+ this.pict.PictApplication.navigateTo('/Records');
658
+ }
659
+ }
660
+
661
+ module.exports = FactoFullRecordViewerView;
662
+
663
+ module.exports.default_configuration = _ViewConfiguration;