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,648 @@
1
+ const libPictView = require('pict-view');
2
+
3
+ const _ViewConfiguration =
4
+ {
5
+ ViewIdentifier: "Facto-Full-Records",
6
+
7
+ DefaultRenderable: "Facto-Full-Records-Content",
8
+ DefaultDestinationAddress: "#Facto-Full-Content-Container",
9
+
10
+ AutoRender: false,
11
+
12
+ CSS: /*css*/`
13
+ .facto-records-pager {
14
+ display: flex;
15
+ align-items: center;
16
+ gap: 0.75em;
17
+ margin-bottom: 1em;
18
+ }
19
+ .facto-records-pager span {
20
+ font-size: 0.85em;
21
+ color: var(--facto-text-secondary);
22
+ }
23
+ .facto-record-data {
24
+ max-width: 400px;
25
+ overflow: hidden;
26
+ text-overflow: ellipsis;
27
+ white-space: nowrap;
28
+ font-family: 'SF Mono', Consolas, monospace;
29
+ font-size: 0.8em;
30
+ color: var(--facto-text-secondary);
31
+ }
32
+ .facto-filter-bar {
33
+ border: 1px solid var(--facto-border, #333);
34
+ border-radius: 6px;
35
+ padding: 1em;
36
+ margin-bottom: 1em;
37
+ background: var(--facto-bg-surface);
38
+ }
39
+ .facto-filter-bar h3 {
40
+ margin: 0 0 0.75em 0;
41
+ font-size: 0.9em;
42
+ color: var(--facto-text-secondary);
43
+ text-transform: uppercase;
44
+ letter-spacing: 0.05em;
45
+ }
46
+ .facto-filter-section {
47
+ margin-bottom: 1em;
48
+ }
49
+ .facto-filter-section:last-child {
50
+ margin-bottom: 0;
51
+ }
52
+ .facto-filter-label {
53
+ display: block;
54
+ font-size: 0.8em;
55
+ color: var(--facto-text-secondary);
56
+ margin-bottom: 0.4em;
57
+ font-weight: 600;
58
+ }
59
+ .facto-source-toggle {
60
+ font-size: 0.75em;
61
+ color: var(--facto-link);
62
+ cursor: pointer;
63
+ margin-left: 0.5em;
64
+ text-decoration: underline;
65
+ }
66
+ .facto-filter-list {
67
+ border: 1px solid var(--facto-border);
68
+ border-radius: 4px;
69
+ max-height: 200px;
70
+ overflow-y: auto;
71
+ background: var(--facto-bg-input);
72
+ }
73
+ .facto-filter-list-item {
74
+ display: flex;
75
+ justify-content: space-between;
76
+ align-items: center;
77
+ padding: 0.4em 0.75em;
78
+ cursor: pointer;
79
+ font-size: 0.85em;
80
+ color: var(--facto-text);
81
+ border-left: 3px solid transparent;
82
+ transition: background-color 0.15s, border-color 0.15s;
83
+ user-select: none;
84
+ }
85
+ .facto-filter-list-item:hover {
86
+ background: var(--facto-bg-elevated);
87
+ }
88
+ .facto-filter-list-item.selected {
89
+ background: color-mix(in srgb, var(--facto-brand) 15%, transparent);
90
+ border-left-color: var(--facto-brand);
91
+ font-weight: 600;
92
+ }
93
+ .facto-filter-list-item + .facto-filter-list-item {
94
+ border-top: 1px solid var(--facto-border-subtle, var(--facto-border));
95
+ }
96
+ .facto-filter-count {
97
+ font-size: 0.8em;
98
+ color: var(--facto-text-tertiary);
99
+ margin-left: 1em;
100
+ white-space: nowrap;
101
+ }
102
+ .facto-filter-list-item.selected .facto-filter-count {
103
+ color: var(--facto-text-secondary);
104
+ }
105
+ .facto-date-filters {
106
+ display: flex;
107
+ align-items: center;
108
+ gap: 0.75em;
109
+ flex-wrap: wrap;
110
+ }
111
+ .facto-date-filters input[type="text"] {
112
+ padding: 0.35em 0.5em;
113
+ font-size: 0.85em;
114
+ border: 1px solid var(--facto-border);
115
+ border-radius: 4px;
116
+ background: var(--facto-bg-input);
117
+ color: var(--facto-text);
118
+ width: 160px;
119
+ margin-bottom: 0;
120
+ }
121
+ .facto-date-filters span {
122
+ font-size: 0.8em;
123
+ color: var(--facto-text-secondary);
124
+ }
125
+ .facto-filter-actions {
126
+ display: flex;
127
+ gap: 0.5em;
128
+ margin-top: 0.75em;
129
+ }
130
+ `,
131
+
132
+ Templates:
133
+ [
134
+ {
135
+ Hash: "Facto-Full-Records-Template",
136
+ Template: /*html*/`
137
+ <div class="facto-content">
138
+ <div class="facto-content-header">
139
+ <h1>Records</h1>
140
+ <p>Browse ingested records across all datasets.</p>
141
+ </div>
142
+
143
+ <div class="facto-filter-bar">
144
+ <h3>Filters</h3>
145
+
146
+ <div class="facto-filter-section">
147
+ <span class="facto-filter-label">
148
+ Data Sources
149
+ <span class="facto-source-toggle" onclick="{~P~}.views['Facto-Full-Records'].toggleAllSources(true)">All</span>
150
+ <span class="facto-source-toggle" onclick="{~P~}.views['Facto-Full-Records'].toggleAllSources(false)">None</span>
151
+ </span>
152
+ <div id="Facto-Full-Records-SourceList" class="facto-filter-list">
153
+ <div class="facto-filter-list-item" style="color:var(--facto-text-secondary); cursor:default;">Loading sources\u2026</div>
154
+ </div>
155
+ </div>
156
+
157
+ <div class="facto-filter-section">
158
+ <span class="facto-filter-label">
159
+ Projections (Datasets)
160
+ <span class="facto-source-toggle" onclick="{~P~}.views['Facto-Full-Records'].toggleAllDatasets(true)">All</span>
161
+ <span class="facto-source-toggle" onclick="{~P~}.views['Facto-Full-Records'].toggleAllDatasets(false)">None</span>
162
+ </span>
163
+ <div id="Facto-Full-Records-DatasetList" class="facto-filter-list">
164
+ <div class="facto-filter-list-item" style="color:var(--facto-text-secondary); cursor:default;">Loading datasets\u2026</div>
165
+ </div>
166
+ </div>
167
+
168
+ <div class="facto-filter-section">
169
+ <span class="facto-filter-label">Date Range (Ingest Date)</span>
170
+ <div class="facto-date-filters">
171
+ <span>From</span>
172
+ <input type="text" id="Facto-Full-Records-DateFrom" placeholder="YYYY, YYYY-MM, or YYYY-MM-DD" />
173
+ <span>To</span>
174
+ <input type="text" id="Facto-Full-Records-DateTo" placeholder="YYYY, YYYY-MM, or YYYY-MM-DD" />
175
+ </div>
176
+ </div>
177
+
178
+ <div class="facto-filter-actions">
179
+ <button class="facto-btn facto-btn-primary facto-btn-small" onclick="{~P~}.views['Facto-Full-Records'].applyFilters()">Apply Filters</button>
180
+ <button class="facto-btn facto-btn-secondary facto-btn-small" onclick="{~P~}.views['Facto-Full-Records'].clearFilters()">Clear</button>
181
+ </div>
182
+ </div>
183
+
184
+ <div class="facto-records-pager">
185
+ <button class="facto-btn facto-btn-secondary facto-btn-small" onclick="{~P~}.views['Facto-Full-Records'].prevPage()">Previous</button>
186
+ <span id="Facto-Full-Records-PageInfo">Page 1</span>
187
+ <button class="facto-btn facto-btn-secondary facto-btn-small" onclick="{~P~}.views['Facto-Full-Records'].nextPage()">Next</button>
188
+ </div>
189
+
190
+ <div id="Facto-Full-Records-List"></div>
191
+ </div>
192
+ `
193
+ }
194
+ ],
195
+
196
+ Renderables:
197
+ [
198
+ {
199
+ RenderableHash: "Facto-Full-Records-Content",
200
+ TemplateHash: "Facto-Full-Records-Template",
201
+ DestinationAddress: "#Facto-Full-Content-Container",
202
+ RenderMethod: "replace"
203
+ }
204
+ ]
205
+ };
206
+
207
+ class FactoFullRecordsView extends libPictView
208
+ {
209
+ constructor(pFable, pOptions, pServiceHash)
210
+ {
211
+ super(pFable, pOptions, pServiceHash);
212
+ }
213
+
214
+ onAfterRender(pRenderable, pRenderDestinationAddress, pRecord, pContent)
215
+ {
216
+ // Load sources and datasets, then render lists and load records
217
+ Promise.all([
218
+ this.pict.providers.Facto.loadSources(),
219
+ this.pict.providers.Facto.loadDatasets()
220
+ ]).then(
221
+ () =>
222
+ {
223
+ this.renderSourceList();
224
+ this.renderDatasetList();
225
+
226
+ // Restore filter UI state from AppData (populated by URL route handler)
227
+ this._restoreFilterUIFromState();
228
+
229
+ this.loadCurrentPage();
230
+
231
+ // Load counts in the background and update badges when ready
232
+ this.pict.providers.Facto.loadSourceCounts().then(
233
+ () => { this.updateSourceCounts(); });
234
+ this.pict.providers.Facto.loadDatasetCounts().then(
235
+ () => { this.updateDatasetCounts(); });
236
+ });
237
+
238
+ return super.onAfterRender(pRenderable, pRenderDestinationAddress, pRecord, pContent);
239
+ }
240
+
241
+ /**
242
+ * Restore filter UI checkboxes and date inputs from AppData state.
243
+ * Called after source/dataset lists are rendered so DOM elements exist.
244
+ */
245
+ _restoreFilterUIFromState()
246
+ {
247
+ let tmpData = this.pict.AppData.Facto;
248
+
249
+ // Restore selected sources
250
+ if (tmpData.RecordFilterSources && tmpData.RecordFilterSources.length > 0)
251
+ {
252
+ let tmpSourceItems = document.querySelectorAll('#Facto-Full-Records-SourceList .facto-filter-list-item');
253
+ for (let i = 0; i < tmpSourceItems.length; i++)
254
+ {
255
+ let tmpID = parseInt(tmpSourceItems[i].getAttribute('data-id'), 10);
256
+ if (tmpData.RecordFilterSources.indexOf(tmpID) >= 0)
257
+ {
258
+ tmpSourceItems[i].classList.add('selected');
259
+ }
260
+ }
261
+ }
262
+
263
+ // Restore selected datasets
264
+ if (tmpData.RecordFilterDatasets && tmpData.RecordFilterDatasets.length > 0)
265
+ {
266
+ let tmpDatasetItems = document.querySelectorAll('#Facto-Full-Records-DatasetList .facto-filter-list-item');
267
+ for (let i = 0; i < tmpDatasetItems.length; i++)
268
+ {
269
+ let tmpID = parseInt(tmpDatasetItems[i].getAttribute('data-id'), 10);
270
+ if (tmpData.RecordFilterDatasets.indexOf(tmpID) >= 0)
271
+ {
272
+ tmpDatasetItems[i].classList.add('selected');
273
+ }
274
+ }
275
+ }
276
+
277
+ // Restore date inputs
278
+ if (tmpData.RecordFilterDateFrom)
279
+ {
280
+ let tmpEl = document.getElementById('Facto-Full-Records-DateFrom');
281
+ if (tmpEl) tmpEl.value = tmpData.RecordFilterDateFrom;
282
+ }
283
+ if (tmpData.RecordFilterDateTo)
284
+ {
285
+ let tmpEl = document.getElementById('Facto-Full-Records-DateTo');
286
+ if (tmpEl) tmpEl.value = tmpData.RecordFilterDateTo;
287
+ }
288
+ }
289
+
290
+ renderSourceList()
291
+ {
292
+ let tmpContainer = document.getElementById('Facto-Full-Records-SourceList');
293
+ if (!tmpContainer)
294
+ {
295
+ return;
296
+ }
297
+
298
+ let tmpSources = this.pict.AppData.Facto.Sources;
299
+ if (!tmpSources || tmpSources.length === 0)
300
+ {
301
+ tmpContainer.innerHTML = '<div class="facto-filter-list-item" style="color:var(--facto-text-secondary); cursor:default;">No sources available.</div>';
302
+ return;
303
+ }
304
+
305
+ let tmpHtml = '';
306
+ for (let i = 0; i < tmpSources.length; i++)
307
+ {
308
+ let tmpSource = tmpSources[i];
309
+ let tmpLabel = tmpSource.Name || ('Source #' + tmpSource.IDSource);
310
+ let tmpCount = (typeof tmpSource.RecordCount !== 'undefined') ? tmpSource.RecordCount.toLocaleString() : '\u2026';
311
+ tmpHtml += '<div class="facto-filter-list-item" data-id="' + tmpSource.IDSource + '" onclick="pict.views[\'Facto-Full-Records\'].toggleListItem(this)">';
312
+ tmpHtml += '<span class="facto-filter-list-name">' + tmpLabel + '</span>';
313
+ tmpHtml += '<span class="facto-filter-count">(' + tmpCount + ')</span>';
314
+ tmpHtml += '</div>';
315
+ }
316
+ tmpContainer.innerHTML = tmpHtml;
317
+ }
318
+
319
+ renderDatasetList()
320
+ {
321
+ let tmpContainer = document.getElementById('Facto-Full-Records-DatasetList');
322
+ if (!tmpContainer)
323
+ {
324
+ return;
325
+ }
326
+
327
+ let tmpDatasets = this.pict.AppData.Facto.Datasets;
328
+ if (!tmpDatasets || tmpDatasets.length === 0)
329
+ {
330
+ tmpContainer.innerHTML = '<div class="facto-filter-list-item" style="color:var(--facto-text-secondary); cursor:default;">No datasets available.</div>';
331
+ return;
332
+ }
333
+
334
+ let tmpHtml = '';
335
+ for (let i = 0; i < tmpDatasets.length; i++)
336
+ {
337
+ let tmpDataset = tmpDatasets[i];
338
+ let tmpLabel = tmpDataset.Name || ('Dataset #' + tmpDataset.IDDataset);
339
+ let tmpCount = (typeof tmpDataset.RecordCount !== 'undefined') ? tmpDataset.RecordCount.toLocaleString() : '\u2026';
340
+ tmpHtml += '<div class="facto-filter-list-item" data-id="' + tmpDataset.IDDataset + '" onclick="pict.views[\'Facto-Full-Records\'].toggleListItem(this)">';
341
+ tmpHtml += '<span class="facto-filter-list-name">' + tmpLabel + '</span>';
342
+ tmpHtml += '<span class="facto-filter-count">(' + tmpCount + ')</span>';
343
+ tmpHtml += '</div>';
344
+ }
345
+ tmpContainer.innerHTML = tmpHtml;
346
+ }
347
+
348
+ updateSourceCounts()
349
+ {
350
+ let tmpContainer = document.getElementById('Facto-Full-Records-SourceList');
351
+ if (!tmpContainer)
352
+ {
353
+ return;
354
+ }
355
+ let tmpSources = this.pict.AppData.Facto.Sources;
356
+ if (!tmpSources)
357
+ {
358
+ return;
359
+ }
360
+ let tmpItems = tmpContainer.querySelectorAll('.facto-filter-list-item');
361
+ for (let i = 0; i < tmpItems.length; i++)
362
+ {
363
+ let tmpID = parseInt(tmpItems[i].getAttribute('data-id'), 10);
364
+ for (let j = 0; j < tmpSources.length; j++)
365
+ {
366
+ if (tmpSources[j].IDSource === tmpID)
367
+ {
368
+ let tmpCountEl = tmpItems[i].querySelector('.facto-filter-count');
369
+ if (tmpCountEl)
370
+ {
371
+ tmpCountEl.textContent = '(' + tmpSources[j].RecordCount.toLocaleString() + ')';
372
+ }
373
+ break;
374
+ }
375
+ }
376
+ }
377
+ }
378
+
379
+ updateDatasetCounts()
380
+ {
381
+ let tmpContainer = document.getElementById('Facto-Full-Records-DatasetList');
382
+ if (!tmpContainer)
383
+ {
384
+ return;
385
+ }
386
+ let tmpDatasets = this.pict.AppData.Facto.Datasets;
387
+ if (!tmpDatasets)
388
+ {
389
+ return;
390
+ }
391
+ let tmpItems = tmpContainer.querySelectorAll('.facto-filter-list-item');
392
+ for (let i = 0; i < tmpItems.length; i++)
393
+ {
394
+ let tmpID = parseInt(tmpItems[i].getAttribute('data-id'), 10);
395
+ for (let j = 0; j < tmpDatasets.length; j++)
396
+ {
397
+ if (tmpDatasets[j].IDDataset === tmpID)
398
+ {
399
+ let tmpCountEl = tmpItems[i].querySelector('.facto-filter-count');
400
+ if (tmpCountEl)
401
+ {
402
+ tmpCountEl.textContent = '(' + tmpDatasets[j].RecordCount.toLocaleString() + ')';
403
+ }
404
+ break;
405
+ }
406
+ }
407
+ }
408
+ }
409
+
410
+ toggleListItem(pElement)
411
+ {
412
+ if (pElement)
413
+ {
414
+ pElement.classList.toggle('selected');
415
+ }
416
+ }
417
+
418
+ toggleAllSources(pSelected)
419
+ {
420
+ let tmpItems = document.querySelectorAll('#Facto-Full-Records-SourceList .facto-filter-list-item');
421
+ for (let i = 0; i < tmpItems.length; i++)
422
+ {
423
+ if (pSelected)
424
+ {
425
+ tmpItems[i].classList.add('selected');
426
+ }
427
+ else
428
+ {
429
+ tmpItems[i].classList.remove('selected');
430
+ }
431
+ }
432
+ }
433
+
434
+ toggleAllDatasets(pSelected)
435
+ {
436
+ let tmpItems = document.querySelectorAll('#Facto-Full-Records-DatasetList .facto-filter-list-item');
437
+ for (let i = 0; i < tmpItems.length; i++)
438
+ {
439
+ if (pSelected)
440
+ {
441
+ tmpItems[i].classList.add('selected');
442
+ }
443
+ else
444
+ {
445
+ tmpItems[i].classList.remove('selected');
446
+ }
447
+ }
448
+ }
449
+
450
+ readFiltersFromDOM()
451
+ {
452
+ // Read selected sources from list items
453
+ let tmpSelectedSources = [];
454
+ let tmpSourceItems = document.querySelectorAll('#Facto-Full-Records-SourceList .facto-filter-list-item.selected');
455
+ for (let i = 0; i < tmpSourceItems.length; i++)
456
+ {
457
+ tmpSelectedSources.push(parseInt(tmpSourceItems[i].getAttribute('data-id'), 10));
458
+ }
459
+
460
+ // Read selected datasets from list items
461
+ let tmpSelectedDatasets = [];
462
+ let tmpDatasetItems = document.querySelectorAll('#Facto-Full-Records-DatasetList .facto-filter-list-item.selected');
463
+ for (let i = 0; i < tmpDatasetItems.length; i++)
464
+ {
465
+ tmpSelectedDatasets.push(parseInt(tmpDatasetItems[i].getAttribute('data-id'), 10));
466
+ }
467
+
468
+ let tmpDateFrom = '';
469
+ let tmpDateTo = '';
470
+ let tmpDateFromEl = document.getElementById('Facto-Full-Records-DateFrom');
471
+ let tmpDateToEl = document.getElementById('Facto-Full-Records-DateTo');
472
+ if (tmpDateFromEl)
473
+ {
474
+ tmpDateFrom = tmpDateFromEl.value;
475
+ }
476
+ if (tmpDateToEl)
477
+ {
478
+ tmpDateTo = tmpDateToEl.value;
479
+ }
480
+
481
+ this.pict.AppData.Facto.RecordFilterSources = tmpSelectedSources;
482
+ this.pict.AppData.Facto.RecordFilterDatasets = tmpSelectedDatasets;
483
+ this.pict.AppData.Facto.RecordFilterDateFrom = tmpDateFrom;
484
+ this.pict.AppData.Facto.RecordFilterDateTo = tmpDateTo;
485
+ }
486
+
487
+ hasActiveFilters()
488
+ {
489
+ let tmpData = this.pict.AppData.Facto;
490
+ return (tmpData.RecordFilterSources.length > 0 ||
491
+ tmpData.RecordFilterDatasets.length > 0 ||
492
+ tmpData.RecordFilterDateFrom ||
493
+ tmpData.RecordFilterDateTo);
494
+ }
495
+
496
+ loadCurrentPage()
497
+ {
498
+ let tmpData = this.pict.AppData.Facto;
499
+ if (this.hasActiveFilters())
500
+ {
501
+ this.pict.providers.Facto.loadFilteredRecords(
502
+ tmpData.RecordPage,
503
+ tmpData.RecordFilterSources,
504
+ tmpData.RecordFilterDateFrom,
505
+ tmpData.RecordFilterDateTo,
506
+ tmpData.RecordFilterDatasets
507
+ ).then(() => { this.refreshList(); });
508
+ }
509
+ else
510
+ {
511
+ this.pict.providers.Facto.loadRecords(tmpData.RecordPage).then(
512
+ () => { this.refreshList(); });
513
+ }
514
+ }
515
+
516
+ applyFilters()
517
+ {
518
+ this.readFiltersFromDOM();
519
+ let tmpData = this.pict.AppData.Facto;
520
+ let tmpProvider = this.pict.providers.Facto;
521
+
522
+ // Build filter objects from current UI state
523
+ let tmpFilters = tmpProvider.buildRecordFiltersFromState(
524
+ tmpData.RecordFilterSources,
525
+ tmpData.RecordFilterDatasets,
526
+ tmpData.RecordFilterDateFrom,
527
+ tmpData.RecordFilterDateTo
528
+ );
529
+
530
+ // Navigate to the filtered URL (this triggers the route handler which loads data)
531
+ let tmpRoute = tmpProvider.buildRecordsFilterRoute(tmpFilters, 0);
532
+ this.pict.PictApplication.navigateTo(tmpRoute);
533
+ }
534
+
535
+ clearFilters()
536
+ {
537
+ this.pict.AppData.Facto.RecordFilterSources = [];
538
+ this.pict.AppData.Facto.RecordFilterDatasets = [];
539
+ this.pict.AppData.Facto.RecordFilterDateFrom = '';
540
+ this.pict.AppData.Facto.RecordFilterDateTo = '';
541
+ this.pict.AppData.Facto.RecordPage = 0;
542
+ this.pict.AppData.Facto.RecordFilterString = '';
543
+
544
+ // Navigate to unfiltered Records (clears the URL)
545
+ this.pict.PictApplication.navigateTo('/Records');
546
+ }
547
+
548
+ refreshList()
549
+ {
550
+ let tmpContainer = document.getElementById('Facto-Full-Records-List');
551
+ if (!tmpContainer) return;
552
+
553
+ let tmpRecords = this.pict.AppData.Facto.Records;
554
+ let tmpPageInfo = document.getElementById('Facto-Full-Records-PageInfo');
555
+ if (tmpPageInfo) tmpPageInfo.textContent = 'Page ' + ((this.pict.AppData.Facto.RecordPage || 0) + 1);
556
+
557
+ if (!tmpRecords || tmpRecords.length === 0)
558
+ {
559
+ tmpContainer.innerHTML = '<div class="facto-empty">No records found. Ingest data via Source Research or the Ingest API.</div>';
560
+ return;
561
+ }
562
+
563
+ let tmpHtml = '<table><thead><tr><th>ID</th><th>Dataset</th><th>Source</th><th>GUID</th><th>Data</th><th></th></tr></thead><tbody>';
564
+ for (let i = 0; i < tmpRecords.length; i++)
565
+ {
566
+ let tmpRec = tmpRecords[i];
567
+ let tmpData = '';
568
+ try { tmpData = JSON.stringify(JSON.parse(tmpRec.Content || '{}')); }
569
+ catch(e) { tmpData = tmpRec.Content || ''; }
570
+
571
+ tmpHtml += '<tr>';
572
+ tmpHtml += '<td>' + (tmpRec.IDRecord || '') + '</td>';
573
+ tmpHtml += '<td>' + (tmpRec.IDDataset || '') + '</td>';
574
+ tmpHtml += '<td>' + (tmpRec.IDSource || '') + '</td>';
575
+ tmpHtml += '<td style="font-size:0.8em; color:var(--facto-text-tertiary);">' + (tmpRec.GUIDRecord || '').substring(0, 8) + '...</td>';
576
+ tmpHtml += '<td class="facto-record-data">' + tmpData + '</td>';
577
+ if (tmpRec.Type === 'projection')
578
+ {
579
+ // Projection records: show content in a modal or expand inline
580
+ let tmpSafeContent = (tmpRec.Content || '').replace(/'/g, '&apos;').replace(/"/g, '&quot;');
581
+ tmpHtml += '<td><button class="facto-btn facto-btn-secondary facto-btn-small" onclick="pict.views[\'Facto-Full-Records\'].viewProjectionRecord(' + tmpRec.IDRecord + ',' + tmpRec.IDDataset + ')">View</button></td>';
582
+ }
583
+ else
584
+ {
585
+ tmpHtml += '<td><button class="facto-btn facto-btn-secondary facto-btn-small" onclick="pict.PictApplication.navigateTo(\'/Record/' + tmpRec.IDRecord + '\')">View</button></td>';
586
+ }
587
+ tmpHtml += '</tr>';
588
+ }
589
+ tmpHtml += '</tbody></table>';
590
+ tmpContainer.innerHTML = tmpHtml;
591
+ }
592
+
593
+ viewProjectionRecord(pIDRecord, pIDDataset)
594
+ {
595
+ let tmpDatasets = this.pict.AppData.Facto.Datasets || [];
596
+ let tmpDataset = tmpDatasets.find(function(d) { return d.IDDataset === pIDDataset; });
597
+ if (!tmpDataset) return;
598
+
599
+ this.pict.PictApplication.navigateTo('/Projection/' + tmpDataset.Name + '/Record/' + pIDRecord);
600
+ }
601
+
602
+ prevPage()
603
+ {
604
+ if (this.pict.AppData.Facto.RecordPage > 0)
605
+ {
606
+ this.pict.AppData.Facto.RecordPage--;
607
+ this._navigateToCurrentFilter();
608
+ }
609
+ }
610
+
611
+ nextPage()
612
+ {
613
+ this.pict.AppData.Facto.RecordPage++;
614
+ this._navigateToCurrentFilter();
615
+ }
616
+
617
+ /**
618
+ * Navigate to the current filter + page state as a URL.
619
+ * Used by pagination and any action that changes page but not filters.
620
+ */
621
+ _navigateToCurrentFilter()
622
+ {
623
+ let tmpData = this.pict.AppData.Facto;
624
+
625
+ if (this.hasActiveFilters())
626
+ {
627
+ let tmpProvider = this.pict.providers.Facto;
628
+ let tmpFilters = tmpProvider.buildRecordFiltersFromState(
629
+ tmpData.RecordFilterSources,
630
+ tmpData.RecordFilterDatasets,
631
+ tmpData.RecordFilterDateFrom,
632
+ tmpData.RecordFilterDateTo
633
+ );
634
+ let tmpBegin = (tmpData.RecordPage || 0) * (tmpData.RecordPageSize || 50);
635
+ let tmpRoute = tmpProvider.buildRecordsFilterRoute(tmpFilters, tmpBegin);
636
+ this.pict.PictApplication.navigateTo(tmpRoute);
637
+ }
638
+ else
639
+ {
640
+ // No filters — just reload the current page directly
641
+ this.loadCurrentPage();
642
+ }
643
+ }
644
+ }
645
+
646
+ module.exports = FactoFullRecordsView;
647
+
648
+ module.exports.default_configuration = _ViewConfiguration;