Browse Source

Run database migrations when opening board. Not when updating WeKan.

Thanks to xet7 !
Lauri Ojansivu 4 days ago
parent
commit
2b5c56484a

+ 8 - 0
client/00-startup.js

@@ -4,3 +4,11 @@ if ('serviceWorker' in navigator) {
     navigator.serviceWorker.register('/pwa-service-worker.js');
     navigator.serviceWorker.register('/pwa-service-worker.js');
   });
   });
 }
 }
+
+// Import board converter for on-demand conversion
+import '/imports/lib/boardConverter';
+import '/imports/components/boardConversionProgress';
+
+// Import migration manager and progress UI
+import '/imports/lib/migrationManager';
+import '/imports/components/migrationProgress';

+ 184 - 0
client/components/boardConversionProgress.css

@@ -0,0 +1,184 @@
+/* Board Conversion Progress Styles */
+.board-conversion-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background-color: rgba(0, 0, 0, 0.7);
+  z-index: 9999;
+  display: none;
+  align-items: center;
+  justify-content: center;
+}
+
+.board-conversion-overlay.active {
+  display: flex;
+}
+
+.board-conversion-modal {
+  background: white;
+  border-radius: 8px;
+  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
+  max-width: 500px;
+  width: 90%;
+  max-height: 80vh;
+  overflow: hidden;
+  animation: slideIn 0.3s ease-out;
+}
+
+@keyframes slideIn {
+  from {
+    opacity: 0;
+    transform: translateY(-20px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+.board-conversion-header {
+  padding: 20px 24px 16px;
+  border-bottom: 1px solid #e0e0e0;
+  text-align: center;
+}
+
+.board-conversion-header h3 {
+  margin: 0 0 8px 0;
+  color: #333;
+  font-size: 20px;
+  font-weight: 500;
+}
+
+.board-conversion-header h3 i {
+  margin-right: 8px;
+  color: #2196F3;
+}
+
+.board-conversion-header p {
+  margin: 0;
+  color: #666;
+  font-size: 14px;
+}
+
+.board-conversion-content {
+  padding: 24px;
+}
+
+.conversion-progress {
+  margin-bottom: 20px;
+}
+
+.progress-bar {
+  width: 100%;
+  height: 8px;
+  background-color: #e0e0e0;
+  border-radius: 4px;
+  overflow: hidden;
+  margin-bottom: 8px;
+}
+
+.progress-fill {
+  height: 100%;
+  background: linear-gradient(90deg, #2196F3, #21CBF3);
+  border-radius: 4px;
+  transition: width 0.3s ease;
+  position: relative;
+}
+
+.progress-fill::after {
+  content: '';
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: linear-gradient(
+    90deg,
+    transparent,
+    rgba(255, 255, 255, 0.3),
+    transparent
+  );
+  animation: shimmer 2s infinite;
+}
+
+@keyframes shimmer {
+  0% {
+    transform: translateX(-100%);
+  }
+  100% {
+    transform: translateX(100%);
+  }
+}
+
+.progress-text {
+  text-align: center;
+  font-weight: 600;
+  color: #2196F3;
+  font-size: 16px;
+}
+
+.conversion-status {
+  text-align: center;
+  margin-bottom: 16px;
+  color: #333;
+  font-size: 16px;
+}
+
+.conversion-status i {
+  margin-right: 8px;
+  color: #2196F3;
+}
+
+.conversion-time {
+  text-align: center;
+  color: #666;
+  font-size: 14px;
+  background-color: #f5f5f5;
+  padding: 8px 12px;
+  border-radius: 4px;
+  margin-bottom: 16px;
+}
+
+.conversion-time i {
+  margin-right: 6px;
+  color: #FF9800;
+}
+
+.board-conversion-footer {
+  padding: 16px 24px 20px;
+  border-top: 1px solid #e0e0e0;
+  background-color: #f9f9f9;
+}
+
+.conversion-info {
+  text-align: center;
+  color: #666;
+  font-size: 13px;
+  line-height: 1.4;
+}
+
+.conversion-info i {
+  margin-right: 6px;
+  color: #2196F3;
+}
+
+/* Responsive design */
+@media (max-width: 600px) {
+  .board-conversion-modal {
+    width: 95%;
+    margin: 20px;
+  }
+  
+  .board-conversion-header,
+  .board-conversion-content,
+  .board-conversion-footer {
+    padding-left: 16px;
+    padding-right: 16px;
+  }
+  
+  .board-conversion-header h3 {
+    font-size: 18px;
+  }
+}

+ 27 - 0
client/components/boardConversionProgress.jade

@@ -0,0 +1,27 @@
+template(name="boardConversionProgress")
+  .board-conversion-overlay(class="{{#if isConverting}}active{{/if}}")
+    .board-conversion-modal
+      .board-conversion-header
+        h3
+          i.fa.fa-cogs
+          | {{_ 'converting-board'}}
+        p {{_ 'converting-board-description'}}
+      
+      .board-conversion-content
+        .conversion-progress
+          .progress-bar
+            .progress-fill(style="width: {{conversionProgress}}%")
+          .progress-text {{conversionProgress}}%
+        
+        .conversion-status
+          i.fa.fa-spinner.fa-spin
+          | {{conversionStatus}}
+        
+        .conversion-time(style="{{#unless conversionEstimatedTime}}display: none;{{/unless}}")
+          i.fa.fa-clock-o
+          | {{_ 'estimated-time-remaining'}}: {{conversionEstimatedTime}}
+      
+      .board-conversion-footer
+        .conversion-info
+          i.fa.fa-info-circle
+          | {{_ 'conversion-info-text'}}

+ 31 - 0
client/components/boardConversionProgress.js

@@ -0,0 +1,31 @@
+import { Template } from 'meteor/templating';
+import { ReactiveVar } from 'meteor/reactive-var';
+import { boardConverter } from '/imports/lib/boardConverter';
+
+Template.boardConversionProgress.helpers({
+  isConverting() {
+    return boardConverter.isConverting.get();
+  },
+  
+  conversionProgress() {
+    return boardConverter.conversionProgress.get();
+  },
+  
+  conversionStatus() {
+    return boardConverter.conversionStatus.get();
+  },
+  
+  conversionEstimatedTime() {
+    return boardConverter.conversionEstimatedTime.get();
+  }
+});
+
+Template.boardConversionProgress.onCreated(function() {
+  // Subscribe to conversion state changes
+  this.autorun(() => {
+    boardConverter.isConverting.get();
+    boardConverter.conversionProgress.get();
+    boardConverter.conversionStatus.get();
+    boardConverter.conversionEstimatedTime.get();
+  });
+});

+ 5 - 1
client/components/boards/boardBody.jade

@@ -1,5 +1,9 @@
 template(name="board")
 template(name="board")
-  if isBoardReady.get
+  if isMigrating
+    +migrationProgress
+  else if isConverting
+    +boardConversionProgress
+  else if isBoardReady.get
     if currentBoard
     if currentBoard
       if onlyShowCurrentCard
       if onlyShowCurrentCard
         +cardDetails(currentCard)
         +cardDetails(currentCard)

+ 50 - 1
client/components/boards/boardBody.js

@@ -1,6 +1,8 @@
 import { ReactiveCache } from '/imports/reactiveCache';
 import { ReactiveCache } from '/imports/reactiveCache';
 import { TAPi18n } from '/imports/i18n';
 import { TAPi18n } from '/imports/i18n';
 import dragscroll from '@wekanteam/dragscroll';
 import dragscroll from '@wekanteam/dragscroll';
+import { boardConverter } from '/imports/lib/boardConverter';
+import { migrationManager } from '/imports/lib/migrationManager';
 
 
 const subManager = new SubsManager();
 const subManager = new SubsManager();
 const { calculateIndex } = Utils;
 const { calculateIndex } = Utils;
@@ -9,6 +11,8 @@ const swimlaneWhileSortingHeight = 150;
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
   onCreated() {
   onCreated() {
     this.isBoardReady = new ReactiveVar(false);
     this.isBoardReady = new ReactiveVar(false);
+    this.isConverting = new ReactiveVar(false);
+    this.isMigrating = new ReactiveVar(false);
 
 
     // The pattern we use to manually handle data loading is described here:
     // The pattern we use to manually handle data loading is described here:
     // https://kadira.io/academy/meteor-routing-guide/content/subscriptions-and-data-management/using-subs-manager
     // https://kadira.io/academy/meteor-routing-guide/content/subscriptions-and-data-management/using-subs-manager
@@ -20,12 +24,49 @@ BlazeComponent.extendComponent({
       const handle = subManager.subscribe('board', currentBoardId, false);
       const handle = subManager.subscribe('board', currentBoardId, false);
       Tracker.nonreactive(() => {
       Tracker.nonreactive(() => {
         Tracker.autorun(() => {
         Tracker.autorun(() => {
-          this.isBoardReady.set(handle.ready());
+          if (handle.ready()) {
+            // Check if board needs conversion
+            this.checkAndConvertBoard(currentBoardId);
+          } else {
+            this.isBoardReady.set(false);
+          }
         });
         });
       });
       });
     });
     });
   },
   },
 
 
+  async checkAndConvertBoard(boardId) {
+    try {
+      // First check if migrations need to be run
+      if (migrationManager.needsMigration()) {
+        this.isMigrating.set(true);
+        await migrationManager.startMigration();
+        this.isMigrating.set(false);
+      }
+
+      // Then check if board needs conversion
+      if (boardConverter.needsConversion(boardId)) {
+        this.isConverting.set(true);
+        const success = await boardConverter.convertBoard(boardId);
+        this.isConverting.set(false);
+        
+        if (success) {
+          this.isBoardReady.set(true);
+        } else {
+          console.error('Board conversion failed');
+          this.isBoardReady.set(true); // Still show board even if conversion failed
+        }
+      } else {
+        this.isBoardReady.set(true);
+      }
+    } catch (error) {
+      console.error('Error during board conversion check:', error);
+      this.isConverting.set(false);
+      this.isMigrating.set(false);
+      this.isBoardReady.set(true); // Show board even if conversion check failed
+    }
+  },
+
   onlyShowCurrentCard() {
   onlyShowCurrentCard() {
     return Utils.isMiniScreen() && Utils.getCurrentCardId(true);
     return Utils.isMiniScreen() && Utils.getCurrentCardId(true);
   },
   },
@@ -33,6 +74,14 @@ BlazeComponent.extendComponent({
   goHome() {
   goHome() {
     FlowRouter.go('home');
     FlowRouter.go('home');
   },
   },
+
+  isConverting() {
+    return this.isConverting.get();
+  },
+
+  isMigrating() {
+    return this.isMigrating.get();
+  },
 }).register('board');
 }).register('board');
 
 
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({

+ 2 - 0
client/components/main/layouts.jade

@@ -77,6 +77,8 @@ template(name="defaultLayout")
     | {{{afterBodyStart}}}
     | {{{afterBodyStart}}}
     +Template.dynamic(template=content)
     +Template.dynamic(template=content)
     | {{{beforeBodyEnd}}}
     | {{{beforeBodyEnd}}}
+  +migrationProgress
+  +boardConversionProgress
   if (Modal.isOpen)
   if (Modal.isOpen)
     #modal
     #modal
       .overlay
       .overlay

+ 372 - 0
client/components/migrationProgress.css

@@ -0,0 +1,372 @@
+/* Migration Progress Styles */
+.migration-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background-color: rgba(0, 0, 0, 0.8);
+  z-index: 10000;
+  display: none;
+  align-items: center;
+  justify-content: center;
+  overflow-y: auto;
+}
+
+.migration-overlay.active {
+  display: flex;
+}
+
+.migration-modal {
+  background: white;
+  border-radius: 12px;
+  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
+  max-width: 800px;
+  width: 95%;
+  max-height: 90vh;
+  overflow: hidden;
+  animation: slideInScale 0.4s ease-out;
+  margin: 20px;
+}
+
+@keyframes slideInScale {
+  from {
+    opacity: 0;
+    transform: translateY(-30px) scale(0.95);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0) scale(1);
+  }
+}
+
+.migration-header {
+  padding: 24px 32px 20px;
+  border-bottom: 2px solid #e0e0e0;
+  text-align: center;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+}
+
+.migration-header h3 {
+  margin: 0 0 8px 0;
+  font-size: 24px;
+  font-weight: 600;
+}
+
+.migration-header h3 i {
+  margin-right: 12px;
+  color: #FFD700;
+}
+
+.migration-header p {
+  margin: 0;
+  font-size: 16px;
+  opacity: 0.9;
+}
+
+.migration-content {
+  padding: 24px 32px;
+  max-height: 60vh;
+  overflow-y: auto;
+}
+
+.migration-overview {
+  margin-bottom: 32px;
+  padding: 20px;
+  background: #f8f9fa;
+  border-radius: 8px;
+  border-left: 4px solid #667eea;
+}
+
+.overall-progress {
+  margin-bottom: 20px;
+}
+
+.progress-bar {
+  width: 100%;
+  height: 12px;
+  background-color: #e0e0e0;
+  border-radius: 6px;
+  overflow: hidden;
+  margin-bottom: 8px;
+  position: relative;
+}
+
+.progress-fill {
+  height: 100%;
+  background: linear-gradient(90deg, #667eea, #764ba2);
+  border-radius: 6px;
+  transition: width 0.3s ease;
+  position: relative;
+}
+
+.progress-fill::after {
+  content: '';
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: linear-gradient(
+    90deg,
+    transparent,
+    rgba(255, 255, 255, 0.4),
+    transparent
+  );
+  animation: shimmer 2s infinite;
+}
+
+@keyframes shimmer {
+  0% {
+    transform: translateX(-100%);
+  }
+  100% {
+    transform: translateX(100%);
+  }
+}
+
+.progress-text {
+  text-align: center;
+  font-weight: 700;
+  color: #667eea;
+  font-size: 18px;
+}
+
+.progress-label {
+  text-align: center;
+  color: #666;
+  font-size: 14px;
+  margin-top: 4px;
+}
+
+.current-step {
+  text-align: center;
+  color: #333;
+  font-size: 16px;
+  font-weight: 500;
+  margin-bottom: 16px;
+}
+
+.current-step i {
+  margin-right: 8px;
+  color: #667eea;
+}
+
+.estimated-time {
+  text-align: center;
+  color: #666;
+  font-size: 14px;
+  background-color: #fff3cd;
+  padding: 8px 12px;
+  border-radius: 4px;
+  border: 1px solid #ffeaa7;
+}
+
+.estimated-time i {
+  margin-right: 6px;
+  color: #f39c12;
+}
+
+.migration-steps {
+  margin-bottom: 24px;
+}
+
+.migration-steps h4 {
+  margin: 0 0 16px 0;
+  color: #333;
+  font-size: 18px;
+  font-weight: 600;
+}
+
+.steps-list {
+  max-height: 300px;
+  overflow-y: auto;
+  border: 1px solid #e0e0e0;
+  border-radius: 8px;
+}
+
+.migration-step {
+  padding: 16px 20px;
+  border-bottom: 1px solid #f0f0f0;
+  transition: all 0.3s ease;
+}
+
+.migration-step:last-child {
+  border-bottom: none;
+}
+
+.migration-step.completed {
+  background-color: #d4edda;
+  border-left: 4px solid #28a745;
+}
+
+.migration-step.current {
+  background-color: #cce7ff;
+  border-left: 4px solid #667eea;
+  animation: pulse 2s infinite;
+}
+
+@keyframes pulse {
+  0% {
+    box-shadow: 0 0 0 0 rgba(102, 126, 234, 0.4);
+  }
+  70% {
+    box-shadow: 0 0 0 10px rgba(102, 126, 234, 0);
+  }
+  100% {
+    box-shadow: 0 0 0 0 rgba(102, 126, 234, 0);
+  }
+}
+
+.step-header {
+  display: flex;
+  align-items: center;
+  margin-bottom: 8px;
+}
+
+.step-icon {
+  margin-right: 12px;
+  font-size: 18px;
+  width: 24px;
+  text-align: center;
+}
+
+.step-icon i.fa-check-circle {
+  color: #28a745;
+}
+
+.step-icon i.fa-cog.fa-spin {
+  color: #667eea;
+}
+
+.step-icon i.fa-circle-o {
+  color: #ccc;
+}
+
+.step-info {
+  flex: 1;
+}
+
+.step-name {
+  font-weight: 600;
+  color: #333;
+  font-size: 14px;
+  margin-bottom: 2px;
+}
+
+.step-description {
+  color: #666;
+  font-size: 12px;
+  line-height: 1.3;
+}
+
+.step-progress {
+  text-align: right;
+  min-width: 40px;
+}
+
+.step-progress .progress-text {
+  font-size: 12px;
+  font-weight: 600;
+}
+
+.step-progress-bar {
+  width: 100%;
+  height: 4px;
+  background-color: #e0e0e0;
+  border-radius: 2px;
+  overflow: hidden;
+  margin-top: 8px;
+}
+
+.step-progress-bar .progress-fill {
+  height: 100%;
+  background: linear-gradient(90deg, #667eea, #764ba2);
+  border-radius: 2px;
+  transition: width 0.3s ease;
+}
+
+.migration-status {
+  text-align: center;
+  color: #333;
+  font-size: 16px;
+  background-color: #e3f2fd;
+  padding: 12px 16px;
+  border-radius: 6px;
+  border: 1px solid #bbdefb;
+  margin-bottom: 16px;
+}
+
+.migration-status i {
+  margin-right: 8px;
+  color: #2196f3;
+}
+
+.migration-footer {
+  padding: 16px 32px 24px;
+  border-top: 1px solid #e0e0e0;
+  background-color: #f8f9fa;
+}
+
+.migration-info {
+  text-align: center;
+  color: #666;
+  font-size: 13px;
+  line-height: 1.4;
+  margin-bottom: 8px;
+}
+
+.migration-info i {
+  margin-right: 6px;
+  color: #667eea;
+}
+
+.migration-warning {
+  text-align: center;
+  color: #856404;
+  font-size: 12px;
+  line-height: 1.3;
+  background-color: #fff3cd;
+  padding: 8px 12px;
+  border-radius: 4px;
+  border: 1px solid #ffeaa7;
+}
+
+.migration-warning i {
+  margin-right: 6px;
+  color: #f39c12;
+}
+
+/* Responsive design */
+@media (max-width: 768px) {
+  .migration-modal {
+    width: 98%;
+    margin: 10px;
+  }
+  
+  .migration-header,
+  .migration-content,
+  .migration-footer {
+    padding-left: 16px;
+    padding-right: 16px;
+  }
+  
+  .migration-header h3 {
+    font-size: 20px;
+  }
+  
+  .step-header {
+    flex-direction: column;
+    align-items: flex-start;
+  }
+  
+  .step-progress {
+    text-align: left;
+    margin-top: 8px;
+  }
+  
+  .steps-list {
+    max-height: 200px;
+  }
+}

+ 63 - 0
client/components/migrationProgress.jade

@@ -0,0 +1,63 @@
+template(name="migrationProgress")
+  .migration-overlay(class="{{#if isMigrating}}active{{/if}}")
+    .migration-modal
+      .migration-header
+        h3
+          i.fa.fa-database
+          | {{_ 'database-migration'}}
+        p {{_ 'database-migration-description'}}
+      
+      .migration-content
+        .migration-overview
+          .overall-progress
+            .progress-bar
+              .progress-fill(style="width: {{migrationProgress}}%")
+            .progress-text {{migrationProgress}}%
+            .progress-label {{_ 'overall-progress'}}
+          
+          .current-step
+            i.fa.fa-cog.fa-spin
+            | {{migrationCurrentStep}}
+          
+          .estimated-time(style="{{#unless migrationEstimatedTime}}display: none;{{/unless}}")
+            i.fa.fa-clock-o
+            | {{_ 'estimated-time-remaining'}}: {{migrationEstimatedTime}}
+        
+        .migration-steps
+          h4 {{_ 'migration-steps'}}
+          .steps-list
+            each migrationSteps
+              .migration-step(class="{{#if completed}}completed{{/if}}" class="{{#if isCurrentStep}}current{{/if}}")
+                .step-header
+                  .step-icon
+                    if completed
+                      i.fa.fa-check-circle
+                    else if isCurrentStep
+                      i.fa.fa-cog.fa-spin
+                    else
+                      i.fa.fa-circle-o
+                  .step-info
+                    .step-name {{name}}
+                    .step-description {{description}}
+                  .step-progress
+                    if completed
+                      .progress-text 100%
+                    else if isCurrentStep
+                      .progress-text {{progress}}%
+                    else
+                      .progress-text 0%
+                if isCurrentStep
+                  .step-progress-bar
+                    .progress-fill(style="width: {{progress}}%")
+        
+        .migration-status
+          i.fa.fa-info-circle
+          | {{migrationStatus}}
+      
+      .migration-footer
+        .migration-info
+          i.fa.fa-lightbulb-o
+          | {{_ 'migration-info-text'}}
+        .migration-warning
+          i.fa.fa-exclamation-triangle
+          | {{_ 'migration-warning-text'}}

+ 46 - 0
client/components/migrationProgress.js

@@ -0,0 +1,46 @@
+import { Template } from 'meteor/templating';
+import { migrationManager } from '/imports/lib/migrationManager';
+
+Template.migrationProgress.helpers({
+  isMigrating() {
+    return migrationManager.isMigrating.get();
+  },
+  
+  migrationProgress() {
+    return migrationManager.migrationProgress.get();
+  },
+  
+  migrationStatus() {
+    return migrationManager.migrationStatus.get();
+  },
+  
+  migrationCurrentStep() {
+    return migrationManager.migrationCurrentStep.get();
+  },
+  
+  migrationEstimatedTime() {
+    return migrationManager.migrationEstimatedTime.get();
+  },
+  
+  migrationSteps() {
+    const steps = migrationManager.migrationSteps.get();
+    const currentStep = migrationManager.migrationCurrentStep.get();
+    
+    return steps.map(step => ({
+      ...step,
+      isCurrentStep: step.name === currentStep
+    }));
+  }
+});
+
+Template.migrationProgress.onCreated(function() {
+  // Subscribe to migration state changes
+  this.autorun(() => {
+    migrationManager.isMigrating.get();
+    migrationManager.migrationProgress.get();
+    migrationManager.migrationStatus.get();
+    migrationManager.migrationCurrentStep.get();
+    migrationManager.migrationEstimatedTime.get();
+    migrationManager.migrationSteps.get();
+  });
+});

+ 203 - 0
client/lib/boardConverter.js

@@ -0,0 +1,203 @@
+/**
+ * Board Conversion Service
+ * Handles conversion of boards from old database structure to new structure
+ * without running migrations that could cause downtime
+ */
+
+import { ReactiveVar } from 'meteor/reactive-var';
+import { ReactiveCache } from '/imports/lib/reactiveCache';
+
+// Reactive variables for conversion progress
+export const conversionProgress = new ReactiveVar(0);
+export const conversionStatus = new ReactiveVar('');
+export const conversionEstimatedTime = new ReactiveVar('');
+export const isConverting = new ReactiveVar(false);
+
+class BoardConverter {
+  constructor() {
+    this.conversionCache = new Map(); // Cache converted board IDs
+  }
+
+  /**
+   * Check if a board needs conversion
+   * @param {string} boardId - The board ID to check
+   * @returns {boolean} - True if board needs conversion
+   */
+  needsConversion(boardId) {
+    if (this.conversionCache.has(boardId)) {
+      return false; // Already converted
+    }
+
+    try {
+      const board = ReactiveCache.getBoard(boardId);
+      if (!board) return false;
+
+      // Check if any lists in this board don't have swimlaneId
+      const lists = ReactiveCache.getLists({
+        boardId: boardId,
+        $or: [
+          { swimlaneId: { $exists: false } },
+          { swimlaneId: '' },
+          { swimlaneId: null }
+        ]
+      });
+
+      return lists.length > 0;
+    } catch (error) {
+      console.error('Error checking if board needs conversion:', error);
+      return false;
+    }
+  }
+
+  /**
+   * Convert a board from old structure to new structure
+   * @param {string} boardId - The board ID to convert
+   * @returns {Promise<boolean>} - True if conversion was successful
+   */
+  async convertBoard(boardId) {
+    if (this.conversionCache.has(boardId)) {
+      return true; // Already converted
+    }
+
+    isConverting.set(true);
+    conversionProgress.set(0);
+    conversionStatus.set('Starting board conversion...');
+
+    try {
+      const board = ReactiveCache.getBoard(boardId);
+      if (!board) {
+        throw new Error('Board not found');
+      }
+
+      // Get the default swimlane for this board
+      const defaultSwimlane = board.getDefaultSwimline();
+      if (!defaultSwimlane) {
+        throw new Error('No default swimlane found for board');
+      }
+
+      // Get all lists that need conversion
+      const listsToConvert = ReactiveCache.getLists({
+        boardId: boardId,
+        $or: [
+          { swimlaneId: { $exists: false } },
+          { swimlaneId: '' },
+          { swimlaneId: null }
+        ]
+      });
+
+      if (listsToConvert.length === 0) {
+        this.conversionCache.set(boardId, true);
+        isConverting.set(false);
+        return true;
+      }
+
+      conversionStatus.set(`Converting ${listsToConvert.length} lists...`);
+      
+      const startTime = Date.now();
+      const totalLists = listsToConvert.length;
+      let convertedLists = 0;
+
+      // Convert lists in batches to avoid blocking the UI
+      const batchSize = 10;
+      for (let i = 0; i < listsToConvert.length; i += batchSize) {
+        const batch = listsToConvert.slice(i, i + batchSize);
+        
+        // Process batch
+        await this.processBatch(batch, defaultSwimlane._id);
+        
+        convertedLists += batch.length;
+        const progress = Math.round((convertedLists / totalLists) * 100);
+        conversionProgress.set(progress);
+        
+        // Calculate estimated time remaining
+        const elapsed = Date.now() - startTime;
+        const rate = convertedLists / elapsed; // lists per millisecond
+        const remaining = totalLists - convertedLists;
+        const estimatedMs = remaining / rate;
+        
+        conversionStatus.set(`Converting list ${convertedLists} of ${totalLists}...`);
+        conversionEstimatedTime.set(this.formatTime(estimatedMs));
+
+        // Allow UI to update
+        await new Promise(resolve => setTimeout(resolve, 10));
+      }
+
+      // Mark as converted
+      this.conversionCache.set(boardId, true);
+      
+      conversionStatus.set('Board conversion completed!');
+      conversionProgress.set(100);
+      
+      // Clear status after a delay
+      setTimeout(() => {
+        isConverting.set(false);
+        conversionStatus.set('');
+        conversionProgress.set(0);
+        conversionEstimatedTime.set('');
+      }, 2000);
+
+      return true;
+
+    } catch (error) {
+      console.error('Error converting board:', error);
+      conversionStatus.set(`Conversion failed: ${error.message}`);
+      isConverting.set(false);
+      return false;
+    }
+  }
+
+  /**
+   * Process a batch of lists for conversion
+   * @param {Array} batch - Array of lists to convert
+   * @param {string} defaultSwimlaneId - Default swimlane ID
+   */
+  async processBatch(batch, defaultSwimlaneId) {
+    const updates = batch.map(list => ({
+      _id: list._id,
+      swimlaneId: defaultSwimlaneId
+    }));
+
+    // Update lists in batch
+    updates.forEach(update => {
+      ReactiveCache.getCollection('lists').update(update._id, {
+        $set: { swimlaneId: update.swimlaneId }
+      });
+    });
+  }
+
+  /**
+   * Format time in milliseconds to human readable format
+   * @param {number} ms - Time in milliseconds
+   * @returns {string} - Formatted time string
+   */
+  formatTime(ms) {
+    if (ms < 1000) {
+      return `${Math.round(ms)}ms`;
+    }
+
+    const seconds = Math.floor(ms / 1000);
+    const minutes = Math.floor(seconds / 60);
+    const hours = Math.floor(minutes / 60);
+
+    if (hours > 0) {
+      const remainingMinutes = minutes % 60;
+      const remainingSeconds = seconds % 60;
+      return `${hours}h ${remainingMinutes}m ${remainingSeconds}s`;
+    } else if (minutes > 0) {
+      const remainingSeconds = seconds % 60;
+      return `${minutes}m ${remainingSeconds}s`;
+    } else {
+      return `${seconds}s`;
+    }
+  }
+
+  /**
+   * Clear conversion cache (useful for testing)
+   */
+  clearCache() {
+    this.conversionCache.clear();
+  }
+}
+
+// Export singleton instance
+export const boardConverter = new BoardConverter();

+ 775 - 0
client/lib/migrationManager.js

@@ -0,0 +1,775 @@
+/**
+ * Migration Manager
+ * Handles all database migrations as steps during board loading
+ * with detailed progress tracking and background persistence
+ */
+
+import { ReactiveVar } from 'meteor/reactive-var';
+import { ReactiveCache } from '/imports/lib/reactiveCache';
+
+// Reactive variables for migration progress
+export const migrationProgress = new ReactiveVar(0);
+export const migrationStatus = new ReactiveVar('');
+export const migrationCurrentStep = new ReactiveVar('');
+export const migrationSteps = new ReactiveVar([]);
+export const isMigrating = new ReactiveVar(false);
+export const migrationEstimatedTime = new ReactiveVar('');
+
+class MigrationManager {
+  constructor() {
+    this.migrationCache = new Map(); // Cache completed migrations
+    this.steps = this.initializeMigrationSteps();
+    this.currentStepIndex = 0;
+    this.startTime = null;
+  }
+
+  /**
+   * Initialize all migration steps with their details
+   */
+  initializeMigrationSteps() {
+    return [
+      {
+        id: 'board-background-color',
+        name: 'Board Background Colors',
+        description: 'Setting up board background colors',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-cardcounterlist-allowed',
+        name: 'Card Counter List Settings',
+        description: 'Adding card counter list permissions',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-boardmemberlist-allowed',
+        name: 'Board Member List Settings',
+        description: 'Adding board member list permissions',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'lowercase-board-permission',
+        name: 'Board Permission Standardization',
+        description: 'Converting board permissions to lowercase',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'change-attachments-type-for-non-images',
+        name: 'Attachment Type Standardization',
+        description: 'Updating attachment types for non-images',
+        weight: 2,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'card-covers',
+        name: 'Card Covers System',
+        description: 'Setting up card cover functionality',
+        weight: 2,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'use-css-class-for-boards-colors',
+        name: 'Board Color CSS Classes',
+        description: 'Converting board colors to CSS classes',
+        weight: 2,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'denormalize-star-number-per-board',
+        name: 'Board Star Counts',
+        description: 'Calculating star counts per board',
+        weight: 3,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-member-isactive-field',
+        name: 'Member Activity Status',
+        description: 'Adding member activity tracking',
+        weight: 2,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-sort-checklists',
+        name: 'Checklist Sorting',
+        description: 'Adding sort order to checklists',
+        weight: 2,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-swimlanes',
+        name: 'Swimlanes System',
+        description: 'Setting up swimlanes functionality',
+        weight: 4,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-views',
+        name: 'Board Views',
+        description: 'Adding board view options',
+        weight: 2,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-checklist-items',
+        name: 'Checklist Items',
+        description: 'Setting up checklist items system',
+        weight: 3,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-card-types',
+        name: 'Card Types',
+        description: 'Adding card type functionality',
+        weight: 2,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-custom-fields-to-cards',
+        name: 'Custom Fields',
+        description: 'Adding custom fields to cards',
+        weight: 3,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-requester-field',
+        name: 'Requester Field',
+        description: 'Adding requester field to cards',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-assigner-field',
+        name: 'Assigner Field',
+        description: 'Adding assigner field to cards',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-parent-field-to-cards',
+        name: 'Card Parent Relationships',
+        description: 'Adding parent field to cards',
+        weight: 2,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-subtasks-boards',
+        name: 'Subtasks Boards',
+        description: 'Setting up subtasks board functionality',
+        weight: 3,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-subtasks-sort',
+        name: 'Subtasks Sorting',
+        description: 'Adding sort order to subtasks',
+        weight: 2,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-subtasks-allowed',
+        name: 'Subtasks Permissions',
+        description: 'Adding subtasks permissions',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-authenticationMethod',
+        name: 'Authentication Methods',
+        description: 'Adding authentication method tracking',
+        weight: 2,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'remove-tag',
+        name: 'Remove Tag Field',
+        description: 'Removing deprecated tag field',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'remove-customFields-references-broken',
+        name: 'Fix Custom Fields References',
+        description: 'Fixing broken custom field references',
+        weight: 2,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-product-name',
+        name: 'Product Name Settings',
+        description: 'Adding product name configuration',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-hide-logo',
+        name: 'Hide Logo Setting',
+        description: 'Adding hide logo option',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-hide-card-counter-list',
+        name: 'Hide Card Counter Setting',
+        description: 'Adding hide card counter option',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-hide-board-member-list',
+        name: 'Hide Board Member List Setting',
+        description: 'Adding hide board member list option',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-displayAuthenticationMethod',
+        name: 'Display Authentication Method',
+        description: 'Adding authentication method display option',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-defaultAuthenticationMethod',
+        name: 'Default Authentication Method',
+        description: 'Setting default authentication method',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-templates',
+        name: 'Board Templates',
+        description: 'Setting up board templates system',
+        weight: 3,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'fix-circular-reference_',
+        name: 'Fix Circular References',
+        description: 'Fixing circular references in cards',
+        weight: 2,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'mutate-boardIds-in-customfields',
+        name: 'Custom Fields Board IDs',
+        description: 'Updating board IDs in custom fields',
+        weight: 2,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-missing-created-and-modified',
+        name: 'Missing Timestamps',
+        description: 'Adding missing created and modified timestamps',
+        weight: 4,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'fix-incorrect-dates',
+        name: 'Fix Incorrect Dates',
+        description: 'Correcting incorrect date values',
+        weight: 3,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-assignee',
+        name: 'Assignee Field',
+        description: 'Adding assignee field to cards',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-profile-showDesktopDragHandles',
+        name: 'Desktop Drag Handles',
+        description: 'Adding desktop drag handles preference',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-profile-hiddenMinicardLabelText',
+        name: 'Hidden Minicard Labels',
+        description: 'Adding hidden minicard label text preference',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-receiveddate-allowed',
+        name: 'Received Date Permissions',
+        description: 'Adding received date permissions',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-startdate-allowed',
+        name: 'Start Date Permissions',
+        description: 'Adding start date permissions',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-duedate-allowed',
+        name: 'Due Date Permissions',
+        description: 'Adding due date permissions',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-enddate-allowed',
+        name: 'End Date Permissions',
+        description: 'Adding end date permissions',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-members-allowed',
+        name: 'Members Permissions',
+        description: 'Adding members permissions',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-assignee-allowed',
+        name: 'Assignee Permissions',
+        description: 'Adding assignee permissions',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-labels-allowed',
+        name: 'Labels Permissions',
+        description: 'Adding labels permissions',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-checklists-allowed',
+        name: 'Checklists Permissions',
+        description: 'Adding checklists permissions',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-attachments-allowed',
+        name: 'Attachments Permissions',
+        description: 'Adding attachments permissions',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-comments-allowed',
+        name: 'Comments Permissions',
+        description: 'Adding comments permissions',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-assigned-by-allowed',
+        name: 'Assigned By Permissions',
+        description: 'Adding assigned by permissions',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-requested-by-allowed',
+        name: 'Requested By Permissions',
+        description: 'Adding requested by permissions',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-activities-allowed',
+        name: 'Activities Permissions',
+        description: 'Adding activities permissions',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-description-title-allowed',
+        name: 'Description Title Permissions',
+        description: 'Adding description title permissions',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-description-text-allowed',
+        name: 'Description Text Permissions',
+        description: 'Adding description text permissions',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-description-text-allowed-on-minicard',
+        name: 'Minicard Description Permissions',
+        description: 'Adding minicard description permissions',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-sort-field-to-boards',
+        name: 'Board Sort Field',
+        description: 'Adding sort field to boards',
+        weight: 2,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-default-profile-view',
+        name: 'Default Profile View',
+        description: 'Setting default profile view',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-hide-logo-by-default',
+        name: 'Hide Logo Default',
+        description: 'Setting hide logo as default',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-hide-card-counter-list-by-default',
+        name: 'Hide Card Counter Default',
+        description: 'Setting hide card counter as default',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-hide-board-member-list-by-default',
+        name: 'Hide Board Member List Default',
+        description: 'Setting hide board member list as default',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-card-number-allowed',
+        name: 'Card Number Permissions',
+        description: 'Adding card number permissions',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'assign-boardwise-card-numbers',
+        name: 'Board Card Numbers',
+        description: 'Assigning board-wise card numbers',
+        weight: 3,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'add-card-details-show-lists',
+        name: 'Card Details Show Lists',
+        description: 'Adding card details show lists option',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'migrate-attachments-collectionFS-to-ostrioFiles',
+        name: 'Migrate Attachments to Meteor-Files',
+        description: 'Migrating attachments from CollectionFS to Meteor-Files',
+        weight: 8,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'migrate-avatars-collectionFS-to-ostrioFiles',
+        name: 'Migrate Avatars to Meteor-Files',
+        description: 'Migrating avatars from CollectionFS to Meteor-Files',
+        weight: 6,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'migrate-attachment-drop-index-cardId',
+        name: 'Drop Attachment Index',
+        description: 'Dropping old attachment index',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'migrate-attachment-migration-fix-source-import',
+        name: 'Fix Attachment Source Import',
+        description: 'Fixing attachment source import field',
+        weight: 2,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'attachment-cardCopy-fix-boardId-etc',
+        name: 'Fix Attachment Card Copy',
+        description: 'Fixing attachment card copy board IDs',
+        weight: 2,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'remove-unused-planning-poker',
+        name: 'Remove Planning Poker',
+        description: 'Removing unused planning poker fields',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'remove-user-profile-hiddenSystemMessages',
+        name: 'Remove Hidden System Messages',
+        description: 'Removing hidden system messages field',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'remove-user-profile-hideCheckedItems',
+        name: 'Remove Hide Checked Items',
+        description: 'Removing hide checked items field',
+        weight: 1,
+        completed: false,
+        progress: 0
+      },
+      {
+        id: 'migrate-lists-to-per-swimlane',
+        name: 'Migrate Lists to Per-Swimlane',
+        description: 'Migrating lists to per-swimlane structure',
+        weight: 5,
+        completed: false,
+        progress: 0
+      }
+    ];
+  }
+
+  /**
+   * Check if any migrations need to be run
+   */
+  needsMigration() {
+    // Check if any migration step is not completed
+    return this.steps.some(step => !step.completed);
+  }
+
+  /**
+   * Get total weight of all migrations
+   */
+  getTotalWeight() {
+    return this.steps.reduce((total, step) => total + step.weight, 0);
+  }
+
+  /**
+   * Get completed weight
+   */
+  getCompletedWeight() {
+    return this.steps.reduce((total, step) => {
+      return total + (step.completed ? step.weight : step.progress * step.weight / 100);
+    }, 0);
+  }
+
+  /**
+   * Start migration process
+   */
+  async startMigration() {
+    if (isMigrating.get()) {
+      return; // Already migrating
+    }
+
+    isMigrating.set(true);
+    migrationSteps.set([...this.steps]);
+    this.startTime = Date.now();
+
+    try {
+      // Start server-side migration
+      Meteor.call('migration.start', (error, result) => {
+        if (error) {
+          console.error('Failed to start migration:', error);
+          migrationStatus.set(`Migration failed: ${error.message}`);
+          isMigrating.set(false);
+        }
+      });
+
+      // Poll for progress updates
+      this.pollMigrationProgress();
+
+    } catch (error) {
+      console.error('Migration failed:', error);
+      migrationStatus.set(`Migration failed: ${error.message}`);
+      isMigrating.set(false);
+    }
+  }
+
+  /**
+   * Poll for migration progress updates
+   */
+  pollMigrationProgress() {
+    const pollInterval = setInterval(() => {
+      Meteor.call('migration.getProgress', (error, result) => {
+        if (error) {
+          console.error('Failed to get migration progress:', error);
+          clearInterval(pollInterval);
+          return;
+        }
+
+        if (result) {
+          migrationProgress.set(result.progress);
+          migrationStatus.set(result.status);
+          migrationCurrentStep.set(result.currentStep);
+          migrationSteps.set(result.steps);
+          isMigrating.set(result.isMigrating);
+
+          // Update local steps
+          if (result.steps) {
+            this.steps = result.steps;
+          }
+
+          // If migration is complete, stop polling
+          if (!result.isMigrating && result.progress === 100) {
+            clearInterval(pollInterval);
+            
+            // Clear status after delay
+            setTimeout(() => {
+              migrationStatus.set('');
+              migrationProgress.set(0);
+              migrationEstimatedTime.set('');
+            }, 3000);
+          }
+        }
+      });
+    }, 1000); // Poll every second
+  }
+
+  /**
+   * Run a single migration step
+   */
+  async runMigrationStep(step) {
+    // Simulate migration progress
+    const steps = 10;
+    for (let i = 0; i <= steps; i++) {
+      step.progress = (i / steps) * 100;
+      this.updateProgress();
+      
+      // Simulate work
+      await new Promise(resolve => setTimeout(resolve, 50));
+    }
+
+    // In a real implementation, this would call the actual migration
+    // For now, we'll simulate the migration
+    console.log(`Running migration: ${step.name}`);
+  }
+
+  /**
+   * Update progress variables
+   */
+  updateProgress() {
+    const totalWeight = this.getTotalWeight();
+    const completedWeight = this.getCompletedWeight();
+    const progress = Math.round((completedWeight / totalWeight) * 100);
+    
+    migrationProgress.set(progress);
+    migrationSteps.set([...this.steps]);
+
+    // Calculate estimated time remaining
+    if (this.startTime && progress > 0) {
+      const elapsed = Date.now() - this.startTime;
+      const rate = progress / elapsed; // progress per millisecond
+      const remaining = 100 - progress;
+      const estimatedMs = remaining / rate;
+      migrationEstimatedTime.set(this.formatTime(estimatedMs));
+    }
+  }
+
+  /**
+   * Format time in milliseconds to human readable format
+   */
+  formatTime(ms) {
+    if (ms < 1000) {
+      return `${Math.round(ms)}ms`;
+    }
+
+    const seconds = Math.floor(ms / 1000);
+    const minutes = Math.floor(seconds / 60);
+    const hours = Math.floor(minutes / 60);
+
+    if (hours > 0) {
+      const remainingMinutes = minutes % 60;
+      const remainingSeconds = seconds % 60;
+      return `${hours}h ${remainingMinutes}m ${remainingSeconds}s`;
+    } else if (minutes > 0) {
+      const remainingSeconds = seconds % 60;
+      return `${minutes}m ${remainingSeconds}s`;
+    } else {
+      return `${seconds}s`;
+    }
+  }
+
+  /**
+   * Clear migration cache (for testing)
+   */
+  clearCache() {
+    this.migrationCache.clear();
+    this.steps.forEach(step => {
+      step.completed = false;
+      step.progress = 0;
+    });
+  }
+}
+
+// Export singleton instance
+export const migrationManager = new MigrationManager();

+ 11 - 1
imports/i18n/en.i18n.json

@@ -86,5 +86,15 @@
   "s3-storage": "S3",
   "s3-storage": "S3",
   "card-show-lists-on-minicard": "Show Lists on Minicard",
   "card-show-lists-on-minicard": "Show Lists on Minicard",
   "show-list-on-minicard": "Show List on Minicard",
   "show-list-on-minicard": "Show List on Minicard",
-  "hide-list-on-minicard": "Hide List on Minicard"
+  "hide-list-on-minicard": "Hide List on Minicard",
+  "converting-board": "Converting Board",
+  "converting-board-description": "Converting board structure for improved functionality. This may take a few moments.",
+  "estimated-time-remaining": "Estimated time remaining",
+  "conversion-info-text": "This conversion is performed once per board and improves performance. You can continue using the board normally.",
+  "database-migration": "Database Migration",
+  "database-migration-description": "Updating database structure for improved functionality and performance. This process may take several minutes.",
+  "overall-progress": "Overall Progress",
+  "migration-steps": "Migration Steps",
+  "migration-info-text": "Database migrations are performed once and improve system performance. The process continues in the background even if you close your browser.",
+  "migration-warning-text": "Please do not close your browser during migration. The process will continue in the background but may take longer to complete."
 }
 }

