Run database migrations when opening board. Not when updating WeKan.

Thanks to xet7 !
This commit is contained in:
Lauri Ojansivu 2025-10-11 19:23:47 +03:00
parent ea30612013
commit 2b5c56484a
20 changed files with 2508 additions and 118 deletions

View file

@ -4,3 +4,11 @@ if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/pwa-service-worker.js');
});
}
// Import board converter for on-demand conversion
import '/imports/lib/boardConverter';
import '/imports/components/boardConversionProgress';
// Import migration manager and progress UI
import '/imports/lib/migrationManager';
import '/imports/components/migrationProgress';

View file

@ -0,0 +1,184 @@
/* Board Conversion Progress Styles */
.board-conversion-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
z-index: 9999;
display: none;
align-items: center;
justify-content: center;
}
.board-conversion-overlay.active {
display: flex;
}
.board-conversion-modal {
background: white;
border-radius: 8px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
max-width: 500px;
width: 90%;
max-height: 80vh;
overflow: hidden;
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.board-conversion-header {
padding: 20px 24px 16px;
border-bottom: 1px solid #e0e0e0;
text-align: center;
}
.board-conversion-header h3 {
margin: 0 0 8px 0;
color: #333;
font-size: 20px;
font-weight: 500;
}
.board-conversion-header h3 i {
margin-right: 8px;
color: #2196F3;
}
.board-conversion-header p {
margin: 0;
color: #666;
font-size: 14px;
}
.board-conversion-content {
padding: 24px;
}
.conversion-progress {
margin-bottom: 20px;
}
.progress-bar {
width: 100%;
height: 8px;
background-color: #e0e0e0;
border-radius: 4px;
overflow: hidden;
margin-bottom: 8px;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #2196F3, #21CBF3);
border-radius: 4px;
transition: width 0.3s ease;
position: relative;
}
.progress-fill::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.3),
transparent
);
animation: shimmer 2s infinite;
}
@keyframes shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
.progress-text {
text-align: center;
font-weight: 600;
color: #2196F3;
font-size: 16px;
}
.conversion-status {
text-align: center;
margin-bottom: 16px;
color: #333;
font-size: 16px;
}
.conversion-status i {
margin-right: 8px;
color: #2196F3;
}
.conversion-time {
text-align: center;
color: #666;
font-size: 14px;
background-color: #f5f5f5;
padding: 8px 12px;
border-radius: 4px;
margin-bottom: 16px;
}
.conversion-time i {
margin-right: 6px;
color: #FF9800;
}
.board-conversion-footer {
padding: 16px 24px 20px;
border-top: 1px solid #e0e0e0;
background-color: #f9f9f9;
}
.conversion-info {
text-align: center;
color: #666;
font-size: 13px;
line-height: 1.4;
}
.conversion-info i {
margin-right: 6px;
color: #2196F3;
}
/* Responsive design */
@media (max-width: 600px) {
.board-conversion-modal {
width: 95%;
margin: 20px;
}
.board-conversion-header,
.board-conversion-content,
.board-conversion-footer {
padding-left: 16px;
padding-right: 16px;
}
.board-conversion-header h3 {
font-size: 18px;
}
}

View file

@ -0,0 +1,27 @@
template(name="boardConversionProgress")
.board-conversion-overlay(class="{{#if isConverting}}active{{/if}}")
.board-conversion-modal
.board-conversion-header
h3
i.fa.fa-cogs
| {{_ 'converting-board'}}
p {{_ 'converting-board-description'}}
.board-conversion-content
.conversion-progress
.progress-bar
.progress-fill(style="width: {{conversionProgress}}%")
.progress-text {{conversionProgress}}%
.conversion-status
i.fa.fa-spinner.fa-spin
| {{conversionStatus}}
.conversion-time(style="{{#unless conversionEstimatedTime}}display: none;{{/unless}}")
i.fa.fa-clock-o
| {{_ 'estimated-time-remaining'}}: {{conversionEstimatedTime}}
.board-conversion-footer
.conversion-info
i.fa.fa-info-circle
| {{_ 'conversion-info-text'}}

View file

@ -0,0 +1,31 @@
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import { boardConverter } from '/imports/lib/boardConverter';
Template.boardConversionProgress.helpers({
isConverting() {
return boardConverter.isConverting.get();
},
conversionProgress() {
return boardConverter.conversionProgress.get();
},
conversionStatus() {
return boardConverter.conversionStatus.get();
},
conversionEstimatedTime() {
return boardConverter.conversionEstimatedTime.get();
}
});
Template.boardConversionProgress.onCreated(function() {
// Subscribe to conversion state changes
this.autorun(() => {
boardConverter.isConverting.get();
boardConverter.conversionProgress.get();
boardConverter.conversionStatus.get();
boardConverter.conversionEstimatedTime.get();
});
});

View file

@ -1,5 +1,9 @@
template(name="board")
if isBoardReady.get
if isMigrating
+migrationProgress
else if isConverting
+boardConversionProgress
else if isBoardReady.get
if currentBoard
if onlyShowCurrentCard
+cardDetails(currentCard)

View file

