Browse Source

Fixes to make board showing correctly.

Thanks to xet7 !
Lauri Ojansivu 2 days ago
parent
commit
bd8c565415

+ 5 - 5
client/00-startup.js

@@ -6,12 +6,12 @@ if ('serviceWorker' in navigator) {
 }
 }
 
 
 // Import board converter for on-demand conversion
 // Import board converter for on-demand conversion
-import '/imports/lib/boardConverter';
-import '/imports/components/boardConversionProgress';
+import '/client/lib/boardConverter';
+import '/client/components/boardConversionProgress';
 
 
 // Import migration manager and progress UI
 // Import migration manager and progress UI
-import '/imports/lib/migrationManager';
-import '/imports/components/migrationProgress';
+import '/client/lib/migrationManager';
+import '/client/components/migrationProgress';
 
 
 // Import cron settings
 // Import cron settings
-import '/imports/components/settings/cronSettings';
+import '/client/components/settings/cronSettings';

+ 15 - 9
client/components/boardConversionProgress.js

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

+ 9 - 3
client/components/boards/boardBody.jade

@@ -1,7 +1,8 @@
 template(name="board")
 template(name="board")
-  if isMigrating
+  
+  if isMigrating.get
     +migrationProgress
     +migrationProgress
-  else if isConverting
+  else if isConverting.get
     +boardConversionProgress
     +boardConversionProgress
   else if isBoardReady.get
   else if isBoardReady.get
     if currentBoard
     if currentBoard
@@ -46,7 +47,12 @@ template(name="boardBody")
         else if isViewCalendar
         else if isViewCalendar
           +calendarView
           +calendarView
         else
         else
-          +listsGroup(currentBoard)
+          // Default view - show swimlanes if they exist, otherwise show lists
+          if hasSwimlanes
+            each currentBoard.swimlanes
+              +swimlane(this)
+          else
+            +listsGroup(currentBoard)
       +sidebar
       +sidebar
 
 
 template(name="calendarView")
 template(name="calendarView")

+ 188 - 48
client/components/boards/boardBody.js

@@ -1,8 +1,11 @@
 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';
+import { boardConverter } from '/client/lib/boardConverter';
+import { migrationManager } from '/client/lib/migrationManager';
+import { attachmentMigrationManager } from '/client/lib/attachmentMigrationManager';
+import { Swimlanes } from '/models/swimlanes';
+import { Lists } from '/models/lists';
 
 
 const subManager = new SubsManager();
 const subManager = new SubsManager();
 const { calculateIndex } = Utils;
 const { calculateIndex } = Utils;
