diff --git a/messages/de.json b/messages/de.json
index eaf1be7fc..d29da7322 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -8322,7 +8322,10 @@
"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."
+ "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.",
+ "retentionLocal": "Gelöschte Konten bleiben in den Dumps, bis diese aussortiert werden: Auf diesem Server bleiben die neuesten {nights}.",
+ "retentionOffhost": "Externe Kopien entfernt die Lebenszyklus-Regel des Buckets nach {days} Tagen.",
+ "retentionOffhostMissing": "Keine Lebenszyklus-Regel im Bucket gilt für database/. Dumps dort und die gelöschten Konten darin werden deshalb nie entfernt. Lege eine Regel für dieses Präfix an."
}
},
"danger-zone": {
diff --git a/messages/en.json b/messages/en.json
index b5656728e..0d22de2aa 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -8322,7 +8322,10 @@
"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."
+ "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.",
+ "retentionLocal": "Deleted accounts stay in the dumps until those age out: the newest {nights} are kept on this server.",
+ "retentionOffhost": "Off-host copies are removed after {days} days by the bucket's lifecycle rule.",
+ "retentionOffhostMissing": "No lifecycle rule in the bucket covers database/, so dumps there, and the deleted accounts inside them, are never removed. Add a rule for that prefix."
}
},
"danger-zone": {
diff --git a/messages/es.json b/messages/es.json
index a9c2b3c55..535cccd86 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -8322,7 +8322,10 @@
"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."
+ "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.",
+ "retentionLocal": "Las cuentas eliminadas siguen en los volcados hasta que estos caducan: en este servidor se guardan los {nights} más recientes.",
+ "retentionOffhost": "La regla de ciclo de vida del bucket elimina las copias externas tras {days} días.",
+ "retentionOffhostMissing": "Ninguna regla de ciclo de vida del bucket cubre database/, así que los volcados de ahí, y las cuentas eliminadas que contienen, nunca se borran. Añade una regla para ese prefijo."
}
},
"danger-zone": {
diff --git a/messages/fr.json b/messages/fr.json
index 2c38c64f1..f3d2c0519 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -8322,7 +8322,10 @@
"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."
+ "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.",
+ "retentionLocal": "Les comptes supprimés restent dans les dumps jusqu'à leur expiration : ce serveur conserve les {nights} plus récents.",
+ "retentionOffhost": "La règle de cycle de vie du bucket supprime les copies hors site après {days} jours.",
+ "retentionOffhostMissing": "Aucune règle de cycle de vie du bucket ne couvre database/ : les dumps qui s'y trouvent, et les comptes supprimés qu'ils contiennent, ne sont jamais effacés. Ajoutez une règle pour ce préfixe."
}
},
"danger-zone": {
diff --git a/messages/it.json b/messages/it.json
index 86af90b9d..fe7e6b4f9 100644
--- a/messages/it.json
+++ b/messages/it.json
@@ -8322,7 +8322,10 @@
"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."
+ "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.",
+ "retentionLocal": "Gli account eliminati restano nei dump finché questi non scadono: su questo server restano i {nights} più recenti.",
+ "retentionOffhost": "La regola del ciclo di vita del bucket elimina le copie esterne dopo {days} giorni.",
+ "retentionOffhostMissing": "Nessuna regola del ciclo di vita del bucket copre database/, quindi i dump lì, e gli account eliminati che contengono, non vengono mai rimossi. Aggiungi una regola per quel prefisso."
}
},
"danger-zone": {
diff --git a/messages/ko.json b/messages/ko.json
index 8325cf886..4300dfc7b 100644
--- a/messages/ko.json
+++ b/messages/ko.json
@@ -8297,7 +8297,10 @@
"failedDescription": "{when}: {reason}",
"disabled": "데이터베이스 덤프가 꺼져 있습니다(DB_BACKUP_ENABLED=false). 다른 방법으로 데이터베이스를 백업하세요.",
"noDirectoryOffhost": "이 서버는 {dir}에 쓸 수 없어 덤프가 외부 저장소로만 가고 여기에는 보관되지 않습니다. 백업 볼륨을 마운트하세요.",
- "noDirectory": "이 서버는 {dir}에 쓸 수 없고 외부 저장소도 설정되지 않아 데이터베이스 덤프를 만들 수 없습니다. 백업 볼륨을 마운트하세요."
+ "noDirectory": "이 서버는 {dir}에 쓸 수 없고 외부 저장소도 설정되지 않아 데이터베이스 덤프를 만들 수 없습니다. 백업 볼륨을 마운트하세요.",
+ "retentionLocal": "삭제된 계정은 덤프가 만료될 때까지 덤프에 남습니다. 이 서버에는 최신 덤프 {nights}개가 보관됩니다.",
+ "retentionOffhost": "버킷의 수명 주기 규칙이 외부 사본을 {days}일 후에 삭제합니다.",
+ "retentionOffhostMissing": "버킷의 어떤 수명 주기 규칙도 database/를 다루지 않아 그곳의 덤프와 그 안의 삭제된 계정이 절대 삭제되지 않습니다. 이 접두사에 대한 규칙을 추가하세요."
}
},
"danger-zone": {
diff --git a/messages/nl.json b/messages/nl.json
index 9026deedf..63d6863ce 100644
--- a/messages/nl.json
+++ b/messages/nl.json
@@ -8322,7 +8322,10 @@
"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."
+ "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.",
+ "retentionLocal": "Verwijderde accounts blijven in de dumps tot die verlopen: op deze server blijven de nieuwste {nights} bewaard.",
+ "retentionOffhost": "De levenscyclusregel van de bucket verwijdert externe kopieën na {days} dagen.",
+ "retentionOffhostMissing": "Geen levenscyclusregel in de bucket geldt voor database/, dus dumps daar, en de verwijderde accounts erin, worden nooit verwijderd. Voeg een regel voor dat voorvoegsel toe."
}
},
"danger-zone": {
diff --git a/messages/pl.json b/messages/pl.json
index 850a5f449..e117927df 100644
--- a/messages/pl.json
+++ b/messages/pl.json
@@ -8322,7 +8322,10 @@
"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."
+ "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.",
+ "retentionLocal": "Usunięte konta pozostają w zrzutach, dopóki te nie wygasną: na tym serwerze zostaje {nights} najnowszych.",
+ "retentionOffhost": "Reguła cyklu życia bucketu usuwa kopie zewnętrzne po {days} dniach.",
+ "retentionOffhostMissing": "Żadna reguła cyklu życia bucketu nie obejmuje database/, więc zrzuty tam, wraz z usuniętymi kontami w nich, nigdy nie są usuwane. Dodaj regułę dla tego prefiksu."
}
},
"danger-zone": {
diff --git a/messages/pt-BR.json b/messages/pt-BR.json
index 7343679c5..b4df76db7 100644
--- a/messages/pt-BR.json
+++ b/messages/pt-BR.json
@@ -8322,7 +8322,10 @@
"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."
+ "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.",
+ "retentionLocal": "Contas excluídas continuam nos dumps até que eles expirem: este servidor guarda os {nights} mais recentes.",
+ "retentionOffhost": "A regra de ciclo de vida do bucket remove as cópias externas após {days} dias.",
+ "retentionOffhostMissing": "Nenhuma regra de ciclo de vida do bucket cobre database/, então os dumps ali, e as contas excluídas dentro deles, nunca são removidos. Adicione uma regra para esse prefixo."
}
},
"danger-zone": {
diff --git a/src/app/api/admin/backups/__tests__/route.test.ts b/src/app/api/admin/backups/__tests__/route.test.ts
index ec407f99f..b52c31a8c 100644
--- a/src/app/api/admin/backups/__tests__/route.test.ts
+++ b/src/app/api/admin/backups/__tests__/route.test.ts
@@ -54,12 +54,14 @@ vi.mock("@/lib/jobs/offhost-backup", () => ({
probeOffhostLifecycle: async () => ({
state: "missing",
expirationDays: null,
+ database: { state: "missing", expirationDays: null },
}),
}));
const databaseHealth = {
enabled: true,
directory: "/var/lib/healthlog/backups",
+ keepNightly: 7,
localDirWritable: true,
offhostConfigured: true,
lastGood: {
@@ -184,7 +186,11 @@ describe("GET /api/admin/backups — off-host freshness", () => {
periodHours: 24,
rows: [],
// No bucket to ask on a host that is not configured.
- lifecycle: { state: "unknown", expirationDays: null },
+ lifecycle: {
+ state: "unknown",
+ expirationDays: null,
+ database: { state: "unknown", expirationDays: null },
+ },
});
});
@@ -247,6 +253,7 @@ describe("GET /api/admin/backups — off-host freshness", () => {
expect(body.offhost.lifecycle).toEqual({
state: "missing",
expirationDays: null,
+ database: { state: "missing", expirationDays: null },
});
expect(body.offhost.pendingDeletions).toEqual({
count: 2,
diff --git a/src/app/api/admin/backups/route.ts b/src/app/api/admin/backups/route.ts
index c9bd278cd..f4854239f 100644
--- a/src/app/api/admin/backups/route.ts
+++ b/src/app/api/admin/backups/route.ts
@@ -41,6 +41,7 @@ import {
probeOffhostLifecycle,
} from "@/lib/jobs/offhost-backup";
import { OFFHOST_BACKUP_QUEUE } from "@/lib/jobs/offhost-backup-queue";
+import { LIFECYCLE_UNKNOWN } from "@/lib/jobs/offhost-lifecycle";
import {
classifyOffhostBackup,
OFFHOST_BACKUP_PERIOD_HOURS,
@@ -202,9 +203,7 @@ export const GET = apiHandler(async () => {
orderBy: { requestedAt: "asc" },
select: { requestedAt: true, lastFailure: true },
}),
- configured
- ? probeOffhostLifecycle()
- : Promise.resolve({ state: "unknown" as const, expirationDays: null }),
+ configured ? probeOffhostLifecycle() : Promise.resolve(LIFECYCLE_UNKNOWN),
getDatabaseBackupHealth(now),
]);
diff --git a/src/components/admin/__tests__/offhost-backups-section.test.tsx b/src/components/admin/__tests__/offhost-backups-section.test.tsx
index 3631fe121..0ca93728b 100644
--- a/src/components/admin/__tests__/offhost-backups-section.test.tsx
+++ b/src/components/admin/__tests__/offhost-backups-section.test.tsx
@@ -26,7 +26,11 @@ import { OffhostBackupsSection } from "../offhost-backups-section";
const QUIET = {
activity: { runningSince: null, interrupted: [] },
pendingDeletions: { count: 0, oldestRequestedAt: null, lastFailure: null },
- lifecycle: { state: "configured" as const, expirationDays: 30 },
+ lifecycle: {
+ state: "configured" as const,
+ expirationDays: 30,
+ database: { state: "configured" as const, expirationDays: 30 },
+ },
};
function render(data: BackupsList["offhost"] | null): string {
@@ -133,7 +137,11 @@ describe("
+ {t("admin.section.backups.database.retentionLocal", { + nights: health.keepNightly, + })} + {bucketLifecycle?.state === "configured" + ? ` ${t("admin.section.backups.database.retentionOffhost", { + days: bucketLifecycle.expirationDays ?? 0, + })}` + : null} +
@@ -99,6 +122,17 @@ export function DatabaseDumpRow({ health }: { health: DatabaseBackupHealth }) { ) : null} + {bucketLifecycle?.state === "missing" ? ( +