fix: Implement tiered backup retention classification (refs #6)
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
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
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>
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* @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 };
|
||||
}
|
||||
Reference in New Issue
Block a user