Fix DB migration from 8.19 to 8.21 stuck forever.

Thanks to MaccabeeY and xet7 !

Fixes #6078
This commit is contained in:
Lauri Ojansivu 2026-01-21 00:56:42 +02:00
parent e177bf54e2
commit a31a615da6
9 changed files with 869 additions and 71 deletions

View file

@ -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);
}

View file

@ -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'}}

View file

@ -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'}}

View file

@ -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);