retold-data-service 2.0.21 → 2.0.23

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 (37) hide show
  1. package/.quackage-comprehension-loader.json +19 -0
  2. package/bin/retold-data-service-clone.js +4 -1
  3. package/generate-bookstore-comprehension.js +645 -0
  4. package/package.json +7 -7
  5. package/source/Retold-Data-Service.js +30 -2
  6. package/source/services/comprehension-loader/ComprehensionLoader-Command-Load.js +345 -0
  7. package/source/services/comprehension-loader/ComprehensionLoader-Command-Schema.js +97 -0
  8. package/source/services/comprehension-loader/ComprehensionLoader-Command-Session.js +221 -0
  9. package/source/services/comprehension-loader/ComprehensionLoader-Command-WebUI.js +57 -0
  10. package/source/services/comprehension-loader/Retold-Data-Service-ComprehensionLoader.js +536 -0
  11. package/source/services/comprehension-loader/pict-app/Pict-Application-ComprehensionLoader-Configuration.json +9 -0
  12. package/source/services/comprehension-loader/pict-app/Pict-Application-ComprehensionLoader.js +86 -0
  13. package/source/services/comprehension-loader/pict-app/Pict-ComprehensionLoader-Bundle.js +6 -0
  14. package/source/services/comprehension-loader/pict-app/providers/Pict-Provider-ComprehensionLoader.js +760 -0
  15. package/source/services/comprehension-loader/pict-app/views/PictView-ComprehensionLoader-Layout.js +360 -0
  16. package/source/services/comprehension-loader/pict-app/views/PictView-ComprehensionLoader-Load.js +472 -0
  17. package/source/services/comprehension-loader/pict-app/views/PictView-ComprehensionLoader-Schema.js +119 -0
  18. package/source/services/comprehension-loader/pict-app/views/PictView-ComprehensionLoader-Session.js +269 -0
  19. package/source/services/comprehension-loader/pict-app/views/PictView-ComprehensionLoader-Source.js +330 -0
  20. package/source/services/comprehension-loader/web/comprehension-loader.js +6794 -0
  21. package/source/services/comprehension-loader/web/comprehension-loader.js.map +1 -0
  22. package/source/services/comprehension-loader/web/comprehension-loader.min.js +2 -0
  23. package/source/services/comprehension-loader/web/comprehension-loader.min.js.map +1 -0
  24. package/source/services/comprehension-loader/web/index.html +17 -0
  25. package/source/services/data-cloner/DataCloner-Command-Schema.js +407 -15
  26. package/source/services/data-cloner/Retold-Data-Service-DataCloner.js +59 -1
  27. package/source/services/data-cloner/pict-app/Pict-Application-DataCloner.js +1 -0
  28. package/source/services/data-cloner/pict-app/providers/Pict-Provider-DataCloner.js +125 -5
  29. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Connection.js +18 -8
  30. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Deploy.js +104 -1
  31. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Export.js +1 -1
  32. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Layout.js +12 -0
  33. package/source/services/data-cloner/web/data-cloner.js +201 -139
  34. package/source/services/data-cloner/web/data-cloner.js.map +1 -1
  35. package/source/services/data-cloner/web/data-cloner.min.js +1 -1
  36. package/source/services/data-cloner/web/data-cloner.min.js.map +1 -1
  37. package/test/RetoldDataService_tests.js +225 -0
