| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510 | /** * 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) {    if (!jobData) {      throw new Error('Job data is required for migration execution');    }        const { stepId } = jobData;    if (!stepId) {      throw new Error('Step ID is required in job data');    }        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, stepId);      // 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, stepId) {    const { name, duration } = stepData;        // Simulate step execution with progress updates for other migrations    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();  },});
 |