12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499 |
- /**
- * 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) {
- 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 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.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();
- }
- });
|