retold-data-service 2.0.21 → 2.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/.quackage-comprehension-loader.json +19 -0
  2. package/bin/retold-data-service-clone.js +4 -1
  3. package/generate-bookstore-comprehension.js +645 -0
  4. package/package.json +7 -7
  5. package/source/Retold-Data-Service.js +30 -2
  6. package/source/services/comprehension-loader/ComprehensionLoader-Command-Load.js +345 -0
  7. package/source/services/comprehension-loader/ComprehensionLoader-Command-Schema.js +97 -0
  8. package/source/services/comprehension-loader/ComprehensionLoader-Command-Session.js +221 -0
  9. package/source/services/comprehension-loader/ComprehensionLoader-Command-WebUI.js +57 -0
  10. package/source/services/comprehension-loader/Retold-Data-Service-ComprehensionLoader.js +536 -0
  11. package/source/services/comprehension-loader/pict-app/Pict-Application-ComprehensionLoader-Configuration.json +9 -0
  12. package/source/services/comprehension-loader/pict-app/Pict-Application-ComprehensionLoader.js +86 -0
  13. package/source/services/comprehension-loader/pict-app/Pict-ComprehensionLoader-Bundle.js +6 -0
  14. package/source/services/comprehension-loader/pict-app/providers/Pict-Provider-ComprehensionLoader.js +760 -0
  15. package/source/services/comprehension-loader/pict-app/views/PictView-ComprehensionLoader-Layout.js +360 -0
  16. package/source/services/comprehension-loader/pict-app/views/PictView-ComprehensionLoader-Load.js +472 -0
  17. package/source/services/comprehension-loader/pict-app/views/PictView-ComprehensionLoader-Schema.js +119 -0
  18. package/source/services/comprehension-loader/pict-app/views/PictView-ComprehensionLoader-Session.js +269 -0
  19. package/source/services/comprehension-loader/pict-app/views/PictView-ComprehensionLoader-Source.js +330 -0
  20. package/source/services/comprehension-loader/web/comprehension-loader.js +6794 -0
  21. package/source/services/comprehension-loader/web/comprehension-loader.js.map +1 -0
  22. package/source/services/comprehension-loader/web/comprehension-loader.min.js +2 -0
  23. package/source/services/comprehension-loader/web/comprehension-loader.min.js.map +1 -0
  24. package/source/services/comprehension-loader/web/index.html +17 -0
  25. package/source/services/data-cloner/DataCloner-Command-Schema.js +407 -15
  26. package/source/services/data-cloner/Retold-Data-Service-DataCloner.js +59 -1
  27. package/source/services/data-cloner/pict-app/Pict-Application-DataCloner.js +1 -0
  28. package/source/services/data-cloner/pict-app/providers/Pict-Provider-DataCloner.js +125 -5
  29. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Connection.js +18 -8
  30. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Deploy.js +104 -1
  31. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Export.js +1 -1
  32. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Layout.js +12 -0
  33. package/source/services/data-cloner/web/data-cloner.js +201 -139
  34. package/source/services/data-cloner/web/data-cloner.js.map +1 -1
  35. package/source/services/data-cloner/web/data-cloner.min.js +1 -1
  36. package/source/services/data-cloner/web/data-cloner.min.js.map +1 -1
  37. package/test/RetoldDataService_tests.js +225 -0
@@ -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
- if (tmpFillMode) {
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
- if (tmpFillMode) {
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 (tmpFillMode) {
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 (skipped in fill mode — values don't fit in narrow columns)
2563
- if (pOptions.ShowValues && !tmpFillMode) {
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 (skipped in fill mode — labels rendered in a separate row)
2569
- if (pOptions.ShowLabels && !tmpFillMode) {
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 (non-fill), measure the longest label so all labels share the same width
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 && !pView.options.FillContainer) {
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}${tmpFillClass}">`;
2708
- tmpHTML += `<div class="pict-histogram-chart ${tmpOrientationClass}${tmpFillClass}">`;
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 || 'data/cloned.sqlite';
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 || 'data/rocksdb';
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 || 'data/bibliograph';
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
- if ((pData.Phase === 'syncing' || pData.Phase === 'stopping') && !this.pict.AppData.DataCloner.StatusDetailExpanded) {
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() || 'data/cloned.sqlite';
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
- this.pict.providers.DataCloner.setStatus('connectionStatus', pData.Message, 'ok');
5894
- this.pict.providers.DataCloner.setSectionPhase(1, 'ok');
5874
+ tmpSelf.pict.providers.DataCloner.setStatus('connectionStatus', pData.Message, 'ok');
5875
+ tmpSelf.pict.providers.DataCloner.setSectionPhase(1, 'ok');
5895
5876
  } else {
5896
- this.pict.providers.DataCloner.setStatus('connectionStatus', 'Connection failed: ' + (pData.Error || 'Unknown error'), 'error');
5897
- this.pict.providers.DataCloner.setSectionPhase(1, 'error');
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
- this.pict.providers.DataCloner.setStatus('connectionStatus', 'Request failed: ' + pError.message, 'error');
5901
- this.pict.providers.DataCloner.setSectionPhase(1, 'error');
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="data/cloned.sqlite" value="data/cloned.sqlite">
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
- tmpSelf.pict.providers.DataCloner.setStatus('deployStatus', pData.Message, 'ok');
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() || 'data/cloned.sqlite';
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; }