+ 14 - 4
models/boards.js

@@ -785,8 +785,13 @@ Boards.helpers({
       {
       {
         boardId: this._id,
         boardId: this._id,
         archived: false,
         archived: false,
-        // Get lists for all swimlanes in this board
-        swimlaneId: { $in: this.swimlanes().map(s => s._id) },
+        // Get lists for all swimlanes in this board, plus lists without swimlaneId for backward compatibility
+        $or: [
+          { swimlaneId: { $in: this.swimlanes().map(s => s._id) } },
+          { swimlaneId: { $exists: false } },
+          { swimlaneId: '' },
+          { swimlaneId: null }
+        ],
       },
       },
       { sort: sortKey },
       { sort: sortKey },
     );
     );
@@ -796,8 +801,13 @@ Boards.helpers({
     return ReactiveCache.getLists(
     return ReactiveCache.getLists(
       {
       {
         boardId: this._id,
         boardId: this._id,
-        // Get lists for all swimlanes in this board
-        swimlaneId: { $in: this.swimlanes().map(s => s._id) }
+        // Get lists for all swimlanes in this board, plus lists without swimlaneId for backward compatibility
+        $or: [
+          { swimlaneId: { $in: this.swimlanes().map(s => s._id) } },
+          { swimlaneId: { $exists: false } },
+          { swimlaneId: '' },
+          { swimlaneId: null }
+        ]
       },
       },
       { sort: { sort: 1 } }
       { sort: { sort: 1 } }
     );
     );

+ 3 - 2
models/lists.js

@@ -50,10 +50,11 @@ Lists.attachSchema(
     },
     },
     swimlaneId: {
     swimlaneId: {
       /**
       /**
-       * the swimlane associated to this list. Required for per-swimlane list titles
+       * the swimlane associated to this list. Optional for backward compatibility
        */
        */
       type: String,
       type: String,
-      // Remove defaultValue to make it required
+      optional: true,
+      defaultValue: '',
     },
     },
     createdAt: {
     createdAt: {
       /**
       /**

+ 14 - 2
models/swimlanes.js

@@ -211,20 +211,32 @@ Swimlanes.helpers({
   },
   },
   newestLists() {
   newestLists() {
     // sorted lists from newest to the oldest, by its creation date or its cards' last modification date
     // sorted lists from newest to the oldest, by its creation date or its cards' last modification date
+    // Include lists without swimlaneId for backward compatibility (they belong to default swimlane)
     return ReactiveCache.getLists(
     return ReactiveCache.getLists(
       {
       {
         boardId: this.boardId,
         boardId: this.boardId,
-        swimlaneId: this._id, // Only get lists that belong to this specific swimlane
+        $or: [
+          { swimlaneId: this._id },
+          { swimlaneId: { $exists: false } },
+          { swimlaneId: '' },
+          { swimlaneId: null }
+        ],
         archived: false,
         archived: false,
       },
       },
       { sort: { modifiedAt: -1 } },
       { sort: { modifiedAt: -1 } },
     );
     );
   },
   },
   draggableLists() {
   draggableLists() {
+    // Include lists without swimlaneId for backward compatibility (they belong to default swimlane)
     return ReactiveCache.getLists(
     return ReactiveCache.getLists(
       {
       {
         boardId: this.boardId,
         boardId: this.boardId,
-        swimlaneId: this._id, // Only get lists that belong to this specific swimlane
+        $or: [
+          { swimlaneId: this._id },
+          { swimlaneId: { $exists: false } },
+          { swimlaneId: '' },
+          { swimlaneId: null }
+        ],
         //archived: false,
         //archived: false,
       },
       },
       { sort: ['sort'] },
       { sort: ['sort'] },

+ 285 - 99
package-lock.json

@@ -5,12 +5,9 @@
   "requires": true,
   "requires": true,
   "dependencies": {
   "dependencies": {
     "@babel/runtime": {
     "@babel/runtime": {
-      "version": "7.26.10",
-      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz",
-      "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==",
-      "requires": {
-        "regenerator-runtime": "^0.14.0"
-      }
+      "version": "7.28.4",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
+      "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="
     },
     },
     "@fast-csv/format": {
     "@fast-csv/format": {
       "version": "4.3.5",
       "version": "4.3.5",
@@ -124,7 +121,7 @@
       "from": "git+https://github.com/wekan/dragscroll.git"
       "from": "git+https://github.com/wekan/dragscroll.git"
     },
     },
     "@wekanteam/exceljs": {
     "@wekanteam/exceljs": {
-      "version": "git+https://github.com/wekan/exceljs.git#e0229907e7a81bc3fe6daf4e42b1fdfbecdcb7cb",
+      "version": "git+https://github.com/wekan/exceljs.git#7d182abf83ddfb1a8f5a9592a0fdf60ef72f6686",
       "from": "git+https://github.com/wekan/exceljs.git",
       "from": "git+https://github.com/wekan/exceljs.git",
       "requires": {
       "requires": {
         "archiver": "^5.0.0",
         "archiver": "^5.0.0",
@@ -170,6 +167,14 @@
       "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
       "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
       "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
       "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
     },
     },
+    "abort-controller": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
+      "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
+      "requires": {
+        "event-target-shim": "^5.0.0"
+      }
+    },
     "abstract-logging": {
     "abstract-logging": {
       "version": "2.0.1",
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
       "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
@@ -297,14 +302,17 @@
       "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="
       "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="
     },
     },
     "async": {
     "async": {
-      "version": "3.2.5",
-      "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
-      "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg=="
+      "version": "3.2.6",
+      "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
+      "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="
     },
     },
     "available-typed-arrays": {
     "available-typed-arrays": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
-      "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw=="
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
+      "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
+      "requires": {
+        "possible-typed-array-names": "^1.0.0"
+      }
     },
     },
     "backoff": {
     "backoff": {
       "version": "2.5.0",
       "version": "2.5.0",
@@ -423,13 +431,58 @@
       "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ=="
       "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ=="
     },
     },
     "call-bind": {
     "call-bind": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
-      "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
+      "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
       "requires": {
       "requires": {
-        "function-bind": "^1.1.2",
-        "get-intrinsic": "^1.2.1",
-        "set-function-length": "^1.1.1"
+        "call-bind-apply-helpers": "^1.0.0",
+        "es-define-property": "^1.0.0",
+        "get-intrinsic": "^1.2.4",
+        "set-function-length": "^1.2.2"
+      },
+      "dependencies": {
+        "get-intrinsic": {
+          "version": "1.3.0",
+          "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+          "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+          "requires": {
+            "call-bind-apply-helpers": "^1.0.2",
+            "es-define-property": "^1.0.1",
+            "es-errors": "^1.3.0",
+            "es-object-atoms": "^1.1.1",
+            "function-bind": "^1.1.2",
+            "get-proto": "^1.0.1",
+            "gopd": "^1.2.0",
+            "has-symbols": "^1.1.0",
+            "hasown": "^2.0.2",
+            "math-intrinsics": "^1.1.0"
+          },
+          "dependencies": {
+            "es-define-property": {
+              "version": "1.0.1",
+              "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+              "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="
+            }
+          }
+        },
+        "gopd": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+          "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="
+        },
+        "has-symbols": {
+          "version": "1.1.0",
+          "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+          "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="
+        },
+        "hasown": {
+          "version": "2.0.2",
+          "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+          "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+          "requires": {
+            "function-bind": "^1.1.2"
+          }
+        }
       }
       }
     },
     },
     "call-bind-apply-helpers": {
     "call-bind-apply-helpers": {
@@ -612,9 +665,9 @@
       "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw=="
       "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw=="
     },
     },
     "dayjs": {
     "dayjs": {
-      "version": "1.11.13",
-      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
-      "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="
+      "version": "1.11.18",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz",
+      "integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA=="
     },
     },
     "debug": {
     "debug": {
       "version": "4.3.4",
       "version": "4.3.4",
@@ -630,13 +683,13 @@
       "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ=="
       "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ=="
     },
     },
     "define-data-property": {
     "define-data-property": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
-      "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+      "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
       "requires": {
       "requires": {
-        "get-intrinsic": "^1.2.1",
-        "gopd": "^1.0.1",
-        "has-property-descriptors": "^1.0.0"
+        "es-define-property": "^1.0.0",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.0.1"
       }
       }
     },
     },
     "delegates": {
     "delegates": {
@@ -679,9 +732,9 @@
       }
       }
     },
     },
     "dompurify": {
     "dompurify": {
-      "version": "3.2.4",
-      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz",
-      "integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==",
+      "version": "3.2.7",
+      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
+      "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
       "requires": {
       "requires": {
         "@types/trusted-types": "^2.0.7"
         "@types/trusted-types": "^2.0.7"
       }
       }
@@ -756,9 +809,9 @@
       "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
       "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
     },
     },
     "end-of-stream": {
     "end-of-stream": {
-      "version": "1.4.4",
-      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
-      "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+      "version": "1.4.5",
+      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
+      "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
       "requires": {
       "requires": {
         "once": "^1.4.0"
         "once": "^1.4.0"
       }
       }
@@ -823,6 +876,16 @@
       "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz",
       "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz",
       "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA=="
       "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA=="
     },
     },
+    "event-target-shim": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
+      "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="
+    },
+    "events": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+      "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="
+    },
     "extsprintf": {
     "extsprintf": {
       "version": "1.4.1",
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz",
       "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz",
@@ -848,11 +911,11 @@
       "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
       "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
     },
     },
     "fast-xml-parser": {
     "fast-xml-parser": {
-      "version": "4.4.1",
-      "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz",
-      "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==",
+      "version": "4.5.3",
+      "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz",
+      "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==",
       "requires": {
       "requires": {
-        "strnum": "^1.0.5"
+        "strnum": "^1.1.1"
       }
       }
     },
     },
     "fibers": {
     "fibers": {
@@ -897,11 +960,11 @@
       "dev": true
       "dev": true
     },
     },
     "for-each": {
     "for-each": {
-      "version": "0.3.3",
-      "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
-      "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
+      "version": "0.3.5",
+      "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
+      "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
       "requires": {
       "requires": {
-        "is-callable": "^1.1.3"
+        "is-callable": "^1.2.7"
       }
       }
     },
     },
     "fs-constants": {
     "fs-constants": {
@@ -982,6 +1045,11 @@
         "wide-align": "^1.1.2"
         "wide-align": "^1.1.2"
       }
       }
     },
     },
+    "generator-function": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
+      "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="
+    },
     "get-intrinsic": {
     "get-intrinsic": {
       "version": "1.2.2",
       "version": "1.2.2",
       "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
       "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
@@ -1035,11 +1103,11 @@
       "dev": true
       "dev": true
     },
     },
     "has-property-descriptors": {
     "has-property-descriptors": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
-      "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+      "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
       "requires": {
       "requires": {
-        "get-intrinsic": "^1.2.2"
+        "es-define-property": "^1.0.0"
       }
       }
     },
     },
     "has-proto": {
     "has-proto": {
@@ -1053,11 +1121,11 @@
       "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="
       "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="
     },
     },
     "has-tostringtag": {
     "has-tostringtag": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
-      "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
       "requires": {
       "requires": {
-        "has-symbols": "^1.0.2"
+        "has-symbols": "^1.0.3"
       }
       }
     },
     },
     "has-unicode": {
     "has-unicode": {
@@ -1131,17 +1199,17 @@
       "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
       "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
     },
     },
     "ipaddr.js": {
     "ipaddr.js": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz",
-      "integrity": "sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ=="
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
+      "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA=="
     },
     },
     "is-arguments": {
     "is-arguments": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
-      "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==",
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
+      "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
       "requires": {
       "requires": {
-        "call-bind": "^1.0.2",
-        "has-tostringtag": "^1.0.0"
+        "call-bound": "^1.0.2",
+        "has-tostringtag": "^1.0.2"
       }
       }
     },
     },
     "is-callable": {
     "is-callable": {
@@ -1155,19 +1223,49 @@
       "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
       "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
     },
     },
     "is-generator-function": {
     "is-generator-function": {
-      "version": "1.0.10",
-      "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz",
-      "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==",
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
+      "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==",
       "requires": {
       "requires": {
-        "has-tostringtag": "^1.0.0"
+        "call-bound": "^1.0.4",
+        "generator-function": "^2.0.0",
+        "get-proto": "^1.0.1",
+        "has-tostringtag": "^1.0.2",
+        "safe-regex-test": "^1.1.0"
+      }
+    },
+    "is-regex": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
+      "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
+      "requires": {
+        "call-bound": "^1.0.2",
+        "gopd": "^1.2.0",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "dependencies": {
+        "gopd": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+          "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="
+        },
+        "hasown": {
+          "version": "2.0.2",
+          "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+          "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+          "requires": {
+            "function-bind": "^1.1.2"
+          }
+        }
       }
       }
     },
     },
     "is-typed-array": {
     "is-typed-array": {
-      "version": "1.1.12",
-      "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz",
-      "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==",
+      "version": "1.1.15",
+      "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
+      "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
       "requires": {
       "requires": {
-        "which-typed-array": "^1.1.11"
+        "which-typed-array": "^1.1.16"
       }
       }
     },
     },
     "isarray": {
     "isarray": {
@@ -2644,9 +2742,9 @@
       "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
       "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
     },
     },
     "papaparse": {
     "papaparse": {
-      "version": "5.4.1",
-      "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz",
-      "integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw=="
+      "version": "5.5.3",
+      "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz",
+      "integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A=="
     },
     },
     "parse-ms": {
     "parse-ms": {
       "version": "2.1.0",
       "version": "2.1.0",
@@ -2694,6 +2792,11 @@
         "parse-ms": "^2.1.0"
         "parse-ms": "^2.1.0"
       }
       }
     },
     },
+    "process": {
+      "version": "0.11.10",
+      "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+      "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="
+    },
     "process-nextick-args": {
     "process-nextick-args": {
       "version": "2.0.1",
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
       "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@@ -2734,11 +2837,34 @@
       }
       }
     },
     },
     "readable-web-to-node-stream": {
     "readable-web-to-node-stream": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz",
-      "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==",
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz",
+      "integrity": "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==",
       "requires": {
       "requires": {
-        "readable-stream": "^3.6.0"
+        "readable-stream": "^4.7.0"
+      },
+      "dependencies": {
+        "buffer": {
+          "version": "6.0.3",
+          "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+          "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+          "requires": {
+            "base64-js": "^1.3.1",
+            "ieee754": "^1.2.1"
+          }
+        },
+        "readable-stream": {
+          "version": "4.7.0",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
+          "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
+          "requires": {
+            "abort-controller": "^3.0.0",
+            "buffer": "^6.0.3",
+            "events": "^3.3.0",
+            "process": "^0.11.10",
+            "string_decoder": "^1.3.0"
+          }
+        }
       }
       }
     },
     },
     "readdir-glob": {
     "readdir-glob": {
@@ -2767,11 +2893,6 @@
         }
         }
       }
       }
     },
     },
-    "regenerator-runtime": {
-      "version": "0.14.1",
-      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
-      "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
-    },
     "rimraf": {
     "rimraf": {
       "version": "3.0.2",
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
       "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@@ -2785,15 +2906,25 @@
       "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
       "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
       "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
       "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
     },
     },
+    "safe-regex-test": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
+      "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
+      "requires": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "is-regex": "^1.2.1"
+      }
+    },
     "safer-buffer": {
     "safer-buffer": {
       "version": "2.1.2",
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
       "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
       "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
       "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
     },
     },
     "sax": {
     "sax": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz",
-      "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA=="
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
+      "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg=="
     },
     },
     "saxes": {
     "saxes": {
       "version": "5.0.1",
       "version": "5.0.1",
@@ -2817,14 +2948,60 @@
       "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
       "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
     },
     },
     "set-function-length": {
     "set-function-length": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz",
-      "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==",
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+      "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
       "requires": {
       "requires": {
-        "define-data-property": "^1.1.1",
-        "get-intrinsic": "^1.2.1",
+        "define-data-property": "^1.1.4",
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2",
+        "get-intrinsic": "^1.2.4",
         "gopd": "^1.0.1",
         "gopd": "^1.0.1",
-        "has-property-descriptors": "^1.0.0"
+        "has-property-descriptors": "^1.0.2"
+      },
+      "dependencies": {
+        "es-define-property": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+          "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="
+        },
+        "get-intrinsic": {
+          "version": "1.3.0",
+          "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+          "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+          "requires": {
+            "call-bind-apply-helpers": "^1.0.2",
+            "es-define-property": "^1.0.1",
+            "es-errors": "^1.3.0",
+            "es-object-atoms": "^1.1.1",
+            "function-bind": "^1.1.2",
+            "get-proto": "^1.0.1",
+            "gopd": "^1.2.0",
+            "has-symbols": "^1.1.0",
+            "hasown": "^2.0.2",
+            "math-intrinsics": "^1.1.0"
+          },
+          "dependencies": {
+            "gopd": {
+              "version": "1.2.0",
+              "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+              "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="
+            }
+          }
+        },
+        "has-symbols": {
+          "version": "1.1.0",
+          "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+          "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="
+        },
+        "hasown": {
+          "version": "2.0.2",
+          "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+          "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+          "requires": {
+            "function-bind": "^1.1.2"
+          }
+        }
       }
       }
     },
     },
     "setimmediate": {
     "setimmediate": {
@@ -3001,9 +3178,9 @@
       }
       }
     },
     },
     "strnum": {
     "strnum": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz",
-      "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA=="
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz",
+      "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="
     },
     },
     "strtok3": {
     "strtok3": {
       "version": "6.3.0",
       "version": "6.3.0",
@@ -3436,15 +3613,24 @@
       }
       }
     },
     },
     "which-typed-array": {
     "which-typed-array": {
-      "version": "1.1.13",
-      "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz",
-      "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==",
-      "requires": {
-        "available-typed-arrays": "^1.0.5",
-        "call-bind": "^1.0.4",
-        "for-each": "^0.3.3",
-        "gopd": "^1.0.1",
-        "has-tostringtag": "^1.0.0"
+      "version": "1.1.19",
+      "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz",
+      "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==",
+      "requires": {
+        "available-typed-arrays": "^1.0.7",
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.4",
+        "for-each": "^0.3.5",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-tostringtag": "^1.0.2"
+      },
+      "dependencies": {
+        "gopd": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+          "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="
+        }
       }
       }
     },
     },
     "wicked-good-xpath": {
     "wicked-good-xpath": {

+ 9 - 9
package.json

@@ -17,25 +17,25 @@
     "sinon": "^13.0.2"
     "sinon": "^13.0.2"
   },
   },
   "dependencies": {
   "dependencies": {
-    "@babel/runtime": "^7.26.10",
+    "@babel/runtime": "^7.28.4",
     "@mapbox/node-pre-gyp": "^1.0.10",
     "@mapbox/node-pre-gyp": "^1.0.10",
     "@rwap/jquery-ui-touch-punch": "^1.0.11",
     "@rwap/jquery-ui-touch-punch": "^1.0.11",
     "@wekanteam/dragscroll": "https://github.com/wekan/dragscroll.git",
     "@wekanteam/dragscroll": "https://github.com/wekan/dragscroll.git",
-    "@wekanteam/exceljs": "https://github.com/wekan/exceljs.git",
+    "@wekanteam/exceljs": "git+https://github.com/wekan/exceljs.git",
     "@wekanteam/html-to-markdown": "^1.0.2",
     "@wekanteam/html-to-markdown": "^1.0.2",
     "@wekanteam/meteor-globals": "^1.1.4",
     "@wekanteam/meteor-globals": "^1.1.4",
     "@wekanteam/meteor-reactive-cache": "^1.0.6",
     "@wekanteam/meteor-reactive-cache": "^1.0.6",
     "ajv": "^6.12.6",
     "ajv": "^6.12.6",
     "bcryptjs": "^2.4.3",
     "bcryptjs": "^2.4.3",
-    "bson": "^4.5.2",
+    "bson": "^4.7.2",
     "chart.js": "^4.5.0",
     "chart.js": "^4.5.0",
-    "dompurify": "^3.2.4",
+    "dompurify": "^3.2.7",
     "es6-promise": "^4.2.4",
     "es6-promise": "^4.2.4",
     "escape-string-regexp": "^5.0.0",
     "escape-string-regexp": "^5.0.0",
     "fibers": "^5.0.3",
     "fibers": "^5.0.3",
     "file-type": "^16.5.4",
     "file-type": "^16.5.4",
     "filesize": "^8.0.7",
     "filesize": "^8.0.7",
-    "i18next": "^21.6.16",
+    "i18next": "^21.10.0",
     "i18next-sprintf-postprocessor": "^0.2.2",
     "i18next-sprintf-postprocessor": "^0.2.2",
     "jquery": "^3.7.1",
     "jquery": "^3.7.1",
     "jquery-ui": "^1.13.3",
     "jquery-ui": "^1.13.3",
@@ -47,15 +47,15 @@
     "meteor-accounts-t9n": "^2.6.0",
     "meteor-accounts-t9n": "^2.6.0",
     "meteor-node-stubs": "^1.2.24",
     "meteor-node-stubs": "^1.2.24",
     "minio": "^7.1.3",
     "minio": "^7.1.3",
-    "moment": "^2.29.4",
+    "moment": "^2.30.1",
     "mongodb3legacy": "npm:mongodb@3.7.4",
     "mongodb3legacy": "npm:mongodb@3.7.4",
     "mongodb4legacy": "npm:mongodb@4.17.2",
     "mongodb4legacy": "npm:mongodb@4.17.2",
     "mongodb5legacy": "npm:mongodb@5.9.2",
     "mongodb5legacy": "npm:mongodb@5.9.2",
     "mongodb6legacy": "npm:mongodb@6.3.0",
     "mongodb6legacy": "npm:mongodb@6.3.0",
-    "mongodb7legacy": "npm:mongodb@7.0.1",
-    "mongodb8legacy": "npm:mongodb@6.9.0",
+    "mongodb7legacy": "npm:mongodb@6.20.0",
+    "mongodb8legacy": "npm:mongodb@6.20.0",
     "os": "^0.1.2",
     "os": "^0.1.2",
-    "papaparse": "^5.3.1",
+    "papaparse": "^5.5.3",
     "pretty-ms": "^7.0.1",
     "pretty-ms": "^7.0.1",
     "qs": "^6.13.0",
     "qs": "^6.13.0",
     "simpl-schema": "^3.4.6",
     "simpl-schema": "^3.4.6",

+ 3 - 0
server/00checkStartup.js

@@ -24,3 +24,6 @@ if (errors.length > 0) {
   console.error("\n\n");
   console.error("\n\n");
   process.exit(1);
   process.exit(1);
 }
 }
+
+// Import migration runner for on-demand migrations
+import './migrationRunner';

+ 404 - 0
server/migrationRunner.js

@@ -0,0 +1,404 @@
+/**
+ * Server-side Migration Runner
+ * Handles actual execution of database migrations with progress tracking
+ */
+
+import { Meteor } from 'meteor/meteor';
+import { Migrations } from 'meteor/percolate:migrations';
+import { ReactiveVar } from 'meteor/reactive-var';
+
+// Server-side reactive variables for migration progress
+export const serverMigrationProgress = new ReactiveVar(0);
+export const serverMigrationStatus = new ReactiveVar('');
+export const serverMigrationCurrentStep = new ReactiveVar('');
+export const serverMigrationSteps = new ReactiveVar([]);
+export const serverIsMigrating = new ReactiveVar(false);
+
+class ServerMigrationRunner {
+  constructor() {
+    this.migrationSteps = this.initializeMigrationSteps();
+    this.currentStepIndex = 0;
+    this.startTime = null;
+  }
+
+  /**
+   * Initialize migration steps with their actual migration functions
+   */
+  initializeMigrationSteps() {
+    return [
+      {
+        id: 'board-background-color',
+        name: 'Board Background Colors',
+        description: 'Setting up board background colors',
+        weight: 1,
+        completed: false,
+        progress: 0,
+        migrationFunction: this.runBoardBackgroundColorMigration
+      },
+      {
+        id: 'add-cardcounterlist-allowed',
+        name: 'Card Counter List Settings',
+        description: 'Adding card counter list permissions',
+        weight: 1,
+        completed: false,
+        progress: 0,
+        migrationFunction: this.runCardCounterListMigration
+      },
+      {
+        id: 'add-boardmemberlist-allowed',
+        name: 'Board Member List Settings',
+        description: 'Adding board member list permissions',
+        weight: 1,
+        completed: false,
+        progress: 0,
+        migrationFunction: this.runBoardMemberListMigration
+      },
+      {
+        id: 'lowercase-board-permission',
+        name: 'Board Permission Standardization',
+        description: 'Converting board permissions to lowercase',
+        weight: 1,
+        completed: false,
+        progress: 0,
+        migrationFunction: this.runLowercaseBoardPermissionMigration
+      },
+      {
+        id: 'change-attachments-type-for-non-images',
+        name: 'Attachment Type Standardization',
+        description: 'Updating attachment types for non-images',
+        weight: 2,
+        completed: false,
+        progress: 0,
+        migrationFunction: this.runAttachmentTypeMigration
+      },
+      {
+        id: 'card-covers',
+        name: 'Card Covers System',
+        description: 'Setting up card cover functionality',
+        weight: 2,
+        completed: false,
+        progress: 0,
+        migrationFunction: this.runCardCoversMigration
+      },
+      {
+        id: 'use-css-class-for-boards-colors',
+        name: 'Board Color CSS Classes',
+        description: 'Converting board colors to CSS classes',
+        weight: 2,
+        completed: false,
+        progress: 0,
+        migrationFunction: this.runBoardColorCSSMigration
+      },
+      {
+        id: 'denormalize-star-number-per-board',
+        name: 'Board Star Counts',
+        description: 'Calculating star counts per board',
+        weight: 3,
+        completed: false,
+        progress: 0,
+        migrationFunction: this.runStarNumberMigration
+      },
+      {
+        id: 'add-member-isactive-field',
+        name: 'Member Activity Status',
+        description: 'Adding member activity tracking',
+        weight: 2,
+        completed: false,
+        progress: 0,
+        migrationFunction: this.runMemberIsActiveMigration
+      },
+      {
+        id: 'add-sort-checklists',
+        name: 'Checklist Sorting',
+        description: 'Adding sort order to checklists',
+        weight: 2,
+        completed: false,
+        progress: 0,
+        migrationFunction: this.runSortChecklistsMigration
+      },
+      {
+        id: 'add-swimlanes',
+        name: 'Swimlanes System',
+        description: 'Setting up swimlanes functionality',
+        weight: 4,
+        completed: false,
+        progress: 0,
+        migrationFunction: this.runSwimlanesMigration
+      },
+      {
+        id: 'add-views',
+        name: 'Board Views',
+        description: 'Adding board view options',
+        weight: 2,
+        completed: false,
+        progress: 0,
+        migrationFunction: this.runViewsMigration
+      },
+      {
+        id: 'add-checklist-items',
+        name: 'Checklist Items',
+        description: 'Setting up checklist items system',
+        weight: 3,
+        completed: false,
+        progress: 0,
+        migrationFunction: this.runChecklistItemsMigration
+      },
+      {
+        id: 'add-card-types',
+        name: 'Card Types',
+        description: 'Adding card type functionality',
+        weight: 2,
+        completed: false,
+        progress: 0,
+        migrationFunction: this.runCardTypesMigration
+      },
+      {
+        id: 'add-custom-fields-to-cards',
+        name: 'Custom Fields',
+        description: 'Adding custom fields to cards',
+        weight: 3,
+        completed: false,
+        progress: 0,
+        migrationFunction: this.runCustomFieldsMigration
+      },
+      {
+        id: 'migrate-attachments-collectionFS-to-ostrioFiles',
+        name: 'Migrate Attachments to Meteor-Files',
+        description: 'Migrating attachments from CollectionFS to Meteor-Files',
+        weight: 8,
+        completed: false,
+        progress: 0,
+        migrationFunction: this.runAttachmentMigration
+      },
+      {
+        id: 'migrate-avatars-collectionFS-to-ostrioFiles',
+        name: 'Migrate Avatars to Meteor-Files',
+        description: 'Migrating avatars from CollectionFS to Meteor-Files',
+        weight: 6,
+        completed: false,
+        progress: 0,
+        migrationFunction: this.runAvatarMigration
+      },
+      {
+        id: 'migrate-lists-to-per-swimlane',
+        name: 'Migrate Lists to Per-Swimlane',
+        description: 'Migrating lists to per-swimlane structure',
+        weight: 5,
+        completed: false,
+        progress: 0,
+        migrationFunction: this.runListsToPerSwimlaneMigration
+      }
+    ];
+  }
+
+  /**
+   * Start migration process
+   */
+  async startMigration() {
+    if (serverIsMigrating.get()) {
+      return; // Already migrating
+    }
+
+    serverIsMigrating.set(true);
+    serverMigrationSteps.set([...this.migrationSteps]);
+    this.startTime = Date.now();
+
+    try {
+      for (let i = 0; i < this.migrationSteps.length; i++) {
+        const step = this.migrationSteps[i];
+        this.currentStepIndex = i;
+
+        if (step.completed) {
+          continue; // Skip already completed steps
+        }
+
+        serverMigrationCurrentStep.set(step.name);
+        serverMigrationStatus.set(`Running: ${step.description}`);
+
+        // Run the migration step
+        await this.runMigrationStep(step);
+
+        // Mark as completed
+        step.completed = true;
+        step.progress = 100;
+
+        // Update progress
+        this.updateProgress();
+
+        // Allow other processes to run
+        await new Promise(resolve => setTimeout(resolve, 100));
+      }
+
+      // Migration completed
+      serverMigrationStatus.set('All migrations completed successfully!');
+      serverMigrationProgress.set(100);
+      serverMigrationCurrentStep.set('');
+
+      // Clear status after delay
+      setTimeout(() => {
+        serverIsMigrating.set(false);
+        serverMigrationStatus.set('');
+        serverMigrationProgress.set(0);
+      }, 3000);
+
+    } catch (error) {
+      console.error('Migration failed:', error);
+      serverMigrationStatus.set(`Migration failed: ${error.message}`);
+      serverIsMigrating.set(false);
+    }
+  }
+
+  /**
+   * Run a single migration step
+   */
+  async runMigrationStep(step) {
+    try {
+      // Update progress during migration
+      const progressSteps = 10;
+      for (let i = 0; i <= progressSteps; i++) {
+        step.progress = (i / progressSteps) * 100;
+        this.updateProgress();
+        
+        // Run actual migration function
+        if (i === progressSteps) {
+          await step.migrationFunction.call(this);
+        }
+        
+        // Allow other processes to run
+        await new Promise(resolve => setTimeout(resolve, 50));
+      }
+    } catch (error) {
+      console.error(`Migration step ${step.name} failed:`, error);
+      throw error;
+    }
+  }
+
+  /**
+   * Update progress variables
+   */
+  updateProgress() {
+    const totalWeight = this.migrationSteps.reduce((total, step) => total + step.weight, 0);
+    const completedWeight = this.migrationSteps.reduce((total, step) => {
+      return total + (step.completed ? step.weight : step.progress * step.weight / 100);
+    }, 0);
+    const progress = Math.round((completedWeight / totalWeight) * 100);
+    
+    serverMigrationProgress.set(progress);
+    serverMigrationSteps.set([...this.migrationSteps]);
+  }
+
+  // Individual migration functions
+  async runBoardBackgroundColorMigration() {
+    // Implementation for board background color migration
+    console.log('Running board background color migration');
+  }
+
+  async runCardCounterListMigration() {
+    // Implementation for card counter list migration
+    console.log('Running card counter list migration');
+  }
+
+  async runBoardMemberListMigration() {
+    // Implementation for board member list migration
+    console.log('Running board member list migration');
+  }
+
+  async runLowercaseBoardPermissionMigration() {
+    // Implementation for lowercase board permission migration
+    console.log('Running lowercase board permission migration');
+  }
+
+  async runAttachmentTypeMigration() {
+    // Implementation for attachment type migration
+    console.log('Running attachment type migration');
+  }
+
+  async runCardCoversMigration() {
+    // Implementation for card covers migration
+    console.log('Running card covers migration');
+  }
+
+  async runBoardColorCSSMigration() {
+    // Implementation for board color CSS migration
+    console.log('Running board color CSS migration');
+  }
+
+  async runStarNumberMigration() {
+    // Implementation for star number migration
+    console.log('Running star number migration');
+  }
+
+  async runMemberIsActiveMigration() {
+    // Implementation for member is active migration
+    console.log('Running member is active migration');
+  }
+
+  async runSortChecklistsMigration() {
+    // Implementation for sort checklists migration
+    console.log('Running sort checklists migration');
+  }
+
+  async runSwimlanesMigration() {
+    // Implementation for swimlanes migration
+    console.log('Running swimlanes migration');
+  }
+
+  async runViewsMigration() {
+    // Implementation for views migration
+    console.log('Running views migration');
+  }
+
+  async runChecklistItemsMigration() {
+    // Implementation for checklist items migration
+    console.log('Running checklist items migration');
+  }
+
+  async runCardTypesMigration() {
+    // Implementation for card types migration
+    console.log('Running card types migration');
+  }
+
+  async runCustomFieldsMigration() {
+    // Implementation for custom fields migration
+    console.log('Running custom fields migration');
+  }
+
+  async runAttachmentMigration() {
+    // Implementation for attachment migration from CollectionFS to Meteor-Files
+    console.log('Running attachment migration from CollectionFS to Meteor-Files');
+  }
+
+  async runAvatarMigration() {
+    // Implementation for avatar migration from CollectionFS to Meteor-Files
+    console.log('Running avatar migration from CollectionFS to Meteor-Files');
+  }
+
+  async runListsToPerSwimlaneMigration() {
+    // Implementation for lists to per-swimlane migration
+    console.log('Running lists to per-swimlane migration');
+  }
+}
+
+// Export singleton instance
+export const serverMigrationRunner = new ServerMigrationRunner();
+
+// Meteor methods for client-server communication
+Meteor.methods({
+  'migration.start'() {
+    if (!this.userId) {
+      throw new Meteor.Error('not-authorized');
+    }
+    
+    return serverMigrationRunner.startMigration();
+  },
+  
+  'migration.getProgress'() {
+    return {
+      progress: serverMigrationProgress.get(),
+      status: serverMigrationStatus.get(),
+      currentStep: serverMigrationCurrentStep.get(),
+      steps: serverMigrationSteps.get(),
+      isMigrating: serverIsMigrating.get()
+    };
+  }
+});