mirror of
https://github.com/wekan/wekan.git
synced 2025-12-16 07:20:12 +01:00
Fixes to make board showing correctly.
Thanks to xet7 !
This commit is contained in:
parent
ffb02fe0ec
commit
bd8c565415
33 changed files with 2372 additions and 2747 deletions
|
|
@ -6,12 +6,12 @@ if ('serviceWorker' in navigator) {
|
|||
}
|
||||
|
||||
// Import board converter for on-demand conversion
|
||||
import '/imports/lib/boardConverter';
|
||||
import '/imports/components/boardConversionProgress';
|
||||
import '/client/lib/boardConverter';
|
||||
import '/client/components/boardConversionProgress';
|
||||
|
||||
// Import migration manager and progress UI
|
||||
import '/imports/lib/migrationManager';
|
||||
import '/imports/components/migrationProgress';
|
||||
import '/client/lib/migrationManager';
|
||||
import '/client/components/migrationProgress';
|
||||
|
||||
// Import cron settings
|
||||
import '/imports/components/settings/cronSettings';
|
||||
import '/client/components/settings/cronSettings';
|
||||
|
|
|
|||
|
|
@ -1,31 +1,37 @@
|
|||
import { Template } from 'meteor/templating';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { boardConverter } from '/imports/lib/boardConverter';
|
||||
import {
|
||||
boardConverter,
|
||||
isConverting,
|
||||
conversionProgress,
|
||||
conversionStatus,
|
||||
conversionEstimatedTime
|
||||
} from '/client/lib/boardConverter';
|
||||
|
||||
Template.boardConversionProgress.helpers({
|
||||
isConverting() {
|
||||
return boardConverter.isConverting.get();
|
||||
return isConverting.get();
|
||||
},
|
||||
|
||||
conversionProgress() {
|
||||
return boardConverter.conversionProgress.get();
|
||||
return conversionProgress.get();
|
||||
},
|
||||
|
||||
conversionStatus() {
|
||||
return boardConverter.conversionStatus.get();
|
||||
return conversionStatus.get();
|
||||
},
|
||||
|
||||
conversionEstimatedTime() {
|
||||
return boardConverter.conversionEstimatedTime.get();
|
||||
return 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();
|
||||
isConverting.get();
|
||||
conversionProgress.get();
|
||||
conversionStatus.get();
|
||||
conversionEstimatedTime.get();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
template(name="board")
|
||||
if isMigrating
|
||||
|
||||
if isMigrating.get
|
||||
+migrationProgress
|
||||
else if isConverting
|
||||
else if isConverting.get
|
||||
+boardConversionProgress
|
||||
else if isBoardReady.get
|
||||
if currentBoard
|
||||
|
|
@ -46,7 +47,12 @@ template(name="boardBody")
|
|||
else if isViewCalendar
|
||||
+calendarView
|
||||
else
|
||||
+listsGroup(currentBoard)
|
||||
// Default view - show swimlanes if they exist, otherwise show lists
|
||||
if hasSwimlanes
|
||||
each currentBoard.swimlanes
|
||||
+swimlane(this)
|
||||
else
|
||||
+listsGroup(currentBoard)
|
||||
+sidebar
|
||||
|
||||
template(name="calendarView")
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
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';
|
||||
import { boardConverter } from '/client/lib/boardConverter';
|
||||
import { migrationManager } from '/client/lib/migrationManager';
|
||||
import { attachmentMigrationManager } from '/client/lib/attachmentMigrationManager';
|
||||
import { Swimlanes } from '/models/swimlanes';
|
||||
import { Lists } from '/models/lists';
|
||||
|
||||
const subManager = new SubsManager();
|
||||
const { calculateIndex } = Utils;
|
||||
|
|
@ -13,6 +16,7 @@ BlazeComponent.extendComponent({
|
|||
this.isBoardReady = new ReactiveVar(false);
|
||||
this.isConverting = new ReactiveVar(false);
|
||||
this.isMigrating = new ReactiveVar(false);
|
||||
this._swimlaneCreated = new Set(); // Track boards where we've created swimlanes
|
||||
|
||||
// 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
|
||||
|
|
@ -21,10 +25,14 @@ BlazeComponent.extendComponent({
|
|||
this.autorun(() => {
|
||||
const currentBoardId = Session.get('currentBoard');
|
||||
if (!currentBoardId) return;
|
||||
|
||||
const handle = subManager.subscribe('board', currentBoardId, false);
|
||||
|
||||
Tracker.nonreactive(() => {
|
||||
Tracker.autorun(() => {
|
||||
if (handle.ready()) {
|
||||
// Ensure default swimlane exists (only once per board)
|
||||
this.ensureDefaultSwimlane(currentBoardId);
|
||||
// Check if board needs conversion
|
||||
this.checkAndConvertBoard(currentBoardId);
|
||||
} else {
|
||||
|
|
@ -35,17 +43,54 @@ BlazeComponent.extendComponent({
|
|||
});
|
||||
},
|
||||
|
||||
ensureDefaultSwimlane(boardId) {
|
||||
// Only create swimlane once per board
|
||||
if (this._swimlaneCreated.has(boardId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (!board) return;
|
||||
|
||||
const swimlanes = board.swimlanes();
|
||||
|
||||
if (swimlanes.length === 0) {
|
||||
const swimlaneId = Swimlanes.insert({
|
||||
title: 'Default',
|
||||
boardId: boardId,
|
||||
});
|
||||
this._swimlaneCreated.add(boardId);
|
||||
} else {
|
||||
this._swimlaneCreated.add(boardId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating default swimlane:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async checkAndConvertBoard(boardId) {
|
||||
try {
|
||||
// First check if migrations need to be run
|
||||
if (migrationManager.needsMigration()) {
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (!board) {
|
||||
this.isBoardReady.set(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if board needs migration based on migration version
|
||||
const needsMigration = !board.migrationVersion || board.migrationVersion < 1;
|
||||
|
||||
if (needsMigration) {
|
||||
// Start background migration for old boards
|
||||
this.isMigrating.set(true);
|
||||
await migrationManager.startMigration();
|
||||
await this.startBackgroundMigration(boardId);
|
||||
this.isMigrating.set(false);
|
||||
}
|
||||
|
||||
// Then check if board needs conversion
|
||||
if (boardConverter.needsConversion(boardId)) {
|
||||
// Check if board needs conversion (for old structure)
|
||||
const needsConversion = boardConverter.needsConversion(boardId);
|
||||
|
||||
if (needsConversion) {
|
||||
this.isConverting.set(true);
|
||||
const success = await boardConverter.convertBoard(boardId);
|
||||
this.isConverting.set(false);
|
||||
|
|
@ -53,12 +98,15 @@ BlazeComponent.extendComponent({
|
|||
if (success) {
|
||||
this.isBoardReady.set(true);
|
||||
} else {
|
||||
console.error('Board conversion failed');
|
||||
console.error('Board conversion failed, setting ready to true anyway');
|
||||
this.isBoardReady.set(true); // Still show board even if conversion failed
|
||||
}
|
||||
} else {
|
||||
this.isBoardReady.set(true);
|
||||
}
|
||||
|
||||
// Start attachment migration in background if needed
|
||||
this.startAttachmentMigrationIfNeeded(boardId);
|
||||
} catch (error) {
|
||||
console.error('Error during board conversion check:', error);
|
||||
this.isConverting.set(false);
|
||||
|
|
@ -67,8 +115,39 @@ BlazeComponent.extendComponent({
|
|||
}
|
||||
},
|
||||
|
||||
async startBackgroundMigration(boardId) {
|
||||
try {
|
||||
// Start background migration using the cron system
|
||||
Meteor.call('boardMigration.startBoardMigration', boardId, (error, result) => {
|
||||
if (error) {
|
||||
console.error('Failed to start background migration:', error);
|
||||
} else {
|
||||
console.log('Background migration started for board:', boardId);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error starting background migration:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async startAttachmentMigrationIfNeeded(boardId) {
|
||||
try {
|
||||
// Check if there are unconverted attachments
|
||||
const unconvertedAttachments = attachmentMigrationManager.getUnconvertedAttachments(boardId);
|
||||
|
||||
if (unconvertedAttachments.length > 0) {
|
||||
console.log(`Starting attachment migration for ${unconvertedAttachments.length} attachments in board ${boardId}`);
|
||||
await attachmentMigrationManager.startAttachmentMigration(boardId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error starting attachment migration:', error);
|
||||
}
|
||||
},
|
||||
|
||||
onlyShowCurrentCard() {
|
||||
return Utils.isMiniScreen() && Utils.getCurrentCardId(true);
|
||||
const isMiniScreen = Utils.isMiniScreen();
|
||||
const currentCardId = Utils.getCurrentCardId(true);
|
||||
return isMiniScreen && currentCardId;
|
||||
},
|
||||
|
||||
goHome() {
|
||||
|
|
@ -82,6 +161,14 @@ BlazeComponent.extendComponent({
|
|||
isMigrating() {
|
||||
return this.isMigrating.get();
|
||||
},
|
||||
|
||||
isBoardReady() {
|
||||
return this.isBoardReady.get();
|
||||
},
|
||||
|
||||
currentBoard() {
|
||||
return Utils.getCurrentBoard();
|
||||
},
|
||||
}).register('board');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
|
|
@ -95,33 +182,37 @@ BlazeComponent.extendComponent({
|
|||
|
||||
// fix swimlanes sort field if there are null values
|
||||
const currentBoardData = Utils.getCurrentBoard();
|
||||
const nullSortSwimlanes = currentBoardData.nullSortSwimlanes();
|
||||
if (nullSortSwimlanes.length > 0) {
|
||||
const swimlanes = currentBoardData.swimlanes();
|
||||
let count = 0;
|
||||
swimlanes.forEach(s => {
|
||||
Swimlanes.update(s._id, {
|
||||
$set: {
|
||||
sort: count,
|
||||
},
|
||||
if (currentBoardData && Swimlanes) {
|
||||
const nullSortSwimlanes = currentBoardData.nullSortSwimlanes();
|
||||
if (nullSortSwimlanes.length > 0) {
|
||||
const swimlanes = currentBoardData.swimlanes();
|
||||
let count = 0;
|
||||
swimlanes.forEach(s => {
|
||||
Swimlanes.update(s._id, {
|
||||
$set: {
|
||||
sort: count,
|
||||
},
|
||||
});
|
||||
count += 1;
|
||||
});
|
||||
count += 1;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// fix lists sort field if there are null values
|
||||
const nullSortLists = currentBoardData.nullSortLists();
|
||||
if (nullSortLists.length > 0) {
|
||||
const lists = currentBoardData.lists();
|
||||
let count = 0;
|
||||
lists.forEach(l => {
|
||||
Lists.update(l._id, {
|
||||
$set: {
|
||||
sort: count,
|
||||
},
|
||||
if (currentBoardData && Lists) {
|
||||
const nullSortLists = currentBoardData.nullSortLists();
|
||||
if (nullSortLists.length > 0) {
|
||||
const lists = currentBoardData.lists();
|
||||
let count = 0;
|
||||
lists.forEach(l => {
|
||||
Lists.update(l._id, {
|
||||
$set: {
|
||||
sort: count,
|
||||
},
|
||||
});
|
||||
count += 1;
|
||||
});
|
||||
count += 1;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
onRendered() {
|
||||
|
|
@ -461,51 +552,100 @@ BlazeComponent.extendComponent({
|
|||
notDisplayThisBoard() {
|
||||
let allowPrivateVisibilityOnly = TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly');
|
||||
let currentBoard = Utils.getCurrentBoard();
|
||||
if (allowPrivateVisibilityOnly !== undefined && allowPrivateVisibilityOnly.booleanValue && currentBoard.permission == 'public') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return allowPrivateVisibilityOnly !== undefined && allowPrivateVisibilityOnly.booleanValue && currentBoard && currentBoard.permission == 'public';
|
||||
},
|
||||
|
||||
isViewSwimlanes() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
let boardView;
|
||||
|
||||
if (currentUser) {
|
||||
return (currentUser.profile || {}).boardView === 'board-view-swimlanes';
|
||||
boardView = (currentUser.profile || {}).boardView;
|
||||
} else {
|
||||
return (
|
||||
window.localStorage.getItem('boardView') === 'board-view-swimlanes'
|
||||
);
|
||||
boardView = window.localStorage.getItem('boardView');
|
||||
}
|
||||
},
|
||||
|
||||
hasSwimlanes() {
|
||||
return Utils.getCurrentBoard().swimlanes().length > 0;
|
||||
|
||||
// If no board view is set, default to swimlanes
|
||||
if (!boardView) {
|
||||
boardView = 'board-view-swimlanes';
|
||||
}
|
||||
|
||||
return boardView === 'board-view-swimlanes';
|
||||
},
|
||||
|
||||
isViewLists() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
let boardView;
|
||||
|
||||
if (currentUser) {
|
||||
return (currentUser.profile || {}).boardView === 'board-view-lists';
|
||||
boardView = (currentUser.profile || {}).boardView;
|
||||
} else {
|
||||
return window.localStorage.getItem('boardView') === 'board-view-lists';
|
||||
boardView = window.localStorage.getItem('boardView');
|
||||
}
|
||||
|
||||
return boardView === 'board-view-lists';
|
||||
},
|
||||
|
||||
isViewCalendar() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
let boardView;
|
||||
|
||||
if (currentUser) {
|
||||
return (currentUser.profile || {}).boardView === 'board-view-cal';
|
||||
boardView = (currentUser.profile || {}).boardView;
|
||||
} else {
|
||||
return window.localStorage.getItem('boardView') === 'board-view-cal';
|
||||
boardView = window.localStorage.getItem('boardView');
|
||||
}
|
||||
|
||||
return boardView === 'board-view-cal';
|
||||
},
|
||||
|
||||
hasSwimlanes() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
if (!currentBoard) return false;
|
||||
|
||||
const swimlanes = currentBoard.swimlanes();
|
||||
return swimlanes.length > 0;
|
||||
},
|
||||
|
||||
|
||||
isVerticalScrollbars() {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
return user && user.isVerticalScrollbars();
|
||||
},
|
||||
|
||||
boardView() {
|
||||
return Utils.boardView();
|
||||
},
|
||||
|
||||
debugBoardState() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
const currentBoardId = Session.get('currentBoard');
|
||||
const isBoardReady = this.isBoardReady.get();
|
||||
const isConverting = this.isConverting.get();
|
||||
const isMigrating = this.isMigrating.get();
|
||||
const boardView = Utils.boardView();
|
||||
|
||||
console.log('=== BOARD DEBUG STATE ===');
|
||||
console.log('currentBoardId:', currentBoardId);
|
||||
console.log('currentBoard:', !!currentBoard, currentBoard ? currentBoard.title : 'none');
|
||||
console.log('isBoardReady:', isBoardReady);
|
||||
console.log('isConverting:', isConverting);
|
||||
console.log('isMigrating:', isMigrating);
|
||||
console.log('boardView:', boardView);
|
||||
console.log('========================');
|
||||
|
||||
return {
|
||||
currentBoardId,
|
||||
hasCurrentBoard: !!currentBoard,
|
||||
currentBoardTitle: currentBoard ? currentBoard.title : 'none',
|
||||
isBoardReady,
|
||||
isConverting,
|
||||
isMigrating,
|
||||
boardView
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
openNewListForm() {
|
||||
if (this.isViewSwimlanes()) {
|
||||
// The form had been removed in 416b17062e57f215206e93a85b02ef9eb1ab4902
|
||||
|
|
|
|||
|
|
@ -81,11 +81,20 @@ BlazeComponent.extendComponent({
|
|||
Modal.open('archivedBoards');
|
||||
},
|
||||
'click .js-toggle-board-view': Popup.open('boardChangeView'),
|
||||
'click .js-toggle-sidebar'() {
|
||||
Sidebar.toggle();
|
||||
},
|
||||
// Sidebar toggle is handled by the sidebar component itself
|
||||
// 'click .js-toggle-sidebar'() {
|
||||
// if (Sidebar) {
|
||||
// Sidebar.toggle();
|
||||
// } else {
|
||||
// console.warn('Sidebar not available for toggle');
|
||||
// }
|
||||
// },
|
||||
'click .js-open-filter-view'() {
|
||||
Sidebar.setView('filter');
|
||||
if (Sidebar) {
|
||||
Sidebar.setView('filter');
|
||||
} else {
|
||||
console.warn('Sidebar not available for setView');
|
||||
}
|
||||
},
|
||||
'click .js-sort-cards': Popup.open('cardsSort'),
|
||||
/*
|
||||
|
|
@ -102,14 +111,22 @@ BlazeComponent.extendComponent({
|
|||
*/
|
||||
'click .js-filter-reset'(event) {
|
||||
event.stopPropagation();
|
||||
Sidebar.setView();
|
||||
if (Sidebar) {
|
||||
Sidebar.setView();
|
||||
} else {
|
||||
console.warn('Sidebar not available for setView');
|
||||
}
|
||||
Filter.reset();
|
||||
},
|
||||
'click .js-sort-reset'() {
|
||||
Session.set('sortBy', '');
|
||||
},
|
||||
'click .js-open-search-view'() {
|
||||
Sidebar.setView('search');
|
||||
if (Sidebar) {
|
||||
Sidebar.setView('search');
|
||||
} else {
|
||||
console.warn('Sidebar not available for setView');
|
||||
}
|
||||
},
|
||||
'click .js-multiselection-activate'() {
|
||||
const currentCard = Utils.getCurrentCardId();
|
||||
|
|
@ -203,6 +220,7 @@ const CreateBoard = BlazeComponent.extendComponent({
|
|||
title: title,
|
||||
permission: 'private',
|
||||
type: 'template-container',
|
||||
migrationVersion: 1, // Latest version - no migration needed
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -246,6 +264,7 @@ const CreateBoard = BlazeComponent.extendComponent({
|
|||
Boards.insert({
|
||||
title,
|
||||
permission: visibility,
|
||||
migrationVersion: 1, // Latest version - no migration needed
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -336,3 +336,36 @@
|
|||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Attachment migration styles */
|
||||
.attachment-item.migrating {
|
||||
position: relative;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.attachment-migration-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.migration-spinner {
|
||||
font-size: 24px;
|
||||
color: #007cba;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.migration-text {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ template(name="attachmentGallery")
|
|||
|
||||
each attachments
|
||||
|
||||
.attachment-item
|
||||
.attachment-item(class="{{#if isAttachmentMigrating _id}}migrating{{/if}}")
|
||||
.attachment-thumbnail-container.open-preview(data-attachment-id="{{_id}}" data-card-id="{{ meta.cardId }}")
|
||||
if link
|
||||
if(isImage)
|
||||
|
|
@ -97,6 +97,12 @@ template(name="attachmentGallery")
|
|||
i.fa.fa-trash.icon(title="{{_ 'delete'}}")
|
||||
a.fa.fa-navicon.icon.js-open-attachment-menu(data-attachment-link="{{link}}" title="{{_ 'attachmentActionsPopup-title'}}")
|
||||
|
||||
// Migration spinner overlay
|
||||
if isAttachmentMigrating _id
|
||||
.attachment-migration-overlay
|
||||
.migration-spinner
|
||||
i.fa.fa-cog.fa-spin
|
||||
.migration-text {{_ 'migrating-attachment'}}
|
||||
|
||||
template(name="attachmentActionsPopup")
|
||||
ul.pop-over-list
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { ObjectID } from 'bson';
|
|||
import DOMPurify from 'dompurify';
|
||||
import { sanitizeHTML, sanitizeText } from '/imports/lib/secureDOMPurify';
|
||||
import uploadProgressManager from '../../lib/uploadProgressManager';
|
||||
import { attachmentMigrationManager } from '/client/lib/attachmentMigrationManager';
|
||||
|
||||
const filesize = require('filesize');
|
||||
const prettyMilliseconds = require('pretty-ms');
|
||||
|
|
@ -576,3 +577,20 @@ BlazeComponent.extendComponent({
|
|||
]
|
||||
}
|
||||
}).register('attachmentRenamePopup');
|
||||
|
||||
// Template helpers for attachment migration status
|
||||
Template.registerHelper('attachmentMigrationStatus', function(attachmentId) {
|
||||
return attachmentMigrationManager.getAttachmentMigrationStatus(attachmentId);
|
||||
});
|
||||
|
||||
Template.registerHelper('isAttachmentMigrating', function(attachmentId) {
|
||||
return attachmentMigrationManager.isAttachmentBeingMigrated(attachmentId);
|
||||
});
|
||||
|
||||
Template.registerHelper('attachmentMigrationProgress', function() {
|
||||
return attachmentMigrationManager.attachmentMigrationProgress.get();
|
||||
});
|
||||
|
||||
Template.registerHelper('attachmentMigrationStatusText', function() {
|
||||
return attachmentMigrationManager.attachmentMigrationStatus.get();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -206,7 +206,9 @@ Template.minicard.helpers({
|
|||
// Show list name if either:
|
||||
// 1. Board-wide setting is enabled, OR
|
||||
// 2. This specific card has the setting enabled
|
||||
return this.currentBoard.allowsShowListsOnMinicard || this.showListOnMinicard;
|
||||
const currentBoard = this.currentBoard;
|
||||
if (!currentBoard) return false;
|
||||
return currentBoard.allowsShowListsOnMinicard || this.showListOnMinicard;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -472,6 +472,14 @@ BlazeComponent.extendComponent({
|
|||
if (!this.selectedBoardId.get()) {
|
||||
return [];
|
||||
}
|
||||
const board = ReactiveCache.getBoard(this.selectedBoardId.get());
|
||||
if (!board) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Ensure default swimlane exists
|
||||
board.getDefaultSwimline();
|
||||
|
||||
const swimlanes = ReactiveCache.getSwimlanes(
|
||||
{
|
||||
boardId: this.selectedBoardId.get()
|
||||
|
|
|
|||
|
|
@ -1,30 +1,38 @@
|
|||
import { Template } from 'meteor/templating';
|
||||
import { migrationManager } from '/imports/lib/migrationManager';
|
||||
import {
|
||||
migrationManager,
|
||||
isMigrating,
|
||||
migrationProgress,
|
||||
migrationStatus,
|
||||
migrationCurrentStep,
|
||||
migrationEstimatedTime,
|
||||
migrationSteps
|
||||
} from '/client/lib/migrationManager';
|
||||
|
||||
Template.migrationProgress.helpers({
|
||||
isMigrating() {
|
||||
return migrationManager.isMigrating.get();
|
||||
return isMigrating.get();
|
||||
},
|
||||
|
||||
migrationProgress() {
|
||||
return migrationManager.migrationProgress.get();
|
||||
return migrationProgress.get();
|
||||
},
|
||||
|
||||
migrationStatus() {
|
||||
return migrationManager.migrationStatus.get();
|
||||
return migrationStatus.get();
|
||||
},
|
||||
|
||||
migrationCurrentStep() {
|
||||
return migrationManager.migrationCurrentStep.get();
|
||||
return migrationCurrentStep.get();
|
||||
},
|
||||
|
||||
migrationEstimatedTime() {
|
||||
return migrationManager.migrationEstimatedTime.get();
|
||||
return migrationEstimatedTime.get();
|
||||
},
|
||||
|
||||
migrationSteps() {
|
||||
const steps = migrationManager.migrationSteps.get();
|
||||
const currentStep = migrationManager.migrationCurrentStep.get();
|
||||
const steps = migrationSteps.get();
|
||||
const currentStep = migrationCurrentStep.get();
|
||||
|
||||
return steps.map(step => ({
|
||||
...step,
|
||||
|
|
@ -36,11 +44,11 @@ Template.migrationProgress.helpers({
|
|||
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();
|
||||
isMigrating.get();
|
||||
migrationProgress.get();
|
||||
migrationStatus.get();
|
||||
migrationCurrentStep.get();
|
||||
migrationEstimatedTime.get();
|
||||
migrationSteps.get();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ class AdminReport extends BlazeComponent {
|
|||
}
|
||||
|
||||
resultsCount() {
|
||||
return this.collection.find().countDocuments();
|
||||
return this.collection.find().count();
|
||||
}
|
||||
|
||||
fileSize(size) {
|
||||
|
|
|
|||
|
|
@ -30,28 +30,33 @@ Template.cronSettings.onCreated(function() {
|
|||
this.boardMigrationStats = new ReactiveVar({});
|
||||
|
||||
// Load initial data
|
||||
this.loadCronData();
|
||||
loadCronData(this);
|
||||
});
|
||||
|
||||
Template.cronSettings.helpers({
|
||||
loading() {
|
||||
return Template.instance().loading.get();
|
||||
const instance = Template.instance();
|
||||
return instance && instance.loading ? instance.loading.get() : true;
|
||||
},
|
||||
|
||||
showMigrations() {
|
||||
return Template.instance().showMigrations.get();
|
||||
const instance = Template.instance();
|
||||
return instance && instance.showMigrations ? instance.showMigrations.get() : true;
|
||||
},
|
||||
|
||||
showBoardOperations() {
|
||||
return Template.instance().showBoardOperations.get();
|
||||
const instance = Template.instance();
|
||||
return instance && instance.showBoardOperations ? instance.showBoardOperations.get() : false;
|
||||
},
|
||||
|
||||
showJobs() {
|
||||
return Template.instance().showJobs.get();
|
||||
const instance = Template.instance();
|
||||
return instance && instance.showJobs ? instance.showJobs.get() : false;
|
||||
},
|
||||
|
||||
showAddJob() {
|
||||
return Template.instance().showAddJob.get();
|
||||
const instance = Template.instance();
|
||||
return instance && instance.showAddJob ? instance.showAddJob.get() : false;
|
||||
},
|
||||
|
||||
migrationProgress() {
|
||||
|
|
@ -86,27 +91,33 @@ Template.cronSettings.helpers({
|
|||
},
|
||||
|
||||
boardOperations() {
|
||||
return Template.instance().boardOperations.get();
|
||||
const instance = Template.instance();
|
||||
return instance && instance.boardOperations ? instance.boardOperations.get() : [];
|
||||
},
|
||||
|
||||
operationStats() {
|
||||
return Template.instance().operationStats.get();
|
||||
const instance = Template.instance();
|
||||
return instance && instance.operationStats ? instance.operationStats.get() : {};
|
||||
},
|
||||
|
||||
pagination() {
|
||||
return Template.instance().pagination.get();
|
||||
const instance = Template.instance();
|
||||
return instance && instance.pagination ? instance.pagination.get() : {};
|
||||
},
|
||||
|
||||
queueStats() {
|
||||
return Template.instance().queueStats.get();
|
||||
const instance = Template.instance();
|
||||
return instance && instance.queueStats ? instance.queueStats.get() : {};
|
||||
},
|
||||
|
||||
systemResources() {
|
||||
return Template.instance().systemResources.get();
|
||||
const instance = Template.instance();
|
||||
return instance && instance.systemResources ? instance.systemResources.get() : {};
|
||||
},
|
||||
|
||||
boardMigrationStats() {
|
||||
return Template.instance().boardMigrationStats.get();
|
||||
const instance = Template.instance();
|
||||
return instance && instance.boardMigrationStats ? instance.boardMigrationStats.get() : {};
|
||||
},
|
||||
|
||||
formatDateTime(date) {
|
||||
|
|
@ -146,7 +157,7 @@ Template.cronSettings.events({
|
|||
instance.showBoardOperations.set(true);
|
||||
instance.showJobs.set(false);
|
||||
instance.showAddJob.set(false);
|
||||
instance.loadBoardOperations();
|
||||
loadBoardOperations(instance);
|
||||
},
|
||||
|
||||
'click .js-cron-jobs'(event) {
|
||||
|
|
@ -156,7 +167,7 @@ Template.cronSettings.events({
|
|||
instance.showBoardOperations.set(false);
|
||||
instance.showJobs.set(true);
|
||||
instance.showAddJob.set(false);
|
||||
instance.loadCronJobs();
|
||||
loadCronJobs(instance);
|
||||
},
|
||||
|
||||
'click .js-cron-add'(event) {
|
||||
|
|
@ -174,8 +185,8 @@ Template.cronSettings.events({
|
|||
console.error('Failed to start migrations:', error);
|
||||
alert('Failed to start migrations: ' + error.message);
|
||||
} else {
|
||||
console.log('Migrations started successfully');
|
||||
Template.instance().pollMigrationProgress();
|
||||
// Migrations started successfully
|
||||
pollMigrationProgress(Template.instance());
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
@ -204,7 +215,7 @@ Template.cronSettings.events({
|
|||
|
||||
'click .js-refresh-jobs'(event) {
|
||||
event.preventDefault();
|
||||
Template.instance().loadCronJobs();
|
||||
loadCronJobs(Template.instance());
|
||||
},
|
||||
|
||||
'click .js-start-job'(event) {
|
||||
|
|
@ -216,7 +227,7 @@ Template.cronSettings.events({
|
|||
alert('Failed to start job: ' + error.message);
|
||||
} else {
|
||||
console.log('Job started successfully');
|
||||
Template.instance().loadCronJobs();
|
||||
loadCronJobs(Template.instance());
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
@ -230,7 +241,7 @@ Template.cronSettings.events({
|
|||
alert('Failed to pause job: ' + error.message);
|
||||
} else {
|
||||
console.log('Job paused successfully');
|
||||
Template.instance().loadCronJobs();
|
||||
loadCronJobs(Template.instance());
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
@ -244,7 +255,7 @@ Template.cronSettings.events({
|
|||
alert('Failed to stop job: ' + error.message);
|
||||
} else {
|
||||
console.log('Job stopped successfully');
|
||||
Template.instance().loadCronJobs();
|
||||
loadCronJobs(Template.instance());
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
@ -259,7 +270,7 @@ Template.cronSettings.events({
|
|||
alert('Failed to remove job: ' + error.message);
|
||||
} else {
|
||||
console.log('Job removed successfully');
|
||||
Template.instance().loadCronJobs();
|
||||
loadCronJobs(Template.instance());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -286,7 +297,7 @@ Template.cronSettings.events({
|
|||
form.reset();
|
||||
Template.instance().showJobs.set(true);
|
||||
Template.instance().showAddJob.set(false);
|
||||
Template.instance().loadCronJobs();
|
||||
loadCronJobs(Template.instance());
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
@ -300,7 +311,7 @@ Template.cronSettings.events({
|
|||
|
||||
'click .js-refresh-board-operations'(event) {
|
||||
event.preventDefault();
|
||||
Template.instance().loadBoardOperations();
|
||||
loadBoardOperations(Template.instance());
|
||||
},
|
||||
|
||||
'click .js-start-test-operation'(event) {
|
||||
|
|
@ -318,7 +329,7 @@ Template.cronSettings.events({
|
|||
alert('Failed to start test operation: ' + error.message);
|
||||
} else {
|
||||
console.log('Test operation started:', result);
|
||||
Template.instance().loadBoardOperations();
|
||||
loadBoardOperations(Template.instance());
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
@ -328,7 +339,7 @@ Template.cronSettings.events({
|
|||
const instance = Template.instance();
|
||||
instance.searchTerm.set(searchTerm);
|
||||
instance.currentPage.set(1);
|
||||
instance.loadBoardOperations();
|
||||
loadBoardOperations(instance);
|
||||
},
|
||||
|
||||
'click .js-prev-page'(event) {
|
||||
|
|
@ -337,7 +348,7 @@ Template.cronSettings.events({
|
|||
const currentPage = instance.currentPage.get();
|
||||
if (currentPage > 1) {
|
||||
instance.currentPage.set(currentPage - 1);
|
||||
instance.loadBoardOperations();
|
||||
loadBoardOperations(instance);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -348,7 +359,7 @@ Template.cronSettings.events({
|
|||
const pagination = instance.pagination.get();
|
||||
if (currentPage < pagination.totalPages) {
|
||||
instance.currentPage.set(currentPage + 1);
|
||||
instance.loadBoardOperations();
|
||||
loadBoardOperations(instance);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -389,16 +400,17 @@ Template.cronSettings.events({
|
|||
console.error('Failed to force board scan:', error);
|
||||
alert('Failed to force board scan: ' + error.message);
|
||||
} else {
|
||||
console.log('Board scan started successfully');
|
||||
// Board scan started successfully
|
||||
// Refresh the data
|
||||
Template.instance().loadBoardOperations();
|
||||
loadBoardOperations(Template.instance());
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Template.cronSettings.prototype.loadCronData = function() {
|
||||
this.loading.set(true);
|
||||
// Helper functions for cron settings
|
||||
function loadCronData(instance) {
|
||||
instance.loading.set(true);
|
||||
|
||||
// Load migration progress
|
||||
Meteor.call('cron.getMigrationProgress', (error, result) => {
|
||||
|
|
@ -412,21 +424,20 @@ Template.cronSettings.prototype.loadCronData = function() {
|
|||
});
|
||||
|
||||
// Load cron jobs
|
||||
this.loadCronJobs();
|
||||
loadCronJobs(instance);
|
||||
|
||||
this.loading.set(false);
|
||||
};
|
||||
instance.loading.set(false);
|
||||
}
|
||||
|
||||
Template.cronSettings.prototype.loadCronJobs = function() {
|
||||
function loadCronJobs(instance) {
|
||||
Meteor.call('cron.getJobs', (error, result) => {
|
||||
if (result) {
|
||||
cronJobs.set(result);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
Template.cronSettings.prototype.loadBoardOperations = function() {
|
||||
const instance = this;
|
||||
function loadBoardOperations(instance) {
|
||||
const page = instance.currentPage.get();
|
||||
const limit = instance.pageSize.get();
|
||||
const searchTerm = instance.searchTerm.get();
|
||||
|
|
@ -474,9 +485,9 @@ Template.cronSettings.prototype.loadBoardOperations = function() {
|
|||
instance.boardMigrationStats.set(result);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
Template.cronSettings.prototype.pollMigrationProgress = function() {
|
||||
function pollMigrationProgress(instance) {
|
||||
const pollInterval = setInterval(() => {
|
||||
Meteor.call('cron.getMigrationProgress', (error, result) => {
|
||||
if (result) {
|
||||
|
|
@ -493,4 +504,4 @@ Template.cronSettings.prototype.pollMigrationProgress = function() {
|
|||
}
|
||||
});
|
||||
}, 1000);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
169
client/lib/attachmentMigrationManager.js
Normal file
169
client/lib/attachmentMigrationManager.js
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
/**
|
||||
* Attachment Migration Manager
|
||||
* Handles migration of attachments from old structure to new structure
|
||||
* with UI feedback and spinners for unconverted attachments
|
||||
*/
|
||||
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
|
||||
// Reactive variables for attachment migration progress
|
||||
export const attachmentMigrationProgress = new ReactiveVar(0);
|
||||
export const attachmentMigrationStatus = new ReactiveVar('');
|
||||
export const isMigratingAttachments = new ReactiveVar(false);
|
||||
export const unconvertedAttachments = new ReactiveVar([]);
|
||||
|
||||
class AttachmentMigrationManager {
|
||||
constructor() {
|
||||
this.migrationCache = new Map(); // Cache migrated attachment IDs
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an attachment needs migration
|
||||
* @param {string} attachmentId - The attachment ID to check
|
||||
* @returns {boolean} - True if attachment needs migration
|
||||
*/
|
||||
needsMigration(attachmentId) {
|
||||
if (this.migrationCache.has(attachmentId)) {
|
||||
return false; // Already migrated
|
||||
}
|
||||
|
||||
try {
|
||||
const attachment = ReactiveCache.getAttachment(attachmentId);
|
||||
if (!attachment) return false;
|
||||
|
||||
// Check if attachment has old structure (no meta field or missing required fields)
|
||||
return !attachment.meta ||
|
||||
!attachment.meta.cardId ||
|
||||
!attachment.meta.boardId ||
|
||||
!attachment.meta.listId;
|
||||
} catch (error) {
|
||||
console.error('Error checking if attachment needs migration:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all unconverted attachments for a board
|
||||
* @param {string} boardId - The board ID
|
||||
* @returns {Array} - Array of unconverted attachments
|
||||
*/
|
||||
getUnconvertedAttachments(boardId) {
|
||||
try {
|
||||
const attachments = ReactiveCache.getAttachments({
|
||||
'meta.boardId': boardId
|
||||
});
|
||||
|
||||
return attachments.filter(attachment => this.needsMigration(attachment._id));
|
||||
} catch (error) {
|
||||
console.error('Error getting unconverted attachments:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start migration for attachments in a board
|
||||
* @param {string} boardId - The board ID
|
||||
*/
|
||||
async startAttachmentMigration(boardId) {
|
||||
if (isMigratingAttachments.get()) {
|
||||
return; // Already migrating
|
||||
}
|
||||
|
||||
isMigratingAttachments.set(true);
|
||||
attachmentMigrationStatus.set('Starting attachment migration...');
|
||||
attachmentMigrationProgress.set(0);
|
||||
|
||||
try {
|
||||
const unconverted = this.getUnconvertedAttachments(boardId);
|
||||
unconvertedAttachments.set(unconverted);
|
||||
|
||||
if (unconverted.length === 0) {
|
||||
attachmentMigrationStatus.set('All attachments are already migrated');
|
||||
attachmentMigrationProgress.set(100);
|
||||
isMigratingAttachments.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Start server-side migration
|
||||
Meteor.call('attachmentMigration.migrateBoardAttachments', boardId, (error, result) => {
|
||||
if (error) {
|
||||
console.error('Failed to start attachment migration:', error);
|
||||
attachmentMigrationStatus.set(`Migration failed: ${error.message}`);
|
||||
isMigratingAttachments.set(false);
|
||||
} else {
|
||||
console.log('Attachment migration started for board:', boardId);
|
||||
this.pollAttachmentMigrationProgress(boardId);
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error starting attachment migration:', error);
|
||||
attachmentMigrationStatus.set(`Migration failed: ${error.message}`);
|
||||
isMigratingAttachments.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll for attachment migration progress
|
||||
* @param {string} boardId - The board ID
|
||||
*/
|
||||
pollAttachmentMigrationProgress(boardId) {
|
||||
const pollInterval = setInterval(() => {
|
||||
Meteor.call('attachmentMigration.getProgress', boardId, (error, result) => {
|
||||
if (error) {
|
||||
console.error('Error getting migration progress:', error);
|
||||
clearInterval(pollInterval);
|
||||
isMigratingAttachments.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result) {
|
||||
attachmentMigrationProgress.set(result.progress);
|
||||
attachmentMigrationStatus.set(result.status);
|
||||
unconvertedAttachments.set(result.unconvertedAttachments || []);
|
||||
|
||||
// Stop polling if migration is complete
|
||||
if (result.progress >= 100 || result.status === 'completed') {
|
||||
clearInterval(pollInterval);
|
||||
isMigratingAttachments.set(false);
|
||||
this.migrationCache.clear(); // Clear cache to refresh data
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an attachment is currently being migrated
|
||||
* @param {string} attachmentId - The attachment ID
|
||||
* @returns {boolean} - True if attachment is being migrated
|
||||
*/
|
||||
isAttachmentBeingMigrated(attachmentId) {
|
||||
const unconverted = unconvertedAttachments.get();
|
||||
return unconverted.some(attachment => attachment._id === attachmentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get migration status for an attachment
|
||||
* @param {string} attachmentId - The attachment ID
|
||||
* @returns {string} - Migration status ('migrated', 'migrating', 'unmigrated')
|
||||
*/
|
||||
getAttachmentMigrationStatus(attachmentId) {
|
||||
if (this.migrationCache.has(attachmentId)) {
|
||||
return 'migrated';
|
||||
}
|
||||
|
||||
if (this.isAttachmentBeingMigrated(attachmentId)) {
|
||||
return 'migrating';
|
||||
}
|
||||
|
||||
return this.needsMigration(attachmentId) ? 'unmigrated' : 'migrated';
|
||||
}
|
||||
}
|
||||
|
||||
export const attachmentMigrationManager = new AttachmentMigrationManager();
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { ReactiveCache } from '/imports/lib/reactiveCache';
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
|
||||
// Reactive variables for conversion progress
|
||||
export const conversionProgress = new ReactiveVar(0);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { ReactiveCache } from '/imports/lib/reactiveCache';
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
|
||||
// Reactive variables for migration progress
|
||||
export const migrationProgress = new ReactiveVar(0);
|
||||
|
|
@ -600,10 +600,16 @@ class MigrationManager {
|
|||
}
|
||||
|
||||
/**
|
||||
* Check if any migrations need to be run
|
||||
* Check if any migrations need to be run for a specific board
|
||||
*/
|
||||
needsMigration() {
|
||||
// Check if any migration step is not completed
|
||||
needsMigration(boardId = null) {
|
||||
if (boardId) {
|
||||
// Check if specific board needs migration based on version
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
return !board || !board.migrationVersion || board.migrationVersion < 1;
|
||||
}
|
||||
|
||||
// Check if any migration step is not completed (global migrations)
|
||||
return this.steps.some(step => !step.completed);
|
||||
}
|
||||
|
||||
|
|
@ -623,6 +629,23 @@ class MigrationManager {
|
|||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a board as migrated
|
||||
*/
|
||||
markBoardAsMigrated(boardId) {
|
||||
try {
|
||||
Meteor.call('boardMigration.markAsMigrated', boardId, 'full_board_migration', (error, result) => {
|
||||
if (error) {
|
||||
console.error('Failed to mark board as migrated:', error);
|
||||
} else {
|
||||
console.log('Board marked as migrated:', boardId);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error marking board as migrated:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start migration process using cron system
|
||||
*/
|
||||
|
|
@ -711,7 +734,7 @@ class MigrationManager {
|
|||
|
||||
// In a real implementation, this would call the actual migration
|
||||
// For now, we'll simulate the migration
|
||||
console.log(`Running migration: ${step.name}`);
|
||||
// Running migration step
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -315,10 +315,10 @@ if (Meteor.isServer) {
|
|||
fs.mkdirSync(storagePath, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
// Add backward compatibility methods
|
||||
Attachments.getAttachmentWithBackwardCompatibility = getAttachmentWithBackwardCompatibility;
|
||||
Attachments.getAttachmentsWithBackwardCompatibility = getAttachmentsWithBackwardCompatibility;
|
||||
}
|
||||
|
||||
// Add backward compatibility methods - available on both client and server
|
||||
Attachments.getAttachmentWithBackwardCompatibility = getAttachmentWithBackwardCompatibility;
|
||||
Attachments.getAttachmentsWithBackwardCompatibility = getAttachmentsWithBackwardCompatibility;
|
||||
|
||||
export default Attachments;
|
||||
|
|
|
|||
|
|
@ -331,6 +331,19 @@ Boards.attachSchema(
|
|||
optional: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
migrationVersion: {
|
||||
/**
|
||||
* The migration version of the board structure.
|
||||
* New boards are created with the latest version and don't need migration.
|
||||
*/
|
||||
type: Number,
|
||||
// eslint-disable-next-line consistent-return
|
||||
autoValue() {
|
||||
if (this.isInsert && !this.isSet) {
|
||||
return 1; // Latest migration version for new boards
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
subtasksDefaultListId: {
|
||||
/**
|
||||
|
|
@ -2196,6 +2209,7 @@ if (Meteor.isServer) {
|
|||
],
|
||||
permission: req.body.permission || 'private',
|
||||
color: req.body.color || 'belize',
|
||||
migrationVersion: 1, // Latest version - no migration needed
|
||||
});
|
||||
const swimlaneId = Swimlanes.insert({
|
||||
title: TAPi18n.__('default'),
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ class MeteorMongoIntegration {
|
|||
this.overrideMongoCollection();
|
||||
|
||||
this.isInitialized = true;
|
||||
console.log('Meteor MongoDB Integration initialized successfully');
|
||||
// Meteor MongoDB Integration initialized successfully (status available in Admin Panel)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -296,11 +296,8 @@ export { meteorMongoIntegration, MeteorMongoIntegration };
|
|||
|
||||
// Auto-initialize if MONGO_URL is available
|
||||
if (Meteor.isServer && process.env.MONGO_URL) {
|
||||
console.log('Auto-initializing Meteor MongoDB Integration with MONGO_URL');
|
||||
// Auto-initializing Meteor MongoDB Integration with MONGO_URL (status available in Admin Panel)
|
||||
meteorMongoIntegration.initialize(process.env.MONGO_URL);
|
||||
}
|
||||
|
||||
// Log initialization
|
||||
if (Meteor.isServer) {
|
||||
console.log('Meteor MongoDB Integration module loaded');
|
||||
}
|
||||
// Meteor MongoDB Integration module loaded (status available in Admin Panel)
|
||||
|
|
|
|||
|
|
@ -288,7 +288,4 @@ const mongodbConnectionManager = new MongoDBConnectionManager();
|
|||
// Export for use in other modules
|
||||
export { mongodbConnectionManager, MongoDBConnectionManager };
|
||||
|
||||
// Log initialization
|
||||
if (Meteor.isServer) {
|
||||
console.log('MongoDB Connection Manager initialized');
|
||||
}
|
||||
// MongoDB Connection Manager initialized (status available in Admin Panel)
|
||||
|
|
|
|||
|
|
@ -270,8 +270,4 @@ const mongodbDriverManager = new MongoDBDriverManager();
|
|||
// Export for use in other modules
|
||||
export { mongodbDriverManager, MongoDBDriverManager };
|
||||
|
||||
// Log initialization
|
||||
if (Meteor.isServer) {
|
||||
console.log('MongoDB Driver Manager initialized');
|
||||
console.log(`Supported MongoDB versions: ${mongodbDriverManager.getSupportedVersions().join(', ')}`);
|
||||
}
|
||||
// MongoDB Driver Manager initialized (status available in Admin Panel)
|
||||
|
|
|
|||
|
|
@ -1850,7 +1850,7 @@ if (Meteor.isServer) {
|
|||
},
|
||||
});
|
||||
Accounts.onCreateUser((options, user) => {
|
||||
const userCount = ReactiveCache.getUsers({}, {}, true).countDocuments();
|
||||
const userCount = ReactiveCache.getUsers({}, {}, true).count();
|
||||
user.isAdmin = userCount === 0;
|
||||
|
||||
if (user.services.oidc) {
|
||||
|
|
|
|||
1495
package-lock.json
generated
1495
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -359,9 +359,12 @@ if (isSandstorm && Meteor.isServer) {
|
|||
// Meteor application. We need to enforce “public” visibility as the sharing
|
||||
// is now handled by Sandstorm.
|
||||
// See https://github.com/wekan/wekan/issues/346
|
||||
// Migration disabled - using backward compatibility approach
|
||||
/*
|
||||
Migrations.add('enforce-public-visibility-for-sandstorm', () => {
|
||||
Boards.update('sandstorm', { $set: { permission: 'public' } });
|
||||
});
|
||||
*/
|
||||
|
||||
// Monkey patch to work around the problem described in
|
||||
// https://github.com/sandstorm-io/meteor-accounts-sandstorm/pull/31
|
||||
|
|
|
|||
|
|
@ -1,6 +1,18 @@
|
|||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
|
||||
// Configure SyncedCron to suppress console logging
|
||||
// This must be done before any SyncedCron operations
|
||||
if (Meteor.isServer) {
|
||||
const { SyncedCron } = require('meteor/percolate:synced-cron');
|
||||
SyncedCron.config({
|
||||
log: false, // Disable console logging
|
||||
collectionName: 'cronJobs', // Use custom collection name
|
||||
utc: false, // Use local time
|
||||
collectionTTL: 172800 // 2 days TTL
|
||||
});
|
||||
}
|
||||
|
||||
let errors = [];
|
||||
if (!process.env.WRITABLE_PATH) {
|
||||
errors.push("WRITABLE_PATH environment variable missing and/or unset, please configure !");
|
||||
|
|
@ -25,9 +37,6 @@ if (errors.length > 0) {
|
|||
process.exit(1);
|
||||
}
|
||||
|
||||
// Import migration runner for on-demand migrations
|
||||
import './migrationRunner';
|
||||
|
||||
// Import cron job storage for persistent job tracking
|
||||
import './cronJobStorage';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,572 +1,204 @@
|
|||
/**
|
||||
* Server-side Attachment Migration System
|
||||
* Handles migration of attachments from old structure to new structure
|
||||
*/
|
||||
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { Attachments, fileStoreStrategyFactory } from '/models/attachments';
|
||||
import { moveToStorage } from '/models/lib/fileStoreStrategy';
|
||||
import os from 'os';
|
||||
import { createHash } from 'crypto';
|
||||
|
||||
// Migration state management
|
||||
const migrationState = {
|
||||
isRunning: false,
|
||||
isPaused: false,
|
||||
targetStorage: null,
|
||||
batchSize: 10,
|
||||
delayMs: 1000,
|
||||
cpuThreshold: 70,
|
||||
progress: 0,
|
||||
totalAttachments: 0,
|
||||
migratedAttachments: 0,
|
||||
currentBatch: [],
|
||||
migrationQueue: [],
|
||||
log: [],
|
||||
startTime: null,
|
||||
lastCpuCheck: 0
|
||||
};
|
||||
// Reactive variables for tracking migration progress
|
||||
const migrationProgress = new ReactiveVar(0);
|
||||
const migrationStatus = new ReactiveVar('');
|
||||
const unconvertedAttachments = new ReactiveVar([]);
|
||||
|
||||
// CPU monitoring
|
||||
function getCpuUsage() {
|
||||
const cpus = os.cpus();
|
||||
let totalIdle = 0;
|
||||
let totalTick = 0;
|
||||
|
||||
cpus.forEach(cpu => {
|
||||
for (const type in cpu.times) {
|
||||
totalTick += cpu.times[type];
|
||||
}
|
||||
totalIdle += cpu.times.idle;
|
||||
});
|
||||
|
||||
const idle = totalIdle / cpus.length;
|
||||
const total = totalTick / cpus.length;
|
||||
const usage = 100 - Math.floor(100 * idle / total);
|
||||
|
||||
return usage;
|
||||
}
|
||||
|
||||
// Logging function
|
||||
function addToLog(message) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const logEntry = `[${timestamp}] ${message}`;
|
||||
migrationState.log.unshift(logEntry);
|
||||
|
||||
// Keep only last 100 log entries
|
||||
if (migrationState.log.length > 100) {
|
||||
migrationState.log = migrationState.log.slice(0, 100);
|
||||
}
|
||||
|
||||
console.log(logEntry);
|
||||
}
|
||||
|
||||
// Get migration status
|
||||
function getMigrationStatus() {
|
||||
return {
|
||||
isRunning: migrationState.isRunning,
|
||||
isPaused: migrationState.isPaused,
|
||||
targetStorage: migrationState.targetStorage,
|
||||
progress: migrationState.progress,
|
||||
totalAttachments: migrationState.totalAttachments,
|
||||
migratedAttachments: migrationState.migratedAttachments,
|
||||
remainingAttachments: migrationState.totalAttachments - migrationState.migratedAttachments,
|
||||
status: migrationState.isRunning ? (migrationState.isPaused ? 'paused' : 'running') : 'idle',
|
||||
log: migrationState.log.slice(0, 10).join('\n'), // Return last 10 log entries
|
||||
startTime: migrationState.startTime,
|
||||
estimatedTimeRemaining: calculateEstimatedTimeRemaining()
|
||||
};
|
||||
}
|
||||
|
||||
// Calculate estimated time remaining
|
||||
function calculateEstimatedTimeRemaining() {
|
||||
if (!migrationState.isRunning || migrationState.migratedAttachments === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const elapsed = Date.now() - migrationState.startTime;
|
||||
const rate = migrationState.migratedAttachments / elapsed; // attachments per ms
|
||||
const remaining = migrationState.totalAttachments - migrationState.migratedAttachments;
|
||||
|
||||
return Math.round(remaining / rate);
|
||||
}
|
||||
|
||||
// Process a single attachment migration
|
||||
function migrateAttachment(attachmentId) {
|
||||
try {
|
||||
const attachment = ReactiveCache.getAttachment(attachmentId);
|
||||
if (!attachment) {
|
||||
addToLog(`Warning: Attachment ${attachmentId} not found`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if already in target storage
|
||||
const currentStorage = fileStoreStrategyFactory.getFileStrategy(attachment, 'original').getStorageName();
|
||||
if (currentStorage === migrationState.targetStorage) {
|
||||
addToLog(`Attachment ${attachmentId} already in target storage ${migrationState.targetStorage}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Perform migration
|
||||
moveToStorage(attachment, migrationState.targetStorage, fileStoreStrategyFactory);
|
||||
addToLog(`Migrated attachment ${attachmentId} from ${currentStorage} to ${migrationState.targetStorage}`);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
addToLog(`Error migrating attachment ${attachmentId}: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Process a batch of attachments
|
||||
function processBatch() {
|
||||
if (!migrationState.isRunning || migrationState.isPaused) {
|
||||
return;
|
||||
class AttachmentMigrationService {
|
||||
constructor() {
|
||||
this.migrationCache = new Map();
|
||||
}
|
||||
|
||||
const batch = migrationState.migrationQueue.splice(0, migrationState.batchSize);
|
||||
if (batch.length === 0) {
|
||||
// Migration complete
|
||||
migrationState.isRunning = false;
|
||||
migrationState.progress = 100;
|
||||
addToLog(`Migration completed. Migrated ${migrationState.migratedAttachments} attachments.`);
|
||||
return;
|
||||
}
|
||||
|
||||
let successCount = 0;
|
||||
batch.forEach(attachmentId => {
|
||||
if (migrateAttachment(attachmentId)) {
|
||||
successCount++;
|
||||
migrationState.migratedAttachments++;
|
||||
}
|
||||
});
|
||||
|
||||
// Update progress
|
||||
migrationState.progress = Math.round((migrationState.migratedAttachments / migrationState.totalAttachments) * 100);
|
||||
|
||||
addToLog(`Processed batch: ${successCount}/${batch.length} successful. Progress: ${migrationState.progress}%`);
|
||||
|
||||
// Check CPU usage
|
||||
const currentTime = Date.now();
|
||||
if (currentTime - migrationState.lastCpuCheck > 5000) { // Check every 5 seconds
|
||||
const cpuUsage = getCpuUsage();
|
||||
migrationState.lastCpuCheck = currentTime;
|
||||
|
||||
if (cpuUsage > migrationState.cpuThreshold) {
|
||||
addToLog(`CPU usage ${cpuUsage}% exceeds threshold ${migrationState.cpuThreshold}%. Pausing migration.`);
|
||||
migrationState.isPaused = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Schedule next batch
|
||||
if (migrationState.isRunning && !migrationState.isPaused) {
|
||||
Meteor.setTimeout(() => {
|
||||
processBatch();
|
||||
}, migrationState.delayMs);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize migration queue
|
||||
function initializeMigrationQueue() {
|
||||
const allAttachments = ReactiveCache.getAttachments();
|
||||
migrationState.totalAttachments = allAttachments.length;
|
||||
migrationState.migrationQueue = allAttachments.map(attachment => attachment._id);
|
||||
migrationState.migratedAttachments = 0;
|
||||
migrationState.progress = 0;
|
||||
|
||||
addToLog(`Initialized migration queue with ${migrationState.totalAttachments} attachments`);
|
||||
}
|
||||
|
||||
// Start migration
|
||||
function startMigration(targetStorage, batchSize, delayMs, cpuThreshold) {
|
||||
if (migrationState.isRunning) {
|
||||
throw new Meteor.Error('migration-already-running', 'Migration is already running');
|
||||
}
|
||||
|
||||
migrationState.isRunning = true;
|
||||
migrationState.isPaused = false;
|
||||
migrationState.targetStorage = targetStorage;
|
||||
migrationState.batchSize = batchSize;
|
||||
migrationState.delayMs = delayMs;
|
||||
migrationState.cpuThreshold = cpuThreshold;
|
||||
migrationState.startTime = Date.now();
|
||||
migrationState.lastCpuCheck = 0;
|
||||
|
||||
initializeMigrationQueue();
|
||||
addToLog(`Started migration to ${targetStorage} with batch size ${batchSize}, delay ${delayMs}ms, CPU threshold ${cpuThreshold}%`);
|
||||
|
||||
// Start processing
|
||||
processBatch();
|
||||
}
|
||||
|
||||
// Pause migration
|
||||
function pauseMigration() {
|
||||
if (!migrationState.isRunning) {
|
||||
throw new Meteor.Error('migration-not-running', 'No migration is currently running');
|
||||
}
|
||||
|
||||
migrationState.isPaused = true;
|
||||
addToLog('Migration paused');
|
||||
}
|
||||
|
||||
// Resume migration
|
||||
function resumeMigration() {
|
||||
if (!migrationState.isRunning) {
|
||||
throw new Meteor.Error('migration-not-running', 'No migration is currently running');
|
||||
}
|
||||
|
||||
if (!migrationState.isPaused) {
|
||||
throw new Meteor.Error('migration-not-paused', 'Migration is not paused');
|
||||
}
|
||||
|
||||
migrationState.isPaused = false;
|
||||
addToLog('Migration resumed');
|
||||
|
||||
// Continue processing
|
||||
processBatch();
|
||||
}
|
||||
|
||||
// Stop migration
|
||||
function stopMigration() {
|
||||
if (!migrationState.isRunning) {
|
||||
throw new Meteor.Error('migration-not-running', 'No migration is currently running');
|
||||
}
|
||||
|
||||
migrationState.isRunning = false;
|
||||
migrationState.isPaused = false;
|
||||
migrationState.migrationQueue = [];
|
||||
addToLog('Migration stopped');
|
||||
}
|
||||
|
||||
// Get attachment storage configuration
|
||||
function getAttachmentStorageConfiguration() {
|
||||
const config = {
|
||||
filesystemPath: process.env.WRITABLE_PATH || '/data',
|
||||
attachmentsPath: `${process.env.WRITABLE_PATH || '/data'}/attachments`,
|
||||
avatarsPath: `${process.env.WRITABLE_PATH || '/data'}/avatars`,
|
||||
gridfsEnabled: true, // Always available
|
||||
s3Enabled: false,
|
||||
s3Endpoint: '',
|
||||
s3Bucket: '',
|
||||
s3Region: '',
|
||||
s3SslEnabled: false,
|
||||
s3Port: 443
|
||||
};
|
||||
|
||||
// Check S3 configuration
|
||||
if (process.env.S3) {
|
||||
/**
|
||||
* Migrate all attachments for a board
|
||||
* @param {string} boardId - The board ID
|
||||
*/
|
||||
async migrateBoardAttachments(boardId) {
|
||||
try {
|
||||
const s3Config = JSON.parse(process.env.S3).s3;
|
||||
if (s3Config && s3Config.key && s3Config.secret && s3Config.bucket) {
|
||||
config.s3Enabled = true;
|
||||
config.s3Endpoint = s3Config.endPoint || '';
|
||||
config.s3Bucket = s3Config.bucket || '';
|
||||
config.s3Region = s3Config.region || '';
|
||||
config.s3SslEnabled = s3Config.sslEnabled || false;
|
||||
config.s3Port = s3Config.port || 443;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing S3 configuration:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
// Get attachment monitoring data
|
||||
function getAttachmentMonitoringData() {
|
||||
const attachments = ReactiveCache.getAttachments();
|
||||
const stats = {
|
||||
totalAttachments: attachments.length,
|
||||
filesystemAttachments: 0,
|
||||
gridfsAttachments: 0,
|
||||
s3Attachments: 0,
|
||||
totalSize: 0,
|
||||
filesystemSize: 0,
|
||||
gridfsSize: 0,
|
||||
s3Size: 0
|
||||
};
|
||||
|
||||
attachments.forEach(attachment => {
|
||||
const storage = fileStoreStrategyFactory.getFileStrategy(attachment, 'original').getStorageName();
|
||||
const size = attachment.size || 0;
|
||||
|
||||
stats.totalSize += size;
|
||||
|
||||
switch (storage) {
|
||||
case 'fs':
|
||||
stats.filesystemAttachments++;
|
||||
stats.filesystemSize += size;
|
||||
break;
|
||||
case 'gridfs':
|
||||
stats.gridfsAttachments++;
|
||||
stats.gridfsSize += size;
|
||||
break;
|
||||
case 's3':
|
||||
stats.s3Attachments++;
|
||||
stats.s3Size += size;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
// Test S3 connection
|
||||
function testS3Connection(s3Config) {
|
||||
// This would implement actual S3 connection testing
|
||||
// For now, we'll just validate the configuration
|
||||
if (!s3Config.secretKey) {
|
||||
throw new Meteor.Error('s3-secret-key-required', 'S3 secret key is required');
|
||||
}
|
||||
|
||||
// In a real implementation, you would test the connection here
|
||||
// For now, we'll just return success
|
||||
return { success: true, message: 'S3 connection test successful' };
|
||||
}
|
||||
|
||||
// Save S3 settings
|
||||
function saveS3Settings(s3Config) {
|
||||
if (!s3Config.secretKey) {
|
||||
throw new Meteor.Error('s3-secret-key-required', 'S3 secret key is required');
|
||||
}
|
||||
|
||||
// In a real implementation, you would save the S3 configuration
|
||||
// For now, we'll just return success
|
||||
return { success: true, message: 'S3 settings saved successfully' };
|
||||
}
|
||||
|
||||
// Meteor methods
|
||||
if (Meteor.isServer) {
|
||||
Meteor.methods({
|
||||
// Migration methods
|
||||
'startAttachmentMigration'(config) {
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'Must be logged in');
|
||||
}
|
||||
|
||||
const user = ReactiveCache.getUser(this.userId);
|
||||
if (!user || !user.isAdmin) {
|
||||
throw new Meteor.Error('not-authorized', 'Admin access required');
|
||||
}
|
||||
|
||||
startMigration(config.targetStorage, config.batchSize, config.delayMs, config.cpuThreshold);
|
||||
return { success: true, message: 'Migration started' };
|
||||
},
|
||||
|
||||
'pauseAttachmentMigration'() {
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'Must be logged in');
|
||||
}
|
||||
|
||||
const user = ReactiveCache.getUser(this.userId);
|
||||
if (!user || !user.isAdmin) {
|
||||
throw new Meteor.Error('not-authorized', 'Admin access required');
|
||||
}
|
||||
|
||||
pauseMigration();
|
||||
return { success: true, message: 'Migration paused' };
|
||||
},
|
||||
|
||||
'resumeAttachmentMigration'() {
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'Must be logged in');
|
||||
}
|
||||
|
||||
const user = ReactiveCache.getUser(this.userId);
|
||||
if (!user || !user.isAdmin) {
|
||||
throw new Meteor.Error('not-authorized', 'Admin access required');
|
||||
}
|
||||
|
||||
resumeMigration();
|
||||
return { success: true, message: 'Migration resumed' };
|
||||
},
|
||||
|
||||
'stopAttachmentMigration'() {
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'Must be logged in');
|
||||
}
|
||||
|
||||
const user = ReactiveCache.getUser(this.userId);
|
||||
if (!user || !user.isAdmin) {
|
||||
throw new Meteor.Error('not-authorized', 'Admin access required');
|
||||
}
|
||||
|
||||
stopMigration();
|
||||
return { success: true, message: 'Migration stopped' };
|
||||
},
|
||||
|
||||
'getAttachmentMigrationSettings'() {
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'Must be logged in');
|
||||
}
|
||||
|
||||
const user = ReactiveCache.getUser(this.userId);
|
||||
if (!user || !user.isAdmin) {
|
||||
throw new Meteor.Error('not-authorized', 'Admin access required');
|
||||
}
|
||||
|
||||
return {
|
||||
batchSize: migrationState.batchSize,
|
||||
delayMs: migrationState.delayMs,
|
||||
cpuThreshold: migrationState.cpuThreshold,
|
||||
status: migrationState.isRunning ? (migrationState.isPaused ? 'paused' : 'running') : 'idle',
|
||||
progress: migrationState.progress
|
||||
};
|
||||
},
|
||||
|
||||
// Configuration methods
|
||||
'getAttachmentStorageConfiguration'() {
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'Must be logged in');
|
||||
}
|
||||
|
||||
const user = ReactiveCache.getUser(this.userId);
|
||||
if (!user || !user.isAdmin) {
|
||||
throw new Meteor.Error('not-authorized', 'Admin access required');
|
||||
}
|
||||
|
||||
return getAttachmentStorageConfiguration();
|
||||
},
|
||||
|
||||
'testS3Connection'(s3Config) {
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'Must be logged in');
|
||||
}
|
||||
|
||||
const user = ReactiveCache.getUser(this.userId);
|
||||
if (!user || !user.isAdmin) {
|
||||
throw new Meteor.Error('not-authorized', 'Admin access required');
|
||||
}
|
||||
|
||||
return testS3Connection(s3Config);
|
||||
},
|
||||
|
||||
'saveS3Settings'(s3Config) {
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'Must be logged in');
|
||||
}
|
||||
|
||||
const user = ReactiveCache.getUser(this.userId);
|
||||
if (!user || !user.isAdmin) {
|
||||
throw new Meteor.Error('not-authorized', 'Admin access required');
|
||||
}
|
||||
|
||||
return saveS3Settings(s3Config);
|
||||
},
|
||||
|
||||
// Monitoring methods
|
||||
'getAttachmentMonitoringData'() {
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'Must be logged in');
|
||||
}
|
||||
|
||||
const user = ReactiveCache.getUser(this.userId);
|
||||
if (!user || !user.isAdmin) {
|
||||
throw new Meteor.Error('not-authorized', 'Admin access required');
|
||||
}
|
||||
|
||||
return getAttachmentMonitoringData();
|
||||
},
|
||||
|
||||
'refreshAttachmentMonitoringData'() {
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'Must be logged in');
|
||||
}
|
||||
|
||||
const user = ReactiveCache.getUser(this.userId);
|
||||
if (!user || !user.isAdmin) {
|
||||
throw new Meteor.Error('not-authorized', 'Admin access required');
|
||||
}
|
||||
|
||||
return getAttachmentMonitoringData();
|
||||
},
|
||||
|
||||
'exportAttachmentMonitoringData'() {
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'Must be logged in');
|
||||
}
|
||||
|
||||
const user = ReactiveCache.getUser(this.userId);
|
||||
if (!user || !user.isAdmin) {
|
||||
throw new Meteor.Error('not-authorized', 'Admin access required');
|
||||
}
|
||||
|
||||
const monitoringData = getAttachmentMonitoringData();
|
||||
const migrationStatus = getMigrationStatus();
|
||||
console.log(`Starting attachment migration for board: ${boardId}`);
|
||||
|
||||
return {
|
||||
timestamp: new Date().toISOString(),
|
||||
monitoring: monitoringData,
|
||||
migration: migrationStatus,
|
||||
system: {
|
||||
cpuUsage: getCpuUsage(),
|
||||
memoryUsage: process.memoryUsage(),
|
||||
uptime: process.uptime()
|
||||
// Get all attachments for the board
|
||||
const attachments = Attachments.find({
|
||||
'meta.boardId': boardId
|
||||
}).fetch();
|
||||
|
||||
const totalAttachments = attachments.length;
|
||||
let migratedCount = 0;
|
||||
|
||||
migrationStatus.set(`Migrating ${totalAttachments} attachments...`);
|
||||
migrationProgress.set(0);
|
||||
|
||||
for (const attachment of attachments) {
|
||||
try {
|
||||
// Check if attachment needs migration
|
||||
if (this.needsMigration(attachment)) {
|
||||
await this.migrateAttachment(attachment);
|
||||
this.migrationCache.set(attachment._id, true);
|
||||
}
|
||||
|
||||
migratedCount++;
|
||||
const progress = Math.round((migratedCount / totalAttachments) * 100);
|
||||
migrationProgress.set(progress);
|
||||
migrationStatus.set(`Migrated ${migratedCount}/${totalAttachments} attachments...`);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error migrating attachment ${attachment._id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Update unconverted attachments list
|
||||
const remainingUnconverted = this.getUnconvertedAttachments(boardId);
|
||||
unconvertedAttachments.set(remainingUnconverted);
|
||||
|
||||
migrationStatus.set('Attachment migration completed');
|
||||
migrationProgress.set(100);
|
||||
|
||||
console.log(`Attachment migration completed for board: ${boardId}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error migrating attachments for board ${boardId}:`, error);
|
||||
migrationStatus.set(`Migration failed: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an attachment needs migration
|
||||
* @param {Object} attachment - The attachment object
|
||||
* @returns {boolean} - True if attachment needs migration
|
||||
*/
|
||||
needsMigration(attachment) {
|
||||
if (this.migrationCache.has(attachment._id)) {
|
||||
return false; // Already migrated
|
||||
}
|
||||
|
||||
// Check if attachment has old structure
|
||||
return !attachment.meta ||
|
||||
!attachment.meta.cardId ||
|
||||
!attachment.meta.boardId ||
|
||||
!attachment.meta.listId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate a single attachment
|
||||
* @param {Object} attachment - The attachment object
|
||||
*/
|
||||
async migrateAttachment(attachment) {
|
||||
try {
|
||||
// Get the card to find board and list information
|
||||
const card = ReactiveCache.getCard(attachment.cardId);
|
||||
if (!card) {
|
||||
console.warn(`Card not found for attachment ${attachment._id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const list = ReactiveCache.getList(card.listId);
|
||||
if (!list) {
|
||||
console.warn(`List not found for attachment ${attachment._id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update attachment with new structure
|
||||
const updateData = {
|
||||
meta: {
|
||||
cardId: attachment.cardId,
|
||||
boardId: list.boardId,
|
||||
listId: card.listId,
|
||||
userId: attachment.userId,
|
||||
createdAt: attachment.createdAt || new Date(),
|
||||
migratedAt: new Date()
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Publications
|
||||
Meteor.publish('attachmentMigrationStatus', function() {
|
||||
if (!this.userId) {
|
||||
return this.ready();
|
||||
}
|
||||
|
||||
const user = ReactiveCache.getUser(this.userId);
|
||||
if (!user || !user.isAdmin) {
|
||||
return this.ready();
|
||||
}
|
||||
|
||||
const self = this;
|
||||
let handle;
|
||||
|
||||
function updateStatus() {
|
||||
const status = getMigrationStatus();
|
||||
self.changed('attachmentMigrationStatus', 'status', status);
|
||||
}
|
||||
|
||||
self.added('attachmentMigrationStatus', 'status', getMigrationStatus());
|
||||
|
||||
// Update every 2 seconds
|
||||
handle = Meteor.setInterval(updateStatus, 2000);
|
||||
|
||||
self.ready();
|
||||
|
||||
self.onStop(() => {
|
||||
if (handle) {
|
||||
Meteor.clearInterval(handle);
|
||||
// Preserve existing meta data if it exists
|
||||
if (attachment.meta) {
|
||||
updateData.meta = {
|
||||
...attachment.meta,
|
||||
...updateData.meta
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Meteor.publish('attachmentMonitoringData', function() {
|
||||
if (!this.userId) {
|
||||
return this.ready();
|
||||
Attachments.update(attachment._id, { $set: updateData });
|
||||
|
||||
console.log(`Migrated attachment ${attachment._id}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error migrating attachment ${attachment._id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const user = ReactiveCache.getUser(this.userId);
|
||||
if (!user || !user.isAdmin) {
|
||||
return this.ready();
|
||||
/**
|
||||
* Get unconverted attachments for a board
|
||||
* @param {string} boardId - The board ID
|
||||
* @returns {Array} - Array of unconverted attachments
|
||||
*/
|
||||
getUnconvertedAttachments(boardId) {
|
||||
try {
|
||||
const attachments = Attachments.find({
|
||||
'meta.boardId': boardId
|
||||
}).fetch();
|
||||
|
||||
return attachments.filter(attachment => this.needsMigration(attachment));
|
||||
} catch (error) {
|
||||
console.error('Error getting unconverted attachments:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const self = this;
|
||||
let handle;
|
||||
/**
|
||||
* Get migration progress
|
||||
* @param {string} boardId - The board ID
|
||||
* @returns {Object} - Migration progress data
|
||||
*/
|
||||
getMigrationProgress(boardId) {
|
||||
const progress = migrationProgress.get();
|
||||
const status = migrationStatus.get();
|
||||
const unconverted = this.getUnconvertedAttachments(boardId);
|
||||
|
||||
function updateMonitoring() {
|
||||
const data = getAttachmentMonitoringData();
|
||||
self.changed('attachmentMonitoringData', 'data', data);
|
||||
}
|
||||
|
||||
self.added('attachmentMonitoringData', 'data', getAttachmentMonitoringData());
|
||||
|
||||
// Update every 10 seconds
|
||||
handle = Meteor.setInterval(updateMonitoring, 10000);
|
||||
|
||||
self.ready();
|
||||
|
||||
self.onStop(() => {
|
||||
if (handle) {
|
||||
Meteor.clearInterval(handle);
|
||||
}
|
||||
});
|
||||
});
|
||||
return {
|
||||
progress,
|
||||
status,
|
||||
unconvertedAttachments: unconverted
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const attachmentMigrationService = new AttachmentMigrationService();
|
||||
|
||||
// Meteor methods
|
||||
Meteor.methods({
|
||||
'attachmentMigration.migrateBoardAttachments'(boardId) {
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
|
||||
return attachmentMigrationService.migrateBoardAttachments(boardId);
|
||||
},
|
||||
|
||||
'attachmentMigration.getProgress'(boardId) {
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
|
||||
return attachmentMigrationService.getMigrationProgress(boardId);
|
||||
},
|
||||
|
||||
'attachmentMigration.getUnconvertedAttachments'(boardId) {
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
|
||||
return attachmentMigrationService.getUnconvertedAttachments(boardId);
|
||||
}
|
||||
});
|
||||
|
||||
export { attachmentMigrationService };
|
||||
|
|
@ -5,7 +5,9 @@
|
|||
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { check, Match } from 'meteor/check';
|
||||
import { cronJobStorage } from './cronJobStorage';
|
||||
import Boards from '/models/boards';
|
||||
|
||||
// Reactive variables for board migration tracking
|
||||
export const unmigratedBoards = new ReactiveVar([]);
|
||||
|
|
@ -38,7 +40,7 @@ class BoardMigrationDetector {
|
|||
this.scanUnmigratedBoards();
|
||||
}, this.scanInterval);
|
||||
|
||||
console.log('Board migration detector started');
|
||||
// Board migration detector started
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -73,7 +75,7 @@ class BoardMigrationDetector {
|
|||
}
|
||||
|
||||
// Check if memory usage is reasonable
|
||||
if (resources.memoryUsage > 70) {
|
||||
if (resources.memoryUsage > 85) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -117,7 +119,7 @@ class BoardMigrationDetector {
|
|||
migrationScanInProgress.set(true);
|
||||
|
||||
try {
|
||||
console.log('Scanning for unmigrated boards...');
|
||||
// Scanning for unmigrated boards
|
||||
|
||||
// Get all boards from the database
|
||||
const boards = this.getAllBoards();
|
||||
|
|
@ -132,7 +134,7 @@ class BoardMigrationDetector {
|
|||
unmigratedBoards.set(unmigrated);
|
||||
lastMigrationScan.set(new Date());
|
||||
|
||||
console.log(`Found ${unmigrated.length} unmigrated boards`);
|
||||
// Found unmigrated boards
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error scanning for unmigrated boards:', error);
|
||||
|
|
@ -213,10 +215,19 @@ class BoardMigrationDetector {
|
|||
/**
|
||||
* Start migration for a specific board
|
||||
*/
|
||||
async startBoardMigration(board) {
|
||||
async startBoardMigration(boardId) {
|
||||
try {
|
||||
console.log(`Starting migration for board: ${board.title || board._id}`);
|
||||
|
||||
const board = Boards.findOne(boardId);
|
||||
if (!board) {
|
||||
throw new Error(`Board ${boardId} not found`);
|
||||
}
|
||||
|
||||
// Check if board already has latest migration version
|
||||
if (board.migrationVersion && board.migrationVersion >= 1) {
|
||||
console.log(`Board ${boardId} already has latest migration version`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create migration job for this board
|
||||
const jobId = `board_migration_${board._id}_${Date.now()}`;
|
||||
|
||||
|
|
@ -246,7 +257,7 @@ class BoardMigrationDetector {
|
|||
return jobId;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error starting migration for board ${board._id}:`, error);
|
||||
console.error(`Error starting migration for board ${boardId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
@ -271,7 +282,7 @@ class BoardMigrationDetector {
|
|||
* Force a full scan of all boards
|
||||
*/
|
||||
async forceScan() {
|
||||
console.log('Forcing full board migration scan...');
|
||||
// Forcing full board migration scan
|
||||
await this.scanUnmigratedBoards();
|
||||
}
|
||||
|
||||
|
|
@ -315,7 +326,7 @@ class BoardMigrationDetector {
|
|||
const updatedUnmigrated = currentUnmigrated.filter(b => b._id !== boardId);
|
||||
unmigratedBoards.set(updatedUnmigrated);
|
||||
|
||||
console.log(`Marked board ${boardId} as migrated for ${migrationType}`);
|
||||
// Marked board as migrated
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error marking board ${boardId} as migrated:`, error);
|
||||
|
|
@ -353,6 +364,8 @@ Meteor.methods({
|
|||
},
|
||||
|
||||
'boardMigration.getBoardStatus'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
|
|
@ -361,10 +374,23 @@ Meteor.methods({
|
|||
},
|
||||
|
||||
'boardMigration.markAsMigrated'(boardId, migrationType) {
|
||||
check(boardId, String);
|
||||
check(migrationType, String);
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
|
||||
return boardMigrationDetector.markBoardAsMigrated(boardId, migrationType);
|
||||
},
|
||||
|
||||
'boardMigration.startBoardMigration'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
|
||||
return boardMigrationDetector.startBoardMigration(boardId);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class CronJobStorage {
|
|||
constructor() {
|
||||
this.maxConcurrentJobs = this.getMaxConcurrentJobs();
|
||||
this.cpuThreshold = 80; // CPU usage threshold percentage
|
||||
this.memoryThreshold = 90; // Memory usage threshold percentage
|
||||
this.memoryThreshold = 95; // Memory usage threshold percentage (increased for better job processing)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -379,12 +379,12 @@ Meteor.startup(() => {
|
|||
// Resume incomplete jobs
|
||||
const resumedJobs = cronJobStorage.resumeIncompleteJobs();
|
||||
if (resumedJobs.length > 0) {
|
||||
console.log(`Resumed ${resumedJobs.length} incomplete cron jobs:`, resumedJobs);
|
||||
// Resumed incomplete cron jobs
|
||||
}
|
||||
|
||||
// Cleanup old jobs
|
||||
const cleanup = cronJobStorage.cleanupOldJobs();
|
||||
if (cleanup.removedQueue > 0 || cleanup.removedStatus > 0 || cleanup.removedSteps > 0) {
|
||||
console.log('Cleaned up old cron jobs:', cleanup);
|
||||
// Cleaned up old cron jobs
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -247,8 +247,10 @@ class CronMigrationManager {
|
|||
// Start job processor
|
||||
this.startJobProcessor();
|
||||
|
||||
// Update cron jobs list
|
||||
this.updateCronJobsList();
|
||||
// Update cron jobs list after a short delay to allow SyncedCron to initialize
|
||||
Meteor.setTimeout(() => {
|
||||
this.updateCronJobsList();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -263,7 +265,7 @@ class CronMigrationManager {
|
|||
this.processJobQueue();
|
||||
}, 5000); // Check every 5 seconds
|
||||
|
||||
console.log('Cron job processor started with CPU throttling');
|
||||
// Cron job processor started with CPU throttling
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -469,7 +471,7 @@ class CronMigrationManager {
|
|||
const { boardId, boardTitle, migrationType } = jobData;
|
||||
|
||||
try {
|
||||
console.log(`Starting board migration for ${boardTitle || boardId}`);
|
||||
// Starting board migration
|
||||
|
||||
// Create migration steps for this board
|
||||
const steps = this.createBoardMigrationSteps(boardId, migrationType);
|
||||
|
|
@ -503,7 +505,7 @@ class CronMigrationManager {
|
|||
// Mark board as migrated
|
||||
this.markBoardAsMigrated(boardId, migrationType);
|
||||
|
||||
console.log(`Completed board migration for ${boardTitle || boardId}`);
|
||||
// Completed board migration
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Board migration failed for ${boardId}:`, error);
|
||||
|
|
@ -633,7 +635,7 @@ class CronMigrationManager {
|
|||
*/
|
||||
async runMigrationStep(step) {
|
||||
try {
|
||||
console.log(`Starting migration: ${step.name}`);
|
||||
// Starting migration step
|
||||
|
||||
cronMigrationCurrentStep.set(step.name);
|
||||
cronMigrationStatus.set(`Running: ${step.description}`);
|
||||
|
|
@ -654,7 +656,7 @@ class CronMigrationManager {
|
|||
step.progress = 100;
|
||||
step.status = 'completed';
|
||||
|
||||
console.log(`Completed migration: ${step.name}`);
|
||||
// Completed migration step
|
||||
|
||||
// Update progress
|
||||
this.updateProgress();
|
||||
|
|
@ -873,6 +875,13 @@ class CronMigrationManager {
|
|||
* Update cron jobs list
|
||||
*/
|
||||
updateCronJobsList() {
|
||||
// Check if SyncedCron is available and has jobs
|
||||
if (!SyncedCron || !SyncedCron.jobs || !Array.isArray(SyncedCron.jobs)) {
|
||||
// SyncedCron not available or no jobs yet
|
||||
cronJobs.set([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const jobs = SyncedCron.jobs.map(job => {
|
||||
const step = this.migrationSteps.find(s => s.cronName === job.name);
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,404 +0,0 @@
|
|||
/**
|
||||
* 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()
|
||||
};
|
||||
}
|
||||
});
|
||||
1581
server/migrations.js
1581
server/migrations.js
File diff suppressed because it is too large
Load diff
|
|
@ -13,57 +13,40 @@ import { meteorMongoIntegration } from '/models/lib/meteorMongoIntegration';
|
|||
|
||||
// Initialize MongoDB driver system on server startup
|
||||
Meteor.startup(async function() {
|
||||
console.log('=== MongoDB Driver System Startup ===');
|
||||
// MongoDB Driver System Startup (status available in Admin Panel)
|
||||
|
||||
try {
|
||||
// Check if MONGO_URL is available
|
||||
const mongoUrl = process.env.MONGO_URL;
|
||||
if (!mongoUrl) {
|
||||
console.log('MONGO_URL not found, skipping MongoDB driver initialization');
|
||||
// MONGO_URL not found, skipping MongoDB driver initialization
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('MONGO_URL found, initializing MongoDB driver system...');
|
||||
console.log(`Connection string: ${mongoUrl.replace(/\/\/.*@/, '//***:***@')}`); // Hide credentials
|
||||
// MONGO_URL found, initializing MongoDB driver system
|
||||
// Connection string: (credentials hidden for security)
|
||||
|
||||
// Initialize the Meteor integration
|
||||
meteorMongoIntegration.initialize(mongoUrl);
|
||||
|
||||
// Test the connection
|
||||
console.log('Testing MongoDB connection...');
|
||||
const testResult = await meteorMongoIntegration.testConnection();
|
||||
|
||||
if (testResult.success) {
|
||||
console.log('✅ MongoDB connection test successful');
|
||||
console.log(` Driver: ${testResult.driver}`);
|
||||
console.log(` Version: ${testResult.version}`);
|
||||
// MongoDB connection test successful
|
||||
// Driver and version information available in Admin Panel
|
||||
} else {
|
||||
console.log('❌ MongoDB connection test failed');
|
||||
console.log(` Error: ${testResult.error}`);
|
||||
console.log(` Driver: ${testResult.driver}`);
|
||||
console.log(` Version: ${testResult.version}`);
|
||||
// MongoDB connection test failed
|
||||
// Error details available in Admin Panel
|
||||
}
|
||||
|
||||
// Log connection statistics
|
||||
// Connection statistics available in Admin Panel
|
||||
const stats = meteorMongoIntegration.getStats();
|
||||
console.log('MongoDB Driver System Statistics:');
|
||||
console.log(` Initialized: ${stats.isInitialized}`);
|
||||
console.log(` Custom Connection: ${stats.hasCustomConnection}`);
|
||||
console.log(` Supported Versions: ${mongodbDriverManager.getSupportedVersions().join(', ')}`);
|
||||
|
||||
// Log driver compatibility information
|
||||
console.log('MongoDB Driver Compatibility:');
|
||||
// Driver compatibility information available in Admin Panel
|
||||
const supportedVersions = mongodbDriverManager.getSupportedVersions();
|
||||
supportedVersions.forEach(version => {
|
||||
const driverInfo = mongodbDriverManager.getDriverInfo(
|
||||
mongodbDriverManager.getDriverForVersion(version)
|
||||
);
|
||||
if (driverInfo) {
|
||||
console.log(` MongoDB ${version}: ${driverInfo.driver} v${driverInfo.version}`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('=== MongoDB Driver System Ready ===');
|
||||
// MongoDB Driver System Ready (status available in Admin Panel)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error during MongoDB driver system startup:', error.message);
|
||||
|
|
|
|||
|
|
@ -706,11 +706,11 @@ function findCards(sessionId, query) {
|
|||
};
|
||||
|
||||
if (cards) {
|
||||
update.$set.totalHits = cards.countDocuments();
|
||||
update.$set.totalHits = cards.count();
|
||||
update.$set.lastHit =
|
||||
query.projection.skip + query.projection.limit < cards.countDocuments()
|
||||
query.projection.skip + query.projection.limit < cards.count()
|
||||
? query.projection.skip + query.projection.limit
|
||||
: cards.countDocuments();
|
||||
: cards.count();
|
||||
update.$set.cards = cards.map(card => {
|
||||
return card._id;
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue