Per-User and Board-level data save fixes. Per-User is collapse, width, height. Per-Board is Swimlanes, Lists, Cards etc.

Thanks to xet7 !

Fixes #5997
This commit is contained in:
Lauri Ojansivu 2025-12-23 07:49:37 +02:00
parent 2e0e1e56b5
commit 414b8dbf41
11 changed files with 2273 additions and 57 deletions

View file

@ -0,0 +1,542 @@
# Wekan Persistence Architecture Improvements
## Changes Implemented
This document describes the architectural improvements made to Wekan's persistence layer to ensure proper separation between board-level data and per-user UI preferences.
---
## 1. Removed Board-Level UI State (✅ COMPLETED)
### 1.1 Collapsed State Removed from Schemas
**Changes:**
- ❌ Removed `collapsed` field from Swimlanes schema ([models/swimlanes.js](models/swimlanes.js))
- ❌ Removed `collapsed` field from Lists schema ([models/lists.js](models/lists.js))
- ❌ Removed `collapse()` mutation from Swimlanes
- ❌ Removed collapsed field from REST API `PUT /api/boards/:boardId/lists/:listId`
**Rationale:**
Collapsed state is a per-user UI preference and should never be stored at the board level. This prevents conflicts where one user collapses a swimlane/list and affects all other users.
**Migration:**
- Existing board-level `collapsed` values will be ignored
- Users' personal collapse preferences are stored in `profile.collapsedSwimlanes` and `profile.collapsedLists`
- For non-logged-in users, collapse state is stored in localStorage and cookies
---
## 2. LocalStorage Validation & Cleanup (✅ COMPLETED)
### 2.1 New Validation Utility
**File:** [client/lib/localStorageValidator.js](client/lib/localStorageValidator.js)
**Features:**
- ✅ Validates all numbers (swimlane heights, list widths) are within valid ranges
- ✅ Validates all booleans (collapse states) are actual boolean values
- ✅ Removes corrupted or invalid data
- ✅ Limits stored data to prevent localStorage bloat:
- Maximum 50 boards per key
- Maximum 100 items per board
- ✅ Auto-cleanup on app startup (once per day)
- ✅ Validation ranges:
- List widths: 100-1000 pixels
- Swimlane heights: -1 (auto) or 50-2000 pixels
- Collapsed states: boolean only
**Usage:**
```javascript
import { validateAndCleanLocalStorage, shouldRunCleanup } from '/client/lib/localStorageValidator';
// Auto-runs on startup
Meteor.startup(() => {
if (shouldRunCleanup()) {
validateAndCleanLocalStorage();
}
});
```
### 2.2 Updated User Storage Methods
**File:** [models/lib/userStorageHelpers.js](models/lib/userStorageHelpers.js)
**Functions:**
- `getValidatedNumber(key, boardId, itemId, defaultValue, min, max)` - Get with validation
- `setValidatedNumber(key, boardId, itemId, value, min, max)` - Set with validation
- `getValidatedBoolean(key, boardId, itemId, defaultValue)` - Get boolean
- `setValidatedBoolean(key, boardId, itemId, value)` - Set boolean
**Validation Applied To:**
- `wekan-list-widths` - List column widths
- `wekan-list-constraints` - List max-width constraints
- `wekan-swimlane-heights` - Swimlane row heights
- `wekan-collapsed-lists` - List collapse states
- `wekan-collapsed-swimlanes` - Swimlane collapse states
---
## 3. Per-User Position History System (✅ COMPLETED)
### 3.1 New Collection: UserPositionHistory
**File:** [models/userPositionHistory.js](models/userPositionHistory.js)
**Purpose:**
Track all position changes (moves, reorders) per user with full undo/redo support.
**Schema Fields:**
- `userId` - User who made the change
- `boardId` - Board where change occurred
- `entityType` - Type: 'swimlane', 'list', 'card', 'checklist', 'checklistItem'
- `entityId` - ID of the moved entity
- `actionType` - Type: 'move', 'create', 'delete', 'restore', 'archive'
- `previousState` - Complete state before change (blackbox object)
- `newState` - Complete state after change (blackbox object)
- `previousSort`, `newSort` - Sort positions
- `previousSwimlaneId`, `newSwimlaneId` - Swimlane changes
- `previousListId`, `newListId` - List changes
- `previousBoardId`, `newBoardId` - Board changes
- `isCheckpoint` - User-marked savepoint
- `checkpointName` - Name for the savepoint
- `batchId` - Group related changes together
- `createdAt` - Timestamp
**Key Features:**
- ✅ Automatic tracking of all card movements
- ✅ Per-user isolation (users only see their own history)
- ✅ Checkpoint/savepoint system for marking important states
- ✅ Batch operations support (group related changes)
- ✅ Auto-cleanup (keeps last 1000 entries per user per board)
- ✅ Checkpoints are never deleted
- ✅ Full undo capability if entity still exists
**Helpers:**
- `getDescription()` - Human-readable change description
- `canUndo()` - Check if change can be undone
- `undo()` - Reverse the change
**Indexes:**
```javascript
{ userId: 1, boardId: 1, createdAt: -1 }
{ userId: 1, entityType: 1, entityId: 1 }
{ userId: 1, isCheckpoint: 1 }
{ batchId: 1 }
{ createdAt: 1 }
```
### 3.2 Meteor Methods for History Management
**Available Methods:**
```javascript
// Create a checkpoint/savepoint
Meteor.call('userPositionHistory.createCheckpoint', boardId, checkpointName);
// Undo a specific change
Meteor.call('userPositionHistory.undo', historyId);
// Get recent changes
Meteor.call('userPositionHistory.getRecent', boardId, limit);
// Get all checkpoints
Meteor.call('userPositionHistory.getCheckpoints', boardId);
// Restore to a checkpoint (undo all changes after it)
Meteor.call('userPositionHistory.restoreToCheckpoint', checkpointId);
```
### 3.3 Automatic Tracking Integration
**Card Moves:** [models/cards.js](models/cards.js)
The `card.move()` method now automatically tracks changes:
```javascript
// Capture previous state
const previousState = {
boardId: this.boardId,
swimlaneId: this.swimlaneId,
listId: this.listId,
sort: this.sort,
};
// After update, track in history
UserPositionHistory.trackChange({
userId: Meteor.userId(),
boardId: this.boardId,
entityType: 'card',
entityId: this._id,
actionType: 'move',
previousState,
newState: { boardId, swimlaneId, listId, sort },
});
```
**TODO:** Add similar tracking for:
- List reordering
- Swimlane reordering
- Checklist/item reordering
---
## 4. SwimlaneId Validation & Rescue (✅ COMPLETED)
### 4.1 Migration: Ensure Valid Swimlane IDs
**File:** [server/migrations/ensureValidSwimlaneIds.js](server/migrations/ensureValidSwimlaneIds.js)
**Purpose:**
Ensure all cards and lists have valid swimlaneId references, rescuing orphaned data.
**Operations:**
1. **Fix Cards Without SwimlaneId**
- Finds cards with missing/null/empty swimlaneId
- Assigns to board's default swimlane
- Creates default swimlane if none exists
2. **Fix Lists Without SwimlaneId**
- Finds lists with missing swimlaneId
- Sets to empty string (for backward compatibility)
3. **Rescue Orphaned Cards**
- Finds cards where swimlaneId points to deleted swimlane
- Creates "Rescued Data (Missing Swimlane)" swimlane (red color, at end)
- Moves orphaned cards there
- Logs activity for transparency
4. **Add Validation Hooks**
- `Cards.before.insert` - Auto-assign default swimlaneId
- `Cards.before.update` - Prevent swimlaneId removal
- Ensures swimlaneId is ALWAYS saved
**Migration Tracking:**
Stored in `migrations` collection:
```javascript
{
name: 'ensure-valid-swimlane-ids',
version: 1,
completedAt: Date,
results: {
cardsFixed: Number,
listsFixed: Number,
cardsRescued: Number,
}
}
```
---
## 5. TODO: Undo/Redo UI (⏳ IN PROGRESS)
### 5.1 Planned UI Components
**Board Toolbar:**
- [ ] Undo button (with keyboard shortcut Ctrl+Z)
- [ ] Redo button (with keyboard shortcut Ctrl+Shift+Z)
- [ ] History dropdown showing recent changes
- [ ] "Create Checkpoint" button
**History Sidebar:**
- [ ] List of recent changes with descriptions
- [ ] Visual timeline
- [ ] Checkpoint markers
- [ ] "Restore to This Point" buttons
- [ ] Search/filter history
### 5.2 Keyboard Shortcuts
```javascript
// To implement in client/lib/keyboard.js
Mousetrap.bind('ctrl+z', () => {
// Undo last change
});
Mousetrap.bind('ctrl+shift+z', () => {
// Redo last undone change
});
Mousetrap.bind('ctrl+shift+s', () => {
// Create checkpoint
});
```
---
## 6. TODO: Search History Feature (⏳ NOT STARTED)
### 6.1 Requirements
Per the user request:
> "For board-level data, for each field (like description, comments etc) at Search All Boards have translatable options to also search from history of boards where user is member of board"
### 6.2 Proposed Implementation
**New Collection: FieldHistory**
```javascript
{
boardId: String,
entityType: String, // 'card', 'list', 'swimlane', 'board'
entityId: String,
fieldName: String, // 'description', 'title', 'comments', etc.
previousValue: String,
newValue: String,
changedBy: String, // userId
changedAt: Date,
}
```
**Search Enhancement:**
- Add "Include History" checkbox to Search All Boards
- Search not just current field values, but also historical values
- Show results with indicator: "Found in history (changed 2 days ago)"
- Allow filtering by:
- Current values only
- Historical values only
- Both current and historical
**Translatable Field Options:**
```javascript
const searchableFieldsI18n = {
'card-title': 'search-card-titles',
'card-description': 'search-card-descriptions',
'card-comments': 'search-card-comments',
'list-title': 'search-list-titles',
'swimlane-title': 'search-swimlane-titles',
'board-title': 'search-board-titles',
// Add i18n keys for each searchable field
};
```
### 6.3 Storage Considerations
**Challenge:** Field history can grow very large
**Solutions:**
1. Only track fields explicitly marked for history
2. Limit history depth (e.g., last 100 changes per field)
3. Auto-delete history older than X months (configurable)
4. Option to disable per board
**Suggested Settings:**
```javascript
{
enableFieldHistory: true,
trackedFields: ['description', 'title', 'comments'],
historyRetentionDays: 90,
maxHistoryPerField: 100,
}
```
---
## 7. Data Validation Summary
### 7.1 Validation Applied
| Data Type | Storage | Validation | Range/Type |
|-----------|---------|------------|------------|
| List Width | localStorage + profile | Number | 100-1000 px |
| List Constraint | localStorage + profile | Number | 100-1000 px |
| Swimlane Height | localStorage + profile | Number | -1 (auto) or 50-2000 px |
| Collapsed Lists | localStorage + profile | Boolean | true/false |
| Collapsed Swimlanes | localStorage + profile | Boolean | true/false |
| SwimlaneId | MongoDB (cards) | String (required) | Valid ObjectId |
| SwimlaneId | MongoDB (lists) | String (optional) | Valid ObjectId or '' |
### 7.2 Auto-Cleanup Rules
**LocalStorage:**
- Corrupted data → Removed
- Invalid types → Removed
- Out-of-range values → Removed
- Excess boards (>50) → Oldest removed
- Excess items per board (>100) → Oldest removed
- Cleanup frequency → Daily (if needed)
**UserPositionHistory:**
- Keeps last 1000 entries per user per board
- Checkpoints never deleted
- Cleanup frequency → Daily
- Old entries (beyond 1000) → Deleted
---
## 8. Migration Guide
### 8.1 For Existing Installations
**Automatic Migrations:**
1. ✅ `ensureValidSwimlaneIds` - Runs automatically on server start
2. ✅ LocalStorage cleanup - Runs automatically on client start (once/day)
**Manual Actions Required:**
- None - all migrations are automatic
### 8.2 For Developers
**When Adding New Per-User Preferences:**
1. Add field to user profile schema:
```javascript
'profile.myNewPreference': {
type: Object,
optional: true,
blackbox: true,
}
```
2. Add validation function:
```javascript
function validateMyNewPreference(data) {
// Validate structure
// Return cleaned data
}
```
3. Add localStorage support:
```javascript
getMyNewPreferenceFromStorage(boardId, itemId) {
if (this._id) {
return this.getMyNewPreference(boardId, itemId);
}
return getValidatedData('wekan-my-preference', validators.myPreference);
}
```
4. Add to cleanup routine in `localStorageValidator.js`
---
## 9. Testing Checklist
### 9.1 Manual Testing
- [ ] Collapse swimlane → Reload → Should remain collapsed (logged-in)
- [ ] Collapse list → Reload → Should remain collapsed (logged-in)
- [ ] Resize list width → Reload → Should maintain width (logged-in)
- [ ] Resize swimlane height → Reload → Should maintain height (logged-in)
- [ ] Logout → Collapse swimlane → Reload → Should remain collapsed (cookies)
- [ ] Move card → Check UserPositionHistory created
- [ ] Move card → Click undo → Card returns to original position
- [ ] Create checkpoint → Move cards → Restore to checkpoint → Cards return
- [ ] Corrupted localStorage → Should be cleaned on next startup
- [ ] Card without swimlaneId → Should be rescued to rescue swimlane
### 9.2 Automated Testing
**Unit Tests Needed:**
- [ ] `localStorageValidator.js` - All validation functions
- [ ] `userStorageHelpers.js` - Get/set functions
- [ ] `userPositionHistory.js` - Undo logic
- [ ] `ensureValidSwimlaneIds.js` - Migration logic
**Integration Tests Needed:**
- [ ] Card move triggers history entry
- [ ] Undo actually reverses move
- [ ] Checkpoint restore works correctly
- [ ] localStorage validation on startup
- [ ] Rescue migration creates rescue swimlane
---
## 10. Performance Considerations
### 10.1 Indexes Added
```javascript
// UserPositionHistory
{ userId: 1, boardId: 1, createdAt: -1 }
{ userId: 1, entityType: 1, entityId: 1 }
{ userId: 1, isCheckpoint: 1 }
{ batchId: 1 }
{ createdAt: 1 }
```
### 10.2 Query Optimization
- UserPositionHistory queries limited to 100 results max
- Auto-cleanup prevents unbounded growth
- Checkpoints indexed separately for fast retrieval
### 10.3 localStorage Limits
- Maximum 50 boards per key (prevents quota exceeded)
- Maximum 100 items per board
- Daily cleanup of excess data
---
## 11. Security Considerations
### 11.1 User Isolation
- ✅ UserPositionHistory isolated per-user (userId filter on all queries)
- ✅ Users can only undo their own changes
- ✅ Checkpoints are per-user
- ✅ History never shared between users
### 11.2 Validation
- ✅ All localStorage data validated before use
- ✅ Number ranges enforced
- ✅ Type checking on all inputs
- ✅ Invalid data rejected (not just sanitized)
### 11.3 Authorization
- ✅ Must be board member to create history entries
- ✅ Must be board member to undo changes
- ✅ Cannot undo other users' changes
---
## 12. Future Enhancements
### 12.1 Planned Features
1. **Field-Level History**
- Track changes to card descriptions, titles, comments
- Search across historical values
- "What was this card's description last week?"
2. **Collaborative Undo**
- See other users' recent changes
- Undo with conflict resolution
- Merge strategies for simultaneous changes
3. **Export History**
- Export position history to CSV/JSON
- Audit trail for compliance
- Analytics on card movement patterns
4. **Visual Timeline**
- Interactive timeline of board changes
- Playback mode to see board evolution
- Heatmap of frequently moved cards
### 12.2 Optimization Opportunities
1. **Batch Operations**
- Group multiple card moves into single history entry
- Reduce database writes
2. **Compression**
- Compress old history entries
- Store diffs instead of full states
3. **Archival**
- Move very old history to separate collection
- Keep last N months in hot storage
---
## Document History
- **Created**: 2025-12-23
- **Last Updated**: 2025-12-23
- **Status**: Implementation In Progress
- **Completed**: Sections 1-4
- **In Progress**: Section 5-6
- **Planned**: Section 6.1-6.3

