Fixes to make board showing correctly.

Thanks to xet7 !
This commit is contained in:
Lauri Ojansivu 2025-10-12 03:48:21 +03:00
parent ffb02fe0ec
commit bd8c565415
33 changed files with 2372 additions and 2747 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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 {

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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