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