mirror of
https://github.com/wekan/wekan.git
synced 2026-01-11 03:58:50 +01:00
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.
This commit is contained in:
parent
d6834d0287
commit
cbb1cd78de
18 changed files with 397 additions and 1805 deletions
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,7 +79,6 @@ template(name="defaultLayout")
|
|||
| {{{afterBodyStart}}}
|
||||
+Template.dynamic(template=content)
|
||||
| {{{beforeBodyEnd}}}
|
||||
+migrationProgress
|
||||
+boardConversionProgress
|
||||
if (Modal.isOpen)
|
||||
#modal
|
||||
|
|
|
|||
|
|
@ -266,4 +266,36 @@
|
|||
.migration-progress-note {
|
||||
color: #a0aec0;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* 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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -588,10 +588,6 @@ template(name="boardMenuPopup")
|
|||
| 📦
|
||||
| {{_ 'archived-items'}}
|
||||
if currentUser.isBoardAdmin
|
||||
li
|
||||
a.js-open-migrations
|
||||
| 🔧
|
||||
| {{_ 'migrations'}}
|
||||
li
|
||||
a.js-change-board-color
|
||||
| 🎨
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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'}}
|
||||
|
|
@ -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');
|
||||
|
|
@ -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();
|
||||
50
imports/cronMigrationClient.js
Normal file
50
imports/cronMigrationClient.js
Normal file
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue