Files
motovaultpro/backend/src/features/backup/domain/backup-classification.service.ts
Eric Gullickson 19ece562ed
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 6m15s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 28s
Deploy to Staging / Verify Staging (pull_request) Successful in 7s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 6s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
fix: Implement tiered backup retention classification (refs #6)
Replace per-schedule count-based retention with unified tiered classification.
Backups are now classified by timestamp into categories (hourly/daily/weekly/monthly)
and are only deleted when they exceed ALL applicable category quotas.

Changes:
- Add backup-classification.service.ts for timestamp-based classification
- Rewrite backup-retention.service.ts with tiered logic
- Add categories and expires_at columns to backup_history
- Add Expires column to desktop and mobile backup UI
- Add unit tests for classification logic (22 tests)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 21:53:43 -06:00

107 lines
3.2 KiB
TypeScript

/**
* @ai-summary Service for classifying backups into tiered retention categories
* @ai-context Pure functions for timestamp-based classification, no database dependencies
*/
import { BackupCategory, TIERED_RETENTION } from './backup.types';
/**
* Classifies a backup by its timestamp into retention categories.
* A backup can belong to multiple categories simultaneously.
*
* Categories:
* - hourly: All backups
* - daily: First backup at midnight UTC (hour = 0)
* - weekly: First backup on Sunday at midnight UTC
* - monthly: First backup on 1st of month at midnight UTC
*/
export function classifyBackup(timestamp: Date): BackupCategory[] {
const categories: BackupCategory[] = ['hourly'];
const utcHour = timestamp.getUTCHours();
const utcDay = timestamp.getUTCDate();
const utcDayOfWeek = timestamp.getUTCDay(); // 0 = Sunday
// Midnight UTC qualifies for daily
if (utcHour === 0) {
categories.push('daily');
// Sunday at midnight qualifies for weekly
if (utcDayOfWeek === 0) {
categories.push('weekly');
}
// 1st of month at midnight qualifies for monthly
if (utcDay === 1) {
categories.push('monthly');
}
}
return categories;
}
/**
* Calculates the expiration date based on the backup's categories.
* Uses the longest retention period among all applicable categories.
*
* Retention periods are count-based in the actual cleanup, but for display
* we estimate based on typical backup frequency:
* - hourly: 8 hours (8 backups * 1 hour)
* - daily: 7 days (7 backups * 1 day)
* - weekly: 4 weeks (4 backups * 1 week)
* - monthly: 12 months (12 backups * 1 month)
*/
export function calculateExpiration(
categories: BackupCategory[],
timestamp: Date
): Date {
const expirationDate = new Date(timestamp);
if (categories.includes('monthly')) {
expirationDate.setUTCMonth(expirationDate.getUTCMonth() + TIERED_RETENTION.monthly);
} else if (categories.includes('weekly')) {
expirationDate.setUTCDate(expirationDate.getUTCDate() + TIERED_RETENTION.weekly * 7);
} else if (categories.includes('daily')) {
expirationDate.setUTCDate(expirationDate.getUTCDate() + TIERED_RETENTION.daily);
} else {
// Hourly only - 8 hours
expirationDate.setUTCHours(expirationDate.getUTCHours() + TIERED_RETENTION.hourly);
}
return expirationDate;
}
/**
* Checks if a backup timestamp represents the first backup of the day (midnight UTC).
*/
export function isFirstBackupOfDay(timestamp: Date): boolean {
return timestamp.getUTCHours() === 0;
}
/**
* Checks if a timestamp falls on a Sunday.
*/
export function isSunday(timestamp: Date): boolean {
return timestamp.getUTCDay() === 0;
}
/**
* Checks if a timestamp falls on the first day of the month.
*/
export function isFirstDayOfMonth(timestamp: Date): boolean {
return timestamp.getUTCDate() === 1;
}
/**
* Classifies a backup and calculates its expiration in one call.
* Convenience function for backup creation flow.
*/
export function classifyAndCalculateExpiration(timestamp: Date): {
categories: BackupCategory[];
expiresAt: Date;
} {
const categories = classifyBackup(timestamp);
const expiresAt = calculateExpiration(categories, timestamp);
return { categories, expiresAt };
}