@@ -0,0 +1,760 @@
1
+ const libPictProvider = require('pict-provider');
2
+
3
+ class ComprehensionLoaderProvider extends libPictProvider
4
+ {
5
+ constructor(pFable, pOptions, pServiceHash)
6
+ {
7
+ super(pFable, pOptions, pServiceHash);
8
+ }
9
+
10
+ // ================================================================
11
+ // API Helper
12
+ // ================================================================
13
+
14
+ api(pMethod, pPath, pBody)
15
+ {
16
+ let tmpOpts = { method: pMethod, headers: {} };
17
+ if (pBody)
18
+ {
19
+ tmpOpts.headers['Content-Type'] = 'application/json';
20
+ tmpOpts.body = JSON.stringify(pBody);
21
+ }
22
+ return fetch(pPath, tmpOpts).then(function(pResponse) { return pResponse.json(); });
23
+ }
24
+
25
+ setStatus(pElementId, pMessage, pType)
26
+ {
27
+ let tmpEl = document.getElementById(pElementId);
28
+ if (!tmpEl) return;
29
+ tmpEl.className = 'status ' + (pType || 'info');
30
+ tmpEl.textContent = pMessage;
31
+ tmpEl.style.display = 'block';
32
+ }
33
+
34
+ escapeHtml(pStr)
35
+ {
36
+ let tmpDiv = document.createElement('div');
37
+ tmpDiv.appendChild(document.createTextNode(pStr));
38
+ return tmpDiv.innerHTML;
39
+ }
40
+
41
+ // ================================================================
42
+ // Phase status indicators
43
+ // ================================================================
44
+
45
+ setSectionPhase(pSection, pState)
46
+ {
47
+ let tmpEl = document.getElementById('phase' + pSection);
48
+ if (!tmpEl) return;
49
+
50
+ tmpEl.className = 'accordion-phase';
51
+
52
+ if (pState === 'ok')
53
+ {
54
+ tmpEl.innerHTML = '✓';
55
+ tmpEl.classList.add('visible', 'accordion-phase-ok');
56
+ }
57
+ else if (pState === 'error')
58
+ {
59
+ tmpEl.innerHTML = '✗';
60
+ tmpEl.classList.add('visible', 'accordion-phase-error');
61
+ }
62
+ else if (pState === 'busy')
63
+ {
64
+ tmpEl.innerHTML = '<span class="phase-spinner"></span>';
65
+ tmpEl.classList.add('visible', 'accordion-phase-busy');
66
+ }
67
+ else
68
+ {
69
+ tmpEl.innerHTML = '';
70
+ }
71
+ }
72
+
73
+ // ================================================================
74
+ // Accordion Previews
75
+ // ================================================================
76
+
77
+ updateAllPreviews()
78
+ {
79
+ // Section 1 — Remote Session
80
+ let tmpServerURL = document.getElementById('serverURL');
81
+ if (!tmpServerURL) return;
82
+ tmpServerURL = tmpServerURL.value;
83
+ let tmpUserName = document.getElementById('userName').value;
84
+ if (tmpServerURL)
85
+ {
86
+ let tmpPreview1 = tmpServerURL;
87
+ if (tmpUserName) tmpPreview1 += ' as ' + tmpUserName;
88
+ document.getElementById('preview1').textContent = tmpPreview1;
89
+ }
90
+ else
91
+ {
92
+ document.getElementById('preview1').textContent = 'Configure remote server URL and credentials';
93
+ }
94
+
95
+ // Section 2 — Remote Schema
96
+ let tmpEntities = this.pict.AppData.ComprehensionLoader.FetchedEntities || [];
97
+ if (tmpEntities.length > 0)
98
+ {
99
+ document.getElementById('preview2').textContent = tmpEntities.length + ' entit' + (tmpEntities.length === 1 ? 'y' : 'ies') + ' discovered';
100
+ }
101
+ else
102
+ {
103
+ let tmpSchemaURL = document.getElementById('schemaURL').value;
104
+ if (tmpSchemaURL)
105
+ {
106
+ document.getElementById('preview2').textContent = 'Schema from ' + tmpSchemaURL;
107
+ }
108
+ else
109
+ {
110
+ document.getElementById('preview2').textContent = 'Fetch entity schema from the remote server';
111
+ }
112
+ }
113
+
114
+ // Section 3 — Comprehension Source
115
+ let tmpSourceMode = document.querySelector('input[name="comprehensionSourceMode"]:checked');
116
+ let tmpModeName = tmpSourceMode ? tmpSourceMode.value : 'url';
117
+ if (tmpModeName === 'url')
118
+ {
119
+ let tmpURL = document.getElementById('comprehensionURL').value;
120
+ if (tmpURL)
121
+ {
122
+ document.getElementById('preview3').textContent = 'URL: ' + tmpURL;
123
+ }
124
+ else
125
+ {
126
+ document.getElementById('preview3').textContent = 'Provide a comprehension JSON URL or upload files';
127
+ }
128
+ }
129
+ else
130
+ {
131
+ document.getElementById('preview3').textContent = 'File upload mode';
132
+ }
133
+
134
+ // Section 4 — Load
135
+ let tmpPreview4El = document.getElementById('preview4');
136
+ if (tmpPreview4El)
137
+ {
138
+ tmpPreview4El.textContent = 'Push comprehension data to the remote server';
139
+ }
140
+ }
141
+
142
+ initAccordionPreviews()
143
+ {
144
+ let tmpSelf = this;
145
+
146
+ let tmpPreviewFields = [
147
+ 'serverURL', 'userName',
148
+ 'schemaURL',
149
+ 'comprehensionURL'
150
+ ];
151
+
152
+ let tmpHandler = function() { tmpSelf.updateAllPreviews(); };
153
+
154
+ for (let i = 0; i < tmpPreviewFields.length; i++)
155
+ {
156
+ let tmpEl = document.getElementById(tmpPreviewFields[i]);
157
+ if (tmpEl)
158
+ {
159
+ tmpEl.addEventListener('input', tmpHandler);
160
+ tmpEl.addEventListener('change', tmpHandler);
161
+ }
162
+ }
163
+
164
+ document.querySelectorAll('input[name="comprehensionSourceMode"]').forEach(function(pEl)
165
+ {
166
+ pEl.addEventListener('change', tmpHandler);
167
+ });
168
+ }
169
+
170
+ // ================================================================
171
+ // LocalStorage Persistence
172
+ // ================================================================
173
+
174
+ saveField(pFieldId)
175
+ {
176
+ let tmpEl = document.getElementById(pFieldId);
177
+ if (tmpEl)
178
+ {
179
+ localStorage.setItem('comprehensionLoader_' + pFieldId, tmpEl.value);
180
+ }
181
+ }
182
+
183
+ restoreFields()
184
+ {
185
+ let tmpPersistFields = this.pict.AppData.ComprehensionLoader.PersistFields;
186
+ for (let i = 0; i < tmpPersistFields.length; i++)
187
+ {
188
+ let tmpId = tmpPersistFields[i];
189
+ let tmpSaved = localStorage.getItem('comprehensionLoader_' + tmpId);
190
+ if (tmpSaved !== null)
191
+ {
192
+ let tmpEl = document.getElementById(tmpId);
193
+ if (tmpEl) tmpEl.value = tmpSaved;
194
+ }
195
+ }
196
+
197
+ // Restore source mode radio
198
+ let tmpSourceMode = localStorage.getItem('comprehensionLoader_comprehensionSourceMode');
199
+ if (tmpSourceMode === 'file')
200
+ {
201
+ let tmpFileRadio = document.getElementById('sourceMode_file');
202
+ if (tmpFileRadio) tmpFileRadio.checked = true;
203
+ }
204
+ }
205
+
206
+ initPersistence()
207
+ {
208
+ let tmpSelf = this;
209
+ this.restoreFields();
210
+
211
+ let tmpPersistFields = this.pict.AppData.ComprehensionLoader.PersistFields;
212
+ for (let i = 0; i < tmpPersistFields.length; i++)
213
+ {
214
+ (function(pId)
215
+ {
216
+ let tmpEl = document.getElementById(pId);
217
+ if (tmpEl)
218
+ {
219
+ tmpEl.addEventListener('input', function() { tmpSelf.saveField(pId); });
220
+ tmpEl.addEventListener('change', function() { tmpSelf.saveField(pId); });
221
+ }
222
+ })(tmpPersistFields[i]);
223
+ }
224
+
225
+ // Persist source mode radio
226
+ document.querySelectorAll('input[name="comprehensionSourceMode"]').forEach(function(pEl)
227
+ {
228
+ pEl.addEventListener('change', function()
229
+ {
230
+ localStorage.setItem('comprehensionLoader_comprehensionSourceMode', this.value);
231
+ });
232
+ });
233
+
234
+ // Persist auto-process checkboxes
235
+ let tmpAutoIds = ['auto1', 'auto2', 'auto3', 'auto4'];
236
+ for (let a = 0; a < tmpAutoIds.length; a++)
237
+ {
238
+ (function(pId)
239
+ {
240
+ let tmpEl = document.getElementById(pId);
241
+ if (tmpEl)
242
+ {
243
+ let tmpSaved = localStorage.getItem('comprehensionLoader_' + pId);
244
+ if (tmpSaved !== null) tmpEl.checked = tmpSaved === 'true';
245
+ tmpEl.addEventListener('change', function()
246
+ {
247
+ localStorage.setItem('comprehensionLoader_' + pId, this.checked);
248
+ });
249
+ }
250
+ })(tmpAutoIds[a]);
251
+ }
252
+ }
253
+
254
+ // ================================================================
255
+ // Live Status Indicator
256
+ // ================================================================
257
+
258
+ startLiveStatusPolling()
259
+ {
260
+ let tmpAppData = this.pict.AppData.ComprehensionLoader;
261
+ if (tmpAppData.LiveStatusTimer) clearInterval(tmpAppData.LiveStatusTimer);
262
+ this.pollLiveStatus();
263
+ let tmpSelf = this;
264
+ tmpAppData.LiveStatusTimer = setInterval(function() { tmpSelf.pollLiveStatus(); }, 1500);
265
+ }
266
+
267
+ pollLiveStatus()
268
+ {
269
+ let tmpSelf = this;
270
+ this.api('GET', '/comprehension_load/load/live-status')
271
+ .then(function(pData)
272
+ {
273
+ tmpSelf.renderLiveStatus(pData);
274
+ })
275
+ .catch(function()
276
+ {
277
+ tmpSelf.renderLiveStatus({ Phase: 'disconnected', Message: 'Cannot reach server', TotalPushed: 0, TotalRecords: 0 });
278
+ });
279
+ }
280
+
281
+ renderLiveStatus(pData)
282
+ {
283
+ // Cache the live status data for the detail view
284
+ this.pict.AppData.ComprehensionLoader.LastLiveStatus = pData;
285
+
286
+ let tmpBar = document.getElementById('liveStatusBar');
287
+ let tmpMsg = document.getElementById('liveStatusMessage');
288
+ let tmpMeta = document.getElementById('liveStatusMeta');
289
+ let tmpProgressFill = document.getElementById('liveStatusProgressFill');
290
+ if (!tmpBar) return;
291
+
292
+ // Update phase class (preserve expanded class if present)
293
+ let tmpWasExpanded = tmpBar.classList.contains('expanded');
294
+ tmpBar.className = 'live-status-bar phase-' + (pData.Phase || 'idle');
295
+ if (tmpWasExpanded) tmpBar.classList.add('expanded');
296
+
297
+ // Update message
298
+ tmpMsg.textContent = pData.Message || 'Idle';
299
+
300
+ // Update meta info
301
+ let tmpMetaParts = [];
302
+ if (pData.Phase === 'loading' || pData.Phase === 'stopping')
303
+ {
304
+ if (pData.Elapsed)
305
+ {
306
+ tmpMetaParts.push('<span class="live-status-meta-item">\u23F1 ' + pData.Elapsed + '</span>');
307
+ }
308
+ if (pData.ETA)
309
+ {
310
+ tmpMetaParts.push('<span class="live-status-meta-item">~' + pData.ETA + ' remaining</span>');
311
+ }
312
+ if (pData.TotalEntities > 0)
313
+ {
314
+ tmpMetaParts.push('<span class="live-status-meta-item"><strong>' + pData.Completed + '</strong> / ' + pData.TotalEntities + ' entities</span>');
315
+ }
316
+ if (pData.TotalPushed > 0)
317
+ {
318
+ let tmpPushed = pData.TotalPushed.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
319
+ if (pData.TotalRecords > 0)
320
+ {
321
+ let tmpTotal = pData.TotalRecords.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
322
+ tmpMetaParts.push('<span class="live-status-meta-item"><strong>' + tmpPushed + '</strong> / ' + tmpTotal + ' records</span>');
323
+ }
324
+ else
325
+ {
326
+ tmpMetaParts.push('<span class="live-status-meta-item"><strong>' + tmpPushed + '</strong> records</span>');
327
+ }
328
+ }
329
+ if (pData.Errors > 0)
330
+ {
331
+ tmpMetaParts.push('<span class="live-status-meta-item" style="color:#dc3545"><strong>' + pData.Errors + '</strong> error' + (pData.Errors === 1 ? '' : 's') + '</span>');
332
+ }
333
+ }
334
+ else if (pData.Phase === 'complete')
335
+ {
336
+ if (pData.TotalPushed > 0)
337
+ {
338
+ let tmpPushed = pData.TotalPushed.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
339
+ tmpMetaParts.push('<span class="live-status-meta-item"><strong>' + tmpPushed + '</strong> records pushed</span>');
340
+ }
341
+ }
342
+ tmpMeta.innerHTML = tmpMetaParts.join('');
343
+
344
+ // Update progress bar
345
+ let tmpPct = 0;
346
+ if (pData.Phase === 'loading' && pData.TotalRecords > 0 && pData.TotalPushed > 0)
347
+ {
348
+ tmpPct = Math.min((pData.TotalPushed / pData.TotalRecords) * 100, 99.9);
349
+ }
350
+ else if (pData.Phase === 'loading' && pData.TotalEntities > 0)
351
+ {
352
+ tmpPct = (pData.Completed / pData.TotalEntities) * 100;
353
+ }
354
+ else if (pData.Phase === 'complete')
355
+ {
356
+ tmpPct = 100;
357
+ }
358
+ tmpProgressFill.style.width = Math.min(100, Math.round(tmpPct)) + '%';
359
+
360
+ // Auto-expand the detail view when load starts
361
+ if ((pData.Phase === 'loading' || pData.Phase === 'stopping') && !this.pict.AppData.ComprehensionLoader.StatusDetailExpanded)
362
+ {
363
+ let tmpLayoutView = this.pict.views['ComprehensionLoader-Layout'];
364
+ if (tmpLayoutView && typeof tmpLayoutView.toggleStatusDetail === 'function')
365
+ {
366
+ tmpLayoutView.toggleStatusDetail();
367
+ }
368
+ }
369
+
370
+ // If the detail view is expanded, re-render it with fresh data
371
+ if (this.pict.AppData.ComprehensionLoader.StatusDetailExpanded)
372
+ {
373
+ this.renderStatusDetail();
374
+ }
375
+
376
+ // Auto-fetch the load report when we detect a completed load but haven't loaded the report yet
377
+ if (pData.Phase === 'complete' && !this.pict.AppData.ComprehensionLoader.LastReport)
378
+ {
379
+ let tmpSelf = this;
380
+ this.api('GET', '/comprehension_load/load/report')
381
+ .then(function(pReportData)
382
+ {
383
+ if (pReportData && pReportData.ReportVersion)
384
+ {
385
+ tmpSelf.pict.AppData.ComprehensionLoader.LastReport = pReportData;
386
+ if (tmpSelf.pict.AppData.ComprehensionLoader.StatusDetailExpanded)
387
+ {
388
+ tmpSelf.renderStatusDetail();
389
+ }
390
+ }
391
+ })
392
+ .catch(function() { /* ignore fetch errors */ });
393
+ }
394
+ }
395
+
396
+ // ================================================================
397
+ // Status Detail Expansion
398
+ // ================================================================
399
+
400
+ onStatusDetailExpanded()
401
+ {
402
+ let tmpAppData = this.pict.AppData.ComprehensionLoader;
403
+ tmpAppData.StatusDetailExpanded = true;
404
+
405
+ // Immediate render from whatever data we have
406
+ this.renderStatusDetail();
407
+
408
+ // Start detail polling (poll /load/status for per-entity data)
409
+ if (tmpAppData.StatusDetailTimer) clearInterval(tmpAppData.StatusDetailTimer);
410
+ let tmpSelf = this;
411
+ tmpAppData.StatusDetailTimer = setInterval(function() { tmpSelf.pollStatusDetail(); }, 2000);
412
+ this.pollStatusDetail();
413
+ }
414
+
415
+ onStatusDetailCollapsed()
416
+ {
417
+ let tmpAppData = this.pict.AppData.ComprehensionLoader;
418
+ tmpAppData.StatusDetailExpanded = false;
419
+
420
+ if (tmpAppData.StatusDetailTimer)
421
+ {
422
+ clearInterval(tmpAppData.StatusDetailTimer);
423
+ tmpAppData.StatusDetailTimer = null;
424
+ }
425
+ }
426
+
427
+ pollStatusDetail()
428
+ {
429
+ let tmpSelf = this;
430
+ this.api('GET', '/comprehension_load/load/status')
431
+ .then(function(pData)
432
+ {
433
+ tmpSelf.pict.AppData.ComprehensionLoader.StatusDetailData = pData;
434
+ tmpSelf.renderStatusDetail();
435
+ })
436
+ .catch(function() { /* ignore poll errors */ });
437
+ }
438
+
439
+ renderStatusDetail()
440
+ {
441
+ let tmpContainer = document.getElementById('ComprehensionLoader-StatusDetail-Container');
442
+ if (!tmpContainer) return;
443
+
444
+ let tmpAppData = this.pict.AppData.ComprehensionLoader;
445
+ let tmpLiveStatus = tmpAppData.LastLiveStatus;
446
+ let tmpStatusData = tmpAppData.StatusDetailData;
447
+ let tmpReport = tmpAppData.LastReport;
448
+
449
+ // Determine data source: live during load, report after load
450
+ let tmpEntities = {};
451
+ let tmpThroughputSamples = [];
452
+ let tmpIsLive = false;
453
+
454
+ if (tmpLiveStatus && (tmpLiveStatus.Phase === 'loading' || tmpLiveStatus.Phase === 'stopping'))
455
+ {
456
+ tmpIsLive = true;
457
+ if (tmpStatusData && tmpStatusData.Entities) tmpEntities = tmpStatusData.Entities;
458
+ if (tmpLiveStatus.ThroughputSamples) tmpThroughputSamples = tmpLiveStatus.ThroughputSamples;
459
+ }
460
+ else if (tmpReport && tmpReport.ReportVersion)
461
+ {
462
+ // Build entities object from report
463
+ for (let i = 0; i < tmpReport.Entities.length; i++)
464
+ {
465
+ let tmpE = tmpReport.Entities[i];
466
+ tmpEntities[tmpE.Name] = tmpE;
467
+ }
468
+ tmpThroughputSamples = tmpReport.ThroughputSamples || [];
469
+ }
470
+ else if (tmpStatusData && tmpStatusData.Entities)
471
+ {
472
+ tmpEntities = tmpStatusData.Entities;
473
+ if (tmpLiveStatus && tmpLiveStatus.ThroughputSamples)
474
+ {
475
+ tmpThroughputSamples = tmpLiveStatus.ThroughputSamples;
476
+ }
477
+ }
478
+
479
+ // Categorize entities
480
+ let tmpRunning = [];
481
+ let tmpPending = [];
482
+ let tmpCompleted = [];
483
+ let tmpErrors = [];
484
+ let tmpEntityNames = Object.keys(tmpEntities);
485
+
486
+ for (let i = 0; i < tmpEntityNames.length; i++)
487
+ {
488
+ let tmpName = tmpEntityNames[i];
489
+ let tmpE = tmpEntities[tmpName];
490
+ if (tmpE.Status === 'Pushing')
491
+ {
492
+ tmpRunning.push({ Name: tmpName, Data: tmpE });
493
+ }
494
+ else if (tmpE.Status === 'Pending')
495
+ {
496
+ tmpPending.push(tmpName);
497
+ }
498
+ else if (tmpE.Status === 'Complete')
499
+ {
500
+ tmpCompleted.push({ Name: tmpName, Data: tmpE });
501
+ }
502
+ else if (tmpE.Status === 'Error')
503
+ {
504
+ tmpErrors.push({ Name: tmpName, Data: tmpE });
505
+ }
506
+ }
507
+
508
+ let tmpHtml = '';
509
+
510
+ // === Section 1: Running Operations ===
511
+ if (tmpRunning.length > 0 || tmpPending.length > 0)
512
+ {
513
+ tmpHtml += '<div class="status-detail-section">';
514
+ tmpHtml += '<div class="status-detail-section-title">Running</div>';
515
+ for (let i = 0; i < tmpRunning.length; i++)
516
+ {
517
+ let tmpOp = tmpRunning[i];
518
+ let tmpPct = tmpOp.Data.Total > 0 ? Math.round((tmpOp.Data.Pushed / tmpOp.Data.Total) * 100) : 0;
519
+ let tmpPushedFmt = this.formatNumber(tmpOp.Data.Pushed || 0);
520
+ let tmpTotalFmt = this.formatNumber(tmpOp.Data.Total || 0);
521
+ tmpHtml += '<div class="running-op-row">';
522
+ tmpHtml += ' <div class="running-op-name">' + this.escapeHtml(tmpOp.Name) + '</div>';
523
+ tmpHtml += ' <div class="running-op-bar"><div class="running-op-bar-fill" style="width:' + tmpPct + '%"></div></div>';
524
+ tmpHtml += ' <div class="running-op-count">' + tmpPushedFmt + ' / ' + tmpTotalFmt + ' (' + tmpPct + '%)</div>';
525
+ tmpHtml += '</div>';
526
+ }
527
+ if (tmpPending.length > 0)
528
+ {
529
+ tmpHtml += '<div class="running-op-pending">' + tmpPending.length + ' entit' + (tmpPending.length === 1 ? 'y' : 'ies') + ' waiting</div>';
530
+ }
531
+ tmpHtml += '</div>';
532
+ }
533
+
534
+ // === Section 2: Completed Operations ===
535
+ if (tmpCompleted.length > 0)
536
+ {
537
+ tmpHtml += '<div class="status-detail-section">';
538
+ tmpHtml += '<div class="status-detail-section-title">Completed (' + tmpCompleted.length + ')</div>';
539
+ for (let i = 0; i < tmpCompleted.length; i++)
540
+ {
541
+ let tmpOp = tmpCompleted[i];
542
+ let tmpPushedFmt = this.formatNumber(tmpOp.Data.Pushed || tmpOp.Data.Total || 0);
543
+ tmpHtml += '<div class="completed-op-row">';
544
+ tmpHtml += '<div class="completed-op-header">';
545
+ tmpHtml += ' <span class="completed-op-checkmark">\u2713</span>';
546
+ tmpHtml += ' <span class="completed-op-name">' + this.escapeHtml(tmpOp.Name) + '</span>';
547
+ tmpHtml += ' <span class="completed-op-stats">' + tmpPushedFmt + ' records</span>';
548
+ tmpHtml += '</div>';
549
+ tmpHtml += '</div>';
550
+ }
551
+ tmpHtml += '</div>';
552
+ }
553
+
554
+ // === Section 3: Errors ===
555
+ if (tmpErrors.length > 0)
556
+ {
557
+ tmpHtml += '<div class="status-detail-section">';
558
+ tmpHtml += '<div class="status-detail-section-title">Errors (' + tmpErrors.length + ')</div>';
559
+ for (let i = 0; i < tmpErrors.length; i++)
560
+ {
561
+ let tmpOp = tmpErrors[i];
562
+ let tmpPushedFmt = this.formatNumber(tmpOp.Data.Pushed || 0);
563
+ let tmpTotalFmt = this.formatNumber(tmpOp.Data.Total || 0);
564
+ tmpHtml += '<div class="error-op-row">';
565
+ tmpHtml += '<div class="error-op-header">';
566
+ tmpHtml += ' <span style="color:#dc3545">\u2717</span>';
567
+ tmpHtml += ' <span class="error-op-name">' + this.escapeHtml(tmpOp.Name) + '</span>';
568
+ tmpHtml += ' <span class="error-op-status">' + tmpPushedFmt + ' / ' + tmpTotalFmt + '</span>';
569
+ tmpHtml += '</div>';
570
+ if (tmpOp.Data.ErrorMessage)
571
+ {
572
+ tmpHtml += '<div class="error-op-message">' + this.escapeHtml(tmpOp.Data.ErrorMessage) + '</div>';
573
+ }
574
+ tmpHtml += '</div>';
575
+ }
576
+ tmpHtml += '</div>';
577
+ }
578
+
579
+ if (tmpHtml === '')
580
+ {
581
+ if (tmpIsLive)
582
+ {
583
+ tmpHtml = '<div style="font-size:0.9em; color:#888; padding:8px 0">Load in progress, waiting for entity data\u2026</div>';
584
+ }
585
+ else
586
+ {
587
+ tmpHtml = '<div style="font-size:0.9em; color:#888; padding:8px 0">No load data available. Run a load to see operation details here.</div>';
588
+ }
589
+ }
590
+
591
+ tmpContainer.innerHTML = tmpHtml;
592
+
593
+ // Update the throughput histogram
594
+ this.updateThroughputHistogram(tmpThroughputSamples);
595
+ }
596
+
597
+ updateThroughputHistogram(pSamples)
598
+ {
599
+ let tmpHistContainer = document.getElementById('ComprehensionLoader-Throughput-Histogram');
600
+ if (!tmpHistContainer) return;
601
+
602
+ if (!pSamples || pSamples.length < 2)
603
+ {
604
+ tmpHistContainer.style.display = 'none';
605
+ return;
606
+ }
607
+
608
+ // Compute raw deltas per interval
609
+ let tmpRawDeltas = [];
610
+ for (let i = 1; i < pSamples.length; i++)
611
+ {
612
+ let tmpDelta = pSamples[i].pushed - pSamples[i - 1].pushed;
613
+ if (tmpDelta < 0) tmpDelta = 0;
614
+ tmpRawDeltas.push({ delta: tmpDelta, t: pSamples[i].t });
615
+ }
616
+
617
+ // Downsample if too many bars
618
+ let tmpContainerWidth = tmpHistContainer.clientWidth || 800;
619
+ let tmpMaxBars = Math.max(20, Math.floor(tmpContainerWidth / 6));
620
+ let tmpAggregated = tmpRawDeltas;
621
+
622
+ if (tmpRawDeltas.length > tmpMaxBars)
623
+ {
624
+ let tmpBucketSize = Math.ceil(tmpRawDeltas.length / tmpMaxBars);
625
+ tmpAggregated = [];
626
+ for (let i = 0; i < tmpRawDeltas.length; i += tmpBucketSize)
627
+ {
628
+ let tmpSum = 0;
629
+ let tmpLastT = 0;
630
+ for (let j = i; j < Math.min(i + tmpBucketSize, tmpRawDeltas.length); j++)
631
+ {
632
+ tmpSum += tmpRawDeltas[j].delta;
633
+ tmpLastT = tmpRawDeltas[j].t;
634
+ }
635
+ tmpAggregated.push({ delta: tmpSum, t: tmpLastT });
636
+ }
637
+ }
638
+
639
+ // Check for data
640
+ let tmpHasData = false;
641
+ for (let i = 0; i < tmpAggregated.length; i++)
642
+ {
643
+ if (tmpAggregated[i].delta > 0) { tmpHasData = true; break; }
644
+ }
645
+ if (!tmpHasData)
646
+ {
647
+ tmpHistContainer.style.display = 'none';
648
+ return;
649
+ }
650
+
651
+ // Build bins
652
+ let tmpStartT = pSamples[0].t;
653
+ let tmpBins = [];
654
+ for (let i = 0; i < tmpAggregated.length; i++)
655
+ {
656
+ let tmpElapsedSec = Math.round((tmpAggregated[i].t - tmpStartT) / 1000);
657
+ tmpBins.push({
658
+ Label: this.formatElapsed(tmpElapsedSec),
659
+ Value: tmpAggregated[i].delta
660
+ });
661
+ }
662
+
663
+ // Update the histogram view
664
+ tmpHistContainer.style.display = '';
665
+ let tmpHistView = this.pict.views['ComprehensionLoader-StatusHistogram'];
666
+ if (tmpHistView)
667
+ {
668
+ tmpHistView.setBins(tmpBins);
669
+ tmpHistView.renderHistogram();
670
+ }
671
+ }
672
+
673
+ formatElapsed(pSec)
674
+ {
675
+ if (pSec < 60) return pSec + 's';
676
+ if (pSec < 3600)
677
+ {
678
+ let tmpM = Math.floor(pSec / 60);
679
+ let tmpS = pSec % 60;
680
+ return tmpM + ':' + (tmpS < 10 ? '0' : '') + tmpS;
681
+ }
682
+ let tmpH = Math.floor(pSec / 3600);
683
+ let tmpM = Math.floor((pSec % 3600) / 60);
684
+ return tmpH + 'h' + (tmpM < 10 ? '0' : '') + tmpM;
685
+ }
686
+
687
+ formatCompact(pNum)
688
+ {
689
+ if (pNum >= 1000000) return (pNum / 1000000).toFixed(1) + 'M';
690
+ if (pNum >= 10000) return (pNum / 1000).toFixed(0) + 'K';
691
+ if (pNum >= 1000) return (pNum / 1000).toFixed(1) + 'K';
692
+ return pNum.toString();
693
+ }
694
+
695
+ formatNumber(pNum)
696
+ {
697
+ return pNum.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
698
+ }
699
+
700
+ // ================================================================
701
+ // Auto-Process
702
+ // ================================================================
703
+
704
+ initAutoProcess()
705
+ {
706
+ let tmpSelf = this;
707
+ this.api('GET', '/comprehension_load/load/live-status')
708
+ .then(function(pData)
709
+ {
710
+ if (pData.Phase === 'loading' || pData.Phase === 'stopping')
711
+ {
712
+ tmpSelf.pict.AppData.ComprehensionLoader.ServerBusyAtLoad = true;
713
+ tmpSelf.setSectionPhase(4, 'busy');
714
+ tmpSelf.pict.views['ComprehensionLoader-Load'].startPolling();
715
+ return;
716
+ }
717
+ tmpSelf.runAutoProcessChain();
718
+ })
719
+ .catch(function()
720
+ {
721
+ // Server unreachable — don't auto-process
722
+ });
723
+ }
724
+
725
+ runAutoProcessChain()
726
+ {
727
+ let tmpSelf = this;
728
+ let tmpDelay = 0;
729
+ let tmpStepDelay = 2000;
730
+
731
+ if (document.getElementById('auto1') && document.getElementById('auto1').checked)
732
+ {
733
+ setTimeout(function() { tmpSelf.pict.views['ComprehensionLoader-Session'].goAction(); }, tmpDelay);
734
+ tmpDelay += tmpStepDelay + 1500;
735
+ }
736
+ if (document.getElementById('auto2') && document.getElementById('auto2').checked)
737
+ {
738
+ setTimeout(function() { tmpSelf.pict.views['ComprehensionLoader-Schema'].fetchSchema(); }, tmpDelay);
739
+ tmpDelay += tmpStepDelay;
740
+ }
741
+ if (document.getElementById('auto3') && document.getElementById('auto3').checked)
742
+ {
743
+ setTimeout(function() { tmpSelf.pict.views['ComprehensionLoader-Source'].goAction(); }, tmpDelay);
744
+ tmpDelay += tmpStepDelay;
745
+ }
746
+ if (document.getElementById('auto4') && document.getElementById('auto4').checked)
747
+ {
748
+ setTimeout(function() { tmpSelf.pict.views['ComprehensionLoader-Load'].startLoad(); }, tmpDelay);
749
+ }
750
+ }
751
+ }
752
+
753
+ module.exports = ComprehensionLoaderProvider;
754
+
755
+ module.exports.default_configuration =
756
+ {
757
+ ProviderIdentifier: 'ComprehensionLoader',
758
+ AutoInitialize: true,
759
+ AutoInitializeOrdinal: 0
760
+ };