| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500 | /** * Cron Migration Manager * Manages database migrations as cron jobs using percolate:synced-cron */import { Meteor } from 'meteor/meteor';import { SyncedCron } from 'meteor/percolate:synced-cron';import { ReactiveVar } from 'meteor/reactive-var';import { cronJobStorage } from './cronJobStorage';// Server-side reactive variables for cron migration progressexport const cronMigrationProgress = new ReactiveVar(0);export const cronMigrationStatus = new ReactiveVar('');export const cronMigrationCurrentStep = new ReactiveVar('');export const cronMigrationSteps = new ReactiveVar([]);export const cronIsMigrating = new ReactiveVar(false);export const cronJobs = new ReactiveVar([]);// Board-specific operation trackingexport const boardOperations = new ReactiveVar(new Map());export const boardOperationProgress = new ReactiveVar(new Map());class CronMigrationManager {  constructor() {    this.migrationSteps = this.initializeMigrationSteps();    this.currentStepIndex = 0;    this.startTime = null;    this.isRunning = false;    this.jobProcessor = null;    this.processingInterval = null;  }  /**   * Initialize migration steps as cron jobs   */  initializeMigrationSteps() {    return [      {        id: 'board-background-color',        name: 'Board Background Colors',        description: 'Setting up board background colors',        weight: 1,        completed: false,        progress: 0,        cronName: 'migration_board_background_color',        schedule: 'every 1 minute', // Will be changed to 'once' when triggered        status: 'stopped'      },      {        id: 'add-cardcounterlist-allowed',        name: 'Card Counter List Settings',        description: 'Adding card counter list permissions',        weight: 1,        completed: false,        progress: 0,        cronName: 'migration_card_counter_list',        schedule: 'every 1 minute',        status: 'stopped'      },      {        id: 'add-boardmemberlist-allowed',        name: 'Board Member List Settings',        description: 'Adding board member list permissions',        weight: 1,        completed: false,        progress: 0,        cronName: 'migration_board_member_list',        schedule: 'every 1 minute',        status: 'stopped'      },      {        id: 'lowercase-board-permission',        name: 'Board Permission Standardization',        description: 'Converting board permissions to lowercase',        weight: 1,        completed: false,        progress: 0,        cronName: 'migration_lowercase_permission',        schedule: 'every 1 minute',        status: 'stopped'      },      {        id: 'change-attachments-type-for-non-images',        name: 'Attachment Type Standardization',        description: 'Updating attachment types for non-images',        weight: 2,        completed: false,        progress: 0,        cronName: 'migration_attachment_types',        schedule: 'every 1 minute',        status: 'stopped'      },      {        id: 'card-covers',        name: 'Card Covers System',        description: 'Setting up card cover functionality',        weight: 2,        completed: false,        progress: 0,        cronName: 'migration_card_covers',        schedule: 'every 1 minute',        status: 'stopped'      },      {        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,        cronName: 'migration_board_color_css',        schedule: 'every 1 minute',        status: 'stopped'      },      {        id: 'denormalize-star-number-per-board',        name: 'Board Star Counts',        description: 'Calculating star counts per board',        weight: 3,        completed: false,        progress: 0,        cronName: 'migration_star_numbers',        schedule: 'every 1 minute',        status: 'stopped'      },      {        id: 'add-member-isactive-field',        name: 'Member Activity Status',        description: 'Adding member activity tracking',        weight: 2,        completed: false,        progress: 0,        cronName: 'migration_member_activity',        schedule: 'every 1 minute',        status: 'stopped'      },      {        id: 'add-sort-checklists',        name: 'Checklist Sorting',        description: 'Adding sort order to checklists',        weight: 2,        completed: false,        progress: 0,        cronName: 'migration_sort_checklists',        schedule: 'every 1 minute',        status: 'stopped'      },      {        id: 'add-swimlanes',        name: 'Swimlanes System',        description: 'Setting up swimlanes functionality',        weight: 4,        completed: false,        progress: 0,        cronName: 'migration_swimlanes',        schedule: 'every 1 minute',        status: 'stopped'      },      {        id: 'add-views',        name: 'Board Views',        description: 'Adding board view options',        weight: 2,        completed: false,        progress: 0,        cronName: 'migration_views',        schedule: 'every 1 minute',        status: 'stopped'      },      {        id: 'add-checklist-items',        name: 'Checklist Items',        description: 'Setting up checklist items system',        weight: 3,        completed: false,        progress: 0,        cronName: 'migration_checklist_items',        schedule: 'every 1 minute',        status: 'stopped'      },      {        id: 'add-card-types',        name: 'Card Types',        description: 'Adding card type functionality',        weight: 2,        completed: false,        progress: 0,        cronName: 'migration_card_types',        schedule: 'every 1 minute',        status: 'stopped'      },      {        id: 'add-custom-fields-to-cards',        name: 'Custom Fields',        description: 'Adding custom fields to cards',        weight: 3,        completed: false,        progress: 0,        cronName: 'migration_custom_fields',        schedule: 'every 1 minute',        status: 'stopped'      },      {        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,        cronName: 'migration_attachments_collectionfs',        schedule: 'every 1 minute',        status: 'stopped'      },      {        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,        cronName: 'migration_avatars_collectionfs',        schedule: 'every 1 minute',        status: 'stopped'      },      {        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,        cronName: 'migration_lists_per_swimlane',        schedule: 'every 1 minute',        status: 'stopped'      }    ];  }  /**   * Initialize all migration cron jobs   */  initializeCronJobs() {    this.migrationSteps.forEach(step => {      this.createCronJob(step);    });        // Start job processor    this.startJobProcessor();        // Update cron jobs list after a short delay to allow SyncedCron to initialize    Meteor.setTimeout(() => {      this.updateCronJobsList();    }, 1000);  }  /**   * Start the job processor for CPU-aware job execution   */  startJobProcessor() {    if (this.processingInterval) {      return; // Already running    }    this.processingInterval = Meteor.setInterval(() => {      this.processJobQueue();    }, 5000); // Check every 5 seconds    // Cron job processor started with CPU throttling  }  /**   * Stop the job processor   */  stopJobProcessor() {    if (this.processingInterval) {      Meteor.clearInterval(this.processingInterval);      this.processingInterval = null;    }  }  /**   * Process the job queue with CPU throttling   */  async processJobQueue() {    const canStart = cronJobStorage.canStartNewJob();        if (!canStart.canStart) {      // Suppress "Cannot start new job: Maximum concurrent jobs reached" message      // console.log(`Cannot start new job: ${canStart.reason}`);      return;    }    const nextJob = cronJobStorage.getNextJob();    if (!nextJob) {      return; // No jobs in queue    }    // Start the job    await this.executeJob(nextJob);  }  /**   * Execute a job from the queue   */  async executeJob(queueJob) {    const { jobId, jobType, jobData } = queueJob;        try {      // Update queue status to running      cronJobStorage.updateQueueStatus(jobId, 'running', { startedAt: new Date() });            // Save job status      cronJobStorage.saveJobStatus(jobId, {        jobType,        status: 'running',        progress: 0,        startedAt: new Date(),        ...jobData      });      // Execute based on job type      if (jobType === 'migration') {        await this.executeMigrationJob(jobId, jobData);      } else if (jobType === 'board_operation') {        await this.executeBoardOperationJob(jobId, jobData);      } else if (jobType === 'board_migration') {        await this.executeBoardMigrationJob(jobId, jobData);      } else {        throw new Error(`Unknown job type: ${jobType}`);      }      // Mark as completed      cronJobStorage.updateQueueStatus(jobId, 'completed', { completedAt: new Date() });      cronJobStorage.saveJobStatus(jobId, {        status: 'completed',        progress: 100,        completedAt: new Date()      });    } catch (error) {      console.error(`Job ${jobId} failed:`, error);            // Mark as failed      cronJobStorage.updateQueueStatus(jobId, 'failed', {         failedAt: new Date(),        error: error.message       });      cronJobStorage.saveJobStatus(jobId, {        status: 'failed',        error: error.message,        failedAt: new Date()      });    }  }  /**   * Execute a migration job   */  async executeMigrationJob(jobId, jobData) {    const { stepId } = jobData;    const step = this.migrationSteps.find(s => s.id === stepId);        if (!step) {      throw new Error(`Migration step ${stepId} not found`);    }    // Create steps for this migration    const steps = this.createMigrationSteps(step);        for (let i = 0; i < steps.length; i++) {      const stepData = steps[i];            // Save step status      cronJobStorage.saveJobStep(jobId, i, {        stepName: stepData.name,        status: 'running',        progress: 0      });      // Execute step      await this.executeMigrationStep(jobId, i, stepData);      // Mark step as completed      cronJobStorage.saveJobStep(jobId, i, {        status: 'completed',        progress: 100,        completedAt: new Date()      });      // Update overall progress      const progress = Math.round(((i + 1) / steps.length) * 100);      cronJobStorage.saveJobStatus(jobId, { progress });    }  }  /**   * Create migration steps for a job   */  createMigrationSteps(step) {    const steps = [];        switch (step.id) {      case 'board-background-color':        steps.push(          { name: 'Initialize board colors', duration: 1000 },          { name: 'Update board documents', duration: 2000 },          { name: 'Finalize changes', duration: 500 }        );        break;      case 'add-cardcounterlist-allowed':        steps.push(          { name: 'Add card counter permissions', duration: 800 },          { name: 'Update existing boards', duration: 1500 },          { name: 'Verify permissions', duration: 700 }        );        break;      case 'migrate-attachments-collectionFS-to-ostrioFiles':        steps.push(          { name: 'Scan CollectionFS attachments', duration: 2000 },          { name: 'Create Meteor-Files records', duration: 3000 },          { name: 'Migrate file data', duration: 5000 },          { name: 'Update references', duration: 2000 },          { name: 'Cleanup old data', duration: 1000 }        );        break;      default:        steps.push(          { name: `Execute ${step.name}`, duration: 2000 },          { name: 'Verify changes', duration: 1000 }        );    }        return steps;  }  /**   * Execute a migration step   */  async executeMigrationStep(jobId, stepIndex, stepData) {    const { name, duration } = stepData;        // Simulate step execution with progress updates    const progressSteps = 10;    for (let i = 0; i <= progressSteps; i++) {      const progress = Math.round((i / progressSteps) * 100);            // Update step progress      cronJobStorage.saveJobStep(jobId, stepIndex, {        progress,        currentAction: `Executing: ${name} (${progress}%)`      });            // Simulate work      await new Promise(resolve => setTimeout(resolve, duration / progressSteps));    }  }  /**   * Execute a board operation job   */  async executeBoardOperationJob(jobId, jobData) {    const { operationType, operationData } = jobData;        // Use existing board operation logic    await this.executeBoardOperation(jobId, operationType, operationData);  }  /**   * Execute a board migration job   */  async executeBoardMigrationJob(jobId, jobData) {    const { boardId, boardTitle, migrationType } = jobData;        try {      // Starting board migration            // Create migration steps for this board      const steps = this.createBoardMigrationSteps(boardId, migrationType);            for (let i = 0; i < steps.length; i++) {        const stepData = steps[i];                // Save step status        cronJobStorage.saveJobStep(jobId, i, {          stepName: stepData.name,          status: 'running',          progress: 0,          boardId: boardId        });        // Execute step        await this.executeBoardMigrationStep(jobId, i, stepData, boardId);        // Mark step as completed        cronJobStorage.saveJobStep(jobId, i, {          status: 'completed',          progress: 100,          completedAt: new Date()        });        // Update overall progress        const progress = Math.round(((i + 1) / steps.length) * 100);        cronJobStorage.saveJobStatus(jobId, { progress });      }      // Mark board as migrated      this.markBoardAsMigrated(boardId, migrationType);            // Completed board migration    } catch (error) {      console.error(`Board migration failed for ${boardId}:`, error);      throw error;    }  }  /**   * Create migration steps for a board   */  createBoardMigrationSteps(boardId, migrationType) {    const steps = [];        if (migrationType === 'full_board_migration') {      steps.push(        { name: 'Check board structure', duration: 500, type: 'validation' },        { name: 'Migrate lists to swimlanes', duration: 2000, type: 'lists' },        { name: 'Migrate attachments', duration: 3000, type: 'attachments' },        { name: 'Update board metadata', duration: 1000, type: 'metadata' },        { name: 'Verify migration', duration: 1000, type: 'verification' }      );    } else {      // Default migration steps      steps.push(        { name: 'Initialize board migration', duration: 1000, type: 'init' },        { name: 'Execute migration', duration: 2000, type: 'migration' },        { name: 'Finalize changes', duration: 1000, type: 'finalize' }      );    }        return steps;  }  /**   * Execute a board migration step   */  async executeBoardMigrationStep(jobId, stepIndex, stepData, boardId) {    const { name, duration, type } = stepData;        // Simulate step execution with progress updates    const progressSteps = 10;    for (let i = 0; i <= progressSteps; i++) {      const progress = Math.round((i / progressSteps) * 100);            // Update step progress      cronJobStorage.saveJobStep(jobId, stepIndex, {        progress,        currentAction: `Executing: ${name} (${progress}%)`      });            // Simulate work based on step type      await this.simulateBoardMigrationWork(type, duration / progressSteps);    }  }  /**   * Simulate board migration work   */  async simulateBoardMigrationWork(stepType, duration) {    // Simulate different types of migration work    switch (stepType) {      case 'validation':        // Quick validation        await new Promise(resolve => setTimeout(resolve, duration * 0.5));        break;      case 'lists':        // List migration work        await new Promise(resolve => setTimeout(resolve, duration));        break;      case 'attachments':        // Attachment migration work        await new Promise(resolve => setTimeout(resolve, duration * 1.2));        break;      case 'metadata':        // Metadata update work        await new Promise(resolve => setTimeout(resolve, duration * 0.8));        break;      case 'verification':        // Verification work        await new Promise(resolve => setTimeout(resolve, duration * 0.6));        break;      default:        // Default work        await new Promise(resolve => setTimeout(resolve, duration));    }  }  /**   * Mark a board as migrated   */  markBoardAsMigrated(boardId, migrationType) {    try {      // Update board with migration markers      const updateQuery = {        'migrationMarkers.fullMigrationCompleted': true,        'migrationMarkers.lastMigration': new Date(),        'migrationMarkers.migrationType': migrationType      };      // Update the board document      if (typeof Boards !== 'undefined') {        Boards.update(boardId, { $set: updateQuery });      }      console.log(`Marked board ${boardId} as migrated`);    } catch (error) {      console.error(`Error marking board ${boardId} as migrated:`, error);    }  }  /**   * Create a cron job for a migration step   */  createCronJob(step) {    SyncedCron.add({      name: step.cronName,      schedule: (parser) => parser.text(step.schedule),      job: () => {        this.runMigrationStep(step);      },    });  }  /**   * Run a migration step   */  async runMigrationStep(step) {    try {      // Starting migration step            cronMigrationCurrentStep.set(step.name);      cronMigrationStatus.set(`Running: ${step.description}`);      cronIsMigrating.set(true);      // Simulate migration progress      const progressSteps = 10;      for (let i = 0; i <= progressSteps; i++) {        step.progress = (i / progressSteps) * 100;        this.updateProgress();                // Simulate work        await new Promise(resolve => setTimeout(resolve, 100));      }      // Mark as completed      step.completed = true;      step.progress = 100;      step.status = 'completed';      // Completed migration step            // Update progress      this.updateProgress();    } catch (error) {      console.error(`Migration ${step.name} failed:`, error);      step.status = 'error';      cronMigrationStatus.set(`Migration failed: ${error.message}`);    }  }  /**   * Start all migrations using job queue   */  async startAllMigrations() {    if (this.isRunning) {      return;    }    this.isRunning = true;    cronIsMigrating.set(true);    cronMigrationStatus.set('Adding migrations to job queue...');    this.startTime = Date.now();    try {      // Add all migration steps to the job queue      for (let i = 0; i < this.migrationSteps.length; i++) {        const step = this.migrationSteps[i];                if (step.completed) {          continue; // Skip already completed steps        }        // Add to job queue        const jobId = `migration_${step.id}_${Date.now()}`;        cronJobStorage.addToQueue(jobId, 'migration', step.weight, {          stepId: step.id,          stepName: step.name,          stepDescription: step.description        });        // Save initial job status        cronJobStorage.saveJobStatus(jobId, {          jobType: 'migration',          status: 'pending',          progress: 0,          stepId: step.id,          stepName: step.name,          stepDescription: step.description        });      }      cronMigrationStatus.set('Migrations added to queue. Processing will begin shortly...');            // Start monitoring progress      this.monitorMigrationProgress();    } catch (error) {      console.error('Failed to start migrations:', error);      cronMigrationStatus.set(`Failed to start migrations: ${error.message}`);      cronIsMigrating.set(false);      this.isRunning = false;    }  }  /**   * Monitor migration progress   */  monitorMigrationProgress() {    const monitorInterval = Meteor.setInterval(() => {      const stats = cronJobStorage.getQueueStats();      const incompleteJobs = cronJobStorage.getIncompleteJobs();            // Update progress      const totalJobs = stats.total;      const completedJobs = stats.completed;      const progress = totalJobs > 0 ? Math.round((completedJobs / totalJobs) * 100) : 0;            cronMigrationProgress.set(progress);            // Update status      if (stats.running > 0) {        const runningJob = incompleteJobs.find(job => job.status === 'running');        if (runningJob) {          cronMigrationCurrentStep.set(runningJob.stepName || 'Processing migration...');          cronMigrationStatus.set(`Running: ${runningJob.stepName || 'Migration in progress'}`);        }      } else if (stats.pending > 0) {        cronMigrationStatus.set(`${stats.pending} migrations pending in queue`);        cronMigrationCurrentStep.set('Waiting for available resources...');      } else if (stats.completed === totalJobs && totalJobs > 0) {        // All migrations completed        cronMigrationStatus.set('All migrations completed successfully!');        cronMigrationProgress.set(100);        cronMigrationCurrentStep.set('');                // Clear status after delay        setTimeout(() => {          cronIsMigrating.set(false);          cronMigrationStatus.set('');          cronMigrationProgress.set(0);        }, 3000);                Meteor.clearInterval(monitorInterval);      }    }, 2000); // Check every 2 seconds  }  /**   * Start a specific cron job   */  async startCronJob(cronName) {    // Change schedule to run once    const job = SyncedCron.jobs.find(j => j.name === cronName);    if (job) {      job.schedule = 'once';      SyncedCron.start();    }  }  /**   * Wait for a cron job to complete   */  async waitForCronJobCompletion(step) {    return new Promise((resolve) => {      const checkInterval = setInterval(() => {        if (step.completed || step.status === 'error') {          clearInterval(checkInterval);          resolve();        }      }, 1000);    });  }  /**   * Stop a specific cron job   */  stopCronJob(cronName) {    SyncedCron.remove(cronName);    const step = this.migrationSteps.find(s => s.cronName === cronName);    if (step) {      step.status = 'stopped';    }    this.updateCronJobsList();  }  /**   * Pause a specific cron job   */  pauseCronJob(cronName) {    SyncedCron.pause(cronName);    const step = this.migrationSteps.find(s => s.cronName === cronName);    if (step) {      step.status = 'paused';    }    this.updateCronJobsList();  }  /**   * Resume a specific cron job   */  resumeCronJob(cronName) {    SyncedCron.resume(cronName);    const step = this.migrationSteps.find(s => s.cronName === cronName);    if (step) {      step.status = 'running';    }    this.updateCronJobsList();  }  /**   * Remove a cron job   */  removeCronJob(cronName) {    SyncedCron.remove(cronName);    this.migrationSteps = this.migrationSteps.filter(s => s.cronName !== cronName);    this.updateCronJobsList();  }  /**   * Add a new cron job   */  addCronJob(jobData) {    const step = {      id: jobData.id || `custom_${Date.now()}`,      name: jobData.name,      description: jobData.description,      weight: jobData.weight || 1,      completed: false,      progress: 0,      cronName: jobData.cronName || `custom_${Date.now()}`,      schedule: jobData.schedule || 'every 1 minute',      status: 'stopped'    };    this.migrationSteps.push(step);    this.createCronJob(step);    this.updateCronJobsList();  }  /**   * 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);        cronMigrationProgress.set(progress);    cronMigrationSteps.set([...this.migrationSteps]);  }  /**   * Update cron jobs list   */  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 step = this.migrationSteps.find(s => s.cronName === job.name);      return {        name: job.name,        schedule: job.schedule,        status: step ? step.status : 'unknown',        lastRun: job.lastRun,        nextRun: job.nextRun,        running: job.running      };    });    cronJobs.set(jobs);  }  /**   * Get all cron jobs   */  getAllCronJobs() {    return cronJobs.get();  }  /**   * Get migration steps   */  getMigrationSteps() {    return this.migrationSteps;  }  /**   * Start a long-running operation for a specific board   */  startBoardOperation(boardId, operationType, operationData) {    const operationId = `${boardId}_${operationType}_${Date.now()}`;        // Add to job queue    cronJobStorage.addToQueue(operationId, 'board_operation', 3, {      boardId,      operationType,      operationData    });    // Save initial job status    cronJobStorage.saveJobStatus(operationId, {      jobType: 'board_operation',      status: 'pending',      progress: 0,      boardId,      operationType,      operationData,      createdAt: new Date()    });    // Update board operations map for backward compatibility    const operation = {      id: operationId,      boardId: boardId,      type: operationType,      data: operationData,      status: 'pending',      progress: 0,      startTime: new Date(),      endTime: null,      error: null    };    const operations = boardOperations.get();    operations.set(operationId, operation);    boardOperations.set(operations);    return operationId;  }  /**   * Execute a board operation   */  async executeBoardOperation(operationId, operationType, operationData) {    const operations = boardOperations.get();    const operation = operations.get(operationId);        if (!operation) {      console.error(`Operation ${operationId} not found`);      return;    }    try {      console.log(`Starting board operation: ${operationType} for board ${operation.boardId}`);            // Update operation status      operation.status = 'running';      operation.progress = 0;      this.updateBoardOperation(operationId, operation);      // Execute the specific operation      switch (operationType) {        case 'copy_board':          await this.copyBoard(operationId, operationData);          break;        case 'move_board':          await this.moveBoard(operationId, operationData);          break;        case 'copy_swimlane':          await this.copySwimlane(operationId, operationData);          break;        case 'move_swimlane':          await this.moveSwimlane(operationId, operationData);          break;        case 'copy_list':          await this.copyList(operationId, operationData);          break;        case 'move_list':          await this.moveList(operationId, operationData);          break;        case 'copy_card':          await this.copyCard(operationId, operationData);          break;        case 'move_card':          await this.moveCard(operationId, operationData);          break;        case 'copy_checklist':          await this.copyChecklist(operationId, operationData);          break;        case 'move_checklist':          await this.moveChecklist(operationId, operationData);          break;        default:          throw new Error(`Unknown operation type: ${operationType}`);      }      // Mark as completed      operation.status = 'completed';      operation.progress = 100;      operation.endTime = new Date();      this.updateBoardOperation(operationId, operation);      console.log(`Completed board operation: ${operationType} for board ${operation.boardId}`);    } catch (error) {      console.error(`Board operation ${operationType} failed:`, error);      operation.status = 'error';      operation.error = error.message;      operation.endTime = new Date();      this.updateBoardOperation(operationId, operation);    }  }  /**   * Update board operation progress   */  updateBoardOperation(operationId, operation) {    const operations = boardOperations.get();    operations.set(operationId, operation);    boardOperations.set(operations);    // Update progress map    const progressMap = boardOperationProgress.get();    progressMap.set(operationId, {      progress: operation.progress,      status: operation.status,      error: operation.error    });    boardOperationProgress.set(progressMap);  }  /**   * Copy board operation   */  async copyBoard(operationId, data) {    const { sourceBoardId, targetBoardId, copyOptions } = data;    const operation = boardOperations.get().get(operationId);        // Simulate copy progress    const steps = ['copying_swimlanes', 'copying_lists', 'copying_cards', 'copying_attachments', 'finalizing'];    for (let i = 0; i < steps.length; i++) {      operation.progress = Math.round(((i + 1) / steps.length) * 100);      this.updateBoardOperation(operationId, operation);            // Simulate work      await new Promise(resolve => setTimeout(resolve, 1000));    }  }  /**   * Move board operation   */  async moveBoard(operationId, data) {    const { sourceBoardId, targetBoardId, moveOptions } = data;    const operation = boardOperations.get().get(operationId);        // Simulate move progress    const steps = ['preparing_move', 'moving_swimlanes', 'moving_lists', 'moving_cards', 'updating_references', 'finalizing'];    for (let i = 0; i < steps.length; i++) {      operation.progress = Math.round(((i + 1) / steps.length) * 100);      this.updateBoardOperation(operationId, operation);            // Simulate work      await new Promise(resolve => setTimeout(resolve, 800));    }  }  /**   * Copy swimlane operation   */  async copySwimlane(operationId, data) {    const { sourceSwimlaneId, targetBoardId, copyOptions } = data;    const operation = boardOperations.get().get(operationId);        // Simulate copy progress    const steps = ['copying_swimlane', 'copying_lists', 'copying_cards', 'finalizing'];    for (let i = 0; i < steps.length; i++) {      operation.progress = Math.round(((i + 1) / steps.length) * 100);      this.updateBoardOperation(operationId, operation);            // Simulate work      await new Promise(resolve => setTimeout(resolve, 500));    }  }  /**   * Move swimlane operation   */  async moveSwimlane(operationId, data) {    const { sourceSwimlaneId, targetBoardId, moveOptions } = data;    const operation = boardOperations.get().get(operationId);        // Simulate move progress    const steps = ['preparing_move', 'moving_swimlane', 'updating_references', 'finalizing'];    for (let i = 0; i < steps.length; i++) {      operation.progress = Math.round(((i + 1) / steps.length) * 100);      this.updateBoardOperation(operationId, operation);            // Simulate work      await new Promise(resolve => setTimeout(resolve, 400));    }  }  /**   * Copy list operation   */  async copyList(operationId, data) {    const { sourceListId, targetBoardId, copyOptions } = data;    const operation = boardOperations.get().get(operationId);        // Simulate copy progress    const steps = ['copying_list', 'copying_cards', 'copying_attachments', 'finalizing'];    for (let i = 0; i < steps.length; i++) {      operation.progress = Math.round(((i + 1) / steps.length) * 100);      this.updateBoardOperation(operationId, operation);            // Simulate work      await new Promise(resolve => setTimeout(resolve, 300));    }  }  /**   * Move list operation   */  async moveList(operationId, data) {    const { sourceListId, targetBoardId, moveOptions } = data;    const operation = boardOperations.get().get(operationId);        // Simulate move progress    const steps = ['preparing_move', 'moving_list', 'updating_references', 'finalizing'];    for (let i = 0; i < steps.length; i++) {      operation.progress = Math.round(((i + 1) / steps.length) * 100);      this.updateBoardOperation(operationId, operation);            // Simulate work      await new Promise(resolve => setTimeout(resolve, 200));    }  }  /**   * Copy card operation   */  async copyCard(operationId, data) {    const { sourceCardId, targetListId, copyOptions } = data;    const operation = boardOperations.get().get(operationId);        // Simulate copy progress    const steps = ['copying_card', 'copying_attachments', 'copying_checklists', 'finalizing'];    for (let i = 0; i < steps.length; i++) {      operation.progress = Math.round(((i + 1) / steps.length) * 100);      this.updateBoardOperation(operationId, operation);            // Simulate work      await new Promise(resolve => setTimeout(resolve, 150));    }  }  /**   * Move card operation   */  async moveCard(operationId, data) {    const { sourceCardId, targetListId, moveOptions } = data;    const operation = boardOperations.get().get(operationId);        // Simulate move progress    const steps = ['preparing_move', 'moving_card', 'updating_references', 'finalizing'];    for (let i = 0; i < steps.length; i++) {      operation.progress = Math.round(((i + 1) / steps.length) * 100);      this.updateBoardOperation(operationId, operation);            // Simulate work      await new Promise(resolve => setTimeout(resolve, 100));    }  }  /**   * Copy checklist operation   */  async copyChecklist(operationId, data) {    const { sourceChecklistId, targetCardId, copyOptions } = data;    const operation = boardOperations.get().get(operationId);        // Simulate copy progress    const steps = ['copying_checklist', 'copying_items', 'finalizing'];    for (let i = 0; i < steps.length; i++) {      operation.progress = Math.round(((i + 1) / steps.length) * 100);      this.updateBoardOperation(operationId, operation);            // Simulate work      await new Promise(resolve => setTimeout(resolve, 100));    }  }  /**   * Move checklist operation   */  async moveChecklist(operationId, data) {    const { sourceChecklistId, targetCardId, moveOptions } = data;    const operation = boardOperations.get().get(operationId);        // Simulate move progress    const steps = ['preparing_move', 'moving_checklist', 'finalizing'];    for (let i = 0; i < steps.length; i++) {      operation.progress = Math.round(((i + 1) / steps.length) * 100);      this.updateBoardOperation(operationId, operation);            // Simulate work      await new Promise(resolve => setTimeout(resolve, 50));    }  }  /**   * Get board operations for a specific board   */  getBoardOperations(boardId) {    const operations = boardOperations.get();    const boardOps = [];        for (const [operationId, operation] of operations) {      if (operation.boardId === boardId) {        boardOps.push(operation);      }    }        return boardOps.sort((a, b) => b.startTime - a.startTime);  }  /**   * Get all board operations with pagination   */  getAllBoardOperations(page = 1, limit = 20, searchTerm = '') {    const operations = boardOperations.get();    const allOps = Array.from(operations.values());        // Filter by search term if provided    let filteredOps = allOps;    if (searchTerm) {      filteredOps = allOps.filter(op =>         op.boardId.toLowerCase().includes(searchTerm.toLowerCase()) ||        op.type.toLowerCase().includes(searchTerm.toLowerCase())      );    }        // Sort by start time (newest first)    filteredOps.sort((a, b) => b.startTime - a.startTime);        // Paginate    const startIndex = (page - 1) * limit;    const endIndex = startIndex + limit;    const paginatedOps = filteredOps.slice(startIndex, endIndex);        return {      operations: paginatedOps,      total: filteredOps.length,      page: page,      limit: limit,      totalPages: Math.ceil(filteredOps.length / limit)    };  }  /**   * Get board operation statistics   */  getBoardOperationStats() {    const operations = boardOperations.get();    const stats = {      total: operations.size,      running: 0,      completed: 0,      error: 0,      byType: {}    };        for (const [operationId, operation] of operations) {      stats[operation.status]++;            if (!stats.byType[operation.type]) {        stats.byType[operation.type] = 0;      }      stats.byType[operation.type]++;    }        return stats;  }}// Export singleton instanceexport const cronMigrationManager = new CronMigrationManager();// Initialize cron jobs on server startMeteor.startup(() => {  cronMigrationManager.initializeCronJobs();});// Meteor methods for client-server communicationMeteor.methods({  'cron.startAllMigrations'() {    if (!this.userId) {      throw new Meteor.Error('not-authorized');    }        return cronMigrationManager.startAllMigrations();  },    'cron.startJob'(cronName) {    if (!this.userId) {      throw new Meteor.Error('not-authorized');    }        return cronMigrationManager.startCronJob(cronName);  },    'cron.stopJob'(cronName) {    if (!this.userId) {      throw new Meteor.Error('not-authorized');    }        return cronMigrationManager.stopCronJob(cronName);  },    'cron.pauseJob'(cronName) {    if (!this.userId) {      throw new Meteor.Error('not-authorized');    }        return cronMigrationManager.pauseCronJob(cronName);  },    'cron.resumeJob'(cronName) {    if (!this.userId) {      throw new Meteor.Error('not-authorized');    }        return cronMigrationManager.resumeCronJob(cronName);  },    'cron.removeJob'(cronName) {    if (!this.userId) {      throw new Meteor.Error('not-authorized');    }        return cronMigrationManager.removeCronJob(cronName);  },    'cron.addJob'(jobData) {    if (!this.userId) {      throw new Meteor.Error('not-authorized');    }        return cronMigrationManager.addCronJob(jobData);  },    'cron.getJobs'() {    return cronMigrationManager.getAllCronJobs();  },    'cron.getMigrationProgress'() {    return {      progress: cronMigrationProgress.get(),      status: cronMigrationStatus.get(),      currentStep: cronMigrationCurrentStep.get(),      steps: cronMigrationSteps.get(),      isMigrating: cronIsMigrating.get()    };  },  'cron.startBoardOperation'(boardId, operationType, operationData) {    if (!this.userId) {      throw new Meteor.Error('not-authorized');    }        return cronMigrationManager.startBoardOperation(boardId, operationType, operationData);  },  'cron.getBoardOperations'(boardId) {    if (!this.userId) {      throw new Meteor.Error('not-authorized');    }        return cronMigrationManager.getBoardOperations(boardId);  },  'cron.getAllBoardOperations'(page, limit, searchTerm) {    if (!this.userId) {      throw new Meteor.Error('not-authorized');    }        return cronMigrationManager.getAllBoardOperations(page, limit, searchTerm);  },  'cron.getBoardOperationStats'() {    if (!this.userId) {      throw new Meteor.Error('not-authorized');    }        return cronMigrationManager.getBoardOperationStats();  },  'cron.getJobDetails'(jobId) {    if (!this.userId) {      throw new Meteor.Error('not-authorized');    }        return cronJobStorage.getJobDetails(jobId);  },  'cron.getQueueStats'() {    if (!this.userId) {      throw new Meteor.Error('not-authorized');    }        return cronJobStorage.getQueueStats();  },  'cron.getSystemResources'() {    if (!this.userId) {      throw new Meteor.Error('not-authorized');    }        return cronJobStorage.getSystemResources();  },  'cron.pauseJob'(jobId) {    if (!this.userId) {      throw new Meteor.Error('not-authorized');    }        cronJobStorage.updateQueueStatus(jobId, 'paused');    cronJobStorage.saveJobStatus(jobId, { status: 'paused' });    return { success: true };  },  'cron.resumeJob'(jobId) {    if (!this.userId) {      throw new Meteor.Error('not-authorized');    }        cronJobStorage.updateQueueStatus(jobId, 'pending');    cronJobStorage.saveJobStatus(jobId, { status: 'pending' });    return { success: true };  },  'cron.stopJob'(jobId) {    if (!this.userId) {      throw new Meteor.Error('not-authorized');    }        cronJobStorage.updateQueueStatus(jobId, 'stopped');    cronJobStorage.saveJobStatus(jobId, {       status: 'stopped',      stoppedAt: new Date()    });    return { success: true };  },  'cron.cleanupOldJobs'(daysOld) {    if (!this.userId) {      throw new Meteor.Error('not-authorized');    }        return cronJobStorage.cleanupOldJobs(daysOld);  },  'cron.getBoardMigrationStats'() {    if (!this.userId) {      throw new Meteor.Error('not-authorized');    }        // Import the board migration detector    const { boardMigrationDetector } = require('./boardMigrationDetector');    return boardMigrationDetector.getMigrationStats();  },  'cron.forceBoardMigrationScan'() {    if (!this.userId) {      throw new Meteor.Error('not-authorized');    }        // Import the board migration detector    const { boardMigrationDetector } = require('./boardMigrationDetector');    return boardMigrationDetector.forceScan();  }});
 |