diff --git a/client/00-startup.js b/client/00-startup.js index 4a717b67c..4f22c0868 100644 --- a/client/00-startup.js +++ b/client/00-startup.js @@ -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'; diff --git a/client/components/boardConversionProgress.css b/client/components/boardConversionProgress.css new file mode 100644 index 000000000..fd186908f --- /dev/null +++ b/client/components/boardConversionProgress.css @@ -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; + } +} diff --git a/client/components/boardConversionProgress.jade b/client/components/boardConversionProgress.jade new file mode 100644 index 000000000..946bbdbaf --- /dev/null +++ b/client/components/boardConversionProgress.jade @@ -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'}} diff --git a/client/components/boardConversionProgress.js b/client/components/boardConversionProgress.js new file mode 100644 index 000000000..e928ac3d2 --- /dev/null +++ b/client/components/boardConversionProgress.js @@ -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(); + }); +}); diff --git a/client/components/boards/boardBody.jade b/client/components/boards/boardBody.jade index 057c4bc49..5dfd4373b 100644 --- a/client/components/boards/boardBody.jade +++ b/client/components/boards/boardBody.jade @@ -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) diff --git a/client/components/boards/boardBody.js b/client/components/boards/boardBody.js index 639a75942..52854c20c 100644 --- a/client/components/boards/boardBody.js +++ b/client/components/boards/boardBody.js @@ -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({ diff --git a/client/components/main/layouts.jade b/client/components/main/layouts.jade index 5d9e7145c..80ff486bb 100644 --- a/client/components/main/layouts.jade +++ b/client/components/main/layouts.jade @@ -77,6 +77,8 @@ template(name="defaultLayout") | {{{afterBodyStart}}} +Template.dynamic(template=content) | {{{beforeBodyEnd}}} + +migrationProgress + +boardConversionProgress if (Modal.isOpen) #modal .overlay diff --git a/client/components/migrationProgress.css b/client/components/migrationProgress.css new file mode 100644 index 000000000..d44f4eda8 --- /dev/null +++ b/client/components/migrationProgress.css @@ -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; + } +} diff --git a/client/components/migrationProgress.jade b/client/components/migrationProgress.jade new file mode 100644 index 000000000..0e4842349 --- /dev/null +++ b/client/components/migrationProgress.jade @@ -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'}} diff --git a/client/components/migrationProgress.js b/client/components/migrationProgress.js new file mode 100644 index 000000000..d36c264f3 --- /dev/null +++ b/client/components/migrationProgress.js @@ -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(); + }); +}); diff --git a/client/lib/boardConverter.js b/client/lib/boardConverter.js new file mode 100644 index 000000000..99337419a --- /dev/null +++ b/client/lib/boardConverter.js @@ -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} - 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(); diff --git a/client/lib/migrationManager.js b/client/lib/migrationManager.js new file mode 100644 index 000000000..0f9e2b6b5 --- /dev/null +++ b/client/lib/migrationManager.js @@ -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(); diff --git a/imports/i18n/en.i18n.json b/imports/i18n/en.i18n.json index f54b3a327..ceb81e4cb 100644 --- a/imports/i18n/en.i18n.json +++ b/imports/i18n/en.i18n.json @@ -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." } diff --git a/models/boards.js b/models/boards.js index 55a97c297..2dc3c62de 100644 --- a/models/boards.js +++ b/models/boards.js @@ -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 } } ); diff --git a/models/lists.js b/models/lists.js index ca9808e68..771ca1f63 100644 --- a/models/lists.js +++ b/models/lists.js @@ -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: { /** diff --git a/models/swimlanes.js b/models/swimlanes.js index 2636b274a..4ff2d03fd 100644 --- a/models/swimlanes.js +++ b/models/swimlanes.js @@ -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'] }, diff --git a/package-lock.json b/package-lock.json index 646dec774..7de61c7a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { diff --git a/package.json b/package.json index 6d7a1dcf1..3f52139ca 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/server/00checkStartup.js b/server/00checkStartup.js index b4d65be52..30ed97de5 100644 --- a/server/00checkStartup.js +++ b/server/00checkStartup.js @@ -24,3 +24,6 @@ if (errors.length > 0) { console.error("\n\n"); process.exit(1); } + +// Import migration runner for on-demand migrations +import './migrationRunner'; diff --git a/server/migrationRunner.js b/server/migrationRunner.js new file mode 100644 index 000000000..47f2773f2 --- /dev/null +++ b/server/migrationRunner.js @@ -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() + }; + } +});