@@ -13,6 +16,7 @@ BlazeComponent.extendComponent({
     this.isBoardReady = new ReactiveVar(false);
     this.isBoardReady = new ReactiveVar(false);
     this.isConverting = new ReactiveVar(false);
     this.isConverting = new ReactiveVar(false);
     this.isMigrating = new ReactiveVar(false);
     this.isMigrating = new ReactiveVar(false);
+    this._swimlaneCreated = new Set(); // Track boards where we've created swimlanes
 
 
     // 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
@@ -21,10 +25,14 @@ BlazeComponent.extendComponent({
     this.autorun(() => {
     this.autorun(() => {
       const currentBoardId = Session.get('currentBoard');
       const currentBoardId = Session.get('currentBoard');
       if (!currentBoardId) return;
       if (!currentBoardId) return;
+      
       const handle = subManager.subscribe('board', currentBoardId, false);
       const handle = subManager.subscribe('board', currentBoardId, false);
+      
       Tracker.nonreactive(() => {
       Tracker.nonreactive(() => {
         Tracker.autorun(() => {
         Tracker.autorun(() => {
           if (handle.ready()) {
           if (handle.ready()) {
+            // Ensure default swimlane exists (only once per board)
+            this.ensureDefaultSwimlane(currentBoardId);
             // Check if board needs conversion
             // Check if board needs conversion
             this.checkAndConvertBoard(currentBoardId);
             this.checkAndConvertBoard(currentBoardId);
           } else {
           } else {
@@ -35,17 +43,54 @@ BlazeComponent.extendComponent({
     });
     });
   },
   },
 
 
+  ensureDefaultSwimlane(boardId) {
+    // Only create swimlane once per board
+    if (this._swimlaneCreated.has(boardId)) {
+      return;
+    }
+
+    try {
+      const board = ReactiveCache.getBoard(boardId);
+      if (!board) return;
+
+      const swimlanes = board.swimlanes();
+      
+      if (swimlanes.length === 0) {
+        const swimlaneId = Swimlanes.insert({
+          title: 'Default',
+          boardId: boardId,
+        });
+        this._swimlaneCreated.add(boardId);
+      } else {
+        this._swimlaneCreated.add(boardId);
+      }
+    } catch (error) {
+      console.error('Error creating default swimlane:', error);
+    }
+  },
+
   async checkAndConvertBoard(boardId) {
   async checkAndConvertBoard(boardId) {
     try {
     try {
-      // First check if migrations need to be run
-      if (migrationManager.needsMigration()) {
+      const board = ReactiveCache.getBoard(boardId);
+      if (!board) {
+        this.isBoardReady.set(true);
+        return;
+      }
+
+      // Check if board needs migration based on migration version
+      const needsMigration = !board.migrationVersion || board.migrationVersion < 1;
+      
+      if (needsMigration) {
+        // Start background migration for old boards
         this.isMigrating.set(true);
         this.isMigrating.set(true);
-        await migrationManager.startMigration();
+        await this.startBackgroundMigration(boardId);
         this.isMigrating.set(false);
         this.isMigrating.set(false);
       }
       }
 
 
-      // Then check if board needs conversion
-      if (boardConverter.needsConversion(boardId)) {
+      // Check if board needs conversion (for old structure)
+      const needsConversion = boardConverter.needsConversion(boardId);
+      
+      if (needsConversion) {
         this.isConverting.set(true);
         this.isConverting.set(true);
         const success = await boardConverter.convertBoard(boardId);
         const success = await boardConverter.convertBoard(boardId);
         this.isConverting.set(false);
         this.isConverting.set(false);
@@ -53,12 +98,15 @@ BlazeComponent.extendComponent({
         if (success) {
         if (success) {
           this.isBoardReady.set(true);
           this.isBoardReady.set(true);
         } else {
         } else {
-          console.error('Board conversion failed');
+          console.error('Board conversion failed, setting ready to true anyway');
           this.isBoardReady.set(true); // Still show board even if conversion failed
           this.isBoardReady.set(true); // Still show board even if conversion failed
         }
         }
       } else {
       } else {
         this.isBoardReady.set(true);
         this.isBoardReady.set(true);
       }
       }
+
+      // Start attachment migration in background if needed
+      this.startAttachmentMigrationIfNeeded(boardId);
     } catch (error) {
     } catch (error) {
       console.error('Error during board conversion check:', error);
       console.error('Error during board conversion check:', error);
       this.isConverting.set(false);
       this.isConverting.set(false);
@@ -67,8 +115,39 @@ BlazeComponent.extendComponent({
     }
     }
   },
   },
 
 
+  async startBackgroundMigration(boardId) {
+    try {
+      // Start background migration using the cron system
+      Meteor.call('boardMigration.startBoardMigration', boardId, (error, result) => {
+        if (error) {
+          console.error('Failed to start background migration:', error);
+        } else {
+          console.log('Background migration started for board:', boardId);
+        }
+      });
+    } catch (error) {
+      console.error('Error starting background migration:', error);
+    }
+  },
+
+  async startAttachmentMigrationIfNeeded(boardId) {
+    try {
+      // Check if there are unconverted attachments
+      const unconvertedAttachments = attachmentMigrationManager.getUnconvertedAttachments(boardId);
+      
+      if (unconvertedAttachments.length > 0) {
+        console.log(`Starting attachment migration for ${unconvertedAttachments.length} attachments in board ${boardId}`);
+        await attachmentMigrationManager.startAttachmentMigration(boardId);
+      }
+    } catch (error) {
+      console.error('Error starting attachment migration:', error);
+    }
+  },
+
   onlyShowCurrentCard() {
   onlyShowCurrentCard() {
-    return Utils.isMiniScreen() && Utils.getCurrentCardId(true);
+    const isMiniScreen = Utils.isMiniScreen();
+    const currentCardId = Utils.getCurrentCardId(true);
+    return isMiniScreen && currentCardId;
   },
   },
 
 
   goHome() {
   goHome() {
@@ -82,6 +161,14 @@ BlazeComponent.extendComponent({
   isMigrating() {
   isMigrating() {
     return this.isMigrating.get();
     return this.isMigrating.get();
   },
   },
+
+  isBoardReady() {
+    return this.isBoardReady.get();
+  },
+
+  currentBoard() {
+    return Utils.getCurrentBoard();
+  },
 }).register('board');
 }).register('board');
 
 
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
@@ -95,33 +182,37 @@ BlazeComponent.extendComponent({
 
 
     // fix swimlanes sort field if there are null values
     // fix swimlanes sort field if there are null values
     const currentBoardData = Utils.getCurrentBoard();
     const currentBoardData = Utils.getCurrentBoard();
-    const nullSortSwimlanes = currentBoardData.nullSortSwimlanes();
-    if (nullSortSwimlanes.length > 0) {
-      const swimlanes = currentBoardData.swimlanes();
-      let count = 0;
-      swimlanes.forEach(s => {
-        Swimlanes.update(s._id, {
-          $set: {
-            sort: count,
-          },
+    if (currentBoardData && Swimlanes) {
+      const nullSortSwimlanes = currentBoardData.nullSortSwimlanes();
+      if (nullSortSwimlanes.length > 0) {
+        const swimlanes = currentBoardData.swimlanes();
+        let count = 0;
+        swimlanes.forEach(s => {
+          Swimlanes.update(s._id, {
+            $set: {
+              sort: count,
+            },
+          });
+          count += 1;
         });
         });
-        count += 1;
-      });
+      }
     }
     }
 
 
     // fix lists sort field if there are null values
     // fix lists sort field if there are null values
-    const nullSortLists = currentBoardData.nullSortLists();
-    if (nullSortLists.length > 0) {
-      const lists = currentBoardData.lists();
-      let count = 0;
-      lists.forEach(l => {
-        Lists.update(l._id, {
-          $set: {
-            sort: count,
-          },
+    if (currentBoardData && Lists) {
+      const nullSortLists = currentBoardData.nullSortLists();
+      if (nullSortLists.length > 0) {
+        const lists = currentBoardData.lists();
+        let count = 0;
+        lists.forEach(l => {
+          Lists.update(l._id, {
+            $set: {
+              sort: count,
+            },
+          });
+          count += 1;
         });
         });
-        count += 1;
-      });
+      }
     }
     }
   },
   },
   onRendered() {
   onRendered() {
@@ -461,51 +552,100 @@ BlazeComponent.extendComponent({
   notDisplayThisBoard() {
   notDisplayThisBoard() {
     let allowPrivateVisibilityOnly = TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly');
     let allowPrivateVisibilityOnly = TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly');
     let currentBoard = Utils.getCurrentBoard();
     let currentBoard = Utils.getCurrentBoard();
-    if (allowPrivateVisibilityOnly !== undefined && allowPrivateVisibilityOnly.booleanValue && currentBoard.permission == 'public') {
-      return true;
-    }
-
-    return false;
+    return allowPrivateVisibilityOnly !== undefined && allowPrivateVisibilityOnly.booleanValue && currentBoard && currentBoard.permission == 'public';
   },
   },
 
 
   isViewSwimlanes() {
   isViewSwimlanes() {
     const currentUser = ReactiveCache.getCurrentUser();
     const currentUser = ReactiveCache.getCurrentUser();
+    let boardView;
+    
     if (currentUser) {
     if (currentUser) {
-      return (currentUser.profile || {}).boardView === 'board-view-swimlanes';
+      boardView = (currentUser.profile || {}).boardView;
     } else {
     } else {
-      return (
-        window.localStorage.getItem('boardView') === 'board-view-swimlanes'
-      );
+      boardView = window.localStorage.getItem('boardView');
     }
     }
-  },
-
-  hasSwimlanes() {
-    return Utils.getCurrentBoard().swimlanes().length > 0;
+    
+    // If no board view is set, default to swimlanes
+    if (!boardView) {
+      boardView = 'board-view-swimlanes';
+    }
+    
+    return boardView === 'board-view-swimlanes';
   },
   },
 
 
   isViewLists() {
   isViewLists() {
     const currentUser = ReactiveCache.getCurrentUser();
     const currentUser = ReactiveCache.getCurrentUser();
+    let boardView;
+    
     if (currentUser) {
     if (currentUser) {
-      return (currentUser.profile || {}).boardView === 'board-view-lists';
+      boardView = (currentUser.profile || {}).boardView;
     } else {
     } else {
-      return window.localStorage.getItem('boardView') === 'board-view-lists';
+      boardView = window.localStorage.getItem('boardView');
     }
     }
+    
+    return boardView === 'board-view-lists';
   },
   },
 
 
   isViewCalendar() {
   isViewCalendar() {
     const currentUser = ReactiveCache.getCurrentUser();
     const currentUser = ReactiveCache.getCurrentUser();
+    let boardView;
+    
     if (currentUser) {
     if (currentUser) {
-      return (currentUser.profile || {}).boardView === 'board-view-cal';
+      boardView = (currentUser.profile || {}).boardView;
     } else {
     } else {
-      return window.localStorage.getItem('boardView') === 'board-view-cal';
+      boardView = window.localStorage.getItem('boardView');
     }
     }
+    
+    return boardView === 'board-view-cal';
+  },
+
+  hasSwimlanes() {
+    const currentBoard = Utils.getCurrentBoard();
+    if (!currentBoard) return false;
+    
+    const swimlanes = currentBoard.swimlanes();
+    return swimlanes.length > 0;
   },
   },
 
 
+
   isVerticalScrollbars() {
   isVerticalScrollbars() {
     const user = ReactiveCache.getCurrentUser();
     const user = ReactiveCache.getCurrentUser();
     return user && user.isVerticalScrollbars();
     return user && user.isVerticalScrollbars();
   },
   },
 
 
+  boardView() {
+    return Utils.boardView();
+  },
+
+  debugBoardState() {
+    const currentBoard = Utils.getCurrentBoard();
+    const currentBoardId = Session.get('currentBoard');
+    const isBoardReady = this.isBoardReady.get();
+    const isConverting = this.isConverting.get();
+    const isMigrating = this.isMigrating.get();
+    const boardView = Utils.boardView();
+    
+    console.log('=== BOARD DEBUG STATE ===');
+    console.log('currentBoardId:', currentBoardId);
+    console.log('currentBoard:', !!currentBoard, currentBoard ? currentBoard.title : 'none');
+    console.log('isBoardReady:', isBoardReady);
+    console.log('isConverting:', isConverting);
+    console.log('isMigrating:', isMigrating);
+    console.log('boardView:', boardView);
+    console.log('========================');
+    
+    return {
+      currentBoardId,
+      hasCurrentBoard: !!currentBoard,
+      currentBoardTitle: currentBoard ? currentBoard.title : 'none',
+      isBoardReady,
+      isConverting,
+      isMigrating,
+      boardView
+    };
+  },
+
+
   openNewListForm() {
   openNewListForm() {
     if (this.isViewSwimlanes()) {
     if (this.isViewSwimlanes()) {
       // The form had been removed in 416b17062e57f215206e93a85b02ef9eb1ab4902
       // The form had been removed in 416b17062e57f215206e93a85b02ef9eb1ab4902

+ 25 - 6
client/components/boards/boardHeader.js

@@ -81,11 +81,20 @@ BlazeComponent.extendComponent({
           Modal.open('archivedBoards');
           Modal.open('archivedBoards');
         },
         },
         'click .js-toggle-board-view': Popup.open('boardChangeView'),
         'click .js-toggle-board-view': Popup.open('boardChangeView'),
-        'click .js-toggle-sidebar'() {
-          Sidebar.toggle();
-        },
+        // Sidebar toggle is handled by the sidebar component itself
+        // 'click .js-toggle-sidebar'() {
+        //   if (Sidebar) {
+        //     Sidebar.toggle();
+        //   } else {
+        //     console.warn('Sidebar not available for toggle');
+        //   }
+        // },
         'click .js-open-filter-view'() {
         'click .js-open-filter-view'() {
-          Sidebar.setView('filter');
+          if (Sidebar) {
+            Sidebar.setView('filter');
+          } else {
+            console.warn('Sidebar not available for setView');
+          }
         },
         },
         'click .js-sort-cards': Popup.open('cardsSort'),
         'click .js-sort-cards': Popup.open('cardsSort'),
         /*
         /*
@@ -102,14 +111,22 @@ BlazeComponent.extendComponent({
         */
         */
         'click .js-filter-reset'(event) {
         'click .js-filter-reset'(event) {
           event.stopPropagation();
           event.stopPropagation();
-          Sidebar.setView();
+          if (Sidebar) {
+            Sidebar.setView();
+          } else {
+            console.warn('Sidebar not available for setView');
+          }
           Filter.reset();
           Filter.reset();
         },
         },
         'click .js-sort-reset'() {
         'click .js-sort-reset'() {
           Session.set('sortBy', '');
           Session.set('sortBy', '');
         },
         },
         'click .js-open-search-view'() {
         'click .js-open-search-view'() {
-          Sidebar.setView('search');
+          if (Sidebar) {
+            Sidebar.setView('search');
+          } else {
+            console.warn('Sidebar not available for setView');
+          }
         },
         },
         'click .js-multiselection-activate'() {
         'click .js-multiselection-activate'() {
           const currentCard = Utils.getCurrentCardId();
           const currentCard = Utils.getCurrentCardId();
@@ -203,6 +220,7 @@ const CreateBoard = BlazeComponent.extendComponent({
             title: title,
             title: title,
             permission: 'private',
             permission: 'private',
             type: 'template-container',
             type: 'template-container',
+            migrationVersion: 1, // Latest version - no migration needed
           }),
           }),
        );
        );
 
 
@@ -246,6 +264,7 @@ const CreateBoard = BlazeComponent.extendComponent({
         Boards.insert({
         Boards.insert({
           title,
           title,
           permission: visibility,
           permission: visibility,
+          migrationVersion: 1, // Latest version - no migration needed
         }),
         }),
       );
       );
 
 

+ 33 - 0
client/components/cards/attachments.css

@@ -336,3 +336,36 @@
     margin-top: 10px;
     margin-top: 10px;
   }
   }
 }
 }
+
+/* Attachment migration styles */
+.attachment-item.migrating {
+  position: relative;
+  opacity: 0.7;
+}
+
+.attachment-migration-overlay {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(255, 255, 255, 0.9);
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  z-index: 10;
+  border-radius: 4px;
+}
+
+.migration-spinner {
+  font-size: 24px;
+  color: #007cba;
+  margin-bottom: 8px;
+}
+
+.migration-text {
+  font-size: 12px;
+  color: #666;
+  text-align: center;
+}

+ 7 - 1
client/components/cards/attachments.jade

@@ -57,7 +57,7 @@ template(name="attachmentGallery")
 
 
     each attachments
     each attachments
 
 
-      .attachment-item
+      .attachment-item(class="{{#if isAttachmentMigrating _id}}migrating{{/if}}")
         .attachment-thumbnail-container.open-preview(data-attachment-id="{{_id}}" data-card-id="{{ meta.cardId }}")
         .attachment-thumbnail-container.open-preview(data-attachment-id="{{_id}}" data-card-id="{{ meta.cardId }}")
           if link
           if link
             if(isImage)
             if(isImage)
@@ -97,6 +97,12 @@ template(name="attachmentGallery")
                       i.fa.fa-trash.icon(title="{{_ 'delete'}}")
                       i.fa.fa-trash.icon(title="{{_ 'delete'}}")
                     a.fa.fa-navicon.icon.js-open-attachment-menu(data-attachment-link="{{link}}" title="{{_ 'attachmentActionsPopup-title'}}")
                     a.fa.fa-navicon.icon.js-open-attachment-menu(data-attachment-link="{{link}}" title="{{_ 'attachmentActionsPopup-title'}}")
 
 
+        // Migration spinner overlay
+        if isAttachmentMigrating _id
+          .attachment-migration-overlay
+            .migration-spinner
+              i.fa.fa-cog.fa-spin
+            .migration-text {{_ 'migrating-attachment'}}
 
 
 template(name="attachmentActionsPopup")
 template(name="attachmentActionsPopup")
   ul.pop-over-list
   ul.pop-over-list

+ 18 - 0
client/components/cards/attachments.js

@@ -3,6 +3,7 @@ import { ObjectID } from 'bson';
 import DOMPurify from 'dompurify';
 import DOMPurify from 'dompurify';
 import { sanitizeHTML, sanitizeText } from '/imports/lib/secureDOMPurify';
 import { sanitizeHTML, sanitizeText } from '/imports/lib/secureDOMPurify';
 import uploadProgressManager from '../../lib/uploadProgressManager';
 import uploadProgressManager from '../../lib/uploadProgressManager';
+import { attachmentMigrationManager } from '/client/lib/attachmentMigrationManager';
 
 
 const filesize = require('filesize');
 const filesize = require('filesize');
 const prettyMilliseconds = require('pretty-ms');
 const prettyMilliseconds = require('pretty-ms');
@@ -576,3 +577,20 @@ BlazeComponent.extendComponent({
     ]
     ]
   }
   }
 }).register('attachmentRenamePopup');
 }).register('attachmentRenamePopup');
+
+// Template helpers for attachment migration status
+Template.registerHelper('attachmentMigrationStatus', function(attachmentId) {
+  return attachmentMigrationManager.getAttachmentMigrationStatus(attachmentId);
+});
+
+Template.registerHelper('isAttachmentMigrating', function(attachmentId) {
+  return attachmentMigrationManager.isAttachmentBeingMigrated(attachmentId);
+});
+
+Template.registerHelper('attachmentMigrationProgress', function() {
+  return attachmentMigrationManager.attachmentMigrationProgress.get();
+});
+
+Template.registerHelper('attachmentMigrationStatusText', function() {
+  return attachmentMigrationManager.attachmentMigrationStatus.get();
+});

+ 3 - 1
client/components/cards/minicard.js

@@ -206,7 +206,9 @@ Template.minicard.helpers({
     // Show list name if either:
     // Show list name if either:
     // 1. Board-wide setting is enabled, OR
     // 1. Board-wide setting is enabled, OR
     // 2. This specific card has the setting enabled
     // 2. This specific card has the setting enabled
-    return this.currentBoard.allowsShowListsOnMinicard || this.showListOnMinicard;
+    const currentBoard = this.currentBoard;
+    if (!currentBoard) return false;
+    return currentBoard.allowsShowListsOnMinicard || this.showListOnMinicard;
   }
   }
 });
 });
 
 

+ 8 - 0
client/components/lists/listBody.js

@@ -472,6 +472,14 @@ BlazeComponent.extendComponent({
     if (!this.selectedBoardId.get()) {
     if (!this.selectedBoardId.get()) {
       return [];
       return [];
     }
     }
+    const board = ReactiveCache.getBoard(this.selectedBoardId.get());
+    if (!board) {
+      return [];
+    }
+    
+    // Ensure default swimlane exists
+    board.getDefaultSwimline();
+    
     const swimlanes = ReactiveCache.getSwimlanes(
     const swimlanes = ReactiveCache.getSwimlanes(
     {
     {
       boardId: this.selectedBoardId.get()
       boardId: this.selectedBoardId.get()

+ 22 - 14
client/components/migrationProgress.js

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

+ 1 - 1
client/components/settings/adminReports.js

@@ -112,7 +112,7 @@ class AdminReport extends BlazeComponent {
   }
   }
 
 
   resultsCount() {
   resultsCount() {
-    return this.collection.find().countDocuments();
+    return this.collection.find().count();
   }
   }
 
 
   fileSize(size) {
   fileSize(size) {

+ 52 - 41
client/components/settings/cronSettings.js

@@ -30,28 +30,33 @@ Template.cronSettings.onCreated(function() {
   this.boardMigrationStats = new ReactiveVar({});
   this.boardMigrationStats = new ReactiveVar({});
 
 
   // Load initial data
   // Load initial data
-  this.loadCronData();
+  loadCronData(this);
 });
 });
 
 
 Template.cronSettings.helpers({
 Template.cronSettings.helpers({
   loading() {
   loading() {
-    return Template.instance().loading.get();
+    const instance = Template.instance();
+    return instance && instance.loading ? instance.loading.get() : true;
   },
   },
   
   
   showMigrations() {
   showMigrations() {
-    return Template.instance().showMigrations.get();
+    const instance = Template.instance();
+    return instance && instance.showMigrations ? instance.showMigrations.get() : true;
   },
   },
   
   
   showBoardOperations() {
   showBoardOperations() {
-    return Template.instance().showBoardOperations.get();
+    const instance = Template.instance();
+    return instance && instance.showBoardOperations ? instance.showBoardOperations.get() : false;
   },
   },
   
   
   showJobs() {
   showJobs() {
-    return Template.instance().showJobs.get();
+    const instance = Template.instance();
+    return instance && instance.showJobs ? instance.showJobs.get() : false;
   },
   },
   
   
   showAddJob() {
   showAddJob() {
-    return Template.instance().showAddJob.get();
+    const instance = Template.instance();
+    return instance && instance.showAddJob ? instance.showAddJob.get() : false;
   },
   },
   
   
   migrationProgress() {
   migrationProgress() {
@@ -86,27 +91,33 @@ Template.cronSettings.helpers({
   },
   },
 
 
   boardOperations() {
   boardOperations() {
-    return Template.instance().boardOperations.get();
+    const instance = Template.instance();
+    return instance && instance.boardOperations ? instance.boardOperations.get() : [];
   },
   },
 
 
   operationStats() {
   operationStats() {
-    return Template.instance().operationStats.get();
+    const instance = Template.instance();
+    return instance && instance.operationStats ? instance.operationStats.get() : {};
   },
   },
 
 
   pagination() {
   pagination() {
-    return Template.instance().pagination.get();
+    const instance = Template.instance();
+    return instance && instance.pagination ? instance.pagination.get() : {};
   },
   },
 
 
   queueStats() {
   queueStats() {
-    return Template.instance().queueStats.get();
+    const instance = Template.instance();
+    return instance && instance.queueStats ? instance.queueStats.get() : {};
   },
   },
 
 
   systemResources() {
   systemResources() {
-    return Template.instance().systemResources.get();
+    const instance = Template.instance();
+    return instance && instance.systemResources ? instance.systemResources.get() : {};
   },
   },
 
 
   boardMigrationStats() {
   boardMigrationStats() {
-    return Template.instance().boardMigrationStats.get();
+    const instance = Template.instance();
+    return instance && instance.boardMigrationStats ? instance.boardMigrationStats.get() : {};
   },
   },
 
 
   formatDateTime(date) {
   formatDateTime(date) {
@@ -146,7 +157,7 @@ Template.cronSettings.events({
     instance.showBoardOperations.set(true);
     instance.showBoardOperations.set(true);
     instance.showJobs.set(false);
     instance.showJobs.set(false);
     instance.showAddJob.set(false);
     instance.showAddJob.set(false);
-    instance.loadBoardOperations();
+    loadBoardOperations(instance);
   },
   },
   
   
   'click .js-cron-jobs'(event) {
   'click .js-cron-jobs'(event) {
@@ -156,7 +167,7 @@ Template.cronSettings.events({
     instance.showBoardOperations.set(false);
     instance.showBoardOperations.set(false);
     instance.showJobs.set(true);
     instance.showJobs.set(true);
     instance.showAddJob.set(false);
     instance.showAddJob.set(false);
-    instance.loadCronJobs();
+    loadCronJobs(instance);
   },
   },
   
   
   'click .js-cron-add'(event) {
   'click .js-cron-add'(event) {
@@ -174,8 +185,8 @@ Template.cronSettings.events({
         console.error('Failed to start migrations:', error);
         console.error('Failed to start migrations:', error);
         alert('Failed to start migrations: ' + error.message);
         alert('Failed to start migrations: ' + error.message);
       } else {
       } else {
-        console.log('Migrations started successfully');
-        Template.instance().pollMigrationProgress();
+        // Migrations started successfully
+        pollMigrationProgress(Template.instance());
       }
       }
     });
     });
   },
   },
@@ -204,7 +215,7 @@ Template.cronSettings.events({
   
   
   'click .js-refresh-jobs'(event) {
   'click .js-refresh-jobs'(event) {
     event.preventDefault();
     event.preventDefault();
-    Template.instance().loadCronJobs();
+    loadCronJobs(Template.instance());
   },
   },
   
   
   'click .js-start-job'(event) {
   'click .js-start-job'(event) {
@@ -216,7 +227,7 @@ Template.cronSettings.events({
         alert('Failed to start job: ' + error.message);
         alert('Failed to start job: ' + error.message);
       } else {
       } else {
         console.log('Job started successfully');
         console.log('Job started successfully');
-        Template.instance().loadCronJobs();
+        loadCronJobs(Template.instance());
       }
       }
     });
     });
   },
   },
@@ -230,7 +241,7 @@ Template.cronSettings.events({
         alert('Failed to pause job: ' + error.message);
         alert('Failed to pause job: ' + error.message);
       } else {
       } else {
         console.log('Job paused successfully');
         console.log('Job paused successfully');
-        Template.instance().loadCronJobs();
+        loadCronJobs(Template.instance());
       }
       }
     });
     });
   },
   },
@@ -244,7 +255,7 @@ Template.cronSettings.events({
         alert('Failed to stop job: ' + error.message);
         alert('Failed to stop job: ' + error.message);
       } else {
       } else {
         console.log('Job stopped successfully');
         console.log('Job stopped successfully');
-        Template.instance().loadCronJobs();
+        loadCronJobs(Template.instance());
       }
       }
     });
     });
   },
   },
@@ -259,7 +270,7 @@ Template.cronSettings.events({
           alert('Failed to remove job: ' + error.message);
           alert('Failed to remove job: ' + error.message);
         } else {
         } else {
           console.log('Job removed successfully');
           console.log('Job removed successfully');
-          Template.instance().loadCronJobs();
+          loadCronJobs(Template.instance());
         }
         }
       });
       });
     }
     }
@@ -286,7 +297,7 @@ Template.cronSettings.events({
         form.reset();
         form.reset();
         Template.instance().showJobs.set(true);
         Template.instance().showJobs.set(true);
         Template.instance().showAddJob.set(false);
         Template.instance().showAddJob.set(false);
-        Template.instance().loadCronJobs();
+        loadCronJobs(Template.instance());
       }
       }
     });
     });
   },
   },
