diff --git a/backend/src/usage/UsageService.js b/backend/src/usage/UsageService.js index 40d949b9..ef74b1f7 100644 --- a/backend/src/usage/UsageService.js +++ b/backend/src/usage/UsageService.js @@ -48,6 +48,7 @@ const SETTING_KEYS = [ 'oidc_issuer_url', 'oidc_client_id', 'backup_enabled', + 'database_backup_enabled', 'backup_destination_type', 'backup_s3_bucket', 'theme_config', @@ -308,6 +309,12 @@ class UsageService { ) { await this.db('product_usage_state') .where({ id: 1 }) + // Never over an opt-out. A withdrawal that arrived while this + // binding lookup was in flight would otherwise be replaced by + // identity_conflict, and tick() stops there — so the deletion the + // operator asked for would never be sent. The collector-conflict + // handler below already guards the same way. + .whereNot({ status: 'deletion_pending' }) .update({ status: 'identity_conflict', last_error: 'INSTANCE_COPY_DETECTED' @@ -507,7 +514,12 @@ class UsageService { features.oauth.configured = truth(settings.oidc_enabled) && Boolean(settings.oidc_issuer_url && settings.oidc_client_id); - features.backup.configured = truth(settings.backup_enabled); + // Either kind counts. The middleware records /backup/* and + // /database-backup/* under the same capability, so reading only + // backup_enabled reported `used: true, configured: false` for an install + // whose only backup is the scheduled database one. + features.backup.configured = + truth(settings.backup_enabled) || truth(settings.database_backup_enabled); features.s3_storage.configured = (settings.backup_destination_type === 's3' && Boolean(settings.backup_s3_bucket)) || diff --git a/frontend/src/features/settings/tabs/ProductUsageTab.tsx b/frontend/src/features/settings/tabs/ProductUsageTab.tsx index 502c71c1..e69a2e8f 100644 --- a/frontend/src/features/settings/tabs/ProductUsageTab.tsx +++ b/frontend/src/features/settings/tabs/ProductUsageTab.tsx @@ -156,7 +156,17 @@ export default function ProductUsageTab() {
{t('productUsage.lastReport', { date: data.last_report_date })}
)} {data.last_error && ( -{t('productUsage.deliveryProblem')}
++ {/* Retrying cannot fix an unreadable signing key, and neither can + disabling: without the original encryption material the + deletion request cannot be signed either. Telling the operator + to retry would send them in a circle. */} + {t( + data.last_error === 'SIGNING_KEY_UNREADABLE' + ? 'productUsage.signingKeyUnreadable' + : 'productUsage.deliveryProblem' + )} +
)}