| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396 | /** * Board Migration Detector * Detects boards that need migration and manages automatic migration scheduling */import { Meteor } from 'meteor/meteor';import { ReactiveVar } from 'meteor/reactive-var';import { check, Match } from 'meteor/check';import { cronJobStorage } from './cronJobStorage';import Boards from '/models/boards';// Reactive variables for board migration trackingexport const unmigratedBoards = new ReactiveVar([]);export const migrationScanInProgress = new ReactiveVar(false);export const lastMigrationScan = new ReactiveVar(null);class BoardMigrationDetector {  constructor() {    this.scanInterval = null;    this.isScanning = false;    this.migrationCheckInterval = 30000; // Check every 30 seconds    this.scanInterval = 60000; // Full scan every minute  }  /**   * Start the automatic migration detector   */  start() {    if (this.scanInterval) {      return; // Already running    }    // Check for idle migration opportunities    this.scanInterval = Meteor.setInterval(() => {      this.checkForIdleMigration();    }, this.migrationCheckInterval);    // Full board scan every minute    this.fullScanInterval = Meteor.setInterval(() => {      this.scanUnmigratedBoards();    }, this.scanInterval);    // Board migration detector started  }  /**   * Stop the automatic migration detector   */  stop() {    if (this.scanInterval) {      Meteor.clearInterval(this.scanInterval);      this.scanInterval = null;    }    if (this.fullScanInterval) {      Meteor.clearInterval(this.fullScanInterval);      this.fullScanInterval = null;    }  }  /**   * Check if system is idle and can run migrations   */  isSystemIdle() {    const resources = cronJobStorage.getSystemResources();    const queueStats = cronJobStorage.getQueueStats();        // Check if no jobs are running    if (queueStats.running > 0) {      return false;    }    // Check if CPU usage is low    if (resources.cpuUsage > 30) { // Lower threshold for idle migration      return false;    }    // Check if memory usage is reasonable    if (resources.memoryUsage > 85) {      return false;    }    return true;  }  /**   * Check for idle migration opportunities   */  async checkForIdleMigration() {    if (!this.isSystemIdle()) {      return;    }    // Get unmigrated boards    const unmigrated = unmigratedBoards.get();    if (unmigrated.length === 0) {      return; // No boards to migrate    }    // Check if we can start a new job    const canStart = cronJobStorage.canStartNewJob();    if (!canStart.canStart) {      return;    }    // Start migrating the next board    const boardToMigrate = unmigrated[0];    await this.startBoardMigration(boardToMigrate);  }  /**   * Scan for unmigrated boards   */  async scanUnmigratedBoards() {    if (this.isScanning) {      return; // Already scanning    }    this.isScanning = true;    migrationScanInProgress.set(true);    try {      // Scanning for unmigrated boards            // Get all boards from the database      const boards = this.getAllBoards();      const unmigrated = [];      for (const board of boards) {        if (await this.needsMigration(board)) {          unmigrated.push(board);        }      }      unmigratedBoards.set(unmigrated);      lastMigrationScan.set(new Date());      // Found unmigrated boards    } catch (error) {      console.error('Error scanning for unmigrated boards:', error);    } finally {      this.isScanning = false;      migrationScanInProgress.set(false);    }  }  /**   * Get all boards from the database   */  getAllBoards() {    // This would need to be implemented based on your board model    // For now, we'll simulate getting boards    try {      // Assuming you have a Boards collection      if (typeof Boards !== 'undefined') {        return Boards.find({}, { fields: { _id: 1, title: 1, createdAt: 1, modifiedAt: 1 } }).fetch();      }            // Fallback: return empty array if Boards collection not available      return [];    } catch (error) {      console.error('Error getting boards:', error);      return [];    }  }  /**   * Check if a board needs migration   */  async needsMigration(board) {    try {      // Check if board has been migrated by looking for migration markers      const migrationMarkers = this.getMigrationMarkers(board._id);            // Check for specific migration indicators      const needsListMigration = !migrationMarkers.listsMigrated;      const needsAttachmentMigration = !migrationMarkers.attachmentsMigrated;      const needsSwimlaneMigration = !migrationMarkers.swimlanesMigrated;            return needsListMigration || needsAttachmentMigration || needsSwimlaneMigration;          } catch (error) {      console.error(`Error checking migration status for board ${board._id}:`, error);      return false;    }  }  /**   * Get migration markers for a board   */  getMigrationMarkers(boardId) {    try {      // Check if board has migration metadata      const board = Boards.findOne(boardId, { fields: { migrationMarkers: 1 } });            if (!board || !board.migrationMarkers) {        return {          listsMigrated: false,          attachmentsMigrated: false,          swimlanesMigrated: false        };      }      return board.migrationMarkers;    } catch (error) {      console.error(`Error getting migration markers for board ${boardId}:`, error);      return {        listsMigrated: false,        attachmentsMigrated: false,        swimlanesMigrated: false      };    }  }  /**   * Start migration for a specific board   */  async startBoardMigration(boardId) {    try {      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      const jobId = `board_migration_${board._id}_${Date.now()}`;            // Add to job queue with high priority      cronJobStorage.addToQueue(jobId, 'board_migration', 1, {        boardId: board._id,        boardTitle: board.title,        migrationType: 'full_board_migration'      });      // Save initial job status      cronJobStorage.saveJobStatus(jobId, {        jobType: 'board_migration',        status: 'pending',        progress: 0,        boardId: board._id,        boardTitle: board.title,        migrationType: 'full_board_migration',        createdAt: new Date()      });      // Remove from unmigrated list      const currentUnmigrated = unmigratedBoards.get();      const updatedUnmigrated = currentUnmigrated.filter(b => b._id !== board._id);      unmigratedBoards.set(updatedUnmigrated);      return jobId;    } catch (error) {      console.error(`Error starting migration for board ${boardId}:`, error);      throw error;    }  }  /**   * Get migration statistics   */  getMigrationStats() {    const unmigrated = unmigratedBoards.get();    const lastScan = lastMigrationScan.get();    const isScanning = migrationScanInProgress.get();    return {      unmigratedCount: unmigrated.length,      lastScanTime: lastScan,      isScanning,      nextScanIn: this.scanInterval ? this.scanInterval / 1000 : 0    };  }  /**   * Force a full scan of all boards   */  async forceScan() {      // Forcing full board migration scan    await this.scanUnmigratedBoards();  }  /**   * Get detailed migration status for a specific board   */  getBoardMigrationStatus(boardId) {    const unmigrated = unmigratedBoards.get();    const isUnmigrated = unmigrated.some(b => b._id === boardId);        if (!isUnmigrated) {      return { needsMigration: false, reason: 'Board is already migrated' };    }    const migrationMarkers = this.getMigrationMarkers(boardId);    const needsMigration = !migrationMarkers.listsMigrated ||                           !migrationMarkers.attachmentsMigrated ||                           !migrationMarkers.swimlanesMigrated;    return {      needsMigration,      migrationMarkers,      reason: needsMigration ? 'Board requires migration' : 'Board is up to date'    };  }  /**   * Mark a board as migrated   */  markBoardAsMigrated(boardId, migrationType) {    try {      // Update migration markers      const updateQuery = {};      updateQuery[`migrationMarkers.${migrationType}Migrated`] = true;      updateQuery['migrationMarkers.lastMigration'] = new Date();      Boards.update(boardId, { $set: updateQuery });      // Remove from unmigrated list if present      const currentUnmigrated = unmigratedBoards.get();      const updatedUnmigrated = currentUnmigrated.filter(b => b._id !== boardId);      unmigratedBoards.set(updatedUnmigrated);      // Marked board as migrated    } catch (error) {      console.error(`Error marking board ${boardId} as migrated:`, error);    }  }}// Export singleton instanceexport const boardMigrationDetector = new BoardMigrationDetector();// Start the detector on server startupMeteor.startup(() => {  // Wait a bit for the system to initialize  Meteor.setTimeout(() => {    boardMigrationDetector.start();  }, 10000); // Start after 10 seconds});// Meteor methods for client accessMeteor.methods({  'boardMigration.getStats'() {    if (!this.userId) {      throw new Meteor.Error('not-authorized');    }        return boardMigrationDetector.getMigrationStats();  },  'boardMigration.forceScan'() {    if (!this.userId) {      throw new Meteor.Error('not-authorized');    }        return boardMigrationDetector.forceScan();  },  'boardMigration.getBoardStatus'(boardId) {    check(boardId, String);        if (!this.userId) {      throw new Meteor.Error('not-authorized');    }        return boardMigrationDetector.getBoardMigrationStatus(boardId);  },  'boardMigration.markAsMigrated'(boardId, migrationType) {    check(boardId, String);    check(migrationType, String);        if (!this.userId) {      throw new Meteor.Error('not-authorized');    }        return boardMigrationDetector.markBoardAsMigrated(boardId, migrationType);  },  'boardMigration.startBoardMigration'(boardId) {    check(boardId, String);        if (!this.userId) {      throw new Meteor.Error('not-authorized');    }        return boardMigrationDetector.startBoardMigration(boardId);  }});
 |