retold-data-service 2.0.16 → 2.0.18
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/.claude/launch.json +2 -2
- package/.quackage.json +19 -0
- package/package.json +13 -6
- package/source/services/data-cloner/DataCloner-Command-Sync.js +83 -50
- package/source/services/data-cloner/DataCloner-Command-WebUI.js +27 -10
- package/source/services/data-cloner/Retold-Data-Service-DataCloner.js +281 -4
- package/source/services/data-cloner/pict-app/Pict-Application-DataCloner-Configuration.json +9 -0
- package/source/services/data-cloner/pict-app/Pict-Application-DataCloner.js +102 -0
- package/source/services/data-cloner/pict-app/Pict-DataCloner-Bundle.js +6 -0
- package/source/services/data-cloner/pict-app/providers/Pict-Provider-DataCloner.js +998 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Connection.js +407 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Deploy.js +126 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Export.js +483 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Layout.js +390 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Schema.js +241 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Session.js +268 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Sync.js +575 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-ViewData.js +176 -0
- package/source/services/data-cloner/web/data-cloner.js +7952 -0
- package/source/services/data-cloner/web/data-cloner.js.map +1 -0
- package/source/services/data-cloner/web/data-cloner.min.js +2 -0
- package/source/services/data-cloner/web/data-cloner.min.js.map +1 -0
- package/source/services/data-cloner/web/index.html +17 -0
- package/test/DataCloner-Integration_tests.js +1205 -0
- package/test/DataCloner-Puppeteer_tests.js +502 -0
- package/test/integration-report.json +311 -0
- package/test/run-integration-tests.js +501 -0
- package/source/services/data-cloner/data-cloner-web.html +0 -2706
|
@@ -0,0 +1,998 @@
|
|
|
1
|
+
const libPictProvider = require('pict-provider');
|
|
2
|
+
|
|
3
|
+
class DataClonerProvider 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 — Database Connection
|
|
80
|
+
let tmpProvider = document.getElementById('connProvider');
|
|
81
|
+
if (!tmpProvider) return;
|
|
82
|
+
tmpProvider = tmpProvider.value;
|
|
83
|
+
let tmpPreview1 = tmpProvider;
|
|
84
|
+
if (tmpProvider === 'SQLite')
|
|
85
|
+
{
|
|
86
|
+
let tmpPath = document.getElementById('sqliteFilePath').value || 'data/cloned.sqlite';
|
|
87
|
+
tmpPreview1 = 'SQLite at ' + tmpPath;
|
|
88
|
+
}
|
|
89
|
+
else if (tmpProvider === 'MySQL')
|
|
90
|
+
{
|
|
91
|
+
let tmpHost = document.getElementById('mysqlServer').value || '127.0.0.1';
|
|
92
|
+
let tmpPort = document.getElementById('mysqlPort').value || '3306';
|
|
93
|
+
let tmpUser = document.getElementById('mysqlUser').value || 'root';
|
|
94
|
+
tmpPreview1 = 'MySQL on ' + tmpHost + ':' + tmpPort + ' as ' + tmpUser;
|
|
95
|
+
}
|
|
96
|
+
else if (tmpProvider === 'MSSQL')
|
|
97
|
+
{
|
|
98
|
+
let tmpHost = document.getElementById('mssqlServer').value || '127.0.0.1';
|
|
99
|
+
let tmpPort = document.getElementById('mssqlPort').value || '1433';
|
|
100
|
+
let tmpUser = document.getElementById('mssqlUser').value || 'sa';
|
|
101
|
+
tmpPreview1 = 'MSSQL on ' + tmpHost + ':' + tmpPort + ' as ' + tmpUser;
|
|
102
|
+
}
|
|
103
|
+
else if (tmpProvider === 'PostgreSQL')
|
|
104
|
+
{
|
|
105
|
+
let tmpHost = document.getElementById('postgresqlHost').value || '127.0.0.1';
|
|
106
|
+
let tmpPort = document.getElementById('postgresqlPort').value || '5432';
|
|
107
|
+
let tmpUser = document.getElementById('postgresqlUser').value || 'postgres';
|
|
108
|
+
tmpPreview1 = 'PostgreSQL on ' + tmpHost + ':' + tmpPort + ' as ' + tmpUser;
|
|
109
|
+
}
|
|
110
|
+
else if (tmpProvider === 'MongoDB')
|
|
111
|
+
{
|
|
112
|
+
let tmpHost = document.getElementById('mongodbHost').value || '127.0.0.1';
|
|
113
|
+
let tmpPort = document.getElementById('mongodbPort').value || '27017';
|
|
114
|
+
tmpPreview1 = 'MongoDB on ' + tmpHost + ':' + tmpPort;
|
|
115
|
+
}
|
|
116
|
+
else if (tmpProvider === 'Solr')
|
|
117
|
+
{
|
|
118
|
+
let tmpHost = document.getElementById('solrHost').value || '127.0.0.1';
|
|
119
|
+
let tmpPort = document.getElementById('solrPort').value || '8983';
|
|
120
|
+
tmpPreview1 = 'Solr on ' + tmpHost + ':' + tmpPort;
|
|
121
|
+
}
|
|
122
|
+
else if (tmpProvider === 'RocksDB')
|
|
123
|
+
{
|
|
124
|
+
let tmpFolder = document.getElementById('rocksdbFolder').value || 'data/rocksdb';
|
|
125
|
+
tmpPreview1 = 'RocksDB at ' + tmpFolder;
|
|
126
|
+
}
|
|
127
|
+
else if (tmpProvider === 'Bibliograph')
|
|
128
|
+
{
|
|
129
|
+
let tmpFolder = document.getElementById('bibliographFolder').value || 'data/bibliograph';
|
|
130
|
+
tmpPreview1 = 'Bibliograph at ' + tmpFolder;
|
|
131
|
+
}
|
|
132
|
+
document.getElementById('preview1').textContent = tmpPreview1;
|
|
133
|
+
|
|
134
|
+
// Section 2 — Remote Session
|
|
135
|
+
let tmpServerURL = document.getElementById('serverURL').value;
|
|
136
|
+
let tmpUserName = document.getElementById('userName').value;
|
|
137
|
+
if (tmpServerURL)
|
|
138
|
+
{
|
|
139
|
+
let tmpPreview2 = tmpServerURL;
|
|
140
|
+
if (tmpUserName) tmpPreview2 += ' as ' + tmpUserName;
|
|
141
|
+
document.getElementById('preview2').textContent = tmpPreview2;
|
|
142
|
+
}
|
|
143
|
+
else
|
|
144
|
+
{
|
|
145
|
+
document.getElementById('preview2').textContent = 'Configure remote server URL and credentials';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Section 3 — Remote Schema
|
|
149
|
+
let tmpTableChecks = document.querySelectorAll('#tableList input[type="checkbox"]:checked');
|
|
150
|
+
if (tmpTableChecks.length > 0)
|
|
151
|
+
{
|
|
152
|
+
document.getElementById('preview3').textContent = tmpTableChecks.length + ' table' + (tmpTableChecks.length === 1 ? '' : 's') + ' selected';
|
|
153
|
+
}
|
|
154
|
+
else
|
|
155
|
+
{
|
|
156
|
+
let tmpSchemaURL = document.getElementById('schemaURL').value;
|
|
157
|
+
if (tmpSchemaURL)
|
|
158
|
+
{
|
|
159
|
+
document.getElementById('preview3').textContent = 'Schema from ' + tmpSchemaURL;
|
|
160
|
+
}
|
|
161
|
+
else
|
|
162
|
+
{
|
|
163
|
+
document.getElementById('preview3').textContent = 'Fetch and select tables from the remote server';
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Section 4 — Deploy Schema
|
|
168
|
+
let tmpDeployedEl = document.getElementById('deployStatus');
|
|
169
|
+
let tmpDeployedText = tmpDeployedEl ? tmpDeployedEl.textContent : '';
|
|
170
|
+
if (tmpDeployedText && tmpDeployedText.indexOf('deployed') !== -1)
|
|
171
|
+
{
|
|
172
|
+
document.getElementById('preview4').textContent = tmpDeployedText;
|
|
173
|
+
}
|
|
174
|
+
else
|
|
175
|
+
{
|
|
176
|
+
document.getElementById('preview4').textContent = 'Create selected tables in the local database';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Section 5 — Synchronize Data
|
|
180
|
+
let tmpSyncMode = document.querySelector('input[name="syncMode"]:checked');
|
|
181
|
+
let tmpModeName = tmpSyncMode ? tmpSyncMode.value : 'Initial';
|
|
182
|
+
let tmpPageSize = document.getElementById('pageSize').value || '100';
|
|
183
|
+
let tmpSyncPreview = tmpModeName + ' sync, page size ' + tmpPageSize;
|
|
184
|
+
let tmpDeleted = document.getElementById('syncDeletedRecords').checked;
|
|
185
|
+
if (tmpDeleted) tmpSyncPreview += ', including deleted';
|
|
186
|
+
document.getElementById('preview5').textContent = tmpSyncPreview;
|
|
187
|
+
|
|
188
|
+
// Section 6 — Export Configuration
|
|
189
|
+
let tmpMaxRecords = document.getElementById('syncMaxRecords').value;
|
|
190
|
+
let tmpLogFile = document.getElementById('syncLogFile').checked;
|
|
191
|
+
let tmpExportParts = [];
|
|
192
|
+
if (tmpMaxRecords && parseInt(tmpMaxRecords, 10) > 0) tmpExportParts.push('max ' + tmpMaxRecords + ' records');
|
|
193
|
+
if (tmpLogFile) tmpExportParts.push('log enabled');
|
|
194
|
+
else tmpExportParts.push('log disabled');
|
|
195
|
+
document.getElementById('preview6').textContent = tmpExportParts.length > 0 ? 'Export: ' + tmpExportParts.join(', ') : 'Generate JSON config for headless cloning';
|
|
196
|
+
|
|
197
|
+
// Section 7 — View Data
|
|
198
|
+
let tmpViewTable = document.getElementById('viewTable').value;
|
|
199
|
+
if (tmpViewTable)
|
|
200
|
+
{
|
|
201
|
+
document.getElementById('preview7').textContent = 'Viewing ' + tmpViewTable;
|
|
202
|
+
}
|
|
203
|
+
else
|
|
204
|
+
{
|
|
205
|
+
document.getElementById('preview7').textContent = 'Browse synced table data';
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
initAccordionPreviews()
|
|
210
|
+
{
|
|
211
|
+
let tmpSelf = this;
|
|
212
|
+
|
|
213
|
+
let tmpPreviewFields = [
|
|
214
|
+
'connProvider', 'sqliteFilePath',
|
|
215
|
+
'mysqlServer', 'mysqlPort', 'mysqlUser',
|
|
216
|
+
'mssqlServer', 'mssqlPort', 'mssqlUser',
|
|
217
|
+
'postgresqlHost', 'postgresqlPort', 'postgresqlUser',
|
|
218
|
+
'mongodbHost', 'mongodbPort',
|
|
219
|
+
'solrHost', 'solrPort',
|
|
220
|
+
'rocksdbFolder', 'bibliographFolder',
|
|
221
|
+
'serverURL', 'userName',
|
|
222
|
+
'schemaURL',
|
|
223
|
+
'pageSize', 'dateTimePrecisionMS',
|
|
224
|
+
'syncMaxRecords',
|
|
225
|
+
'viewTable', 'viewLimit'
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
let tmpHandler = function() { tmpSelf.updateAllPreviews(); };
|
|
229
|
+
|
|
230
|
+
for (let i = 0; i < tmpPreviewFields.length; i++)
|
|
231
|
+
{
|
|
232
|
+
let tmpEl = document.getElementById(tmpPreviewFields[i]);
|
|
233
|
+
if (tmpEl)
|
|
234
|
+
{
|
|
235
|
+
tmpEl.addEventListener('input', tmpHandler);
|
|
236
|
+
tmpEl.addEventListener('change', tmpHandler);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Checkboxes and radios
|
|
241
|
+
let tmpCheckboxes = ['syncDeletedRecords', 'syncLogFile'];
|
|
242
|
+
for (let i = 0; i < tmpCheckboxes.length; i++)
|
|
243
|
+
{
|
|
244
|
+
let tmpEl = document.getElementById(tmpCheckboxes[i]);
|
|
245
|
+
if (tmpEl) tmpEl.addEventListener('change', tmpHandler);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
document.querySelectorAll('input[name="syncMode"]').forEach(function(pEl)
|
|
249
|
+
{
|
|
250
|
+
pEl.addEventListener('change', tmpHandler);
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ================================================================
|
|
255
|
+
// LocalStorage Persistence
|
|
256
|
+
// ================================================================
|
|
257
|
+
|
|
258
|
+
saveField(pFieldId)
|
|
259
|
+
{
|
|
260
|
+
let tmpEl = document.getElementById(pFieldId);
|
|
261
|
+
if (tmpEl)
|
|
262
|
+
{
|
|
263
|
+
localStorage.setItem('dataCloner_' + pFieldId, tmpEl.value);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
restoreFields()
|
|
268
|
+
{
|
|
269
|
+
let tmpPersistFields = this.pict.AppData.DataCloner.PersistFields;
|
|
270
|
+
for (let i = 0; i < tmpPersistFields.length; i++)
|
|
271
|
+
{
|
|
272
|
+
let tmpId = tmpPersistFields[i];
|
|
273
|
+
let tmpSaved = localStorage.getItem('dataCloner_' + tmpId);
|
|
274
|
+
if (tmpSaved !== null)
|
|
275
|
+
{
|
|
276
|
+
let tmpEl = document.getElementById(tmpId);
|
|
277
|
+
if (tmpEl) tmpEl.value = tmpSaved;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Restore checkbox state
|
|
282
|
+
let tmpSyncDeleted = localStorage.getItem('dataCloner_syncDeletedRecords');
|
|
283
|
+
if (tmpSyncDeleted !== null)
|
|
284
|
+
{
|
|
285
|
+
document.getElementById('syncDeletedRecords').checked = tmpSyncDeleted === 'true';
|
|
286
|
+
}
|
|
287
|
+
// Restore sync mode
|
|
288
|
+
let tmpSyncMode = localStorage.getItem('dataCloner_syncMode');
|
|
289
|
+
if (tmpSyncMode === 'Ongoing')
|
|
290
|
+
{
|
|
291
|
+
document.getElementById('syncModeOngoing').checked = true;
|
|
292
|
+
}
|
|
293
|
+
let tmpSolrSecure = localStorage.getItem('dataCloner_solrSecure');
|
|
294
|
+
if (tmpSolrSecure !== null)
|
|
295
|
+
{
|
|
296
|
+
document.getElementById('solrSecure').checked = tmpSolrSecure === 'true';
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
initPersistence()
|
|
301
|
+
{
|
|
302
|
+
let tmpSelf = this;
|
|
303
|
+
this.restoreFields();
|
|
304
|
+
|
|
305
|
+
let tmpPersistFields = this.pict.AppData.DataCloner.PersistFields;
|
|
306
|
+
for (let i = 0; i < tmpPersistFields.length; i++)
|
|
307
|
+
{
|
|
308
|
+
(function(pId)
|
|
309
|
+
{
|
|
310
|
+
let tmpEl = document.getElementById(pId);
|
|
311
|
+
if (tmpEl)
|
|
312
|
+
{
|
|
313
|
+
tmpEl.addEventListener('input', function() { tmpSelf.saveField(pId); });
|
|
314
|
+
tmpEl.addEventListener('change', function() { tmpSelf.saveField(pId); });
|
|
315
|
+
}
|
|
316
|
+
})(tmpPersistFields[i]);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Persist sync deleted checkbox
|
|
320
|
+
let tmpSyncDeletedEl = document.getElementById('syncDeletedRecords');
|
|
321
|
+
if (tmpSyncDeletedEl)
|
|
322
|
+
{
|
|
323
|
+
tmpSyncDeletedEl.addEventListener('change', function()
|
|
324
|
+
{
|
|
325
|
+
localStorage.setItem('dataCloner_syncDeletedRecords', this.checked);
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Persist sync mode radio
|
|
330
|
+
document.querySelectorAll('input[name="syncMode"]').forEach(function(pEl)
|
|
331
|
+
{
|
|
332
|
+
pEl.addEventListener('change', function()
|
|
333
|
+
{
|
|
334
|
+
localStorage.setItem('dataCloner_syncMode', this.value);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// Persist solr secure checkbox
|
|
339
|
+
let tmpSolrSecureEl = document.getElementById('solrSecure');
|
|
340
|
+
if (tmpSolrSecureEl)
|
|
341
|
+
{
|
|
342
|
+
tmpSolrSecureEl.addEventListener('change', function()
|
|
343
|
+
{
|
|
344
|
+
localStorage.setItem('dataCloner_solrSecure', this.checked);
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Persist auto-process checkboxes
|
|
349
|
+
let tmpAutoIds = ['auto1', 'auto2', 'auto3', 'auto4', 'auto5'];
|
|
350
|
+
for (let a = 0; a < tmpAutoIds.length; a++)
|
|
351
|
+
{
|
|
352
|
+
(function(pId)
|
|
353
|
+
{
|
|
354
|
+
let tmpEl = document.getElementById(pId);
|
|
355
|
+
if (tmpEl)
|
|
356
|
+
{
|
|
357
|
+
let tmpSaved = localStorage.getItem('dataCloner_' + pId);
|
|
358
|
+
if (tmpSaved !== null) tmpEl.checked = tmpSaved === 'true';
|
|
359
|
+
tmpEl.addEventListener('change', function()
|
|
360
|
+
{
|
|
361
|
+
localStorage.setItem('dataCloner_' + pId, this.checked);
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
})(tmpAutoIds[a]);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ================================================================
|
|
369
|
+
// Live Status Indicator
|
|
370
|
+
// ================================================================
|
|
371
|
+
|
|
372
|
+
startLiveStatusPolling()
|
|
373
|
+
{
|
|
374
|
+
let tmpAppData = this.pict.AppData.DataCloner;
|
|
375
|
+
if (tmpAppData.LiveStatusTimer) clearInterval(tmpAppData.LiveStatusTimer);
|
|
376
|
+
this.pollLiveStatus();
|
|
377
|
+
let tmpSelf = this;
|
|
378
|
+
tmpAppData.LiveStatusTimer = setInterval(function() { tmpSelf.pollLiveStatus(); }, 1500);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
pollLiveStatus()
|
|
382
|
+
{
|
|
383
|
+
let tmpSelf = this;
|
|
384
|
+
this.api('GET', '/clone/sync/live-status')
|
|
385
|
+
.then(function(pData)
|
|
386
|
+
{
|
|
387
|
+
tmpSelf.renderLiveStatus(pData);
|
|
388
|
+
})
|
|
389
|
+
.catch(function()
|
|
390
|
+
{
|
|
391
|
+
tmpSelf.renderLiveStatus({ Phase: 'disconnected', Message: 'Cannot reach server', TotalSynced: 0, TotalRecords: 0 });
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
renderLiveStatus(pData)
|
|
396
|
+
{
|
|
397
|
+
// Cache the live status data for the detail view
|
|
398
|
+
this.pict.AppData.DataCloner.LastLiveStatus = pData;
|
|
399
|
+
|
|
400
|
+
let tmpBar = document.getElementById('liveStatusBar');
|
|
401
|
+
let tmpMsg = document.getElementById('liveStatusMessage');
|
|
402
|
+
let tmpMeta = document.getElementById('liveStatusMeta');
|
|
403
|
+
let tmpProgressFill = document.getElementById('liveStatusProgressFill');
|
|
404
|
+
if (!tmpBar) return;
|
|
405
|
+
|
|
406
|
+
// Update phase class (preserve expanded class if present)
|
|
407
|
+
let tmpWasExpanded = tmpBar.classList.contains('expanded');
|
|
408
|
+
tmpBar.className = 'live-status-bar phase-' + (pData.Phase || 'idle');
|
|
409
|
+
if (tmpWasExpanded) tmpBar.classList.add('expanded');
|
|
410
|
+
|
|
411
|
+
// Update message
|
|
412
|
+
tmpMsg.textContent = pData.Message || 'Idle';
|
|
413
|
+
|
|
414
|
+
// Update meta info
|
|
415
|
+
let tmpMetaParts = [];
|
|
416
|
+
if (pData.Phase === 'syncing' || pData.Phase === 'stopping')
|
|
417
|
+
{
|
|
418
|
+
if (pData.Elapsed)
|
|
419
|
+
{
|
|
420
|
+
tmpMetaParts.push('<span class="live-status-meta-item">\u23F1 ' + pData.Elapsed + '</span>');
|
|
421
|
+
}
|
|
422
|
+
if (pData.ETA)
|
|
423
|
+
{
|
|
424
|
+
tmpMetaParts.push('<span class="live-status-meta-item">~' + pData.ETA + ' remaining</span>');
|
|
425
|
+
}
|
|
426
|
+
if (pData.TotalTables > 0)
|
|
427
|
+
{
|
|
428
|
+
tmpMetaParts.push('<span class="live-status-meta-item"><strong>' + pData.Completed + '</strong> / ' + pData.TotalTables + ' tables</span>');
|
|
429
|
+
}
|
|
430
|
+
if (pData.TotalSynced > 0)
|
|
431
|
+
{
|
|
432
|
+
let tmpSynced = pData.TotalSynced.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
433
|
+
if (pData.PreCountGrandTotal > 0)
|
|
434
|
+
{
|
|
435
|
+
let tmpGrandTotal = pData.PreCountGrandTotal.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
436
|
+
tmpMetaParts.push('<span class="live-status-meta-item"><strong>' + tmpSynced + '</strong> / ' + tmpGrandTotal + ' records</span>');
|
|
437
|
+
}
|
|
438
|
+
else
|
|
439
|
+
{
|
|
440
|
+
tmpMetaParts.push('<span class="live-status-meta-item"><strong>' + tmpSynced + '</strong> records</span>');
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
else if (pData.PreCountGrandTotal > 0)
|
|
444
|
+
{
|
|
445
|
+
let tmpGrandTotal = pData.PreCountGrandTotal.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
446
|
+
tmpMetaParts.push('<span class="live-status-meta-item">' + tmpGrandTotal + ' records to sync</span>');
|
|
447
|
+
}
|
|
448
|
+
if (pData.PreCountProgress && pData.PreCountProgress.Counted < pData.PreCountProgress.TotalTables)
|
|
449
|
+
{
|
|
450
|
+
tmpMetaParts.push('<span class="live-status-meta-item">counting: ' + pData.PreCountProgress.Counted + ' / ' + pData.PreCountProgress.TotalTables + '</span>');
|
|
451
|
+
}
|
|
452
|
+
if (pData.Errors > 0)
|
|
453
|
+
{
|
|
454
|
+
tmpMetaParts.push('<span class="live-status-meta-item" style="color:#dc3545"><strong>' + pData.Errors + '</strong> error' + (pData.Errors === 1 ? '' : 's') + '</span>');
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
else if (pData.Phase === 'complete')
|
|
458
|
+
{
|
|
459
|
+
if (pData.TotalSynced > 0)
|
|
460
|
+
{
|
|
461
|
+
let tmpSynced = pData.TotalSynced.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
462
|
+
tmpMetaParts.push('<span class="live-status-meta-item"><strong>' + tmpSynced + '</strong> records synced</span>');
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
tmpMeta.innerHTML = tmpMetaParts.join('');
|
|
466
|
+
|
|
467
|
+
// Update progress bar
|
|
468
|
+
let tmpPct = 0;
|
|
469
|
+
if (pData.Phase === 'syncing' && pData.PreCountGrandTotal > 0 && pData.TotalSynced > 0)
|
|
470
|
+
{
|
|
471
|
+
tmpPct = Math.min((pData.TotalSynced / pData.PreCountGrandTotal) * 100, 99.9);
|
|
472
|
+
}
|
|
473
|
+
else if (pData.Phase === 'syncing' && pData.TotalTables > 0)
|
|
474
|
+
{
|
|
475
|
+
let tmpTablePct = (pData.Completed / pData.TotalTables) * 100;
|
|
476
|
+
if (pData.ActiveProgress && pData.ActiveProgress.Total > 0)
|
|
477
|
+
{
|
|
478
|
+
let tmpEntityPct = (pData.ActiveProgress.Synced / pData.ActiveProgress.Total) * (100 / pData.TotalTables);
|
|
479
|
+
tmpPct = tmpTablePct + tmpEntityPct;
|
|
480
|
+
}
|
|
481
|
+
else
|
|
482
|
+
{
|
|
483
|
+
tmpPct = tmpTablePct;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
else if (pData.Phase === 'complete')
|
|
487
|
+
{
|
|
488
|
+
tmpPct = 100;
|
|
489
|
+
}
|
|
490
|
+
tmpProgressFill.style.width = Math.min(100, Math.round(tmpPct)) + '%';
|
|
491
|
+
|
|
492
|
+
// If the detail view is expanded, re-render it with fresh data
|
|
493
|
+
if (this.pict.AppData.DataCloner.StatusDetailExpanded)
|
|
494
|
+
{
|
|
495
|
+
this.renderStatusDetail();
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Auto-fetch the sync report when we detect a completed sync but haven't loaded the report yet
|
|
499
|
+
if (pData.Phase === 'complete' && !this.pict.AppData.DataCloner.LastReport)
|
|
500
|
+
{
|
|
501
|
+
let tmpSelf = this;
|
|
502
|
+
this.api('GET', '/clone/sync/report')
|
|
503
|
+
.then(function(pReportData)
|
|
504
|
+
{
|
|
505
|
+
if (pReportData && pReportData.ReportVersion)
|
|
506
|
+
{
|
|
507
|
+
tmpSelf.pict.AppData.DataCloner.LastReport = pReportData;
|
|
508
|
+
if (tmpSelf.pict.AppData.DataCloner.StatusDetailExpanded)
|
|
509
|
+
{
|
|
510
|
+
tmpSelf.renderStatusDetail();
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
})
|
|
514
|
+
.catch(function() { /* ignore fetch errors */ });
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// ================================================================
|
|
519
|
+
// Status Detail Expansion
|
|
520
|
+
// ================================================================
|
|
521
|
+
|
|
522
|
+
onStatusDetailExpanded()
|
|
523
|
+
{
|
|
524
|
+
let tmpAppData = this.pict.AppData.DataCloner;
|
|
525
|
+
tmpAppData.StatusDetailExpanded = true;
|
|
526
|
+
|
|
527
|
+
// Immediate render from whatever data we have
|
|
528
|
+
this.renderStatusDetail();
|
|
529
|
+
|
|
530
|
+
// Start detail polling (poll /sync/status for per-table data)
|
|
531
|
+
if (tmpAppData.StatusDetailTimer) clearInterval(tmpAppData.StatusDetailTimer);
|
|
532
|
+
let tmpSelf = this;
|
|
533
|
+
tmpAppData.StatusDetailTimer = setInterval(function() { tmpSelf.pollStatusDetail(); }, 2000);
|
|
534
|
+
this.pollStatusDetail();
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
onStatusDetailCollapsed()
|
|
538
|
+
{
|
|
539
|
+
let tmpAppData = this.pict.AppData.DataCloner;
|
|
540
|
+
tmpAppData.StatusDetailExpanded = false;
|
|
541
|
+
|
|
542
|
+
if (tmpAppData.StatusDetailTimer)
|
|
543
|
+
{
|
|
544
|
+
clearInterval(tmpAppData.StatusDetailTimer);
|
|
545
|
+
tmpAppData.StatusDetailTimer = null;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
pollStatusDetail()
|
|
550
|
+
{
|
|
551
|
+
let tmpSelf = this;
|
|
552
|
+
this.api('GET', '/clone/sync/status')
|
|
553
|
+
.then(function(pData)
|
|
554
|
+
{
|
|
555
|
+
tmpSelf.pict.AppData.DataCloner.StatusDetailData = pData;
|
|
556
|
+
tmpSelf.renderStatusDetail();
|
|
557
|
+
})
|
|
558
|
+
.catch(function() { /* ignore poll errors */ });
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
renderStatusDetail()
|
|
562
|
+
{
|
|
563
|
+
let tmpContainer = document.getElementById('DataCloner-StatusDetail-Container');
|
|
564
|
+
if (!tmpContainer) return;
|
|
565
|
+
|
|
566
|
+
let tmpAppData = this.pict.AppData.DataCloner;
|
|
567
|
+
let tmpLiveStatus = tmpAppData.LastLiveStatus;
|
|
568
|
+
let tmpStatusData = tmpAppData.StatusDetailData;
|
|
569
|
+
let tmpReport = tmpAppData.LastReport;
|
|
570
|
+
|
|
571
|
+
// Determine data source: live during sync, report after sync
|
|
572
|
+
let tmpTables = {};
|
|
573
|
+
let tmpThroughputSamples = [];
|
|
574
|
+
let tmpEventLog = [];
|
|
575
|
+
let tmpIsLive = false;
|
|
576
|
+
|
|
577
|
+
if (tmpLiveStatus && (tmpLiveStatus.Phase === 'syncing' || tmpLiveStatus.Phase === 'stopping'))
|
|
578
|
+
{
|
|
579
|
+
tmpIsLive = true;
|
|
580
|
+
if (tmpStatusData && tmpStatusData.Tables) tmpTables = tmpStatusData.Tables;
|
|
581
|
+
if (tmpLiveStatus.ThroughputSamples) tmpThroughputSamples = tmpLiveStatus.ThroughputSamples;
|
|
582
|
+
}
|
|
583
|
+
else if (tmpReport && tmpReport.ReportVersion)
|
|
584
|
+
{
|
|
585
|
+
// Build tables object from report
|
|
586
|
+
for (let i = 0; i < tmpReport.Tables.length; i++)
|
|
587
|
+
{
|
|
588
|
+
let tmpT = tmpReport.Tables[i];
|
|
589
|
+
tmpTables[tmpT.Name] = tmpT;
|
|
590
|
+
}
|
|
591
|
+
tmpThroughputSamples = tmpReport.ThroughputSamples || [];
|
|
592
|
+
tmpEventLog = tmpReport.EventLog || [];
|
|
593
|
+
}
|
|
594
|
+
else if (tmpStatusData && tmpStatusData.Tables)
|
|
595
|
+
{
|
|
596
|
+
tmpTables = tmpStatusData.Tables;
|
|
597
|
+
// Use throughput samples from live status if available (e.g. after page reload with completed sync)
|
|
598
|
+
if (tmpLiveStatus && tmpLiveStatus.ThroughputSamples)
|
|
599
|
+
{
|
|
600
|
+
tmpThroughputSamples = tmpLiveStatus.ThroughputSamples;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Categorize tables
|
|
605
|
+
let tmpRunning = [];
|
|
606
|
+
let tmpPending = [];
|
|
607
|
+
let tmpCompleted = [];
|
|
608
|
+
let tmpErrors = [];
|
|
609
|
+
let tmpTableNames = Object.keys(tmpTables);
|
|
610
|
+
|
|
611
|
+
for (let i = 0; i < tmpTableNames.length; i++)
|
|
612
|
+
{
|
|
613
|
+
let tmpName = tmpTableNames[i];
|
|
614
|
+
let tmpT = tmpTables[tmpName];
|
|
615
|
+
if (tmpT.Status === 'Syncing')
|
|
616
|
+
{
|
|
617
|
+
tmpRunning.push({ Name: tmpName, Data: tmpT });
|
|
618
|
+
}
|
|
619
|
+
else if (tmpT.Status === 'Pending')
|
|
620
|
+
{
|
|
621
|
+
tmpPending.push(tmpName);
|
|
622
|
+
}
|
|
623
|
+
else if (tmpT.Status === 'Complete')
|
|
624
|
+
{
|
|
625
|
+
tmpCompleted.push({ Name: tmpName, Data: tmpT });
|
|
626
|
+
}
|
|
627
|
+
else if (tmpT.Status === 'Error' || tmpT.Status === 'Partial')
|
|
628
|
+
{
|
|
629
|
+
tmpErrors.push({ Name: tmpName, Data: tmpT });
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
let tmpHtml = '';
|
|
634
|
+
|
|
635
|
+
// === Section 1: Running Operations ===
|
|
636
|
+
if (tmpRunning.length > 0 || tmpPending.length > 0)
|
|
637
|
+
{
|
|
638
|
+
tmpHtml += '<div class="status-detail-section">';
|
|
639
|
+
tmpHtml += '<div class="status-detail-section-title">Running</div>';
|
|
640
|
+
for (let i = 0; i < tmpRunning.length; i++)
|
|
641
|
+
{
|
|
642
|
+
let tmpOp = tmpRunning[i];
|
|
643
|
+
let tmpPct = tmpOp.Data.Total > 0 ? Math.round((tmpOp.Data.Synced / tmpOp.Data.Total) * 100) : 0;
|
|
644
|
+
let tmpSyncedFmt = this.formatNumber(tmpOp.Data.Synced || 0);
|
|
645
|
+
let tmpTotalFmt = this.formatNumber(tmpOp.Data.Total || 0);
|
|
646
|
+
tmpHtml += '<div class="running-op-row">';
|
|
647
|
+
tmpHtml += ' <div class="running-op-name">' + this.escapeHtml(tmpOp.Name) + '</div>';
|
|
648
|
+
tmpHtml += ' <div class="running-op-bar"><div class="running-op-bar-fill" style="width:' + tmpPct + '%"></div></div>';
|
|
649
|
+
tmpHtml += ' <div class="running-op-count">' + tmpSyncedFmt + ' / ' + tmpTotalFmt + ' (' + tmpPct + '%)</div>';
|
|
650
|
+
tmpHtml += '</div>';
|
|
651
|
+
}
|
|
652
|
+
if (tmpPending.length > 0)
|
|
653
|
+
{
|
|
654
|
+
tmpHtml += '<div class="running-op-pending">' + tmpPending.length + ' table' + (tmpPending.length === 1 ? '' : 's') + ' waiting</div>';
|
|
655
|
+
}
|
|
656
|
+
tmpHtml += '</div>';
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// === Section 2: Completed Successful Operations ===
|
|
660
|
+
if (tmpCompleted.length > 0)
|
|
661
|
+
{
|
|
662
|
+
tmpHtml += '<div class="status-detail-section">';
|
|
663
|
+
tmpHtml += '<div class="status-detail-section-title">Completed (' + tmpCompleted.length + ')</div>';
|
|
664
|
+
|
|
665
|
+
for (let i = 0; i < tmpCompleted.length; i++)
|
|
666
|
+
{
|
|
667
|
+
tmpHtml += this.renderCompletedRow(tmpCompleted[i]);
|
|
668
|
+
}
|
|
669
|
+
tmpHtml += '</div>';
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// === Section 3: Unsuccessful Operations ===
|
|
673
|
+
if (tmpErrors.length > 0)
|
|
674
|
+
{
|
|
675
|
+
tmpHtml += '<div class="status-detail-section">';
|
|
676
|
+
tmpHtml += '<div class="status-detail-section-title">Errors (' + tmpErrors.length + ')</div>';
|
|
677
|
+
for (let i = 0; i < tmpErrors.length; i++)
|
|
678
|
+
{
|
|
679
|
+
tmpHtml += this.renderErrorRow(tmpErrors[i], tmpEventLog);
|
|
680
|
+
}
|
|
681
|
+
tmpHtml += '</div>';
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (tmpHtml === '')
|
|
685
|
+
{
|
|
686
|
+
if (tmpIsLive)
|
|
687
|
+
{
|
|
688
|
+
tmpHtml = '<div style="font-size:0.9em; color:#888; padding:8px 0">Sync in progress, waiting for table data\u2026</div>';
|
|
689
|
+
}
|
|
690
|
+
else
|
|
691
|
+
{
|
|
692
|
+
tmpHtml = '<div style="font-size:0.9em; color:#888; padding:8px 0">No sync data available. Run a sync to see operation details here.</div>';
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
tmpContainer.innerHTML = tmpHtml;
|
|
697
|
+
|
|
698
|
+
// Update the throughput histogram via pict-section-histogram
|
|
699
|
+
this.updateThroughputHistogram(tmpThroughputSamples);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
updateThroughputHistogram(pSamples)
|
|
703
|
+
{
|
|
704
|
+
let tmpHistContainer = document.getElementById('DataCloner-Throughput-Histogram');
|
|
705
|
+
if (!tmpHistContainer) return;
|
|
706
|
+
|
|
707
|
+
if (!pSamples || pSamples.length < 2)
|
|
708
|
+
{
|
|
709
|
+
tmpHistContainer.style.display = 'none';
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// --- Step 1: Compute raw deltas per 10s interval ---
|
|
714
|
+
let tmpRawDeltas = [];
|
|
715
|
+
for (let i = 1; i < pSamples.length; i++)
|
|
716
|
+
{
|
|
717
|
+
let tmpDelta = pSamples[i].synced - pSamples[i - 1].synced;
|
|
718
|
+
if (tmpDelta < 0) tmpDelta = 0;
|
|
719
|
+
tmpRawDeltas.push({ delta: tmpDelta, t: pSamples[i].t });
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// --- Step 2: Downsample if there are too many bars ---
|
|
723
|
+
let tmpContainerWidth = tmpHistContainer.clientWidth || 800;
|
|
724
|
+
let tmpMaxBars = Math.max(20, Math.floor(tmpContainerWidth / 6));
|
|
725
|
+
let tmpAggregated = tmpRawDeltas;
|
|
726
|
+
|
|
727
|
+
if (tmpRawDeltas.length > tmpMaxBars)
|
|
728
|
+
{
|
|
729
|
+
let tmpBucketSize = Math.ceil(tmpRawDeltas.length / tmpMaxBars);
|
|
730
|
+
tmpAggregated = [];
|
|
731
|
+
for (let i = 0; i < tmpRawDeltas.length; i += tmpBucketSize)
|
|
732
|
+
{
|
|
733
|
+
let tmpSum = 0;
|
|
734
|
+
let tmpLastT = 0;
|
|
735
|
+
for (let j = i; j < Math.min(i + tmpBucketSize, tmpRawDeltas.length); j++)
|
|
736
|
+
{
|
|
737
|
+
tmpSum += tmpRawDeltas[j].delta;
|
|
738
|
+
tmpLastT = tmpRawDeltas[j].t;
|
|
739
|
+
}
|
|
740
|
+
tmpAggregated.push({ delta: tmpSum, t: tmpLastT });
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// --- Step 3: Check for data ---
|
|
745
|
+
let tmpHasData = false;
|
|
746
|
+
for (let i = 0; i < tmpAggregated.length; i++)
|
|
747
|
+
{
|
|
748
|
+
if (tmpAggregated[i].delta > 0) { tmpHasData = true; break; }
|
|
749
|
+
}
|
|
750
|
+
if (!tmpHasData)
|
|
751
|
+
{
|
|
752
|
+
tmpHistContainer.style.display = 'none';
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// --- Step 4: Build bins for the histogram library ---
|
|
757
|
+
let tmpStartT = pSamples[0].t;
|
|
758
|
+
let tmpBins = [];
|
|
759
|
+
for (let i = 0; i < tmpAggregated.length; i++)
|
|
760
|
+
{
|
|
761
|
+
let tmpElapsedSec = Math.round((tmpAggregated[i].t - tmpStartT) / 1000);
|
|
762
|
+
tmpBins.push({
|
|
763
|
+
Label: this.formatElapsed(tmpElapsedSec),
|
|
764
|
+
Value: tmpAggregated[i].delta
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// --- Step 5: Update the histogram view via the library ---
|
|
769
|
+
tmpHistContainer.style.display = '';
|
|
770
|
+
let tmpHistView = this.pict.views['DataCloner-StatusHistogram'];
|
|
771
|
+
if (tmpHistView)
|
|
772
|
+
{
|
|
773
|
+
tmpHistView.setBins(tmpBins);
|
|
774
|
+
tmpHistView.renderHistogram();
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
formatElapsed(pSec)
|
|
779
|
+
{
|
|
780
|
+
if (pSec < 60) return pSec + 's';
|
|
781
|
+
if (pSec < 3600)
|
|
782
|
+
{
|
|
783
|
+
let tmpM = Math.floor(pSec / 60);
|
|
784
|
+
let tmpS = pSec % 60;
|
|
785
|
+
return tmpM + ':' + (tmpS < 10 ? '0' : '') + tmpS;
|
|
786
|
+
}
|
|
787
|
+
let tmpH = Math.floor(pSec / 3600);
|
|
788
|
+
let tmpM = Math.floor((pSec % 3600) / 60);
|
|
789
|
+
return tmpH + 'h' + (tmpM < 10 ? '0' : '') + tmpM;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
formatCompact(pNum)
|
|
793
|
+
{
|
|
794
|
+
if (pNum >= 1000000) return (pNum / 1000000).toFixed(1) + 'M';
|
|
795
|
+
if (pNum >= 10000) return (pNum / 1000).toFixed(0) + 'K';
|
|
796
|
+
if (pNum >= 1000) return (pNum / 1000).toFixed(1) + 'K';
|
|
797
|
+
return pNum.toString();
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
formatNumber(pNum)
|
|
801
|
+
{
|
|
802
|
+
return pNum.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
renderCompletedRow(pOp)
|
|
806
|
+
{
|
|
807
|
+
let tmpNew = pOp.Data.New || 0;
|
|
808
|
+
let tmpUpdated = pOp.Data.Updated || 0;
|
|
809
|
+
let tmpUnchanged = pOp.Data.Unchanged || 0;
|
|
810
|
+
let tmpDeleted = pOp.Data.Deleted || 0;
|
|
811
|
+
let tmpServerTotal = pOp.Data.ServerTotal || 0;
|
|
812
|
+
|
|
813
|
+
// Grand total for the ratio bar: all records the adapter dealt with
|
|
814
|
+
let tmpGrandTotal = tmpUnchanged + tmpNew + tmpUpdated + tmpDeleted;
|
|
815
|
+
if (tmpGrandTotal === 0 && tmpServerTotal > 0)
|
|
816
|
+
{
|
|
817
|
+
tmpGrandTotal = tmpServerTotal;
|
|
818
|
+
tmpUnchanged = tmpServerTotal;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
let tmpHtml = '<div class="completed-op-row">';
|
|
822
|
+
tmpHtml += '<div class="completed-op-header">';
|
|
823
|
+
tmpHtml += ' <span class="completed-op-checkmark">\u2713</span>';
|
|
824
|
+
tmpHtml += ' <span class="completed-op-name">' + this.escapeHtml(pOp.Name) + '</span>';
|
|
825
|
+
tmpHtml += '</div>';
|
|
826
|
+
|
|
827
|
+
// Ratio bar: Unchanged / New / Updated / Deleted
|
|
828
|
+
if (tmpGrandTotal > 0)
|
|
829
|
+
{
|
|
830
|
+
let tmpUnchangedPct = Math.round((tmpUnchanged / tmpGrandTotal) * 100);
|
|
831
|
+
let tmpNewPct = Math.round((tmpNew / tmpGrandTotal) * 100);
|
|
832
|
+
let tmpUpdatedPct = Math.round((tmpUpdated / tmpGrandTotal) * 100);
|
|
833
|
+
let tmpDeletedPct = Math.round((tmpDeleted / tmpGrandTotal) * 100);
|
|
834
|
+
|
|
835
|
+
// Ensure percentages sum to 100
|
|
836
|
+
let tmpPctSum = tmpUnchangedPct + tmpNewPct + tmpUpdatedPct + tmpDeletedPct;
|
|
837
|
+
if (tmpPctSum !== 100 && tmpPctSum > 0)
|
|
838
|
+
{
|
|
839
|
+
tmpUnchangedPct += (100 - tmpPctSum);
|
|
840
|
+
if (tmpUnchangedPct < 0) tmpUnchangedPct = 0;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
tmpHtml += '<div class="ratio-bar-container">';
|
|
844
|
+
if (tmpUnchangedPct > 0) tmpHtml += '<div class="ratio-bar-segment unchanged" style="width:' + tmpUnchangedPct + '%" title="Unchanged: ' + this.formatNumber(tmpUnchanged) + '"></div>';
|
|
845
|
+
if (tmpNewPct > 0) tmpHtml += '<div class="ratio-bar-segment new-records" style="width:' + tmpNewPct + '%" title="New: ' + this.formatNumber(tmpNew) + '"></div>';
|
|
846
|
+
if (tmpUpdatedPct > 0) tmpHtml += '<div class="ratio-bar-segment updated" style="width:' + tmpUpdatedPct + '%" title="Updated: ' + this.formatNumber(tmpUpdated) + '"></div>';
|
|
847
|
+
if (tmpDeletedPct > 0) tmpHtml += '<div class="ratio-bar-segment deleted" style="width:' + tmpDeletedPct + '%" title="Deleted: ' + this.formatNumber(tmpDeleted) + '"></div>';
|
|
848
|
+
tmpHtml += '</div>';
|
|
849
|
+
|
|
850
|
+
tmpHtml += '<div class="ratio-bar-legend">';
|
|
851
|
+
if (tmpUnchanged > 0) tmpHtml += '<span class="ratio-bar-legend-item"><span class="ratio-bar-legend-dot unchanged-dot"></span> Unchanged (' + this.formatNumber(tmpUnchanged) + ')</span>';
|
|
852
|
+
if (tmpNew > 0) tmpHtml += '<span class="ratio-bar-legend-item"><span class="ratio-bar-legend-dot new-dot"></span> New (' + this.formatNumber(tmpNew) + ')</span>';
|
|
853
|
+
if (tmpUpdated > 0) tmpHtml += '<span class="ratio-bar-legend-item"><span class="ratio-bar-legend-dot updated-dot"></span> Updated (' + this.formatNumber(tmpUpdated) + ')</span>';
|
|
854
|
+
if (tmpDeleted > 0) tmpHtml += '<span class="ratio-bar-legend-item"><span class="ratio-bar-legend-dot deleted-dot"></span> Deleted (' + this.formatNumber(tmpDeleted) + ')</span>';
|
|
855
|
+
tmpHtml += '</div>';
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
tmpHtml += '</div>';
|
|
859
|
+
return tmpHtml;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
renderErrorRow(pOp, pEventLog)
|
|
863
|
+
{
|
|
864
|
+
let tmpSynced = pOp.Data.Synced || 0;
|
|
865
|
+
let tmpTotal = pOp.Data.Total || 0;
|
|
866
|
+
let tmpSyncedFmt = this.formatNumber(tmpSynced);
|
|
867
|
+
let tmpTotalFmt = this.formatNumber(tmpTotal);
|
|
868
|
+
|
|
869
|
+
let tmpHtml = '<div class="error-op-row">';
|
|
870
|
+
tmpHtml += '<div class="error-op-header">';
|
|
871
|
+
tmpHtml += ' <span style="color:#dc3545">\u2717</span>';
|
|
872
|
+
tmpHtml += ' <span class="error-op-name">' + this.escapeHtml(pOp.Name) + '</span>';
|
|
873
|
+
tmpHtml += ' <span class="error-op-status">' + pOp.Data.Status + ' \u2014 ' + tmpSyncedFmt + ' / ' + tmpTotalFmt + '</span>';
|
|
874
|
+
tmpHtml += '</div>';
|
|
875
|
+
|
|
876
|
+
if (pOp.Data.ErrorMessage)
|
|
877
|
+
{
|
|
878
|
+
tmpHtml += '<div class="error-op-message">' + this.escapeHtml(pOp.Data.ErrorMessage) + '</div>';
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// Extract relevant log entries from EventLog
|
|
882
|
+
if (pEventLog && pEventLog.length > 0)
|
|
883
|
+
{
|
|
884
|
+
let tmpRelevantLogs = [];
|
|
885
|
+
for (let j = 0; j < pEventLog.length; j++)
|
|
886
|
+
{
|
|
887
|
+
let tmpLog = pEventLog[j];
|
|
888
|
+
if (tmpLog.Data && tmpLog.Data.Table === pOp.Name &&
|
|
889
|
+
(tmpLog.Type === 'TableError' || tmpLog.Type === 'TablePartial'))
|
|
890
|
+
{
|
|
891
|
+
tmpRelevantLogs.push(tmpLog);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
if (tmpRelevantLogs.length > 0)
|
|
895
|
+
{
|
|
896
|
+
tmpHtml += '<div class="error-op-log-entries">';
|
|
897
|
+
for (let j = 0; j < tmpRelevantLogs.length; j++)
|
|
898
|
+
{
|
|
899
|
+
let tmpTimestamp = tmpRelevantLogs[j].Timestamp.replace('T', ' ').replace(/\.\d+Z$/, '');
|
|
900
|
+
tmpHtml += '<div>' + this.escapeHtml(tmpTimestamp + ' ' + tmpRelevantLogs[j].Message) + '</div>';
|
|
901
|
+
}
|
|
902
|
+
tmpHtml += '</div>';
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
tmpHtml += '</div>';
|
|
907
|
+
return tmpHtml;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// ================================================================
|
|
911
|
+
// Deployed Tables Persistence
|
|
912
|
+
// ================================================================
|
|
913
|
+
|
|
914
|
+
saveDeployedTables()
|
|
915
|
+
{
|
|
916
|
+
localStorage.setItem('dataCloner_deployedTables', JSON.stringify(this.pict.AppData.DataCloner.DeployedTables));
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
restoreDeployedTables()
|
|
920
|
+
{
|
|
921
|
+
try
|
|
922
|
+
{
|
|
923
|
+
let tmpRaw = localStorage.getItem('dataCloner_deployedTables');
|
|
924
|
+
if (tmpRaw)
|
|
925
|
+
{
|
|
926
|
+
this.pict.AppData.DataCloner.DeployedTables = JSON.parse(tmpRaw);
|
|
927
|
+
this.pict.views['DataCloner-ViewData'].populateViewTableDropdown();
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
catch (pError) { /* ignore */ }
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// ================================================================
|
|
934
|
+
// Auto-Process
|
|
935
|
+
// ================================================================
|
|
936
|
+
|
|
937
|
+
initAutoProcess()
|
|
938
|
+
{
|
|
939
|
+
let tmpSelf = this;
|
|
940
|
+
this.api('GET', '/clone/sync/live-status')
|
|
941
|
+
.then(function(pData)
|
|
942
|
+
{
|
|
943
|
+
if (pData.Phase === 'syncing' || pData.Phase === 'stopping')
|
|
944
|
+
{
|
|
945
|
+
tmpSelf.pict.AppData.DataCloner.ServerBusyAtLoad = true;
|
|
946
|
+
tmpSelf.setSectionPhase(5, 'busy');
|
|
947
|
+
tmpSelf.pict.views['DataCloner-Sync'].startPolling();
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
tmpSelf.runAutoProcessChain();
|
|
951
|
+
})
|
|
952
|
+
.catch(function()
|
|
953
|
+
{
|
|
954
|
+
// Server unreachable — don't auto-process
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
runAutoProcessChain()
|
|
959
|
+
{
|
|
960
|
+
let tmpSelf = this;
|
|
961
|
+
let tmpDelay = 0;
|
|
962
|
+
let tmpStepDelay = 2000;
|
|
963
|
+
|
|
964
|
+
if (document.getElementById('auto1') && document.getElementById('auto1').checked)
|
|
965
|
+
{
|
|
966
|
+
setTimeout(function() { tmpSelf.pict.views['DataCloner-Connection'].connectProvider(); }, tmpDelay);
|
|
967
|
+
tmpDelay += tmpStepDelay;
|
|
968
|
+
}
|
|
969
|
+
if (document.getElementById('auto2') && document.getElementById('auto2').checked)
|
|
970
|
+
{
|
|
971
|
+
setTimeout(function() { tmpSelf.pict.views['DataCloner-Session'].goAction(); }, tmpDelay);
|
|
972
|
+
tmpDelay += tmpStepDelay + 1500;
|
|
973
|
+
}
|
|
974
|
+
if (document.getElementById('auto3') && document.getElementById('auto3').checked)
|
|
975
|
+
{
|
|
976
|
+
setTimeout(function() { tmpSelf.pict.views['DataCloner-Schema'].fetchSchema(); }, tmpDelay);
|
|
977
|
+
tmpDelay += tmpStepDelay;
|
|
978
|
+
}
|
|
979
|
+
if (document.getElementById('auto4') && document.getElementById('auto4').checked)
|
|
980
|
+
{
|
|
981
|
+
setTimeout(function() { tmpSelf.pict.views['DataCloner-Deploy'].deploySchema(); }, tmpDelay);
|
|
982
|
+
tmpDelay += tmpStepDelay;
|
|
983
|
+
}
|
|
984
|
+
if (document.getElementById('auto5') && document.getElementById('auto5').checked)
|
|
985
|
+
{
|
|
986
|
+
setTimeout(function() { tmpSelf.pict.views['DataCloner-Sync'].startSync(); }, tmpDelay);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
module.exports = DataClonerProvider;
|
|
992
|
+
|
|
993
|
+
module.exports.default_configuration =
|
|
994
|
+
{
|
|
995
|
+
ProviderIdentifier: 'DataCloner',
|
|
996
|
+
AutoInitialize: true,
|
|
997
|
+
AutoInitializeOrdinal: 0
|
|
998
|
+
};
|