@@ -300,7 +311,7 @@ Template.cronSettings.events({
 
 
   'click .js-refresh-board-operations'(event) {
   'click .js-refresh-board-operations'(event) {
     event.preventDefault();
     event.preventDefault();
-    Template.instance().loadBoardOperations();
+    loadBoardOperations(Template.instance());
   },
   },
 
 
   'click .js-start-test-operation'(event) {
   'click .js-start-test-operation'(event) {
@@ -318,7 +329,7 @@ Template.cronSettings.events({
         alert('Failed to start test operation: ' + error.message);
         alert('Failed to start test operation: ' + error.message);
       } else {
       } else {
         console.log('Test operation started:', result);
         console.log('Test operation started:', result);
-        Template.instance().loadBoardOperations();
+        loadBoardOperations(Template.instance());
       }
       }
     });
     });
   },
   },
@@ -328,7 +339,7 @@ Template.cronSettings.events({
     const instance = Template.instance();
     const instance = Template.instance();
     instance.searchTerm.set(searchTerm);
     instance.searchTerm.set(searchTerm);
     instance.currentPage.set(1);
     instance.currentPage.set(1);
-    instance.loadBoardOperations();
+    loadBoardOperations(instance);
   },
   },
 
 
   'click .js-prev-page'(event) {
   'click .js-prev-page'(event) {
@@ -337,7 +348,7 @@ Template.cronSettings.events({
     const currentPage = instance.currentPage.get();
     const currentPage = instance.currentPage.get();
     if (currentPage > 1) {
     if (currentPage > 1) {
       instance.currentPage.set(currentPage - 1);
       instance.currentPage.set(currentPage - 1);
-      instance.loadBoardOperations();
+      loadBoardOperations(instance);
     }
     }
   },
   },
 
 
@@ -348,7 +359,7 @@ Template.cronSettings.events({
     const pagination = instance.pagination.get();
     const pagination = instance.pagination.get();
     if (currentPage < pagination.totalPages) {
     if (currentPage < pagination.totalPages) {
       instance.currentPage.set(currentPage + 1);
       instance.currentPage.set(currentPage + 1);
-      instance.loadBoardOperations();
+      loadBoardOperations(instance);
     }
     }
   },
   },
 
 
@@ -389,16 +400,17 @@ Template.cronSettings.events({
         console.error('Failed to force board scan:', error);
         console.error('Failed to force board scan:', error);
         alert('Failed to force board scan: ' + error.message);
         alert('Failed to force board scan: ' + error.message);
       } else {
       } else {
-        console.log('Board scan started successfully');
+        // Board scan started successfully
         // Refresh the data
         // Refresh the data
-        Template.instance().loadBoardOperations();
+        loadBoardOperations(Template.instance());
       }
       }
     });
     });
   }
   }
 });
 });
 
 
-Template.cronSettings.prototype.loadCronData = function() {
-  this.loading.set(true);
+// Helper functions for cron settings
+function loadCronData(instance) {
+  instance.loading.set(true);
   
   
   // Load migration progress
   // Load migration progress
   Meteor.call('cron.getMigrationProgress', (error, result) => {
   Meteor.call('cron.getMigrationProgress', (error, result) => {
@@ -412,21 +424,20 @@ Template.cronSettings.prototype.loadCronData = function() {
   });
   });
   
   
   // Load cron jobs
   // Load cron jobs
-  this.loadCronJobs();
+  loadCronJobs(instance);
   
   
-  this.loading.set(false);
-};
+  instance.loading.set(false);
+}
 
 
-Template.cronSettings.prototype.loadCronJobs = function() {
+function loadCronJobs(instance) {
   Meteor.call('cron.getJobs', (error, result) => {
   Meteor.call('cron.getJobs', (error, result) => {
     if (result) {
     if (result) {
       cronJobs.set(result);
       cronJobs.set(result);
     }
     }
   });
   });
-};
+}
 
 
-Template.cronSettings.prototype.loadBoardOperations = function() {
-  const instance = this;
+function loadBoardOperations(instance) {
   const page = instance.currentPage.get();
   const page = instance.currentPage.get();
   const limit = instance.pageSize.get();
   const limit = instance.pageSize.get();
   const searchTerm = instance.searchTerm.get();
   const searchTerm = instance.searchTerm.get();
@@ -474,9 +485,9 @@ Template.cronSettings.prototype.loadBoardOperations = function() {
       instance.boardMigrationStats.set(result);
       instance.boardMigrationStats.set(result);
     }
     }
   });
   });
-};
+}
 
 
-Template.cronSettings.prototype.pollMigrationProgress = function() {
+function pollMigrationProgress(instance) {
   const pollInterval = setInterval(() => {
   const pollInterval = setInterval(() => {
     Meteor.call('cron.getMigrationProgress', (error, result) => {
     Meteor.call('cron.getMigrationProgress', (error, result) => {
       if (result) {
       if (result) {
@@ -493,4 +504,4 @@ Template.cronSettings.prototype.pollMigrationProgress = function() {
       }
       }
     });
     });
   }, 1000);
   }, 1000);
-};
+}

+ 169 - 0
client/lib/attachmentMigrationManager.js

@@ -0,0 +1,169 @@
+/**
+ * Attachment Migration Manager
+ * Handles migration of attachments from old structure to new structure
+ * with UI feedback and spinners for unconverted attachments
+ */
+
+import { ReactiveVar } from 'meteor/reactive-var';
+import { ReactiveCache } from '/imports/reactiveCache';
+
+// Reactive variables for attachment migration progress
+export const attachmentMigrationProgress = new ReactiveVar(0);
+export const attachmentMigrationStatus = new ReactiveVar('');
+export const isMigratingAttachments = new ReactiveVar(false);
+export const unconvertedAttachments = new ReactiveVar([]);
+
+class AttachmentMigrationManager {
+  constructor() {
+    this.migrationCache = new Map(); // Cache migrated attachment IDs
+  }
+
+  /**
+   * Check if an attachment needs migration
+   * @param {string} attachmentId - The attachment ID to check
+   * @returns {boolean} - True if attachment needs migration
+   */
+  needsMigration(attachmentId) {
+    if (this.migrationCache.has(attachmentId)) {
+      return false; // Already migrated
+    }
+
+    try {
+      const attachment = ReactiveCache.getAttachment(attachmentId);
+      if (!attachment) return false;
+
+      // Check if attachment has old structure (no meta field or missing required fields)
+      return !attachment.meta || 
+             !attachment.meta.cardId || 
+             !attachment.meta.boardId ||
+             !attachment.meta.listId;
+    } catch (error) {
+      console.error('Error checking if attachment needs migration:', error);
+      return false;
+    }
+  }
+
+  /**
+   * Get all unconverted attachments for a board
+   * @param {string} boardId - The board ID
+   * @returns {Array} - Array of unconverted attachments
+   */
+  getUnconvertedAttachments(boardId) {
+    try {
+      const attachments = ReactiveCache.getAttachments({
+        'meta.boardId': boardId
+      });
+
+      return attachments.filter(attachment => this.needsMigration(attachment._id));
+    } catch (error) {
+      console.error('Error getting unconverted attachments:', error);
+      return [];
+    }
+  }
+
+  /**
+   * Start migration for attachments in a board
+   * @param {string} boardId - The board ID
+   */
+  async startAttachmentMigration(boardId) {
+    if (isMigratingAttachments.get()) {
+      return; // Already migrating
+    }
+
+    isMigratingAttachments.set(true);
+    attachmentMigrationStatus.set('Starting attachment migration...');
+    attachmentMigrationProgress.set(0);
+
+    try {
+      const unconverted = this.getUnconvertedAttachments(boardId);
+      unconvertedAttachments.set(unconverted);
+
+      if (unconverted.length === 0) {
+        attachmentMigrationStatus.set('All attachments are already migrated');
+        attachmentMigrationProgress.set(100);
+        isMigratingAttachments.set(false);
+        return;
+      }
+
+      // Start server-side migration
+      Meteor.call('attachmentMigration.migrateBoardAttachments', boardId, (error, result) => {
+        if (error) {
+          console.error('Failed to start attachment migration:', error);
+          attachmentMigrationStatus.set(`Migration failed: ${error.message}`);
+          isMigratingAttachments.set(false);
+        } else {
+          console.log('Attachment migration started for board:', boardId);
+          this.pollAttachmentMigrationProgress(boardId);
+        }
+      });
+
+    } catch (error) {
+      console.error('Error starting attachment migration:', error);
+      attachmentMigrationStatus.set(`Migration failed: ${error.message}`);
+      isMigratingAttachments.set(false);
+    }
+  }
+
+  /**
+   * Poll for attachment migration progress
+   * @param {string} boardId - The board ID
+   */
+  pollAttachmentMigrationProgress(boardId) {
+    const pollInterval = setInterval(() => {
+      Meteor.call('attachmentMigration.getProgress', boardId, (error, result) => {
+        if (error) {
+          console.error('Error getting migration progress:', error);
+          clearInterval(pollInterval);
+          isMigratingAttachments.set(false);
+          return;
+        }
+
+        if (result) {
+          attachmentMigrationProgress.set(result.progress);
+          attachmentMigrationStatus.set(result.status);
+          unconvertedAttachments.set(result.unconvertedAttachments || []);
+
+          // Stop polling if migration is complete
+          if (result.progress >= 100 || result.status === 'completed') {
+            clearInterval(pollInterval);
+            isMigratingAttachments.set(false);
+            this.migrationCache.clear(); // Clear cache to refresh data
+          }
+        }
+      });
+    }, 1000);
+  }
+
+  /**
+   * Check if an attachment is currently being migrated
+   * @param {string} attachmentId - The attachment ID
+   * @returns {boolean} - True if attachment is being migrated
+   */
+  isAttachmentBeingMigrated(attachmentId) {
+    const unconverted = unconvertedAttachments.get();
+    return unconverted.some(attachment => attachment._id === attachmentId);
+  }
+
+  /**
+   * Get migration status for an attachment
+   * @param {string} attachmentId - The attachment ID
+   * @returns {string} - Migration status ('migrated', 'migrating', 'unmigrated')
+   */
+  getAttachmentMigrationStatus(attachmentId) {
+    if (this.migrationCache.has(attachmentId)) {
+      return 'migrated';
+    }
+
+    if (this.isAttachmentBeingMigrated(attachmentId)) {
+      return 'migrating';
+    }
+
+    return this.needsMigration(attachmentId) ? 'unmigrated' : 'migrated';
+  }
+}
+
+export const attachmentMigrationManager = new AttachmentMigrationManager();
+
+
+
+

+ 1 - 1
client/lib/boardConverter.js

@@ -5,7 +5,7 @@
  */
  */
 
 
 import { ReactiveVar } from 'meteor/reactive-var';
 import { ReactiveVar } from 'meteor/reactive-var';
-import { ReactiveCache } from '/imports/lib/reactiveCache';
+import { ReactiveCache } from '/imports/reactiveCache';
 
 
 // Reactive variables for conversion progress
 // Reactive variables for conversion progress
 export const conversionProgress = new ReactiveVar(0);
 export const conversionProgress = new ReactiveVar(0);

+ 28 - 5
client/lib/migrationManager.js

@@ -5,7 +5,7 @@
  */
  */
 
 
 import { ReactiveVar } from 'meteor/reactive-var';
 import { ReactiveVar } from 'meteor/reactive-var';
-import { ReactiveCache } from '/imports/lib/reactiveCache';
+import { ReactiveCache } from '/imports/reactiveCache';
 
 
 // Reactive variables for migration progress
 // Reactive variables for migration progress
 export const migrationProgress = new ReactiveVar(0);
 export const migrationProgress = new ReactiveVar(0);
@@ -600,10 +600,16 @@ class MigrationManager {
   }
   }
 
 
   /**
   /**
-   * Check if any migrations need to be run
+   * Check if any migrations need to be run for a specific board
    */
    */
-  needsMigration() {
-    // Check if any migration step is not completed
+  needsMigration(boardId = null) {
+    if (boardId) {
+      // Check if specific board needs migration based on version
+      const board = ReactiveCache.getBoard(boardId);
+      return !board || !board.migrationVersion || board.migrationVersion < 1;
+    }
+    
+    // Check if any migration step is not completed (global migrations)
     return this.steps.some(step => !step.completed);
     return this.steps.some(step => !step.completed);
   }
   }
 
 
@@ -623,6 +629,23 @@ class MigrationManager {
     }, 0);
     }, 0);
   }
   }
 
 
+  /**
+   * Mark a board as migrated
+   */
+  markBoardAsMigrated(boardId) {
+    try {
+      Meteor.call('boardMigration.markAsMigrated', boardId, 'full_board_migration', (error, result) => {
+        if (error) {
+          console.error('Failed to mark board as migrated:', error);
+        } else {
+          console.log('Board marked as migrated:', boardId);
+        }
+      });
+    } catch (error) {
+      console.error('Error marking board as migrated:', error);
+    }
+  }
+
   /**
   /**
    * Start migration process using cron system
    * Start migration process using cron system
    */
    */
