feat: Add 'Needs Backup' filter to Dashboard

Add a new filter button that shows only guests with stale, critical,
or missing backups. This makes it easy to identify which VMs and
containers need attention for backup scheduling.

- Adds backupMode state with 'all' and 'needs-backup' options
- Filters out templates (they don't need backups)
- Uses existing getBackupInfo() thresholds (>24h stale, >72h critical)
- Integrates with Reset button and Escape key handling
- Persists filter state in localStorage

Related to #762
This commit is contained in:
rcourtman 2025-12-02 20:00:33 +00:00
parent 6c76823754
commit b70a4eba9d
2 changed files with 46 additions and 1 deletions

View file

@ -20,6 +20,7 @@ import { getNodeDisplayName } from '@/utils/nodes';
import { logger } from '@/utils/logger';
import { usePersistentSignal } from '@/hooks/usePersistentSignal';
import { STORAGE_KEYS } from '@/utils/localStorage';
import { getBackupInfo } from '@/utils/format';
type GuestMetadataRecord = Record<string, GuestMetadata>;
type IdleCallbackHandle = number;
@ -196,6 +197,7 @@ interface DashboardProps {
type ViewMode = 'all' | 'vm' | 'lxc';
type StatusMode = 'all' | 'running' | 'degraded' | 'stopped';
type BackupMode = 'all' | 'needs-backup';
type GroupingMode = 'grouped' | 'flat';
export function Dashboard(props: DashboardProps) {
@ -236,6 +238,11 @@ export function Dashboard(props: DashboardProps) {
: 'all',
});
// Backup filter mode - filter by backup status
const [backupMode, setBackupMode] = usePersistentSignal<BackupMode>('dashboardBackupMode', 'all', {
deserialize: (raw) => (raw === 'all' || raw === 'needs-backup' ? raw : 'all'),
});
// Grouping mode - grouped by node or flat list
const [groupingMode, setGroupingMode] = usePersistentSignal<GroupingMode>(
'dashboardGroupingMode',
@ -426,7 +433,8 @@ export function Dashboard(props: DashboardProps) {
sortDirection() !== 'asc' ||
selectedNode() !== null ||
viewMode() !== 'all' ||
statusMode() !== 'all';
statusMode() !== 'all' ||
backupMode() !== 'all';
if (hasActiveFilters) {
// Clear ALL filters including search text, tag filters, node selection, and view modes
@ -437,6 +445,7 @@ export function Dashboard(props: DashboardProps) {
setSelectedNode(null);
setViewMode('all');
setStatusMode('all');
setBackupMode('all');
// Blur the search input if it's focused
if (searchInputRef && document.activeElement === searchInputRef) {
@ -502,6 +511,17 @@ export function Dashboard(props: DashboardProps) {
guests = guests.filter((g) => g.status !== 'running');
}
// Filter by backup status
if (backupMode() === 'needs-backup') {
guests = guests.filter((g) => {
// Skip templates - they don't need backups
if (g.template) return false;
const backupInfo = getBackupInfo(g.lastBackup);
// Show guests that need backup: stale, critical, or never backed up
return backupInfo.status === 'stale' || backupInfo.status === 'critical' || backupInfo.status === 'never';
});
}
// Apply search/filter
const searchTerm = search().trim();
if (searchTerm) {
@ -800,6 +820,8 @@ export function Dashboard(props: DashboardProps) {
setViewMode={setViewMode}
statusMode={statusMode}
setStatusMode={setStatusMode}
backupMode={backupMode}
setBackupMode={setBackupMode}
groupingMode={groupingMode}
setGroupingMode={setGroupingMode}
setSortKey={setSortKey}

View file

@ -13,6 +13,8 @@ interface DashboardFilterProps {
setViewMode: (value: 'all' | 'vm' | 'lxc') => void;
statusMode: () => 'all' | 'running' | 'degraded' | 'stopped';
setStatusMode: (value: 'all' | 'running' | 'degraded' | 'stopped') => void;
backupMode: () => 'all' | 'needs-backup';
setBackupMode: (value: 'all' | 'needs-backup') => void;
groupingMode: () => 'grouped' | 'flat';
setGroupingMode: (value: 'grouped' | 'flat') => void;
setSortKey: (value: string) => void;
@ -369,6 +371,25 @@ export const DashboardFilter: Component<DashboardFilterProps> = (props) => {
<div class="h-5 w-px bg-gray-200 dark:bg-gray-600 hidden sm:block"></div>
{/* Backup Filter */}
<button
type="button"
onClick={() => props.setBackupMode(props.backupMode() === 'needs-backup' ? 'all' : 'needs-backup')}
class={`inline-flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium rounded-lg transition-all ${props.backupMode() === 'needs-backup'
? 'bg-orange-100 dark:bg-orange-900/50 text-orange-700 dark:text-orange-300 shadow-sm'
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100'
}`}
title="Show guests with stale or missing backups"
>
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
<path d="M12 8v4M12 16h.01" />
</svg>
Needs Backup
</button>
<div class="h-5 w-px bg-gray-200 dark:bg-gray-600 hidden sm:block"></div>
{/* Grouping Mode Toggle */}
<div class="inline-flex rounded-lg bg-gray-100 dark:bg-gray-700 p-0.5">
<button
@ -410,12 +431,14 @@ export const DashboardFilter: Component<DashboardFilterProps> = (props) => {
props.setSortDirection('asc');
props.setViewMode('all');
props.setStatusMode('all');
props.setBackupMode('all');
props.setGroupingMode('grouped');
}}
title="Reset all filters"
class={`flex items-center justify-center px-2.5 py-1 text-xs font-medium rounded-lg transition-colors ${props.search() ||
props.viewMode() !== 'all' ||
props.statusMode() !== 'all' ||
props.backupMode() !== 'all' ||
props.groupingMode() !== 'grouped'
? 'text-blue-700 dark:text-blue-300 bg-blue-100 dark:bg-blue-900/50 hover:bg-blue-200 dark:hover:bg-blue-900/70'
: 'text-gray-600 dark:text-gray-400 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600'