View file

@ -0,0 +1,472 @@
# Wekan Persistence Audit Report
## Overview
This document audits the persistence mechanisms for Wekan board data, including swimlanes, lists, cards, checklists, and their properties (order, color, background, titles, etc.), as well as per-user settings.
---
## 1. BOARD-LEVEL PERSISTENCE (Persisted Across All Users)
### 1.1 Swimlanes
**Collection**: `swimlanes` ([models/swimlanes.js](models/swimlanes.js))
**Persisted Fields**:
- ✅ `title` - Swimlane title (via `rename()` mutation)
- ✅ `sort` - Swimlane ordering/position (decimal number)
- ✅ `color` - Swimlane color (via `setColor()` mutation)
- ✅ `collapsed` - Swimlane collapsed state (via `collapse()` mutation) **⚠️ See note below**
- ✅ `archived` - Swimlane archived status
**Persistence Mechanism**:
- Direct MongoDB updates via `Swimlanes.update()` and `Swimlanes.direct.update()`
- Automatic timestamps: `updatedAt`, `modifiedAt` fields
- Activity tracking for title changes and archive/restore operations
**Issues Found**:
- ⚠️ **ISSUE**: `collapsed` field in swimlanes.js line 127 is set to `defaultValue: false` but the `isCollapsed()` helper (line 251-263) checks for per-user stored values. This creates a mismatch between board-level and per-user storage.
---
### 1.2 Lists
**Collection**: `lists` ([models/lists.js](models/lists.js))
**Persisted Fields**:
- ✅ `title` - List title
- ✅ `sort` - List ordering/position (decimal number)
- ✅ `color` - List color
- ✅ `collapsed` - List collapsed state (board-wide via REST API line 768-775) **⚠️ See note below**
- ✅ `starred` - List starred status
- ✅ `wipLimit` - WIP limit configuration
- ✅ `archived` - List archived status
**Persistence Mechanism**:
- Direct MongoDB updates via `Lists.update()` and `Lists.direct.update()`
- Automatic timestamps: `updatedAt`, `modifiedAt`
- Activity tracking for title changes, archive/restore
**Issues Found**:
- ⚠️ **ISSUE**: Similar to swimlanes, `collapsed` field (line 147) defaults to `false` but the `isCollapsed()` helper (line 303-311) also checks for per-user stored values. The REST API allows board-level collapsed state updates (line 768-775), but client also stores per-user via `getCollapsedListFromStorage()`.
- ⚠️ **ISSUE**: The `swimlaneId` field is part of the list (line 48), but `draggableLists()` method (line 275) filters by board only, suggesting lists are shared across swimlanes rather than per-swimlane.
---
### 1.3 Cards
**Collection**: `cards` ([models/cards.js](models/cards.js))
**Persisted Fields**:
- ✅ `title` - Card title
- ✅ `sort` - Card ordering/position within list
- ✅ `color` - Card color (via `setColor()` mutation, line 2268)
- ✅ `boardId`, `swimlaneId`, `listId` - Card location
- ✅ `archived` - Card archived status
- ✅ `description` - Card description
- ✅ Custom fields, labels, members, assignees, etc.
**Persistence Mechanism**:
- `move()` method (line 2063+) handles reordering and moving cards across swimlanes/lists/boards
- Automatic timestamp updates via `modifiedAt`, `dateLastActivity`
- Activity tracking for moves, title changes, etc.
- Attachment metadata updated alongside card moves (line 2101-2115)
**Issues Found**:
- ✅ **OK**: Order/sort persistence working correctly via card.move() and card.moveOptionalArgs()
- ✅ **OK**: Color persistence working correctly
- ✅ **OK**: Title changes persisted automatically
---
### 1.4 Checklists
**Collection**: `checklists` ([models/checklists.js](models/checklists.js))
**Persisted Fields**:
- ✅ `title` - Checklist title (via `setTitle()` mutation)
- ✅ `sort` - Checklist ordering (decimal number)
- ✅ `hideCheckedChecklistItems` - Toggle for hiding checked items
- ✅ `hideAllChecklistItems` - Toggle for hiding all items
**Persistence Mechanism**:
- Direct MongoDB updates via `Checklists.update()`
- Automatic timestamps: `createdAt`, `modifiedAt`
- Activity tracking for creation and removal
---
### 1.5 Checklist Items
**Collection**: `checklistItems` ([models/checklistItems.js](models/checklistItems.js))
**Persisted Fields**:
- ✅ `title` - Item text (via `setTitle()` mutation)
- ✅ `sort` - Item ordering within checklist (decimal number)
- ✅ `isFinished` - Item completion status (via `check()`, `uncheck()`, `toggleItem()` mutations)
**Persistence Mechanism**:
- `move()` mutation (line 159-168) handles reordering within checklists
- Direct MongoDB updates via `ChecklistItems.update()`
- Automatic timestamps: `createdAt`, `modifiedAt`
- Activity tracking for item creation/removal and completion state changes
**Issue Found**:
- ✅ **OK**: Item order and completion status persist correctly
---
### 1.6 Position History Tracking
**Collection**: `positionHistory` ([models/positionHistory.js](models/positionHistory.js))
**Purpose**: Tracks original positions of swimlanes, lists, and cards before changes
**Features**:
- ✅ Stores original `sort` position
- ✅ Stores original titles
- ✅ Supports swimlanes, lists, and cards
- ✅ Provides helpers to check if entity moved from original position
**Implementation Notes**:
- Swimlanes track position automatically on insert (swimlanes.js line 387-393)
- Lists track position automatically on insert (lists.js line 487+)
- Can detect moves via `hasMoved()` and `hasMovedFromOriginalPosition()` helpers
---
## 2. PER-USER SETTINGS (NOT Persisted Across Boards)
### 2.1 Per-Board, Per-User Settings
**Storage**: User `profile` subdocuments ([models/users.js](models/users.js))
#### A. List Widths
- **Field**: `profile.listWidths` (line 527)
- **Structure**: `listWidths[boardId][listId] = width`
- **Persistence**: Via `setListWidth()` mutation (line 1834)
- **Retrieval**: `getListWidth()`, `getListWidthFromStorage()` (line 1288-1313)
- **Constraints**: Also stored in `profile.listConstraints`
- ✅ **Status**: Working correctly
#### B. Swimlane Heights
- **Field**: `profile.swimlaneHeights` (searchable in line 1047+)
- **Structure**: `swimlaneHeights[boardId][swimlaneId] = height`
- **Persistence**: Via `setSwimlaneHeight()` mutation (line 1878)
- **Retrieval**: `getSwimlaneHeight()`, `getSwimlaneHeightFromStorage()` (line 1050-1080)
- ✅ **Status**: Working correctly
#### C. Collapsed Swimlanes (Per-User)
- **Field**: `profile.collapsedSwimlanes` (line 1900)
- **Structure**: `collapsedSwimlanes[boardId][swimlaneId] = boolean`
- **Persistence**: Via `setCollapsedSwimlane()` mutation (line 1900-1906)
- **Retrieval**: `getCollapsedSwimlaneFromStorage()` (swimlanes.js line 251-263)
- **Client-Side Fallback**: `Users.getPublicCollapsedSwimlane()` for public/non-logged-in users (users.js line 60-73)
- ✅ **Status**: Working correctly for logged-in users
#### D. Collapsed Lists (Per-User)
- **Field**: `profile.collapsedLists` (line 1893)
- **Structure**: `collapsedLists[boardId][listId] = boolean`
- **Persistence**: Via `setCollapsedList()` mutation (line 1893-1899)
- **Retrieval**: `getCollapsedListFromStorage()` (lists.js line 303-311)
- **Client-Side Fallback**: `Users.getPublicCollapsedList()` for public users (users.js line 44-52)
- ✅ **Status**: Working correctly for logged-in users
#### E. Card Collapsed State (Global Per-User)
- **Field**: `profile.cardCollapsed` (line 267)
- **Persistence**: Via `setCardCollapsed()` method (line 2088-2091)
- **Retrieval**: `cardCollapsed()` helper in cardDetails.js (line 100-107)
- **Client-Side Fallback**: `Users.getPublicCardCollapsed()` for public users (users.js line 80-85)
- ✅ **Status**: Working correctly (applies to all boards for a user)
#### F. Card Maximized State (Global Per-User)
- **Field**: `profile.cardMaximized` (line 260)
- **Persistence**: Via `toggleCardMaximized()` mutation (line 1720-1726)
- **Retrieval**: `hasCardMaximized()` helper (line 1194-1196)
- ✅ **Status**: Working correctly
#### G. Board Workspace Trees (Global Per-User)
- **Field**: `profile.boardWorkspacesTree` (line 1981-2026)
- **Purpose**: Stores nested workspace structure for organizing boards
- **Persistence**: Via `setWorkspacesTree()` method (line 1995-2000)
- ✅ **Status**: Working correctly
#### H. Board Workspace Assignments (Global Per-User)
- **Field**: `profile.boardWorkspaceAssignments` (line 2002-2011)
- **Purpose**: Maps each board to a workspace ID
- **Persistence**: Via `assignBoardToWorkspace()` and `unassignBoardFromWorkspace()` methods
- ✅ **Status**: Working correctly
#### I. All Boards Workspaces Setting
- **Field**: `profile.boardView` (line 1807)
- **Persistence**: Via `setBoardView()` method (line 1805-1809)
- **Description**: Per-user preference for "All Boards" view style
- ✅ **Status**: Working correctly
---
### 2.2 Client-Side Storage (Non-Logged-In Users)
**Storage Methods**:
1. **Cookies** (via `readCookieMap()`/`writeCookieMap()`):
- `wekan-collapsed-lists` - Collapsed list states (users.js line 44-58)
- `wekan-collapsed-swimlanes` - Collapsed swimlane states
2. **localStorage**:
- `wekan-list-widths` - List widths (getListWidthFromStorage, line 1316-1327)
- `wekan-swimlane-heights` - Swimlane heights (setSwimlaneHeightToStorage, line 1100-1123)
**Coverage**:
- ✅ Collapse status for lists and swimlanes
- ✅ Width constraints for lists
- ✅ Height constraints for swimlanes
- ❌ Card collapsed state (only via cookies, fallback available)
---
## 3. CRITICAL FINDINGS & ISSUES
### 3.1 HIGH PRIORITY ISSUES
#### Issue #1: Collapsed State Inconsistency (Swimlanes)
**Severity**: HIGH
**Location**: [models/swimlanes.js](models/swimlanes.js) lines 127, 251-263
**Problem**:
- The swimlane schema defines `collapsed` as a board-level field (defaults to false)
- But the `isCollapsed()` helper prioritizes per-user stored values from the user profile
- This creates confusion: is collapsed state board-wide or per-user?
**Expected Behavior**: Per-user settings should be stored in `profile.collapsedSwimlanes`, not in the swimlane document itself.
**Recommendation**:
```javascript
// CURRENT (WRONG):
collapsed: {
type: Boolean,
defaultValue: false, // Board-wide field
},
// SUGGESTED (CORRECT):
// Remove 'collapsed' from swimlane schema
// Only store per-user state in profile.collapsedSwimlanes
```
---
#### Issue #2: Collapsed State Inconsistency (Lists)
**Severity**: HIGH
**Location**: [models/lists.js](models/lists.js) lines 147, 303-311
**Problem**:
- Similar to swimlanes, lists have a board-level `collapsed` field
- REST API allows updating this field (line 768-775)
- But `isCollapsed()` helper checks per-user values first
- Migrations copy `collapsed` status between lists (fixMissingListsMigration.js line 165)
**Recommendation**: Clarify whether collapsed state should be:
1. **Option A**: Board-level only (remove per-user override)
2. **Option B**: Per-user only (remove board-level field)
3. **Option C**: Hybrid with clear precedence rules
---
#### Issue #3: Swimlane/List Organization Model Unclear
**Severity**: MEDIUM
**Location**: [models/lists.js](models/lists.js) lines 48, 201-230, 275
**Problem**:
- Lists have a `swimlaneId` field but `draggableLists()` filters by `boardId` only
- Some methods reference `myLists()` which filters by both `boardId` and `swimlaneId`
- Migrations suggest lists were transitioning from per-swimlane to shared-across-swimlane model
**Questions**:
- Are lists shared across all swimlanes or isolated to each swimlane?
- What happens when dragging a list to a different swimlane?
**Recommendation**: Document the intended architecture clearly.
---
### 3.2 MEDIUM PRIORITY ISSUES
#### Issue #4: Position History Only Tracks Original Position
**Severity**: MEDIUM
**Location**: [models/positionHistory.js](models/positionHistory.js)
**Problem**:
- Position history tracks the *original* position when an entity is created
- It does NOT track subsequent moves/reorders
- Historical audit trail of all position changes is lost
**Impact**: Cannot determine full history of where a card/list was located over time
**Recommendation**: Consider extending to track all position changes with timestamps.
---
#### Issue #5: Card Collapsed State is Global Per-User, Not Per-Card
**Severity**: LOW
**Location**: [models/users.js](models/users.js) line 267, users.js line 2088-2091
**Problem**:
- `profile.cardCollapsed` is a single boolean affecting all cards for a user
- It's not per-card or per-board, just a global toggle
- Name is misleading
**Recommendation**: Consider renaming to `cardDetailsCollapsedByDefault` or similar.
---
#### Issue #6: Public User Settings Storage Incomplete
**Severity**: MEDIUM
**Location**: [models/users.js](models/users.js) lines 44-85
**Problem**:
- Cookie-based storage for public users only covers:
- Collapsed lists
- Collapsed swimlanes
- Missing storage for:
- List widths
- Swimlane heights
- Card collapsed state
**Impact**: Public/non-logged-in users lose UI preferences on page reload
**Recommendation**: Implement localStorage storage for all per-user preferences.
---
### 3.3 VERIFICATION CHECKLIST
| Item | Status | Notes |
|------|--------|-------|
| Swimlane order persistence | ✅ | Via `sort` field, board-level |
| List order persistence | ✅ | Via `sort` field, board-level |
| Card order persistence | ✅ | Via `sort` field, card.move() |
| Checklist order persistence | ✅ | Via `sort` field |
| Checklist item order persistence | ✅ | Via `sort` field, ChecklistItems.move() |
| Swimlane color changes | ✅ | Via `setColor()` mutation |
| List color changes | ✅ | Via REST API or direct update |
| Card color changes | ✅ | Via `setColor()` mutation |
| Swimlane title changes | ✅ | Via `rename()` mutation, activity tracked |
| List title changes | ✅ | Via REST API or `rename()` mutation, activity tracked |
| Card title changes | ✅ | Via direct update, activity tracked |
| Checklist title changes | ✅ | Via `setTitle()` mutation |
| Checklist item title changes | ✅ | Via `setTitle()` mutation |
| Per-user list widths | ✅ | Via `profile.listWidths` |
| Per-user swimlane heights | ✅ | Via `profile.swimlaneHeights` |
| Per-user swimlane collapse state | ✅ | Via `profile.collapsedSwimlanes` |
| Per-user list collapse state | ✅ | Via `profile.collapsedLists` |
| Per-user card collapse state | ✅ | Via `profile.cardCollapsed` |
| Per-user board workspace organization | ✅ | Via `profile.boardWorkspacesTree` |
| Activity logging for changes | ✅ | Via Activities collection |
---
## 4. RECOMMENDATIONS
### 4.1 Immediate Actions
1. **Clarify Collapsed State Architecture**
- Decide if collapsed state should be per-user or board-wide
- Update swimlanes.js and lists.js schema accordingly
- Update documentation
2. **Complete Public User Storage**
- Implement localStorage for list widths/swimlane heights for non-logged-in users
- Test persistence across page reloads
3. **Review Position History Usage**
- Confirm if current position history implementation meets requirements
- Consider extending to track all changes (not just original position)
### 4.2 Long-Term Improvements
1. **Audit Trail Feature**
- Extend position history to track all moves with timestamps
- Enable board managers to see complete history of card/list movements
2. **Data Integrity Tests**
- Add integration tests to verify:
- Order is persisted correctly after drag-drop
- Color changes persist across sessions
- Per-user settings apply only to correct user
- Per-user settings don't leak across boards
3. **Database Indexes**
- Verify indexes exist for common queries:
- `sort` fields for swimlanes, lists, cards, checklists
- `boardId` fields for filtering
### 4.3 Code Quality Improvements
1. **Document Persistence Model**
- Add clear comments explaining which fields are board-level vs. per-user
- Document swimlane/list relationship model
2. **Consistent Naming**
- Rename misleading field names (e.g., `cardCollapsed`)
- Align method names with actual functionality
---
## 5. SUMMARY TABLE
### Board-Level Persistence (Shared Across Users)
| Entity | Field | Type | Persisted | Notes |
|--------|-------|------|-----------|-------|
| Swimlane | title | Text | ✅ | Via rename() |
| Swimlane | sort | Number | ✅ | For ordering |
| Swimlane | color | String | ✅ | Via setColor() |
| Swimlane | collapsed | Boolean | ⚠️ | **Issue #1**: Conflicts with per-user storage |
| Swimlane | archived | Boolean | ✅ | Via archive()/restore() |
| | | | | |
| List | title | Text | ✅ | Via rename() or REST |
| List | sort | Number | ✅ | For ordering |
| List | color | String | ✅ | Via REST or update |
| List | collapsed | Boolean | ⚠️ | **Issue #2**: Conflicts with per-user storage |
| List | starred | Boolean | ✅ | Via REST or update |
| List | wipLimit | Object | ✅ | Via REST or setWipLimit() |
| List | archived | Boolean | ✅ | Via archive() |
| | | | | |
| Card | title | Text | ✅ | Direct update |
| Card | sort | Number | ✅ | Via move() |
| Card | color | String | ✅ | Via setColor() |
| Card | boardId/swimlaneId/listId | String | ✅ | Via move() |
| Card | archived | Boolean | ✅ | Via archive() |
| Card | description | Text | ✅ | Direct update |
| Card | customFields | Array | ✅ | Direct update |
| | | | | |
| Checklist | title | Text | ✅ | Via setTitle() |
| Checklist | sort | Number | ✅ | Direct update |
| Checklist | hideCheckedChecklistItems | Boolean | ✅ | Via toggle mutation |
| Checklist | hideAllChecklistItems | Boolean | ✅ | Via toggle mutation |
| | | | | |
| ChecklistItem | title | Text | ✅ | Via setTitle() |
| ChecklistItem | sort | Number | ✅ | Via move() |
| ChecklistItem | isFinished | Boolean | ✅ | Via check/uncheck/toggle |
### Per-User Settings (NOT Persisted Across Boards)
| Setting | Storage | Scope | Notes |
|---------|---------|-------|-------|
| List Widths | profile.listWidths | Per-board, per-user | ✅ Working |
| Swimlane Heights | profile.swimlaneHeights | Per-board, per-user | ✅ Working |
| Collapsed Swimlanes | profile.collapsedSwimlanes | Per-board, per-user | ✅ Working |
| Collapsed Lists | profile.collapsedLists | Per-board, per-user | ✅ Working |
| Card Collapsed State | profile.cardCollapsed | Global per-user | ⚠️ Name misleading |
| Card Maximized State | profile.cardMaximized | Global per-user | ✅ Working |
| Board Workspaces | profile.boardWorkspacesTree | Global per-user | ✅ Working |
| Board Workspace Assignments | profile.boardWorkspaceAssignments | Global per-user | ✅ Working |
| Board View Style | profile.boardView | Global per-user | ✅ Working |
---
## Document History
- **Created**: 2025-12-23
- **Status**: Initial Audit Complete
- **Reviewed**: Swimlanes, Lists, Cards, Checklists, ChecklistItems, PositionHistory, Users
- **Next Review**: After addressing high-priority issues

View file

@ -0,0 +1,4 @@
Per-User vs Per-Board saving of data: Audit Plan 2025-12-23
All collapse state and swimlane height and list width is per-user and should always be persisted at users profile and localstorage, and validated that there is only numbers etc valid data. Invalid data and expired data is deleted from localstorage. swimlane height, list width and collapsed state need to be removed from board level, they are only user level. save swimlaneId always when saving data when swimlaneId is available. If there is no swimlaneId, show not-visible data at rescued data swimlane, similar like there is board settings / migrations to make invisible cards visible, by adding missing data. save position history to each user profile, and have undo redo buttons and list of history savepoints where is possible to return, similar like existing activities data schema, but no overwriting history. for board-level data, for each field (like description, comments etc) at Search All Boards have translateable options to also search from history of boards where user is member of board