feat(backup): coverage diagnostic — what will the next backup miss?
Stage C of the three-stage backup-hardening plan (Stage A: inline
DB dump + fail-loud landed in 7fdf01a; Stage B: config-driven walker
in 302fc6b). Answers the "what would I lose if I clicked Run Backup
Now right now?" question that Stage B made possible to answer.
Backend:
- new backupCoverageService.js: per-path coverage classification,
drift detection (top-level subdirs not in backup_paths and not
in the backups/tmp allow-list), DB-dump mode + staleness block
- new GET /api/admin/system-health/backup-coverage route, same
auth + settings.view permission as /backup-integrity
- 7 integration scenarios pinning the classifier behaviour
Frontend:
- new BackupCoverageCard with auto-fetch (cheap; no recursion)
- new Coverage tab on BackupManagement next to Integrity
- en + de i18n; other locales fall back to en keys until a native
speaker reviews
Verification:
- 26/26 backup integration tests pass (Stage A 5 + Stage B 7 +
Stage C 7 + adminBackupIntegrity 4 + businessDocs 3)
- frontend build clean
- 4 pre-existing integration failures confirmed unrelated
This commit is contained in:
@@ -245,6 +245,58 @@ export interface BackupIntegrityReport {
|
||||
existsButNoHash: BackupIntegrityExistsButNoHashRow[];
|
||||
}
|
||||
|
||||
// ---- Backup-coverage (Stage C of backup-hardening plan) -----------------
|
||||
|
||||
export type BackupPathCoverage =
|
||||
| 'will-scan'
|
||||
| 'skipped-by-toggle'
|
||||
| 'skipped-by-feature-flag'
|
||||
| 'missing-on-disk';
|
||||
|
||||
export interface BackupCoveragePath {
|
||||
path: string;
|
||||
includeInDefault: boolean;
|
||||
featureFlag: string | null;
|
||||
featureFlagValue: boolean | null;
|
||||
displayOrder: number;
|
||||
description: string | null;
|
||||
existsOnDisk: boolean;
|
||||
coverage: BackupPathCoverage;
|
||||
}
|
||||
|
||||
export interface BackupCoverageDatabase {
|
||||
mode: 'inline' | 'scheduled-only';
|
||||
inlineDumpExplicitlyDisabled: boolean;
|
||||
lastDumpAt: string | null;
|
||||
lastDumpType: string | null;
|
||||
lastDumpSizeBytes: number;
|
||||
lastDumpFilePath: string | null;
|
||||
lastDumpAgeMs: number | null;
|
||||
lastDumpStale: boolean | null;
|
||||
ok: boolean;
|
||||
}
|
||||
|
||||
export interface BackupCoverageReport {
|
||||
generatedAt: string;
|
||||
database: BackupCoverageDatabase;
|
||||
paths: BackupCoveragePath[];
|
||||
drift: {
|
||||
unconfiguredOnDisk: string[];
|
||||
expectedNonBackupDirs: string[];
|
||||
};
|
||||
summary: {
|
||||
configuredCount: number;
|
||||
willScanCount: number;
|
||||
skippedByToggleCount: number;
|
||||
skippedByFeatureFlagCount: number;
|
||||
missingOnDiskCount: number;
|
||||
driftCount: number;
|
||||
tableMissingFallbackInUse: boolean;
|
||||
databaseOk: boolean;
|
||||
overallOk: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AdminProfile {
|
||||
id: number;
|
||||
username: string;
|
||||
@@ -319,6 +371,16 @@ export const adminService = {
|
||||
return response.data.report;
|
||||
},
|
||||
|
||||
// Backup-coverage diagnostic (Stage C). Answers "what will the
|
||||
// next backup actually include / skip / silently miss?" — read-only,
|
||||
// no parameters. See backupCoverageService.js for the full report shape.
|
||||
async getBackupCoverage(): Promise<BackupCoverageReport> {
|
||||
const response = await api.get<{ report: BackupCoverageReport }>(
|
||||
'/admin/system-health/backup-coverage',
|
||||
);
|
||||
return response.data.report;
|
||||
},
|
||||
|
||||
// Format activity message
|
||||
formatActivityMessage(activity: Activity): string {
|
||||
// Feature-flag toggles carry a `changed` diff in metadata. Render
|
||||
|
||||
Reference in New Issue
Block a user