feat(admin): show the newest database dump on the backups page

GET /api/admin/backups now carries `database`: the newest good dump (when,
how big, kept on the server, off-host or both), the newest failed run,
whether this server can write its backup directory, and whether off-host
copies are configured. The backups card leads with one row built from it,
with a notice when the last run failed and when dumps can only go
off-host or cannot be taken at all. The route stays an unpublished admin
console route, so the OpenAPI document does not change.
This commit is contained in:
Marc-André Bombeck committed 2026-10-25 12:56:33 +01:00
1 parent e665f17f52
commit 3646502a86
14 files changed
+282 -10

No files matched your search

+14 -1
View File
@@ -8310,7 +8310,20 @@
"restoreSkippedMoodLabelCiphertext": "Name eines Stimmungs-Tags oder einer Gruppe mit einem Schlüssel verschlüsselt, den dieser Server nicht hat (ohne Namen behalten)",
"restoreSkippedVisitCiphertext": "Text zu Arztbesuch oder Praxis mit einem Schlüssel verschlüsselt, den dieser Server nicht hat (ohne ihn behalten)",
"restoreSkippedVaccinationCiphertext": "Impf-Notiz mit einem Schlüssel verschlüsselt, den dieser Server nicht hat (Impfung ohne sie behalten)",
"restoreSkippedEnvironmentLocationCiphertext": "Ort eines Umweltzeitraums oder -tags mit einem Schlüssel verschlüsselt, den dieser Server nicht hat (verschlüsselt behalten, diese Tage werden nicht neu abgerufen)"
"restoreSkippedEnvironmentLocationCiphertext": "Ort eines Umweltzeitraums oder -tags mit einem Schlüssel verschlüsselt, den dieser Server nicht hat (verschlüsselt behalten, diese Tage werden nicht neu abgerufen)",
"database": {
"title": "Datenbank-Dump",
"lastGood": "Letzter gültiger Dump {when}, {size}, {where}.",
"whereBoth": "auf diesem Server und extern gespeichert",
"whereLocal": "auf diesem Server gespeichert",
"whereOffhost": "nur extern gespeichert",
"none": "Noch kein Datenbank-Dump. Der erste läuft heute Nacht.",
"failedTitle": "Der letzte Datenbank-Dump ist fehlgeschlagen",
"failedDescription": "{when}: {reason}",
"disabled": "Datenbank-Dumps sind abgeschaltet (DB_BACKUP_ENABLED=false). Sichere die Datenbank auf anderem Weg.",
"noDirectoryOffhost": "Dieser Server kann nicht nach {dir} schreiben. Dumps gehen deshalb nur extern, hier wird keiner aufbewahrt. Binde das Backup-Volume ein.",
"noDirectory": "Dieser Server kann nicht nach {dir} schreiben und es ist kein externer Speicher eingerichtet. Deshalb entsteht kein Datenbank-Dump. Binde das Backup-Volume ein."
}
},
"danger-zone": {
"title": "Gefahrenzone",
+14 -1
View File
@@ -8310,7 +8310,20 @@
"restoreSkippedMoodLabelCiphertext": "Mood tag or group name sealed with a key this server does not hold (kept without its name)",
"restoreSkippedVisitCiphertext": "Visit or practitioner text sealed with a key this server does not hold (kept without it)",
"restoreSkippedVaccinationCiphertext": "Vaccination note sealed with a key this server does not hold (dose kept without it)",
"restoreSkippedEnvironmentLocationCiphertext": "Location of an environment period or day sealed with a key this server does not hold (kept sealed, those days are not refreshed)"
"restoreSkippedEnvironmentLocationCiphertext": "Location of an environment period or day sealed with a key this server does not hold (kept sealed, those days are not refreshed)",
"database": {
"title": "Database dump",
"lastGood": "Last good dump {when}, {size}, {where}.",
"whereBoth": "kept on this server and off-host",
"whereLocal": "kept on this server",
"whereOffhost": "off-host only",
"none": "No database dump yet. The first one runs tonight.",
"failedTitle": "The last database dump failed",
"failedDescription": "{when}: {reason}",
"disabled": "Database dumps are switched off (DB_BACKUP_ENABLED=false). Back the database up another way.",
"noDirectoryOffhost": "This server cannot write to {dir}, so dumps go off-host only and none is kept here. Mount the backups volume.",
"noDirectory": "This server cannot write to {dir} and no off-host bucket is set, so no database dump can be taken. Mount the backups volume."
}
},
"danger-zone": {
"title": "Danger zone",
+14 -1
View File
@@ -8310,7 +8310,20 @@
"restoreSkippedMoodLabelCiphertext": "Nombre de etiqueta o grupo de ánimo cifrado con una clave que este servidor no tiene (se conserva sin el nombre)",
"restoreSkippedVisitCiphertext": "Texto de visita o profesional cifrado con una clave que este servidor no tiene (se conserva sin él)",
"restoreSkippedVaccinationCiphertext": "Nota de vacunación cifrada con una clave que este servidor no tiene (la dosis se conserva sin ella)",
"restoreSkippedEnvironmentLocationCiphertext": "Ubicación de un periodo o día ambiental cifrada con una clave que este servidor no tiene (se conserva cifrada, esos días no se actualizan)"
"restoreSkippedEnvironmentLocationCiphertext": "Ubicación de un periodo o día ambiental cifrada con una clave que este servidor no tiene (se conserva cifrada, esos días no se actualizan)",
"database": {
"title": "Volcado de la base de datos",
"lastGood": "Último volcado correcto {when}, {size}, {where}.",
"whereBoth": "guardado en este servidor y fuera de él",
"whereLocal": "guardado en este servidor",
"whereOffhost": "solo fuera del servidor",
"none": "Aún no hay ningún volcado de la base de datos. El primero se hace esta noche.",
"failedTitle": "El último volcado de la base de datos falló",
"failedDescription": "{when}: {reason}",
"disabled": "Los volcados de la base de datos están desactivados (DB_BACKUP_ENABLED=false). Haz la copia de la base de datos de otra forma.",
"noDirectoryOffhost": "Este servidor no puede escribir en {dir}, así que los volcados solo se guardan fuera y aquí no queda ninguno. Monta el volumen de copias.",
"noDirectory": "Este servidor no puede escribir en {dir} y no hay almacenamiento externo configurado, así que no se puede hacer ningún volcado. Monta el volumen de copias."
}
},
"danger-zone": {
"title": "Zona de peligro",
+14 -1
View File
@@ -8310,7 +8310,20 @@
"restoreSkippedMoodLabelCiphertext": "Nom d’étiquette ou de groupe d’humeur chiffré avec une clé que ce serveur ne possède pas (conservé sans son nom)",
"restoreSkippedVisitCiphertext": "Texte de consultation ou de praticien chiffré avec une clé que ce serveur ne possède pas (conservé sans)",
"restoreSkippedVaccinationCiphertext": "Note de vaccination chiffrée avec une clé que ce serveur ne possède pas (dose conservée sans)",
"restoreSkippedEnvironmentLocationCiphertext": "Lieu d'une période ou d'un jour environnemental chiffré avec une clé que ce serveur ne possède pas (conservé chiffré, ces jours ne sont pas actualisés)"
"restoreSkippedEnvironmentLocationCiphertext": "Lieu d'une période ou d'un jour environnemental chiffré avec une clé que ce serveur ne possède pas (conservé chiffré, ces jours ne sont pas actualisés)",
"database": {
"title": "Dump de la base de données",
"lastGood": "Dernier dump valide {when}, {size}, {where}.",
"whereBoth": "conservé sur ce serveur et hors site",
"whereLocal": "conservé sur ce serveur",
"whereOffhost": "hors site uniquement",
"none": "Aucun dump de la base de données pour l'instant. Le premier a lieu cette nuit.",
"failedTitle": "Le dernier dump de la base de données a échoué",
"failedDescription": "{when} : {reason}",
"disabled": "Les dumps de la base de données sont désactivés (DB_BACKUP_ENABLED=false). Sauvegardez la base de données autrement.",
"noDirectoryOffhost": "Ce serveur ne peut pas écrire dans {dir} : les dumps partent uniquement hors site et aucun n'est conservé ici. Montez le volume de sauvegarde.",
"noDirectory": "Ce serveur ne peut pas écrire dans {dir} et aucun stockage hors site n'est configuré : aucun dump ne peut être fait. Montez le volume de sauvegarde."
}
},
"danger-zone": {
"title": "Zone de danger",
+14 -1
View File
@@ -8310,7 +8310,20 @@
"restoreSkippedMoodLabelCiphertext": "Nome di tag o gruppo dell’umore cifrato con una chiave che questo server non possiede (mantenuto senza nome)",
"restoreSkippedVisitCiphertext": "Testo di visita o professionista cifrato con una chiave che questo server non possiede (mantenuto senza)",
"restoreSkippedVaccinationCiphertext": "Nota di vaccinazione cifrata con una chiave che questo server non possiede (dose mantenuta senza)",
"restoreSkippedEnvironmentLocationCiphertext": "Luogo di un periodo o giorno ambientale cifrato con una chiave che questo server non possiede (mantenuto cifrato, quei giorni non vengono aggiornati)"
"restoreSkippedEnvironmentLocationCiphertext": "Luogo di un periodo o giorno ambientale cifrato con una chiave che questo server non possiede (mantenuto cifrato, quei giorni non vengono aggiornati)",
"database": {
"title": "Dump del database",
"lastGood": "Ultimo dump valido {when}, {size}, {where}.",
"whereBoth": "conservato su questo server e fuori sede",
"whereLocal": "conservato su questo server",
"whereOffhost": "solo fuori sede",
"none": "Ancora nessun dump del database. Il primo viene eseguito stanotte.",
"failedTitle": "L'ultimo dump del database non è riuscito",
"failedDescription": "{when}: {reason}",
"disabled": "I dump del database sono disattivati (DB_BACKUP_ENABLED=false). Esegui il backup del database in un altro modo.",
"noDirectoryOffhost": "Questo server non può scrivere in {dir}, quindi i dump vanno solo fuori sede e qui non ne resta nessuno. Monta il volume dei backup.",
"noDirectory": "Questo server non può scrivere in {dir} e non è configurato alcuno storage esterno, quindi non si può fare alcun dump. Monta il volume dei backup."
}
},
"danger-zone": {
"title": "Zona pericolosa",
+14 -1
View File
@@ -8285,7 +8285,20 @@
"restoreSkippedMoodLabelCiphertext": "이 서버에 없는 키로 암호화된 기분 태그 또는 그룹 이름(이름 없이 유지됨)",
"restoreSkippedVisitCiphertext": "이 서버에 없는 키로 암호화된 진료 또는 의료진 텍스트(해당 내용 없이 유지됨)",
"restoreSkippedVaccinationCiphertext": "이 서버에 없는 키로 암호화된 예방접종 메모(접종 기록은 메모 없이 유지됨)",
"restoreSkippedEnvironmentLocationCiphertext": "이 서버에 없는 키로 암호화된 환경 기간 또는 날짜의 위치(암호화된 채 유지되며 해당 날짜는 새로 가져오지 않음)"
"restoreSkippedEnvironmentLocationCiphertext": "이 서버에 없는 키로 암호화된 환경 기간 또는 날짜의 위치(암호화된 채 유지되며 해당 날짜는 새로 가져오지 않음)",
"database": {
"title": "데이터베이스 덤프",
"lastGood": "마지막 정상 덤프 {when}, {size}, {where}.",
"whereBoth": "이 서버와 외부 저장소에 보관됨",
"whereLocal": "이 서버에 보관됨",
"whereOffhost": "외부 저장소에만 있음",
"none": "아직 데이터베이스 덤프가 없습니다. 첫 덤프는 오늘 밤에 실행됩니다.",
"failedTitle": "마지막 데이터베이스 덤프가 실패했습니다",
"failedDescription": "{when}: {reason}",
"disabled": "데이터베이스 덤프가 꺼져 있습니다(DB_BACKUP_ENABLED=false). 다른 방법으로 데이터베이스를 백업하세요.",
"noDirectoryOffhost": "이 서버는 {dir}에 쓸 수 없어 덤프가 외부 저장소로만 가고 여기에는 보관되지 않습니다. 백업 볼륨을 마운트하세요.",
"noDirectory": "이 서버는 {dir}에 쓸 수 없고 외부 저장소도 설정되지 않아 데이터베이스 덤프를 만들 수 없습니다. 백업 볼륨을 마운트하세요."
}
},
"danger-zone": {
"title": "위험 구역",
+14 -1
View File
@@ -8310,7 +8310,20 @@
"restoreSkippedMoodLabelCiphertext": "Stemmingslabel of groepsnaam versleuteld met een sleutel die deze server niet heeft (behouden zonder naam)",
"restoreSkippedVisitCiphertext": "Tekst van bezoek of zorgverlener versleuteld met een sleutel die deze server niet heeft (behouden zonder)",
"restoreSkippedVaccinationCiphertext": "Vaccinatienotitie versleuteld met een sleutel die deze server niet heeft (dosis behouden zonder)",
"restoreSkippedEnvironmentLocationCiphertext": "Locatie van een omgevingsperiode of -dag versleuteld met een sleutel die deze server niet heeft (versleuteld behouden, die dagen worden niet bijgewerkt)"
"restoreSkippedEnvironmentLocationCiphertext": "Locatie van een omgevingsperiode of -dag versleuteld met een sleutel die deze server niet heeft (versleuteld behouden, die dagen worden niet bijgewerkt)",
"database": {
"title": "Databasedump",
"lastGood": "Laatste geldige dump {when}, {size}, {where}.",
"whereBoth": "bewaard op deze server en extern",
"whereLocal": "bewaard op deze server",
"whereOffhost": "alleen extern",
"none": "Nog geen databasedump. De eerste wordt vannacht gemaakt.",
"failedTitle": "De laatste databasedump is mislukt",
"failedDescription": "{when}: {reason}",
"disabled": "Databasedumps staan uit (DB_BACKUP_ENABLED=false). Maak op een andere manier een back-up van de database.",
"noDirectoryOffhost": "Deze server kan niet schrijven naar {dir}, dus dumps gaan alleen extern en hier wordt er geen bewaard. Koppel het back-upvolume.",
"noDirectory": "Deze server kan niet schrijven naar {dir} en er is geen externe opslag ingesteld, dus er kan geen databasedump worden gemaakt. Koppel het back-upvolume."
}
},
"danger-zone": {
"title": "Gevarenzone",
+14 -1
View File
@@ -8310,7 +8310,20 @@
"restoreSkippedMoodLabelCiphertext": "Nazwa tagu lub grupy nastroju zaszyfrowana kluczem, którego ten serwer nie ma (zachowana bez nazwy)",
"restoreSkippedVisitCiphertext": "Tekst wizyty lub lekarza zaszyfrowany kluczem, którego ten serwer nie ma (zachowany bez niego)",
"restoreSkippedVaccinationCiphertext": "Notatka szczepienia zaszyfrowana kluczem, którego ten serwer nie ma (dawka zachowana bez niej)",
"restoreSkippedEnvironmentLocationCiphertext": "Lokalizacja okresu lub dnia środowiskowego zaszyfrowana kluczem, którego ten serwer nie ma (zachowana w postaci zaszyfrowanej, te dni nie są odświeżane)"
"restoreSkippedEnvironmentLocationCiphertext": "Lokalizacja okresu lub dnia środowiskowego zaszyfrowana kluczem, którego ten serwer nie ma (zachowana w postaci zaszyfrowanej, te dni nie są odświeżane)",
"database": {
"title": "Zrzut bazy danych",
"lastGood": "Ostatni poprawny zrzut {when}, {size}, {where}.",
"whereBoth": "przechowywany na tym serwerze i poza nim",
"whereLocal": "przechowywany na tym serwerze",
"whereOffhost": "tylko poza serwerem",
"none": "Nie ma jeszcze zrzutu bazy danych. Pierwszy powstanie tej nocy.",
"failedTitle": "Ostatni zrzut bazy danych się nie powiódł",
"failedDescription": "{when}: {reason}",
"disabled": "Zrzuty bazy danych są wyłączone (DB_BACKUP_ENABLED=false). Zrób kopię bazy danych w inny sposób.",
"noDirectoryOffhost": "Ten serwer nie może zapisywać w {dir}, więc zrzuty trafiają tylko poza serwer i żaden nie zostaje tutaj. Podłącz wolumin kopii.",
"noDirectory": "Ten serwer nie może zapisywać w {dir} i nie ustawiono magazynu zewnętrznego, więc nie da się wykonać zrzutu bazy. Podłącz wolumin kopii."
}
},
"danger-zone": {
"title": "Strefa zagrożenia",
+14 -1
View File
@@ -8310,7 +8310,20 @@
"restoreSkippedMoodLabelCiphertext": "Etiqueta ou nome de grupo de humor selado com uma chave que este servidor não possui (mantido sem o nome)",
"restoreSkippedVisitCiphertext": "Texto de consulta ou profissional selado com uma chave que este servidor não possui (mantido sem ele)",
"restoreSkippedVaccinationCiphertext": "Nota de vacinação selada com uma chave que este servidor não possui (dose mantida sem ela)",
"restoreSkippedEnvironmentLocationCiphertext": "Local de um período ou dia de ambiente selado com uma chave que este servidor não possui (mantido selado, esses dias não são atualizados)"
"restoreSkippedEnvironmentLocationCiphertext": "Local de um período ou dia de ambiente selado com uma chave que este servidor não possui (mantido selado, esses dias não são atualizados)",
"database": {
"title": "Dump do banco de dados",
"lastGood": "Último dump válido {when}, {size}, {where}.",
"whereBoth": "guardado neste servidor e fora dele",
"whereLocal": "guardado neste servidor",
"whereOffhost": "somente fora do servidor",
"none": "Ainda não há dump do banco de dados. O primeiro é feito esta noite.",
"failedTitle": "O último dump do banco de dados falhou",
"failedDescription": "{when}: {reason}",
"disabled": "Os dumps do banco de dados estão desligados (DB_BACKUP_ENABLED=false). Faça o backup do banco de outra forma.",
"noDirectoryOffhost": "Este servidor não consegue gravar em {dir}, então os dumps vão só para fora e nenhum fica aqui. Monte o volume de backups.",
"noDirectory": "Este servidor não consegue gravar em {dir} e não há armazenamento externo configurado, então nenhum dump pode ser feito. Monte o volume de backups."
}
},
"danger-zone": {
"title": "Zona de perigo",
@@ -57,6 +57,24 @@ vi.mock("@/lib/jobs/offhost-backup", () => ({
}),
}));
const databaseHealth = {
enabled: true,
directory: "/var/lib/healthlog/backups",
localDirWritable: true,
offhostConfigured: true,
lastGood: {
at: "2026-09-21T23:31:40.000Z",
fileName: "healthlog-20260921T233000Z-daily.dump",
sizeBytes: 52_428_800,
local: true,
offhost: true,
},
lastFailure: null,
};
vi.mock("@/lib/database-backup/health", () => ({
getDatabaseBackupHealth: async () => databaseHealth,
}));
import { GET } from "../route";
import type { BackupsList } from "@/types/backups";
@@ -285,3 +303,10 @@ describe("GET /api/admin/backups — a run in progress and one a process died un
});
});
});
describe("GET /api/admin/backups — the database dump", () => {
it("carries the dump's health beside the account copies", async () => {
const body = await read();
expect(body.database).toEqual(databaseHealth);
});
});
+8 -1
View File
@@ -14,6 +14,10 @@
* stopped six weeks ago — every row it lists has a perfectly ordinary
* timestamp either way.
*
* The database row comes first in what an operator needs and last in this
* file: the nightly whole-database dump (`getDatabaseBackupHealth`), the
* one copy that rebuilds a lost server, read from its own ledger.
*
* And alongside both, the off-host leg: one row per account saying when this
* host last put that account's encrypted copy in the operator's bucket and how
* stale that is against the nightly schedule. It comes from the ledger the
@@ -31,6 +35,7 @@ import {
} from "@/lib/jobs/job-failures";
import { readBackupPassActivity } from "@/lib/jobs/backup-pass-attempts";
import { summariseBackupSchedule } from "@/lib/jobs/backup-schedule-status";
import { getDatabaseBackupHealth } from "@/lib/database-backup/health";
import {
offhostBackupConfigured,
probeOffhostLifecycle,
@@ -190,7 +195,7 @@ export const GET = apiHandler(async () => {
readBackupPassActivity(prisma, DATA_BACKUP_QUEUE, weeklyRunningSince),
readBackupPassActivity(prisma, OFFHOST_BACKUP_QUEUE, offhostRunningSince),
]);
const [purgeCount, oldestPurge, lifecycle] = await Promise.all([
const [purgeCount, oldestPurge, lifecycle, database] = await Promise.all([
prisma.offhostPurgeRequest.count({ where: stillInBucket }),
prisma.offhostPurgeRequest.findFirst({
where: stillInBucket,
@@ -200,6 +205,7 @@ export const GET = apiHandler(async () => {
configured
? probeOffhostLifecycle()
: Promise.resolve({ state: "unknown" as const, expirationDays: null }),
getDatabaseBackupHealth(now),
]);
const payload: BackupsList = {
@@ -229,6 +235,7 @@ export const GET = apiHandler(async () => {
},
lifecycle,
},
database,
};
return apiSuccess(payload);
+2
View File
@@ -28,6 +28,7 @@ import type { BackupRow, BackupsList } from "@/types/backups";
import { RestoreJobStatus, visibleRestoreJobs } from "./backup-restore-status";
import { BackupRowsCards, BackupRowsTable } from "./backups/backup-rows";
import { DatabaseDumpRow } from "./backups/database-dump-row";
import { BackupUploadCard } from "./backups/backup-upload-card";
import {
RestoreRefusalNotice,
@@ -110,6 +111,7 @@ export function BackupsSection() {
}
/>
{data?.database ? <DatabaseDumpRow health={data.database} /> : null}
{data?.schedule ? (
<ScheduleHealthNotice schedule={data.schedule} />
) : null}
@@ -0,0 +1,114 @@
"use client";
/**
* The nightly whole-database dump, as one row on the backups card.
*
* The account copies below it restore one account on this server; the dump is
* the copy that rebuilds the server. So the row leads the card and says three
* things: when the newest good dump was taken and where it is, whether the
* newest run failed, and whether this server can keep a dump at all.
*/
import { AlertTriangle, HardDrive } from "lucide-react";
import { SettingsInfoTile } from "@/components/settings/_info-tile";
import { formatBytes } from "@/lib/format/bytes";
import { useFormatters, useTranslations } from "@/lib/i18n/context";
import type { DatabaseBackupHealth } from "@/lib/database-backup/health";
function whereKept(
good: NonNullable<DatabaseBackupHealth["lastGood"]>,
t: ReturnType<typeof useTranslations>["t"],
) {
if (good.local && good.offhost) {
return t("admin.section.backups.database.whereBoth");
}
if (good.offhost) return t("admin.section.backups.database.whereOffhost");
return t("admin.section.backups.database.whereLocal");
}
export function DatabaseDumpRow({ health }: { health: DatabaseBackupHealth }) {
const { t, locale } = useTranslations();
const fmt = useFormatters();
const good = health.lastGood;
const failure = health.lastFailure;
const failedSinceGood =
failure !== null && (good === null || failure.at > good.at);
let setupProblem: string | null = null;
if (!health.enabled) {
setupProblem = t("admin.section.backups.database.disabled");
} else if (!health.localDirWritable) {
setupProblem = health.offhostConfigured
? t("admin.section.backups.database.noDirectoryOffhost", {
dir: health.directory,
})
: t("admin.section.backups.database.noDirectory", {
dir: health.directory,
});
}
return (
<div className="space-y-2" data-slot="database-dump">
<div className="flex items-start gap-2">
<HardDrive
className="text-muted-foreground mt-0.5 h-4 w-4 shrink-0"
aria-hidden="true"
/>
<div>
<p className="text-sm font-medium">
{t("admin.section.backups.database.title")}
</p>
<p
className="text-muted-foreground text-xs"
data-slot="database-dump-last-good"
>
{good
? t("admin.section.backups.database.lastGood", {
when: fmt.dateTime(good.at),
size: formatBytes(good.sizeBytes ?? 0, locale),
where: whereKept(good, t),
})
: t("admin.section.backups.database.none")}
</p>
</div>
</div>
{failedSinceGood && failure ? (
<div
role="alert"
data-slot="database-dump-failed"
className="border-destructive/40 bg-destructive/10 rounded-md border px-3 py-2 text-sm"
>
<div className="flex items-start gap-2">
<AlertTriangle
className="mt-0.5 h-4 w-4 shrink-0"
aria-hidden="true"
/>
<div>
<p className="font-medium">
{t("admin.section.backups.database.failedTitle")}
</p>
<p className="text-xs">
{t("admin.section.backups.database.failedDescription", {
when: fmt.dateTime(failure.at),
reason: failure.error,
})}
</p>
</div>
</div>
</div>
) : null}
{setupProblem ? (
<SettingsInfoTile
icon={AlertTriangle}
tone="warning"
role="alert"
data-slot="database-dump-setup"
>
{setupProblem}
</SettingsInfoTile>
) : null}
</div>
);
}
+7
View File
@@ -9,6 +9,7 @@
* 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 { 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";
@@ -101,6 +102,12 @@ export interface BackupsList {
* 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