Merge branch 'main' into feature/reactive-cache-async-migration

This commit is contained in:
Harry Adel 2026-02-17 16:45:06 +02:00
commit 5212f3beb3
328 changed files with 15124 additions and 3392 deletions

View file

@ -1,14 +1,14 @@
/**
* Comprehensive Board Migration System
*
*
* This migration handles all database structure changes from previous Wekan versions
* to the current per-swimlane lists structure. It ensures:
*
*
* 1. All cards are visible with proper swimlaneId and listId
* 2. Lists are per-swimlane (no shared lists across swimlanes)
* 3. No empty lists are created
* 4. Handles various database structure versions from git history
*
*
* Supported versions and their database structures:
* - v7.94 and earlier: Shared lists across all swimlanes
* - v8.00-v8.02: Transition period with mixed structures
@ -178,7 +178,7 @@ class ComprehensiveBoardMigration {
const updateProgress = (stepName, stepProgress, stepStatus, stepDetails = null) => {
currentStep++;
const overallProgress = Math.round((currentStep / totalSteps) * 100);
const progressData = {
overallProgress,
currentStep: currentStep,
@ -206,7 +206,7 @@ class ComprehensiveBoardMigration {
issuesFound: results.steps.analyze.issueCount,
needsMigration: results.steps.analyze.needsMigration
});
// Step 2: Fix orphaned cards
updateProgress('fix_orphaned_cards', 0, 'Fixing orphaned cards...');
results.steps.fixOrphanedCards = await this.fixOrphanedCards(boardId, (progress, status) => {
@ -323,7 +323,7 @@ class ComprehensiveBoardMigration {
if (!card.listId) {
// Find or create a default list for this swimlane
const swimlaneId = updates.swimlaneId || card.swimlaneId;
let defaultList = lists.find(list =>
let defaultList = lists.find(list =>
list.swimlaneId === swimlaneId && list.title === 'Default'
);
@ -426,7 +426,7 @@ class ComprehensiveBoardMigration {
// Check if we already have a list with the same title in this swimlane
let targetList = existingLists.find(list => list.title === originalList.title);
if (!targetList) {
// Create a new list for this swimlane
const newListData = {
@ -508,12 +508,12 @@ class ComprehensiveBoardMigration {
for (const list of lists) {
const listCards = cards.filter(card => card.listId === list._id);
if (listCards.length === 0) {
// Remove empty list
Lists.remove(list._id);
listsRemoved++;
if (process.env.DEBUG === 'true') {
console.log(`Removed empty list: ${list.title} (${list._id})`);
}
@ -563,7 +563,7 @@ class ComprehensiveBoardMigration {
const avatarUrl = user.profile.avatarUrl;
let needsUpdate = false;
let cleanUrl = avatarUrl;
// Check if URL has problematic parameters
if (avatarUrl.includes('auth=false') || avatarUrl.includes('brokenIsFine=true')) {
// Remove problematic parameters
@ -573,13 +573,13 @@ class ComprehensiveBoardMigration {
cleanUrl = cleanUrl.replace(/\?$/g, '');
needsUpdate = true;
}
// Check if URL is using old CollectionFS format
if (avatarUrl.includes('/cfs/files/avatars/')) {
cleanUrl = cleanUrl.replace('/cfs/files/avatars/', '/cdn/storage/avatars/');
needsUpdate = true;
}
// Check if URL is missing the /cdn/storage/avatars/ prefix
if (avatarUrl.includes('avatars/') && !avatarUrl.includes('/cdn/storage/avatars/') && !avatarUrl.includes('/cfs/files/avatars/')) {
// This might be a relative URL, make it absolute
@ -588,7 +588,7 @@ class ComprehensiveBoardMigration {
needsUpdate = true;
}
}
if (needsUpdate) {
// Update user's avatar URL
Users.update(user._id, {
@ -597,7 +597,7 @@ class ComprehensiveBoardMigration {
modifiedAt: new Date()
}
});
avatarsFixed++;
}
}
@ -619,7 +619,7 @@ class ComprehensiveBoardMigration {
const attachmentUrl = attachment.url;
let needsUpdate = false;
let cleanUrl = attachmentUrl;
// Check if URL has problematic parameters
if (attachmentUrl.includes('auth=false') || attachmentUrl.includes('brokenIsFine=true')) {
// Remove problematic parameters
@ -629,26 +629,26 @@ class ComprehensiveBoardMigration {
cleanUrl = cleanUrl.replace(/\?$/g, '');
needsUpdate = true;
}
// Check if URL is using old CollectionFS format
if (attachmentUrl.includes('/cfs/files/attachments/')) {
cleanUrl = cleanUrl.replace('/cfs/files/attachments/', '/cdn/storage/attachments/');
needsUpdate = true;
}
// Check if URL has /original/ path that should be removed
if (attachmentUrl.includes('/original/')) {
cleanUrl = cleanUrl.replace(/\/original\/[^\/\?#]+/, '');
needsUpdate = true;
}
// If we have a file ID, generate a universal URL
const fileId = attachment._id;
if (fileId && !isUniversalFileUrl(cleanUrl, 'attachment')) {
cleanUrl = generateUniversalAttachmentUrl(fileId);
needsUpdate = true;
}
if (needsUpdate) {
// Update attachment URL
Attachments.update(attachment._id, {
@ -657,7 +657,7 @@ class ComprehensiveBoardMigration {
modifiedAt: new Date()
}
});
attachmentsFixed++;
}
}

View file

@ -1,6 +1,6 @@
/**
* Delete Duplicate Empty Lists Migration
*
*
* Safely deletes empty duplicate lists from a board:
* 1. First converts any shared lists to per-swimlane lists
* 2. Only deletes per-swimlane lists that:
@ -42,9 +42,9 @@ class DeleteDuplicateEmptyListsMigration {
const listCards = cards.filter(card => card.listId === list._id);
if (listCards.length === 0) {
// Check if there's a duplicate list with the same title that has cards
const duplicateListsWithSameTitle = lists.filter(l =>
l._id !== list._id &&
l.title === list.title &&
const duplicateListsWithSameTitle = lists.filter(l =>
l._id !== list._id &&
l.title === list.title &&
l.boardId === boardId
);
@ -137,8 +137,8 @@ class DeleteDuplicateEmptyListsMigration {
if (swimlaneCards.length > 0) {
// Check if per-swimlane list already exists
const existingList = lists.find(l =>
l.title === sharedList.title &&
const existingList = lists.find(l =>
l.title === sharedList.title &&
l.swimlaneId === swimlane._id &&
l._id !== sharedList._id
);
@ -230,9 +230,9 @@ class DeleteDuplicateEmptyListsMigration {
}
// Safety check 3: There must be another list with the same title on the same board that has cards
const duplicateListsWithSameTitle = lists.filter(l =>
l._id !== list._id &&
l.title === list.title &&
const duplicateListsWithSameTitle = lists.filter(l =>
l._id !== list._id &&
l.title === list.title &&
l.boardId === boardId
);

View file

@ -1,11 +1,11 @@
/**
* Migration: Ensure all entities have valid swimlaneId
*
*
* This migration ensures that:
* 1. All cards have a valid swimlaneId
* 2. All lists have a valid swimlaneId (if applicable)
* 3. Orphaned entities (without valid swimlaneId) are moved to a "Rescued Data" swimlane
*
*
* This is similar to the existing rescue migration but specifically for swimlaneId validation
*/
@ -60,7 +60,7 @@ function getOrCreateRescuedSwimlane(boardId) {
});
rescuedSwimlane = Swimlanes.findOne(swimlaneId);
Activities.insert({
userId: 'migration',
type: 'swimlane',
@ -164,7 +164,7 @@ function getOrCreateRescuedSwimlane(boardId) {
let rescuedCount = 0;
const allCards = Cards.find({}).fetch();
allCards.forEach(card => {
if (!card.swimlaneId) return; // Handled by fixCardsWithoutSwimlaneId
@ -173,7 +173,7 @@ function getOrCreateRescuedSwimlane(boardId) {
if (!swimlane) {
// Orphaned card - swimlane doesn't exist
const rescuedSwimlane = getOrCreateRescuedSwimlane(card.boardId);
if (rescuedSwimlane) {
Cards.update(card._id, {
$set: { swimlaneId: rescuedSwimlane._id },
@ -290,7 +290,7 @@ function getOrCreateRescuedSwimlane(boardId) {
);
console.log(`Migration ${MIGRATION_NAME} completed successfully`);
return {
success: true,
cardsFixed: cardResults.fixedCount,
@ -306,7 +306,7 @@ function getOrCreateRescuedSwimlane(boardId) {
// Install validation hooks on startup (always run these for data integrity)
Meteor.startup(() => {
if (!Meteor.isServer) return;
try {
addSwimlaneIdValidationHooks();
console.log('SwimlaneId validation hooks installed');

View file

@ -61,17 +61,17 @@ class FixAllFileUrlsMigration {
*/
hasProblematicUrl(url) {
if (!url) return false;
// Check for auth parameters
if (url.includes('auth=false') || url.includes('brokenIsFine=true')) {
return true;
}
// Check for absolute URLs with domains
if (url.startsWith('http://') || url.startsWith('https://')) {
return true;
}
// Check for ROOT_URL dependencies
if (Meteor.isServer && process.env.ROOT_URL) {
try {
@ -83,12 +83,12 @@ class FixAllFileUrlsMigration {
// Ignore URL parsing errors
}
}
// Check for non-universal file URLs
if (url.includes('/cfs/files/') && !isUniversalFileUrl(url, 'attachment') && !isUniversalFileUrl(url, 'avatar')) {
return true;
}
return false;
}
@ -120,7 +120,7 @@ class FixAllFileUrlsMigration {
}
console.log(`Universal file URL migration completed for board ${boardId}. Fixed ${filesFixed} file URLs.`);
return {
success: errors.length === 0,
filesFixed,
@ -145,12 +145,12 @@ class FixAllFileUrlsMigration {
for (const user of users) {
if (user.profile && user.profile.avatarUrl) {
const avatarUrl = user.profile.avatarUrl;
if (this.hasProblematicUrl(avatarUrl)) {
try {
// Extract file ID from URL
const fileId = extractFileIdFromUrl(avatarUrl, 'avatar');
let cleanUrl;
if (fileId) {
// Generate universal URL
@ -159,7 +159,7 @@ class FixAllFileUrlsMigration {
// Clean existing URL
cleanUrl = cleanFileUrl(avatarUrl, 'avatar');
}
if (cleanUrl && cleanUrl !== avatarUrl) {
// Update user's avatar URL
Users.update(user._id, {
@ -168,9 +168,9 @@ class FixAllFileUrlsMigration {
modifiedAt: new Date()
}
});
avatarsFixed++;
if (process.env.DEBUG === 'true') {
console.log(`Fixed avatar URL for user ${user.username}: ${avatarUrl} -> ${cleanUrl}`);
}
@ -200,7 +200,7 @@ class FixAllFileUrlsMigration {
try {
const fileId = attachment._id;
const cleanUrl = generateUniversalAttachmentUrl(fileId);
if (cleanUrl && cleanUrl !== attachment.url) {
// Update attachment URL
Attachments.update(attachment._id, {
@ -209,9 +209,9 @@ class FixAllFileUrlsMigration {
modifiedAt: new Date()
}
});
attachmentsFixed++;
if (process.env.DEBUG === 'true') {
console.log(`Fixed attachment URL: ${attachment.url} -> ${cleanUrl}`);
}
@ -239,7 +239,7 @@ class FixAllFileUrlsMigration {
try {
const fileId = attachment._id || extractFileIdFromUrl(attachment.url, 'attachment');
const cleanUrl = fileId ? generateUniversalAttachmentUrl(fileId) : cleanFileUrl(attachment.url, 'attachment');
if (cleanUrl && cleanUrl !== attachment.url) {
// Update attachment with fixed URL
Attachments.update(attachment._id, {
@ -248,9 +248,9 @@ class FixAllFileUrlsMigration {
modifiedAt: new Date()
}
});
attachmentsFixed++;
if (process.env.DEBUG === 'true') {
console.log(`Fixed attachment URL ${attachment._id}`);
}

View file

@ -37,7 +37,7 @@ class FixAvatarUrlsMigration {
}
}
}
return false;
}
@ -65,7 +65,7 @@ class FixAvatarUrlsMigration {
const avatarUrl = user.profile.avatarUrl;
let needsUpdate = false;
let cleanUrl = avatarUrl;
// Check if URL has problematic parameters
if (avatarUrl.includes('auth=false') || avatarUrl.includes('brokenIsFine=true')) {
// Remove problematic parameters
@ -75,13 +75,13 @@ class FixAvatarUrlsMigration {
cleanUrl = cleanUrl.replace(/\?$/g, '');
needsUpdate = true;
}
// Check if URL is using old CollectionFS format
if (avatarUrl.includes('/cfs/files/avatars/')) {
cleanUrl = cleanUrl.replace('/cfs/files/avatars/', '/cdn/storage/avatars/');
needsUpdate = true;
}
// Check if URL is missing the /cdn/storage/avatars/ prefix
if (avatarUrl.includes('avatars/') && !avatarUrl.includes('/cdn/storage/avatars/') && !avatarUrl.includes('/cfs/files/avatars/')) {
// This might be a relative URL, make it absolute
@ -90,14 +90,14 @@ class FixAvatarUrlsMigration {
needsUpdate = true;
}
}
// If we have a file ID, generate a universal URL
const fileId = extractFileIdFromUrl(avatarUrl, 'avatar');
if (fileId && !isUniversalFileUrl(cleanUrl, 'avatar')) {
cleanUrl = generateUniversalAvatarUrl(fileId);
needsUpdate = true;
}
if (needsUpdate) {
// Update user's avatar URL
Users.update(user._id, {
@ -106,9 +106,9 @@ class FixAvatarUrlsMigration {
modifiedAt: new Date()
}
});
avatarsFixed++;
if (process.env.DEBUG === 'true') {
console.log(`Fixed avatar URL for user ${user.username}: ${avatarUrl} -> ${cleanUrl}`);
}
@ -117,7 +117,7 @@ class FixAvatarUrlsMigration {
}
console.log(`Avatar URL fix migration completed for board ${boardId}. Fixed ${avatarsFixed} avatar URLs.`);
return {
success: true,
avatarsFixed,

View file

@ -1,17 +1,17 @@
/**
* Fix Missing Lists Migration
*
*
* This migration fixes the issue where cards have incorrect listId references
* due to the per-swimlane lists change. It detects cards with mismatched
* listId/swimlaneId and creates the missing lists.
*
*
* Issue: When upgrading from v7.94 to v8.02, cards that were in different
* swimlanes but shared the same list now have wrong listId references.
*
*
* Example:
* - Card1: listId: 'HB93dWNnY5bgYdtxc', swimlaneId: 'sK69SseWkh3tMbJvg'
* - Card2: listId: 'HB93dWNnY5bgYdtxc', swimlaneId: 'XeecF9nZxGph4zcT4'
*
*
* Card2 should have a different listId that corresponds to its swimlane.
*/
@ -90,7 +90,7 @@ class FixMissingListsMigration {
// Create maps for efficient lookup
const listSwimlaneMap = new Map();
const swimlaneListsMap = new Map();
lists.forEach(list => {
listSwimlaneMap.set(list._id, list.swimlaneId || '');
if (!swimlaneListsMap.has(list.swimlaneId || '')) {
@ -142,7 +142,7 @@ class FixMissingListsMigration {
// Check if we already have a list with the same title in this swimlane
let targetList = existingLists.find(list => list.title === originalList.title);
if (!targetList) {
// Create a new list for this swimlane
const newListData = {
@ -168,7 +168,7 @@ class FixMissingListsMigration {
const newListId = Lists.insert(newListData);
targetList = { _id: newListId, ...newListData };
createdLists++;
if (process.env.DEBUG === 'true') {
console.log(`Created new list "${originalList.title}" for swimlane ${swimlaneId}`);
}
@ -198,7 +198,7 @@ class FixMissingListsMigration {
if (process.env.DEBUG === 'true') {
console.log(`Fix missing lists migration completed for board ${boardId}: created ${createdLists} lists, updated ${updatedCards} cards`);
}
return {
success: true,
createdLists,

View file

@ -1,8 +1,8 @@
/**
* Restore All Archived Migration
*
*
* Restores all archived swimlanes, lists, and cards.
* If any restored items are missing swimlaneId, listId, or cardId,
* If any restored items are missing swimlaneId, listId, or cardId,
* creates/assigns proper IDs to make them visible.
*/
@ -90,7 +90,7 @@ class RestoreAllArchivedMigration {
if (!list.swimlaneId) {
// Try to find a suitable swimlane or use default
let targetSwimlane = activeSwimlanes.find(s => !s.archived);
if (!targetSwimlane) {
// No active swimlane found, create default
const swimlaneId = Swimlanes.insert({
@ -139,11 +139,11 @@ class RestoreAllArchivedMigration {
if (!card.listId) {
// Find or create a default list
let targetList = allLists.find(l => !l.archived);
if (!targetList) {
// No active list found, create one
const defaultSwimlane = allSwimlanes.find(s => !s.archived) || allSwimlanes[0];
const listId = Lists.insert({
title: TAPi18n.__('default'),
boardId: boardId,

View file

@ -1,6 +1,6 @@
/**
* Restore Lost Cards Migration
*
*
* Finds and restores cards and lists that have missing swimlaneId, listId, or are orphaned.
* Creates a "Lost Cards" swimlane and restores visibility of lost items.
* Only processes non-archived items.