123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554 |
- /**
- * 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 progress
- export 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 tracking
- export 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 and version
- const updateQuery = {
- 'migrationMarkers.fullMigrationCompleted': true,
- 'migrationMarkers.lastMigration': new Date(),
- 'migrationMarkers.migrationType': migrationType,
- 'migrationVersion': 1 // Set migration version to prevent re-migration
- };
- // 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;
- }
- /**
- * Clear all cron jobs and restart migration system
- */
- clearAllCronJobs() {
- try {
- // Stop all existing cron jobs
- if (SyncedCron && SyncedCron.jobs) {
- SyncedCron.jobs.forEach(job => {
- try {
- SyncedCron.remove(job.name);
- } catch (error) {
- console.warn(`Failed to remove cron job ${job.name}:`, error.message);
- }
- });
- }
- // Clear job storage
- cronJobStorage.clearAllJobs();
- // Reset migration steps
- this.migrationSteps = this.initializeMigrationSteps();
- this.currentStepIndex = 0;
- this.isRunning = false;
- // Restart the migration system
- this.initialize();
- console.log('All cron jobs cleared and migration system restarted');
- return { success: true, message: 'All cron jobs cleared and migration system restarted' };
- } catch (error) {
- console.error('Error clearing cron jobs:', error);
- return { success: false, error: error.message };
- }
- }
- }
- // Export singleton instance
- export const cronMigrationManager = new CronMigrationManager();
- // Initialize cron jobs on server start
- Meteor.startup(() => {
- cronMigrationManager.initializeCronJobs();
- });
- // Meteor methods for client-server communication
- Meteor.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.clearAllJobs'() {
- if (!this.userId) {
- throw new Meteor.Error('not-authorized');
- }
-
- return cronMigrationManager.clearAllCronJobs();
- },
- '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();
- },
- });
|