@@ -711,7 +734,7 @@ class MigrationManager {
 
 
     // In a real implementation, this would call the actual migration
     // In a real implementation, this would call the actual migration
     // For now, we'll simulate the migration
     // For now, we'll simulate the migration
-    console.log(`Running migration: ${step.name}`);
+    // Running migration step
   }
   }
 
 
   /**
   /**

+ 4 - 4
models/attachments.js

@@ -315,10 +315,10 @@ if (Meteor.isServer) {
       fs.mkdirSync(storagePath, { recursive: true });
       fs.mkdirSync(storagePath, { recursive: true });
     }
     }
   });
   });
-
-  // Add backward compatibility methods
-  Attachments.getAttachmentWithBackwardCompatibility = getAttachmentWithBackwardCompatibility;
-  Attachments.getAttachmentsWithBackwardCompatibility = getAttachmentsWithBackwardCompatibility;
 }
 }
 
 
+// Add backward compatibility methods - available on both client and server
+Attachments.getAttachmentWithBackwardCompatibility = getAttachmentWithBackwardCompatibility;
+Attachments.getAttachmentsWithBackwardCompatibility = getAttachmentsWithBackwardCompatibility;
+
 export default Attachments;
 export default Attachments;

+ 14 - 0
models/boards.js

@@ -331,6 +331,19 @@ Boards.attachSchema(
       optional: true,
       optional: true,
       defaultValue: null,
       defaultValue: null,
     },
     },
+    migrationVersion: {
+      /**
+       * The migration version of the board structure.
+       * New boards are created with the latest version and don't need migration.
+       */
+      type: Number,
+      // eslint-disable-next-line consistent-return
+      autoValue() {
+        if (this.isInsert && !this.isSet) {
+          return 1; // Latest migration version for new boards
+        }
+      },
+    },
 
 
     subtasksDefaultListId: {
     subtasksDefaultListId: {
       /**
       /**
@@ -2196,6 +2209,7 @@ if (Meteor.isServer) {
         ],
         ],
         permission: req.body.permission || 'private',
         permission: req.body.permission || 'private',
         color: req.body.color || 'belize',
         color: req.body.color || 'belize',
+        migrationVersion: 1, // Latest version - no migration needed
       });
       });
       const swimlaneId = Swimlanes.insert({
       const swimlaneId = Swimlanes.insert({
         title: TAPi18n.__('default'),
         title: TAPi18n.__('default'),

+ 3 - 6
models/lib/meteorMongoIntegration.js

@@ -50,7 +50,7 @@ class MeteorMongoIntegration {
     this.overrideMongoCollection();
     this.overrideMongoCollection();
 
 
     this.isInitialized = true;
     this.isInitialized = true;
-    console.log('Meteor MongoDB Integration initialized successfully');
+    // Meteor MongoDB Integration initialized successfully (status available in Admin Panel)
   }
   }
 
 
   /**
   /**
@@ -296,11 +296,8 @@ export { meteorMongoIntegration, MeteorMongoIntegration };
 
 
 // Auto-initialize if MONGO_URL is available
 // Auto-initialize if MONGO_URL is available
 if (Meteor.isServer && process.env.MONGO_URL) {
 if (Meteor.isServer && process.env.MONGO_URL) {
-  console.log('Auto-initializing Meteor MongoDB Integration with MONGO_URL');
+  // Auto-initializing Meteor MongoDB Integration with MONGO_URL (status available in Admin Panel)
   meteorMongoIntegration.initialize(process.env.MONGO_URL);
   meteorMongoIntegration.initialize(process.env.MONGO_URL);
 }
 }
 
 
-// Log initialization
-if (Meteor.isServer) {
-  console.log('Meteor MongoDB Integration module loaded');
-}
+// Meteor MongoDB Integration module loaded (status available in Admin Panel)

+ 1 - 4
models/lib/mongodbConnectionManager.js

@@ -288,7 +288,4 @@ const mongodbConnectionManager = new MongoDBConnectionManager();
 // Export for use in other modules
 // Export for use in other modules
 export { mongodbConnectionManager, MongoDBConnectionManager };
 export { mongodbConnectionManager, MongoDBConnectionManager };
 
 
-// Log initialization
-if (Meteor.isServer) {
-  console.log('MongoDB Connection Manager initialized');
-}
+// MongoDB Connection Manager initialized (status available in Admin Panel)

+ 1 - 5
models/lib/mongodbDriverManager.js

@@ -270,8 +270,4 @@ const mongodbDriverManager = new MongoDBDriverManager();
 // Export for use in other modules
 // Export for use in other modules
 export { mongodbDriverManager, MongoDBDriverManager };
 export { mongodbDriverManager, MongoDBDriverManager };
 
 
-// Log initialization
-if (Meteor.isServer) {
-  console.log('MongoDB Driver Manager initialized');
-  console.log(`Supported MongoDB versions: ${mongodbDriverManager.getSupportedVersions().join(', ')}`);
-}
+// MongoDB Driver Manager initialized (status available in Admin Panel)

+ 1 - 1
models/users.js

@@ -1850,7 +1850,7 @@ if (Meteor.isServer) {
     },
     },
   });
   });
   Accounts.onCreateUser((options, user) => {
   Accounts.onCreateUser((options, user) => {
-    const userCount = ReactiveCache.getUsers({}, {}, true).countDocuments();
+    const userCount = ReactiveCache.getUsers({}, {}, true).count();
     user.isAdmin = userCount === 0;
     user.isAdmin = userCount === 0;
 
 
     if (user.services.oidc) {
     if (user.services.oidc) {

File diff suppressed because it is too large
+ 1120 - 0
package-lock.json


+ 3 - 0
sandstorm.js

@@ -359,9 +359,12 @@ if (isSandstorm && Meteor.isServer) {
   // Meteor application. We need to enforce “public” visibility as the sharing
   // Meteor application. We need to enforce “public” visibility as the sharing
   // is now handled by Sandstorm.
   // is now handled by Sandstorm.
   // See https://github.com/wekan/wekan/issues/346
   // See https://github.com/wekan/wekan/issues/346
+  // Migration disabled - using backward compatibility approach
+  /*
   Migrations.add('enforce-public-visibility-for-sandstorm', () => {
   Migrations.add('enforce-public-visibility-for-sandstorm', () => {
     Boards.update('sandstorm', { $set: { permission: 'public' } });
     Boards.update('sandstorm', { $set: { permission: 'public' } });
   });
   });
+  */
 
 
   // Monkey patch to work around the problem described in
   // Monkey patch to work around the problem described in
   // https://github.com/sandstorm-io/meteor-accounts-sandstorm/pull/31
   // https://github.com/sandstorm-io/meteor-accounts-sandstorm/pull/31

+ 12 - 3
server/00checkStartup.js

@@ -1,6 +1,18 @@
 const fs = require('fs');
 const fs = require('fs');
 const os = require('os');
 const os = require('os');
 
 
+// Configure SyncedCron to suppress console logging
+// This must be done before any SyncedCron operations
+if (Meteor.isServer) {
+  const { SyncedCron } = require('meteor/percolate:synced-cron');
+  SyncedCron.config({
+    log: false, // Disable console logging
+    collectionName: 'cronJobs', // Use custom collection name
+    utc: false, // Use local time
+    collectionTTL: 172800 // 2 days TTL
+  });
+}
+
 let errors = [];
 let errors = [];
 if (!process.env.WRITABLE_PATH) {
 if (!process.env.WRITABLE_PATH) {
   errors.push("WRITABLE_PATH environment variable missing and/or unset, please configure !");
   errors.push("WRITABLE_PATH environment variable missing and/or unset, please configure !");
@@ -25,9 +37,6 @@ if (errors.length > 0) {
   process.exit(1);
   process.exit(1);
 }
 }
 
 
-// Import migration runner for on-demand migrations
-import './migrationRunner';
-
 // Import cron job storage for persistent job tracking
 // Import cron job storage for persistent job tracking
 import './cronJobStorage';
 import './cronJobStorage';
 
 

+ 161 - 529
server/attachmentMigration.js

@@ -1,572 +1,204 @@
+/**
+ * Server-side Attachment Migration System
+ * Handles migration of attachments from old structure to new structure
+ */
+
 import { Meteor } from 'meteor/meteor';
 import { Meteor } from 'meteor/meteor';
+import { ReactiveVar } from 'meteor/reactive-var';
 import { ReactiveCache } from '/imports/reactiveCache';
 import { ReactiveCache } from '/imports/reactiveCache';
-import { Attachments, fileStoreStrategyFactory } from '/models/attachments';
-import { moveToStorage } from '/models/lib/fileStoreStrategy';
-import os from 'os';
-import { createHash } from 'crypto';
-
-// Migration state management
-const migrationState = {
-  isRunning: false,
-  isPaused: false,
-  targetStorage: null,
-  batchSize: 10,
-  delayMs: 1000,
-  cpuThreshold: 70,
-  progress: 0,
-  totalAttachments: 0,
-  migratedAttachments: 0,
-  currentBatch: [],
-  migrationQueue: [],
-  log: [],
-  startTime: null,
-  lastCpuCheck: 0
-};
-
-// CPU monitoring
-function getCpuUsage() {
-  const cpus = os.cpus();
-  let totalIdle = 0;
-  let totalTick = 0;
-  
-  cpus.forEach(cpu => {
-    for (const type in cpu.times) {
-      totalTick += cpu.times[type];
-    }
-    totalIdle += cpu.times.idle;
-  });
-  
-  const idle = totalIdle / cpus.length;
-  const total = totalTick / cpus.length;
-  const usage = 100 - Math.floor(100 * idle / total);
-  
-  return usage;
-}
 
 
-// Logging function
-function addToLog(message) {
-  const timestamp = new Date().toISOString();
-  const logEntry = `[${timestamp}] ${message}`;
-  migrationState.log.unshift(logEntry);
-  
-  // Keep only last 100 log entries
-  if (migrationState.log.length > 100) {
-    migrationState.log = migrationState.log.slice(0, 100);
-  }
-  
-  console.log(logEntry);
-}
+// Reactive variables for tracking migration progress
+const migrationProgress = new ReactiveVar(0);
+const migrationStatus = new ReactiveVar('');
+const unconvertedAttachments = new ReactiveVar([]);
 
 
-// Get migration status
-function getMigrationStatus() {
-  return {
-    isRunning: migrationState.isRunning,
-    isPaused: migrationState.isPaused,
-    targetStorage: migrationState.targetStorage,
-    progress: migrationState.progress,
-    totalAttachments: migrationState.totalAttachments,
-    migratedAttachments: migrationState.migratedAttachments,
-    remainingAttachments: migrationState.totalAttachments - migrationState.migratedAttachments,
-    status: migrationState.isRunning ? (migrationState.isPaused ? 'paused' : 'running') : 'idle',
-    log: migrationState.log.slice(0, 10).join('\n'), // Return last 10 log entries
-    startTime: migrationState.startTime,
-    estimatedTimeRemaining: calculateEstimatedTimeRemaining()
-  };
-}
-
-// Calculate estimated time remaining
-function calculateEstimatedTimeRemaining() {
-  if (!migrationState.isRunning || migrationState.migratedAttachments === 0) {
-    return null;
+class AttachmentMigrationService {
+  constructor() {
+    this.migrationCache = new Map();
   }
   }
-  
-  const elapsed = Date.now() - migrationState.startTime;
-  const rate = migrationState.migratedAttachments / elapsed; // attachments per ms
-  const remaining = migrationState.totalAttachments - migrationState.migratedAttachments;
-  
-  return Math.round(remaining / rate);
-}
 
 
-// Process a single attachment migration
-function migrateAttachment(attachmentId) {
-  try {
-    const attachment = ReactiveCache.getAttachment(attachmentId);
-    if (!attachment) {
-      addToLog(`Warning: Attachment ${attachmentId} not found`);
-      return false;
-    }
-
-    // Check if already in target storage
-    const currentStorage = fileStoreStrategyFactory.getFileStrategy(attachment, 'original').getStorageName();
-    if (currentStorage === migrationState.targetStorage) {
-      addToLog(`Attachment ${attachmentId} already in target storage ${migrationState.targetStorage}`);
-      return true;
-    }
+  /**
+   * Migrate all attachments for a board
+   * @param {string} boardId - The board ID
+   */
+  async migrateBoardAttachments(boardId) {
+    try {
+      console.log(`Starting attachment migration for board: ${boardId}`);
+      
+      // Get all attachments for the board
+      const attachments = Attachments.find({
+        'meta.boardId': boardId
+      }).fetch();
+
+      const totalAttachments = attachments.length;
+      let migratedCount = 0;
+
+      migrationStatus.set(`Migrating ${totalAttachments} attachments...`);
+      migrationProgress.set(0);
+
+      for (const attachment of attachments) {
+        try {
+          // Check if attachment needs migration
+          if (this.needsMigration(attachment)) {
+            await this.migrateAttachment(attachment);
+            this.migrationCache.set(attachment._id, true);
+          }
+          
+          migratedCount++;
+          const progress = Math.round((migratedCount / totalAttachments) * 100);
+          migrationProgress.set(progress);
+          migrationStatus.set(`Migrated ${migratedCount}/${totalAttachments} attachments...`);
+          
+        } catch (error) {
+          console.error(`Error migrating attachment ${attachment._id}:`, error);
+        }
+      }
 
 
-    // Perform migration
-    moveToStorage(attachment, migrationState.targetStorage, fileStoreStrategyFactory);
-    addToLog(`Migrated attachment ${attachmentId} from ${currentStorage} to ${migrationState.targetStorage}`);
-    
-    return true;
-  } catch (error) {
-    addToLog(`Error migrating attachment ${attachmentId}: ${error.message}`);
-    return false;
-  }
-}
+      // Update unconverted attachments list
+      const remainingUnconverted = this.getUnconvertedAttachments(boardId);
+      unconvertedAttachments.set(remainingUnconverted);
 
 
-// Process a batch of attachments
-function processBatch() {
-  if (!migrationState.isRunning || migrationState.isPaused) {
-    return;
-  }
+      migrationStatus.set('Attachment migration completed');
+      migrationProgress.set(100);
 
 
-  const batch = migrationState.migrationQueue.splice(0, migrationState.batchSize);
-  if (batch.length === 0) {
-    // Migration complete
-    migrationState.isRunning = false;
-    migrationState.progress = 100;
-    addToLog(`Migration completed. Migrated ${migrationState.migratedAttachments} attachments.`);
-    return;
-  }
+      console.log(`Attachment migration completed for board: ${boardId}`);
 
 
-  let successCount = 0;
-  batch.forEach(attachmentId => {
-    if (migrateAttachment(attachmentId)) {
-      successCount++;
-      migrationState.migratedAttachments++;
-    }
-  });
-
-  // Update progress
-  migrationState.progress = Math.round((migrationState.migratedAttachments / migrationState.totalAttachments) * 100);
-  
-  addToLog(`Processed batch: ${successCount}/${batch.length} successful. Progress: ${migrationState.progress}%`);
-
-  // Check CPU usage
-  const currentTime = Date.now();
-  if (currentTime - migrationState.lastCpuCheck > 5000) { // Check every 5 seconds
-    const cpuUsage = getCpuUsage();
-    migrationState.lastCpuCheck = currentTime;
-    
-    if (cpuUsage > migrationState.cpuThreshold) {
-      addToLog(`CPU usage ${cpuUsage}% exceeds threshold ${migrationState.cpuThreshold}%. Pausing migration.`);
-      migrationState.isPaused = true;
-      return;
+    } catch (error) {
+      console.error(`Error migrating attachments for board ${boardId}:`, error);
+      migrationStatus.set(`Migration failed: ${error.message}`);
+      throw error;
     }
     }
   }
   }
 
 
-  // Schedule next batch
-  if (migrationState.isRunning && !migrationState.isPaused) {
-    Meteor.setTimeout(() => {
-      processBatch();
-    }, migrationState.delayMs);
-  }
-}
-
-// Initialize migration queue
-function initializeMigrationQueue() {
-  const allAttachments = ReactiveCache.getAttachments();
-  migrationState.totalAttachments = allAttachments.length;
-  migrationState.migrationQueue = allAttachments.map(attachment => attachment._id);
-  migrationState.migratedAttachments = 0;
-  migrationState.progress = 0;
-  
-  addToLog(`Initialized migration queue with ${migrationState.totalAttachments} attachments`);
-}
-
-// Start migration
-function startMigration(targetStorage, batchSize, delayMs, cpuThreshold) {
-  if (migrationState.isRunning) {
-    throw new Meteor.Error('migration-already-running', 'Migration is already running');
-  }
-
-  migrationState.isRunning = true;
-  migrationState.isPaused = false;
-  migrationState.targetStorage = targetStorage;
-  migrationState.batchSize = batchSize;
-  migrationState.delayMs = delayMs;
-  migrationState.cpuThreshold = cpuThreshold;
-  migrationState.startTime = Date.now();
-  migrationState.lastCpuCheck = 0;
-
-  initializeMigrationQueue();
-  addToLog(`Started migration to ${targetStorage} with batch size ${batchSize}, delay ${delayMs}ms, CPU threshold ${cpuThreshold}%`);
-
-  // Start processing
-  processBatch();
-}
-
-// Pause migration
-function pauseMigration() {
-  if (!migrationState.isRunning) {
-    throw new Meteor.Error('migration-not-running', 'No migration is currently running');
-  }
-
-  migrationState.isPaused = true;
-  addToLog('Migration paused');
-}
+  /**
+   * Check if an attachment needs migration
+   * @param {Object} attachment - The attachment object
+   * @returns {boolean} - True if attachment needs migration
+   */
+  needsMigration(attachment) {
+    if (this.migrationCache.has(attachment._id)) {
+      return false; // Already migrated
+    }
 
 
-// Resume migration
-function resumeMigration() {
-  if (!migrationState.isRunning) {
-    throw new Meteor.Error('migration-not-running', 'No migration is currently running');
+    // Check if attachment has old structure
+    return !attachment.meta || 
+           !attachment.meta.cardId || 
+           !attachment.meta.boardId ||
+           !attachment.meta.listId;
   }
   }
 
 
-  if (!migrationState.isPaused) {
-    throw new Meteor.Error('migration-not-paused', 'Migration is not paused');
-  }
+  /**
+   * Migrate a single attachment
+   * @param {Object} attachment - The attachment object
+   */
+  async migrateAttachment(attachment) {
+    try {
+      // Get the card to find board and list information
+      const card = ReactiveCache.getCard(attachment.cardId);
+      if (!card) {
+        console.warn(`Card not found for attachment ${attachment._id}`);
+        return;
+      }
+
+      const list = ReactiveCache.getList(card.listId);
+      if (!list) {
+        console.warn(`List not found for attachment ${attachment._id}`);
+        return;
+      }
+
+      // Update attachment with new structure
+      const updateData = {
+        meta: {
+          cardId: attachment.cardId,
+          boardId: list.boardId,
+          listId: card.listId,
+          userId: attachment.userId,
+          createdAt: attachment.createdAt || new Date(),
+          migratedAt: new Date()
+        }
+      };
 
 
-  migrationState.isPaused = false;
-  addToLog('Migration resumed');
-  
-  // Continue processing
-  processBatch();
-}
+      // Preserve existing meta data if it exists
+      if (attachment.meta) {
+        updateData.meta = {
+          ...attachment.meta,
+          ...updateData.meta
+        };
+      }
 
 
-// Stop migration
-function stopMigration() {
-  if (!migrationState.isRunning) {
-    throw new Meteor.Error('migration-not-running', 'No migration is currently running');
-  }
+      Attachments.update(attachment._id, { $set: updateData });
 
 
-  migrationState.isRunning = false;
-  migrationState.isPaused = false;
-  migrationState.migrationQueue = [];
-  addToLog('Migration stopped');
-}
+      console.log(`Migrated attachment ${attachment._id}`);
 
 
-// Get attachment storage configuration
-function getAttachmentStorageConfiguration() {
-  const config = {
-    filesystemPath: process.env.WRITABLE_PATH || '/data',
-    attachmentsPath: `${process.env.WRITABLE_PATH || '/data'}/attachments`,
-    avatarsPath: `${process.env.WRITABLE_PATH || '/data'}/avatars`,
-    gridfsEnabled: true, // Always available
-    s3Enabled: false,
-    s3Endpoint: '',
-    s3Bucket: '',
-    s3Region: '',
-    s3SslEnabled: false,
-    s3Port: 443
-  };
-
-  // Check S3 configuration
-  if (process.env.S3) {
-    try {
-      const s3Config = JSON.parse(process.env.S3).s3;
-      if (s3Config && s3Config.key && s3Config.secret && s3Config.bucket) {
-        config.s3Enabled = true;
-        config.s3Endpoint = s3Config.endPoint || '';
-        config.s3Bucket = s3Config.bucket || '';
-        config.s3Region = s3Config.region || '';
-        config.s3SslEnabled = s3Config.sslEnabled || false;
-        config.s3Port = s3Config.port || 443;
-      }
     } catch (error) {
     } catch (error) {
-      console.error('Error parsing S3 configuration:', error);
+      console.error(`Error migrating attachment ${attachment._id}:`, error);
+      throw error;
     }
     }
   }
   }
 
 
-  return config;
-}
+  /**
+   * Get unconverted attachments for a board
+   * @param {string} boardId - The board ID
+   * @returns {Array} - Array of unconverted attachments
+   */
+  getUnconvertedAttachments(boardId) {
+    try {
+      const attachments = Attachments.find({
+        'meta.boardId': boardId
+      }).fetch();
 
 
-// Get attachment monitoring data
-function getAttachmentMonitoringData() {
-  const attachments = ReactiveCache.getAttachments();
-  const stats = {
-    totalAttachments: attachments.length,
-    filesystemAttachments: 0,
-    gridfsAttachments: 0,
-    s3Attachments: 0,
-    totalSize: 0,
-    filesystemSize: 0,
-    gridfsSize: 0,
-    s3Size: 0
-  };
-
-  attachments.forEach(attachment => {
-    const storage = fileStoreStrategyFactory.getFileStrategy(attachment, 'original').getStorageName();
-    const size = attachment.size || 0;
-    
-    stats.totalSize += size;
-    
-    switch (storage) {
-      case 'fs':
-        stats.filesystemAttachments++;
-        stats.filesystemSize += size;
-        break;
-      case 'gridfs':
-        stats.gridfsAttachments++;
-        stats.gridfsSize += size;
-        break;
-      case 's3':
-        stats.s3Attachments++;
-        stats.s3Size += size;
-        break;
+      return attachments.filter(attachment => this.needsMigration(attachment));
+    } catch (error) {
+      console.error('Error getting unconverted attachments:', error);
+      return [];
     }
     }
-  });
-
-  return stats;
-}
-
-// Test S3 connection
-function testS3Connection(s3Config) {
-  // This would implement actual S3 connection testing
-  // For now, we'll just validate the configuration
-  if (!s3Config.secretKey) {
-    throw new Meteor.Error('s3-secret-key-required', 'S3 secret key is required');
   }
   }
 
 
-  // In a real implementation, you would test the connection here
-  // For now, we'll just return success
-  return { success: true, message: 'S3 connection test successful' };
-}
-
-// Save S3 settings
-function saveS3Settings(s3Config) {
-  if (!s3Config.secretKey) {
-    throw new Meteor.Error('s3-secret-key-required', 'S3 secret key is required');
+  /**
+   * Get migration progress
+   * @param {string} boardId - The board ID
+   * @returns {Object} - Migration progress data
+   */
+  getMigrationProgress(boardId) {
+    const progress = migrationProgress.get();
+    const status = migrationStatus.get();
+    const unconverted = this.getUnconvertedAttachments(boardId);
+
+    return {
+      progress,
+      status,
+      unconvertedAttachments: unconverted
+    };
   }
   }
-
-  // In a real implementation, you would save the S3 configuration
-  // For now, we'll just return success
-  return { success: true, message: 'S3 settings saved successfully' };
 }
 }
 
 
-// Meteor methods
-if (Meteor.isServer) {
-  Meteor.methods({
-    // Migration methods
-    'startAttachmentMigration'(config) {
-      if (!this.userId) {
-        throw new Meteor.Error('not-authorized', 'Must be logged in');
-      }
-
-      const user = ReactiveCache.getUser(this.userId);
-      if (!user || !user.isAdmin) {
-        throw new Meteor.Error('not-authorized', 'Admin access required');
-      }
-
-      startMigration(config.targetStorage, config.batchSize, config.delayMs, config.cpuThreshold);
-      return { success: true, message: 'Migration started' };
-    },
-
-    'pauseAttachmentMigration'() {
-      if (!this.userId) {
-        throw new Meteor.Error('not-authorized', 'Must be logged in');
-      }
-
-      const user = ReactiveCache.getUser(this.userId);
-      if (!user || !user.isAdmin) {
-        throw new Meteor.Error('not-authorized', 'Admin access required');
-      }
-
-      pauseMigration();
-      return { success: true, message: 'Migration paused' };
-    },
-
-    'resumeAttachmentMigration'() {
-      if (!this.userId) {
-        throw new Meteor.Error('not-authorized', 'Must be logged in');
-      }
+const attachmentMigrationService = new AttachmentMigrationService();
 
 
-      const user = ReactiveCache.getUser(this.userId);
-      if (!user || !user.isAdmin) {
-        throw new Meteor.Error('not-authorized', 'Admin access required');
-      }
-
-      resumeMigration();
-      return { success: true, message: 'Migration resumed' };
-    },
-
-    'stopAttachmentMigration'() {
-      if (!this.userId) {
-        throw new Meteor.Error('not-authorized', 'Must be logged in');
-      }
-
-      const user = ReactiveCache.getUser(this.userId);
-      if (!user || !user.isAdmin) {
-        throw new Meteor.Error('not-authorized', 'Admin access required');
-      }
-
-      stopMigration();
-      return { success: true, message: 'Migration stopped' };
-    },
-
-    'getAttachmentMigrationSettings'() {
-      if (!this.userId) {
-        throw new Meteor.Error('not-authorized', 'Must be logged in');
-      }
-
-      const user = ReactiveCache.getUser(this.userId);
-      if (!user || !user.isAdmin) {
-        throw new Meteor.Error('not-authorized', 'Admin access required');
-      }
-
-      return {
-        batchSize: migrationState.batchSize,
-        delayMs: migrationState.delayMs,
-        cpuThreshold: migrationState.cpuThreshold,
-        status: migrationState.isRunning ? (migrationState.isPaused ? 'paused' : 'running') : 'idle',
-        progress: migrationState.progress
-      };
-    },
-
-    // Configuration methods
-    'getAttachmentStorageConfiguration'() {
-      if (!this.userId) {
-        throw new Meteor.Error('not-authorized', 'Must be logged in');
-      }
-
-      const user = ReactiveCache.getUser(this.userId);
-      if (!user || !user.isAdmin) {
-        throw new Meteor.Error('not-authorized', 'Admin access required');
-      }
-
-      return getAttachmentStorageConfiguration();
-    },
-
-    'testS3Connection'(s3Config) {
-      if (!this.userId) {
-        throw new Meteor.Error('not-authorized', 'Must be logged in');
-      }
-
-      const user = ReactiveCache.getUser(this.userId);
-      if (!user || !user.isAdmin) {
-        throw new Meteor.Error('not-authorized', 'Admin access required');
-      }
-
-      return testS3Connection(s3Config);
-    },
-
-    'saveS3Settings'(s3Config) {
-      if (!this.userId) {
-        throw new Meteor.Error('not-authorized', 'Must be logged in');
-      }
-
-      const user = ReactiveCache.getUser(this.userId);
-      if (!user || !user.isAdmin) {
-        throw new Meteor.Error('not-authorized', 'Admin access required');
-      }
-
-      return saveS3Settings(s3Config);
-    },
-
-    // Monitoring methods
-    'getAttachmentMonitoringData'() {
-      if (!this.userId) {
-        throw new Meteor.Error('not-authorized', 'Must be logged in');
-      }
-
-      const user = ReactiveCache.getUser(this.userId);
-      if (!user || !user.isAdmin) {
-        throw new Meteor.Error('not-authorized', 'Admin access required');
-      }
-
-      return getAttachmentMonitoringData();
-    },
-
-    'refreshAttachmentMonitoringData'() {
-      if (!this.userId) {
-        throw new Meteor.Error('not-authorized', 'Must be logged in');
-      }
-
-      const user = ReactiveCache.getUser(this.userId);
-      if (!user || !user.isAdmin) {
-        throw new Meteor.Error('not-authorized', 'Admin access required');
-      }
-
-      return getAttachmentMonitoringData();
-    },
-
-    'exportAttachmentMonitoringData'() {
-      if (!this.userId) {
-        throw new Meteor.Error('not-authorized', 'Must be logged in');
-      }
-
-      const user = ReactiveCache.getUser(this.userId);
-      if (!user || !user.isAdmin) {
-        throw new Meteor.Error('not-authorized', 'Admin access required');
-      }
-
-      const monitoringData = getAttachmentMonitoringData();
-      const migrationStatus = getMigrationStatus();
-      
-      return {
-        timestamp: new Date().toISOString(),
-        monitoring: monitoringData,
-        migration: migrationStatus,
-        system: {
-          cpuUsage: getCpuUsage(),
-          memoryUsage: process.memoryUsage(),
-          uptime: process.uptime()
-        }
-      };
-    }
-  });
-
-  // Publications
-  Meteor.publish('attachmentMigrationStatus', function() {
+// Meteor methods
+Meteor.methods({
+  'attachmentMigration.migrateBoardAttachments'(boardId) {
     if (!this.userId) {
     if (!this.userId) {
-      return this.ready();
+      throw new Meteor.Error('not-authorized');
     }
     }
 
 
-    const user = ReactiveCache.getUser(this.userId);
-    if (!user || !user.isAdmin) {
-      return this.ready();
-    }
+    return attachmentMigrationService.migrateBoardAttachments(boardId);
+  },
 
 
-    const self = this;
-    let handle;
-
-    function updateStatus() {
-      const status = getMigrationStatus();
-      self.changed('attachmentMigrationStatus', 'status', status);
-    }
-
-    self.added('attachmentMigrationStatus', 'status', getMigrationStatus());
-
-    // Update every 2 seconds
-    handle = Meteor.setInterval(updateStatus, 2000);
-
-    self.ready();
-
-    self.onStop(() => {
-      if (handle) {
-        Meteor.clearInterval(handle);
-      }
-    });
-  });
-
-  Meteor.publish('attachmentMonitoringData', function() {
+  'attachmentMigration.getProgress'(boardId) {
     if (!this.userId) {
     if (!this.userId) {
-      return this.ready();
+      throw new Meteor.Error('not-authorized');
     }
     }
 
 
-    const user = ReactiveCache.getUser(this.userId);
-    if (!user || !user.isAdmin) {
-      return this.ready();
-    }
-
-    const self = this;
-    let handle;
+    return attachmentMigrationService.getMigrationProgress(boardId);
+  },
 
 
-    function updateMonitoring() {
-      const data = getAttachmentMonitoringData();
-      self.changed('attachmentMonitoringData', 'data', data);
+  'attachmentMigration.getUnconvertedAttachments'(boardId) {
+    if (!this.userId) {
+      throw new Meteor.Error('not-authorized');
     }
     }
 
 
-    self.added('attachmentMonitoringData', 'data', getAttachmentMonitoringData());
-
-    // Update every 10 seconds
-    handle = Meteor.setInterval(updateMonitoring, 10000);
-
-    self.ready();
+    return attachmentMigrationService.getUnconvertedAttachments(boardId);
+  }
+});
 
 
-    self.onStop(() => {
-      if (handle) {
-        Meteor.clearInterval(handle);
-      }
-    });
-  });
-}
+export { attachmentMigrationService };

+ 36 - 10
server/boardMigrationDetector.js

@@ -5,7 +5,9 @@
 
 
 import { Meteor } from 'meteor/meteor';
 import { Meteor } from 'meteor/meteor';
 import { ReactiveVar } from 'meteor/reactive-var';
 import { ReactiveVar } from 'meteor/reactive-var';
+import { check, Match } from 'meteor/check';
 import { cronJobStorage } from './cronJobStorage';
 import { cronJobStorage } from './cronJobStorage';
+import Boards from '/models/boards';
 
 
 // Reactive variables for board migration tracking
 // Reactive variables for board migration tracking
 export const unmigratedBoards = new ReactiveVar([]);
 export const unmigratedBoards = new ReactiveVar([]);
@@ -38,7 +40,7 @@ class BoardMigrationDetector {
       this.scanUnmigratedBoards();
       this.scanUnmigratedBoards();
     }, this.scanInterval);
     }, this.scanInterval);
 
 
