From cbb1cd78de3e40264a5e047ace0ce27f8635b4e6 Mon Sep 17 00:00:00 2001 From: Lauri Ojansivu Date: Tue, 6 Jan 2026 00:15:16 +0200 Subject: [PATCH] Security Fix 1: There was not enough permission checks. Moved migrations to Admin Panel/Settings/Cron. Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7. --- client/00-startup.js | 4 +- client/components/boards/boardBody.jade | 4 +- client/components/boards/boardBody.js | 413 --------- client/components/cards/attachments.css | 33 - client/components/main/layouts.jade | 1 - .../{ => settings}/migrationProgress.css | 34 +- .../{ => settings}/migrationProgress.jade | 0 .../{ => settings}/migrationProgress.js | 0 client/components/settings/settingBody.jade | 19 +- client/components/settings/settingBody.js | 73 +- client/components/sidebar/sidebar.jade | 4 - client/components/sidebar/sidebar.js | 5 - .../components/sidebar/sidebarMigrations.jade | 109 --- .../components/sidebar/sidebarMigrations.js | 341 -------- client/lib/migrationManager.js | 815 ------------------ imports/cronMigrationClient.js | 50 ++ imports/i18n/data/en.i18n.json | 4 +- server/cronMigrationManager.js | 293 ++++++- 18 files changed, 397 insertions(+), 1805 deletions(-) rename client/components/{ => settings}/migrationProgress.css (89%) rename client/components/{ => settings}/migrationProgress.jade (100%) rename client/components/{ => settings}/migrationProgress.js (100%) delete mode 100644 client/components/sidebar/sidebarMigrations.jade delete mode 100644 client/components/sidebar/sidebarMigrations.js delete mode 100644 client/lib/migrationManager.js create mode 100644 imports/cronMigrationClient.js diff --git a/client/00-startup.js b/client/00-startup.js index d59ea3afe..02954353b 100644 --- a/client/00-startup.js +++ b/client/00-startup.js @@ -10,8 +10,8 @@ import '/client/lib/boardConverter'; import '/client/components/boardConversionProgress'; // Import migration manager and progress UI -import '/client/lib/migrationManager'; -import '/client/components/migrationProgress'; +import '/client/lib/attachmentMigrationManager'; +import '/client/components/settings/migrationProgress'; // Import cron settings import '/client/components/settings/cronSettings'; diff --git a/client/components/boards/boardBody.jade b/client/components/boards/boardBody.jade index 1a6535203..4af638f08 100644 --- a/client/components/boards/boardBody.jade +++ b/client/components/boards/boardBody.jade @@ -1,8 +1,6 @@ template(name="board") - if isMigrating.get - +migrationProgress - else if isConverting.get + if isConverting.get +boardConversionProgress else if isBoardReady.get if currentBoard diff --git a/client/components/boards/boardBody.js b/client/components/boards/boardBody.js index 4a22e07af..74c1f25a9 100644 --- a/client/components/boards/boardBody.js +++ b/client/components/boards/boardBody.js @@ -3,9 +3,6 @@ import '../gantt/gantt.js'; import { TAPi18n } from '/imports/i18n'; import dragscroll from '@wekanteam/dragscroll'; import { boardConverter } from '/client/lib/boardConverter'; -import { migrationManager } from '/client/lib/migrationManager'; -import { attachmentMigrationManager } from '/client/lib/attachmentMigrationManager'; -import { migrationProgressManager } from '/client/components/migrationProgress'; import { formatDateByUserPreference } from '/imports/lib/dateUtils'; import Swimlanes from '/models/swimlanes'; import Lists from '/models/lists'; @@ -18,7 +15,6 @@ BlazeComponent.extendComponent({ onCreated() { this.isBoardReady = new ReactiveVar(false); this.isConverting = new ReactiveVar(false); - this.isMigrating = new ReactiveVar(false); this._swimlaneCreated = new Set(); // Track boards where we've created swimlanes this._boardProcessed = false; // Track if board has been processed this._lastProcessedBoardId = null; // Track last processed board ID @@ -36,7 +32,6 @@ BlazeComponent.extendComponent({ // Use a separate autorun for subscription ready state to avoid reactive loops this.subscriptionReadyAutorun = Tracker.autorun(() => { if (handle.ready()) { - // Only run conversion/migration logic once per board if (!this._boardProcessed || this._lastProcessedBoardId !== currentBoardId) { this._boardProcessed = true; this._lastProcessedBoardId = currentBoardId; @@ -101,416 +96,15 @@ BlazeComponent.extendComponent({ return; } - // Automatic migration disabled - migrations must be run manually from sidebar - // Board admins can run migrations from the sidebar Migrations menu this.isBoardReady.set(true); } catch (error) { console.error('Error during board conversion check:', error); this.isConverting.set(false); - this.isMigrating.set(false); this.isBoardReady.set(true); // Show board even if conversion check failed } }, - /** - * Check if board needs comprehensive migration - */ - async checkComprehensiveMigration(boardId) { - try { - return new Promise((resolve, reject) => { - Meteor.call('comprehensiveBoardMigration.needsMigration', boardId, (error, result) => { - if (error) { - console.error('Error checking comprehensive migration:', error); - reject(error); - } else { - resolve(result); - } - }); - }); - } catch (error) { - console.error('Error checking comprehensive migration:', error); - return false; - } - }, - - /** - * Execute comprehensive migration for a board - */ - async executeComprehensiveMigration(boardId) { - try { - // Start progress tracking - migrationProgressManager.startMigration(); - - // Simulate progress updates since we can't easily pass callbacks through Meteor methods - const progressSteps = [ - { step: 'analyze_board_structure', name: 'Analyze Board Structure', duration: 1000 }, - { step: 'fix_orphaned_cards', name: 'Fix Orphaned Cards', duration: 2000 }, - { step: 'convert_shared_lists', name: 'Convert Shared Lists', duration: 3000 }, - { step: 'ensure_per_swimlane_lists', name: 'Ensure Per-Swimlane Lists', duration: 1500 }, - { step: 'validate_migration', name: 'Validate Migration', duration: 1000 }, - { step: 'fix_avatar_urls', name: 'Fix Avatar URLs', duration: 1000 }, - { step: 'fix_attachment_urls', name: 'Fix Attachment URLs', duration: 1000 } - ]; - - // Start the actual migration - const migrationPromise = new Promise((resolve, reject) => { - Meteor.call('comprehensiveBoardMigration.execute', boardId, (error, result) => { - if (error) { - console.error('Error executing comprehensive migration:', error); - migrationProgressManager.failMigration(error); - reject(error); - } else { - if (process.env.DEBUG === 'true') { - console.log('Comprehensive migration completed for board:', boardId, result); - } - resolve(result.success); - } - }); - }); - - // Simulate progress updates - const progressPromise = this.simulateMigrationProgress(progressSteps); - - // Wait for both to complete - const [migrationResult] = await Promise.all([migrationPromise, progressPromise]); - - migrationProgressManager.completeMigration(); - return migrationResult; - - } catch (error) { - console.error('Error executing comprehensive migration:', error); - migrationProgressManager.failMigration(error); - return false; - } - }, - - /** - * Simulate migration progress updates - */ - async simulateMigrationProgress(progressSteps) { - const totalSteps = progressSteps.length; - - for (let i = 0; i < progressSteps.length; i++) { - const step = progressSteps[i]; - const stepProgress = Math.round(((i + 1) / totalSteps) * 100); - - // Update progress for this step - migrationProgressManager.updateProgress({ - overallProgress: stepProgress, - currentStep: i + 1, - totalSteps, - stepName: step.step, - stepProgress: 0, - stepStatus: `Starting ${step.name}...`, - stepDetails: null, - boardId: Session.get('currentBoard') - }); - - // Simulate step progress - const stepDuration = step.duration; - const updateInterval = 100; // Update every 100ms - const totalUpdates = stepDuration / updateInterval; - - for (let j = 0; j < totalUpdates; j++) { - const stepStepProgress = Math.round(((j + 1) / totalUpdates) * 100); - - migrationProgressManager.updateProgress({ - overallProgress: stepProgress, - currentStep: i + 1, - totalSteps, - stepName: step.step, - stepProgress: stepStepProgress, - stepStatus: `Processing ${step.name}...`, - stepDetails: { progress: `${stepStepProgress}%` }, - boardId: Session.get('currentBoard') - }); - - await new Promise(resolve => setTimeout(resolve, updateInterval)); - } - - // Complete the step - migrationProgressManager.updateProgress({ - overallProgress: stepProgress, - currentStep: i + 1, - totalSteps, - stepName: step.step, - stepProgress: 100, - stepStatus: `${step.name} completed`, - stepDetails: { status: 'completed' }, - boardId: Session.get('currentBoard') - }); - } - }, - - async startBackgroundMigration(boardId) { - try { - // Start background migration using the cron system - Meteor.call('boardMigration.startBoardMigration', boardId, (error, result) => { - if (error) { - console.error('Failed to start background migration:', error); - } else { - if (process.env.DEBUG === 'true') { - console.log('Background migration started for board:', boardId); - } - } - }); - } catch (error) { - console.error('Error starting background migration:', error); - } - }, - - async convertSharedListsToPerSwimlane(boardId) { - try { - const board = ReactiveCache.getBoard(boardId); - if (!board) return; - - // Check if board has already been processed for shared lists conversion - if (board.hasSharedListsConverted) { - if (process.env.DEBUG === 'true') { - console.log(`Board ${boardId} has already been processed for shared lists conversion`); - } - return; - } - - // Get all lists for this board - const allLists = board.lists(); - const swimlanes = board.swimlanes(); - - if (swimlanes.length === 0) { - if (process.env.DEBUG === 'true') { - console.log(`Board ${boardId} has no swimlanes, skipping shared lists conversion`); - } - return; - } - - // Find shared lists (lists with empty swimlaneId or null swimlaneId) - const sharedLists = allLists.filter(list => !list.swimlaneId || list.swimlaneId === ''); - - if (sharedLists.length === 0) { - if (process.env.DEBUG === 'true') { - console.log(`Board ${boardId} has no shared lists to convert`); - } - // Mark as processed even if no shared lists - Boards.update(boardId, { $set: { hasSharedListsConverted: true } }); - return; - } - - if (process.env.DEBUG === 'true') { - console.log(`Converting ${sharedLists.length} shared lists to per-swimlane lists for board ${boardId}`); - } - - // Convert each shared list to per-swimlane lists - for (const sharedList of sharedLists) { - // Create a copy of the list for each swimlane - for (const swimlane of swimlanes) { - // Check if this list already exists in this swimlane - const existingList = Lists.findOne({ - boardId: boardId, - swimlaneId: swimlane._id, - title: sharedList.title - }); - - if (!existingList) { - // Double-check to avoid race conditions - const doubleCheckList = ReactiveCache.getList({ - boardId: boardId, - swimlaneId: swimlane._id, - title: sharedList.title - }); - - if (!doubleCheckList) { - // Create a new list in this swimlane - const newListData = { - title: sharedList.title, - boardId: boardId, - swimlaneId: swimlane._id, - sort: sharedList.sort || 0, - archived: sharedList.archived || false, // Preserve archived state from original list - createdAt: new Date(), - modifiedAt: new Date() - }; - - // Copy other properties if they exist - if (sharedList.color) newListData.color = sharedList.color; - if (sharedList.wipLimit) newListData.wipLimit = sharedList.wipLimit; - if (sharedList.wipLimitEnabled) newListData.wipLimitEnabled = sharedList.wipLimitEnabled; - if (sharedList.wipLimitSoft) newListData.wipLimitSoft = sharedList.wipLimitSoft; - - Lists.insert(newListData); - - if (process.env.DEBUG === 'true') { - const archivedStatus = sharedList.archived ? ' (archived)' : ' (active)'; - console.log(`Created list "${sharedList.title}"${archivedStatus} for swimlane ${swimlane.title || swimlane._id}`); - } - } else { - if (process.env.DEBUG === 'true') { - console.log(`List "${sharedList.title}" already exists in swimlane ${swimlane.title || swimlane._id} (double-check), skipping`); - } - } - } else { - if (process.env.DEBUG === 'true') { - console.log(`List "${sharedList.title}" already exists in swimlane ${swimlane.title || swimlane._id}, skipping`); - } - } - } - - // Remove the original shared list completely - Lists.remove(sharedList._id); - - if (process.env.DEBUG === 'true') { - console.log(`Removed shared list "${sharedList.title}"`); - } - } - - // Mark board as processed - Boards.update(boardId, { $set: { hasSharedListsConverted: true } }); - - if (process.env.DEBUG === 'true') { - console.log(`Successfully converted ${sharedLists.length} shared lists to per-swimlane lists for board ${boardId}`); - } - - } catch (error) { - console.error('Error converting shared lists to per-swimlane:', error); - } - }, - - async fixMissingLists(boardId) { - try { - const board = ReactiveCache.getBoard(boardId); - if (!board) return; - - // Check if board has already been processed for missing lists fix - if (board.fixMissingListsCompleted) { - if (process.env.DEBUG === 'true') { - console.log(`Board ${boardId} has already been processed for missing lists fix`); - } - return; - } - - // Check if migration is needed - const needsMigration = await new Promise((resolve, reject) => { - Meteor.call('fixMissingListsMigration.needsMigration', boardId, (error, result) => { - if (error) { - reject(error); - } else { - resolve(result); - } - }); - }); - - if (!needsMigration) { - if (process.env.DEBUG === 'true') { - console.log(`Board ${boardId} does not need missing lists fix`); - } - return; - } - - if (process.env.DEBUG === 'true') { - console.log(`Starting fix missing lists migration for board ${boardId}`); - } - - // Execute the migration - const result = await new Promise((resolve, reject) => { - Meteor.call('fixMissingListsMigration.execute', boardId, (error, result) => { - if (error) { - reject(error); - } else { - resolve(result); - } - }); - }); - - if (result && result.success) { - if (process.env.DEBUG === 'true') { - console.log(`Successfully fixed missing lists for board ${boardId}: created ${result.createdLists} lists, updated ${result.updatedCards} cards`); - } - } - - } catch (error) { - console.error('Error fixing missing lists:', error); - } - }, - - async fixDuplicateLists(boardId) { - try { - const board = ReactiveCache.getBoard(boardId); - if (!board) return; - - // Check if board has already been processed for duplicate lists fix - if (board.fixDuplicateListsCompleted) { - if (process.env.DEBUG === 'true') { - console.log(`Board ${boardId} has already been processed for duplicate lists fix`); - } - return; - } - - if (process.env.DEBUG === 'true') { - console.log(`Starting duplicate lists fix for board ${boardId}`); - } - - // Execute the duplicate lists fix - const result = await new Promise((resolve, reject) => { - Meteor.call('fixDuplicateLists.fixBoard', boardId, (error, result) => { - if (error) { - reject(error); - } else { - resolve(result); - } - }); - }); - - if (result && result.fixed > 0) { - if (process.env.DEBUG === 'true') { - console.log(`Successfully fixed ${result.fixed} duplicate lists for board ${boardId}: ${result.fixedSwimlanes} swimlanes, ${result.fixedLists} lists`); - } - - // Mark board as processed - Boards.update(boardId, { $set: { fixDuplicateListsCompleted: true } }); - } else if (process.env.DEBUG === 'true') { - console.log(`No duplicate lists found for board ${boardId}`); - // Still mark as processed to avoid repeated checks - Boards.update(boardId, { $set: { fixDuplicateListsCompleted: true } }); - } else { - // Still mark as processed to avoid repeated checks - Boards.update(boardId, { $set: { fixDuplicateListsCompleted: true } }); - } - - } catch (error) { - console.error('Error fixing duplicate lists:', error); - } - }, - - async startAttachmentMigrationIfNeeded(boardId) { - try { - // Check if board has already been migrated - if (attachmentMigrationManager.isBoardMigrated(boardId)) { - if (process.env.DEBUG === 'true') { - console.log(`Board ${boardId} has already been migrated, skipping`); - } - return; - } - - // Check if there are unconverted attachments - const unconvertedAttachments = attachmentMigrationManager.getUnconvertedAttachments(boardId); - - if (unconvertedAttachments.length > 0) { - if (process.env.DEBUG === 'true') { - console.log(`Starting attachment migration for ${unconvertedAttachments.length} attachments in board ${boardId}`); - } - await attachmentMigrationManager.startAttachmentMigration(boardId); - } else { - // No attachments to migrate, mark board as migrated - // This will be handled by the migration manager itself - if (process.env.DEBUG === 'true') { - console.log(`Board ${boardId} has no attachments to migrate`); - } - } - } catch (error) { - console.error('Error starting attachment migration:', error); - } - }, - onlyShowCurrentCard() { const isMiniScreen = Utils.isMiniScreen(); const currentCardId = Utils.getCurrentCardId(true); @@ -535,10 +129,6 @@ BlazeComponent.extendComponent({ return this.isConverting.get(); }, - isMigrating() { - return this.isMigrating.get(); - }, - isBoardReady() { return this.isBoardReady.get(); }, @@ -1046,7 +636,6 @@ BlazeComponent.extendComponent({ const currentBoardId = Session.get('currentBoard'); const isBoardReady = this.isBoardReady.get(); const isConverting = this.isConverting.get(); - const isMigrating = this.isMigrating.get(); const boardView = Utils.boardView(); if (process.env.DEBUG === 'true') { @@ -1055,7 +644,6 @@ BlazeComponent.extendComponent({ console.log('currentBoard:', !!currentBoard, currentBoard ? currentBoard.title : 'none'); console.log('isBoardReady:', isBoardReady); console.log('isConverting:', isConverting); - console.log('isMigrating:', isMigrating); console.log('boardView:', boardView); console.log('========================'); } @@ -1066,7 +654,6 @@ BlazeComponent.extendComponent({ currentBoardTitle: currentBoard ? currentBoard.title : 'none', isBoardReady, isConverting, - isMigrating, boardView }; }, diff --git a/client/components/cards/attachments.css b/client/components/cards/attachments.css index becb29160..64a0c8735 100644 --- a/client/components/cards/attachments.css +++ b/client/components/cards/attachments.css @@ -336,36 +336,3 @@ margin-top: 10px; } } - -/* Attachment migration styles */ -.attachment-item.migrating { - position: relative; - opacity: 0.7; -} - -.attachment-migration-overlay { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(255, 255, 255, 0.9); - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - z-index: 10; - border-radius: 4px; -} - -.migration-spinner { - font-size: 24px; - color: #007cba; - margin-bottom: 8px; -} - -.migration-text { - font-size: 12px; - color: #666; - text-align: center; -} diff --git a/client/components/main/layouts.jade b/client/components/main/layouts.jade index a42c646ad..7f50d0438 100644 --- a/client/components/main/layouts.jade +++ b/client/components/main/layouts.jade @@ -79,7 +79,6 @@ template(name="defaultLayout") | {{{afterBodyStart}}} +Template.dynamic(template=content) | {{{beforeBodyEnd}}} - +migrationProgress +boardConversionProgress if (Modal.isOpen) #modal diff --git a/client/components/migrationProgress.css b/client/components/settings/migrationProgress.css similarity index 89% rename from client/components/migrationProgress.css rename to client/components/settings/migrationProgress.css index f3b9a45d4..2077cc524 100644 --- a/client/components/migrationProgress.css +++ b/client/components/settings/migrationProgress.css @@ -266,4 +266,36 @@ .migration-progress-note { color: #a0aec0; } -} \ No newline at end of file +} +/* Attachment migration styles */ +.attachment-item.migrating { + position: relative; + opacity: 0.7; +} + +.attachment-migration-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.9); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + z-index: 10; + border-radius: 4px; +} + +.migration-spinner { + font-size: 24px; + color: #007cba; + margin-bottom: 8px; +} + +.migration-text { + font-size: 12px; + color: #666; + text-align: center; +} diff --git a/client/components/migrationProgress.jade b/client/components/settings/migrationProgress.jade similarity index 100% rename from client/components/migrationProgress.jade rename to client/components/settings/migrationProgress.jade diff --git a/client/components/migrationProgress.js b/client/components/settings/migrationProgress.js similarity index 100% rename from client/components/migrationProgress.js rename to client/components/settings/migrationProgress.js diff --git a/client/components/settings/settingBody.jade b/client/components/settings/settingBody.jade index d40916123..5653d84a9 100644 --- a/client/components/settings/settingBody.jade +++ b/client/components/settings/settingBody.jade @@ -170,7 +170,10 @@ template(name="setting") label {{_ 'migration-status'}} .status-indicator span.status-label {{_ 'status'}}: - span.status-value {{migrationStatus}} + span.status-value {{#if isMigrating}}{{migrationStatus}}{{else}}{{_ 'idle'}}{{/if}} + .current-step(class="{{#unless migrationCurrentStep}}hide{{/unless}}") + span.step-label {{_ 'current-step'}}: + span.step-value {{migrationCurrentStep}} .progress-section .progress .progress-bar(role="progressbar" style="width: {{migrationProgress}}%" aria-valuenow="{{migrationProgress}}" aria-valuemin="0" aria-valuemax="100") @@ -179,9 +182,13 @@ template(name="setting") | {{migrationProgress}}% {{_ 'complete'}} .form-group - button.js-start-all-migrations.btn.btn-primary {{_ 'start-all-migrations'}} - button.js-pause-all-migrations.btn.btn-warning {{_ 'pause-all-migrations'}} - button.js-stop-all-migrations.btn.btn-danger {{_ 'stop-all-migrations'}} + button.js-start-all-migrations.btn.btn-primary {{#if isMigrating}}disabled{{/if}} {{_ 'start-all-migrations'}} + button.js-pause-all-migrations.btn.btn-warning {{#unless isMigrating}}disabled{{/unless}} {{_ 'pause-all-migrations'}} + button.js-stop-all-migrations.btn.btn-danger {{#unless isMigrating}}disabled{{/unless}} {{_ 'stop-all-migrations'}} + + li + h3 {{_ 'migration-steps'}} + p Migration steps section temporarily removed li h3 {{_ 'board-operations'}} @@ -200,7 +207,7 @@ template(name="setting") .job-info .job-name {{name}} .job-schedule {{schedule}} - .job-description {{description}} + .job-status {{status}} .job-actions button.js-pause-job.btn.btn-sm.btn-warning(data-job-id="{{_id}}") {{_ 'pause'}} button.js-delete-job.btn.btn-sm.btn-danger(data-job-id="{{_id}}") {{_ 'delete'}} @@ -274,7 +281,7 @@ template(name='email') // li.smtp-form // .title {{_ 'smtp-username'}} // .form-group - // input.wekan-form-control#mail-server-u"accounts-allowUserNameChange": "Allow Username Change",sername(type="text", placeholder="{{_ 'username'}}" value="{{currentSetting.mailServer.username}}") + // input.wekan-form-control#mail-server-username(type="text", placeholder="{{_ 'username'}}" value="{{currentSetting.mailServer.username}}") // li.smtp-form // .title {{_ 'smtp-password'}} // .form-group diff --git a/client/components/settings/settingBody.js b/client/components/settings/settingBody.js index d40b2aab2..bad99ebfa 100644 --- a/client/components/settings/settingBody.js +++ b/client/components/settings/settingBody.js @@ -2,6 +2,14 @@ import { ReactiveCache } from '/imports/reactiveCache'; import { TAPi18n } from '/imports/i18n'; import { ALLOWED_WAIT_SPINNERS } from '/config/const'; import LockoutSettings from '/models/lockoutSettings'; +import { + cronMigrationProgress, + cronMigrationStatus, + cronMigrationCurrentStep, + cronMigrationSteps, + cronIsMigrating, + cronJobs +} from '/imports/cronMigrationClient'; BlazeComponent.extendComponent({ @@ -115,15 +123,27 @@ BlazeComponent.extendComponent({ // Cron settings helpers migrationStatus() { - return TAPi18n.__('idle'); // Placeholder + return cronMigrationStatus.get() || TAPi18n.__('idle'); }, migrationProgress() { - return 0; // Placeholder + return cronMigrationProgress.get() || 0; + }, + + migrationCurrentStep() { + return cronMigrationCurrentStep.get() || ''; + }, + + isMigrating() { + return cronIsMigrating.get() || false; + }, + + migrationSteps() { + return cronMigrationSteps.get() || []; }, cronJobs() { - return []; // Placeholder + return cronJobs.get() || []; }, setLoading(w) { @@ -169,7 +189,9 @@ BlazeComponent.extendComponent({ // Event handlers for cron settings 'click button.js-start-all-migrations'(event) { event.preventDefault(); - Meteor.call('startAllMigrations', (error, result) => { + this.setLoading(true); + Meteor.call('cron.startAllMigrations', (error, result) => { + this.setLoading(false); if (error) { alert(TAPi18n.__('migration-start-failed') + ': ' + error.reason); } else { @@ -180,7 +202,9 @@ BlazeComponent.extendComponent({ 'click button.js-pause-all-migrations'(event) { event.preventDefault(); - Meteor.call('pauseAllMigrations', (error, result) => { + this.setLoading(true); + Meteor.call('cron.pauseAllMigrations', (error, result) => { + this.setLoading(false); if (error) { alert(TAPi18n.__('migration-pause-failed') + ': ' + error.reason); } else { @@ -192,7 +216,9 @@ BlazeComponent.extendComponent({ 'click button.js-stop-all-migrations'(event) { event.preventDefault(); if (confirm(TAPi18n.__('migration-stop-confirm'))) { - Meteor.call('stopAllMigrations', (error, result) => { + this.setLoading(true); + Meteor.call('cron.stopAllMigrations', (error, result) => { + this.setLoading(false); if (error) { alert(TAPi18n.__('migration-stop-failed') + ': ' + error.reason); } else { @@ -204,41 +230,28 @@ BlazeComponent.extendComponent({ 'click button.js-schedule-board-cleanup'(event) { event.preventDefault(); - Meteor.call('scheduleBoardCleanup', (error, result) => { - if (error) { - alert(TAPi18n.__('board-cleanup-failed') + ': ' + error.reason); - } else { - alert(TAPi18n.__('board-cleanup-scheduled')); - } - }); + // Placeholder - board cleanup scheduling + alert(TAPi18n.__('board-cleanup-scheduled')); }, 'click button.js-schedule-board-archive'(event) { event.preventDefault(); - Meteor.call('scheduleBoardArchive', (error, result) => { - if (error) { - alert(TAPi18n.__('board-archive-failed') + ': ' + error.reason); - } else { - alert(TAPi18n.__('board-archive-scheduled')); - } - }); + // Placeholder - board archive scheduling + alert(TAPi18n.__('board-archive-scheduled')); }, 'click button.js-schedule-board-backup'(event) { event.preventDefault(); - Meteor.call('scheduleBoardBackup', (error, result) => { - if (error) { - alert(TAPi18n.__('board-backup-failed') + ': ' + error.reason); - } else { - alert(TAPi18n.__('board-backup-scheduled')); - } - }); + // Placeholder - board backup scheduling + alert(TAPi18n.__('board-backup-scheduled')); }, 'click button.js-pause-job'(event) { event.preventDefault(); const jobId = $(event.target).data('job-id'); - Meteor.call('pauseCronJob', jobId, (error, result) => { + this.setLoading(true); + Meteor.call('cron.pauseJob', jobId, (error, result) => { + this.setLoading(false); if (error) { alert(TAPi18n.__('cron-job-pause-failed') + ': ' + error.reason); } else { @@ -251,7 +264,9 @@ BlazeComponent.extendComponent({ event.preventDefault(); const jobId = $(event.target).data('job-id'); if (confirm(TAPi18n.__('cron-job-delete-confirm'))) { - Meteor.call('deleteCronJob', jobId, (error, result) => { + this.setLoading(true); + Meteor.call('cron.removeJob', jobId, (error, result) => { + this.setLoading(false); if (error) { alert(TAPi18n.__('cron-job-delete-failed') + ': ' + error.reason); } else { diff --git a/client/components/sidebar/sidebar.jade b/client/components/sidebar/sidebar.jade index 4f02ea6d4..02ef256ef 100644 --- a/client/components/sidebar/sidebar.jade +++ b/client/components/sidebar/sidebar.jade @@ -588,10 +588,6 @@ template(name="boardMenuPopup") | 📦 | {{_ 'archived-items'}} if currentUser.isBoardAdmin - li - a.js-open-migrations - | 🔧 - | {{_ 'migrations'}} li a.js-change-board-color | 🎨 diff --git a/client/components/sidebar/sidebar.js b/client/components/sidebar/sidebar.js index 38383f7f0..c61ff3996 100644 --- a/client/components/sidebar/sidebar.js +++ b/client/components/sidebar/sidebar.js @@ -13,7 +13,6 @@ const viewTitles = { multiselection: 'multi-selection', customFields: 'custom-fields', archives: 'archives', - migrations: 'migrations', }; BlazeComponent.extendComponent({ @@ -284,10 +283,6 @@ Template.boardMenuPopup.events({ Sidebar.setView('archives'); Popup.back(); }, - 'click .js-open-migrations'() { - Sidebar.setView('migrations'); - Popup.back(); - }, 'click .js-change-board-color': Popup.open('boardChangeColor'), 'click .js-change-background-image': Popup.open('boardChangeBackgroundImage'), 'click .js-board-info-on-my-boards': Popup.open('boardInfoOnMyBoards'), diff --git a/client/components/sidebar/sidebarMigrations.jade b/client/components/sidebar/sidebarMigrations.jade deleted file mode 100644 index f5f7f08f8..000000000 --- a/client/components/sidebar/sidebarMigrations.jade +++ /dev/null @@ -1,109 +0,0 @@ -template(name='migrationsSidebar') - if currentUser.isBoardAdmin - .sidebar-migrations - h3 - | 🔧 - | {{_ 'migrations'}} - p.quiet {{_ 'migrations-description'}} - - .migrations-list - h4 {{_ 'board-migrations'}} - .migration-item - a.js-run-migration(data-migration="comprehensive") - .migration-name - | {{_ 'comprehensive-board-migration'}} - .migration-status - if comprehensiveMigrationNeeded - span.badge.badge-warning {{_ 'migration-needed'}} - else - span.badge.badge-success {{_ 'migration-complete'}} - - .migration-item - a.js-run-migration(data-migration="fixMissingLists") - .migration-name - | {{_ 'fix-missing-lists-migration'}} - .migration-status - if fixMissingListsNeeded - span.badge.badge-warning {{_ 'migration-needed'}} - else - span.badge.badge-success {{_ 'migration-complete'}} - - .migration-item - a.js-run-migration(data-migration="deleteDuplicateEmptyLists") - .migration-name - | {{_ 'delete-duplicate-empty-lists-migration'}} - .migration-status - if deleteDuplicateEmptyListsNeeded - span.badge.badge-warning {{_ 'migration-needed'}} - else - span.badge.badge-success {{_ 'migration-complete'}} - - .migration-item - a.js-run-migration(data-migration="restoreLostCards") - .migration-name - | {{_ 'restore-lost-cards-migration'}} - .migration-status - if restoreLostCardsNeeded - span.badge.badge-warning {{_ 'migration-needed'}} - else - span.badge.badge-success {{_ 'migration-complete'}} - - .migration-item - a.js-run-migration(data-migration="restoreAllArchived") - .migration-name - | {{_ 'restore-all-archived-migration'}} - .migration-status - if restoreAllArchivedNeeded - span.badge.badge-warning {{_ 'migration-needed'}} - else - span.badge.badge-success {{_ 'migration-complete'}} - - .migration-item - a.js-run-migration(data-migration="fixAvatarUrls") - .migration-name - | {{_ 'fix-avatar-urls-migration'}} - .migration-status - if fixAvatarUrlsNeeded - span.badge.badge-warning {{_ 'migration-needed'}} - else - span.badge.badge-success {{_ 'migration-complete'}} - - .migration-item - a.js-run-migration(data-migration="fixAllFileUrls") - .migration-name - | {{_ 'fix-all-file-urls-migration'}} - .migration-status - if fixAllFileUrlsNeeded - span.badge.badge-warning {{_ 'migration-needed'}} - else - span.badge.badge-success {{_ 'migration-complete'}} - else - p.quiet {{_ 'migrations-admin-only'}} - -template(name='runComprehensiveMigrationPopup') - p {{_ 'run-comprehensive-migration-confirm'}} - button.js-confirm.primary.full(type="submit") {{_ 'run-migration'}} - -template(name='runFixMissingListsMigrationPopup') - p {{_ 'run-fix-missing-lists-migration-confirm'}} - button.js-confirm.primary.full(type="submit") {{_ 'run-migration'}} - -template(name='runDeleteDuplicateEmptyListsMigrationPopup') - p {{_ 'run-delete-duplicate-empty-lists-migration-confirm'}} - button.js-confirm.primary.full(type="submit") {{_ 'run-migration'}} - -template(name='runRestoreLostCardsMigrationPopup') - p {{_ 'run-restore-lost-cards-migration-confirm'}} - button.js-confirm.primary.full(type="submit") {{_ 'run-migration'}} - -template(name='runRestoreAllArchivedMigrationPopup') - p {{_ 'run-restore-all-archived-migration-confirm'}} - button.js-confirm.primary.full(type="submit") {{_ 'run-migration'}} - -template(name='runFixAvatarUrlsMigrationPopup') - p {{_ 'run-fix-avatar-urls-migration-confirm'}} - button.js-confirm.primary.full(type="submit") {{_ 'run-migration'}} - -template(name='runFixAllFileUrlsMigrationPopup') - p {{_ 'run-fix-all-file-urls-migration-confirm'}} - button.js-confirm.primary.full(type="submit") {{_ 'run-migration'}} diff --git a/client/components/sidebar/sidebarMigrations.js b/client/components/sidebar/sidebarMigrations.js deleted file mode 100644 index 89d3343ec..000000000 --- a/client/components/sidebar/sidebarMigrations.js +++ /dev/null @@ -1,341 +0,0 @@ -import { ReactiveCache } from '/imports/reactiveCache'; -import { TAPi18n } from '/imports/i18n'; -import { migrationProgressManager } from '/client/components/migrationProgress'; - -BlazeComponent.extendComponent({ - onCreated() { - this.migrationStatuses = new ReactiveVar({}); - this.loadMigrationStatuses(); - }, - - loadMigrationStatuses() { - const boardId = Session.get('currentBoard'); - if (!boardId) return; - - // Check comprehensive migration - Meteor.call('comprehensiveBoardMigration.needsMigration', boardId, (err, res) => { - if (!err) { - const statuses = this.migrationStatuses.get(); - statuses.comprehensive = res; - this.migrationStatuses.set(statuses); - } - }); - - // Check fix missing lists migration - Meteor.call('fixMissingListsMigration.needsMigration', boardId, (err, res) => { - if (!err) { - const statuses = this.migrationStatuses.get(); - statuses.fixMissingLists = res; - this.migrationStatuses.set(statuses); - } - }); - - // Check delete duplicate empty lists migration - Meteor.call('deleteDuplicateEmptyLists.needsMigration', boardId, (err, res) => { - if (!err) { - const statuses = this.migrationStatuses.get(); - statuses.deleteDuplicateEmptyLists = res; - this.migrationStatuses.set(statuses); - } - }); - - // Check restore lost cards migration - Meteor.call('restoreLostCards.needsMigration', boardId, (err, res) => { - if (!err) { - const statuses = this.migrationStatuses.get(); - statuses.restoreLostCards = res; - this.migrationStatuses.set(statuses); - } - }); - - // Check restore all archived migration - Meteor.call('restoreAllArchived.needsMigration', boardId, (err, res) => { - if (!err) { - const statuses = this.migrationStatuses.get(); - statuses.restoreAllArchived = res; - this.migrationStatuses.set(statuses); - } - }); - - // Check fix avatar URLs migration (board-specific) - Meteor.call('fixAvatarUrls.needsMigration', boardId, (err, res) => { - if (!err) { - const statuses = this.migrationStatuses.get(); - statuses.fixAvatarUrls = res; - this.migrationStatuses.set(statuses); - } - }); - - // Check fix all file URLs migration (board-specific) - Meteor.call('fixAllFileUrls.needsMigration', boardId, (err, res) => { - if (!err) { - const statuses = this.migrationStatuses.get(); - statuses.fixAllFileUrls = res; - this.migrationStatuses.set(statuses); - } - }); - }, - - comprehensiveMigrationNeeded() { - return this.migrationStatuses.get().comprehensive === true; - }, - - fixMissingListsNeeded() { - return this.migrationStatuses.get().fixMissingLists === true; - }, - - deleteDuplicateEmptyListsNeeded() { - return this.migrationStatuses.get().deleteDuplicateEmptyLists === true; - }, - - restoreLostCardsNeeded() { - return this.migrationStatuses.get().restoreLostCards === true; - }, - - restoreAllArchivedNeeded() { - return this.migrationStatuses.get().restoreAllArchived === true; - }, - - fixAvatarUrlsNeeded() { - return this.migrationStatuses.get().fixAvatarUrls === true; - }, - - fixAllFileUrlsNeeded() { - return this.migrationStatuses.get().fixAllFileUrls === true; - }, - - // Simulate migration progress updates using the global progress popup - async simulateMigrationProgress(progressSteps) { - const totalSteps = progressSteps.length; - for (let i = 0; i < progressSteps.length; i++) { - const step = progressSteps[i]; - const overall = Math.round(((i + 1) / totalSteps) * 100); - - // Start step - migrationProgressManager.updateProgress({ - overallProgress: overall, - currentStep: i + 1, - totalSteps, - stepName: step.step, - stepProgress: 0, - stepStatus: `Starting ${step.name}...`, - stepDetails: null, - boardId: Session.get('currentBoard'), - }); - - const stepDuration = step.duration; - const updateInterval = 100; - const totalUpdates = Math.max(1, Math.floor(stepDuration / updateInterval)); - for (let j = 0; j < totalUpdates; j++) { - const per = Math.round(((j + 1) / totalUpdates) * 100); - migrationProgressManager.updateProgress({ - overallProgress: overall, - currentStep: i + 1, - totalSteps, - stepName: step.step, - stepProgress: per, - stepStatus: `Processing ${step.name}...`, - stepDetails: { progress: `${per}%` }, - boardId: Session.get('currentBoard'), - }); - // eslint-disable-next-line no-await-in-loop - await new Promise((r) => setTimeout(r, updateInterval)); - } - - // Complete step - migrationProgressManager.updateProgress({ - overallProgress: overall, - currentStep: i + 1, - totalSteps, - stepName: step.step, - stepProgress: 100, - stepStatus: `${step.name} completed`, - stepDetails: { status: 'completed' }, - boardId: Session.get('currentBoard'), - }); - } - }, - - runMigration(migrationType) { - const boardId = Session.get('currentBoard'); - - let methodName; - let methodArgs = []; - - switch (migrationType) { - case 'comprehensive': - methodName = 'comprehensiveBoardMigration.execute'; - methodArgs = [boardId]; - break; - - case 'fixMissingLists': - methodName = 'fixMissingListsMigration.execute'; - methodArgs = [boardId]; - break; - - case 'deleteDuplicateEmptyLists': - methodName = 'deleteDuplicateEmptyLists.execute'; - methodArgs = [boardId]; - break; - - case 'restoreLostCards': - methodName = 'restoreLostCards.execute'; - methodArgs = [boardId]; - break; - - case 'restoreAllArchived': - methodName = 'restoreAllArchived.execute'; - methodArgs = [boardId]; - break; - - case 'fixAvatarUrls': - methodName = 'fixAvatarUrls.execute'; - methodArgs = [boardId]; - break; - - case 'fixAllFileUrls': - methodName = 'fixAllFileUrls.execute'; - methodArgs = [boardId]; - break; - } - - if (methodName) { - // Define simulated steps per migration type - const stepsByType = { - comprehensive: [ - { step: 'analyze_board_structure', name: 'Analyze Board Structure', duration: 800 }, - { step: 'fix_orphaned_cards', name: 'Fix Orphaned Cards', duration: 1200 }, - { step: 'convert_shared_lists', name: 'Convert Shared Lists', duration: 1000 }, - { step: 'ensure_per_swimlane_lists', name: 'Ensure Per-Swimlane Lists', duration: 800 }, - { step: 'validate_migration', name: 'Validate Migration', duration: 800 }, - { step: 'fix_avatar_urls', name: 'Fix Avatar URLs', duration: 600 }, - { step: 'fix_attachment_urls', name: 'Fix Attachment URLs', duration: 600 }, - ], - fixMissingLists: [ - { step: 'analyze_lists', name: 'Analyze Lists', duration: 600 }, - { step: 'create_missing_lists', name: 'Create Missing Lists', duration: 900 }, - { step: 'update_cards', name: 'Update Cards', duration: 900 }, - { step: 'finalize', name: 'Finalize', duration: 400 }, - ], - deleteDuplicateEmptyLists: [ - { step: 'convert_shared_lists', name: 'Convert Shared Lists', duration: 700 }, - { step: 'delete_duplicate_empty_lists', name: 'Delete Duplicate Empty Lists', duration: 800 }, - ], - restoreLostCards: [ - { step: 'ensure_lost_cards_swimlane', name: 'Ensure Lost Cards Swimlane', duration: 600 }, - { step: 'restore_lists', name: 'Restore Lists', duration: 800 }, - { step: 'restore_cards', name: 'Restore Cards', duration: 1000 }, - ], - restoreAllArchived: [ - { step: 'restore_swimlanes', name: 'Restore Swimlanes', duration: 800 }, - { step: 'restore_lists', name: 'Restore Lists', duration: 900 }, - { step: 'restore_cards', name: 'Restore Cards', duration: 1000 }, - { step: 'fix_missing_ids', name: 'Fix Missing IDs', duration: 600 }, - ], - fixAvatarUrls: [ - { step: 'scan_users', name: 'Checking board member avatars', duration: 500 }, - { step: 'fix_urls', name: 'Fixing avatar URLs', duration: 900 }, - ], - fixAllFileUrls: [ - { step: 'scan_files', name: 'Checking board file attachments', duration: 600 }, - { step: 'fix_urls', name: 'Fixing file URLs', duration: 1000 }, - ], - }; - - const steps = stepsByType[migrationType] || [ - { step: 'running', name: 'Running Migration', duration: 1000 }, - ]; - - // Kick off popup and simulated progress - migrationProgressManager.startMigration(); - const progressPromise = this.simulateMigrationProgress(steps); - - // Start migration call - const callPromise = new Promise((resolve, reject) => { - Meteor.call(methodName, ...methodArgs, (err, result) => { - if (err) return reject(err); - return resolve(result); - }); - }); - - Promise.allSettled([callPromise, progressPromise]).then(([callRes]) => { - if (callRes.status === 'rejected') { - migrationProgressManager.failMigration(callRes.reason); - } else { - const result = callRes.value; - // Summarize result details in the popup - let summary = {}; - if (result && result.results) { - // Comprehensive returns {success, results} - const r = result.results; - summary = { - totalCardsProcessed: r.totalCardsProcessed, - totalListsProcessed: r.totalListsProcessed, - totalListsCreated: r.totalListsCreated, - }; - } else if (result && result.changes) { - // Many migrations return a changes string array - summary = { changes: result.changes.join(' | ') }; - } else if (result && typeof result === 'object') { - summary = result; - } - - migrationProgressManager.updateProgress({ - overallProgress: 100, - currentStep: steps.length, - totalSteps: steps.length, - stepName: 'completed', - stepProgress: 100, - stepStatus: 'Migration completed', - stepDetails: summary, - boardId: Session.get('currentBoard'), - }); - - migrationProgressManager.completeMigration(); - - // Refresh status badges slightly after - Meteor.setTimeout(() => { - this.loadMigrationStatuses(); - }, 1000); - } - }); - } - }, - - events() { - const self = this; // Capture component reference - - return [ - { - 'click .js-run-migration[data-migration="comprehensive"]': Popup.afterConfirm('runComprehensiveMigration', function() { - self.runMigration('comprehensive'); - Popup.back(); - }), - 'click .js-run-migration[data-migration="fixMissingLists"]': Popup.afterConfirm('runFixMissingListsMigration', function() { - self.runMigration('fixMissingLists'); - Popup.back(); - }), - 'click .js-run-migration[data-migration="deleteDuplicateEmptyLists"]': Popup.afterConfirm('runDeleteDuplicateEmptyListsMigration', function() { - self.runMigration('deleteDuplicateEmptyLists'); - Popup.back(); - }), - 'click .js-run-migration[data-migration="restoreLostCards"]': Popup.afterConfirm('runRestoreLostCardsMigration', function() { - self.runMigration('restoreLostCards'); - Popup.back(); - }), - 'click .js-run-migration[data-migration="restoreAllArchived"]': Popup.afterConfirm('runRestoreAllArchivedMigration', function() { - self.runMigration('restoreAllArchived'); - Popup.back(); - }), - 'click .js-run-migration[data-migration="fixAvatarUrls"]': Popup.afterConfirm('runFixAvatarUrlsMigration', function() { - self.runMigration('fixAvatarUrls'); - Popup.back(); - }), - 'click .js-run-migration[data-migration="fixAllFileUrls"]': Popup.afterConfirm('runFixAllFileUrlsMigration', function() { - self.runMigration('fixAllFileUrls'); - Popup.back(); - }), - }, - ]; - }, -}).register('migrationsSidebar'); diff --git a/client/lib/migrationManager.js b/client/lib/migrationManager.js deleted file mode 100644 index 19ea53f10..000000000 --- a/client/lib/migrationManager.js +++ /dev/null @@ -1,815 +0,0 @@ -/** - * Migration Manager - * Handles all database migrations as steps during board loading - * with detailed progress tracking and background persistence - */ - -import { ReactiveVar } from 'meteor/reactive-var'; -import { ReactiveCache } from '/imports/reactiveCache'; - -// Reactive variables for migration progress -export const migrationProgress = new ReactiveVar(0); -export const migrationStatus = new ReactiveVar(''); -export const migrationCurrentStep = new ReactiveVar(''); -export const migrationSteps = new ReactiveVar([]); -export const isMigrating = new ReactiveVar(false); -export const migrationEstimatedTime = new ReactiveVar(''); - -class MigrationManager { - constructor() { - this.migrationCache = new Map(); // Cache completed migrations - this.steps = this.initializeMigrationSteps(); - this.currentStepIndex = 0; - this.startTime = null; - } - - /** - * Initialize all migration steps with their details - */ - initializeMigrationSteps() { - return [ - { - id: 'board-background-color', - name: 'Board Background Colors', - description: 'Setting up board background colors', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'add-cardcounterlist-allowed', - name: 'Card Counter List Settings', - description: 'Adding card counter list permissions', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'add-boardmemberlist-allowed', - name: 'Board Member List Settings', - description: 'Adding board member list permissions', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'lowercase-board-permission', - name: 'Board Permission Standardization', - description: 'Converting board permissions to lowercase', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'change-attachments-type-for-non-images', - name: 'Attachment Type Standardization', - description: 'Updating attachment types for non-images', - weight: 2, - completed: false, - progress: 0 - }, - { - id: 'card-covers', - name: 'Card Covers System', - description: 'Setting up card cover functionality', - weight: 2, - completed: false, - progress: 0 - }, - { - 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 - }, - { - id: 'denormalize-star-number-per-board', - name: 'Board Star Counts', - description: 'Calculating star counts per board', - weight: 3, - completed: false, - progress: 0 - }, - { - id: 'add-member-isactive-field', - name: 'Member Activity Status', - description: 'Adding member activity tracking', - weight: 2, - completed: false, - progress: 0 - }, - { - id: 'add-sort-checklists', - name: 'Checklist Sorting', - description: 'Adding sort order to checklists', - weight: 2, - completed: false, - progress: 0 - }, - { - id: 'add-swimlanes', - name: 'Swimlanes System', - description: 'Setting up swimlanes functionality', - weight: 4, - completed: false, - progress: 0 - }, - { - id: 'add-views', - name: 'Board Views', - description: 'Adding board view options', - weight: 2, - completed: false, - progress: 0 - }, - { - id: 'add-checklist-items', - name: 'Checklist Items', - description: 'Setting up checklist items system', - weight: 3, - completed: false, - progress: 0 - }, - { - id: 'add-card-types', - name: 'Card Types', - description: 'Adding card type functionality', - weight: 2, - completed: false, - progress: 0 - }, - { - id: 'add-custom-fields-to-cards', - name: 'Custom Fields', - description: 'Adding custom fields to cards', - weight: 3, - completed: false, - progress: 0 - }, - { - id: 'add-requester-field', - name: 'Requester Field', - description: 'Adding requester field to cards', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'add-assigner-field', - name: 'Assigner Field', - description: 'Adding assigner field to cards', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'add-parent-field-to-cards', - name: 'Card Parent Relationships', - description: 'Adding parent field to cards', - weight: 2, - completed: false, - progress: 0 - }, - { - id: 'add-subtasks-boards', - name: 'Subtasks Boards', - description: 'Setting up subtasks board functionality', - weight: 3, - completed: false, - progress: 0 - }, - { - id: 'add-subtasks-sort', - name: 'Subtasks Sorting', - description: 'Adding sort order to subtasks', - weight: 2, - completed: false, - progress: 0 - }, - { - id: 'add-subtasks-allowed', - name: 'Subtasks Permissions', - description: 'Adding subtasks permissions', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'add-authenticationMethod', - name: 'Authentication Methods', - description: 'Adding authentication method tracking', - weight: 2, - completed: false, - progress: 0 - }, - { - id: 'remove-tag', - name: 'Remove Tag Field', - description: 'Removing deprecated tag field', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'remove-customFields-references-broken', - name: 'Fix Custom Fields References', - description: 'Fixing broken custom field references', - weight: 2, - completed: false, - progress: 0 - }, - { - id: 'add-product-name', - name: 'Product Name Settings', - description: 'Adding product name configuration', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'add-hide-logo', - name: 'Hide Logo Setting', - description: 'Adding hide logo option', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'add-hide-card-counter-list', - name: 'Hide Card Counter Setting', - description: 'Adding hide card counter option', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'add-hide-board-member-list', - name: 'Hide Board Member List Setting', - description: 'Adding hide board member list option', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'add-displayAuthenticationMethod', - name: 'Display Authentication Method', - description: 'Adding authentication method display option', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'add-defaultAuthenticationMethod', - name: 'Default Authentication Method', - description: 'Setting default authentication method', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'add-templates', - name: 'Board Templates', - description: 'Setting up board templates system', - weight: 3, - completed: false, - progress: 0 - }, - { - id: 'fix-circular-reference_', - name: 'Fix Circular References', - description: 'Fixing circular references in cards', - weight: 2, - completed: false, - progress: 0 - }, - { - id: 'mutate-boardIds-in-customfields', - name: 'Custom Fields Board IDs', - description: 'Updating board IDs in custom fields', - weight: 2, - completed: false, - progress: 0 - }, - { - id: 'add-missing-created-and-modified', - name: 'Missing Timestamps', - description: 'Adding missing created and modified timestamps', - weight: 4, - completed: false, - progress: 0 - }, - { - id: 'fix-incorrect-dates', - name: 'Fix Incorrect Dates', - description: 'Correcting incorrect date values', - weight: 3, - completed: false, - progress: 0 - }, - { - id: 'add-assignee', - name: 'Assignee Field', - description: 'Adding assignee field to cards', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'add-profile-showDesktopDragHandles', - name: 'Desktop Drag Handles', - description: 'Adding desktop drag handles preference', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'add-profile-hiddenMinicardLabelText', - name: 'Hidden Minicard Labels', - description: 'Adding hidden minicard label text preference', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'add-receiveddate-allowed', - name: 'Received Date Permissions', - description: 'Adding received date permissions', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'add-startdate-allowed', - name: 'Start Date Permissions', - description: 'Adding start date permissions', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'add-duedate-allowed', - name: 'Due Date Permissions', - description: 'Adding due date permissions', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'add-enddate-allowed', - name: 'End Date Permissions', - description: 'Adding end date permissions', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'add-members-allowed', - name: 'Members Permissions', - description: 'Adding members permissions', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'add-assignee-allowed', - name: 'Assignee Permissions', - description: 'Adding assignee permissions', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'add-labels-allowed', - name: 'Labels Permissions', - description: 'Adding labels permissions', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'add-checklists-allowed', - name: 'Checklists Permissions', - description: 'Adding checklists permissions', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'add-attachments-allowed', - name: 'Attachments Permissions', - description: 'Adding attachments permissions', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'add-comments-allowed', - name: 'Comments Permissions', - description: 'Adding comments permissions', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'add-assigned-by-allowed', - name: 'Assigned By Permissions', - description: 'Adding assigned by permissions', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'add-requested-by-allowed', - name: 'Requested By Permissions', - description: 'Adding requested by permissions', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'add-activities-allowed', - name: 'Activities Permissions', - description: 'Adding activities permissions', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'add-description-title-allowed', - name: 'Description Title Permissions', - description: 'Adding description title permissions', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'add-description-text-allowed', - name: 'Description Text Permissions', - description: 'Adding description text permissions', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'add-description-text-allowed-on-minicard', - name: 'Minicard Description Permissions', - description: 'Adding minicard description permissions', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'add-sort-field-to-boards', - name: 'Board Sort Field', - description: 'Adding sort field to boards', - weight: 2, - completed: false, - progress: 0 - }, - { - id: 'add-default-profile-view', - name: 'Default Profile View', - description: 'Setting default profile view', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'add-hide-logo-by-default', - name: 'Hide Logo Default', - description: 'Setting hide logo as default', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'add-hide-card-counter-list-by-default', - name: 'Hide Card Counter Default', - description: 'Setting hide card counter as default', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'add-hide-board-member-list-by-default', - name: 'Hide Board Member List Default', - description: 'Setting hide board member list as default', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'add-card-number-allowed', - name: 'Card Number Permissions', - description: 'Adding card number permissions', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'assign-boardwise-card-numbers', - name: 'Board Card Numbers', - description: 'Assigning board-wise card numbers', - weight: 3, - completed: false, - progress: 0 - }, - { - id: 'add-card-details-show-lists', - name: 'Card Details Show Lists', - description: 'Adding card details show lists option', - weight: 1, - completed: false, - progress: 0 - }, - { - 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 - }, - { - 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 - }, - { - id: 'migrate-attachment-drop-index-cardId', - name: 'Drop Attachment Index', - description: 'Dropping old attachment index', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'migrate-attachment-migration-fix-source-import', - name: 'Fix Attachment Source Import', - description: 'Fixing attachment source import field', - weight: 2, - completed: false, - progress: 0 - }, - { - id: 'attachment-cardCopy-fix-boardId-etc', - name: 'Fix Attachment Card Copy', - description: 'Fixing attachment card copy board IDs', - weight: 2, - completed: false, - progress: 0 - }, - { - id: 'remove-unused-planning-poker', - name: 'Remove Planning Poker', - description: 'Removing unused planning poker fields', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'remove-user-profile-hiddenSystemMessages', - name: 'Remove Hidden System Messages', - description: 'Removing hidden system messages field', - weight: 1, - completed: false, - progress: 0 - }, - { - id: 'remove-user-profile-hideCheckedItems', - name: 'Remove Hide Checked Items', - description: 'Removing hide checked items field', - weight: 1, - completed: false, - progress: 0 - }, - { - 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 - } - ]; - } - - /** - * Check if any migrations need to be run for a specific board - */ - needsMigration(boardId = null) { - if (boardId) { - // Check if specific board needs migration based on version - const board = ReactiveCache.getBoard(boardId); - return !board || !board.migrationVersion || board.migrationVersion < 1; - } - - // Check if any migration step is not completed (global migrations) - return this.steps.some(step => !step.completed); - } - - /** - * Get total weight of all migrations - */ - getTotalWeight() { - return this.steps.reduce((total, step) => total + step.weight, 0); - } - - /** - * Get completed weight - */ - getCompletedWeight() { - return this.steps.reduce((total, step) => { - return total + (step.completed ? step.weight : step.progress * step.weight / 100); - }, 0); - } - - /** - * Mark a board as migrated - */ - markBoardAsMigrated(boardId) { - try { - Meteor.call('boardMigration.markAsMigrated', boardId, 'full_board_migration', (error, result) => { - if (error) { - console.error('Failed to mark board as migrated:', error); - } else { - console.log('Board marked as migrated:', boardId); - } - }); - } catch (error) { - console.error('Error marking board as migrated:', error); - } - } - - /** - * Fix boards that are stuck in migration loop - */ - fixStuckBoards() { - try { - Meteor.call('boardMigration.fixStuckBoards', (error, result) => { - if (error) { - console.error('Failed to fix stuck boards:', error); - } else { - console.log('Fix stuck boards result:', result); - } - }); - } catch (error) { - console.error('Error fixing stuck boards:', error); - } - } - - /** - * Start migration process using cron system - */ - async startMigration() { - if (isMigrating.get()) { - return; // Already migrating - } - - isMigrating.set(true); - migrationSteps.set([...this.steps]); - this.startTime = Date.now(); - - try { - // Start server-side cron migrations - Meteor.call('cron.startAllMigrations', (error, result) => { - if (error) { - console.error('Failed to start cron migrations:', error); - migrationStatus.set(`Migration failed: ${error.message}`); - isMigrating.set(false); - } - }); - - // Poll for progress updates - this.pollCronMigrationProgress(); - - } catch (error) { - console.error('Migration failed:', error); - migrationStatus.set(`Migration failed: ${error.message}`); - isMigrating.set(false); - } - } - - /** - * Poll for cron migration progress updates - */ - pollCronMigrationProgress() { - const pollInterval = setInterval(() => { - Meteor.call('cron.getMigrationProgress', (error, result) => { - if (error) { - console.error('Failed to get cron migration progress:', error); - clearInterval(pollInterval); - return; - } - - if (result) { - migrationProgress.set(result.progress); - migrationStatus.set(result.status); - migrationCurrentStep.set(result.currentStep); - migrationSteps.set(result.steps); - isMigrating.set(result.isMigrating); - - // Update local steps - if (result.steps) { - this.steps = result.steps; - } - - // If migration is complete, stop polling - if (!result.isMigrating && result.progress === 100) { - clearInterval(pollInterval); - - // Clear status after delay - setTimeout(() => { - migrationStatus.set(''); - migrationProgress.set(0); - migrationEstimatedTime.set(''); - }, 3000); - } - } - }); - }, 1000); // Poll every second - } - - /** - * Run a single migration step - */ - async runMigrationStep(step) { - // Simulate migration progress - const steps = 10; - for (let i = 0; i <= steps; i++) { - step.progress = (i / steps) * 100; - this.updateProgress(); - - // Simulate work - await new Promise(resolve => setTimeout(resolve, 50)); - } - - // In a real implementation, this would call the actual migration - // For now, we'll simulate the migration - // Running migration step - } - - /** - * Update progress variables - */ - updateProgress() { - const totalWeight = this.getTotalWeight(); - const completedWeight = this.getCompletedWeight(); - const progress = Math.round((completedWeight / totalWeight) * 100); - - migrationProgress.set(progress); - migrationSteps.set([...this.steps]); - - // Calculate estimated time remaining - if (this.startTime && progress > 0) { - const elapsed = Date.now() - this.startTime; - const rate = progress / elapsed; // progress per millisecond - const remaining = 100 - progress; - const estimatedMs = remaining / rate; - migrationEstimatedTime.set(this.formatTime(estimatedMs)); - } - } - - /** - * Format time in milliseconds to human readable format - */ - formatTime(ms) { - if (ms < 1000) { - return `${Math.round(ms)}ms`; - } - - const seconds = Math.floor(ms / 1000); - const minutes = Math.floor(seconds / 60); - const hours = Math.floor(minutes / 60); - - if (hours > 0) { - const remainingMinutes = minutes % 60; - const remainingSeconds = seconds % 60; - return `${hours}h ${remainingMinutes}m ${remainingSeconds}s`; - } else if (minutes > 0) { - const remainingSeconds = seconds % 60; - return `${minutes}m ${remainingSeconds}s`; - } else { - return `${seconds}s`; - } - } - - /** - * Clear migration cache (for testing) - */ - clearCache() { - this.migrationCache.clear(); - this.steps.forEach(step => { - step.completed = false; - step.progress = 0; - }); - } -} - -// Export singleton instance -export const migrationManager = new MigrationManager(); diff --git a/imports/cronMigrationClient.js b/imports/cronMigrationClient.js new file mode 100644 index 000000000..aa28a009e --- /dev/null +++ b/imports/cronMigrationClient.js @@ -0,0 +1,50 @@ +import { Meteor } from 'meteor/meteor'; +import { ReactiveVar } from 'meteor/reactive-var'; + +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([]); + +function fetchProgress() { + Meteor.call('cron.getMigrationProgress', (err, res) => { + if (err) return; + if (!res) return; + cronMigrationProgress.set(res.progress || 0); + cronMigrationStatus.set(res.status || ''); + cronMigrationCurrentStep.set(res.currentStep || ''); + cronMigrationSteps.set(res.steps || []); + cronIsMigrating.set(res.isMigrating || false); + }); +} + +// Expose cron jobs via method +function fetchJobs() { + Meteor.call('cron.getJobs', (err, res) => { + if (err) return; + cronJobs.set(res || []); + }); +} + +if (Meteor.isClient) { + // Initial fetch + fetchProgress(); + fetchJobs(); + + // Poll periodically + Meteor.setInterval(() => { + fetchProgress(); + fetchJobs(); + }, 2000); +} + +export default { + cronMigrationProgress, + cronMigrationStatus, + cronMigrationCurrentStep, + cronMigrationSteps, + cronIsMigrating, + cronJobs, +}; diff --git a/imports/i18n/data/en.i18n.json b/imports/i18n/data/en.i18n.json index 94bfb3925..d48099de1 100644 --- a/imports/i18n/data/en.i18n.json +++ b/imports/i18n/data/en.i18n.json @@ -1570,6 +1570,7 @@ "operation-type": "Operation Type", "overall-progress": "Overall Progress", "page": "Page", + "pause": "Pause", "pause-migration": "Pause Migration", "previous": "Previous", "refresh": "Refresh", @@ -1599,5 +1600,6 @@ "weight": "Weight", "idle": "Idle", "complete": "Complete", - "cron": "Cron" + "cron": "Cron", + "current-step": "Current Step" } diff --git a/server/cronMigrationManager.js b/server/cronMigrationManager.js index a830f4ab4..c509a396a 100644 --- a/server/cronMigrationManager.js +++ b/server/cronMigrationManager.js @@ -1357,66 +1357,119 @@ Meteor.startup(() => { // Meteor methods for client-server communication Meteor.methods({ 'cron.startAllMigrations'() { - if (!this.userId) { - throw new Meteor.Error('not-authorized'); + const userId = this.userId; + if (!userId) { + throw new Meteor.Error('not-authorized', 'Must be logged in'); + } + const user = ReactiveCache.getUser(userId); + if (!user || !user.isAdmin) { + throw new Meteor.Error('not-authorized', 'Admin access required'); } return cronMigrationManager.startAllMigrations(); }, 'cron.startJob'(cronName) { - if (!this.userId) { - throw new Meteor.Error('not-authorized'); + const userId = this.userId; + if (!userId) { + throw new Meteor.Error('not-authorized', 'Must be logged in'); + } + const user = ReactiveCache.getUser(userId); + if (!user || !user.isAdmin) { + throw new Meteor.Error('not-authorized', 'Admin access required'); } return cronMigrationManager.startCronJob(cronName); }, 'cron.stopJob'(cronName) { - if (!this.userId) { - throw new Meteor.Error('not-authorized'); + const userId = this.userId; + if (!userId) { + throw new Meteor.Error('not-authorized', 'Must be logged in'); + } + const user = ReactiveCache.getUser(userId); + if (!user || !user.isAdmin) { + throw new Meteor.Error('not-authorized', 'Admin access required'); } return cronMigrationManager.stopCronJob(cronName); }, 'cron.pauseJob'(cronName) { - if (!this.userId) { - throw new Meteor.Error('not-authorized'); + const userId = this.userId; + if (!userId) { + throw new Meteor.Error('not-authorized', 'Must be logged in'); + } + const user = ReactiveCache.getUser(userId); + if (!user || !user.isAdmin) { + throw new Meteor.Error('not-authorized', 'Admin access required'); } return cronMigrationManager.pauseCronJob(cronName); }, 'cron.resumeJob'(cronName) { - if (!this.userId) { - throw new Meteor.Error('not-authorized'); + const userId = this.userId; + if (!userId) { + throw new Meteor.Error('not-authorized', 'Must be logged in'); + } + const user = ReactiveCache.getUser(userId); + if (!user || !user.isAdmin) { + throw new Meteor.Error('not-authorized', 'Admin access required'); } return cronMigrationManager.resumeCronJob(cronName); }, 'cron.removeJob'(cronName) { - if (!this.userId) { - throw new Meteor.Error('not-authorized'); + const userId = this.userId; + if (!userId) { + throw new Meteor.Error('not-authorized', 'Must be logged in'); + } + const user = ReactiveCache.getUser(userId); + if (!user || !user.isAdmin) { + throw new Meteor.Error('not-authorized', 'Admin access required'); } return cronMigrationManager.removeCronJob(cronName); }, 'cron.addJob'(jobData) { - if (!this.userId) { - throw new Meteor.Error('not-authorized'); + const userId = this.userId; + if (!userId) { + throw new Meteor.Error('not-authorized', 'Must be logged in'); + } + const user = ReactiveCache.getUser(userId); + if (!user || !user.isAdmin) { + throw new Meteor.Error('not-authorized', 'Admin access required'); } return cronMigrationManager.addCronJob(jobData); }, 'cron.getJobs'() { + const userId = this.userId; + if (!userId) { + throw new Meteor.Error('not-authorized', 'Must be logged in'); + } + const user = ReactiveCache.getUser(userId); + if (!user || !user.isAdmin) { + throw new Meteor.Error('not-authorized', 'Admin access required'); + } + return cronMigrationManager.getAllCronJobs(); }, 'cron.getMigrationProgress'() { + const userId = this.userId; + if (!userId) { + throw new Meteor.Error('not-authorized', 'Must be logged in'); + } + const user = ReactiveCache.getUser(userId); + if (!user || !user.isAdmin) { + throw new Meteor.Error('not-authorized', 'Admin access required'); + } + return { progress: cronMigrationProgress.get(), status: cronMigrationStatus.get(), @@ -1427,72 +1480,153 @@ Meteor.methods({ }, 'cron.startBoardOperation'(boardId, operationType, operationData) { - if (!this.userId) { - throw new Meteor.Error('not-authorized'); + const userId = this.userId; + if (!userId) { + throw new Meteor.Error('not-authorized', 'Must be logged in'); + } + + // Check if user is global admin OR board admin + const user = ReactiveCache.getUser(userId); + const board = ReactiveCache.getBoard(boardId); + + if (!user) { + throw new Meteor.Error('not-authorized', 'User not found'); + } + + if (!board) { + throw new Meteor.Error('not-found', 'Board not found'); + } + + // Check global admin or board admin + const isGlobalAdmin = user.isAdmin; + const isBoardAdmin = board.members && board.members.some(member => + member.userId === userId && member.isAdmin + ); + + if (!isGlobalAdmin && !isBoardAdmin) { + throw new Meteor.Error('not-authorized', 'Admin access required for this board'); } return cronMigrationManager.startBoardOperation(boardId, operationType, operationData); }, 'cron.getBoardOperations'(boardId) { - if (!this.userId) { - throw new Meteor.Error('not-authorized'); + const userId = this.userId; + if (!userId) { + throw new Meteor.Error('not-authorized', 'Must be logged in'); + } + + // Check if user is global admin OR board admin + const user = ReactiveCache.getUser(userId); + const board = ReactiveCache.getBoard(boardId); + + if (!user) { + throw new Meteor.Error('not-authorized', 'User not found'); + } + + if (!board) { + throw new Meteor.Error('not-found', 'Board not found'); + } + + // Check global admin or board admin + const isGlobalAdmin = user.isAdmin; + const isBoardAdmin = board.members && board.members.some(member => + member.userId === userId && member.isAdmin + ); + + if (!isGlobalAdmin && !isBoardAdmin) { + throw new Meteor.Error('not-authorized', 'Admin access required for this board'); } return cronMigrationManager.getBoardOperations(boardId); }, 'cron.getAllBoardOperations'(page, limit, searchTerm) { - if (!this.userId) { - throw new Meteor.Error('not-authorized'); + const userId = this.userId; + if (!userId) { + throw new Meteor.Error('not-authorized', 'Must be logged in'); + } + const user = ReactiveCache.getUser(userId); + if (!user || !user.isAdmin) { + throw new Meteor.Error('not-authorized', 'Admin access required'); } return cronMigrationManager.getAllBoardOperations(page, limit, searchTerm); }, 'cron.getBoardOperationStats'() { - if (!this.userId) { - throw new Meteor.Error('not-authorized'); + const userId = this.userId; + if (!userId) { + throw new Meteor.Error('not-authorized', 'Must be logged in'); + } + const user = ReactiveCache.getUser(userId); + if (!user || !user.isAdmin) { + throw new Meteor.Error('not-authorized', 'Admin access required'); } return cronMigrationManager.getBoardOperationStats(); }, 'cron.getJobDetails'(jobId) { - if (!this.userId) { - throw new Meteor.Error('not-authorized'); + const userId = this.userId; + if (!userId) { + throw new Meteor.Error('not-authorized', 'Must be logged in'); + } + const user = ReactiveCache.getUser(userId); + if (!user || !user.isAdmin) { + throw new Meteor.Error('not-authorized', 'Admin access required'); } return cronJobStorage.getJobDetails(jobId); }, 'cron.getQueueStats'() { - if (!this.userId) { - throw new Meteor.Error('not-authorized'); + const userId = this.userId; + if (!userId) { + throw new Meteor.Error('not-authorized', 'Must be logged in'); + } + const user = ReactiveCache.getUser(userId); + if (!user || !user.isAdmin) { + throw new Meteor.Error('not-authorized', 'Admin access required'); } return cronJobStorage.getQueueStats(); }, 'cron.getSystemResources'() { - if (!this.userId) { - throw new Meteor.Error('not-authorized'); + const userId = this.userId; + if (!userId) { + throw new Meteor.Error('not-authorized', 'Must be logged in'); + } + const user = ReactiveCache.getUser(userId); + if (!user || !user.isAdmin) { + throw new Meteor.Error('not-authorized', 'Admin access required'); } return cronJobStorage.getSystemResources(); }, 'cron.clearAllJobs'() { - if (!this.userId) { - throw new Meteor.Error('not-authorized'); + const userId = this.userId; + if (!userId) { + throw new Meteor.Error('not-authorized', 'Must be logged in'); + } + const user = ReactiveCache.getUser(userId); + if (!user || !user.isAdmin) { + throw new Meteor.Error('not-authorized', 'Admin access required'); } return cronMigrationManager.clearAllCronJobs(); }, 'cron.pauseJob'(jobId) { - if (!this.userId) { - throw new Meteor.Error('not-authorized'); + const userId = this.userId; + if (!userId) { + throw new Meteor.Error('not-authorized', 'Must be logged in'); + } + const user = ReactiveCache.getUser(userId); + if (!user || !user.isAdmin) { + throw new Meteor.Error('not-authorized', 'Admin access required'); } cronJobStorage.updateQueueStatus(jobId, 'paused'); @@ -1501,8 +1635,13 @@ Meteor.methods({ }, 'cron.resumeJob'(jobId) { - if (!this.userId) { - throw new Meteor.Error('not-authorized'); + const userId = this.userId; + if (!userId) { + throw new Meteor.Error('not-authorized', 'Must be logged in'); + } + const user = ReactiveCache.getUser(userId); + if (!user || !user.isAdmin) { + throw new Meteor.Error('not-authorized', 'Admin access required'); } cronJobStorage.updateQueueStatus(jobId, 'pending'); @@ -1511,8 +1650,13 @@ Meteor.methods({ }, 'cron.stopJob'(jobId) { - if (!this.userId) { - throw new Meteor.Error('not-authorized'); + const userId = this.userId; + if (!userId) { + throw new Meteor.Error('not-authorized', 'Must be logged in'); + } + const user = ReactiveCache.getUser(userId); + if (!user || !user.isAdmin) { + throw new Meteor.Error('not-authorized', 'Admin access required'); } cronJobStorage.updateQueueStatus(jobId, 'stopped'); @@ -1524,16 +1668,76 @@ Meteor.methods({ }, 'cron.cleanupOldJobs'(daysOld) { - if (!this.userId) { - throw new Meteor.Error('not-authorized'); + const userId = this.userId; + if (!userId) { + throw new Meteor.Error('not-authorized', 'Must be logged in'); + } + const user = ReactiveCache.getUser(userId); + if (!user || !user.isAdmin) { + throw new Meteor.Error('not-authorized', 'Admin access required'); } return cronJobStorage.cleanupOldJobs(daysOld); }, + 'cron.pauseAllMigrations'() { + const userId = this.userId; + if (!userId) { + throw new Meteor.Error('not-authorized', 'Must be logged in'); + } + const user = ReactiveCache.getUser(userId); + if (!user || !user.isAdmin) { + throw new Meteor.Error('not-authorized', 'Admin access required'); + } + + // Pause all running jobs in the queue + const runningJobs = cronJobStorage.getIncompleteJobs().filter(job => job.status === 'running'); + runningJobs.forEach(job => { + cronJobStorage.updateQueueStatus(job.jobId, 'paused'); + cronJobStorage.saveJobStatus(job.jobId, { status: 'paused' }); + }); + + cronMigrationStatus.set('All migrations paused'); + return { success: true, message: 'All migrations paused' }; + }, + + 'cron.stopAllMigrations'() { + const userId = this.userId; + if (!userId) { + throw new Meteor.Error('not-authorized', 'Must be logged in'); + } + const user = ReactiveCache.getUser(userId); + if (!user || !user.isAdmin) { + throw new Meteor.Error('not-authorized', 'Admin access required'); + } + + // Stop all running and pending jobs + const incompleteJobs = cronJobStorage.getIncompleteJobs(); + incompleteJobs.forEach(job => { + cronJobStorage.updateQueueStatus(job.jobId, 'stopped', { stoppedAt: new Date() }); + cronJobStorage.saveJobStatus(job.jobId, { + status: 'stopped', + stoppedAt: new Date() + }); + }); + + // Reset migration state + cronIsMigrating.set(false); + cronMigrationStatus.set('All migrations stopped'); + cronMigrationProgress.set(0); + cronMigrationCurrentStep.set(''); + + return { success: true, message: 'All migrations stopped' }; + }, + 'cron.getBoardMigrationStats'() { - if (!this.userId) { - throw new Meteor.Error('not-authorized'); + const userId = this.userId; + if (!userId) { + throw new Meteor.Error('not-authorized', 'Must be logged in'); + } + const user = ReactiveCache.getUser(userId); + if (!user || !user.isAdmin) { + throw new Meteor.Error('not-authorized', 'Admin access required'); } // Import the board migration detector @@ -1542,8 +1746,13 @@ Meteor.methods({ }, 'cron.forceBoardMigrationScan'() { - if (!this.userId) { - throw new Meteor.Error('not-authorized'); + const userId = this.userId; + if (!userId) { + throw new Meteor.Error('not-authorized', 'Must be logged in'); + } + const user = ReactiveCache.getUser(userId); + if (!user || !user.isAdmin) { + throw new Meteor.Error('not-authorized', 'Admin access required'); } // Import the board migration detector