retold-data-service 2.0.21 → 2.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.quackage-comprehension-loader.json +19 -0
- package/bin/retold-data-service-clone.js +4 -1
- package/generate-bookstore-comprehension.js +645 -0
- package/package.json +7 -7
- package/source/Retold-Data-Service.js +30 -2
- package/source/services/comprehension-loader/ComprehensionLoader-Command-Load.js +345 -0
- package/source/services/comprehension-loader/ComprehensionLoader-Command-Schema.js +97 -0
- package/source/services/comprehension-loader/ComprehensionLoader-Command-Session.js +221 -0
- package/source/services/comprehension-loader/ComprehensionLoader-Command-WebUI.js +57 -0
- package/source/services/comprehension-loader/Retold-Data-Service-ComprehensionLoader.js +536 -0
- package/source/services/comprehension-loader/pict-app/Pict-Application-ComprehensionLoader-Configuration.json +9 -0
- package/source/services/comprehension-loader/pict-app/Pict-Application-ComprehensionLoader.js +86 -0
- package/source/services/comprehension-loader/pict-app/Pict-ComprehensionLoader-Bundle.js +6 -0
- package/source/services/comprehension-loader/pict-app/providers/Pict-Provider-ComprehensionLoader.js +760 -0
- package/source/services/comprehension-loader/pict-app/views/PictView-ComprehensionLoader-Layout.js +360 -0
- package/source/services/comprehension-loader/pict-app/views/PictView-ComprehensionLoader-Load.js +472 -0
- package/source/services/comprehension-loader/pict-app/views/PictView-ComprehensionLoader-Schema.js +119 -0
- package/source/services/comprehension-loader/pict-app/views/PictView-ComprehensionLoader-Session.js +269 -0
- package/source/services/comprehension-loader/pict-app/views/PictView-ComprehensionLoader-Source.js +330 -0
- package/source/services/comprehension-loader/web/comprehension-loader.js +6794 -0
- package/source/services/comprehension-loader/web/comprehension-loader.js.map +1 -0
- package/source/services/comprehension-loader/web/comprehension-loader.min.js +2 -0
- package/source/services/comprehension-loader/web/comprehension-loader.min.js.map +1 -0
- package/source/services/comprehension-loader/web/index.html +17 -0
- package/source/services/data-cloner/DataCloner-Command-Schema.js +407 -15
- package/source/services/data-cloner/Retold-Data-Service-DataCloner.js +59 -1
- package/source/services/data-cloner/pict-app/Pict-Application-DataCloner.js +1 -0
- package/source/services/data-cloner/pict-app/providers/Pict-Provider-DataCloner.js +125 -5
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Connection.js +18 -8
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Deploy.js +104 -1
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Export.js +1 -1
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Layout.js +12 -0
- package/source/services/data-cloner/web/data-cloner.js +201 -139
- 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/RetoldDataService_tests.js +225 -0
|
@@ -1856,20 +1856,10 @@
|
|
|
1856
1856
|
"BarThickness": 30,
|
|
1857
1857
|
// Gap between bars in pixels (browser) or characters (cli/consoleui)
|
|
1858
1858
|
"BarGap": 4,
|
|
1859
|
-
// When true, bar groups expand to fill the container width (vertical) or
|
|
1860
|
-
// height (horizontal) using CSS flex-grow instead of a fixed BarThickness.
|
|
1861
|
-
// Labels and values overflow their column so they remain readable even when
|
|
1862
|
-
// bars are very narrow. Best suited for time-series or dense histograms.
|
|
1863
|
-
"FillContainer": false,
|
|
1864
1859
|
// Whether to show value labels on/above bars
|
|
1865
1860
|
"ShowValues": true,
|
|
1866
1861
|
// Whether to show bin labels (x-axis for vertical, y-axis for horizontal)
|
|
1867
1862
|
"ShowLabels": true,
|
|
1868
|
-
// In FillContainer mode, controls label density in the separate label row.
|
|
1869
|
-
// 0 = auto-compute (space labels approximately 80px apart based on container
|
|
1870
|
-
// width), N > 0 = show a label every N bars starting from index 0.
|
|
1871
|
-
// Ignored when FillContainer is false (every bar shows its own label).
|
|
1872
|
-
"LabelInterval": 0,
|
|
1873
1863
|
// Color of the bars (CSS color for browser, ANSI color name for cli/consoleui)
|
|
1874
1864
|
"BarColor": "#4A90D9",
|
|
1875
1865
|
// Color of selected bars
|
|
@@ -2075,44 +2065,6 @@
|
|
|
2075
2065
|
box-shadow: 0 0 0 3px rgba(74, 144, 217, 0.3);
|
|
2076
2066
|
outline: none;
|
|
2077
2067
|
}
|
|
2078
|
-
.pict-histogram-container.pict-histogram-fill
|
|
2079
|
-
{
|
|
2080
|
-
display: block;
|
|
2081
|
-
width: 100%;
|
|
2082
|
-
}
|
|
2083
|
-
.pict-histogram-fill .pict-histogram-chart
|
|
2084
|
-
{
|
|
2085
|
-
width: 100%;
|
|
2086
|
-
}
|
|
2087
|
-
.pict-histogram-fill .pict-histogram-bar-group
|
|
2088
|
-
{
|
|
2089
|
-
flex: 1 1 0%;
|
|
2090
|
-
min-width: 0;
|
|
2091
|
-
}
|
|
2092
|
-
.pict-histogram-fill .pict-histogram-bar
|
|
2093
|
-
{
|
|
2094
|
-
width: 100%;
|
|
2095
|
-
}
|
|
2096
|
-
.pict-histogram-axis-line
|
|
2097
|
-
{
|
|
2098
|
-
width: 100%;
|
|
2099
|
-
height: 1px;
|
|
2100
|
-
background: #ccc;
|
|
2101
|
-
}
|
|
2102
|
-
.pict-histogram-label-row
|
|
2103
|
-
{
|
|
2104
|
-
display: flex;
|
|
2105
|
-
width: 100%;
|
|
2106
|
-
}
|
|
2107
|
-
.pict-histogram-fill-label
|
|
2108
|
-
{
|
|
2109
|
-
font-size: 10px;
|
|
2110
|
-
color: #666;
|
|
2111
|
-
text-align: center;
|
|
2112
|
-
white-space: nowrap;
|
|
2113
|
-
overflow: visible;
|
|
2114
|
-
line-height: 16px;
|
|
2115
|
-
}
|
|
2116
2068
|
`
|
|
2117
2069
|
};
|
|
2118
2070
|
}, {}],
|
|
@@ -2532,41 +2484,29 @@
|
|
|
2532
2484
|
let tmpSelectableClass = pOptions.Selectable ? ' pict-histogram-selectable' : '';
|
|
2533
2485
|
let tmpSelectedClass = pIsSelected ? ' pict-histogram-selected' : '';
|
|
2534
2486
|
let tmpInRangeClass = pInRange ? ' pict-histogram-in-range' : '';
|
|
2535
|
-
let tmpFillMode = pOptions.FillContainer;
|
|
2536
2487
|
let tmpBarStyle = '';
|
|
2537
2488
|
if (tmpVertical) {
|
|
2538
|
-
|
|
2539
|
-
tmpBarStyle = `height:${pBarSize}px;background-color:${tmpBarColor};`;
|
|
2540
|
-
} else {
|
|
2541
|
-
tmpBarStyle = `height:${pBarSize}px;width:${pOptions.BarThickness}px;background-color:${tmpBarColor};`;
|
|
2542
|
-
}
|
|
2489
|
+
tmpBarStyle = `height:${pBarSize}px;width:${pOptions.BarThickness}px;background-color:${tmpBarColor};`;
|
|
2543
2490
|
} else {
|
|
2544
|
-
|
|
2545
|
-
tmpBarStyle = `width:${pBarSize}px;background-color:${tmpBarColor};`;
|
|
2546
|
-
} else {
|
|
2547
|
-
tmpBarStyle = `width:${pBarSize}px;height:${pOptions.BarThickness}px;background-color:${tmpBarColor};`;
|
|
2548
|
-
}
|
|
2491
|
+
tmpBarStyle = `width:${pBarSize}px;height:${pOptions.BarThickness}px;background-color:${tmpBarColor};`;
|
|
2549
2492
|
}
|
|
2550
2493
|
let tmpGroupWidth = pOptions.BarThickness + pOptions.BarGap;
|
|
2551
2494
|
let tmpGroupStyle = '';
|
|
2552
|
-
if (
|
|
2553
|
-
// No fixed dimensions — CSS flex:1 handles sizing
|
|
2554
|
-
tmpGroupStyle = '';
|
|
2555
|
-
} else if (tmpVertical) {
|
|
2495
|
+
if (tmpVertical) {
|
|
2556
2496
|
tmpGroupStyle = `margin:0 ${pOptions.BarGap / 2}px;width:${tmpGroupWidth}px;`;
|
|
2557
2497
|
} else {
|
|
2558
2498
|
tmpGroupStyle = `margin:${pOptions.BarGap / 2}px 0;`;
|
|
2559
2499
|
}
|
|
2560
2500
|
let tmpHTML = `<div class="pict-histogram-bar-group" style="${tmpGroupStyle}" data-histogram-index="${pIndex}">`;
|
|
2561
2501
|
if (tmpVertical) {
|
|
2562
|
-
// Value label above bar
|
|
2563
|
-
if (pOptions.ShowValues
|
|
2502
|
+
// Value label above bar
|
|
2503
|
+
if (pOptions.ShowValues) {
|
|
2564
2504
|
tmpHTML += `<div class="pict-histogram-value-label" style="width:${tmpGroupWidth}px;">${tmpValue}</div>`;
|
|
2565
2505
|
}
|
|
2566
2506
|
// Bar
|
|
2567
2507
|
tmpHTML += `<div class="pict-histogram-bar${tmpSelectableClass}${tmpSelectedClass}${tmpInRangeClass}" style="${tmpBarStyle}" data-histogram-index="${pIndex}"></div>`;
|
|
2568
|
-
// Bin label below bar
|
|
2569
|
-
if (pOptions.ShowLabels
|
|
2508
|
+
// Bin label below bar
|
|
2509
|
+
if (pOptions.ShowLabels) {
|
|
2570
2510
|
tmpHTML += `<div class="pict-histogram-bin-label" style="width:${tmpGroupWidth}px;">${tmpLabel}</div>`;
|
|
2571
2511
|
}
|
|
2572
2512
|
} else {
|
|
@@ -2620,52 +2560,6 @@
|
|
|
2620
2560
|
return tmpHTML;
|
|
2621
2561
|
}
|
|
2622
2562
|
|
|
2623
|
-
/**
|
|
2624
|
-
* Build the label row for FillContainer vertical mode.
|
|
2625
|
-
*
|
|
2626
|
-
* Labels are rendered in a separate flex row below the axis line, with
|
|
2627
|
-
* automatic interval calculation to avoid overlap when there are many bins.
|
|
2628
|
-
*
|
|
2629
|
-
* @param {object} pView - The histogram view instance
|
|
2630
|
-
* @param {Array} pBins - The bin data array
|
|
2631
|
-
* @returns {string} HTML fragment
|
|
2632
|
-
*/
|
|
2633
|
-
function buildFillLabelRow(pView, pBins) {
|
|
2634
|
-
if (!pBins || pBins.length === 0) {
|
|
2635
|
-
return '';
|
|
2636
|
-
}
|
|
2637
|
-
|
|
2638
|
-
// Determine label interval: explicit setting or auto-compute
|
|
2639
|
-
let tmpLabelInterval = pView.options.LabelInterval || 0;
|
|
2640
|
-
if (tmpLabelInterval <= 0) {
|
|
2641
|
-
// Auto-compute: space labels approximately 80px apart
|
|
2642
|
-
let tmpTargetElementSet = pView.services.ContentAssignment.getElement(pView.options.TargetElementAddress);
|
|
2643
|
-
let tmpContainerWidth = 800;
|
|
2644
|
-
if (tmpTargetElementSet && tmpTargetElementSet.length > 0 && tmpTargetElementSet[0]) {
|
|
2645
|
-
tmpContainerWidth = tmpTargetElementSet[0].clientWidth || 800;
|
|
2646
|
-
}
|
|
2647
|
-
if (pBins.length > 0) {
|
|
2648
|
-
let tmpBarWidth = tmpContainerWidth / pBins.length;
|
|
2649
|
-
tmpLabelInterval = Math.max(1, Math.ceil(80 / tmpBarWidth));
|
|
2650
|
-
} else {
|
|
2651
|
-
tmpLabelInterval = 1;
|
|
2652
|
-
}
|
|
2653
|
-
}
|
|
2654
|
-
let tmpHTML = '<div class="pict-histogram-label-row">';
|
|
2655
|
-
for (let i = 0; i < pBins.length; i++) {
|
|
2656
|
-
let tmpIsLabeled = i % tmpLabelInterval === 0;
|
|
2657
|
-
if (tmpIsLabeled) {
|
|
2658
|
-
let tmpLabel = pBins[i][pView.options.LabelProperty] || '';
|
|
2659
|
-
// Span covers this label and the unlabeled bars until the next label
|
|
2660
|
-
let tmpSpan = Math.min(tmpLabelInterval, pBins.length - i);
|
|
2661
|
-
tmpHTML += `<div class="pict-histogram-fill-label" style="flex:${tmpSpan};">${tmpLabel}</div>`;
|
|
2662
|
-
i += tmpSpan - 1;
|
|
2663
|
-
}
|
|
2664
|
-
}
|
|
2665
|
-
tmpHTML += '</div>';
|
|
2666
|
-
return tmpHTML;
|
|
2667
|
-
}
|
|
2668
|
-
|
|
2669
2563
|
/**
|
|
2670
2564
|
* Render the full histogram into the target element.
|
|
2671
2565
|
*
|
|
@@ -2689,11 +2583,10 @@
|
|
|
2689
2583
|
}
|
|
2690
2584
|
let tmpVertical = pView.options.Orientation === 'vertical';
|
|
2691
2585
|
let tmpOrientationClass = tmpVertical ? 'pict-histogram-vertical' : 'pict-histogram-horizontal';
|
|
2692
|
-
let tmpFillClass = pView.options.FillContainer ? ' pict-histogram-fill' : '';
|
|
2693
2586
|
|
|
2694
|
-
// For horizontal mode
|
|
2587
|
+
// For horizontal mode, measure the longest label so all labels share the same width
|
|
2695
2588
|
let tmpLabelWidth = 0;
|
|
2696
|
-
if (!tmpVertical && pView.options.ShowLabels
|
|
2589
|
+
if (!tmpVertical && pView.options.ShowLabels) {
|
|
2697
2590
|
for (let i = 0; i < tmpBins.length; i++) {
|
|
2698
2591
|
let tmpLabel = String(tmpBins[i][pView.options.LabelProperty] || '');
|
|
2699
2592
|
// Approximate character width at 11px font: ~6.5px per character
|
|
@@ -2704,8 +2597,8 @@
|
|
|
2704
2597
|
}
|
|
2705
2598
|
tmpLabelWidth = Math.max(tmpLabelWidth, 40);
|
|
2706
2599
|
}
|
|
2707
|
-
let tmpHTML = `<div class="pict-histogram-container ${tmpOrientationClass}
|
|
2708
|
-
tmpHTML += `<div class="pict-histogram-chart ${tmpOrientationClass}
|
|
2600
|
+
let tmpHTML = `<div class="pict-histogram-container ${tmpOrientationClass}">`;
|
|
2601
|
+
tmpHTML += `<div class="pict-histogram-chart ${tmpOrientationClass}">`;
|
|
2709
2602
|
for (let i = 0; i < tmpBins.length; i++) {
|
|
2710
2603
|
let tmpVal = tmpBins[i][pView.options.ValueProperty] || 0;
|
|
2711
2604
|
let tmpBarSize = Math.round(tmpVal / tmpMaxValue * pView.options.MaxBarSize);
|
|
@@ -2718,12 +2611,6 @@
|
|
|
2718
2611
|
}
|
|
2719
2612
|
tmpHTML += '</div>';
|
|
2720
2613
|
|
|
2721
|
-
// In FillContainer vertical mode, render axis line and label row separately
|
|
2722
|
-
if (pView.options.FillContainer && tmpVertical && pView.options.ShowLabels) {
|
|
2723
|
-
tmpHTML += '<div class="pict-histogram-axis-line"></div>';
|
|
2724
|
-
tmpHTML += buildFillLabelRow(pView, tmpBins);
|
|
2725
|
-
}
|
|
2726
|
-
|
|
2727
2614
|
// Range slider for "range" selection mode
|
|
2728
2615
|
if (pView.options.Selectable && pView.options.SelectionMode === 'range') {
|
|
2729
2616
|
tmpHTML += buildRangeSliderHTML(pView);
|
|
@@ -4889,6 +4776,7 @@
|
|
|
4889
4776
|
SyncPollTimer: null,
|
|
4890
4777
|
LiveStatusTimer: null,
|
|
4891
4778
|
StatusDetailExpanded: false,
|
|
4779
|
+
StatusDetailAutoExpanded: false,
|
|
4892
4780
|
StatusDetailTimer: null,
|
|
4893
4781
|
StatusDetailData: null,
|
|
4894
4782
|
LastLiveStatus: null,
|
|
@@ -5009,7 +4897,7 @@
|
|
|
5009
4897
|
tmpProvider = tmpProvider.value;
|
|
5010
4898
|
let tmpPreview1 = tmpProvider;
|
|
5011
4899
|
if (tmpProvider === 'SQLite') {
|
|
5012
|
-
let tmpPath = document.getElementById('sqliteFilePath').value || '
|
|
4900
|
+
let tmpPath = document.getElementById('sqliteFilePath').value || '~/headlight-liveconnect-local/cloned.sqlite';
|
|
5013
4901
|
tmpPreview1 = 'SQLite at ' + tmpPath;
|
|
5014
4902
|
} else if (tmpProvider === 'MySQL') {
|
|
5015
4903
|
let tmpHost = document.getElementById('mysqlServer').value || '127.0.0.1';
|
|
@@ -5035,10 +4923,10 @@
|
|
|
5035
4923
|
let tmpPort = document.getElementById('solrPort').value || '8983';
|
|
5036
4924
|
tmpPreview1 = 'Solr on ' + tmpHost + ':' + tmpPort;
|
|
5037
4925
|
} else if (tmpProvider === 'RocksDB') {
|
|
5038
|
-
let tmpFolder = document.getElementById('rocksdbFolder').value || '
|
|
4926
|
+
let tmpFolder = document.getElementById('rocksdbFolder').value || '~/headlight-liveconnect-local/rocksdb';
|
|
5039
4927
|
tmpPreview1 = 'RocksDB at ' + tmpFolder;
|
|
5040
4928
|
} else if (tmpProvider === 'Bibliograph') {
|
|
5041
|
-
let tmpFolder = document.getElementById('bibliographFolder').value || '
|
|
4929
|
+
let tmpFolder = document.getElementById('bibliographFolder').value || '~/headlight-liveconnect-local/bibliograph';
|
|
5042
4930
|
tmpPreview1 = 'Bibliograph at ' + tmpFolder;
|
|
5043
4931
|
}
|
|
5044
4932
|
document.getElementById('preview1').textContent = tmpPreview1;
|
|
@@ -5334,14 +5222,22 @@
|
|
|
5334
5222
|
}
|
|
5335
5223
|
tmpProgressFill.style.width = Math.min(100, Math.round(tmpPct)) + '%';
|
|
5336
5224
|
|
|
5337
|
-
// Auto-expand the detail view when sync starts so users see counting progress
|
|
5338
|
-
|
|
5225
|
+
// Auto-expand the detail view once when sync first starts so users see counting progress.
|
|
5226
|
+
// Only expand once per sync run — if the user collapses it, respect that choice.
|
|
5227
|
+
if ((pData.Phase === 'syncing' || pData.Phase === 'stopping') && !this.pict.AppData.DataCloner.StatusDetailExpanded && !this.pict.AppData.DataCloner.StatusDetailAutoExpanded) {
|
|
5228
|
+
this.pict.AppData.DataCloner.StatusDetailAutoExpanded = true;
|
|
5339
5229
|
let tmpLayoutView = this.pict.views['DataCloner-Layout'];
|
|
5340
5230
|
if (tmpLayoutView && typeof tmpLayoutView.toggleStatusDetail === 'function') {
|
|
5341
5231
|
tmpLayoutView.toggleStatusDetail();
|
|
5342
5232
|
}
|
|
5343
5233
|
}
|
|
5344
5234
|
|
|
5235
|
+
// Reset the auto-expand flag when the sync is no longer running,
|
|
5236
|
+
// so the next sync run will auto-expand again.
|
|
5237
|
+
if (pData.Phase !== 'syncing' && pData.Phase !== 'stopping') {
|
|
5238
|
+
this.pict.AppData.DataCloner.StatusDetailAutoExpanded = false;
|
|
5239
|
+
}
|
|
5240
|
+
|
|
5345
5241
|
// If the detail view is expanded, re-render it with fresh data
|
|
5346
5242
|
if (this.pict.AppData.DataCloner.StatusDetailExpanded) {
|
|
5347
5243
|
this.renderStatusDetail();
|
|
@@ -5449,6 +5345,11 @@
|
|
|
5449
5345
|
// During the counting phase, show per-table counts as they arrive
|
|
5450
5346
|
if (tmpLiveStatus && tmpLiveStatus.PreCountProgress && tmpLiveStatus.PreCountProgress.Tables && tmpLiveStatus.Phase === 'syncing' && tmpLiveStatus.PreCountProgress.Counted < tmpLiveStatus.PreCountProgress.TotalTables) {
|
|
5451
5347
|
this.renderCountingPhaseDetail(tmpContainer, tmpLiveStatus.PreCountProgress);
|
|
5348
|
+
// Prepend the summary banner above the counting detail
|
|
5349
|
+
let tmpSummaryBanner = this.buildStatusSummaryHtml();
|
|
5350
|
+
if (tmpSummaryBanner) {
|
|
5351
|
+
tmpContainer.innerHTML = tmpSummaryBanner + tmpContainer.innerHTML;
|
|
5352
|
+
}
|
|
5452
5353
|
// Hide histogram during counting
|
|
5453
5354
|
let tmpHistContainer = document.getElementById('DataCloner-Throughput-Histogram');
|
|
5454
5355
|
if (tmpHistContainer) tmpHistContainer.style.display = 'none';
|
|
@@ -5510,6 +5411,9 @@
|
|
|
5510
5411
|
}
|
|
5511
5412
|
let tmpHtml = '';
|
|
5512
5413
|
|
|
5414
|
+
// === Summary Banner (mirrors collapsed bar counters) ===
|
|
5415
|
+
tmpHtml += this.buildStatusSummaryHtml();
|
|
5416
|
+
|
|
5513
5417
|
// === Section 1: Running Operations ===
|
|
5514
5418
|
if (tmpRunning.length > 0 || tmpPending.length > 0) {
|
|
5515
5419
|
tmpHtml += '<div class="status-detail-section">';
|
|
@@ -5634,6 +5538,76 @@
|
|
|
5634
5538
|
tmpHistView.renderHistogram();
|
|
5635
5539
|
}
|
|
5636
5540
|
}
|
|
5541
|
+
buildStatusSummaryHtml() {
|
|
5542
|
+
let tmpLiveStatus = this.pict.AppData.DataCloner.LastLiveStatus;
|
|
5543
|
+
let tmpReport = this.pict.AppData.DataCloner.LastReport;
|
|
5544
|
+
let tmpParts = [];
|
|
5545
|
+
let tmpMessage = '';
|
|
5546
|
+
if (tmpLiveStatus && (tmpLiveStatus.Phase === 'syncing' || tmpLiveStatus.Phase === 'stopping')) {
|
|
5547
|
+
tmpMessage = tmpLiveStatus.Message || '';
|
|
5548
|
+
if (tmpLiveStatus.Elapsed) {
|
|
5549
|
+
tmpParts.push('<span class="live-status-meta-item">\u23F1 ' + this.escapeHtml(tmpLiveStatus.Elapsed) + '</span>');
|
|
5550
|
+
}
|
|
5551
|
+
if (tmpLiveStatus.ETA) {
|
|
5552
|
+
tmpParts.push('<span class="live-status-meta-item">~' + this.escapeHtml(tmpLiveStatus.ETA) + ' remaining</span>');
|
|
5553
|
+
}
|
|
5554
|
+
if (tmpLiveStatus.TotalTables > 0) {
|
|
5555
|
+
tmpParts.push('<span class="live-status-meta-item"><strong>' + tmpLiveStatus.Completed + '</strong> / ' + tmpLiveStatus.TotalTables + ' tables</span>');
|
|
5556
|
+
}
|
|
5557
|
+
if (tmpLiveStatus.TotalSynced > 0) {
|
|
5558
|
+
let tmpSynced = this.formatNumber(tmpLiveStatus.TotalSynced);
|
|
5559
|
+
if (tmpLiveStatus.PreCountGrandTotal > 0) {
|
|
5560
|
+
let tmpGrandTotal = this.formatNumber(tmpLiveStatus.PreCountGrandTotal);
|
|
5561
|
+
tmpParts.push('<span class="live-status-meta-item"><strong>' + tmpSynced + '</strong> / ' + tmpGrandTotal + ' records</span>');
|
|
5562
|
+
} else {
|
|
5563
|
+
tmpParts.push('<span class="live-status-meta-item"><strong>' + tmpSynced + '</strong> records</span>');
|
|
5564
|
+
}
|
|
5565
|
+
} else if (tmpLiveStatus.PreCountGrandTotal > 0) {
|
|
5566
|
+
let tmpGrandTotal = this.formatNumber(tmpLiveStatus.PreCountGrandTotal);
|
|
5567
|
+
tmpParts.push('<span class="live-status-meta-item">' + tmpGrandTotal + ' records to sync</span>');
|
|
5568
|
+
}
|
|
5569
|
+
if (tmpLiveStatus.PreCountProgress && tmpLiveStatus.PreCountProgress.Counted < tmpLiveStatus.PreCountProgress.TotalTables) {
|
|
5570
|
+
let tmpCountedSoFar = tmpLiveStatus.PreCountGrandTotal > 0 ? ' (' + this.formatNumber(tmpLiveStatus.PreCountGrandTotal) + ' records found)' : '';
|
|
5571
|
+
tmpParts.push('<span class="live-status-meta-item">counting: ' + tmpLiveStatus.PreCountProgress.Counted + ' / ' + tmpLiveStatus.PreCountProgress.TotalTables + ' tables' + tmpCountedSoFar + '</span>');
|
|
5572
|
+
}
|
|
5573
|
+
if (tmpLiveStatus.Errors > 0) {
|
|
5574
|
+
tmpParts.push('<span class="live-status-meta-item" style="color:#dc3545"><strong>' + tmpLiveStatus.Errors + '</strong> error' + (tmpLiveStatus.Errors === 1 ? '' : 's') + '</span>');
|
|
5575
|
+
}
|
|
5576
|
+
} else if (tmpLiveStatus && tmpLiveStatus.Phase === 'complete') {
|
|
5577
|
+
tmpMessage = tmpLiveStatus.Message || 'Sync complete';
|
|
5578
|
+
if (tmpLiveStatus.Elapsed) {
|
|
5579
|
+
tmpParts.push('<span class="live-status-meta-item">\u23F1 ' + this.escapeHtml(tmpLiveStatus.Elapsed) + '</span>');
|
|
5580
|
+
}
|
|
5581
|
+
if (tmpLiveStatus.TotalSynced > 0) {
|
|
5582
|
+
let tmpSynced = this.formatNumber(tmpLiveStatus.TotalSynced);
|
|
5583
|
+
tmpParts.push('<span class="live-status-meta-item"><strong>' + tmpSynced + '</strong> records synced</span>');
|
|
5584
|
+
}
|
|
5585
|
+
} else if (tmpReport && tmpReport.ReportVersion) {
|
|
5586
|
+
tmpMessage = 'Sync ' + (tmpReport.Outcome || 'complete').toLowerCase();
|
|
5587
|
+
if (tmpReport.RunTimestamps && tmpReport.RunTimestamps.DurationSeconds) {
|
|
5588
|
+
tmpParts.push('<span class="live-status-meta-item">\u23F1 ' + this.formatElapsed(tmpReport.RunTimestamps.DurationSeconds) + '</span>');
|
|
5589
|
+
}
|
|
5590
|
+
if (tmpReport.Summary) {
|
|
5591
|
+
if (tmpReport.Summary.TotalSynced > 0) {
|
|
5592
|
+
tmpParts.push('<span class="live-status-meta-item"><strong>' + this.formatNumber(tmpReport.Summary.TotalSynced) + '</strong> records synced</span>');
|
|
5593
|
+
}
|
|
5594
|
+
tmpParts.push('<span class="live-status-meta-item"><strong>' + tmpReport.Summary.TotalTables + '</strong> tables</span>');
|
|
5595
|
+
if (tmpReport.Summary.TotalErrors > 0) {
|
|
5596
|
+
tmpParts.push('<span class="live-status-meta-item" style="color:#dc3545"><strong>' + tmpReport.Summary.TotalErrors + '</strong> error' + (tmpReport.Summary.TotalErrors === 1 ? '' : 's') + '</span>');
|
|
5597
|
+
}
|
|
5598
|
+
}
|
|
5599
|
+
}
|
|
5600
|
+
if (!tmpMessage && tmpParts.length === 0) return '';
|
|
5601
|
+
let tmpHtml = '<div class="status-detail-summary">';
|
|
5602
|
+
if (tmpMessage) {
|
|
5603
|
+
tmpHtml += '<div class="status-detail-summary-message">' + this.escapeHtml(tmpMessage) + '</div>';
|
|
5604
|
+
}
|
|
5605
|
+
if (tmpParts.length > 0) {
|
|
5606
|
+
tmpHtml += '<div class="status-detail-summary-counters">' + tmpParts.join('') + '</div>';
|
|
5607
|
+
}
|
|
5608
|
+
tmpHtml += '</div>';
|
|
5609
|
+
return tmpHtml;
|
|
5610
|
+
}
|
|
5637
5611
|
formatElapsed(pSec) {
|
|
5638
5612
|
if (pSec < 60) return pSec + 's';
|
|
5639
5613
|
if (pSec < 3600) {
|
|
@@ -5839,7 +5813,7 @@
|
|
|
5839
5813
|
let tmpProvider = document.getElementById('connProvider').value;
|
|
5840
5814
|
let tmpConfig = {};
|
|
5841
5815
|
if (tmpProvider === 'SQLite') {
|
|
5842
|
-
tmpConfig.SQLiteFilePath = document.getElementById('sqliteFilePath').value.trim() || '
|
|
5816
|
+
tmpConfig.SQLiteFilePath = document.getElementById('sqliteFilePath').value.trim() || '~/headlight-liveconnect-local/cloned.sqlite';
|
|
5843
5817
|
} else if (tmpProvider === 'MySQL') {
|
|
5844
5818
|
tmpConfig.host = document.getElementById('mysqlServer').value.trim() || '127.0.0.1';
|
|
5845
5819
|
tmpConfig.port = parseInt(document.getElementById('mysqlPort').value, 10) || 3306;
|
|
@@ -5885,20 +5859,28 @@
|
|
|
5885
5859
|
};
|
|
5886
5860
|
}
|
|
5887
5861
|
connectProvider() {
|
|
5862
|
+
// Guard against re-entrant calls (e.g. rapid auto-connect polling)
|
|
5863
|
+
if (this._connectInFlight) {
|
|
5864
|
+
return;
|
|
5865
|
+
}
|
|
5866
|
+
this._connectInFlight = true;
|
|
5888
5867
|
let tmpConnInfo = this.getProviderConfig();
|
|
5889
5868
|
this.pict.providers.DataCloner.setSectionPhase(1, 'busy');
|
|
5890
5869
|
this.pict.providers.DataCloner.setStatus('connectionStatus', 'Connecting to ' + tmpConnInfo.Provider + '...', 'info');
|
|
5870
|
+
let tmpSelf = this;
|
|
5891
5871
|
this.pict.providers.DataCloner.api('POST', '/clone/connection/configure', tmpConnInfo).then(pData => {
|
|
5872
|
+
tmpSelf._connectInFlight = false;
|
|
5892
5873
|
if (pData.Success) {
|
|
5893
|
-
|
|
5894
|
-
|
|
5874
|
+
tmpSelf.pict.providers.DataCloner.setStatus('connectionStatus', pData.Message, 'ok');
|
|
5875
|
+
tmpSelf.pict.providers.DataCloner.setSectionPhase(1, 'ok');
|
|
5895
5876
|
} else {
|
|
5896
|
-
|
|
5897
|
-
|
|
5877
|
+
tmpSelf.pict.providers.DataCloner.setStatus('connectionStatus', 'Connection failed: ' + (pData.Error || 'Unknown error'), 'error');
|
|
5878
|
+
tmpSelf.pict.providers.DataCloner.setSectionPhase(1, 'error');
|
|
5898
5879
|
}
|
|
5899
5880
|
}).catch(pError => {
|
|
5900
|
-
|
|
5901
|
-
|
|
5881
|
+
tmpSelf._connectInFlight = false;
|
|
5882
|
+
tmpSelf.pict.providers.DataCloner.setStatus('connectionStatus', 'Request failed: ' + pError.message, 'error');
|
|
5883
|
+
tmpSelf.pict.providers.DataCloner.setSectionPhase(1, 'error');
|
|
5902
5884
|
});
|
|
5903
5885
|
}
|
|
5904
5886
|
testConnection() {
|
|
@@ -5972,7 +5954,7 @@
|
|
|
5972
5954
|
<!-- SQLite Config -->
|
|
5973
5955
|
<div id="configSQLite">
|
|
5974
5956
|
<label for="sqliteFilePath">SQLite File Path</label>
|
|
5975
|
-
<input type="text" id="sqliteFilePath" placeholder="
|
|
5957
|
+
<input type="text" id="sqliteFilePath" placeholder="~/headlight-liveconnect-local/cloned.sqlite" value="~/headlight-liveconnect-local/cloned.sqlite">
|
|
5976
5958
|
</div>
|
|
5977
5959
|
|
|
5978
5960
|
<!-- MySQL Config -->
|
|
@@ -6182,7 +6164,16 @@
|
|
|
6182
6164
|
Tables: tmpSelectedTables
|
|
6183
6165
|
}).then(function (pData) {
|
|
6184
6166
|
if (pData.Success) {
|
|
6185
|
-
|
|
6167
|
+
let tmpStatusMsg = pData.Message;
|
|
6168
|
+
|
|
6169
|
+
// Append migration details if schema deltas were applied
|
|
6170
|
+
if (Array.isArray(pData.MigrationsApplied) && pData.MigrationsApplied.length > 0) {
|
|
6171
|
+
let tmpDetails = pData.MigrationsApplied.map(function (pM) {
|
|
6172
|
+
return pM.Table + ': +' + pM.ColumnsAdded.join(', +');
|
|
6173
|
+
});
|
|
6174
|
+
tmpStatusMsg += '\nMigrations: ' + tmpDetails.join('; ');
|
|
6175
|
+
}
|
|
6176
|
+
tmpSelf.pict.providers.DataCloner.setStatus('deployStatus', tmpStatusMsg, 'ok');
|
|
6186
6177
|
tmpSelf.pict.providers.DataCloner.setSectionPhase(4, 'ok');
|
|
6187
6178
|
tmpSelf.pict.AppData.DataCloner.DeployedTables = pData.TablesDeployed || tmpSelectedTables;
|
|
6188
6179
|
tmpSelf.pict.providers.DataCloner.saveDeployedTables();
|
|
@@ -6197,6 +6188,63 @@
|
|
|
6197
6188
|
tmpSelf.pict.providers.DataCloner.setSectionPhase(4, 'error');
|
|
6198
6189
|
});
|
|
6199
6190
|
}
|
|
6191
|
+
auditGUIDIndices() {
|
|
6192
|
+
let tmpReportEl = document.getElementById('guidIndexReport');
|
|
6193
|
+
if (tmpReportEl) tmpReportEl.innerHTML = '<span style="color:#888">Checking GUID indices...</span>';
|
|
6194
|
+
let tmpSelf = this;
|
|
6195
|
+
this.pict.providers.DataCloner.api('GET', '/clone/schema/guid-index-audit').then(function (pData) {
|
|
6196
|
+
if (!tmpReportEl) return;
|
|
6197
|
+
if (!pData.Success) {
|
|
6198
|
+
tmpReportEl.innerHTML = '<span style="color:red">' + (pData.Error || 'Audit failed') + '</span>';
|
|
6199
|
+
return;
|
|
6200
|
+
}
|
|
6201
|
+
if (pData.MissingCount === 0) {
|
|
6202
|
+
tmpReportEl.innerHTML = '<span style="color:green">All GUID columns have indices.</span>';
|
|
6203
|
+
return;
|
|
6204
|
+
}
|
|
6205
|
+
let tmpHTML = '<div style="margin-top:6px"><strong>' + pData.Message + '</strong></div>';
|
|
6206
|
+
tmpHTML += '<table style="font-size:0.85em; margin:6px 0; border-collapse:collapse; width:100%">';
|
|
6207
|
+
tmpHTML += '<tr style="text-align:left; border-bottom:1px solid #ccc"><th style="padding:3px 8px">Table</th><th style="padding:3px 8px">GUID Column</th><th style="padding:3px 8px">Index</th></tr>';
|
|
6208
|
+
for (let t = 0; t < pData.Tables.length; t++) {
|
|
6209
|
+
let tmpTable = pData.Tables[t];
|
|
6210
|
+
for (let c = 0; c < tmpTable.GUIDColumns.length; c++) {
|
|
6211
|
+
let tmpCol = tmpTable.GUIDColumns[c];
|
|
6212
|
+
let tmpStatus = tmpCol.HasIndex ? '<span style="color:green">' + tmpCol.IndexName + '</span>' : '<span style="color:red">MISSING</span>';
|
|
6213
|
+
tmpHTML += '<tr style="border-bottom:1px solid #eee"><td style="padding:3px 8px">' + tmpTable.Table + '</td><td style="padding:3px 8px">' + tmpCol.Column + '</td><td style="padding:3px 8px">' + tmpStatus + '</td></tr>';
|
|
6214
|
+
}
|
|
6215
|
+
}
|
|
6216
|
+
tmpHTML += '</table>';
|
|
6217
|
+
tmpHTML += '<button class="primary" style="margin-top:4px" onclick="pict.views[\'DataCloner-Deploy\'].createMissingGUIDIndices()">Create Missing Indices</button>';
|
|
6218
|
+
tmpReportEl.innerHTML = tmpHTML;
|
|
6219
|
+
}).catch(function (pError) {
|
|
6220
|
+
if (tmpReportEl) tmpReportEl.innerHTML = '<span style="color:red">Request failed: ' + pError.message + '</span>';
|
|
6221
|
+
});
|
|
6222
|
+
}
|
|
6223
|
+
createMissingGUIDIndices() {
|
|
6224
|
+
let tmpReportEl = document.getElementById('guidIndexReport');
|
|
6225
|
+
if (tmpReportEl) tmpReportEl.innerHTML = '<span style="color:#888">Creating GUID indices...</span>';
|
|
6226
|
+
let tmpSelf = this;
|
|
6227
|
+
this.pict.providers.DataCloner.api('POST', '/clone/schema/guid-index-create').then(function (pData) {
|
|
6228
|
+
if (!tmpReportEl) return;
|
|
6229
|
+
if (!pData.Success) {
|
|
6230
|
+
tmpReportEl.innerHTML = '<span style="color:red">' + (pData.Error || 'Index creation failed') + '</span>';
|
|
6231
|
+
return;
|
|
6232
|
+
}
|
|
6233
|
+
let tmpHTML = '<div style="margin-top:6px; color:green"><strong>' + pData.Message + '</strong></div>';
|
|
6234
|
+
if (pData.IndicesCreated && pData.IndicesCreated.length > 0) {
|
|
6235
|
+
tmpHTML += '<ul style="font-size:0.85em; margin:4px 0">';
|
|
6236
|
+
for (let i = 0; i < pData.IndicesCreated.length; i++) {
|
|
6237
|
+
let tmpIdx = pData.IndicesCreated[i];
|
|
6238
|
+
tmpHTML += '<li>' + tmpIdx.Table + ': ' + tmpIdx.IndexName + '</li>';
|
|
6239
|
+
}
|
|
6240
|
+
tmpHTML += '</ul>';
|
|
6241
|
+
}
|
|
6242
|
+
tmpHTML += '<button style="margin-top:4px" onclick="pict.views[\'DataCloner-Deploy\'].auditGUIDIndices()">Re-check</button>';
|
|
6243
|
+
tmpReportEl.innerHTML = tmpHTML;
|
|
6244
|
+
}).catch(function (pError) {
|
|
6245
|
+
if (tmpReportEl) tmpReportEl.innerHTML = '<span style="color:red">Request failed: ' + pError.message + '</span>';
|
|
6246
|
+
});
|
|
6247
|
+
}
|
|
6200
6248
|
resetDatabase() {
|
|
6201
6249
|
if (!confirm('This will delete ALL data in the local SQLite database. Continue?')) {
|
|
6202
6250
|
return;
|
|
@@ -6241,8 +6289,10 @@
|
|
|
6241
6289
|
<div class="accordion-body">
|
|
6242
6290
|
<p style="font-size:0.9em; color:#666; margin-bottom:10px">Creates the selected tables in the local database and sets up CRUD endpoints (e.g. GET /1.0/Documents).</p>
|
|
6243
6291
|
<button class="primary" onclick="pict.views['DataCloner-Deploy'].deploySchema()">Deploy Selected Tables</button>
|
|
6292
|
+
<button onclick="pict.views['DataCloner-Deploy'].auditGUIDIndices()">Check GUID Indices</button>
|
|
6244
6293
|
<button class="danger" onclick="pict.views['DataCloner-Deploy'].resetDatabase()">Reset Database</button>
|
|
6245
6294
|
<div id="deployStatus"></div>
|
|
6295
|
+
<div id="guidIndexReport"></div>
|
|
6246
6296
|
</div>
|
|
6247
6297
|
</div>
|
|
6248
6298
|
</div>
|
|
@@ -6274,7 +6324,7 @@
|
|
|
6274
6324
|
};
|
|
6275
6325
|
let tmpDbConfig = tmpConfig.LocalDatabase.Config;
|
|
6276
6326
|
if (tmpProvider === 'SQLite') {
|
|
6277
|
-
tmpDbConfig.SQLiteFilePath = document.getElementById('sqliteFilePath').value.trim() || '
|
|
6327
|
+
tmpDbConfig.SQLiteFilePath = document.getElementById('sqliteFilePath').value.trim() || '~/headlight-liveconnect-local/cloned.sqlite';
|
|
6278
6328
|
} else if (tmpProvider === 'MySQL') {
|
|
6279
6329
|
tmpDbConfig.host = document.getElementById('mysqlServer').value.trim() || '127.0.0.1';
|
|
6280
6330
|
tmpDbConfig.port = parseInt(document.getElementById('mysqlPort').value, 10) || 3306;
|
|
@@ -6924,6 +6974,18 @@ select { background: #fff; width: 100%; padding: 8px 12px; border: 1px solid #cc
|
|
|
6924
6974
|
padding: 12px 20px 16px; max-height: 60vh; overflow-y: auto;
|
|
6925
6975
|
}
|
|
6926
6976
|
|
|
6977
|
+
/* Status Detail Summary Banner */
|
|
6978
|
+
.status-detail-summary {
|
|
6979
|
+
display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
|
|
6980
|
+
padding: 8px 0 12px; margin-bottom: 12px; border-bottom: 1px solid #e9ecef;
|
|
6981
|
+
}
|
|
6982
|
+
.status-detail-summary-message {
|
|
6983
|
+
font-size: 0.92em; color: #333; font-weight: 600;
|
|
6984
|
+
}
|
|
6985
|
+
.status-detail-summary-counters {
|
|
6986
|
+
display: flex; gap: 16px; flex-wrap: wrap; font-size: 0.82em; color: #666;
|
|
6987
|
+
}
|
|
6988
|
+
|
|
6927
6989
|
/* Status Detail Sections */
|
|
6928
6990
|
.status-detail-section { margin-bottom: 14px; }
|
|
6929
6991
|
.status-detail-section:last-child { margin-bottom: 0; }
|