retold-data-service 2.0.18 → 2.0.19
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.
- package/package.json +2 -2
- package/source/services/Retold-Data-Service-MeadowEndpoints.js +12 -2
- package/source/services/data-cloner/DataCloner-Command-Schema.js +4 -3
- package/source/services/data-cloner/DataCloner-Command-Sync.js +16 -3
- package/source/services/data-cloner/DataCloner-ProviderRegistry.js +1 -1
- package/source/services/data-cloner/Retold-Data-Service-DataCloner.js +47 -7
- package/source/services/data-cloner/pict-app/Pict-Application-DataCloner.js +2 -1
- package/source/services/data-cloner/pict-app/providers/Pict-Provider-DataCloner.js +86 -2
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Layout.js +28 -0
- package/source/services/data-cloner/web/data-cloner.js +95 -3
- package/source/services/data-cloner/web/data-cloner.js.map +1 -1
- package/source/services/data-cloner/web/data-cloner.min.js +1 -1
- package/source/services/data-cloner/web/data-cloner.min.js.map +1 -1
- package/test/integration-report.json +35 -116
- package/test/run-integration-tests.js +52 -12
- package/test/run-integration-tests.sh +221 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "retold-data-service",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.19",
|
|
4
4
|
"description": "Serve up a whole model!",
|
|
5
5
|
"main": "source/Retold-Data-Service.js",
|
|
6
6
|
"bin": {
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"meadow": "^2.0.33",
|
|
73
73
|
"meadow-connection-mysql": "^1.0.14",
|
|
74
74
|
"meadow-endpoints": "^4.0.14",
|
|
75
|
-
"meadow-integration": "^1.0.
|
|
75
|
+
"meadow-integration": "^1.0.16",
|
|
76
76
|
"meadow-migrationmanager": "^0.0.4",
|
|
77
77
|
"orator": "^6.0.4",
|
|
78
78
|
"orator-http-proxy": "^1.0.5",
|
|
@@ -137,6 +137,8 @@ class RetoldDataServiceMeadowEndpoints extends libFableServiceProviderBase
|
|
|
137
137
|
{
|
|
138
138
|
let tmpDALEntityName = tmpEntityList[i];
|
|
139
139
|
|
|
140
|
+
let tmpRoutesAlreadyConnected = this._MeadowEndpoints.hasOwnProperty(tmpDALEntityName);
|
|
141
|
+
|
|
140
142
|
if (this._DAL.hasOwnProperty(tmpDALEntityName))
|
|
141
143
|
{
|
|
142
144
|
this.fable.log.warn(`Entity [${tmpDALEntityName}] already exists in the DAL (from another model); overwriting.`);
|
|
@@ -152,8 +154,16 @@ class RetoldDataServiceMeadowEndpoints extends libFableServiceProviderBase
|
|
|
152
154
|
this._DAL[tmpDALEntityName].setProvider(tmpStorageProvider);
|
|
153
155
|
this.fable.log.info(`...initializing the ${tmpDALEntityName} Meadow Endpoints`);
|
|
154
156
|
this._MeadowEndpoints[tmpDALEntityName] = libMeadowEndpoints.new(this._DAL[tmpDALEntityName]);
|
|
155
|
-
|
|
156
|
-
|
|
157
|
+
|
|
158
|
+
if (!tmpRoutesAlreadyConnected)
|
|
159
|
+
{
|
|
160
|
+
this.fable.log.info(`...mapping the ${tmpDALEntityName} Meadow Endpoints to Orator`);
|
|
161
|
+
this._MeadowEndpoints[tmpDALEntityName].connectRoutes(this.fable.OratorServiceServer);
|
|
162
|
+
}
|
|
163
|
+
else
|
|
164
|
+
{
|
|
165
|
+
this.fable.log.info(`...routes for ${tmpDALEntityName} already registered; skipping connectRoutes.`);
|
|
166
|
+
}
|
|
157
167
|
}
|
|
158
168
|
catch (pError)
|
|
159
169
|
{
|
|
@@ -274,10 +274,11 @@ module.exports = (pDataClonerService, pOratorServiceServer) =>
|
|
|
274
274
|
// Extract the entity name from the URL for error tracking
|
|
275
275
|
let tmpEntityHint = pURL.split('/')[0].replace(/s$/, '');
|
|
276
276
|
|
|
277
|
-
// Use the longer timeout for
|
|
278
|
-
|
|
277
|
+
// Use the longer timeout for Max and Count queries (which can be
|
|
278
|
+
// slow on large tables with millions of rows)
|
|
279
|
+
let tmpIsHeavyQuery = (pURL.indexOf('/Max/') > -1) || (pURL.match(/\/Count(\/|$)/) !== null);
|
|
279
280
|
let tmpPreviousTimeout = libHttps.globalAgent.options.timeout;
|
|
280
|
-
libHttps.globalAgent.options.timeout =
|
|
281
|
+
libHttps.globalAgent.options.timeout = tmpIsHeavyQuery
|
|
281
282
|
? tmpFable.MeadowCloneRestClient.maxRequestTimeout
|
|
282
283
|
: tmpFable.MeadowCloneRestClient.requestTimeout;
|
|
283
284
|
|
|
@@ -207,6 +207,13 @@ module.exports = (pDataClonerService, pOratorServiceServer) =>
|
|
|
207
207
|
let tmpSnap = { Status: tmpP.Status, Total: tmpP.Total || 0, Synced: tmpP.Synced || 0, Errors: tmpP.Errors || 0 };
|
|
208
208
|
if (tmpP.ErrorMessage) tmpSnap.ErrorMessage = tmpP.ErrorMessage;
|
|
209
209
|
|
|
210
|
+
// Include per-record breakdown fields if available (set after entity sync completes)
|
|
211
|
+
if (tmpP.hasOwnProperty('New')) tmpSnap.New = tmpP.New;
|
|
212
|
+
if (tmpP.hasOwnProperty('Updated')) tmpSnap.Updated = tmpP.Updated;
|
|
213
|
+
if (tmpP.hasOwnProperty('Unchanged')) tmpSnap.Unchanged = tmpP.Unchanged;
|
|
214
|
+
if (tmpP.hasOwnProperty('Deleted')) tmpSnap.Deleted = tmpP.Deleted;
|
|
215
|
+
if (tmpP.hasOwnProperty('ServerTotal')) tmpSnap.ServerTotal = tmpP.ServerTotal;
|
|
216
|
+
|
|
210
217
|
if ((tmpP.Status === 'Syncing' || tmpP.Status === 'Pending') && tmpFable.MeadowSync && tmpFable.MeadowSync.MeadowSyncEntities)
|
|
211
218
|
{
|
|
212
219
|
let tmpSyncEntity = tmpFable.MeadowSync.MeadowSyncEntities[tmpName];
|
|
@@ -300,9 +307,15 @@ module.exports = (pDataClonerService, pOratorServiceServer) =>
|
|
|
300
307
|
// Check for pre-count phase
|
|
301
308
|
if (tmpCloneState.SyncPhase === 'counting')
|
|
302
309
|
{
|
|
303
|
-
let tmpPC = tmpCloneState.PreCountProgress || { Counted: 0, TotalTables: 0 };
|
|
310
|
+
let tmpPC = tmpCloneState.PreCountProgress || { Counted: 0, TotalTables: 0, Tables: [] };
|
|
304
311
|
tmpMessage = `Analyzing tables: counted ${tmpPC.Counted} / ${tmpPC.TotalTables}...`;
|
|
305
312
|
|
|
313
|
+
let tmpCountElapsed = null;
|
|
314
|
+
if (tmpPC.StartTime)
|
|
315
|
+
{
|
|
316
|
+
tmpCountElapsed = fFormatDuration(Date.now() - tmpPC.StartTime);
|
|
317
|
+
}
|
|
318
|
+
|
|
306
319
|
pResponse.send(200,
|
|
307
320
|
{
|
|
308
321
|
Phase: tmpPhase,
|
|
@@ -315,10 +328,10 @@ module.exports = (pDataClonerService, pOratorServiceServer) =>
|
|
|
315
328
|
TotalTables: tmpPC.TotalTables,
|
|
316
329
|
TotalSynced: 0,
|
|
317
330
|
TotalRecords: 0,
|
|
318
|
-
Elapsed:
|
|
331
|
+
Elapsed: tmpCountElapsed,
|
|
319
332
|
SyncMode: tmpCloneState.SyncMode,
|
|
320
333
|
ETA: null,
|
|
321
|
-
PreCountGrandTotal: 0,
|
|
334
|
+
PreCountGrandTotal: tmpCloneState.PreCountGrandTotal || 0,
|
|
322
335
|
PreCountProgress: tmpPC,
|
|
323
336
|
ThroughputSamples: []
|
|
324
337
|
});
|
|
@@ -10,7 +10,7 @@ const _ProviderRegistry = {
|
|
|
10
10
|
SQLite: { moduleName: 'meadow-connection-sqlite', serviceName: 'MeadowSQLiteProvider', configKey: 'SQLite' },
|
|
11
11
|
MySQL: { moduleName: 'meadow-connection-mysql', serviceName: 'MeadowMySQLProvider', configKey: 'MySQL' },
|
|
12
12
|
MSSQL: { moduleName: 'meadow-connection-mssql', serviceName: 'MeadowMSSQLProvider', configKey: 'MSSQL' },
|
|
13
|
-
PostgreSQL: { moduleName: 'meadow-connection-postgresql', serviceName: '
|
|
13
|
+
PostgreSQL: { moduleName: 'meadow-connection-postgresql', serviceName: 'MeadowPostgreSQLProvider', configKey: 'PostgreSQL' },
|
|
14
14
|
Solr: { moduleName: 'meadow-connection-solr', serviceName: 'MeadowConnectionSolr', configKey: 'Solr' },
|
|
15
15
|
MongoDB: { moduleName: 'meadow-connection-mongodb', serviceName: 'MeadowConnectionMongoDB', configKey: 'MongoDB' },
|
|
16
16
|
RocksDB: { moduleName: 'meadow-connection-rocksdb', serviceName: 'MeadowConnectionRocksDB', configKey: 'RocksDB' },
|
|
@@ -642,7 +642,12 @@ class RetoldDataServiceDataCloner extends libFableServiceProviderBase
|
|
|
642
642
|
preCountTables(pTables, fCallback)
|
|
643
643
|
{
|
|
644
644
|
this._cloneState.SyncPhase = 'counting';
|
|
645
|
-
this._cloneState.PreCountProgress = {
|
|
645
|
+
this._cloneState.PreCountProgress = {
|
|
646
|
+
Counted: 0,
|
|
647
|
+
TotalTables: pTables.length,
|
|
648
|
+
StartTime: Date.now(),
|
|
649
|
+
Tables: []
|
|
650
|
+
};
|
|
646
651
|
this._cloneState.PreCountGrandTotal = 0;
|
|
647
652
|
|
|
648
653
|
this.fable.log.info(`Data Cloner: Pre-counting records for ${pTables.length} tables...`);
|
|
@@ -656,6 +661,7 @@ class RetoldDataServiceDataCloner extends libFableServiceProviderBase
|
|
|
656
661
|
return fNext();
|
|
657
662
|
}
|
|
658
663
|
|
|
664
|
+
let tmpTableStartTime = Date.now();
|
|
659
665
|
let tmpCountURL = `${pTableName}s/Count`;
|
|
660
666
|
this.fable.MeadowCloneRestClient.getJSON(tmpCountURL,
|
|
661
667
|
(pError, pResponse, pBody) =>
|
|
@@ -669,7 +675,14 @@ class RetoldDataServiceDataCloner extends libFableServiceProviderBase
|
|
|
669
675
|
{
|
|
670
676
|
this._cloneState.SyncProgress[pTableName].Total = tmpCount;
|
|
671
677
|
}
|
|
678
|
+
let tmpElapsedMs = Date.now() - tmpTableStartTime;
|
|
672
679
|
this._cloneState.PreCountProgress.Counted++;
|
|
680
|
+
this._cloneState.PreCountProgress.Tables.push({
|
|
681
|
+
Name: pTableName,
|
|
682
|
+
Count: tmpCount,
|
|
683
|
+
ElapsedMs: tmpElapsedMs,
|
|
684
|
+
Error: pError ? true : false
|
|
685
|
+
});
|
|
673
686
|
this._cloneState.PreCountGrandTotal += tmpCount;
|
|
674
687
|
return fNext();
|
|
675
688
|
});
|
|
@@ -866,15 +879,42 @@ class RetoldDataServiceDataCloner extends libFableServiceProviderBase
|
|
|
866
879
|
if (this._cloneState.SyncLogFileLogger)
|
|
867
880
|
{
|
|
868
881
|
let tmpLogPath = this._cloneState.SyncLogFilePath || '';
|
|
869
|
-
this._cloneState.SyncLogFileLogger
|
|
882
|
+
let tmpLogger = this._cloneState.SyncLogFileLogger;
|
|
883
|
+
|
|
884
|
+
// Log before closing so the message is captured in the file
|
|
885
|
+
this.fable.log.info(`Data Cloner: Log file closing — ${tmpLogPath}`);
|
|
886
|
+
|
|
887
|
+
// Remove the logger from fable.log stream arrays so no writes
|
|
888
|
+
// are dispatched to it after the underlying stream is closed.
|
|
889
|
+
let tmpStreamArrays = [
|
|
890
|
+
'logStreams', 'logStreamsTrace', 'logStreamsDebug',
|
|
891
|
+
'logStreamsInfo', 'logStreamsWarn', 'logStreamsError', 'logStreamsFatal'
|
|
892
|
+
];
|
|
893
|
+
for (let s = 0; s < tmpStreamArrays.length; s++)
|
|
870
894
|
{
|
|
871
|
-
this.
|
|
895
|
+
let tmpArr = this.fable.log[tmpStreamArrays[s]];
|
|
896
|
+
if (Array.isArray(tmpArr))
|
|
872
897
|
{
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
898
|
+
let tmpIdx = tmpArr.indexOf(tmpLogger);
|
|
899
|
+
if (tmpIdx > -1) tmpArr.splice(tmpIdx, 1);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
876
903
|
this._cloneState.SyncLogFileLogger = null;
|
|
877
904
|
this._cloneState.SyncLogFilePath = null;
|
|
905
|
+
|
|
906
|
+
// Now flush and close safely — no more writes can reach this stream
|
|
907
|
+
try
|
|
908
|
+
{
|
|
909
|
+
tmpLogger.flushBufferToLogFile(() =>
|
|
910
|
+
{
|
|
911
|
+
tmpLogger.closeWriter(() => {});
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
catch (pCloseErr)
|
|
915
|
+
{
|
|
916
|
+
// Ignore close errors
|
|
917
|
+
}
|
|
878
918
|
}
|
|
879
919
|
|
|
880
920
|
return;
|
|
@@ -915,7 +955,7 @@ class RetoldDataServiceDataCloner extends libFableServiceProviderBase
|
|
|
915
955
|
{
|
|
916
956
|
let tmpResults = tmpSyncEntity.syncResults;
|
|
917
957
|
tmpProgress.New = tmpResults.Created || 0;
|
|
918
|
-
tmpProgress.Updated = 0;
|
|
958
|
+
tmpProgress.Updated = tmpResults.Updated || 0;
|
|
919
959
|
tmpProgress.Unchanged = tmpResults.LocalRecordCount || 0;
|
|
920
960
|
tmpProgress.Deleted = tmpResults.Deleted || 0;
|
|
921
961
|
tmpProgress.ServerTotal = tmpResults.ServerRecordCount || 0;
|
|
@@ -73,7 +73,8 @@ class DataClonerApplication extends libPictApplication
|
|
|
73
73
|
'solrHost', 'solrPort', 'solrCore', 'solrPath',
|
|
74
74
|
'mongodbHost', 'mongodbPort', 'mongodbUser', 'mongodbPassword', 'mongodbDatabase', 'mongodbConnectionLimit',
|
|
75
75
|
'rocksdbFolder',
|
|
76
|
-
'bibliographFolder'
|
|
76
|
+
'bibliographFolder',
|
|
77
|
+
'syncMaxRecords'
|
|
77
78
|
]
|
|
78
79
|
};
|
|
79
80
|
|
|
@@ -447,7 +447,10 @@ class DataClonerProvider extends libPictProvider
|
|
|
447
447
|
}
|
|
448
448
|
if (pData.PreCountProgress && pData.PreCountProgress.Counted < pData.PreCountProgress.TotalTables)
|
|
449
449
|
{
|
|
450
|
-
|
|
450
|
+
let tmpCountedSoFar = pData.PreCountGrandTotal > 0
|
|
451
|
+
? ' (' + pData.PreCountGrandTotal.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') + ' records found)'
|
|
452
|
+
: '';
|
|
453
|
+
tmpMetaParts.push('<span class="live-status-meta-item">counting: ' + pData.PreCountProgress.Counted + ' / ' + pData.PreCountProgress.TotalTables + ' tables' + tmpCountedSoFar + '</span>');
|
|
451
454
|
}
|
|
452
455
|
if (pData.Errors > 0)
|
|
453
456
|
{
|
|
@@ -466,7 +469,12 @@ class DataClonerProvider extends libPictProvider
|
|
|
466
469
|
|
|
467
470
|
// Update progress bar
|
|
468
471
|
let tmpPct = 0;
|
|
469
|
-
if (pData.Phase === 'syncing' && pData.
|
|
472
|
+
if (pData.Phase === 'syncing' && pData.PreCountProgress && pData.PreCountProgress.Counted < pData.PreCountProgress.TotalTables)
|
|
473
|
+
{
|
|
474
|
+
// During counting phase, show table counting progress
|
|
475
|
+
tmpPct = Math.min((pData.PreCountProgress.Counted / pData.PreCountProgress.TotalTables) * 100, 99);
|
|
476
|
+
}
|
|
477
|
+
else if (pData.Phase === 'syncing' && pData.PreCountGrandTotal > 0 && pData.TotalSynced > 0)
|
|
470
478
|
{
|
|
471
479
|
tmpPct = Math.min((pData.TotalSynced / pData.PreCountGrandTotal) * 100, 99.9);
|
|
472
480
|
}
|
|
@@ -489,6 +497,16 @@ class DataClonerProvider extends libPictProvider
|
|
|
489
497
|
}
|
|
490
498
|
tmpProgressFill.style.width = Math.min(100, Math.round(tmpPct)) + '%';
|
|
491
499
|
|
|
500
|
+
// Auto-expand the detail view when sync starts so users see counting progress
|
|
501
|
+
if ((pData.Phase === 'syncing' || pData.Phase === 'stopping') && !this.pict.AppData.DataCloner.StatusDetailExpanded)
|
|
502
|
+
{
|
|
503
|
+
let tmpLayoutView = this.pict.views['DataCloner-Layout'];
|
|
504
|
+
if (tmpLayoutView && typeof tmpLayoutView.toggleStatusDetail === 'function')
|
|
505
|
+
{
|
|
506
|
+
tmpLayoutView.toggleStatusDetail();
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
492
510
|
// If the detail view is expanded, re-render it with fresh data
|
|
493
511
|
if (this.pict.AppData.DataCloner.StatusDetailExpanded)
|
|
494
512
|
{
|
|
@@ -558,6 +576,59 @@ class DataClonerProvider extends libPictProvider
|
|
|
558
576
|
.catch(function() { /* ignore poll errors */ });
|
|
559
577
|
}
|
|
560
578
|
|
|
579
|
+
renderCountingPhaseDetail(pContainer, pPreCountProgress)
|
|
580
|
+
{
|
|
581
|
+
let tmpTables = pPreCountProgress.Tables || [];
|
|
582
|
+
let tmpCounted = pPreCountProgress.Counted || 0;
|
|
583
|
+
let tmpTotal = pPreCountProgress.TotalTables || 0;
|
|
584
|
+
let tmpRunningTotal = 0;
|
|
585
|
+
|
|
586
|
+
let tmpHtml = '<div class="status-detail-section">';
|
|
587
|
+
tmpHtml += '<div class="status-detail-section-title">Counting Records (' + tmpCounted + ' / ' + tmpTotal + ' tables)</div>';
|
|
588
|
+
|
|
589
|
+
if (tmpTables.length > 0)
|
|
590
|
+
{
|
|
591
|
+
tmpHtml += '<table class="precount-table">';
|
|
592
|
+
tmpHtml += '<thead><tr><th>Table</th><th style="text-align:right">Records</th><th style="text-align:right">Time</th></tr></thead>';
|
|
593
|
+
tmpHtml += '<tbody>';
|
|
594
|
+
for (let i = 0; i < tmpTables.length; i++)
|
|
595
|
+
{
|
|
596
|
+
let tmpT = tmpTables[i];
|
|
597
|
+
tmpRunningTotal += tmpT.Count;
|
|
598
|
+
let tmpCountFmt = this.formatNumber(tmpT.Count);
|
|
599
|
+
let tmpTimeFmt = tmpT.ElapsedMs < 1000
|
|
600
|
+
? tmpT.ElapsedMs + 'ms'
|
|
601
|
+
: (tmpT.ElapsedMs / 1000).toFixed(1) + 's';
|
|
602
|
+
let tmpRowClass = tmpT.Error ? ' class="precount-error"' : '';
|
|
603
|
+
tmpHtml += '<tr' + tmpRowClass + '>';
|
|
604
|
+
tmpHtml += '<td>' + this.escapeHtml(tmpT.Name) + '</td>';
|
|
605
|
+
tmpHtml += '<td style="text-align:right; font-variant-numeric:tabular-nums">' + tmpCountFmt + '</td>';
|
|
606
|
+
tmpHtml += '<td style="text-align:right; font-variant-numeric:tabular-nums; color:#888">' + tmpTimeFmt + '</td>';
|
|
607
|
+
tmpHtml += '</tr>';
|
|
608
|
+
}
|
|
609
|
+
tmpHtml += '</tbody>';
|
|
610
|
+
tmpHtml += '<tfoot><tr>';
|
|
611
|
+
tmpHtml += '<td><strong>Total</strong></td>';
|
|
612
|
+
tmpHtml += '<td style="text-align:right; font-variant-numeric:tabular-nums"><strong>' + this.formatNumber(tmpRunningTotal) + '</strong></td>';
|
|
613
|
+
tmpHtml += '<td></td>';
|
|
614
|
+
tmpHtml += '</tr></tfoot>';
|
|
615
|
+
tmpHtml += '</table>';
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Show pending indicator for remaining tables
|
|
619
|
+
let tmpRemaining = tmpTotal - tmpCounted;
|
|
620
|
+
if (tmpRemaining > 0)
|
|
621
|
+
{
|
|
622
|
+
tmpHtml += '<div class="precount-pending">';
|
|
623
|
+
tmpHtml += '<span class="precount-spinner"></span> ';
|
|
624
|
+
tmpHtml += tmpRemaining + ' table' + (tmpRemaining === 1 ? '' : 's') + ' remaining…';
|
|
625
|
+
tmpHtml += '</div>';
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
tmpHtml += '</div>';
|
|
629
|
+
pContainer.innerHTML = tmpHtml;
|
|
630
|
+
}
|
|
631
|
+
|
|
561
632
|
renderStatusDetail()
|
|
562
633
|
{
|
|
563
634
|
let tmpContainer = document.getElementById('DataCloner-StatusDetail-Container');
|
|
@@ -568,6 +639,19 @@ class DataClonerProvider extends libPictProvider
|
|
|
568
639
|
let tmpStatusData = tmpAppData.StatusDetailData;
|
|
569
640
|
let tmpReport = tmpAppData.LastReport;
|
|
570
641
|
|
|
642
|
+
// During the counting phase, show per-table counts as they arrive
|
|
643
|
+
if (tmpLiveStatus && tmpLiveStatus.PreCountProgress
|
|
644
|
+
&& tmpLiveStatus.PreCountProgress.Tables
|
|
645
|
+
&& tmpLiveStatus.Phase === 'syncing'
|
|
646
|
+
&& tmpLiveStatus.PreCountProgress.Counted < tmpLiveStatus.PreCountProgress.TotalTables)
|
|
647
|
+
{
|
|
648
|
+
this.renderCountingPhaseDetail(tmpContainer, tmpLiveStatus.PreCountProgress);
|
|
649
|
+
// Hide histogram during counting
|
|
650
|
+
let tmpHistContainer = document.getElementById('DataCloner-Throughput-Histogram');
|
|
651
|
+
if (tmpHistContainer) tmpHistContainer.style.display = 'none';
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
|
|
571
655
|
// Determine data source: live during sync, report after sync
|
|
572
656
|
let tmpTables = {};
|
|
573
657
|
let tmpThroughputSamples = [];
|
|
@@ -339,6 +339,34 @@ select { background: #fff; width: 100%; padding: 8px 12px; border: 1px solid #cc
|
|
|
339
339
|
font-size: 0.78em; color: #888; margin-top: 4px; padding-left: 18px;
|
|
340
340
|
font-family: monospace; max-height: 80px; overflow-y: auto;
|
|
341
341
|
}
|
|
342
|
+
|
|
343
|
+
/* Pre-count Table */
|
|
344
|
+
.precount-table {
|
|
345
|
+
width: 100%; border-collapse: collapse; font-size: 0.88em;
|
|
346
|
+
}
|
|
347
|
+
.precount-table thead th {
|
|
348
|
+
text-align: left; font-weight: 600; color: #555; font-size: 0.85em;
|
|
349
|
+
text-transform: uppercase; letter-spacing: 0.3px;
|
|
350
|
+
padding: 4px 8px 6px; border-bottom: 2px solid #e9ecef;
|
|
351
|
+
}
|
|
352
|
+
.precount-table tbody td {
|
|
353
|
+
padding: 4px 8px; border-bottom: 1px solid #f0f0f0;
|
|
354
|
+
}
|
|
355
|
+
.precount-table tbody tr:last-child td { border-bottom: 1px solid #e9ecef; }
|
|
356
|
+
.precount-table tfoot td {
|
|
357
|
+
padding: 6px 8px 2px; font-size: 0.95em;
|
|
358
|
+
}
|
|
359
|
+
.precount-error td { color: #dc3545; }
|
|
360
|
+
.precount-pending {
|
|
361
|
+
color: #888; font-size: 0.85em; font-style: italic; padding: 8px 0 2px;
|
|
362
|
+
display: flex; align-items: center; gap: 6px;
|
|
363
|
+
}
|
|
364
|
+
.precount-spinner {
|
|
365
|
+
display: inline-block; width: 12px; height: 12px;
|
|
366
|
+
border: 2px solid #ddd; border-top-color: #4a90d9;
|
|
367
|
+
border-radius: 50%; animation: precount-spin 0.8s linear infinite;
|
|
368
|
+
}
|
|
369
|
+
@keyframes precount-spin { to { transform: rotate(360deg); } }
|
|
342
370
|
`,
|
|
343
371
|
Templates:
|
|
344
372
|
[
|
|
@@ -4892,7 +4892,7 @@
|
|
|
4892
4892
|
StatusDetailTimer: null,
|
|
4893
4893
|
StatusDetailData: null,
|
|
4894
4894
|
LastLiveStatus: null,
|
|
4895
|
-
PersistFields: ['serverURL', 'authMethod', 'authURI', 'checkURI', 'cookieName', 'cookieValueAddr', 'cookieValueTemplate', 'loginMarker', 'userName', 'password', 'schemaURL', 'pageSize', 'dateTimePrecisionMS', 'connProvider', 'sqliteFilePath', 'mysqlServer', 'mysqlPort', 'mysqlUser', 'mysqlPassword', 'mysqlDatabase', 'mysqlConnectionLimit', 'mssqlServer', 'mssqlPort', 'mssqlUser', 'mssqlPassword', 'mssqlDatabase', 'mssqlConnectionLimit', 'postgresqlHost', 'postgresqlPort', 'postgresqlUser', 'postgresqlPassword', 'postgresqlDatabase', 'postgresqlConnectionLimit', 'solrHost', 'solrPort', 'solrCore', 'solrPath', 'mongodbHost', 'mongodbPort', 'mongodbUser', 'mongodbPassword', 'mongodbDatabase', 'mongodbConnectionLimit', 'rocksdbFolder', 'bibliographFolder']
|
|
4895
|
+
PersistFields: ['serverURL', 'authMethod', 'authURI', 'checkURI', 'cookieName', 'cookieValueAddr', 'cookieValueTemplate', 'loginMarker', 'userName', 'password', 'schemaURL', 'pageSize', 'dateTimePrecisionMS', 'connProvider', 'sqliteFilePath', 'mysqlServer', 'mysqlPort', 'mysqlUser', 'mysqlPassword', 'mysqlDatabase', 'mysqlConnectionLimit', 'mssqlServer', 'mssqlPort', 'mssqlUser', 'mssqlPassword', 'mssqlDatabase', 'mssqlConnectionLimit', 'postgresqlHost', 'postgresqlPort', 'postgresqlUser', 'postgresqlPassword', 'postgresqlDatabase', 'postgresqlConnectionLimit', 'solrHost', 'solrPort', 'solrCore', 'solrPath', 'mongodbHost', 'mongodbPort', 'mongodbUser', 'mongodbPassword', 'mongodbDatabase', 'mongodbConnectionLimit', 'rocksdbFolder', 'bibliographFolder', 'syncMaxRecords']
|
|
4896
4896
|
};
|
|
4897
4897
|
|
|
4898
4898
|
// Make pict available for inline onclick handlers
|
|
@@ -5287,7 +5287,8 @@
|
|
|
5287
5287
|
tmpMetaParts.push('<span class="live-status-meta-item">' + tmpGrandTotal + ' records to sync</span>');
|
|
5288
5288
|
}
|
|
5289
5289
|
if (pData.PreCountProgress && pData.PreCountProgress.Counted < pData.PreCountProgress.TotalTables) {
|
|
5290
|
-
|
|
5290
|
+
let tmpCountedSoFar = pData.PreCountGrandTotal > 0 ? ' (' + pData.PreCountGrandTotal.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') + ' records found)' : '';
|
|
5291
|
+
tmpMetaParts.push('<span class="live-status-meta-item">counting: ' + pData.PreCountProgress.Counted + ' / ' + pData.PreCountProgress.TotalTables + ' tables' + tmpCountedSoFar + '</span>');
|
|
5291
5292
|
}
|
|
5292
5293
|
if (pData.Errors > 0) {
|
|
5293
5294
|
tmpMetaParts.push('<span class="live-status-meta-item" style="color:#dc3545"><strong>' + pData.Errors + '</strong> error' + (pData.Errors === 1 ? '' : 's') + '</span>');
|
|
@@ -5302,7 +5303,10 @@
|
|
|
5302
5303
|
|
|
5303
5304
|
// Update progress bar
|
|
5304
5305
|
let tmpPct = 0;
|
|
5305
|
-
if (pData.Phase === 'syncing' && pData.
|
|
5306
|
+
if (pData.Phase === 'syncing' && pData.PreCountProgress && pData.PreCountProgress.Counted < pData.PreCountProgress.TotalTables) {
|
|
5307
|
+
// During counting phase, show table counting progress
|
|
5308
|
+
tmpPct = Math.min(pData.PreCountProgress.Counted / pData.PreCountProgress.TotalTables * 100, 99);
|
|
5309
|
+
} else if (pData.Phase === 'syncing' && pData.PreCountGrandTotal > 0 && pData.TotalSynced > 0) {
|
|
5306
5310
|
tmpPct = Math.min(pData.TotalSynced / pData.PreCountGrandTotal * 100, 99.9);
|
|
5307
5311
|
} else if (pData.Phase === 'syncing' && pData.TotalTables > 0) {
|
|
5308
5312
|
let tmpTablePct = pData.Completed / pData.TotalTables * 100;
|
|
@@ -5317,6 +5321,14 @@
|
|
|
5317
5321
|
}
|
|
5318
5322
|
tmpProgressFill.style.width = Math.min(100, Math.round(tmpPct)) + '%';
|
|
5319
5323
|
|
|
5324
|
+
// Auto-expand the detail view when sync starts so users see counting progress
|
|
5325
|
+
if ((pData.Phase === 'syncing' || pData.Phase === 'stopping') && !this.pict.AppData.DataCloner.StatusDetailExpanded) {
|
|
5326
|
+
let tmpLayoutView = this.pict.views['DataCloner-Layout'];
|
|
5327
|
+
if (tmpLayoutView && typeof tmpLayoutView.toggleStatusDetail === 'function') {
|
|
5328
|
+
tmpLayoutView.toggleStatusDetail();
|
|
5329
|
+
}
|
|
5330
|
+
}
|
|
5331
|
+
|
|
5320
5332
|
// If the detail view is expanded, re-render it with fresh data
|
|
5321
5333
|
if (this.pict.AppData.DataCloner.StatusDetailExpanded) {
|
|
5322
5334
|
this.renderStatusDetail();
|
|
@@ -5370,6 +5382,49 @@
|
|
|
5370
5382
|
tmpSelf.renderStatusDetail();
|
|
5371
5383
|
}).catch(function () {/* ignore poll errors */});
|
|
5372
5384
|
}
|
|
5385
|
+
renderCountingPhaseDetail(pContainer, pPreCountProgress) {
|
|
5386
|
+
let tmpTables = pPreCountProgress.Tables || [];
|
|
5387
|
+
let tmpCounted = pPreCountProgress.Counted || 0;
|
|
5388
|
+
let tmpTotal = pPreCountProgress.TotalTables || 0;
|
|
5389
|
+
let tmpRunningTotal = 0;
|
|
5390
|
+
let tmpHtml = '<div class="status-detail-section">';
|
|
5391
|
+
tmpHtml += '<div class="status-detail-section-title">Counting Records (' + tmpCounted + ' / ' + tmpTotal + ' tables)</div>';
|
|
5392
|
+
if (tmpTables.length > 0) {
|
|
5393
|
+
tmpHtml += '<table class="precount-table">';
|
|
5394
|
+
tmpHtml += '<thead><tr><th>Table</th><th style="text-align:right">Records</th><th style="text-align:right">Time</th></tr></thead>';
|
|
5395
|
+
tmpHtml += '<tbody>';
|
|
5396
|
+
for (let i = 0; i < tmpTables.length; i++) {
|
|
5397
|
+
let tmpT = tmpTables[i];
|
|
5398
|
+
tmpRunningTotal += tmpT.Count;
|
|
5399
|
+
let tmpCountFmt = this.formatNumber(tmpT.Count);
|
|
5400
|
+
let tmpTimeFmt = tmpT.ElapsedMs < 1000 ? tmpT.ElapsedMs + 'ms' : (tmpT.ElapsedMs / 1000).toFixed(1) + 's';
|
|
5401
|
+
let tmpRowClass = tmpT.Error ? ' class="precount-error"' : '';
|
|
5402
|
+
tmpHtml += '<tr' + tmpRowClass + '>';
|
|
5403
|
+
tmpHtml += '<td>' + this.escapeHtml(tmpT.Name) + '</td>';
|
|
5404
|
+
tmpHtml += '<td style="text-align:right; font-variant-numeric:tabular-nums">' + tmpCountFmt + '</td>';
|
|
5405
|
+
tmpHtml += '<td style="text-align:right; font-variant-numeric:tabular-nums; color:#888">' + tmpTimeFmt + '</td>';
|
|
5406
|
+
tmpHtml += '</tr>';
|
|
5407
|
+
}
|
|
5408
|
+
tmpHtml += '</tbody>';
|
|
5409
|
+
tmpHtml += '<tfoot><tr>';
|
|
5410
|
+
tmpHtml += '<td><strong>Total</strong></td>';
|
|
5411
|
+
tmpHtml += '<td style="text-align:right; font-variant-numeric:tabular-nums"><strong>' + this.formatNumber(tmpRunningTotal) + '</strong></td>';
|
|
5412
|
+
tmpHtml += '<td></td>';
|
|
5413
|
+
tmpHtml += '</tr></tfoot>';
|
|
5414
|
+
tmpHtml += '</table>';
|
|
5415
|
+
}
|
|
5416
|
+
|
|
5417
|
+
// Show pending indicator for remaining tables
|
|
5418
|
+
let tmpRemaining = tmpTotal - tmpCounted;
|
|
5419
|
+
if (tmpRemaining > 0) {
|
|
5420
|
+
tmpHtml += '<div class="precount-pending">';
|
|
5421
|
+
tmpHtml += '<span class="precount-spinner"></span> ';
|
|
5422
|
+
tmpHtml += tmpRemaining + ' table' + (tmpRemaining === 1 ? '' : 's') + ' remaining…';
|
|
5423
|
+
tmpHtml += '</div>';
|
|
5424
|
+
}
|
|
5425
|
+
tmpHtml += '</div>';
|
|
5426
|
+
pContainer.innerHTML = tmpHtml;
|
|
5427
|
+
}
|
|
5373
5428
|
renderStatusDetail() {
|
|
5374
5429
|
let tmpContainer = document.getElementById('DataCloner-StatusDetail-Container');
|
|
5375
5430
|
if (!tmpContainer) return;
|
|
@@ -5378,6 +5433,15 @@
|
|
|
5378
5433
|
let tmpStatusData = tmpAppData.StatusDetailData;
|
|
5379
5434
|
let tmpReport = tmpAppData.LastReport;
|
|
5380
5435
|
|
|
5436
|
+
// During the counting phase, show per-table counts as they arrive
|
|
5437
|
+
if (tmpLiveStatus && tmpLiveStatus.PreCountProgress && tmpLiveStatus.PreCountProgress.Tables && tmpLiveStatus.Phase === 'syncing' && tmpLiveStatus.PreCountProgress.Counted < tmpLiveStatus.PreCountProgress.TotalTables) {
|
|
5438
|
+
this.renderCountingPhaseDetail(tmpContainer, tmpLiveStatus.PreCountProgress);
|
|
5439
|
+
// Hide histogram during counting
|
|
5440
|
+
let tmpHistContainer = document.getElementById('DataCloner-Throughput-Histogram');
|
|
5441
|
+
if (tmpHistContainer) tmpHistContainer.style.display = 'none';
|
|
5442
|
+
return;
|
|
5443
|
+
}
|
|
5444
|
+
|
|
5381
5445
|
// Determine data source: live during sync, report after sync
|
|
5382
5446
|
let tmpTables = {};
|
|
5383
5447
|
let tmpThroughputSamples = [];
|
|
@@ -6909,6 +6973,34 @@ select { background: #fff; width: 100%; padding: 8px 12px; border: 1px solid #cc
|
|
|
6909
6973
|
font-size: 0.78em; color: #888; margin-top: 4px; padding-left: 18px;
|
|
6910
6974
|
font-family: monospace; max-height: 80px; overflow-y: auto;
|
|
6911
6975
|
}
|
|
6976
|
+
|
|
6977
|
+
/* Pre-count Table */
|
|
6978
|
+
.precount-table {
|
|
6979
|
+
width: 100%; border-collapse: collapse; font-size: 0.88em;
|
|
6980
|
+
}
|
|
6981
|
+
.precount-table thead th {
|
|
6982
|
+
text-align: left; font-weight: 600; color: #555; font-size: 0.85em;
|
|
6983
|
+
text-transform: uppercase; letter-spacing: 0.3px;
|
|
6984
|
+
padding: 4px 8px 6px; border-bottom: 2px solid #e9ecef;
|
|
6985
|
+
}
|
|
6986
|
+
.precount-table tbody td {
|
|
6987
|
+
padding: 4px 8px; border-bottom: 1px solid #f0f0f0;
|
|
6988
|
+
}
|
|
6989
|
+
.precount-table tbody tr:last-child td { border-bottom: 1px solid #e9ecef; }
|
|
6990
|
+
.precount-table tfoot td {
|
|
6991
|
+
padding: 6px 8px 2px; font-size: 0.95em;
|
|
6992
|
+
}
|
|
6993
|
+
.precount-error td { color: #dc3545; }
|
|
6994
|
+
.precount-pending {
|
|
6995
|
+
color: #888; font-size: 0.85em; font-style: italic; padding: 8px 0 2px;
|
|
6996
|
+
display: flex; align-items: center; gap: 6px;
|
|
6997
|
+
}
|
|
6998
|
+
.precount-spinner {
|
|
6999
|
+
display: inline-block; width: 12px; height: 12px;
|
|
7000
|
+
border: 2px solid #ddd; border-top-color: #4a90d9;
|
|
7001
|
+
border-radius: 50%; animation: precount-spin 0.8s linear infinite;
|
|
7002
|
+
}
|
|
7003
|
+
@keyframes precount-spin { to { transform: rotate(360deg); } }
|
|
6912
7004
|
`,
|
|
6913
7005
|
Templates: [{
|
|
6914
7006
|
Hash: 'DataCloner-Layout',
|