-    console.log('Board migration detector started');
+    // Board migration detector started
   }
   }
 
 
   /**
   /**
@@ -73,7 +75,7 @@ class BoardMigrationDetector {
     }
     }
 
 
     // Check if memory usage is reasonable
     // Check if memory usage is reasonable
-    if (resources.memoryUsage > 70) {
+    if (resources.memoryUsage > 85) {
       return false;
       return false;
     }
     }
 
 
@@ -117,7 +119,7 @@ class BoardMigrationDetector {
     migrationScanInProgress.set(true);
     migrationScanInProgress.set(true);
 
 
     try {
     try {
-      console.log('Scanning for unmigrated boards...');
+      // Scanning for unmigrated boards
       
       
       // Get all boards from the database
       // Get all boards from the database
       const boards = this.getAllBoards();
       const boards = this.getAllBoards();
@@ -132,7 +134,7 @@ class BoardMigrationDetector {
       unmigratedBoards.set(unmigrated);
       unmigratedBoards.set(unmigrated);
       lastMigrationScan.set(new Date());
       lastMigrationScan.set(new Date());
 
 
-      console.log(`Found ${unmigrated.length} unmigrated boards`);
+      // Found unmigrated boards
 
 
     } catch (error) {
     } catch (error) {
       console.error('Error scanning for unmigrated boards:', error);
       console.error('Error scanning for unmigrated boards:', error);
@@ -213,10 +215,19 @@ class BoardMigrationDetector {
   /**
   /**
    * Start migration for a specific board
    * Start migration for a specific board
    */
    */