@ -1,6 +1,8 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
import dragscroll from '@wekanteam/dragscroll';
import { boardConverter } from '/imports/lib/boardConverter';
import { migrationManager } from '/imports/lib/migrationManager';
const subManager = new SubsManager();
const { calculateIndex } = Utils;
@ -9,6 +11,8 @@ const swimlaneWhileSortingHeight = 150;
BlazeComponent.extendComponent({
onCreated() {
this.isBoardReady = new ReactiveVar(false);
this.isConverting = new ReactiveVar(false);
this.isMigrating = new ReactiveVar(false);
// The pattern we use to manually handle data loading is described here:
// https://kadira.io/academy/meteor-routing-guide/content/subscriptions-and-data-management/using-subs-manager
@ -20,12 +24,49 @@ BlazeComponent.extendComponent({
const handle = subManager.subscribe('board', currentBoardId, false);
Tracker.nonreactive(() => {
Tracker.autorun(() => {
this.isBoardReady.set(handle.ready());
if (handle.ready()) {
// Check if board needs conversion
this.checkAndConvertBoard(currentBoardId);
} else {
this.isBoardReady.set(false);
}
});
});
});
},
async checkAndConvertBoard(boardId) {
try {
// First check if migrations need to be run
if (migrationManager.needsMigration()) {
this.isMigrating.set(true);
await migrationManager.startMigration();
this.isMigrating.set(false);
}
// Then check if board needs conversion
if (boardConverter.needsConversion(boardId)) {
this.isConverting.set(true);
const success = await boardConverter.convertBoard(boardId);
this.isConverting.set(false);
if (success) {
this.isBoardReady.set(true);
} else {
console.error('Board conversion failed');
this.isBoardReady.set(true); // Still show board even if conversion failed
}
} else {
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
}
},
onlyShowCurrentCard() {
return Utils.isMiniScreen() && Utils.getCurrentCardId(true);
},
@ -33,6 +74,14 @@ BlazeComponent.extendComponent({
goHome() {
FlowRouter.go('home');
},
isConverting() {
return this.isConverting.get();
},
isMigrating() {
return this.isMigrating.get();
},
}).register('board');
BlazeComponent.extendComponent({

View file

@ -77,6 +77,8 @@ template(name="defaultLayout")
| {{{afterBodyStart}}}
+Template.dynamic(template=content)
| {{{beforeBodyEnd}}}
+migrationProgress
+boardConversionProgress
if (Modal.isOpen)
#modal
.overlay

View file

@ -0,0 +1,372 @@
/* Migration Progress Styles */
.migration-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
z-index: 10000;
display: none;
align-items: center;
justify-content: center;
overflow-y: auto;
}
.migration-overlay.active {
display: flex;
}
.migration-modal {
background: white;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
max-width: 800px;
width: 95%;
max-height: 90vh;
overflow: hidden;
animation: slideInScale 0.4s ease-out;
margin: 20px;
}
@keyframes slideInScale {
from {
opacity: 0;
transform: translateY(-30px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.migration-header {
padding: 24px 32px 20px;
border-bottom: 2px solid #e0e0e0;
text-align: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.migration-header h3 {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 600;
}
.migration-header h3 i {
margin-right: 12px;
color: #FFD700;
}
.migration-header p {
margin: 0;
font-size: 16px;
opacity: 0.9;
}
.migration-content {
padding: 24px 32px;
max-height: 60vh;
overflow-y: auto;
}
.migration-overview {
margin-bottom: 32px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
border-left: 4px solid #667eea;
}
.overall-progress {
margin-bottom: 20px;
}
.progress-bar {
width: 100%;
height: 12px;
background-color: #e0e0e0;
border-radius: 6px;
overflow: hidden;
margin-bottom: 8px;
position: relative;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea, #764ba2);
border-radius: 6px;
transition: width 0.3s ease;
position: relative;
}
.progress-fill::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.4),
transparent
);
animation: shimmer 2s infinite;
}
@keyframes shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
.progress-text {
text-align: center;
font-weight: 700;
color: #667eea;
font-size: 18px;
}
.progress-label {
text-align: center;
color: #666;
font-size: 14px;
margin-top: 4px;
}
.current-step {
text-align: center;
color: #333;
font-size: 16px;
font-weight: 500;
margin-bottom: 16px;
}
.current-step i {
margin-right: 8px;
color: #667eea;
}
.estimated-time {
text-align: center;
color: #666;
font-size: 14px;
background-color: #fff3cd;
padding: 8px 12px;
border-radius: 4px;
border: 1px solid #ffeaa7;
}
.estimated-time i {
margin-right: 6px;
color: #f39c12;
}
.migration-steps {
margin-bottom: 24px;
}
.migration-steps h4 {
margin: 0 0 16px 0;
color: #333;
font-size: 18px;
font-weight: 600;
}
.steps-list {
max-height: 300px;
overflow-y: auto;
border: 1px solid #e0e0e0;
border-radius: 8px;
}
.migration-step {
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
transition: all 0.3s ease;
}
.migration-step:last-child {
border-bottom: none;
}
.migration-step.completed {
background-color: #d4edda;
border-left: 4px solid #28a745;
}
.migration-step.current {
background-color: #cce7ff;
border-left: 4px solid #667eea;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(102, 126, 234, 0.4);
}
70% {
box-shadow: 0 0 0 10px rgba(102, 126, 234, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(102, 126, 234, 0);
}
}
.step-header {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.step-icon {
margin-right: 12px;
font-size: 18px;
width: 24px;
text-align: center;
}
.step-icon i.fa-check-circle {
color: #28a745;
}
.step-icon i.fa-cog.fa-spin {
color: #667eea;
}
.step-icon i.fa-circle-o {
color: #ccc;
}
.step-info {
flex: 1;
}
.step-name {
font-weight: 600;
color: #333;
font-size: 14px;
margin-bottom: 2px;
}
.step-description {
color: #666;
font-size: 12px;
line-height: 1.3;
}
.step-progress {
text-align: right;
min-width: 40px;
}
.step-progress .progress-text {
font-size: 12px;
font-weight: 600;
}
.step-progress-bar {
width: 100%;
height: 4px;
background-color: #e0e0e0;
border-radius: 2px;
overflow: hidden;
margin-top: 8px;
}
.step-progress-bar .progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea, #764ba2);
border-radius: 2px;
transition: width 0.3s ease;
}
.migration-status {
text-align: center;
color: #333;
font-size: 16px;
background-color: #e3f2fd;
padding: 12px 16px;
border-radius: 6px;
border: 1px solid #bbdefb;
margin-bottom: 16px;
}
.migration-status i {
margin-right: 8px;
color: #2196f3;
}
.migration-footer {
padding: 16px 32px 24px;
border-top: 1px solid #e0e0e0;
background-color: #f8f9fa;
}
.migration-info {
text-align: center;
color: #666;
font-size: 13px;
line-height: 1.4;
margin-bottom: 8px;
}
.migration-info i {
margin-right: 6px;
color: #667eea;
}
.migration-warning {
text-align: center;
color: #856404;
font-size: 12px;
line-height: 1.3;
background-color: #fff3cd;
padding: 8px 12px;
border-radius: 4px;
border: 1px solid #ffeaa7;
}
.migration-warning i {
margin-right: 6px;
color: #f39c12;
}
/* Responsive design */
@media (max-width: 768px) {
.migration-modal {
width: 98%;
margin: 10px;
}
.migration-header,
.migration-content,
.migration-footer {
padding-left: 16px;
padding-right: 16px;
}
.migration-header h3 {
font-size: 20px;
}
.step-header {
flex-direction: column;
align-items: flex-start;
}
.step-progress {
text-align: left;
margin-top: 8px;
}
.steps-list {
max-height: 200px;
}
}

View file

@ -0,0 +1,63 @@
template(name="migrationProgress")
.migration-overlay(class="{{#if isMigrating}}active{{/if}}")
.migration-modal
.migration-header
h3
i.fa.fa-database
| {{_ 'database-migration'}}
p {{_ 'database-migration-description'}}
.migration-content
.migration-overview
.overall-progress
.progress-bar
.progress-fill(style="width: {{migrationProgress}}%")
.progress-text {{migrationProgress}}%
.progress-label {{_ 'overall-progress'}}
.current-step
i.fa.fa-cog.fa-spin
| {{migrationCurrentStep}}
.estimated-time(style="{{#unless migrationEstimatedTime}}display: none;{{/unless}}")
i.fa.fa-clock-o
| {{_ 'estimated-time-remaining'}}: {{migrationEstimatedTime}}
.migration-steps
h4 {{_ 'migration-steps'}}
.steps-list
each migrationSteps
.migration-step(class="{{#if completed}}completed{{/if}}" class="{{#if isCurrentStep}}current{{/if}}")
.step-header
.step-icon
if completed
i.fa.fa-check-circle
else if isCurrentStep
i.fa.fa-cog.fa-spin
else
i.fa.fa-circle-o
.step-info
.step-name {{name}}
.step-description {{description}}
.step-progress
if completed
.progress-text 100%
else if isCurrentStep
.progress-text {{progress}}%
else
.progress-text 0%
if isCurrentStep
.step-progress-bar
.progress-fill(style="width: {{progress}}%")
.migration-status
i.fa.fa-info-circle
| {{migrationStatus}}
.migration-footer
.migration-info
i.fa.fa-lightbulb-o
| {{_ 'migration-info-text'}}
.migration-warning
i.fa.fa-exclamation-triangle
| {{_ 'migration-warning-text'}}

View file

@ -0,0 +1,46 @@
import { Template } from 'meteor/templating';
import { migrationManager } from '/imports/lib/migrationManager';
Template.migrationProgress.helpers({
isMigrating() {
return migrationManager.isMigrating.get();
},
migrationProgress() {
return migrationManager.migrationProgress.get();
},
migrationStatus() {
return migrationManager.migrationStatus.get();
},
migrationCurrentStep() {
return migrationManager.migrationCurrentStep.get();
},
migrationEstimatedTime() {
return migrationManager.migrationEstimatedTime.get();
},
migrationSteps() {
const steps = migrationManager.migrationSteps.get();
const currentStep = migrationManager.migrationCurrentStep.get();
return steps.map(step => ({
...step,
isCurrentStep: step.name === currentStep
}));
}
});
Template.migrationProgress.onCreated(function() {
// Subscribe to migration state changes
this.autorun(() => {
migrationManager.isMigrating.get();
migrationManager.migrationProgress.get();
migrationManager.migrationStatus.get();
migrationManager.migrationCurrentStep.get();
migrationManager.migrationEstimatedTime.get();
migrationManager.migrationSteps.get();
});
});

View file

@ -0,0 +1,203 @@
/**
* Board Conversion Service
* Handles conversion of boards from old database structure to new structure
* without running migrations that could cause downtime
*/
import { ReactiveVar } from 'meteor/reactive-var';
import { ReactiveCache } from '/imports/lib/reactiveCache';
// Reactive variables for conversion progress
export const conversionProgress = new ReactiveVar(0);
export const conversionStatus = new ReactiveVar('');
export const conversionEstimatedTime = new ReactiveVar('');
export const isConverting = new ReactiveVar(false);
class BoardConverter {
constructor() {
this.conversionCache = new Map(); // Cache converted board IDs
}
/**
* Check if a board needs conversion
* @param {string} boardId - The board ID to check
* @returns {boolean} - True if board needs conversion
*/
needsConversion(boardId) {
if (this.conversionCache.has(boardId)) {
return false; // Already converted
}
try {
const board = ReactiveCache.getBoard(boardId);
if (!board) return false;
// Check if any lists in this board don't have swimlaneId
const lists = ReactiveCache.getLists({
boardId: boardId,
$or: [
{ swimlaneId: { $exists: false } },
{ swimlaneId: '' },
{ swimlaneId: null }
]
});
return lists.length > 0;
} catch (error) {
console.error('Error checking if board needs conversion:', error);
return false;
}
}
/**
* Convert a board from old structure to new structure
* @param {string} boardId - The board ID to convert
* @returns {Promise<boolean>} - True if conversion was successful
*/
async convertBoard(boardId) {
if (this.conversionCache.has(boardId)) {
return true; // Already converted
}
isConverting.set(true);
conversionProgress.set(0);
conversionStatus.set('Starting board conversion...');
try {
const board = ReactiveCache.getBoard(boardId);
if (!board) {
throw new Error('Board not found');
}
// Get the default swimlane for this board
const defaultSwimlane = board.getDefaultSwimline();
if (!defaultSwimlane) {
throw new Error('No default swimlane found for board');
}
// Get all lists that need conversion
const listsToConvert = ReactiveCache.getLists({
boardId: boardId,
$or: [
{ swimlaneId: { $exists: false } },
{ swimlaneId: '' },
{ swimlaneId: null }
]
});
if (listsToConvert.length === 0) {
this.conversionCache.set(boardId, true);
isConverting.set(false);
return true;
}
conversionStatus.set(`Converting ${listsToConvert.length} lists...`);
const startTime = Date.now();
const totalLists = listsToConvert.length;
let convertedLists = 0;
// Convert lists in batches to avoid blocking the UI
const batchSize = 10;
for (let i = 0; i < listsToConvert.length; i += batchSize) {
const batch = listsToConvert.slice(i, i + batchSize);
// Process batch
await this.processBatch(batch, defaultSwimlane._id);
convertedLists += batch.length;
const progress = Math.round((convertedLists / totalLists) * 100);
conversionProgress.set(progress);
// Calculate estimated time remaining
const elapsed = Date.now() - startTime;
const rate = convertedLists / elapsed; // lists per millisecond
const remaining = totalLists - convertedLists;
const estimatedMs = remaining / rate;
conversionStatus.set(`Converting list ${convertedLists} of ${totalLists}...`);
conversionEstimatedTime.set(this.formatTime(estimatedMs));
// Allow UI to update
await new Promise(resolve => setTimeout(resolve, 10));
}
// Mark as converted
this.conversionCache.set(boardId, true);
conversionStatus.set('Board conversion completed!');
conversionProgress.set(100);
// Clear status after a delay
setTimeout(() => {
isConverting.set(false);
conversionStatus.set('');
conversionProgress.set(0);
conversionEstimatedTime.set('');
}, 2000);
return true;
} catch (error) {
console.error('Error converting board:', error);
conversionStatus.set(`Conversion failed: ${error.message}`);
isConverting.set(false);
return false;
}
}
/**
* Process a batch of lists for conversion
* @param {Array} batch - Array of lists to convert
* @param {string} defaultSwimlaneId - Default swimlane ID
*/
async processBatch(batch, defaultSwimlaneId) {
const updates = batch.map(list => ({
_id: list._id,
swimlaneId: defaultSwimlaneId
}));
// Update lists in batch
updates.forEach(update => {
ReactiveCache.getCollection('lists').update(update._id, {
$set: { swimlaneId: update.swimlaneId }
});
});
}
/**
* Format time in milliseconds to human readable format
* @param {number} ms - Time in milliseconds
* @returns {string} - Formatted time string
*/
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 conversion cache (useful for testing)
*/
clearCache() {
this.conversionCache.clear();
}
}
// Export singleton instance
export const boardConverter = new BoardConverter();

View file

@ -0,0 +1,775 @@
/**
* 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/lib/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
*/
needsMigration() {
// Check if any migration step is not completed
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);
}
/**
* Start migration process
*/
async startMigration() {
if (isMigrating.get()) {
return; // Already migrating
}
isMigrating.set(true);
migrationSteps.set([...this.steps]);
this.startTime = Date.now();
try {
// Start server-side migration
Meteor.call('migration.start', (error, result) => {
if (error) {
console.error('Failed to start migration:', error);
migrationStatus.set(`Migration failed: ${error.message}`);
isMigrating.set(false);
}
});
// Poll for progress updates
this.pollMigrationProgress();
} catch (error) {
console.error('Migration failed:', error);
migrationStatus.set(`Migration failed: ${error.message}`);
isMigrating.set(false);
}
}
/**
* Poll for migration progress updates
*/
pollMigrationProgress() {
const pollInterval = setInterval(() => {
Meteor.call('migration.getProgress', (error, result) => {
if (error) {
console.error('Failed to get 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
console.log(`Running migration: ${step.name}`);
}
/**
* 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();

View file

@ -86,5 +86,15 @@
"s3-storage": "S3",
"card-show-lists-on-minicard": "Show Lists on Minicard",
"show-list-on-minicard": "Show List on Minicard",
"hide-list-on-minicard": "Hide List on Minicard"
"hide-list-on-minicard": "Hide List on Minicard",
"converting-board": "Converting Board",
"converting-board-description": "Converting board structure for improved functionality. This may take a few moments.",
"estimated-time-remaining": "Estimated time remaining",
"conversion-info-text": "This conversion is performed once per board and improves performance. You can continue using the board normally.",
"database-migration": "Database Migration",
"database-migration-description": "Updating database structure for improved functionality and performance. This process may take several minutes.",
"overall-progress": "Overall Progress",
"migration-steps": "Migration Steps",
"migration-info-text": "Database migrations are performed once and improve system performance. The process continues in the background even if you close your browser.",
"migration-warning-text": "Please do not close your browser during migration. The process will continue in the background but may take longer to complete."
}

View file

@ -785,8 +785,13 @@ Boards.helpers({
{
boardId: this._id,
archived: false,
// Get lists for all swimlanes in this board
swimlaneId: { $in: this.swimlanes().map(s => s._id) },
// Get lists for all swimlanes in this board, plus lists without swimlaneId for backward compatibility
$or: [
{ swimlaneId: { $in: this.swimlanes().map(s => s._id) } },
{ swimlaneId: { $exists: false } },
{ swimlaneId: '' },
{ swimlaneId: null }
],
},
{ sort: sortKey },
);
@ -796,8 +801,13 @@ Boards.helpers({
return ReactiveCache.getLists(
{
boardId: this._id,
// Get lists for all swimlanes in this board
swimlaneId: { $in: this.swimlanes().map(s => s._id) }
// Get lists for all swimlanes in this board, plus lists without swimlaneId for backward compatibility
$or: [
{ swimlaneId: { $in: this.swimlanes().map(s => s._id) } },
{ swimlaneId: { $exists: false } },
{ swimlaneId: '' },
{ swimlaneId: null }
]
},
{ sort: { sort: 1 } }
);

View file

@ -50,10 +50,11 @@ Lists.attachSchema(
},
swimlaneId: {
/**
* the swimlane associated to this list. Required for per-swimlane list titles
* the swimlane associated to this list. Optional for backward compatibility
*/
type: String,
// Remove defaultValue to make it required
optional: true,
defaultValue: '',
},
createdAt: {
/**

View file

@ -211,20 +211,32 @@ Swimlanes.helpers({
},
newestLists() {
// sorted lists from newest to the oldest, by its creation date or its cards' last modification date
// Include lists without swimlaneId for backward compatibility (they belong to default swimlane)
return ReactiveCache.getLists(
{
boardId: this.boardId,
swimlaneId: this._id, // Only get lists that belong to this specific swimlane
$or: [
{ swimlaneId: this._id },
{ swimlaneId: { $exists: false } },
{ swimlaneId: '' },
{ swimlaneId: null }
],
archived: false,
},
{ sort: { modifiedAt: -1 } },
);
},
draggableLists() {
// Include lists without swimlaneId for backward compatibility (they belong to default swimlane)
return ReactiveCache.getLists(
{
boardId: this.boardId,
swimlaneId: this._id, // Only get lists that belong to this specific swimlane
$or: [
{ swimlaneId: this._id },
{ swimlaneId: { $exists: false } },
{ swimlaneId: '' },
{ swimlaneId: null }
],
//archived: false,
},
{ sort: ['sort'] },

382
package-lock.json generated
View file

@ -5,12 +5,9 @@
"requires": true,
"dependencies": {
"@babel/runtime": {
"version": "7.26.10",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz",
"integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==",
"requires": {
"regenerator-runtime": "^0.14.0"
}
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="
},
"@fast-csv/format": {
"version": "4.3.5",
@ -124,7 +121,7 @@
"from": "git+https://github.com/wekan/dragscroll.git"
},
"@wekanteam/exceljs": {
"version": "git+https://github.com/wekan/exceljs.git#e0229907e7a81bc3fe6daf4e42b1fdfbecdcb7cb",
"version": "git+https://github.com/wekan/exceljs.git#7d182abf83ddfb1a8f5a9592a0fdf60ef72f6686",
"from": "git+https://github.com/wekan/exceljs.git",
"requires": {
"archiver": "^5.0.0",
@ -170,6 +167,14 @@
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
},
"abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"requires": {
"event-target-shim": "^5.0.0"
}
},
"abstract-logging": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
@ -297,14 +302,17 @@
"integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="
},
"async": {
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
"integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg=="
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="
},
"available-typed-arrays": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
"integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw=="
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
"integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
"requires": {
"possible-typed-array-names": "^1.0.0"
}
},
"backoff": {
"version": "2.5.0",
@ -423,13 +431,58 @@
"integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ=="
},
"call-bind": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
"integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
"integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
"requires": {
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.1",
"set-function-length": "^1.1.1"
"call-bind-apply-helpers": "^1.0.0",
"es-define-property": "^1.0.0",
"get-intrinsic": "^1.2.4",
"set-function-length": "^1.2.2"
},
"dependencies": {
"get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"requires": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"dependencies": {
"es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="
}
}
},
"gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="
},
"has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="
},
"hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"requires": {
"function-bind": "^1.1.2"
}
}
}
},
"call-bind-apply-helpers": {
@ -612,9 +665,9 @@
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw=="
},
"dayjs": {
"version": "1.11.13",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="
"version": "1.11.18",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz",
"integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA=="
},
"debug": {
"version": "4.3.4",
@ -630,13 +683,13 @@
"integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ=="
},
"define-data-property": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
"integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"requires": {
"get-intrinsic": "^1.2.1",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.0"
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"gopd": "^1.0.1"
}
},
"delegates": {
@ -679,9 +732,9 @@
}
},
"dompurify": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz",
"integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==",
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
"integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
"requires": {
"@types/trusted-types": "^2.0.7"
}
@ -756,9 +809,9 @@
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"requires": {
"once": "^1.4.0"
}
@ -823,6 +876,16 @@
"resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz",
"integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA=="
},
"event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="
},
"events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="
},
"extsprintf": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz",
@ -848,11 +911,11 @@
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
},
"fast-xml-parser": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz",
"integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==",
"version": "4.5.3",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz",
"integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==",
"requires": {
"strnum": "^1.0.5"
"strnum": "^1.1.1"
}
},
"fibers": {
@ -897,11 +960,11 @@
"dev": true
},
"for-each": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
"integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
"integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
"requires": {
"is-callable": "^1.1.3"
"is-callable": "^1.2.7"
}
},
"fs-constants": {
@ -982,6 +1045,11 @@
"wide-align": "^1.1.2"
}
},
"generator-function": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
"integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="
},
"get-intrinsic": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
@ -1035,11 +1103,11 @@
"dev": true
},
"has-property-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
"integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"requires": {
"get-intrinsic": "^1.2.2"
"es-define-property": "^1.0.0"
}
},
"has-proto": {
@ -1053,11 +1121,11 @@
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="
},
"has-tostringtag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
"integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"requires": {
"has-symbols": "^1.0.2"
"has-symbols": "^1.0.3"
}
},
"has-unicode": {
@ -1131,17 +1199,17 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"ipaddr.js": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz",
"integrity": "sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ=="
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
"integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA=="
},
"is-arguments": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
"integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
"integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
"requires": {
"call-bind": "^1.0.2",
"has-tostringtag": "^1.0.0"
"call-bound": "^1.0.2",
"has-tostringtag": "^1.0.2"
}
},
"is-callable": {
@ -1155,19 +1223,49 @@
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
},
"is-generator-function": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz",
"integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
"integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==",
"requires": {
"has-tostringtag": "^1.0.0"
"call-bound": "^1.0.4",
"generator-function": "^2.0.0",
"get-proto": "^1.0.1",
"has-tostringtag": "^1.0.2",
"safe-regex-test": "^1.1.0"
}
},
"is-regex": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
"integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
"requires": {
"call-bound": "^1.0.2",
"gopd": "^1.2.0",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"dependencies": {
"gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="
},
"hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"requires": {
"function-bind": "^1.1.2"
}
}
}
},
"is-typed-array": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz",
"integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==",
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
"integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
"requires": {
"which-typed-array": "^1.1.11"
"which-typed-array": "^1.1.16"
}
},
"isarray": {
@ -2644,9 +2742,9 @@
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
},
"papaparse": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz",
"integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw=="
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz",
"integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A=="
},
"parse-ms": {
"version": "2.1.0",
@ -2694,6 +2792,11 @@
"parse-ms": "^2.1.0"
}
},
"process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="
},
"process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@ -2734,11 +2837,34 @@
}
},
"readable-web-to-node-stream": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz",
"integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==",
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz",
"integrity": "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==",
"requires": {
"readable-stream": "^3.6.0"
"readable-stream": "^4.7.0"
},
"dependencies": {
"buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"requires": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"readable-stream": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
"requires": {
"abort-controller": "^3.0.0",
"buffer": "^6.0.3",
"events": "^3.3.0",
"process": "^0.11.10",
"string_decoder": "^1.3.0"
}
}
}
},
"readdir-glob": {
@ -2767,11 +2893,6 @@
}
}
},
"regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@ -2785,15 +2906,25 @@
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
},
"safe-regex-test": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
"integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
"requires": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"is-regex": "^1.2.1"
}
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"sax": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz",
"integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA=="
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
"integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg=="
},
"saxes": {
"version": "5.0.1",
@ -2817,14 +2948,60 @@
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
},
"set-function-length": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz",
"integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"requires": {
"define-data-property": "^1.1.1",
"get-intrinsic": "^1.2.1",
"define-data-property": "^1.1.4",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.0"
"has-property-descriptors": "^1.0.2"
},
"dependencies": {
"es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="
},
"get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"requires": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"dependencies": {
"gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="
}
}
},
"has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="
},
"hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"requires": {
"function-bind": "^1.1.2"
}
}
}
},
"setimmediate": {
@ -3001,9 +3178,9 @@
}
},
"strnum": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz",
"integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA=="
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz",
"integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="
},
"strtok3": {
"version": "6.3.0",
@ -3436,15 +3613,24 @@
}
},
"which-typed-array": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz",
"integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==",
"version": "1.1.19",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz",
"integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==",
"requires": {
"available-typed-arrays": "^1.0.5",
"call-bind": "^1.0.4",
"for-each": "^0.3.3",
"gopd": "^1.0.1",
"has-tostringtag": "^1.0.0"
"available-typed-arrays": "^1.0.7",
"call-bind": "^1.0.8",
"call-bound": "^1.0.4",
"for-each": "^0.3.5",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-tostringtag": "^1.0.2"
},
"dependencies": {
"gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="
}
}
},
"wicked-good-xpath": {

View file

@ -17,25 +17,25 @@
"sinon": "^13.0.2"
},
"dependencies": {
"@babel/runtime": "^7.26.10",
"@babel/runtime": "^7.28.4",
"@mapbox/node-pre-gyp": "^1.0.10",
"@rwap/jquery-ui-touch-punch": "^1.0.11",
"@wekanteam/dragscroll": "https://github.com/wekan/dragscroll.git",
"@wekanteam/exceljs": "https://github.com/wekan/exceljs.git",
"@wekanteam/exceljs": "git+https://github.com/wekan/exceljs.git",
"@wekanteam/html-to-markdown": "^1.0.2",
"@wekanteam/meteor-globals": "^1.1.4",
"@wekanteam/meteor-reactive-cache": "^1.0.6",
"ajv": "^6.12.6",
"bcryptjs": "^2.4.3",
"bson": "^4.5.2",
"bson": "^4.7.2",
"chart.js": "^4.5.0",
"dompurify": "^3.2.4",
"dompurify": "^3.2.7",
"es6-promise": "^4.2.4",
"escape-string-regexp": "^5.0.0",
"fibers": "^5.0.3",
"file-type": "^16.5.4",
"filesize": "^8.0.7",
"i18next": "^21.6.16",
"i18next": "^21.10.0",
"i18next-sprintf-postprocessor": "^0.2.2",
"jquery": "^3.7.1",
"jquery-ui": "^1.13.3",
@ -47,15 +47,15 @@
"meteor-accounts-t9n": "^2.6.0",
"meteor-node-stubs": "^1.2.24",
"minio": "^7.1.3",
"moment": "^2.29.4",
"moment": "^2.30.1",
"mongodb3legacy": "npm:mongodb@3.7.4",
"mongodb4legacy": "npm:mongodb@4.17.2",
"mongodb5legacy": "npm:mongodb@5.9.2",
"mongodb6legacy": "npm:mongodb@6.3.0",
"mongodb7legacy": "npm:mongodb@7.0.1",
"mongodb8legacy": "npm:mongodb@6.9.0",
"mongodb7legacy": "npm:mongodb@6.20.0",
"mongodb8legacy": "npm:mongodb@6.20.0",
"os": "^0.1.2",
"papaparse": "^5.3.1",
"papaparse": "^5.5.3",
"pretty-ms": "^7.0.1",
"qs": "^6.13.0",
"simpl-schema": "^3.4.6",

View file

@ -24,3 +24,6 @@ if (errors.length > 0) {
console.error("\n\n");
process.exit(1);
}
// Import migration runner for on-demand migrations
import './migrationRunner';

404
server/migrationRunner.js Normal file
View file

@ -0,0 +1,404 @@
/**
* Server-side Migration Runner
* Handles actual execution of database migrations with progress tracking
*/
import { Meteor } from 'meteor/meteor';
import { Migrations } from 'meteor/percolate:migrations';
import { ReactiveVar } from 'meteor/reactive-var';
// Server-side reactive variables for migration progress
export const serverMigrationProgress = new ReactiveVar(0);
export const serverMigrationStatus = new ReactiveVar('');
export const serverMigrationCurrentStep = new ReactiveVar('');
export const serverMigrationSteps = new ReactiveVar([]);
export const serverIsMigrating = new ReactiveVar(false);
class ServerMigrationRunner {
constructor() {
this.migrationSteps = this.initializeMigrationSteps();
this.currentStepIndex = 0;
this.startTime = null;
}
/**
* Initialize migration steps with their actual migration functions
*/
initializeMigrationSteps() {
return [
{
id: 'board-background-color',
name: 'Board Background Colors',
description: 'Setting up board background colors',
weight: 1,
completed: false,
progress: 0,
migrationFunction: this.runBoardBackgroundColorMigration
},
{
id: 'add-cardcounterlist-allowed',
name: 'Card Counter List Settings',
description: 'Adding card counter list permissions',
weight: 1,
completed: false,
progress: 0,
migrationFunction: this.runCardCounterListMigration
},
{
id: 'add-boardmemberlist-allowed',
name: 'Board Member List Settings',
description: 'Adding board member list permissions',
weight: 1,
completed: false,
progress: 0,
migrationFunction: this.runBoardMemberListMigration
},
{
id: 'lowercase-board-permission',
name: 'Board Permission Standardization',
description: 'Converting board permissions to lowercase',
weight: 1,
completed: false,
progress: 0,
migrationFunction: this.runLowercaseBoardPermissionMigration
},
{
id: 'change-attachments-type-for-non-images',
name: 'Attachment Type Standardization',
description: 'Updating attachment types for non-images',
weight: 2,
completed: false,
progress: 0,
migrationFunction: this.runAttachmentTypeMigration
},
{
id: 'card-covers',
name: 'Card Covers System',
description: 'Setting up card cover functionality',
weight: 2,
completed: false,
progress: 0,
migrationFunction: this.runCardCoversMigration
},
{
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,
migrationFunction: this.runBoardColorCSSMigration
},
{
id: 'denormalize-star-number-per-board',
name: 'Board Star Counts',
description: 'Calculating star counts per board',
weight: 3,
completed: false,
progress: 0,
migrationFunction: this.runStarNumberMigration
},
{
id: 'add-member-isactive-field',
name: 'Member Activity Status',
description: 'Adding member activity tracking',
weight: 2,
completed: false,
progress: 0,
migrationFunction: this.runMemberIsActiveMigration
},
{
id: 'add-sort-checklists',
name: 'Checklist Sorting',
description: 'Adding sort order to checklists',
weight: 2,
completed: false,
progress: 0,
migrationFunction: this.runSortChecklistsMigration
},
{
id: 'add-swimlanes',
name: 'Swimlanes System',
description: 'Setting up swimlanes functionality',
weight: 4,
completed: false,
progress: 0,
migrationFunction: this.runSwimlanesMigration
},
{
id: 'add-views',
name: 'Board Views',
description: 'Adding board view options',
weight: 2,
completed: false,
progress: 0,
migrationFunction: this.runViewsMigration
},
{
id: 'add-checklist-items',
name: 'Checklist Items',
description: 'Setting up checklist items system',
weight: 3,
completed: false,
progress: 0,
migrationFunction: this.runChecklistItemsMigration
},
{
id: 'add-card-types',
name: 'Card Types',
description: 'Adding card type functionality',
weight: 2,
completed: false,
progress: 0,
migrationFunction: this.runCardTypesMigration
},
{
id: 'add-custom-fields-to-cards',
name: 'Custom Fields',
description: 'Adding custom fields to cards',
weight: 3,
completed: false,
progress: 0,
migrationFunction: this.runCustomFieldsMigration
},
{
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,
migrationFunction: this.runAttachmentMigration
},
{
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,
migrationFunction: this.runAvatarMigration
},
{
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,
migrationFunction: this.runListsToPerSwimlaneMigration
}
];
}
/**
* Start migration process
*/
async startMigration() {
if (serverIsMigrating.get()) {
return; // Already migrating
}
serverIsMigrating.set(true);
serverMigrationSteps.set([...this.migrationSteps]);
this.startTime = Date.now();
try {
for (let i = 0; i < this.migrationSteps.length; i++) {
const step = this.migrationSteps[i];
this.currentStepIndex = i;
if (step.completed) {
continue; // Skip already completed steps
}
serverMigrationCurrentStep.set(step.name);
serverMigrationStatus.set(`Running: ${step.description}`);
// Run the migration step
await this.runMigrationStep(step);
// Mark as completed
step.completed = true;
step.progress = 100;
// Update progress
this.updateProgress();
// Allow other processes to run
await new Promise(resolve => setTimeout(resolve, 100));
}
// Migration completed
serverMigrationStatus.set('All migrations completed successfully!');
serverMigrationProgress.set(100);
serverMigrationCurrentStep.set('');
// Clear status after delay
setTimeout(() => {
serverIsMigrating.set(false);
serverMigrationStatus.set('');
serverMigrationProgress.set(0);
}, 3000);
} catch (error) {
console.error('Migration failed:', error);
serverMigrationStatus.set(`Migration failed: ${error.message}`);
serverIsMigrating.set(false);
}
}
/**
* Run a single migration step
*/
async runMigrationStep(step) {
try {
// Update progress during migration
const progressSteps = 10;
for (let i = 0; i <= progressSteps; i++) {
step.progress = (i / progressSteps) * 100;
this.updateProgress();
// Run actual migration function
if (i === progressSteps) {
await step.migrationFunction.call(this);
}
// Allow other processes to run
await new Promise(resolve => setTimeout(resolve, 50));
}
} catch (error) {
console.error(`Migration step ${step.name} failed:`, error);
throw error;
}
}
/**
* Update progress variables
*/
updateProgress() {
const totalWeight = this.migrationSteps.reduce((total, step) => total + step.weight, 0);
const completedWeight = this.migrationSteps.reduce((total, step) => {
return total + (step.completed ? step.weight : step.progress * step.weight / 100);
}, 0);
const progress = Math.round((completedWeight / totalWeight) * 100);
serverMigrationProgress.set(progress);
serverMigrationSteps.set([...this.migrationSteps]);
}
// Individual migration functions
async runBoardBackgroundColorMigration() {
// Implementation for board background color migration
console.log('Running board background color migration');
}
async runCardCounterListMigration() {
// Implementation for card counter list migration
console.log('Running card counter list migration');
}
async runBoardMemberListMigration() {
// Implementation for board member list migration
console.log('Running board member list migration');
}
async runLowercaseBoardPermissionMigration() {
// Implementation for lowercase board permission migration
console.log('Running lowercase board permission migration');
}
async runAttachmentTypeMigration() {
// Implementation for attachment type migration
console.log('Running attachment type migration');
}
async runCardCoversMigration() {
// Implementation for card covers migration
console.log('Running card covers migration');
}
async runBoardColorCSSMigration() {
// Implementation for board color CSS migration
console.log('Running board color CSS migration');
}
async runStarNumberMigration() {
// Implementation for star number migration
console.log('Running star number migration');
}
async runMemberIsActiveMigration() {
// Implementation for member is active migration
console.log('Running member is active migration');
}
async runSortChecklistsMigration() {
// Implementation for sort checklists migration
console.log('Running sort checklists migration');
}
async runSwimlanesMigration() {
// Implementation for swimlanes migration
console.log('Running swimlanes migration');
}
async runViewsMigration() {
// Implementation for views migration
console.log('Running views migration');
}
async runChecklistItemsMigration() {
// Implementation for checklist items migration
console.log('Running checklist items migration');
}
async runCardTypesMigration() {
// Implementation for card types migration
console.log('Running card types migration');
}
async runCustomFieldsMigration() {
// Implementation for custom fields migration
console.log('Running custom fields migration');
}
async runAttachmentMigration() {
// Implementation for attachment migration from CollectionFS to Meteor-Files
console.log('Running attachment migration from CollectionFS to Meteor-Files');
}
async runAvatarMigration() {
// Implementation for avatar migration from CollectionFS to Meteor-Files
console.log('Running avatar migration from CollectionFS to Meteor-Files');
}
async runListsToPerSwimlaneMigration() {
// Implementation for lists to per-swimlane migration
console.log('Running lists to per-swimlane migration');
}
}
// Export singleton instance
export const serverMigrationRunner = new ServerMigrationRunner();
// Meteor methods for client-server communication
Meteor.methods({
'migration.start'() {
if (!this.userId) {
throw new Meteor.Error('not-authorized');
}
return serverMigrationRunner.startMigration();
},
'migration.getProgress'() {
return {
progress: serverMigrationProgress.get(),
status: serverMigrationStatus.get(),
currentStep: serverMigrationCurrentStep.get(),
steps: serverMigrationSteps.get(),
isMigrating: serverIsMigrating.get()
};
}
});