mirror of
https://github.com/wekan/wekan.git
synced 2026-01-23 09:46:09 +01:00
Fix DB migration from 8.19 to 8.21 stuck forever.
Thanks to MaccabeeY and xet7 ! Fixes #6078
This commit is contained in:
parent
e177bf54e2
commit
a31a615da6
9 changed files with 869 additions and 71 deletions
|
|
@ -862,3 +862,43 @@
|
|||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Progress bar styles for #cron-setting section */
|
||||
#cron-setting .progress-section {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
#cron-setting .step-counter {
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#cron-setting .progress {
|
||||
height: 30px;
|
||||
background-color: #e9ecef;
|
||||
border-radius: 4px;
|
||||
overflow: visible;
|
||||
margin-bottom: 5px;
|
||||
max-width: calc(100% - 40px);
|
||||
}
|
||||
|
||||
#cron-setting .progress-bar {
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
transition: width 0.3s ease;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#cron-setting .progress-text {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
margin-top: 5px;
|
||||
max-width: calc(100% - 40px);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,21 +3,52 @@ template(name="cronSettings")
|
|||
li
|
||||
h3 {{_ 'cron-migrations'}}
|
||||
.form-group
|
||||
label {{_ 'migration-status'}}
|
||||
.status-indicator
|
||||
span.status-label {{_ 'status'}}:
|
||||
span.status-value {{migrationStatus}}
|
||||
.progress-section
|
||||
.progress
|
||||
.progress-bar(role="progressbar" style="width: {{migrationProgress}}%" aria-valuenow="{{migrationProgress}}" aria-valuemin="0" aria-valuemax="100")
|
||||
| {{migrationProgress}}%
|
||||
.progress-text
|
||||
| {{migrationProgress}}% {{_ 'complete'}}
|
||||
label {{_ 'select-migration'}}
|
||||
select.js-migration-select.wekan-form-control
|
||||
option(value="0") 0 - {{_ 'all-migrations'}}
|
||||
each migrationStepsWithIndex
|
||||
option(value="{{index}}") {{index}} - {{name}}
|
||||
|
||||
.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'}}
|
||||
label {{_ 'migration-status'}}
|
||||
.status-indicator
|
||||
span.status-value {{migrationStatus}}
|
||||
if isMigrating
|
||||
.progress-section
|
||||
.step-counter
|
||||
| Step {{migrationCurrentStepNum}}/{{migrationTotalSteps}}
|
||||
.progress
|
||||
.progress-bar(role="progressbar" style="width: {{migrationProgress}}%" aria-valuenow="{{migrationProgress}}" aria-valuemin="0" aria-valuemax="100")
|
||||
| {{migrationProgress}}%
|
||||
.progress-text
|
||||
| {{migrationProgress}}% {{_ 'complete'}}
|
||||
|
||||
.form-group
|
||||
button.js-start-migration.btn.btn-primary(disabled="{{#if isMigrating}}disabled{{/if}}") {{_ 'start'}}
|
||||
button.js-pause-migration.btn.btn-warning(disabled="{{#unless isMigrating}}disabled{{/unless}}") {{_ 'pause'}}
|
||||
button.js-stop-migration.btn.btn-danger(disabled="{{#unless isMigrating}}disabled{{/unless}}") {{_ 'stop'}}
|
||||
|
||||
.form-group.migration-errors-section
|
||||
h4 {{_ 'cron-migration-errors'}}
|
||||
if hasErrors
|
||||
.error-actions
|
||||
button.js-clear-all-errors.btn.btn-sm.btn-warning {{_ 'cron-clear-errors'}}
|
||||
.errors-list
|
||||
each migrationErrors
|
||||
.error-item(class="error-{{severity}}")
|
||||
.error-header
|
||||
span.error-severity(class="severity-{{severity}}") {{severity}}
|
||||
span.error-time {{formatDateTime createdAt}}
|
||||
if stepId
|
||||
span.error-step {{stepId}}
|
||||
.error-message {{errorMessage}}
|
||||
if context
|
||||
.error-context
|
||||
each contextValue context
|
||||
span.context-item {{this}}
|
||||
else
|
||||
.no-errors
|
||||
| {{_ 'cron-no-errors'}}
|
||||
|
||||
li
|
||||
h3 {{_ 'board-operations'}}
|
||||
|
|
|
|||
|
|
@ -170,25 +170,22 @@ template(name="setting")
|
|||
label {{_ 'migration-status'}}
|
||||
.status-indicator
|
||||
span.status-label {{_ 'status'}}:
|
||||
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")
|
||||
| {{migrationProgress}}%
|
||||
.progress-text
|
||||
| {{migrationProgress}}% {{_ 'complete'}}
|
||||
span.status-value
|
||||
if isMigrating
|
||||
i.fa.fa-spinner.fa-spin(style="margin-right: 8px;")
|
||||
| {{#if isMigrating}}{{migrationStatus}}{{else}}{{_ 'idle'}}{{/if}}
|
||||
if isMigrating
|
||||
.progress-section
|
||||
.progress
|
||||
.progress-bar(role="progressbar" style="width: {{migrationProgress}}%" aria-valuenow="{{migrationProgress}}" aria-valuemin="0" aria-valuemax="100")
|
||||
| {{migrationProgress}}%
|
||||
.progress-text
|
||||
| {{migrationProgress}}% {{_ 'complete'}}
|
||||
|
||||
.form-group
|
||||
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
|
||||
button.js-start-all-migrations.btn.btn-primary(disabled="{{#if isMigrating}}disabled{{/if}}") {{_ 'start-all-migrations'}}
|
||||
button.js-pause-all-migrations.btn.btn-warning(disabled="{{#unless isMigrating}}disabled{{/unless}}") {{_ 'pause-all-migrations'}}
|
||||
button.js-stop-all-migrations.btn.btn-danger(disabled="{{#unless isMigrating}}disabled{{/unless}}") {{_ 'stop-all-migrations'}}
|
||||
|
||||
li
|
||||
h3 {{_ 'board-operations'}}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@ import {
|
|||
cronMigrationCurrentStep,
|
||||
cronMigrationSteps,
|
||||
cronIsMigrating,
|
||||
cronJobs
|
||||
cronJobs,
|
||||
cronMigrationCurrentStepNum,
|
||||
cronMigrationTotalSteps
|
||||
} from '/imports/cronMigrationClient';
|
||||
|
||||
|
||||
|
|
@ -27,6 +29,7 @@ BlazeComponent.extendComponent({
|
|||
this.webhookSetting = new ReactiveVar(false);
|
||||
this.attachmentSettings = new ReactiveVar(false);
|
||||
this.cronSettings = new ReactiveVar(false);
|
||||
this.migrationErrorsList = new ReactiveVar([]);
|
||||
|
||||
Meteor.subscribe('setting');
|
||||
Meteor.subscribe('mailServer');
|
||||
|
|
@ -36,6 +39,23 @@ BlazeComponent.extendComponent({
|
|||
Meteor.subscribe('accessibilitySettings');
|
||||
Meteor.subscribe('globalwebhooks');
|
||||
Meteor.subscribe('lockoutSettings');
|
||||
|
||||
// Poll for migration errors
|
||||
this.errorPollInterval = Meteor.setInterval(() => {
|
||||
if (this.cronSettings.get()) {
|
||||
Meteor.call('cron.getAllMigrationErrors', 50, (error, result) => {
|
||||
if (!error && result) {
|
||||
this.migrationErrorsList.set(result);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 5000); // Poll every 5 seconds
|
||||
},
|
||||
|
||||
onDestroyed() {
|
||||
if (this.errorPollInterval) {
|
||||
Meteor.clearInterval(this.errorPollInterval);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
|
|
@ -142,10 +162,40 @@ BlazeComponent.extendComponent({
|
|||
return cronMigrationSteps.get() || [];
|
||||
},
|
||||
|
||||
migrationStepsWithIndex() {
|
||||
const steps = cronMigrationSteps.get() || [];
|
||||
return steps.map((step, idx) => ({
|
||||
...step,
|
||||
index: idx + 1
|
||||
}));
|
||||
},
|
||||
|
||||
cronJobs() {
|
||||
return cronJobs.get() || [];
|
||||
},
|
||||
|
||||
migrationCurrentStepNum() {
|
||||
return cronMigrationCurrentStepNum.get() || 0;
|
||||
},
|
||||
|
||||
migrationTotalSteps() {
|
||||
return cronMigrationTotalSteps.get() || 0;
|
||||
},
|
||||
|
||||
migrationErrors() {
|
||||
return this.migrationErrorsList ? this.migrationErrorsList.get() : [];
|
||||
},
|
||||
|
||||
hasErrors() {
|
||||
const errors = this.migrationErrors();
|
||||
return errors && errors.length > 0;
|
||||
},
|
||||
|
||||
formatDateTime(date) {
|
||||
if (!date) return '';
|
||||
return moment(date).format('YYYY-MM-DD HH:mm:ss');
|
||||
},
|
||||
|
||||
setLoading(w) {
|
||||
this.loading.set(w);
|
||||
},
|
||||
|
|
@ -187,20 +237,35 @@ BlazeComponent.extendComponent({
|
|||
},
|
||||
|
||||
// Event handlers for cron settings
|
||||
'click button.js-start-all-migrations'(event) {
|
||||
'click button.js-start-migration'(event) {
|
||||
event.preventDefault();
|
||||
this.setLoading(true);
|
||||
Meteor.call('cron.startAllMigrations', (error, result) => {
|
||||
this.setLoading(false);
|
||||
if (error) {
|
||||
alert(TAPi18n.__('migration-start-failed') + ': ' + error.reason);
|
||||
} else {
|
||||
alert(TAPi18n.__('migration-started'));
|
||||
}
|
||||
});
|
||||
const selectedIndex = parseInt($('.js-migration-select').val() || '0', 10);
|
||||
|
||||
if (selectedIndex === 0) {
|
||||
// Run all migrations
|
||||
Meteor.call('cron.startAllMigrations', (error, result) => {
|
||||
this.setLoading(false);
|
||||
if (error) {
|
||||
alert(TAPi18n.__('migration-start-failed') + ': ' + error.reason);
|
||||
} else {
|
||||
alert(TAPi18n.__('migration-started'));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Run specific migration
|
||||
Meteor.call('cron.startSpecificMigration', selectedIndex - 1, (error, result) => {
|
||||
this.setLoading(false);
|
||||
if (error) {
|
||||
alert(TAPi18n.__('migration-start-failed') + ': ' + error.reason);
|
||||
} else {
|
||||
alert(TAPi18n.__('migration-started'));
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
'click button.js-pause-all-migrations'(event) {
|
||||
'click button.js-pause-migration'(event) {
|
||||
event.preventDefault();
|
||||
this.setLoading(true);
|
||||
Meteor.call('cron.pauseAllMigrations', (error, result) => {
|
||||
|
|
@ -213,7 +278,7 @@ BlazeComponent.extendComponent({
|
|||
});
|
||||
},
|
||||
|
||||
'click button.js-stop-all-migrations'(event) {
|
||||
'click button.js-stop-migration'(event) {
|
||||
event.preventDefault();
|
||||
if (confirm(TAPi18n.__('migration-stop-confirm'))) {
|
||||
this.setLoading(true);
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ export const cronMigrationCurrentStep = new ReactiveVar('');
|
|||
export const cronMigrationSteps = new ReactiveVar([]);
|
||||
export const cronIsMigrating = new ReactiveVar(false);
|
||||
export const cronJobs = new ReactiveVar([]);
|
||||
export const cronMigrationCurrentStepNum = new ReactiveVar(0);
|
||||
export const cronMigrationTotalSteps = new ReactiveVar(0);
|
||||
|
||||
function fetchProgress() {
|
||||
Meteor.call('cron.getMigrationProgress', (err, res) => {
|
||||
|
|
@ -17,6 +19,8 @@ function fetchProgress() {
|
|||
cronMigrationCurrentStep.set(res.currentStep || '');
|
||||
cronMigrationSteps.set(res.steps || []);
|
||||
cronIsMigrating.set(res.isMigrating || false);
|
||||
cronMigrationCurrentStepNum.set(res.currentStepNum || 0);
|
||||
cronMigrationTotalSteps.set(res.totalSteps || 0);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1389,9 +1389,31 @@
|
|||
"cron-job-deleted": "Scheduled job deleted successfully",
|
||||
"cron-job-pause-failed": "Failed to pause scheduled job",
|
||||
"cron-job-paused": "Scheduled job paused successfully",
|
||||
"cron-migration-errors": "Migration Errors",
|
||||
"cron-migration-warnings": "Migration Warnings",
|
||||
"cron-no-errors": "No errors to display",
|
||||
"cron-error-severity": "Severity",
|
||||
"cron-error-time": "Time",
|
||||
"cron-error-message": "Error Message",
|
||||
"cron-error-details": "Details",
|
||||
"cron-clear-errors": "Clear All Errors",
|
||||
"cron-retry-failed": "Retry Failed Migrations",
|
||||
"cron-resume-paused": "Resume Paused Migrations",
|
||||
"cron-errors-cleared": "All errors cleared successfully",
|
||||
"cron-no-failed-migrations": "No failed migrations to retry",
|
||||
"cron-no-paused-migrations": "No paused migrations to resume",
|
||||
"cron-migrations-resumed": "Migrations resumed successfully",
|
||||
"cron-migrations-retried": "Failed migrations retried successfully",
|
||||
"complete": "Complete",
|
||||
"idle": "Idle",
|
||||
"filesystem-path-description": "Base path for file storage",
|
||||
"gridfs-enabled": "GridFS Enabled",
|
||||
"gridfs-enabled-description": "Use MongoDB GridFS for file storage",
|
||||
"all-migrations": "All Migrations",
|
||||
"select-migration": "Select Migration",
|
||||
"start": "Start",
|
||||
"pause": "Pause",
|
||||
"stop": "Stop",
|
||||
"migration-pause-failed": "Failed to pause migrations",
|
||||
"migration-paused": "Migrations paused successfully",
|
||||
"migration-progress": "Migration Progress",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { Mongo } from 'meteor/mongo';
|
|||
export const CronJobStatus = new Mongo.Collection('cronJobStatus');
|
||||
export const CronJobSteps = new Mongo.Collection('cronJobSteps');
|
||||
export const CronJobQueue = new Mongo.Collection('cronJobQueue');
|
||||
export const CronJobErrors = new Mongo.Collection('cronJobErrors');
|
||||
|
||||
// Indexes for performance
|
||||
if (Meteor.isServer) {
|
||||
|
|
@ -29,6 +30,12 @@ if (Meteor.isServer) {
|
|||
CronJobQueue._collection.createIndex({ priority: 1, createdAt: 1 });
|
||||
CronJobQueue._collection.createIndex({ status: 1 });
|
||||
CronJobQueue._collection.createIndex({ jobType: 1 });
|
||||
|
||||
// Index for job errors queries
|
||||
CronJobErrors._collection.createIndex({ jobId: 1, createdAt: -1 });
|
||||
CronJobErrors._collection.createIndex({ stepId: 1 });
|
||||
CronJobErrors._collection.createIndex({ severity: 1 });
|
||||
CronJobErrors._collection.createIndex({ createdAt: -1 });
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -146,6 +153,59 @@ class CronJobStorage {
|
|||
}, { sort: { stepIndex: 1 } }).fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save job error to persistent storage
|
||||
*/
|
||||
saveJobError(jobId, errorData) {
|
||||
const now = new Date();
|
||||
const { stepId, stepIndex, error, severity = 'error', context = {} } = errorData;
|
||||
|
||||
CronJobErrors.insert({
|
||||
jobId,
|
||||
stepId,
|
||||
stepIndex,
|
||||
errorMessage: typeof error === 'string' ? error : error.message || 'Unknown error',
|
||||
errorStack: error.stack || null,
|
||||
severity,
|
||||
context,
|
||||
createdAt: now
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get job errors from persistent storage
|
||||
*/
|
||||
getJobErrors(jobId, options = {}) {
|
||||
const { limit = 100, severity = null } = options;
|
||||
|
||||
const query = { jobId };
|
||||
if (severity) {
|
||||
query.severity = severity;
|
||||
}
|
||||
|
||||
return CronJobErrors.find(query, {
|
||||
sort: { createdAt: -1 },
|
||||
limit
|
||||
}).fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all recent errors across all jobs
|
||||
*/
|
||||
getAllRecentErrors(limit = 50) {
|
||||
return CronJobErrors.find({}, {
|
||||
sort: { createdAt: -1 },
|
||||
limit
|
||||
}).fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear errors for a specific job
|
||||
*/
|
||||
clearJobErrors(jobId) {
|
||||
return CronJobErrors.remove({ jobId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Add job to queue
|
||||
*/
|
||||
|
|
@ -379,6 +439,7 @@ class CronJobStorage {
|
|||
CronJobStatus.remove({});
|
||||
CronJobSteps.remove({});
|
||||
CronJobQueue.remove({});
|
||||
CronJobErrors.remove({});
|
||||
|
||||
console.log('All cron job data cleared from storage');
|
||||
return { success: true, message: 'All cron job data cleared' };
|
||||
|
|
|
|||
|
|
@ -6,7 +6,13 @@
|
|||
import { Meteor } from 'meteor/meteor';
|
||||
import { SyncedCron } from 'meteor/quave:synced-cron';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { cronJobStorage } from './cronJobStorage';
|
||||
import { check, Match } from 'meteor/check';
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { cronJobStorage, CronJobStatus } from './cronJobStorage';
|
||||
import Users from '/models/users';
|
||||
import Boards from '/models/boards';
|
||||
import { runEnsureValidSwimlaneIdsMigration } from './migrations/ensureValidSwimlaneIds';
|
||||
|
||||
|
||||
// Server-side reactive variables for cron migration progress
|
||||
export const cronMigrationProgress = new ReactiveVar(0);
|
||||
|
|
@ -15,6 +21,8 @@ export const cronMigrationCurrentStep = new ReactiveVar('');
|
|||
export const cronMigrationSteps = new ReactiveVar([]);
|
||||
export const cronIsMigrating = new ReactiveVar(false);
|
||||
export const cronJobs = new ReactiveVar([]);
|
||||
export const cronMigrationCurrentStepNum = new ReactiveVar(0);
|
||||
export const cronMigrationTotalSteps = new ReactiveVar(0);
|
||||
|
||||
// Board-specific operation tracking
|
||||
export const boardOperations = new ReactiveVar(new Map());
|
||||
|
|
@ -28,6 +36,7 @@ class CronMigrationManager {
|
|||
this.isRunning = false;
|
||||
this.jobProcessor = null;
|
||||
this.processingInterval = null;
|
||||
this.monitorInterval = null;
|
||||
this.pausedJobs = new Map(); // Store paused job configs for per-job pause/resume
|
||||
}
|
||||
|
||||
|
|
@ -135,6 +144,17 @@ class CronMigrationManager {
|
|||
schedule: 'every 1 minute',
|
||||
status: 'stopped'
|
||||
},
|
||||
{
|
||||
id: 'ensure-valid-swimlane-ids',
|
||||
name: 'Validate Swimlane IDs',
|
||||
description: 'Ensuring all cards and lists have valid swimlaneId references',
|
||||
weight: 2,
|
||||
completed: false,
|
||||
progress: 0,
|
||||
cronName: 'migration_swimlane_ids',
|
||||
schedule: 'every 1 minute',
|
||||
status: 'stopped'
|
||||
},
|
||||
{
|
||||
id: 'add-sort-checklists',
|
||||
name: 'Checklist Sorting',
|
||||
|
|
@ -447,6 +467,18 @@ class CronMigrationManager {
|
|||
async executeMigrationStep(jobId, stepIndex, stepData, stepId) {
|
||||
const { name, duration } = stepData;
|
||||
|
||||
// Check if this is the star count migration that needs real implementation
|
||||
if (stepId === 'denormalize-star-number-per-board') {
|
||||
await this.executeDenormalizeStarCount(jobId, stepIndex, stepData);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is the swimlane validation migration
|
||||
if (stepId === 'ensure-valid-swimlane-ids') {
|
||||
await this.executeEnsureValidSwimlaneIds(jobId, stepIndex, stepData);
|
||||
return;
|
||||
}
|
||||
|
||||
// Simulate step execution with progress updates for other migrations
|
||||
const progressSteps = 10;
|
||||
for (let i = 0; i <= progressSteps; i++) {
|
||||
|
|
@ -463,6 +495,173 @@ class CronMigrationManager {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the denormalize star count migration
|
||||
*/
|
||||
async executeDenormalizeStarCount(jobId, stepIndex, stepData) {
|
||||
try {
|
||||
const { name } = stepData;
|
||||
|
||||
// Update progress: Starting
|
||||
cronJobStorage.saveJobStep(jobId, stepIndex, {
|
||||
progress: 0,
|
||||
currentAction: 'Counting starred boards across all users...'
|
||||
});
|
||||
|
||||
// Build a map of boardId -> star count
|
||||
const starCounts = new Map();
|
||||
|
||||
// Get all users with starred boards
|
||||
const users = Users.find(
|
||||
{ 'profile.starredBoards': { $exists: true, $ne: [] } },
|
||||
{ fields: { 'profile.starredBoards': 1 } }
|
||||
).fetch();
|
||||
|
||||
// Update progress: Counting
|
||||
cronJobStorage.saveJobStep(jobId, stepIndex, {
|
||||
progress: 20,
|
||||
currentAction: `Analyzing ${users.length} users with starred boards...`
|
||||
});
|
||||
|
||||
// Count stars for each board
|
||||
users.forEach(user => {
|
||||
const starredBoards = (user.profile && user.profile.starredBoards) || [];
|
||||
starredBoards.forEach(boardId => {
|
||||
starCounts.set(boardId, (starCounts.get(boardId) || 0) + 1);
|
||||
});
|
||||
});
|
||||
|
||||
// Update progress: Updating boards
|
||||
cronJobStorage.saveJobStep(jobId, stepIndex, {
|
||||
progress: 50,
|
||||
currentAction: `Updating star counts for ${starCounts.size} boards...`
|
||||
});
|
||||
|
||||
// Update all boards with their star counts
|
||||
let updatedCount = 0;
|
||||
const totalBoards = starCounts.size;
|
||||
|
||||
for (const [boardId, count] of starCounts.entries()) {
|
||||
try {
|
||||
Boards.update(boardId, { $set: { stars: count } });
|
||||
updatedCount++;
|
||||
|
||||
// Update progress periodically
|
||||
if (updatedCount % 10 === 0 || updatedCount === totalBoards) {
|
||||
const progress = 50 + Math.round((updatedCount / totalBoards) * 40);
|
||||
cronJobStorage.saveJobStep(jobId, stepIndex, {
|
||||
progress,
|
||||
currentAction: `Updated ${updatedCount}/${totalBoards} boards...`
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to update star count for board ${boardId}:`, error);
|
||||
// Store error in database
|
||||
cronJobStorage.saveJobError(jobId, {
|
||||
stepId: 'denormalize-star-number-per-board',
|
||||
stepIndex,
|
||||
error,
|
||||
severity: 'warning',
|
||||
context: { boardId, operation: 'update_star_count' }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Also set stars to 0 for boards that have no stars
|
||||
cronJobStorage.saveJobStep(jobId, stepIndex, {
|
||||
progress: 90,
|
||||
currentAction: 'Initializing boards with no stars...'
|
||||
});
|
||||
|
||||
const boardsWithoutStars = Boards.find(
|
||||
{
|
||||
$or: [
|
||||
{ stars: { $exists: false } },
|
||||
{ stars: null }
|
||||
]
|
||||
},
|
||||
{ fields: { _id: 1 } }
|
||||
).fetch();
|
||||
|
||||
boardsWithoutStars.forEach(board => {
|
||||
// Only set to 0 if not already counted
|
||||
if (!starCounts.has(board._id)) {
|
||||
try {
|
||||
Boards.update(board._id, { $set: { stars: 0 } });
|
||||
} catch (error) {
|
||||
console.error(`Failed to initialize star count for board ${board._id}:`, error);
|
||||
// Store error in database
|
||||
cronJobStorage.saveJobError(jobId, {
|
||||
stepId: 'denormalize-star-number-per-board',
|
||||
stepIndex,
|
||||
error,
|
||||
severity: 'warning',
|
||||
context: { boardId: board._id, operation: 'initialize_star_count' }
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Complete
|
||||
cronJobStorage.saveJobStep(jobId, stepIndex, {
|
||||
progress: 100,
|
||||
currentAction: `Migration complete: Updated ${updatedCount} boards with star counts`
|
||||
});
|
||||
|
||||
console.log(`Star count migration completed: ${updatedCount} boards updated, ${boardsWithoutStars.length} initialized to 0`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error executing denormalize star count migration:', error);
|
||||
// Store error in database
|
||||
cronJobStorage.saveJobError(jobId, {
|
||||
stepId: 'denormalize-star-number-per-board',
|
||||
stepIndex,
|
||||
error,
|
||||
severity: 'error',
|
||||
context: { operation: 'denormalize_star_count_migration' }
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the ensure valid swimlane IDs migration
|
||||
*/
|
||||
async executeEnsureValidSwimlaneIds(jobId, stepIndex, stepData) {
|
||||
try {
|
||||
const { name } = stepData;
|
||||
|
||||
// Update progress: Starting
|
||||
cronJobStorage.saveJobStep(jobId, stepIndex, {
|
||||
progress: 0,
|
||||
currentAction: 'Starting swimlane ID validation...'
|
||||
});
|
||||
|
||||
// Run the migration function
|
||||
const result = await runEnsureValidSwimlaneIdsMigration();
|
||||
|
||||
// Update progress: Complete
|
||||
cronJobStorage.saveJobStep(jobId, stepIndex, {
|
||||
progress: 100,
|
||||
currentAction: `Migration complete: Fixed ${result.cardsFixed || 0} cards, ${result.listsFixed || 0} lists, rescued ${result.cardsRescued || 0} orphaned cards`
|
||||
});
|
||||
|
||||
console.log(`Swimlane ID validation migration completed:`, result);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error executing swimlane ID validation migration:', error);
|
||||
// Store error in database
|
||||
cronJobStorage.saveJobError(jobId, {
|
||||
stepId: 'ensure-valid-swimlane-ids',
|
||||
stepIndex,
|
||||
error,
|
||||
severity: 'error',
|
||||
context: { operation: 'ensure_valid_swimlane_ids_migration' }
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Execute a board operation job
|
||||
|
|
@ -697,7 +896,10 @@ class CronMigrationManager {
|
|||
|
||||
this.isRunning = true;
|
||||
cronIsMigrating.set(true);
|
||||
cronMigrationStatus.set('Adding migrations to job queue...');
|
||||
cronMigrationStatus.set('Starting...');
|
||||
cronMigrationProgress.set(0);
|
||||
cronMigrationCurrentStepNum.set(0);
|
||||
cronMigrationTotalSteps.set(0);
|
||||
this.startTime = Date.now();
|
||||
|
||||
try {
|
||||
|
|
@ -737,7 +939,7 @@ class CronMigrationManager {
|
|||
});
|
||||
}
|
||||
|
||||
cronMigrationStatus.set('Migrations added to queue. Processing will begin shortly...');
|
||||
// Status will be updated by monitorMigrationProgress
|
||||
|
||||
// Start monitoring progress
|
||||
this.monitorMigrationProgress();
|
||||
|
|
@ -750,45 +952,119 @@ class CronMigrationManager {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a specific migration by index
|
||||
*/
|
||||
async startSpecificMigration(migrationIndex) {
|
||||
if (this.isRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
const step = this.migrationSteps[migrationIndex];
|
||||
if (!step) {
|
||||
throw new Meteor.Error('invalid-migration', 'Migration not found');
|
||||
}
|
||||
|
||||
this.isRunning = true;
|
||||
cronIsMigrating.set(true);
|
||||
cronMigrationStatus.set('Starting...');
|
||||
cronMigrationProgress.set(0);
|
||||
cronMigrationCurrentStepNum.set(1);
|
||||
cronMigrationTotalSteps.set(1);
|
||||
this.startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Remove cron job to prevent conflicts
|
||||
try {
|
||||
SyncedCron.remove(step.cronName);
|
||||
} catch (error) {
|
||||
// Ignore errors if cron job doesn't exist
|
||||
}
|
||||
|
||||
// Add single migration step to the job queue
|
||||
const jobId = `migration_${step.id}_${Date.now()}`;
|
||||
cronJobStorage.addToQueue(jobId, 'migration', step.weight, {
|
||||
stepId: step.id,
|
||||
stepName: step.name,
|
||||
stepDescription: step.description
|
||||
});
|
||||
|
||||
// Save initial job status
|
||||
cronJobStorage.saveJobStatus(jobId, {
|
||||
jobType: 'migration',
|
||||
status: 'pending',
|
||||
progress: 0,
|
||||
stepId: step.id,
|
||||
stepName: step.name,
|
||||
stepDescription: step.description
|
||||
});
|
||||
|
||||
// Status will be updated by monitorMigrationProgress
|
||||
|
||||
// Start monitoring progress
|
||||
this.monitorMigrationProgress();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to start migration:', error);
|
||||
cronMigrationStatus.set(`Failed to start migration: ${error.message}`);
|
||||
cronIsMigrating.set(false);
|
||||
this.isRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitor migration progress
|
||||
*/
|
||||
monitorMigrationProgress() {
|
||||
const monitorInterval = Meteor.setInterval(() => {
|
||||
// Clear any existing monitor interval
|
||||
if (this.monitorInterval) {
|
||||
Meteor.clearInterval(this.monitorInterval);
|
||||
}
|
||||
|
||||
this.monitorInterval = Meteor.setInterval(() => {
|
||||
const stats = cronJobStorage.getQueueStats();
|
||||
const incompleteJobs = cronJobStorage.getIncompleteJobs();
|
||||
|
||||
// Update progress
|
||||
// Check if all migrations are completed first
|
||||
const totalJobs = stats.total;
|
||||
const completedJobs = stats.completed;
|
||||
const progress = totalJobs > 0 ? Math.round((completedJobs / totalJobs) * 100) : 0;
|
||||
|
||||
if (stats.completed === totalJobs && totalJobs > 0 && stats.running === 0) {
|
||||
// All migrations completed - immediately clear isMigrating to hide progress
|
||||
cronIsMigrating.set(false);
|
||||
cronMigrationStatus.set('All migrations completed successfully!');
|
||||
cronMigrationProgress.set(0);
|
||||
cronMigrationCurrentStep.set('');
|
||||
cronMigrationCurrentStepNum.set(0);
|
||||
cronMigrationTotalSteps.set(0);
|
||||
|
||||
// Clear status message after delay
|
||||
setTimeout(() => {
|
||||
cronMigrationStatus.set('');
|
||||
}, 5000);
|
||||
|
||||
Meteor.clearInterval(this.monitorInterval);
|
||||
this.monitorInterval = null;
|
||||
return; // Exit early to avoid setting progress to 100%
|
||||
}
|
||||
|
||||
// Update progress for active migrations
|
||||
const progress = totalJobs > 0 ? Math.round((completedJobs / totalJobs) * 100) : 0;
|
||||
cronMigrationProgress.set(progress);
|
||||
cronMigrationTotalSteps.set(totalJobs);
|
||||
const currentStepNum = completedJobs + (stats.running > 0 ? 1 : 0);
|
||||
cronMigrationCurrentStepNum.set(currentStepNum);
|
||||
|
||||
// Update status
|
||||
if (stats.running > 0) {
|
||||
const runningJob = incompleteJobs.find(job => job.status === 'running');
|
||||
if (runningJob) {
|
||||
cronMigrationCurrentStep.set(runningJob.stepName || 'Processing migration...');
|
||||
cronMigrationStatus.set(`Running: ${runningJob.stepName || 'Migration in progress'}`);
|
||||
cronMigrationStatus.set(`Running: ${currentStepNum}/${totalJobs} ${runningJob.stepName || 'Migration in progress'}`);
|
||||
cronMigrationCurrentStep.set('');
|
||||
}
|
||||
} else if (stats.pending > 0) {
|
||||
cronMigrationStatus.set(`${stats.pending} migrations pending in queue`);
|
||||
cronMigrationCurrentStep.set('Waiting for available resources...');
|
||||
} else if (stats.completed === totalJobs && totalJobs > 0) {
|
||||
// All migrations completed
|
||||
cronMigrationStatus.set('All migrations completed successfully!');
|
||||
cronMigrationProgress.set(100);
|
||||
cronMigrationCurrentStep.set('');
|
||||
|
||||
// Clear status after delay
|
||||
setTimeout(() => {
|
||||
cronIsMigrating.set(false);
|
||||
cronMigrationStatus.set('');
|
||||
cronMigrationProgress.set(0);
|
||||
}, 3000);
|
||||
|
||||
Meteor.clearInterval(monitorInterval);
|
||||
}
|
||||
}, 2000); // Check every 2 seconds
|
||||
}
|
||||
|
|
@ -1380,6 +1656,120 @@ class CronMigrationManager {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause all migrations
|
||||
*/
|
||||
pauseAllMigrations() {
|
||||
this.isRunning = false;
|
||||
cronIsMigrating.set(false);
|
||||
cronMigrationStatus.set('Migrations paused');
|
||||
|
||||
// Update all pending jobs in queue to paused
|
||||
const pendingJobs = cronJobStorage.getIncompleteJobs();
|
||||
pendingJobs.forEach(job => {
|
||||
if (job.status === 'pending' || job.status === 'running') {
|
||||
cronJobStorage.updateQueueStatus(job.jobId, 'paused');
|
||||
cronJobStorage.saveJobStatus(job.jobId, { status: 'paused' });
|
||||
}
|
||||
});
|
||||
|
||||
return { success: true, message: 'All migrations paused' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume all paused migrations
|
||||
*/
|
||||
resumeAllMigrations() {
|
||||
// Find all paused jobs and resume them
|
||||
const pausedJobs = CronJobStatus.find({ status: 'paused' }).fetch();
|
||||
|
||||
if (pausedJobs.length === 0) {
|
||||
return { success: false, message: 'No paused migrations to resume' };
|
||||
}
|
||||
|
||||
pausedJobs.forEach(job => {
|
||||
cronJobStorage.updateQueueStatus(job.jobId, 'pending');
|
||||
cronJobStorage.saveJobStatus(job.jobId, { status: 'pending' });
|
||||
});
|
||||
|
||||
this.isRunning = true;
|
||||
cronIsMigrating.set(true);
|
||||
cronMigrationStatus.set('Resuming migrations...');
|
||||
|
||||
// Restart monitoring
|
||||
this.monitorMigrationProgress();
|
||||
|
||||
return { success: true, message: `Resumed ${pausedJobs.length} migrations` };
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry failed migrations
|
||||
*/
|
||||
retryFailedMigrations() {
|
||||
const failedJobs = CronJobStatus.find({ status: 'failed' }).fetch();
|
||||
|
||||
if (failedJobs.length === 0) {
|
||||
return { success: false, message: 'No failed migrations to retry' };
|
||||
}
|
||||
|
||||
// Clear errors for failed jobs
|
||||
failedJobs.forEach(job => {
|
||||
cronJobStorage.clearJobErrors(job.jobId);
|
||||
cronJobStorage.updateQueueStatus(job.jobId, 'pending');
|
||||
cronJobStorage.saveJobStatus(job.jobId, {
|
||||
status: 'pending',
|
||||
progress: 0,
|
||||
error: null
|
||||
});
|
||||
});
|
||||
|
||||
if (!this.isRunning) {
|
||||
this.isRunning = true;
|
||||
cronIsMigrating.set(true);
|
||||
cronMigrationStatus.set('Retrying failed migrations...');
|
||||
this.monitorMigrationProgress();
|
||||
}
|
||||
|
||||
return { success: true, message: `Retrying ${failedJobs.length} failed migrations` };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all migration errors
|
||||
*/
|
||||
getAllMigrationErrors(limit = 50) {
|
||||
return cronJobStorage.getAllRecentErrors(limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get errors for a specific job
|
||||
*/
|
||||
getJobErrors(jobId, options = {}) {
|
||||
return cronJobStorage.getJobErrors(jobId, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get migration stats including errors
|
||||
*/
|
||||
getMigrationStats() {
|
||||
const queueStats = cronJobStorage.getQueueStats();
|
||||
const allErrors = cronJobStorage.getAllRecentErrors(100);
|
||||
const errorsByJob = {};
|
||||
|
||||
allErrors.forEach(error => {
|
||||
if (!errorsByJob[error.jobId]) {
|
||||
errorsByJob[error.jobId] = [];
|
||||
}
|
||||
errorsByJob[error.jobId].push(error);
|
||||
});
|
||||
|
||||
return {
|
||||
...queueStats,
|
||||
totalErrors: allErrors.length,
|
||||
errorsByJob,
|
||||
recentErrors: allErrors.slice(0, 10)
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
|
|
@ -1405,6 +1795,20 @@ Meteor.methods({
|
|||
return cronMigrationManager.startAllMigrations();
|
||||
},
|
||||
|
||||
'cron.startSpecificMigration'(migrationIndex) {
|
||||
check(migrationIndex, Number);
|
||||
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.startSpecificMigration(migrationIndex);
|
||||
},
|
||||
|
||||
'cron.startJob'(cronName) {
|
||||
const userId = this.userId;
|
||||
if (!userId) {
|
||||
|
|
@ -1511,10 +1915,95 @@ Meteor.methods({
|
|||
status: cronMigrationStatus.get(),
|
||||
currentStep: cronMigrationCurrentStep.get(),
|
||||
steps: cronMigrationSteps.get(),
|
||||
isMigrating: cronIsMigrating.get()
|
||||
isMigrating: cronIsMigrating.get(),
|
||||
currentStepNum: cronMigrationCurrentStepNum.get(),
|
||||
totalSteps: cronMigrationTotalSteps.get()
|
||||
};
|
||||
},
|
||||
|
||||
'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');
|
||||
}
|
||||
|
||||
return cronMigrationManager.pauseAllMigrations();
|
||||
},
|
||||
|
||||
'cron.resumeAllMigrations'() {
|
||||
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.resumeAllMigrations();
|
||||
},
|
||||
|
||||
'cron.retryFailedMigrations'() {
|
||||
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.retryFailedMigrations();
|
||||
},
|
||||
|
||||
'cron.getAllMigrationErrors'(limit = 50) {
|
||||
check(limit, Match.Optional(Number));
|
||||
|
||||
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.getAllMigrationErrors(limit);
|
||||
},
|
||||
|
||||
'cron.getJobErrors'(jobId, options = {}) {
|
||||
check(jobId, String);
|
||||
check(options, Match.Optional(Object));
|
||||
|
||||
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.getJobErrors(jobId, options);
|
||||
},
|
||||
|
||||
'cron.getMigrationStats'() {
|
||||
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.getMigrationStats();
|
||||
},
|
||||
|
||||
'cron.startBoardOperation'(boardId, operationType, operationData) {
|
||||
const userId = this.userId;
|
||||
if (!userId) {
|
||||
|
|
@ -1747,6 +2236,12 @@ Meteor.methods({
|
|||
throw new Meteor.Error('not-authorized', 'Admin access required');
|
||||
}
|
||||
|
||||
// Clear monitor interval first to prevent status override
|
||||
if (cronMigrationManager.monitorInterval) {
|
||||
Meteor.clearInterval(cronMigrationManager.monitorInterval);
|
||||
cronMigrationManager.monitorInterval = null;
|
||||
}
|
||||
|
||||
// Stop all running and pending jobs
|
||||
const incompleteJobs = cronJobStorage.getIncompleteJobs();
|
||||
incompleteJobs.forEach(job => {
|
||||
|
|
@ -1757,11 +2252,19 @@ Meteor.methods({
|
|||
});
|
||||
});
|
||||
|
||||
// Reset migration state
|
||||
// Reset migration state immediately
|
||||
cronMigrationManager.isRunning = false;
|
||||
cronIsMigrating.set(false);
|
||||
cronMigrationStatus.set('All migrations stopped');
|
||||
cronMigrationProgress.set(0);
|
||||
cronMigrationCurrentStep.set('');
|
||||
cronMigrationCurrentStepNum.set(0);
|
||||
cronMigrationTotalSteps.set(0);
|
||||
cronMigrationStatus.set('All migrations stopped');
|
||||
|
||||
// Clear status message after delay
|
||||
setTimeout(() => {
|
||||
cronMigrationStatus.set('');
|
||||
}, 3000);
|
||||
|
||||
return { success: true, message: 'All migrations stopped' };
|
||||
},
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@
|
|||
// Helper collection to track migrations - must be defined first
|
||||
const Migrations = new Mongo.Collection('migrations');
|
||||
|
||||
// DISABLED: This migration now runs from Admin Panel / Cron / Run All Migrations
|
||||
// Instead of running automatically on startup
|
||||
/*
|
||||
Meteor.startup(() => {
|
||||
// Only run on server
|
||||
if (!Meteor.isServer) return;
|
||||
|
|
@ -26,11 +29,16 @@ Meteor.startup(() => {
|
|||
}
|
||||
|
||||
console.log(`Running migration: ${MIGRATION_NAME} v${MIGRATION_VERSION}`);
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get or create a "Rescued Data" swimlane for a board
|
||||
*/
|
||||
function getOrCreateRescuedSwimlane(boardId) {
|
||||
// Export migration functions for use by cron migration manager
|
||||
export const MIGRATION_NAME = 'ensure-valid-swimlane-ids';
|
||||
export const MIGRATION_VERSION = 1;
|
||||
|
||||
/**
|
||||
* Get or create a "Rescued Data" swimlane for a board
|
||||
*/
|
||||
function getOrCreateRescuedSwimlane(boardId) {
|
||||
const board = Boards.findOne(boardId);
|
||||
if (!board) return null;
|
||||
|
||||
|
|
@ -243,6 +251,72 @@ Meteor.startup(() => {
|
|||
});
|
||||
}
|
||||
|
||||
// Exported function to run the migration from cron
|
||||
export function runEnsureValidSwimlaneIdsMigration() {
|
||||
const existingMigration = Migrations.findOne({ name: MIGRATION_NAME });
|
||||
if (existingMigration && existingMigration.version >= MIGRATION_VERSION) {
|
||||
console.log(`Migration ${MIGRATION_NAME} already completed`);
|
||||
return { alreadyCompleted: true, ...existingMigration.results };
|
||||
}
|
||||
|
||||
console.log(`Running migration: ${MIGRATION_NAME} v${MIGRATION_VERSION}`);
|
||||
|
||||
try {
|
||||
// Run all fix operations
|
||||
const cardResults = fixCardsWithoutSwimlaneId();
|
||||
const listResults = fixListsWithoutSwimlaneId();
|
||||
const rescueResults = rescueOrphanedCards();
|
||||
|
||||
console.log('Migration results:');
|
||||
console.log(`- Fixed ${cardResults.fixedCount} cards without swimlaneId`);
|
||||
console.log(`- Fixed ${listResults.fixedCount} lists without swimlaneId`);
|
||||
console.log(`- Rescued ${rescueResults.rescuedCount} orphaned cards`);
|
||||
|
||||
// Record migration completion
|
||||
Migrations.upsert(
|
||||
{ name: MIGRATION_NAME },
|
||||
{
|
||||
$set: {
|
||||
name: MIGRATION_NAME,
|
||||
version: MIGRATION_VERSION,
|
||||
completedAt: new Date(),
|
||||
results: {
|
||||
cardsFixed: cardResults.fixedCount,
|
||||
listsFixed: listResults.fixedCount,
|
||||
cardsRescued: rescueResults.rescuedCount,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
console.log(`Migration ${MIGRATION_NAME} completed successfully`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
cardsFixed: cardResults.fixedCount,
|
||||
listsFixed: listResults.fixedCount,
|
||||
cardsRescued: rescueResults.rescuedCount,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Migration ${MIGRATION_NAME} failed:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Install validation hooks on startup (always run these for data integrity)
|
||||
Meteor.startup(() => {
|
||||
if (!Meteor.isServer) return;
|
||||
|
||||
try {
|
||||
addSwimlaneIdValidationHooks();
|
||||
console.log('SwimlaneId validation hooks installed');
|
||||
} catch (error) {
|
||||
console.error('Failed to install swimlaneId validation hooks:', error);
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
// OLD AUTO-RUN CODE - DISABLED
|
||||
try {
|
||||
// Run all fix operations
|
||||
const cardResults = fixCardsWithoutSwimlaneId();
|
||||
|
|
@ -284,3 +358,4 @@ Meteor.startup(() => {
|
|||
console.error('Failed to install swimlaneId validation hooks:', error);
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue