Skip to content

Commit 8de0d47

Browse files
rcourtmanclaude
andcommitted
feat: add backup calendar heatmap and enhanced backup visualization
- Add comprehensive backup calendar heatmap with monthly view - Implement interactive calendar filtering linked to backup table - Add backup summary cards for quick overview statistics - Enhance backup table with click-to-filter calendar functionality - Improve PBS and backup data visualization - Add responsive design for mobile and desktop views - Include backup type indicators and failure detection - Persistent filter state across API updates 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 9f0403d commit 8de0d47

File tree

5 files changed

+1808
-44
lines changed

5 files changed

+1808
-44
lines changed

src/public/index.html

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,33 @@
717717
</div>
718718
<div id="backups" class="tab-content hidden bg-white dark:bg-gray-800 rounded-b rounded-tr shadow p-3 mb-2">
719719
<p id="backups-loading-message" class="text-gray-500 dark:text-gray-400 p-4 text-center mb-2">Loading backup status overview...</p>
720+
721+
<!-- Consolidated Backup Summary -->
722+
<div id="backup-summary-container" class="hidden mb-4">
723+
<!-- Consolidated backup summary will be inserted here -->
724+
</div>
725+
726+
<!-- Hidden: Node backup cards (replaced by consolidated summary) -->
727+
<div id="node-backup-cards" class="hidden mb-3">
728+
<!-- No longer used - replaced by consolidated summary -->
729+
</div>
730+
731+
<!-- Backup History Visualization -->
732+
<div id="backup-visualization-section" class="hidden mb-6 space-y-4">
733+
<!-- Hidden: Summary cards (replaced by consolidated summary) -->
734+
<div id="backup-summary-cards-container" class="hidden">
735+
<!-- No longer used - replaced by consolidated summary -->
736+
</div>
737+
<div id="backup-calendar-container" class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4">
738+
<div class="flex items-center justify-between mb-4">
739+
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-300">Backup History Calendar</h3>
740+
</div>
741+
<div id="backup-calendar-heatmap">
742+
<!-- Calendar heatmap will be inserted here -->
743+
</div>
744+
</div>
745+
</div>
746+
720747
<div class="backups-filter flex flex-col md:flex-row justify-between items-stretch md:items-center gap-3 mb-3 p-2 bg-gray-50 dark:bg-gray-700/50 border border-gray-200 dark:border-gray-700 rounded">
721748
<div class="dashboard-filter-controls flex-grow flex items-center gap-2">
722749
<input type="text" id="backups-search" placeholder="Search Name, ID, Node (use ',' for OR)" class="flex-grow min-w-[200px] max-w-full p-1 px-2 h-7 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 focus:ring-1 focus:ring-blue-500 focus:border-blue-500 outline-none" />
@@ -808,13 +835,13 @@
808835
<span id="backups-status-text" class="text-xs text-gray-500 dark:text-gray-400">Loading backup status...</span>
809836
<div class="text-xs text-gray-500 dark:text-gray-400 flex items-center gap-3">
810837
<span class="flex items-center gap-1">
811-
<span class="w-2 h-2 bg-purple-500 rounded-full"></span>PBS
838+
<span class="w-2 h-2 bg-green-500 rounded-full"></span>PBS
812839
</span>
813840
<span class="flex items-center gap-1">
814-
<span class="w-2 h-2 bg-orange-500 rounded-full"></span>PVE
841+
<span class="w-2 h-2 bg-yellow-400 rounded-full"></span>PVE
815842
</span>
816843
<span class="flex items-center gap-1">
817-
<span class="w-2 h-2 bg-blue-500 rounded-full"></span>Snapshots
844+
<span class="w-2 h-2 bg-blue-400 rounded-full"></span>Snapshots
818845
</span>
819846
<span class="mx-2"></span>
820847
<span class="flex items-center gap-1">
@@ -920,6 +947,8 @@
920947
<script src="/js/ui/dashboard.js" defer></script>
921948
<script src="/js/ui/storage.js" defer></script>
922949
<script src="/js/ui/pbs.js" defer></script>
950+
<script src="/js/ui/backup-summary-cards.js" defer></script>
951+
<script src="/js/ui/calendar-heatmap.js" defer></script>
923952
<script src="/js/ui/backups.js" defer></script>
924953
<script src="/js/alertsHandler.js"></script>
925954
<script src="/js/hotReload.js" defer></script>
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
PulseApp.ui = PulseApp.ui || {};
2+
3+
PulseApp.ui.backupSummaryCards = (() => {
4+
5+
function calculateBackupStatistics(backupData, guestId) {
6+
const now = Date.now();
7+
const stats = {
8+
lastBackup: { time: null, type: null, status: 'none' },
9+
coverage: { daily: 0, weekly: 0, monthly: 0 },
10+
protected: { count: 0, oldestDate: null, coverage: 0 },
11+
health: { score: 0, issues: [] }
12+
};
13+
14+
// Find most recent backup across all types
15+
const allBackups = [];
16+
17+
['pbsSnapshots', 'pveBackups', 'vmSnapshots'].forEach(type => {
18+
if (!backupData[type]) return;
19+
20+
const items = guestId
21+
? backupData[type].filter(item => item.vmid == guestId)
22+
: backupData[type];
23+
24+
items.forEach(item => {
25+
const timestamp = item.ctime || item.snaptime || item['backup-time'];
26+
if (timestamp) {
27+
allBackups.push({
28+
time: timestamp * 1000,
29+
type: type,
30+
protected: item.protected || false,
31+
verification: item.verification
32+
});
33+
}
34+
});
35+
});
36+
37+
// Sort by time descending
38+
allBackups.sort((a, b) => b.time - a.time);
39+
40+
// Last backup info
41+
if (allBackups.length > 0) {
42+
const last = allBackups[0];
43+
stats.lastBackup = {
44+
time: last.time,
45+
type: last.type,
46+
status: last.verification?.state === 'failed' ? 'failed' : 'success',
47+
age: now - last.time
48+
};
49+
}
50+
51+
// Calculate coverage (how many backups in each period)
52+
const oneDayAgo = now - 24 * 60 * 60 * 1000;
53+
const oneWeekAgo = now - 7 * 24 * 60 * 60 * 1000;
54+
const oneMonthAgo = now - 30 * 24 * 60 * 60 * 1000;
55+
56+
allBackups.forEach(backup => {
57+
if (backup.time >= oneDayAgo) stats.coverage.daily++;
58+
if (backup.time >= oneWeekAgo) stats.coverage.weekly++;
59+
if (backup.time >= oneMonthAgo) stats.coverage.monthly++;
60+
});
61+
62+
// Protected backups analysis
63+
const protectedBackups = allBackups.filter(b => b.protected);
64+
stats.protected.count = protectedBackups.length;
65+
if (protectedBackups.length > 0) {
66+
const oldestProtected = protectedBackups[protectedBackups.length - 1];
67+
stats.protected.oldestDate = oldestProtected.time;
68+
stats.protected.coverage = Math.floor((now - oldestProtected.time) / (24 * 60 * 60 * 1000));
69+
}
70+
71+
return stats;
72+
}
73+
74+
return {
75+
calculateBackupStatistics
76+
};
77+
})();

0 commit comments

Comments
 (0)