mirror of
https://github.com/wekan/wekan.git
synced 2026-01-01 15:18:49 +01:00
542 lines
15 KiB
Markdown
542 lines
15 KiB
Markdown
# 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
|
|
|