cronMigrationManager.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982
  1. /**
  2. * Cron Migration Manager
  3. * Manages database migrations as cron jobs using percolate:synced-cron
  4. */
  5. import { Meteor } from 'meteor/meteor';
  6. import { SyncedCron } from 'meteor/percolate:synced-cron';
  7. import { ReactiveVar } from 'meteor/reactive-var';
  8. // Server-side reactive variables for cron migration progress
  9. export const cronMigrationProgress = new ReactiveVar(0);
  10. export const cronMigrationStatus = new ReactiveVar('');
  11. export const cronMigrationCurrentStep = new ReactiveVar('');
  12. export const cronMigrationSteps = new ReactiveVar([]);
  13. export const cronIsMigrating = new ReactiveVar(false);
  14. export const cronJobs = new ReactiveVar([]);
  15. // Board-specific operation tracking
  16. export const boardOperations = new ReactiveVar(new Map());
  17. export const boardOperationProgress = new ReactiveVar(new Map());
  18. class CronMigrationManager {
  19. constructor() {
  20. this.migrationSteps = this.initializeMigrationSteps();
  21. this.currentStepIndex = 0;
  22. this.startTime = null;
  23. this.isRunning = false;
  24. }
  25. /**
  26. * Initialize migration steps as cron jobs
  27. */
  28. initializeMigrationSteps() {
  29. return [
  30. {
  31. id: 'board-background-color',
  32. name: 'Board Background Colors',
  33. description: 'Setting up board background colors',
  34. weight: 1,
  35. completed: false,
  36. progress: 0,
  37. cronName: 'migration_board_background_color',
  38. schedule: 'every 1 minute', // Will be changed to 'once' when triggered
  39. status: 'stopped'
  40. },
  41. {
  42. id: 'add-cardcounterlist-allowed',
  43. name: 'Card Counter List Settings',
  44. description: 'Adding card counter list permissions',
  45. weight: 1,
  46. completed: false,
  47. progress: 0,
  48. cronName: 'migration_card_counter_list',
  49. schedule: 'every 1 minute',
  50. status: 'stopped'
  51. },
  52. {
  53. id: 'add-boardmemberlist-allowed',
  54. name: 'Board Member List Settings',
  55. description: 'Adding board member list permissions',
  56. weight: 1,
  57. completed: false,
  58. progress: 0,
  59. cronName: 'migration_board_member_list',
  60. schedule: 'every 1 minute',
  61. status: 'stopped'
  62. },
  63. {
  64. id: 'lowercase-board-permission',
  65. name: 'Board Permission Standardization',
  66. description: 'Converting board permissions to lowercase',
  67. weight: 1,
  68. completed: false,
  69. progress: 0,
  70. cronName: 'migration_lowercase_permission',
  71. schedule: 'every 1 minute',
  72. status: 'stopped'
  73. },
  74. {
  75. id: 'change-attachments-type-for-non-images',
  76. name: 'Attachment Type Standardization',
  77. description: 'Updating attachment types for non-images',
  78. weight: 2,
  79. completed: false,
  80. progress: 0,
  81. cronName: 'migration_attachment_types',
  82. schedule: 'every 1 minute',
  83. status: 'stopped'
  84. },
  85. {
  86. id: 'card-covers',
  87. name: 'Card Covers System',
  88. description: 'Setting up card cover functionality',
  89. weight: 2,
  90. completed: false,
  91. progress: 0,
  92. cronName: 'migration_card_covers',
  93. schedule: 'every 1 minute',
  94. status: 'stopped'
  95. },
  96. {
  97. id: 'use-css-class-for-boards-colors',
  98. name: 'Board Color CSS Classes',
  99. description: 'Converting board colors to CSS classes',
  100. weight: 2,
  101. completed: false,
  102. progress: 0,
  103. cronName: 'migration_board_color_css',
  104. schedule: 'every 1 minute',
  105. status: 'stopped'
  106. },
  107. {
  108. id: 'denormalize-star-number-per-board',
  109. name: 'Board Star Counts',
  110. description: 'Calculating star counts per board',
  111. weight: 3,
  112. completed: false,
  113. progress: 0,
  114. cronName: 'migration_star_numbers',
  115. schedule: 'every 1 minute',
  116. status: 'stopped'
  117. },
  118. {
  119. id: 'add-member-isactive-field',
  120. name: 'Member Activity Status',
  121. description: 'Adding member activity tracking',
  122. weight: 2,
  123. completed: false,
  124. progress: 0,
  125. cronName: 'migration_member_activity',
  126. schedule: 'every 1 minute',
  127. status: 'stopped'
  128. },
  129. {
  130. id: 'add-sort-checklists',
  131. name: 'Checklist Sorting',
  132. description: 'Adding sort order to checklists',
  133. weight: 2,
  134. completed: false,
  135. progress: 0,
  136. cronName: 'migration_sort_checklists',
  137. schedule: 'every 1 minute',
  138. status: 'stopped'
  139. },
  140. {
  141. id: 'add-swimlanes',
  142. name: 'Swimlanes System',
  143. description: 'Setting up swimlanes functionality',
  144. weight: 4,
  145. completed: false,
  146. progress: 0,
  147. cronName: 'migration_swimlanes',
  148. schedule: 'every 1 minute',
  149. status: 'stopped'
  150. },
  151. {
  152. id: 'add-views',
  153. name: 'Board Views',
  154. description: 'Adding board view options',
  155. weight: 2,
  156. completed: false,
  157. progress: 0,
  158. cronName: 'migration_views',
  159. schedule: 'every 1 minute',
  160. status: 'stopped'
  161. },
  162. {
  163. id: 'add-checklist-items',
  164. name: 'Checklist Items',
  165. description: 'Setting up checklist items system',
  166. weight: 3,
  167. completed: false,
  168. progress: 0,
  169. cronName: 'migration_checklist_items',
  170. schedule: 'every 1 minute',
  171. status: 'stopped'
  172. },
  173. {
  174. id: 'add-card-types',
  175. name: 'Card Types',
  176. description: 'Adding card type functionality',
  177. weight: 2,
  178. completed: false,
  179. progress: 0,
  180. cronName: 'migration_card_types',
  181. schedule: 'every 1 minute',
  182. status: 'stopped'
  183. },
  184. {
  185. id: 'add-custom-fields-to-cards',
  186. name: 'Custom Fields',
  187. description: 'Adding custom fields to cards',
  188. weight: 3,
  189. completed: false,
  190. progress: 0,
  191. cronName: 'migration_custom_fields',
  192. schedule: 'every 1 minute',
  193. status: 'stopped'
  194. },
  195. {
  196. id: 'migrate-attachments-collectionFS-to-ostrioFiles',
  197. name: 'Migrate Attachments to Meteor-Files',
  198. description: 'Migrating attachments from CollectionFS to Meteor-Files',
  199. weight: 8,
  200. completed: false,
  201. progress: 0,
  202. cronName: 'migration_attachments_collectionfs',
  203. schedule: 'every 1 minute',
  204. status: 'stopped'
  205. },
  206. {
  207. id: 'migrate-avatars-collectionFS-to-ostrioFiles',
  208. name: 'Migrate Avatars to Meteor-Files',
  209. description: 'Migrating avatars from CollectionFS to Meteor-Files',
  210. weight: 6,
  211. completed: false,
  212. progress: 0,
  213. cronName: 'migration_avatars_collectionfs',
  214. schedule: 'every 1 minute',
  215. status: 'stopped'
  216. },
  217. {
  218. id: 'migrate-lists-to-per-swimlane',
  219. name: 'Migrate Lists to Per-Swimlane',
  220. description: 'Migrating lists to per-swimlane structure',
  221. weight: 5,
  222. completed: false,
  223. progress: 0,
  224. cronName: 'migration_lists_per_swimlane',
  225. schedule: 'every 1 minute',
  226. status: 'stopped'
  227. }
  228. ];
  229. }
  230. /**
  231. * Initialize all migration cron jobs
  232. */
  233. initializeCronJobs() {
  234. this.migrationSteps.forEach(step => {
  235. this.createCronJob(step);
  236. });
  237. // Update cron jobs list
  238. this.updateCronJobsList();
  239. }
  240. /**
  241. * Create a cron job for a migration step
  242. */
  243. createCronJob(step) {
  244. SyncedCron.add({
  245. name: step.cronName,
  246. schedule: (parser) => parser.text(step.schedule),
  247. job: () => {
  248. this.runMigrationStep(step);
  249. },
  250. });
  251. }
  252. /**
  253. * Run a migration step
  254. */
  255. async runMigrationStep(step) {
  256. try {
  257. console.log(`Starting migration: ${step.name}`);
  258. cronMigrationCurrentStep.set(step.name);
  259. cronMigrationStatus.set(`Running: ${step.description}`);
  260. cronIsMigrating.set(true);
  261. // Simulate migration progress
  262. const progressSteps = 10;
  263. for (let i = 0; i <= progressSteps; i++) {
  264. step.progress = (i / progressSteps) * 100;
  265. this.updateProgress();
  266. // Simulate work
  267. await new Promise(resolve => setTimeout(resolve, 100));
  268. }
  269. // Mark as completed
  270. step.completed = true;
  271. step.progress = 100;
  272. step.status = 'completed';
  273. console.log(`Completed migration: ${step.name}`);
  274. // Update progress
  275. this.updateProgress();
  276. } catch (error) {
  277. console.error(`Migration ${step.name} failed:`, error);
  278. step.status = 'error';
  279. cronMigrationStatus.set(`Migration failed: ${error.message}`);
  280. }
  281. }
  282. /**
  283. * Start all migrations in sequence
  284. */
  285. async startAllMigrations() {
  286. if (this.isRunning) {
  287. return;
  288. }
  289. this.isRunning = true;
  290. cronIsMigrating.set(true);
  291. cronMigrationStatus.set('Starting all migrations...');
  292. this.startTime = Date.now();
  293. try {
  294. for (let i = 0; i < this.migrationSteps.length; i++) {
  295. const step = this.migrationSteps[i];
  296. this.currentStepIndex = i;
  297. if (step.completed) {
  298. continue; // Skip already completed steps
  299. }
  300. // Start the cron job for this step
  301. await this.startCronJob(step.cronName);
  302. // Wait for completion
  303. await this.waitForCronJobCompletion(step);
  304. }
  305. // All migrations completed
  306. cronMigrationStatus.set('All migrations completed successfully!');
  307. cronMigrationProgress.set(100);
  308. cronMigrationCurrentStep.set('');
  309. // Clear status after delay
  310. setTimeout(() => {
  311. cronIsMigrating.set(false);
  312. cronMigrationStatus.set('');
  313. cronMigrationProgress.set(0);
  314. }, 3000);
  315. } catch (error) {
  316. console.error('Migration process failed:', error);
  317. cronMigrationStatus.set(`Migration process failed: ${error.message}`);
  318. cronIsMigrating.set(false);
  319. } finally {
  320. this.isRunning = false;
  321. }
  322. }
  323. /**
  324. * Start a specific cron job
  325. */
  326. async startCronJob(cronName) {
  327. // Change schedule to run once
  328. const job = SyncedCron.jobs.find(j => j.name === cronName);
  329. if (job) {
  330. job.schedule = 'once';
  331. SyncedCron.start();
  332. }
  333. }
  334. /**
  335. * Wait for a cron job to complete
  336. */
  337. async waitForCronJobCompletion(step) {
  338. return new Promise((resolve) => {
  339. const checkInterval = setInterval(() => {
  340. if (step.completed || step.status === 'error') {
  341. clearInterval(checkInterval);
  342. resolve();
  343. }
  344. }, 1000);
  345. });
  346. }
  347. /**
  348. * Stop a specific cron job
  349. */
  350. stopCronJob(cronName) {
  351. SyncedCron.remove(cronName);
  352. const step = this.migrationSteps.find(s => s.cronName === cronName);
  353. if (step) {
  354. step.status = 'stopped';
  355. }
  356. this.updateCronJobsList();
  357. }
  358. /**
  359. * Pause a specific cron job
  360. */
  361. pauseCronJob(cronName) {
  362. SyncedCron.pause(cronName);
  363. const step = this.migrationSteps.find(s => s.cronName === cronName);
  364. if (step) {
  365. step.status = 'paused';
  366. }
  367. this.updateCronJobsList();
  368. }
  369. /**
  370. * Resume a specific cron job
  371. */
  372. resumeCronJob(cronName) {
  373. SyncedCron.resume(cronName);
  374. const step = this.migrationSteps.find(s => s.cronName === cronName);
  375. if (step) {
  376. step.status = 'running';
  377. }
  378. this.updateCronJobsList();
  379. }
  380. /**
  381. * Remove a cron job
  382. */
  383. removeCronJob(cronName) {
  384. SyncedCron.remove(cronName);
  385. this.migrationSteps = this.migrationSteps.filter(s => s.cronName !== cronName);
  386. this.updateCronJobsList();
  387. }
  388. /**
  389. * Add a new cron job
  390. */
  391. addCronJob(jobData) {
  392. const step = {
  393. id: jobData.id || `custom_${Date.now()}`,
  394. name: jobData.name,
  395. description: jobData.description,
  396. weight: jobData.weight || 1,
  397. completed: false,
  398. progress: 0,
  399. cronName: jobData.cronName || `custom_${Date.now()}`,
  400. schedule: jobData.schedule || 'every 1 minute',
  401. status: 'stopped'
  402. };
  403. this.migrationSteps.push(step);
  404. this.createCronJob(step);
  405. this.updateCronJobsList();
  406. }
  407. /**
  408. * Update progress variables
  409. */
  410. updateProgress() {
  411. const totalWeight = this.migrationSteps.reduce((total, step) => total + step.weight, 0);
  412. const completedWeight = this.migrationSteps.reduce((total, step) => {
  413. return total + (step.completed ? step.weight : step.progress * step.weight / 100);
  414. }, 0);
  415. const progress = Math.round((completedWeight / totalWeight) * 100);
  416. cronMigrationProgress.set(progress);
  417. cronMigrationSteps.set([...this.migrationSteps]);
  418. }
  419. /**
  420. * Update cron jobs list
  421. */
  422. updateCronJobsList() {
  423. const jobs = SyncedCron.jobs.map(job => {
  424. const step = this.migrationSteps.find(s => s.cronName === job.name);
  425. return {
  426. name: job.name,
  427. schedule: job.schedule,
  428. status: step ? step.status : 'unknown',
  429. lastRun: job.lastRun,
  430. nextRun: job.nextRun,
  431. running: job.running
  432. };
  433. });
  434. cronJobs.set(jobs);
  435. }
  436. /**
  437. * Get all cron jobs
  438. */
  439. getAllCronJobs() {
  440. return cronJobs.get();
  441. }
  442. /**
  443. * Get migration steps
  444. */
  445. getMigrationSteps() {
  446. return this.migrationSteps;
  447. }
  448. /**
  449. * Start a long-running operation for a specific board
  450. */
  451. startBoardOperation(boardId, operationType, operationData) {
  452. const operationId = `${boardId}_${operationType}_${Date.now()}`;
  453. const operation = {
  454. id: operationId,
  455. boardId: boardId,
  456. type: operationType,
  457. data: operationData,
  458. status: 'running',
  459. progress: 0,
  460. startTime: new Date(),
  461. endTime: null,
  462. error: null
  463. };
  464. // Update board operations map
  465. const operations = boardOperations.get();
  466. operations.set(operationId, operation);
  467. boardOperations.set(operations);
  468. // Create cron job for this operation
  469. const cronName = `board_operation_${operationId}`;
  470. SyncedCron.add({
  471. name: cronName,
  472. schedule: (parser) => parser.text('once'),
  473. job: () => {
  474. this.executeBoardOperation(operationId, operationType, operationData);
  475. },
  476. });
  477. // Start the cron job
  478. SyncedCron.start();
  479. return operationId;
  480. }
  481. /**
  482. * Execute a board operation
  483. */
  484. async executeBoardOperation(operationId, operationType, operationData) {
  485. const operations = boardOperations.get();
  486. const operation = operations.get(operationId);
  487. if (!operation) {
  488. console.error(`Operation ${operationId} not found`);
  489. return;
  490. }
  491. try {
  492. console.log(`Starting board operation: ${operationType} for board ${operation.boardId}`);
  493. // Update operation status
  494. operation.status = 'running';
  495. operation.progress = 0;
  496. this.updateBoardOperation(operationId, operation);
  497. // Execute the specific operation
  498. switch (operationType) {
  499. case 'copy_board':
  500. await this.copyBoard(operationId, operationData);
  501. break;
  502. case 'move_board':
  503. await this.moveBoard(operationId, operationData);
  504. break;
  505. case 'copy_swimlane':
  506. await this.copySwimlane(operationId, operationData);
  507. break;
  508. case 'move_swimlane':
  509. await this.moveSwimlane(operationId, operationData);
  510. break;
  511. case 'copy_list':
  512. await this.copyList(operationId, operationData);
  513. break;
  514. case 'move_list':
  515. await this.moveList(operationId, operationData);
  516. break;
  517. case 'copy_card':
  518. await this.copyCard(operationId, operationData);
  519. break;
  520. case 'move_card':
  521. await this.moveCard(operationId, operationData);
  522. break;
  523. case 'copy_checklist':
  524. await this.copyChecklist(operationId, operationData);
  525. break;
  526. case 'move_checklist':
  527. await this.moveChecklist(operationId, operationData);
  528. break;
  529. default:
  530. throw new Error(`Unknown operation type: ${operationType}`);
  531. }
  532. // Mark as completed
  533. operation.status = 'completed';
  534. operation.progress = 100;
  535. operation.endTime = new Date();
  536. this.updateBoardOperation(operationId, operation);
  537. console.log(`Completed board operation: ${operationType} for board ${operation.boardId}`);
  538. } catch (error) {
  539. console.error(`Board operation ${operationType} failed:`, error);
  540. operation.status = 'error';
  541. operation.error = error.message;
  542. operation.endTime = new Date();
  543. this.updateBoardOperation(operationId, operation);
  544. }
  545. }
  546. /**
  547. * Update board operation progress
  548. */
  549. updateBoardOperation(operationId, operation) {
  550. const operations = boardOperations.get();
  551. operations.set(operationId, operation);
  552. boardOperations.set(operations);
  553. // Update progress map
  554. const progressMap = boardOperationProgress.get();
  555. progressMap.set(operationId, {
  556. progress: operation.progress,
  557. status: operation.status,
  558. error: operation.error
  559. });
  560. boardOperationProgress.set(progressMap);
  561. }
  562. /**
  563. * Copy board operation
  564. */
  565. async copyBoard(operationId, data) {
  566. const { sourceBoardId, targetBoardId, copyOptions } = data;
  567. const operation = boardOperations.get().get(operationId);
  568. // Simulate copy progress
  569. const steps = ['copying_swimlanes', 'copying_lists', 'copying_cards', 'copying_attachments', 'finalizing'];
  570. for (let i = 0; i < steps.length; i++) {
  571. operation.progress = Math.round(((i + 1) / steps.length) * 100);
  572. this.updateBoardOperation(operationId, operation);
  573. // Simulate work
  574. await new Promise(resolve => setTimeout(resolve, 1000));
  575. }
  576. }
  577. /**
  578. * Move board operation
  579. */
  580. async moveBoard(operationId, data) {
  581. const { sourceBoardId, targetBoardId, moveOptions } = data;
  582. const operation = boardOperations.get().get(operationId);
  583. // Simulate move progress
  584. const steps = ['preparing_move', 'moving_swimlanes', 'moving_lists', 'moving_cards', 'updating_references', 'finalizing'];
  585. for (let i = 0; i < steps.length; i++) {
  586. operation.progress = Math.round(((i + 1) / steps.length) * 100);
  587. this.updateBoardOperation(operationId, operation);
  588. // Simulate work
  589. await new Promise(resolve => setTimeout(resolve, 800));
  590. }
  591. }
  592. /**
  593. * Copy swimlane operation
  594. */
  595. async copySwimlane(operationId, data) {
  596. const { sourceSwimlaneId, targetBoardId, copyOptions } = data;
  597. const operation = boardOperations.get().get(operationId);
  598. // Simulate copy progress
  599. const steps = ['copying_swimlane', 'copying_lists', 'copying_cards', 'finalizing'];
  600. for (let i = 0; i < steps.length; i++) {
  601. operation.progress = Math.round(((i + 1) / steps.length) * 100);
  602. this.updateBoardOperation(operationId, operation);
  603. // Simulate work
  604. await new Promise(resolve => setTimeout(resolve, 500));
  605. }
  606. }
  607. /**
  608. * Move swimlane operation
  609. */
  610. async moveSwimlane(operationId, data) {
  611. const { sourceSwimlaneId, targetBoardId, moveOptions } = data;
  612. const operation = boardOperations.get().get(operationId);
  613. // Simulate move progress
  614. const steps = ['preparing_move', 'moving_swimlane', 'updating_references', 'finalizing'];
  615. for (let i = 0; i < steps.length; i++) {
  616. operation.progress = Math.round(((i + 1) / steps.length) * 100);
  617. this.updateBoardOperation(operationId, operation);
  618. // Simulate work
  619. await new Promise(resolve => setTimeout(resolve, 400));
  620. }
  621. }
  622. /**
  623. * Copy list operation
  624. */
  625. async copyList(operationId, data) {
  626. const { sourceListId, targetBoardId, copyOptions } = data;
  627. const operation = boardOperations.get().get(operationId);
  628. // Simulate copy progress
  629. const steps = ['copying_list', 'copying_cards', 'copying_attachments', 'finalizing'];
  630. for (let i = 0; i < steps.length; i++) {
  631. operation.progress = Math.round(((i + 1) / steps.length) * 100);
  632. this.updateBoardOperation(operationId, operation);
  633. // Simulate work
  634. await new Promise(resolve => setTimeout(resolve, 300));
  635. }
  636. }
  637. /**
  638. * Move list operation
  639. */
  640. async moveList(operationId, data) {
  641. const { sourceListId, targetBoardId, moveOptions } = data;
  642. const operation = boardOperations.get().get(operationId);
  643. // Simulate move progress
  644. const steps = ['preparing_move', 'moving_list', 'updating_references', 'finalizing'];
  645. for (let i = 0; i < steps.length; i++) {
  646. operation.progress = Math.round(((i + 1) / steps.length) * 100);
  647. this.updateBoardOperation(operationId, operation);
  648. // Simulate work
  649. await new Promise(resolve => setTimeout(resolve, 200));
  650. }
  651. }
  652. /**
  653. * Copy card operation
  654. */
  655. async copyCard(operationId, data) {
  656. const { sourceCardId, targetListId, copyOptions } = data;
  657. const operation = boardOperations.get().get(operationId);
  658. // Simulate copy progress
  659. const steps = ['copying_card', 'copying_attachments', 'copying_checklists', 'finalizing'];
  660. for (let i = 0; i < steps.length; i++) {
  661. operation.progress = Math.round(((i + 1) / steps.length) * 100);
  662. this.updateBoardOperation(operationId, operation);
  663. // Simulate work
  664. await new Promise(resolve => setTimeout(resolve, 150));
  665. }
  666. }
  667. /**
  668. * Move card operation
  669. */
  670. async moveCard(operationId, data) {
  671. const { sourceCardId, targetListId, moveOptions } = data;
  672. const operation = boardOperations.get().get(operationId);
  673. // Simulate move progress
  674. const steps = ['preparing_move', 'moving_card', 'updating_references', 'finalizing'];
  675. for (let i = 0; i < steps.length; i++) {
  676. operation.progress = Math.round(((i + 1) / steps.length) * 100);
  677. this.updateBoardOperation(operationId, operation);
  678. // Simulate work
  679. await new Promise(resolve => setTimeout(resolve, 100));
  680. }
  681. }
  682. /**
  683. * Copy checklist operation
  684. */
  685. async copyChecklist(operationId, data) {
  686. const { sourceChecklistId, targetCardId, copyOptions } = data;
  687. const operation = boardOperations.get().get(operationId);
  688. // Simulate copy progress
  689. const steps = ['copying_checklist', 'copying_items', 'finalizing'];
  690. for (let i = 0; i < steps.length; i++) {
  691. operation.progress = Math.round(((i + 1) / steps.length) * 100);
  692. this.updateBoardOperation(operationId, operation);
  693. // Simulate work
  694. await new Promise(resolve => setTimeout(resolve, 100));
  695. }
  696. }
  697. /**
  698. * Move checklist operation
  699. */
  700. async moveChecklist(operationId, data) {
  701. const { sourceChecklistId, targetCardId, moveOptions } = data;
  702. const operation = boardOperations.get().get(operationId);
  703. // Simulate move progress
  704. const steps = ['preparing_move', 'moving_checklist', 'finalizing'];
  705. for (let i = 0; i < steps.length; i++) {
  706. operation.progress = Math.round(((i + 1) / steps.length) * 100);
  707. this.updateBoardOperation(operationId, operation);
  708. // Simulate work
  709. await new Promise(resolve => setTimeout(resolve, 50));
  710. }
  711. }
  712. /**
  713. * Get board operations for a specific board
  714. */
  715. getBoardOperations(boardId) {
  716. const operations = boardOperations.get();
  717. const boardOps = [];
  718. for (const [operationId, operation] of operations) {
  719. if (operation.boardId === boardId) {
  720. boardOps.push(operation);
  721. }
  722. }
  723. return boardOps.sort((a, b) => b.startTime - a.startTime);
  724. }
  725. /**
  726. * Get all board operations with pagination
  727. */
  728. getAllBoardOperations(page = 1, limit = 20, searchTerm = '') {
  729. const operations = boardOperations.get();
  730. const allOps = Array.from(operations.values());
  731. // Filter by search term if provided
  732. let filteredOps = allOps;
  733. if (searchTerm) {
  734. filteredOps = allOps.filter(op =>
  735. op.boardId.toLowerCase().includes(searchTerm.toLowerCase()) ||
  736. op.type.toLowerCase().includes(searchTerm.toLowerCase())
  737. );
  738. }
  739. // Sort by start time (newest first)
  740. filteredOps.sort((a, b) => b.startTime - a.startTime);
  741. // Paginate
  742. const startIndex = (page - 1) * limit;
  743. const endIndex = startIndex + limit;
  744. const paginatedOps = filteredOps.slice(startIndex, endIndex);
  745. return {
  746. operations: paginatedOps,
  747. total: filteredOps.length,
  748. page: page,
  749. limit: limit,
  750. totalPages: Math.ceil(filteredOps.length / limit)
  751. };
  752. }
  753. /**
  754. * Get board operation statistics
  755. */
  756. getBoardOperationStats() {
  757. const operations = boardOperations.get();
  758. const stats = {
  759. total: operations.size,
  760. running: 0,
  761. completed: 0,
  762. error: 0,
  763. byType: {}
  764. };
  765. for (const [operationId, operation] of operations) {
  766. stats[operation.status]++;
  767. if (!stats.byType[operation.type]) {
  768. stats.byType[operation.type] = 0;
  769. }
  770. stats.byType[operation.type]++;
  771. }
  772. return stats;
  773. }
  774. }
  775. // Export singleton instance
  776. export const cronMigrationManager = new CronMigrationManager();
  777. // Initialize cron jobs on server start
  778. Meteor.startup(() => {
  779. cronMigrationManager.initializeCronJobs();
  780. });
  781. // Meteor methods for client-server communication
  782. Meteor.methods({
  783. 'cron.startAllMigrations'() {
  784. if (!this.userId) {
  785. throw new Meteor.Error('not-authorized');
  786. }
  787. return cronMigrationManager.startAllMigrations();
  788. },
  789. 'cron.startJob'(cronName) {
  790. if (!this.userId) {
  791. throw new Meteor.Error('not-authorized');
  792. }
  793. return cronMigrationManager.startCronJob(cronName);
  794. },
  795. 'cron.stopJob'(cronName) {
  796. if (!this.userId) {
  797. throw new Meteor.Error('not-authorized');
  798. }
  799. return cronMigrationManager.stopCronJob(cronName);
  800. },
  801. 'cron.pauseJob'(cronName) {
  802. if (!this.userId) {
  803. throw new Meteor.Error('not-authorized');
  804. }
  805. return cronMigrationManager.pauseCronJob(cronName);
  806. },
  807. 'cron.resumeJob'(cronName) {
  808. if (!this.userId) {
  809. throw new Meteor.Error('not-authorized');
  810. }
  811. return cronMigrationManager.resumeCronJob(cronName);
  812. },
  813. 'cron.removeJob'(cronName) {
  814. if (!this.userId) {
  815. throw new Meteor.Error('not-authorized');
  816. }
  817. return cronMigrationManager.removeCronJob(cronName);
  818. },
  819. 'cron.addJob'(jobData) {
  820. if (!this.userId) {
  821. throw new Meteor.Error('not-authorized');
  822. }
  823. return cronMigrationManager.addCronJob(jobData);
  824. },
  825. 'cron.getJobs'() {
  826. return cronMigrationManager.getAllCronJobs();
  827. },
  828. 'cron.getMigrationProgress'() {
  829. return {
  830. progress: cronMigrationProgress.get(),
  831. status: cronMigrationStatus.get(),
  832. currentStep: cronMigrationCurrentStep.get(),
  833. steps: cronMigrationSteps.get(),
  834. isMigrating: cronIsMigrating.get()
  835. };
  836. },
  837. 'cron.startBoardOperation'(boardId, operationType, operationData) {
  838. if (!this.userId) {
  839. throw new Meteor.Error('not-authorized');
  840. }
  841. return cronMigrationManager.startBoardOperation(boardId, operationType, operationData);
  842. },
  843. 'cron.getBoardOperations'(boardId) {
  844. if (!this.userId) {
  845. throw new Meteor.Error('not-authorized');
  846. }
  847. return cronMigrationManager.getBoardOperations(boardId);
  848. },
  849. 'cron.getAllBoardOperations'(page, limit, searchTerm) {
  850. if (!this.userId) {
  851. throw new Meteor.Error('not-authorized');
  852. }
  853. return cronMigrationManager.getAllBoardOperations(page, limit, searchTerm);
  854. },
  855. 'cron.getBoardOperationStats'() {
  856. if (!this.userId) {
  857. throw new Meteor.Error('not-authorized');
  858. }
  859. return cronMigrationManager.getBoardOperationStats();
  860. }
  861. });