Major enhancements to backup monitoring accuracy and user experience: ## Core Fixes - **Fix timestamp precision loss**: PVE/PBS age filtering now shows correct times (17h vs incorrect 36h) - **Server-side deduplication**: Shared storage backups no longer counted multiple times - **Client-side deduplication**: Calendar drill-down shows each guest only once per date - **Clean UI**: Removed redundant backup count numbers when type badges are shown ## Technical Implementation - Added `latestTimes` object with type-specific timestamps to guest data structure - Implemented `deduplicateStorageBackups()` function using volid-based deduplication - Enhanced calendar-heatmap processing to merge duplicate guest entries by vmid - Updated backup detail card to use direct timestamp lookup instead of date aggregation ## Files Modified - `server/dataFetcher.js`: Added shared storage deduplication logic - `src/public/js/ui/backups.js`: Enhanced guest data with type-specific latest times - `src/public/js/ui/backup-detail-card.js`: Fixed age calculation and removed redundant counts - `src/public/js/ui/calendar-heatmap.js`: Implemented client-side guest deduplication - `docs/BACKUP_ARCHITECTURE.md`: Documented pragmatic solution approach ## Result - Accurate backup ages when filtering by PVE/PBS/Snapshots - No more duplicate counting from shared storage (NFS, Ceph, etc.) - Clean calendar view with single guest entries per date - Foundation for future backup system enhancements 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
7.3 KiB
Backup Data Architecture & Pragmatic Solution
Overview
Pulse's backup monitoring system faces architectural challenges due to diverse Proxmox environments. This document outlines complexities and provides a pragmatic, minimal solution approach.
Backup Types & Behaviors
1. PVE Local Backups (vzdump)
- Location: Local storage on individual PVE nodes
- Scope: Node-specific, not visible to other nodes
- Identification: Node + vmid + timestamp
- Migration Impact: Backup stays on original node even if VM migrates
2. PVE Shared Storage Backups (vzdump to NFS/Ceph)
- Location: Shared storage visible to multiple nodes
- Scope: Cluster-wide visibility
- Identification: Multiple nodes see same backup file
- Migration Impact: Backup visible regardless of current VM location
- Duplication Risk: Same backup appears once per node that mounts the storage
3. PBS Remote Backups
- Location: Centralized PBS server
- Scope: Cross-cluster, can backup from multiple PVE clusters
- Identification: PBS datastore + backup-id + timestamp
- Migration Impact: Independent of VM location
- Centralized: Single source of truth per PBS server
4. VM/Container Snapshots
- Location: VM storage location (can be local or shared)
- Scope: Follows VM storage
- Identification: Node + vmid + snapshot name + timestamp
- Migration Impact: Snapshots follow VM if on shared storage
Problem Analysis: Real vs. Theoretical
Critical Issues (Must Fix)
- Timestamp Precision Loss - Core user issue causing wrong age display
- Impact: High - incorrect data shown to users daily
- Fix Complexity: Low - simple data structure enhancement
Medium Issues (Monitor & Fix If Needed)
-
Shared Storage Duplication - Inflated backup counts
- Impact: Medium - cosmetic but confusing
- Frequency: Only affects shared storage users
-
VM Migration Tracking - Some backups may be missed
- Impact: Medium - historical data gaps
- Frequency: Only when VMs migrate AND users drill into history
Low Priority Issues (Address If Encountered)
-
Cross-cluster vmid conflicts - Data mixing between unrelated VMs
- Impact: Low - rare edge case
- Frequency: Only with multiple clusters + same vmids
-
Guest name conflicts - Cosmetic confusion
- Impact: Low - doesn't affect functionality
Pragmatic Solution: Enhanced Current System
Principle: Minimal Disruption, Maximum Benefit
Instead of complete architectural overhaul, enhance existing structure to solve core issues:
Current Structure (Problematic)
guest: {
guestId: 111,
guestName: "debian",
latestBackupTime: 1749344442, // Could be ANY backup type - PROBLEM
backupDates: [
{
date: "2025-06-07", // Date only - loses time precision - PROBLEM
types: ["pbsSnapshots", "pveBackups"],
count: 3
}
]
}
Enhanced Structure (Solution)
guest: {
guestId: 111,
guestName: "debian",
// Keep existing for compatibility
latestBackupTime: 1749344442, // Overall latest (any type)
// NEW: Type-specific latest times - SOLVES CORE ISSUE
latestTimes: {
pve: 1749300000, // Latest PVE backup timestamp (17h ago)
pbs: 1749344442, // Latest PBS backup timestamp (11h ago)
snapshots: 1749330000 // Latest snapshot timestamp
},
// Enhanced existing structure
backupDates: [
{
date: "2025-06-07",
types: ["pveBackups"],
count: 1,
// NEW: Preserve actual timestamps for precision
timestamps: {
pveBackups: [1749300000], // Actual 8pm timestamp
pbsSnapshots: [],
vmSnapshots: []
}
},
{
date: "2025-06-08",
types: ["pbsSnapshots"],
count: 1,
timestamps: {
pveBackups: [],
pbsSnapshots: [1749344442], // Actual PBS timestamp
vmSnapshots: []
}
}
]
}
Simple Age Calculation Fix
function getFilteredAge(guest, backupTypeFilter) {
const now = Date.now();
let latestTimestamp;
// Direct timestamp lookup - fast and accurate
switch(backupTypeFilter) {
case 'pve':
latestTimestamp = guest.latestTimes?.pve;
break;
case 'pbs':
latestTimestamp = guest.latestTimes?.pbs;
break;
case 'snapshots':
latestTimestamp = guest.latestTimes?.snapshots;
break;
case 'all':
latestTimestamp = guest.latestBackupTime; // Overall latest
break;
}
// Fallback to overall if type-specific not available
latestTimestamp = latestTimestamp || guest.latestBackupTime;
return latestTimestamp
? (now - latestTimestamp * 1000) / (1000 * 60 * 60 * 24)
: Infinity;
}
Implementation Plan
Phase 1: Fix Core Issue (1-2 days)
-
Server-side Enhancement:
// During guest processing, calculate type-specific latest times guest.latestTimes = { pve: findLatestBackupByType(guestBackups, 'pve'), pbs: findLatestBackupByType(guestBackups, 'pbs'), snapshots: findLatestBackupByType(guestBackups, 'snapshots') }; -
Client-side Fix:
// Use type-specific timestamps in age calculations const age = getFilteredAge(guest, activeFilter);
Phase 2: Enhanced Precision (Optional - 1 day)
- Add timestamp arrays to backupDates for calendar precision
- Enhance daily drill-down to show exact times
Phase 3: Address Other Issues (As Needed)
- Shared Storage Deduplication: Only if users report confusion
- Migration Tracking: Only if users report missing backups
- Identity Conflicts: Only if actually encountered in production
Benefits of This Approach
✅ Solves Your Immediate Problem
- PVE filter shows 17h (correct) instead of 36h (wrong)
- Accurate age calculations for all backup types
- No precision loss from date aggregation
✅ Minimal Risk
- Extends existing structure instead of replacing
- Backward compatible
- Small, focused changes
- Easy to test and verify
✅ Performance Benefits
- Direct timestamp lookups (O(1))
- No complex nested object traversal
- Minimal server-side processing overhead
- Fast client-side age calculations
✅ Maintainability
- Simple to understand and debug
- Follows existing patterns
- Clear separation of concerns without over-engineering
- Easy to extend if needed
✅ Future-Proof
- Foundation for addressing other issues incrementally
- Doesn't prevent future enhancements
- Data structure supports additional metadata
Result: Your Specific Fix
Before (Broken):
Filter: PVE → Shows 36h age (wrong - using midnight of backup date)
Filter: PBS → Shows 11h age
Filter: All → Shows 11h age
After (Fixed):
Filter: PVE → Shows 17h age (correct - using actual 8pm backup timestamp)
Filter: PBS → Shows 11h age (correct)
Filter: All → Shows 11h age (correct - most recent overall)
Conclusion
This pragmatic approach delivers 80% of the benefits with 20% of the complexity. It solves the core user-facing issue while maintaining system simplicity and keeping the door open for future enhancements if they become necessary.
The other theoretical issues (duplication, migration, conflicts) can be addressed incrementally if and when they actually impact users in practice.