mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
A database dump is the whole database at one moment, so an account deleted since stays in the dumps taken before until they age out. The database row on the backups card now says so with the numbers that decide it: how many nightly dumps this server keeps, and after how many days the bucket's lifecycle rule removes the off-host copies. The lifecycle probe used to answer with the shortest expiry of any enabled rule, whatever its prefix, so a rule scoped to the account copies read as covering the dumps too. It now also answers for `database/` alone, counting only rules whose filter takes in a dump's key (prefix, legacy prefix, an And filter; a tag filter never matches untagged objects), and the card warns when no rule covers that prefix: dumps there, and the deleted accounts in them, would never be removed. The reading moved into its own module.
118 lines
4.6 KiB
TypeScript
118 lines
4.6 KiB
TypeScript
/**
|
|
* v1.4.41 (org-audit rec #2) — DTOs for `/api/admin/backups`.
|
|
*
|
|
* Pre-v1.4.41 these interfaces lived inside the route handler at
|
|
* `src/app/api/admin/backups/route.ts` and were imported directly from
|
|
* the route module by `src/components/admin/backups-section.tsx`. That
|
|
* was a textbook layer violation (component → route handler) and the
|
|
* only one of its kind in the codebase. Hoisting the shapes here gives
|
|
* both sides a route-handler-independent shared type home; the route
|
|
* keeps owning the HTTP contract, the component keeps owning the UI.
|
|
*/
|
|
import type { DatabaseBackupHealth } from "@/lib/database-backup/health";
|
|
import type { OffhostLifecycle } from "@/lib/jobs/offhost-lifecycle";
|
|
import type { BackupPassActivity } from "@/lib/jobs/backup-pass-attempts";
|
|
import type { BackupScheduleStatus } from "@/lib/jobs/backup-schedule-status";
|
|
import type { OffhostBackupFreshness } from "@/lib/jobs/offhost-backup-freshness";
|
|
|
|
export interface BackupRow {
|
|
id: string;
|
|
userId: string;
|
|
username: string;
|
|
type: string;
|
|
/**
|
|
* Size of the encrypted blob in bytes — useful for capacity planning.
|
|
* The blob itself is never returned.
|
|
*/
|
|
sizeBytes: number;
|
|
createdAt: string;
|
|
}
|
|
|
|
/** One account's newest object in the operator's off-host bucket. */
|
|
export interface OffhostAccountRow {
|
|
userId: string;
|
|
username: string;
|
|
/**
|
|
* ISO instant a run last walked this account, or null when none has. Null
|
|
* is what the whole cohort reads until the first run after the upgrade that
|
|
* adds the ledger, and the card says so rather than claiming `never`.
|
|
*/
|
|
lastAttemptAt: string | null;
|
|
/** ISO instant the newest object landed, or null when there is none. */
|
|
lastSuccessAt: string | null;
|
|
/** Size of that object in bytes, or null when there is none. */
|
|
sizeBytes: number | null;
|
|
/** Whole hours since it landed, or null when there is none. */
|
|
ageHours: number | null;
|
|
freshness: OffhostBackupFreshness;
|
|
}
|
|
|
|
/**
|
|
* The off-host picture, per account.
|
|
*
|
|
* Computed from the ledger the nightly worker writes, never from the bucket:
|
|
* the page must be able to answer "does this account have a recent copy
|
|
* off-host" on a host whose worker holds no listing grant on the bucket.
|
|
*/
|
|
export interface OffhostBackupOverview {
|
|
/** Whether this host has the variables the nightly job needs. */
|
|
configured: boolean;
|
|
/** The run in progress, and accounts a run died under. */
|
|
activity: BackupPassActivity;
|
|
/** Hours between two scheduled runs — the number behind the verdicts. */
|
|
periodHours: number;
|
|
/** One row per account. Empty when off-host backup is not configured. */
|
|
rows: OffhostAccountRow[];
|
|
/**
|
|
* Deleted or wiped accounts whose copies are still in the bucket. A count
|
|
* above zero with an error means the bucket refused the deletion (most
|
|
* often a credential without DeleteObject); the job retries nightly.
|
|
*/
|
|
pendingDeletions: {
|
|
count: number;
|
|
oldestRequestedAt: string | null;
|
|
lastFailure: string | null;
|
|
};
|
|
/**
|
|
* Whether the bucket retires old copies by itself. Read from the bucket
|
|
* (`GetBucketLifecycleConfiguration`), cached for ten minutes; `unknown`
|
|
* when the credential may not read it.
|
|
*/
|
|
lifecycle: OffhostLifecycle;
|
|
}
|
|
|
|
export interface BackupsList {
|
|
rows: BackupRow[];
|
|
/**
|
|
* Whether the weekly schedule is still producing copies, and how the last
|
|
* scheduled run ended. A row's own timestamp cannot answer that — a copy
|
|
* made six weeks ago and one made on Sunday look identical in a table.
|
|
*/
|
|
schedule: BackupScheduleStatus;
|
|
/**
|
|
* The weekly run in progress, and accounts a run died under: a start
|
|
* without a finish, which a crash leaves and a caught failure does not.
|
|
*/
|
|
scheduleActivity: BackupPassActivity;
|
|
/**
|
|
* The off-host leg. A weekly row in this table says the copy that lives in
|
|
* this database is current; it says nothing about whether anything reached
|
|
* the operator's bucket, and those two fail independently.
|
|
*/
|
|
offhost: OffhostBackupOverview;
|
|
/**
|
|
* The nightly whole-database dump, the copy that rebuilds a lost server:
|
|
* the newest good dump (kept here, off-host, or both), the newest failed
|
|
* run, and whether this server can keep dumps at all.
|
|
*/
|
|
database: DatabaseBackupHealth;
|
|
/**
|
|
* Soft retention hint — the worker is configured for weekly backups
|
|
* (see `DATA_BACKUP_CRON` in `src/lib/jobs/reminder-worker.ts`), and
|
|
* the model upserts in-place per (userId, type), so each user has
|
|
* exactly one current snapshot. The frontend uses this to label the
|
|
* grid; no server-side enforcement.
|
|
*/
|
|
retentionDays: number;
|
|
}
|