retold-data-service 2.0.17 → 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 +110 -2
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Layout.js +28 -0
- package/source/services/data-cloner/web/data-cloner.js +112 -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,11 +497,40 @@ 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
|
{
|
|
495
513
|
this.renderStatusDetail();
|
|
496
514
|
}
|
|
515
|
+
|
|
516
|
+
// Auto-fetch the sync report when we detect a completed sync but haven't loaded the report yet
|
|
517
|
+
if (pData.Phase === 'complete' && !this.pict.AppData.DataCloner.LastReport)
|
|
518
|
+
{
|
|
519
|
+
let tmpSelf = this;
|
|
520
|
+
this.api('GET', '/clone/sync/report')
|
|
521
|
+
.then(function(pReportData)
|
|
522
|
+
{
|
|
523
|
+
if (pReportData && pReportData.ReportVersion)
|
|
524
|
+
{
|
|
525
|
+
tmpSelf.pict.AppData.DataCloner.LastReport = pReportData;
|
|
526
|
+
if (tmpSelf.pict.AppData.DataCloner.StatusDetailExpanded)
|
|
527
|
+
{
|
|
528
|
+
tmpSelf.renderStatusDetail();
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
})
|
|
532
|
+
.catch(function() { /* ignore fetch errors */ });
|
|
533
|
+
}
|
|
497
534
|
}
|
|
498
535
|
|
|
499
536
|
// ================================================================
|
|
@@ -539,6 +576,59 @@ class DataClonerProvider extends libPictProvider
|
|
|
539
576
|
.catch(function() { /* ignore poll errors */ });
|
|
540
577
|
}
|
|
541
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
|
+
|
|
542
632
|
renderStatusDetail()
|
|
543
633
|
{
|
|
544
634
|
let tmpContainer = document.getElementById('DataCloner-StatusDetail-Container');
|
|
@@ -549,6 +639,19 @@ class DataClonerProvider extends libPictProvider
|
|
|
549
639
|
let tmpStatusData = tmpAppData.StatusDetailData;
|
|
550
640
|
let tmpReport = tmpAppData.LastReport;
|
|
551
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
|
+
|
|
552
655
|
// Determine data source: live during sync, report after sync
|
|
553
656
|
let tmpTables = {};
|
|
554
657
|
let tmpThroughputSamples = [];
|
|
@@ -575,6 +678,11 @@ class DataClonerProvider extends libPictProvider
|
|
|
575
678
|
else if (tmpStatusData && tmpStatusData.Tables)
|
|
576
679
|
{
|
|
577
680
|
tmpTables = tmpStatusData.Tables;
|
|
681
|
+
// Use throughput samples from live status if available (e.g. after page reload with completed sync)
|
|
682
|
+
if (tmpLiveStatus && tmpLiveStatus.ThroughputSamples)
|
|
683
|
+
{
|
|
684
|
+
tmpThroughputSamples = tmpLiveStatus.ThroughputSamples;
|
|
685
|
+
}
|
|
578
686
|
}
|
|
579
687
|
|
|
580
688
|
// Categorize tables
|
|
@@ -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,10 +5321,31 @@
|
|
|
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();
|
|
5323
5335
|
}
|
|
5336
|
+
|
|
5337
|
+
// Auto-fetch the sync report when we detect a completed sync but haven't loaded the report yet
|
|
5338
|
+
if (pData.Phase === 'complete' && !this.pict.AppData.DataCloner.LastReport) {
|
|
5339
|
+
let tmpSelf = this;
|
|
5340
|
+
this.api('GET', '/clone/sync/report').then(function (pReportData) {
|
|
5341
|
+
if (pReportData && pReportData.ReportVersion) {
|
|
5342
|
+
tmpSelf.pict.AppData.DataCloner.LastReport = pReportData;
|
|
5343
|
+
if (tmpSelf.pict.AppData.DataCloner.StatusDetailExpanded) {
|
|
5344
|
+
tmpSelf.renderStatusDetail();
|
|
5345
|
+
}
|
|
5346
|
+
}
|
|
5347
|
+
}).catch(function () {/* ignore fetch errors */});
|
|
5348
|
+
}
|
|
5324
5349
|
}
|
|
5325
5350
|
|
|
5326
5351
|
// ================================================================
|
|
@@ -5357,6 +5382,49 @@
|
|
|
5357
5382
|
tmpSelf.renderStatusDetail();
|
|
5358
5383
|
}).catch(function () {/* ignore poll errors */});
|
|
5359
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
|
+
}
|
|
5360
5428
|
renderStatusDetail() {
|
|
5361
5429
|
let tmpContainer = document.getElementById('DataCloner-StatusDetail-Container');
|
|
5362
5430
|
if (!tmpContainer) return;
|
|
@@ -5365,6 +5433,15 @@
|
|
|
5365
5433
|
let tmpStatusData = tmpAppData.StatusDetailData;
|
|
5366
5434
|
let tmpReport = tmpAppData.LastReport;
|
|
5367
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
|
+
|
|
5368
5445
|
// Determine data source: live during sync, report after sync
|
|
5369
5446
|
let tmpTables = {};
|
|
5370
5447
|
let tmpThroughputSamples = [];
|
|
@@ -5384,6 +5461,10 @@
|
|
|
5384
5461
|
tmpEventLog = tmpReport.EventLog || [];
|
|
5385
5462
|
} else if (tmpStatusData && tmpStatusData.Tables) {
|
|
5386
5463
|
tmpTables = tmpStatusData.Tables;
|
|
5464
|
+
// Use throughput samples from live status if available (e.g. after page reload with completed sync)
|
|
5465
|
+
if (tmpLiveStatus && tmpLiveStatus.ThroughputSamples) {
|
|
5466
|
+
tmpThroughputSamples = tmpLiveStatus.ThroughputSamples;
|
|
5467
|
+
}
|
|
5387
5468
|
}
|
|
5388
5469
|
|
|
5389
5470
|
// Categorize tables
|
|
@@ -6892,6 +6973,34 @@ select { background: #fff; width: 100%; padding: 8px 12px; border: 1px solid #cc
|
|
|
6892
6973
|
font-size: 0.78em; color: #888; margin-top: 4px; padding-left: 18px;
|
|
6893
6974
|
font-family: monospace; max-height: 80px; overflow-y: auto;
|
|
6894
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); } }
|
|
6895
7004
|
`,
|
|
6896
7005
|
Templates: [{
|
|
6897
7006
|
Hash: 'DataCloner-Layout',
|