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' + )} +

)}
{data.status === 'disabled' ? ( diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index df29a7e7..bd2b1364 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -24,6 +24,7 @@ "hash": "Dein vertraulicher Abfrage-Hash", "lastReport": "Zuletzt angenommener Bericht: {{date}} (UTC)", "deliveryProblem": "Die Übertragung benötigt Aufmerksamkeit. Bei Löschung oder Identitätskonflikt ist die Erfassung gestoppt. Versuche es erneut oder deaktiviere die Teilnahme, um die Daten zu löschen.", + "signingKeyUnreadable": "Der Signaturschlüssel für die Nutzungsdaten kann nicht gelesen werden. Meist wurde USAGE_ENCRYPTION_KEY — oder das als Rückfallwert genutzte JWT_SECRET — geändert. Berichte können nicht gesendet und auch die Löschanfrage kann nicht signiert werden. Stellen Sie das ursprüngliche Schlüsselmaterial wieder her, um die Löschung abzuschließen; erneutes Senden oder Deaktivieren allein behebt dies nicht.", "inspect": "Genau sehen, was geteilt wird", "preview": "Nächsten Bericht ansehen", "lastPacket": "Zuletzt angenommenes signiertes Paket", diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 7f9d1e37..b9f7c1de 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -24,6 +24,7 @@ "hash": "Your private lookup hash", "lastReport": "Last accepted report: {{date}} (UTC)", "deliveryProblem": "Delivery needs attention. Collection stops during deletion or an identity conflict. Use retry, or disable participation to delete its data.", + "signingKeyUnreadable": "The usage signing key cannot be read, which usually means USAGE_ENCRYPTION_KEY — or JWT_SECRET, which it falls back to — was changed. Reports cannot be sent and the deletion request cannot be signed either. Restore the original encryption material to finish deletion; retrying or disabling will not resolve it on its own.", "inspect": "See exactly what is shared", "preview": "Preview next report", "lastPacket": "Last accepted signed packet",