-  async startBoardMigration(board) {
+  async startBoardMigration(boardId) {
     try {
     try {
-      console.log(`Starting migration for board: ${board.title || board._id}`);
-      
+      const board = Boards.findOne(boardId);
+      if (!board) {
+        throw new Error(`Board ${boardId} not found`);
+      }
+
+      // Check if board already has latest migration version
+      if (board.migrationVersion && board.migrationVersion >= 1) {
+        console.log(`Board ${boardId} already has latest migration version`);
+        return null;
+      }
+
       // Create migration job for this board
       // Create migration job for this board
       const jobId = `board_migration_${board._id}_${Date.now()}`;
       const jobId = `board_migration_${board._id}_${Date.now()}`;
       
       
@@ -246,7 +257,7 @@ class BoardMigrationDetector {
       return jobId;
       return jobId;
 
 
     } catch (error) {
     } catch (error) {
-      console.error(`Error starting migration for board ${board._id}:`, error);
+      console.error(`Error starting migration for board ${boardId}:`, error);
       throw error;
       throw error;
     }
     }
   }
   }
@@ -271,7 +282,7 @@ class BoardMigrationDetector {
    * Force a full scan of all boards
    * Force a full scan of all boards
    */
    */
   async forceScan() {
   async forceScan() {
-    console.log('Forcing full board migration scan...');
+      // Forcing full board migration scan
     await this.scanUnmigratedBoards();
     await this.scanUnmigratedBoards();
   }
   }
 
 
@@ -315,7 +326,7 @@ class BoardMigrationDetector {
       const updatedUnmigrated = currentUnmigrated.filter(b => b._id !== boardId);
       const updatedUnmigrated = currentUnmigrated.filter(b => b._id !== boardId);
       unmigratedBoards.set(updatedUnmigrated);
       unmigratedBoards.set(updatedUnmigrated);
 
 
-      console.log(`Marked board ${boardId} as migrated for ${migrationType}`);
+      // Marked board as migrated
 
 
     } catch (error) {
     } catch (error) {
       console.error(`Error marking board ${boardId} as migrated:`, error);
       console.error(`Error marking board ${boardId} as migrated:`, error);
@@ -353,6 +364,8 @@ Meteor.methods({
   },
   },
 
 
   'boardMigration.getBoardStatus'(boardId) {
   'boardMigration.getBoardStatus'(boardId) {
+    check(boardId, String);
+    
     if (!this.userId) {
     if (!this.userId) {
       throw new Meteor.Error('not-authorized');
       throw new Meteor.Error('not-authorized');
     }
     }
@@ -361,10 +374,23 @@ Meteor.methods({
   },
   },
 
 
   'boardMigration.markAsMigrated'(boardId, migrationType) {
   'boardMigration.markAsMigrated'(boardId, migrationType) {
+    check(boardId, String);
+    check(migrationType, String);
+    
     if (!this.userId) {
     if (!this.userId) {
       throw new Meteor.Error('not-authorized');
       throw new Meteor.Error('not-authorized');
     }
     }
     
     
     return boardMigrationDetector.markBoardAsMigrated(boardId, migrationType);
     return boardMigrationDetector.markBoardAsMigrated(boardId, migrationType);
+  },
+
+  'boardMigration.startBoardMigration'(boardId) {
+    check(boardId, String);
+    
+    if (!this.userId) {
+      throw new Meteor.Error('not-authorized');
+    }
+    
+    return boardMigrationDetector.startBoardMigration(boardId);
   }
   }
 });
 });

+ 3 - 3
server/cronJobStorage.js

@@ -36,7 +36,7 @@ class CronJobStorage {
   constructor() {
   constructor() {
     this.maxConcurrentJobs = this.getMaxConcurrentJobs();
     this.maxConcurrentJobs = this.getMaxConcurrentJobs();
     this.cpuThreshold = 80; // CPU usage threshold percentage
     this.cpuThreshold = 80; // CPU usage threshold percentage
-    this.memoryThreshold = 90; // Memory usage threshold percentage
+    this.memoryThreshold = 95; // Memory usage threshold percentage (increased for better job processing)
   }
   }
 
 
   /**
   /**
@@ -379,12 +379,12 @@ Meteor.startup(() => {
   // Resume incomplete jobs
   // Resume incomplete jobs
   const resumedJobs = cronJobStorage.resumeIncompleteJobs();
   const resumedJobs = cronJobStorage.resumeIncompleteJobs();
   if (resumedJobs.length > 0) {
   if (resumedJobs.length > 0) {
-    console.log(`Resumed ${resumedJobs.length} incomplete cron jobs:`, resumedJobs);
+    // Resumed incomplete cron jobs
   }
   }
   
   
   // Cleanup old jobs
   // Cleanup old jobs
   const cleanup = cronJobStorage.cleanupOldJobs();
   const cleanup = cronJobStorage.cleanupOldJobs();
   if (cleanup.removedQueue > 0 || cleanup.removedStatus > 0 || cleanup.removedSteps > 0) {
   if (cleanup.removedQueue > 0 || cleanup.removedStatus > 0 || cleanup.removedSteps > 0) {
-    console.log('Cleaned up old cron jobs:', cleanup);
+    // Cleaned up old cron jobs
   }
   }
 });
 });

+ 16 - 7
server/cronMigrationManager.js

@@ -247,8 +247,10 @@ class CronMigrationManager {
     // Start job processor
     // Start job processor
     this.startJobProcessor();
     this.startJobProcessor();
     
     
-    // Update cron jobs list
-    this.updateCronJobsList();
+    // Update cron jobs list after a short delay to allow SyncedCron to initialize
+    Meteor.setTimeout(() => {
+      this.updateCronJobsList();
+    }, 1000);
   }
   }
 
 
   /**
   /**
@@ -263,7 +265,7 @@ class CronMigrationManager {
       this.processJobQueue();
       this.processJobQueue();
     }, 5000); // Check every 5 seconds
     }, 5000); // Check every 5 seconds
 
 
-    console.log('Cron job processor started with CPU throttling');
+    // Cron job processor started with CPU throttling
   }
   }
 
 
   /**
   /**
@@ -469,7 +471,7 @@ class CronMigrationManager {
     const { boardId, boardTitle, migrationType } = jobData;
     const { boardId, boardTitle, migrationType } = jobData;
     
     
     try {
     try {
-      console.log(`Starting board migration for ${boardTitle || boardId}`);
+      // Starting board migration
       
       
       // Create migration steps for this board
       // Create migration steps for this board
       const steps = this.createBoardMigrationSteps(boardId, migrationType);
       const steps = this.createBoardMigrationSteps(boardId, migrationType);
@@ -503,7 +505,7 @@ class CronMigrationManager {
       // Mark board as migrated
       // Mark board as migrated
       this.markBoardAsMigrated(boardId, migrationType);
       this.markBoardAsMigrated(boardId, migrationType);
       
       
-      console.log(`Completed board migration for ${boardTitle || boardId}`);
+      // Completed board migration
 
 
     } catch (error) {
     } catch (error) {
       console.error(`Board migration failed for ${boardId}:`, error);
       console.error(`Board migration failed for ${boardId}:`, error);
@@ -633,7 +635,7 @@ class CronMigrationManager {
    */
    */
   async runMigrationStep(step) {
   async runMigrationStep(step) {
     try {
     try {
-      console.log(`Starting migration: ${step.name}`);
+      // Starting migration step
       
       
       cronMigrationCurrentStep.set(step.name);
       cronMigrationCurrentStep.set(step.name);
       cronMigrationStatus.set(`Running: ${step.description}`);
       cronMigrationStatus.set(`Running: ${step.description}`);
@@ -654,7 +656,7 @@ class CronMigrationManager {
       step.progress = 100;
       step.progress = 100;
       step.status = 'completed';
       step.status = 'completed';
 
 
-      console.log(`Completed migration: ${step.name}`);
+      // Completed migration step
       
       
       // Update progress
       // Update progress
       this.updateProgress();
       this.updateProgress();
@@ -873,6 +875,13 @@ class CronMigrationManager {
    * Update cron jobs list
    * Update cron jobs list
    */
    */
   updateCronJobsList() {
   updateCronJobsList() {
+    // Check if SyncedCron is available and has jobs
+    if (!SyncedCron || !SyncedCron.jobs || !Array.isArray(SyncedCron.jobs)) {
+      // SyncedCron not available or no jobs yet
+      cronJobs.set([]);
+      return;
+    }
+
     const jobs = SyncedCron.jobs.map(job => {
     const jobs = SyncedCron.jobs.map(job => {
       const step = this.migrationSteps.find(s => s.cronName === job.name);
       const step = this.migrationSteps.find(s => s.cronName === job.name);
       return {
       return {

+ 0 - 404
server/migrationRunner.js

@@ -1,404 +0,0 @@
-/**
- * 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()
-    };
-  }
-});

+ 0 - 1581
server/migrations.js

@@ -1,1581 +0,0 @@
-import fs from 'fs';
-import path from 'path';
-import { TAPi18n } from '/imports/i18n';
-import AccountSettings from '../models/accountSettings';
-import TableVisibilityModeSettings from '../models/tableVisibilityModeSettings';
-import Actions from '../models/actions';
-import Activities from '../models/activities';
-import Announcements from '../models/announcements';
-import Attachments from '../models/attachments';
-//import AttachmentsOld from '../models/attachments_old';
-import Avatars from '../models/avatars';
-//import AvatarsOld from '../models/avatars_old';
-import Boards from '../models/boards';
-import CardComments from '../models/cardComments';
-import Cards from '../models/cards';
-import ChecklistItems from '../models/checklistItems';
-import Checklists from '../models/checklists';
-import CustomFields from '../models/customFields';
-import Integrations from '../models/integrations';
-import InvitationCodes from '../models/invitationCodes';
-import Lists from '../models/lists';
-import Rules from '../models/rules';
-import Settings from '../models/settings';
-import Swimlanes from '../models/swimlanes';
-import Triggers from '../models/triggers';
-import UnsavedEdits from '../models/unsavedEdits';
-import Users from '../models/users';
-
-// MIGRATIONS DISABLED - BACKWARD COMPATIBILITY APPROACH
-// All migrations have been disabled to prevent downtime and performance issues
-// with large databases. Instead, the application now uses backward compatibility
-// code that detects the current database structure and works with both migrated
-// and non-migrated data without requiring any migrations to run.
-//
-// This approach ensures:
-// 1. No migration downtime
-// 2. Immediate functionality for all database states
-// 3. Gradual data structure updates as users interact with the system
-// 4. Full backward compatibility with existing data
-//
-// Original migration API (now disabled):
-//   Migrations.add(name, migrationCallback, optionalOrder);
-
-// Note that we have extra migrations defined in `sandstorm.js` that are
-// exclusive to Sandstorm and shouldn't be executed in the general case.
-// XXX I guess if we had ES6 modules we could
-// `import { isSandstorm } from sandstorm.js` and define the migration here as
-// well, but for now I want to avoid definied too many globals.
-
-// In the context of migration functions we don't want to validate database
-// mutation queries against the current (ie, latest) collection schema. Doing
-// that would work at the time we write the migration but would break in the
-// future when we'll update again the concerned collection schema.
-//
-// To prevent this bug we always have to disable the schema validation and
-// argument transformations. We generally use the shorthandlers defined below.
-const noValidate = {
-  validate: false,
-  filter: false,
-  autoConvert: false,
-  removeEmptyStrings: false,
-  getAutoValues: false,
-};
-const noValidateMulti = { ...noValidate, multi: true };
-
-// ============================================================================
-// ALL MIGRATIONS DISABLED - BACKWARD COMPATIBILITY APPROACH
-// ============================================================================
-// All migrations below have been disabled to prevent downtime and performance
-// issues with large databases. The application now uses backward compatibility
-// code in the models and client code to handle both migrated and non-migrated
-// database structures without requiring any migrations to run.
-//
-// This ensures immediate functionality regardless of database state.
-// ============================================================================
-
-/*
-Migrations.add('board-background-color', () => {
-  const defaultColor = '#16A085';
-  Boards.update(
-    {
-      background: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        background: {
-          type: 'color',
-          color: defaultColor,
-        },
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('add-cardcounterlist-allowed', () => {
-  Boards.update(
-    {
-      allowsCardCounterList: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        allowsCardCounterList: true,
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-/*
-Migrations.add('add-boardmemberlist-allowed', () => {
-  Boards.update(
-    {
-      allowsBoardMemberList: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        allowsBoardMemberList: true,
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('lowercase-board-permission', () => {
-  ['Public', 'Private'].forEach(permission => {
-    Boards.update(
-      { permission },
-      { $set: { permission: permission.toLowerCase() } },
-      noValidateMulti,
-    );
-  });
-});
-
-/*
-// Security migration: see https://github.com/wekan/wekan/issues/99
-Migrations.add('change-attachments-type-for-non-images', () => {
-  const newTypeForNonImage = 'application/octet-stream';
-  Attachments.find().forEach(file => {
-    if (!file.isImage()) {
-      Attachments.update(
-        file._id,
-        {
-          $set: {
-            'original.type': newTypeForNonImage,
-            'copies.attachments.type': newTypeForNonImage,
-          },
-        },
-        noValidate,
-      );
-    }
-  });
-});
-
-Migrations.add('card-covers', () => {
-  Cards.find().forEach(card => {
-    const cover = Attachments.findOne({ cardId: card._id, cover: true });
-    if (cover) {
-      Cards.update(card._id, { $set: { coverId: cover._id } }, noValidate);
-    }
-  });
-  Attachments.update({}, { $unset: { cover: '' } }, noValidateMulti);
-});
-
-Migrations.add('use-css-class-for-boards-colors', () => {
-  const associationTable = {
-    '#27AE60': 'nephritis',
-    '#C0392B': 'pomegranate',
-    '#2980B9': 'belize',
-    '#8E44AD': 'wisteria',
-    '#2C3E50': 'midnight',
-    '#E67E22': 'pumpkin',
-    '#CD5A91': 'moderatepink',
-    '#00AECC': 'strongcyan',
-    '#4BBF6B': 'limegreen',
-    '#2C3E51': 'dark',
-    '#27AE61': 'relax',
-    '#568BA2': 'corteza',
-    '#499BEA': 'clearblue',
-    '#596557': 'natural',
-    '#2A80B8': 'modern',
-    '#2a2a2a': 'moderndark',
-    '#222222': 'exodark',
-    '#0A0A14': 'cleandark',
-    '#FFFFFF': 'cleanlight',
-  };
-  Boards.find().forEach(board => {
-    const oldBoardColor = board.background.color;
-    const newBoardColor = associationTable[oldBoardColor];
-    Boards.update(
-      board._id,
-      {
-        $set: { color: newBoardColor },
-        $unset: { background: '' },
-      },
-      noValidate,
-    );
-  });
-});
-
-Migrations.add('denormalize-star-number-per-board', () => {
-  Boards.find().forEach(board => {
-    const nStars = Users.find({ 'profile.starredBoards': board._id }).countDocuments();
-    Boards.update(board._id, { $set: { stars: nStars } }, noValidate);
-  });
-});
-
-// We want to keep a trace of former members so we can efficiently publish their
-// infos in the general board publication.
-Migrations.add('add-member-isactive-field', () => {
-  Boards.find({}, { fields: { members: 1 } }).forEach(board => {
-    const allUsersWithSomeActivity = _.chain(
-      Activities.find(
-        { boardId: board._id },
-        { fields: { userId: 1 } },
-      ).fetch(),
-    )
-      .pluck('userId')
-      .uniq()
-      .value();
-    const currentUsers = _.pluck(board.members, 'userId');
-    const formerUsers = _.difference(allUsersWithSomeActivity, currentUsers);
-
-    const newMemberSet = [];
-    board.members.forEach(member => {
-      member.isActive = true;
-      newMemberSet.push(member);
-    });
-    formerUsers.forEach(userId => {
-      newMemberSet.push({
-        userId,
-        isAdmin: false,
-        isActive: false,
-      });
-    });
-    Boards.update(board._id, { $set: { members: newMemberSet } }, noValidate);
-  });
-});
-
-Migrations.add('add-sort-checklists', () => {
-  Checklists.find().forEach((checklist, index) => {
-    if (!checklist.hasOwnProperty('sort')) {
-      Checklists.direct.update(
-        checklist._id,
-        { $set: { sort: index } },
-        noValidate,
-      );
-    }
-    checklist.items.forEach((item, index) => {
-      if (!item.hasOwnProperty('sort')) {
-        Checklists.direct.update(
-          { _id: checklist._id, 'items._id': item._id },
-          { $set: { 'items.$.sort': index } },
-          noValidate,
-        );
-      }
-    });
-  });
-});
-
-Migrations.add('add-swimlanes', () => {
-  Boards.find().forEach(board => {
-    const swimlaneId = board.getDefaultSwimline()._id;
-    Cards.find({ boardId: board._id }).forEach(card => {
-      if (!card.hasOwnProperty('swimlaneId')) {
-        Cards.direct.update(
-          { _id: card._id },
-          { $set: { swimlaneId } },
-          noValidate,
-        );
-      }
-    });
-  });
-});
-
-Migrations.add('add-views', () => {
-  Boards.find().forEach(board => {
-    if (!board.hasOwnProperty('view')) {
-      Boards.direct.update(
-        { _id: board._id },
-        { $set: { view: 'board-view-swimlanes' } },
-        noValidate,
-      );
-    }
-  });
-});
-
-Migrations.add('add-checklist-items', () => {
-  Checklists.find().forEach(checklist => {
-    // Create new items
-    _.sortBy(checklist.items, 'sort').forEach((item, index) => {
-      ChecklistItems.direct.insert({
-        title: item.title ? item.title : 'Checklist',
-        sort: index,
-        isFinished: item.isFinished,
-        checklistId: checklist._id,
-        cardId: checklist.cardId,
-      });
-    });
-
-    // Delete old ones
-    Checklists.direct.update(
-      { _id: checklist._id },
-      { $unset: { items: 1 } },
-      noValidate,
-    );
-  });
-});
-
-Migrations.add('add-card-types', () => {
-  Cards.find().forEach(card => {
-    Cards.direct.update(
-      { _id: card._id },
-      {
-        $set: {
-          type: 'cardType-card',
-          linkedId: null,
-        },
-      },
-      noValidate,
-    );
-  });
-});
-
-Migrations.add('add-custom-fields-to-cards', () => {
-  Cards.update(
-    {
-      customFields: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        customFields: [],
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('add-requester-field', () => {
-  Cards.update(
-    {
-      requestedBy: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        requestedBy: '',
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('add-assigner-field', () => {
-  Cards.update(
-    {
-      assignedBy: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        assignedBy: '',
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('add-parent-field-to-cards', () => {
-  Cards.update(
-    {
-      parentId: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        parentId: '',
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('add-subtasks-boards', () => {
-  Boards.update(
-    {
-      subtasksDefaultBoardId: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        subtasksDefaultBoardId: null,
-        subtasksDefaultListId: null,
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('add-subtasks-sort', () => {
-  Boards.update(
-    {
-      subtaskSort: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        subtaskSort: -1,
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('add-subtasks-allowed', () => {
-  Boards.update(
-    {
-      allowsSubtasks: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        allowsSubtasks: true,
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('add-subtasks-allowed', () => {
-  Boards.update(
-    {
-      presentParentTask: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        presentParentTask: 'no-parent',
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('add-authenticationMethod', () => {
-  Users.update(
-    {
-      authenticationMethod: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        authenticationMethod: 'password',
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('remove-tag', () => {
-  Users.update(
-    {},
-    {
-      $unset: {
-        'profile.tags': 1,
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('remove-customFields-references-broken', () => {
-  Cards.update(
-    { 'customFields.$value': null },
-    {
-      $pull: {
-        customFields: { value: null },
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('add-product-name', () => {
-  Settings.update(
-    {
-      productName: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        productName: '',
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('add-hide-logo', () => {
-  Settings.update(
-    {
-      hideLogo: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        hideLogo: false,
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('add-hide-card-counter-list', () => {
-  Settings.update(
-    {
-      hideCardCounterList: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        hideCardCounterList: false,
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('add-hide-board-member-list', () => {
-  Settings.update(
-    {
-      hideBoardMemberList: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        hideBoardMemberList: false,
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('add-displayAuthenticationMethod', () => {
-  Settings.update(
-    {
-      displayAuthenticationMethod: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        displayAuthenticationMethod: true,
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('add-defaultAuthenticationMethod', () => {
-  Settings.update(
-    {
-      defaultAuthenticationMethod: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        defaultAuthenticationMethod: 'password',
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('add-templates', () => {
-  Boards.update(
-    {
-      type: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        type: 'board',
-      },
-    },
-    noValidateMulti,
-  );
-  Swimlanes.update(
-    {
-      type: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        type: 'swimlane',
-      },
-    },
-    noValidateMulti,
-  );
-  Lists.update(
-    {
-      type: {
-        $exists: false,
-      },
-      swimlaneId: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        type: 'list',
-        swimlaneId: '',
-      },
-    },
-    noValidateMulti,
-  );
-  Users.find({
-    'profile.templatesBoardId': {
-      $exists: false,
-    },
-  }).forEach(user => {
-    // Create board and swimlanes
-    Boards.insert(
-      {
-        title: TAPi18n.__('templates'),
-        permission: 'private',
-        type: 'template-container',
-        members: [
-          {
-            userId: user._id,
-            isAdmin: true,
-            isActive: true,
-            isNoComments: false,
-            isCommentOnly: false,
-          },
-        ],
-      },
-      (err, boardId) => {
-        // Insert the reference to our templates board
-        Users.update(user._id, {
-          $set: { 'profile.templatesBoardId': boardId },
-        });
-
-        // Insert the card templates swimlane
-        Swimlanes.insert(
-          {
-            title: TAPi18n.__('card-templates-swimlane'),
-            boardId,
-            sort: 1,
-            type: 'template-container',
-          },
-          (err, swimlaneId) => {
-            // Insert the reference to out card templates swimlane
-            Users.update(user._id, {
-              $set: { 'profile.cardTemplatesSwimlaneId': swimlaneId },
-            });
-          },
-        );
-
-        // Insert the list templates swimlane
-        Swimlanes.insert(
-          {
-            title: TAPi18n.__('list-templates-swimlane'),
-            boardId,
-            sort: 2,
-            type: 'template-container',
-          },
-          (err, swimlaneId) => {
-            // Insert the reference to out list templates swimlane
-            Users.update(user._id, {
-              $set: { 'profile.listTemplatesSwimlaneId': swimlaneId },
-            });
-          },
-        );
-
-        // Insert the board templates swimlane
-        Swimlanes.insert(
-          {
-            title: TAPi18n.__('board-templates-swimlane'),
-            boardId,
-            sort: 3,
-            type: 'template-container',
-          },
-          (err, swimlaneId) => {
-            // Insert the reference to out board templates swimlane
-            Users.update(user._id, {
-              $set: { 'profile.boardTemplatesSwimlaneId': swimlaneId },
-            });
-          },
-        );
-      },
-    );
-  });
-});
-
-Migrations.add('fix-circular-reference_', () => {
-  Cards.find().forEach(card => {
-    if (card.parentId === card._id) {
-      Cards.update(card._id, { $set: { parentId: '' } }, noValidateMulti);
-    }
-  });
-});
-
-Migrations.add('mutate-boardIds-in-customfields', () => {
-  CustomFields.find().forEach(cf => {
-    CustomFields.update(
-      cf,
-      {
-        $set: {
-          boardIds: [cf.boardId],
-        },
-        $unset: {
-          boardId: '',
-        },
-      },
-      noValidateMulti,
-    );
-  });
-});
-
-const modifiedAtTables = [
-  AccountSettings,
-  TableVisibilityModeSettings,
-  Actions,
-  Activities,
-  Announcements,
-  Boards,
-  CardComments,
-  Cards,
-  ChecklistItems,
-  Checklists,
-  CustomFields,
-  Integrations,
-  InvitationCodes,
-  Lists,
-  Rules,
-  Settings,
-  Swimlanes,
-  Triggers,
-  UnsavedEdits,
-  Users,
-];
-
-Migrations.add('add-missing-created-and-modified', () => {
-  Promise.all(
-    modifiedAtTables.map(db =>
-      db
-        .rawCollection()
-        .updateMany(
-          { modifiedAt: { $exists: false } },
-          { $set: { modifiedAt: new Date() } },
-          { multi: true },
-        )
-        .then(() =>
-          db
-            .rawCollection()
-            .updateMany(
-              { createdAt: { $exists: false } },
-              { $set: { createdAt: new Date() } },
-              { multi: true },
-            ),
-        ),
-    ),
-  )
-    .then(() => {
-      // eslint-disable-next-line no-console
-      console.info('Successfully added createdAt and updatedAt to all tables');
-    })
-    .catch(e => {
-      // eslint-disable-next-line no-console
-      console.error(e);
-    });
-});
-
-Migrations.add('fix-incorrect-dates', () => {
-  const tables = [
-    AccountSettings,
-    TableVisibilityModeSettings,
-    Actions,
-    Activities,
-    Announcements,
-    Boards,
-    CardComments,
-    Cards,
-    ChecklistItems,
-    Checklists,
-    CustomFields,
-    Integrations,
-    InvitationCodes,
-    Lists,
-    Rules,
-    Settings,
-    Swimlanes,
-    Triggers,
-    UnsavedEdits,
-  ];
-
-  // Dates were previously created with Date.now() which is a number, not a date
-  tables.forEach(t =>
-    t
-      .rawCollection()
-      .find({ $or: [{ createdAt: { $type: 1 } }, { updatedAt: { $type: 1 } }] })
-      .forEach(({ _id, createdAt, updatedAt }) => {
-        t.rawCollection().updateMany(
-          { _id },
-          {
-            $set: {
-              createdAt: new Date(createdAt),
-              updatedAt: new Date(updatedAt),
-            },
-          },
-        );
-      }),
-  );
-});
-
-Migrations.add('add-assignee', () => {
-  Cards.update(
-    {
-      assignees: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        assignees: [],
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('add-profile-showDesktopDragHandles', () => {
-  Users.update(
-    {
-      'profile.showDesktopDragHandles': {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        'profile.showDesktopDragHandles': false,
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('add-profile-hiddenMinicardLabelText', () => {
-  Users.update(
-    {
-      'profile.hiddenMinicardLabelText': {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        'profile.hiddenMinicardLabelText': false,
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('add-receiveddate-allowed', () => {
-  Boards.update(
-    {
-      allowsReceivedDate: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        allowsReceivedDate: true,
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('add-startdate-allowed', () => {
-  Boards.update(
-    {
-      allowsStartDate: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        allowsStartDate: true,
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('add-duedate-allowed', () => {
-  Boards.update(
-    {
-      allowsDueDate: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        allowsDueDate: true,
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('add-enddate-allowed', () => {
-  Boards.update(
-    {
-      allowsEndDate: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        allowsEndDate: true,
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('add-members-allowed', () => {
-  Boards.update(
-    {
-      allowsMembers: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        allowsMembers: true,
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('add-assignee-allowed', () => {
-  Boards.update(
-    {
-      allowsAssignee: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        allowsAssignee: true,
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('add-labels-allowed', () => {
-  Boards.update(
-    {
-      allowsLabels: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        allowsLabels: true,
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('add-checklists-allowed', () => {
-  Boards.update(
-    {
-      allowsChecklists: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        allowsChecklists: true,
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('add-attachments-allowed', () => {
-  Boards.update(
-    {
-      allowsAttachments: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        allowsAttachments: true,
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('add-comments-allowed', () => {
-  Boards.update(
-    {
-      allowsComments: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        allowsComments: true,
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('add-assigned-by-allowed', () => {
-  Boards.update(
-    {
-      allowsAssignedBy: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        allowsAssignedBy: true,
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('add-requested-by-allowed', () => {
-  Boards.update(
-    {
-      allowsRequestedBy: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        allowsRequestedBy: true,
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('add-activities-allowed', () => {
-  Boards.update(
-    {
-      allowsActivities: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        allowsActivities: true,
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('add-description-title-allowed', () => {
-  Boards.update(
-    {
-      allowsDescriptionTitle: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        allowsDescriptionTitle: true,
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('add-description-text-allowed', () => {
-  Boards.update(
-    {
-      allowsDescriptionText: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        allowsDescriptionText: true,
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('add-description-text-allowed-on-minicard', () => {
-  Boards.update(
-    {
-      allowsDescriptionTextOnMinicard: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        allowsDescriptionTextOnMinicard: true,
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-
-Migrations.add('add-sort-field-to-boards', () => {
-  Boards.find().forEach((board, index) => {
-    if (!board.hasOwnProperty('sort')) {
-      Boards.direct.update(board._id, { $set: { sort: index } }, noValidate);
-    }
-  });
-});
-
-Migrations.add('add-default-profile-view', () => {
-  Users.find().forEach(user => {
-    if (!user.hasOwnProperty('profile.boardView')) {
-      // Set default view
-      Users.direct.update(
-        { _id: user._id },
-        { $set: { 'profile.boardView': 'board-view-swimlanes' } },
-        noValidate,
-      );
-    }
-  });
-});
-
-Migrations.add('add-hide-logo-by-default', () => {
-  Settings.update(
-    {
-      hideLogo: {
-        hideLogo: false,
-      },
-    },
-    {
-      $set: {
-        hideLogo: true,
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('add-hide-card-counter-list-by-default', () => {
-  Settings.update(
-    {
-      hideCardCounterList: {
-        hideCardCounterList: false,
-      },
-    },
-    {
-      $set: {
-        hideCardCounterList: true,
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('add-hide-board-member-list-by-default', () => {
-  Settings.update(
-    {
-      hideBoardMemberList: {
-        hideBoardMember: false,
-      },
-    },
-    {
-      $set: {
-        hideBoardMemberList: true,
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('add-card-number-allowed', () => {
-  Boards.update(
-    {
-      allowsCardNumber: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        allowsCardNumber: false,
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('assign-boardwise-card-numbers', () => {
-  Boards.find().forEach(board => {
-    let nextCardNumber = board.getNextCardNumber();
-    Cards.find(
-      {
-        boardId: board._id,
-        cardNumber: {
-          $exists: false
-        }
-      },
-      {
-        sort: { createdAt: 1 }
-      }
-    ).forEach(card => {
-      Cards.update(
-        card._id,
-        { $set: { cardNumber: nextCardNumber } },
-        noValidate);
-      nextCardNumber++;
-    });
-  })
-});
-
-Migrations.add('add-card-details-show-lists', () => {
-  Boards.update(
-    {
-      allowsShowLists: {
-        $exists: false,
-      },
-    },
-    {
-      $set: {
-        allowsShowLists: true,
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-/*
-Migrations.add('migrate-attachments-collectionFS-to-ostrioFiles', () => {
-  Meteor.settings.public.ostrioFilesMigrationInProgress = "true";
-  AttachmentsOld.find().forEach(function(fileObj) {
-    const newFileName = fileObj.name();
-    const storagePath = Attachments.storagePath({});
-    // OLD:
-    // const filePath = path.join(storagePath, `${fileObj._id}-${newFileName}`);
-    // NEW: Save file only with filename of ObjectID, not including filename.
-    // Fixes https://github.com/wekan/wekan/issues/4416#issuecomment-1510517168
-    //const filePath = path.join(storagePath, `${fileObj._id}`);
-    const filePath = path.join(storagePath, `${fileObj._id}-${newFileName}`);
-
-    // This is "example" variable, change it to the userId that you might be using.
-    const userId = fileObj.userId;
-
-    const fileType = fileObj.type();
-    const fileSize = fileObj.size();
-    const fileId = fileObj._id;
-
-    const readStream = fileObj.createReadStream('attachments');
-    const writeStream = fs.createWriteStream(filePath);
-
-    writeStream.on('error', function(err) {
-      console.log('Writing error: ', err, filePath);
-    });
-
-    // Once we have a file, then upload it to our new data storage
-    readStream.on('end', () => {
-      console.log('Ended: ', filePath);
-      // UserFiles is the new Meteor-Files/FilesCollection collection instance
-
-      Attachments.addFile(
-        filePath,
-        {
-          fileName: newFileName,
-          type: fileType,
-          meta: {
-            boardId: fileObj.boardId,
-            cardId: fileObj.cardId,
-            listId: fileObj.listId,
-            swimlaneId: fileObj.swimlaneId,
-            uploadedBeforeMigration: fileObj.uploadedAt,
-            migrationTime: new Date(),
-            copies: fileObj.copies,
-            source: 'import'
-          },
-          userId,
-          size: fileSize,
-          fileId,
-        },
-        (err, fileRef) => {
-          if (err) {
-            console.log(err);
-          } else {
-            console.log('File Inserted: ', fileRef._id);
-            // Set the userId again
-            Attachments.update({ _id: fileRef._id }, { $set: { userId } });
-            fileObj.remove();
-          }
-        },
-        true,
-      ); // proceedAfterUpload
-    });
-
-    readStream.on('error', error => {
-      console.log('Error: ', filePath, error);
-    });
-
-    readStream.pipe(writeStream);
-  });
-  Meteor.settings.public.ostrioFilesMigrationInProgress = "false";
-});
-
-Migrations.add('migrate-avatars-collectionFS-to-ostrioFiles', () => {
-  Meteor.settings.public.ostrioFilesMigrationInProgress = "true";
-  AvatarsOld.find().forEach(function(fileObj) {
-    const newFileName = fileObj.name();
-    const storagePath = Avatars.storagePath({});
-    // OLD:
-    // const filePath = path.join(storagePath, `${fileObj._id}-${newFileName}`);
-    // NEW: Save file only with filename of ObjectID, not including filename.
-    // Fixes https://github.com/wekan/wekan/issues/4416#issuecomment-1510517168
-    //const filePath = path.join(storagePath, `${fileObj._id}`);
-    const filePath = path.join(storagePath, `${fileObj._id}-${newFileName}`);
-
-    // This is "example" variable, change it to the userId that you might be using.
-    const userId = fileObj.userId;
-
-    const fileType = fileObj.type();
-    const fileSize = fileObj.size();
-    const fileId = fileObj._id;
-
-    const readStream = fileObj.createReadStream('avatars');
-    const writeStream = fs.createWriteStream(filePath);
-
-    writeStream.on('error', function(err) {
-      console.log('Writing error: ', err, filePath);
-    });
-
-    // Once we have a file, then upload it to our new data storage
-    readStream.on('end', () => {
-      console.log('Ended: ', filePath);
-      // UserFiles is the new Meteor-Files/FilesCollection collection instance
-
-      Avatars.addFile(
-        filePath,
-        {
-          fileName: newFileName,
-          type: fileType,
-          meta: {
-            boardId: fileObj.boardId,
-            cardId: fileObj.cardId,
-            listId: fileObj.listId,
-            swimlaneId: fileObj.swimlaneId,
-          },
-          userId,
-          size: fileSize,
-          fileId,
-        },
-        (err, fileRef) => {
-          if (err) {
-            console.log(err);
-          } else {
-            console.log('File Inserted: ', newFileName, fileRef._id);
-            // Set the userId again
-            Avatars.update({ _id: fileRef._id }, { $set: { userId } });
-            Users.find().forEach(user => {
-              const old_url = fileObj.url();
-              new_url = Avatars.findOne({ _id: fileRef._id }).link(
-                'original',
-                '/',
-              );
-              if (user.profile.avatarUrl !== undefined) {
-                if (user.profile.avatarUrl.startsWith(old_url)) {
-                  // Set avatar url to new url
-                  Users.direct.update(
-                    { _id: user._id },
-                    { $set: { 'profile.avatarUrl': new_url } },
-                    noValidate,
-                  );
-                  console.log('User avatar updated: ', user._id, new_url);
-                }
-              }
-            });
-            fileObj.remove();
-          }
-        },
-        true, // proceedAfterUpload
-      );
-    });
-
-    readStream.on('error', error => {
-      console.log('Error: ', filePath, error);
-    });
-
-    readStream.pipe(writeStream);
-  });
-  Meteor.settings.public.ostrioFilesMigrationInProgress = "false";
-});
-
-Migrations.add('migrate-attachment-drop-index-cardId', () => {
-  try {
-    Attachments.collection._dropIndex({'cardId': 1});
-  } catch (error) {
-  }
-});
-
-Migrations.add('migrate-attachment-migration-fix-source-import', () => {
-  // there was an error at first versions, so source was import, instead of import
-  Attachments.update(
-    {"meta.source":"import,"},
-    {$set:{"meta.source":"import"}},
-    noValidateMulti
-  );
-});
-
-/*
-Migrations.add('attachment-cardCopy-fix-boardId-etc', () => {
-  Attachments.find( {"meta.source": "copy"} ).forEach(_attachment => {
-    const cardId = _attachment.meta.cardId;
-    const card = Cards.findOne(cardId);
-    if (card.boardId !== undefined && card.listId !== undefined && card.swimlaneId !== undefined) {
-      console.log("update attachment id: ", _attachment._id);
-      Attachments.update(_attachment._id, {
-        $set: {
-          "meta.boardId": card.boardId,
-          "meta.listId": card.listId,
-          "meta.swimlaneId": card.swimlaneId,
-        }
-      });
-    }
-  });
-});
-
-Migrations.add('remove-unused-planning-poker', () => {
-  Cards.update(
-    {
-      "poker.question": false,
-    },
-    {
-      $unset: {
-        "poker": 1,
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('remove-user-profile-hiddenSystemMessages', () => {
-  Users.update(
-    {
-      "profile.hiddenSystemMessages": {
-        $exists: true,
-      },
-    },
-    {
-      $unset: {
-        "profile.hiddenSystemMessages": 1,
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-Migrations.add('remove-user-profile-hideCheckedItems', () => {
-  Users.update(
-    {
-      "profile.hideCheckedItems": {
-        $exists: true,
-      },
-    },
-    {
-      $unset: {
-        "profile.hideCheckedItems": 1,
-      },
-    },
-    noValidateMulti,
-  );
-});
-
-// Migration disabled - using backward compatibility approach instead
-// This migration was taking too long for large databases
-// The feature now works with both old lists (without swimlaneId) and new lists (with swimlaneId)
-/*
-Migrations.add('migrate-lists-to-per-swimlane', () => {
-  if (process.env.DEBUG === 'true') {
-    console.log('Starting migration: migrate-lists-to-per-swimlane');
-  }
-
-  try {
-    // Get all boards
-    const boards = Boards.find({}).fetch();
-
-    boards.forEach(board => {
-      if (process.env.DEBUG === 'true') {
-        console.log(`Processing board: ${board.title} (${board._id})`);
-      }
-
-      // Get the default swimlane for this board
-      const defaultSwimlane = board.getDefaultSwimline();
-      if (!defaultSwimlane) {
-        if (process.env.DEBUG === 'true') {
-          console.log(`No default swimlane found for board ${board._id}, skipping`);
-        }
-        return;
-      }
-
-      // Get all lists for this board that don't have a swimlaneId or have empty swimlaneId
-      const listsWithoutSwimlane = Lists.find({
-        boardId: board._id,
-        $or: [
-          { swimlaneId: { $exists: false } },
-          { swimlaneId: '' },
-          { swimlaneId: null }
-        ]
-      }).fetch();
-
-      if (process.env.DEBUG === 'true') {
-        console.log(`Found ${listsWithoutSwimlane.length} lists without swimlaneId in board ${board._id}`);
-      }
-
-      // Update each list to belong to the default swimlane
-      listsWithoutSwimlane.forEach(list => {
-        if (process.env.DEBUG === 'true') {
-          console.log(`Updating list "${list.title}" to belong to swimlane "${defaultSwimlane.title}"`);
-        }
-
-        Lists.direct.update(list._id, {
-          $set: {
-            swimlaneId: defaultSwimlane._id
-          }
-        }, noValidate);
-      });
-    });
-
-    if (process.env.DEBUG === 'true') {
-      console.log('Migration migrate-lists-to-per-swimlane completed successfully');
-    }
-
-  } catch (error) {
-    console.error('Error during migration migrate-lists-to-per-swimlane:', error);
-    throw error;
-  }
-});
-*/
-
-// ============================================================================
-// END OF DISABLED MIGRATIONS
-// ============================================================================
-// All migrations above have been disabled. The application now uses backward
-// compatibility code to handle both migrated and non-migrated database structures.
-// ============================================================================

+ 11 - 28
server/mongodb-driver-startup.js

@@ -13,57 +13,40 @@ import { meteorMongoIntegration } from '/models/lib/meteorMongoIntegration';
 
 
 // Initialize MongoDB driver system on server startup
 // Initialize MongoDB driver system on server startup
 Meteor.startup(async function() {
 Meteor.startup(async function() {
-  console.log('=== MongoDB Driver System Startup ===');
+  // MongoDB Driver System Startup (status available in Admin Panel)
   
   
   try {
   try {
     // Check if MONGO_URL is available
     // Check if MONGO_URL is available
     const mongoUrl = process.env.MONGO_URL;
     const mongoUrl = process.env.MONGO_URL;
     if (!mongoUrl) {
     if (!mongoUrl) {
-      console.log('MONGO_URL not found, skipping MongoDB driver initialization');
+      // MONGO_URL not found, skipping MongoDB driver initialization
       return;
       return;
     }
     }
 
 
-    console.log('MONGO_URL found, initializing MongoDB driver system...');
-    console.log(`Connection string: ${mongoUrl.replace(/\/\/.*@/, '//***:***@')}`); // Hide credentials
+    // MONGO_URL found, initializing MongoDB driver system
+    // Connection string: (credentials hidden for security)
 
 
     // Initialize the Meteor integration
     // Initialize the Meteor integration
     meteorMongoIntegration.initialize(mongoUrl);
     meteorMongoIntegration.initialize(mongoUrl);
 
 
     // Test the connection
     // Test the connection
-    console.log('Testing MongoDB connection...');
     const testResult = await meteorMongoIntegration.testConnection();
     const testResult = await meteorMongoIntegration.testConnection();
     
     
     if (testResult.success) {
     if (testResult.success) {
-      console.log('✅ MongoDB connection test successful');
-      console.log(`   Driver: ${testResult.driver}`);
-      console.log(`   Version: ${testResult.version}`);
+      // MongoDB connection test successful
+      // Driver and version information available in Admin Panel
     } else {
     } else {
-      console.log('❌ MongoDB connection test failed');
-      console.log(`   Error: ${testResult.error}`);
-      console.log(`   Driver: ${testResult.driver}`);
-      console.log(`   Version: ${testResult.version}`);
+      // MongoDB connection test failed
+      // Error details available in Admin Panel
     }
     }
 
 
-    // Log connection statistics
+    // Connection statistics available in Admin Panel
     const stats = meteorMongoIntegration.getStats();
     const stats = meteorMongoIntegration.getStats();
-    console.log('MongoDB Driver System Statistics:');
-    console.log(`   Initialized: ${stats.isInitialized}`);
-    console.log(`   Custom Connection: ${stats.hasCustomConnection}`);
-    console.log(`   Supported Versions: ${mongodbDriverManager.getSupportedVersions().join(', ')}`);
 
 
-    // Log driver compatibility information
-    console.log('MongoDB Driver Compatibility:');
+    // Driver compatibility information available in Admin Panel
     const supportedVersions = mongodbDriverManager.getSupportedVersions();
     const supportedVersions = mongodbDriverManager.getSupportedVersions();
-    supportedVersions.forEach(version => {
-      const driverInfo = mongodbDriverManager.getDriverInfo(
-        mongodbDriverManager.getDriverForVersion(version)
-      );
-      if (driverInfo) {
-        console.log(`   MongoDB ${version}: ${driverInfo.driver} v${driverInfo.version}`);
-      }
-    });
 
 
-    console.log('=== MongoDB Driver System Ready ===');
+    // MongoDB Driver System Ready (status available in Admin Panel)
 
 
   } catch (error) {
   } catch (error) {
     console.error('Error during MongoDB driver system startup:', error.message);
     console.error('Error during MongoDB driver system startup:', error.message);

+ 3 - 3
server/publications/cards.js

@@ -706,11 +706,11 @@ function findCards(sessionId, query) {
   };
   };
 
 
   if (cards) {
   if (cards) {
-    update.$set.totalHits = cards.countDocuments();
+    update.$set.totalHits = cards.count();
     update.$set.lastHit =
     update.$set.lastHit =
-      query.projection.skip + query.projection.limit < cards.countDocuments()
+      query.projection.skip + query.projection.limit < cards.count()
         ? query.projection.skip + query.projection.limit
         ? query.projection.skip + query.projection.limit
-        : cards.countDocuments();
+        : cards.count();
     update.$set.cards = cards.map(card => {
     update.$set.cards = cards.map(card => {
       return card._id;
       return card._id;
     });
     });

Some files were not shown because too many files changed in this diff