diff --git a/frontend/src/features/settings/__tests__/ProductUsageTab.test.tsx b/frontend/src/features/settings/__tests__/ProductUsageTab.test.tsx index 8e8c39db..844caf23 100644 --- a/frontend/src/features/settings/__tests__/ProductUsageTab.test.tsx +++ b/frontend/src/features/settings/__tests__/ProductUsageTab.test.tsx @@ -103,6 +103,32 @@ it('pending v2 confirmation clearly keeps v1 and cannot queue another upgrade', expect(screen.getByRole('button', { name: 'productUsage.reviewUpgrade' })).toBeDisabled(); expect(service.upgradeConsent).not.toHaveBeenCalled(); }); +it.each(['activation_pending', 'deletion_pending'] as const)('a stuck packet in %s names nothing, since neither control is actually gated by it', async (pendingStatus) => { + // Outside `active`, the portal renders as a plain always-enabled link and + // no v5-upgrade section exists — pending_action blocks neither, so the + // note must not claim it does. + vi.mocked(service.status).mockResolvedValue({ ...status, status: pendingStatus, collector_url: 'https://usage.picpeak.app', pending_action: pendingStatus === 'activation_pending' ? 'register' : 'delete' }); + mount(); + await screen.findByText(`productUsage.states.${pendingStatus}`); + expect(screen.queryByText('productUsage.pendingBlocksActions')).toBeNull(); + expect(screen.queryByText('productUsage.pendingBlocksPortal')).toBeNull(); +}); +it('a stuck report on an already-current schema names only the portal, not a non-existent upgrade button', async () => { + vi.mocked(service.status).mockResolvedValue({ ...status, status: 'active', consent_update_available: false, pending_action: 'report' }); + mount(); + expect(await screen.findByText('productUsage.pendingBlocksPortal')).toBeInTheDocument(); + expect(screen.queryByText('productUsage.pendingBlocksActions')).toBeNull(); + expect(screen.queryByText('productUsage.reviewUpgrade')).toBeNull(); + expect(screen.getByRole('button', { name: 'productUsage.openUsagePortal' })).toBeDisabled(); +}); +it('a stuck report with a v5 upgrade available names both actually-gated controls', async () => { + vi.mocked(service.status).mockResolvedValue({ ...status, status: 'active', consent_update_available: true, collector_url: 'https://usage.picpeak.app', pending_action: 'report' }); + mount(); + expect(await screen.findByText('productUsage.pendingBlocksActions')).toBeInTheDocument(); + expect(screen.queryByText('productUsage.pendingBlocksPortal')).toBeNull(); + expect(screen.getByRole('button', { name: 'productUsage.reviewUpgrade' })).toBeDisabled(); + expect(screen.getByRole('button', { name: 'productUsage.openUsagePortal' })).toBeDisabled(); +}); describe('product usage controls', () => { it('offers identity-free audit receipts after opt-out without restoring participation controls', async () => { vi.mocked(service.status).mockResolvedValue({ diff --git a/frontend/src/features/settings/tabs/ProductUsageTab.tsx b/frontend/src/features/settings/tabs/ProductUsageTab.tsx index c4bc3ae8..8022c870 100644 --- a/frontend/src/features/settings/tabs/ProductUsageTab.tsx +++ b/frontend/src/features/settings/tabs/ProductUsageTab.tsx @@ -223,14 +223,20 @@ export default function ProductUsageTab() { )} - {data.pending_action && data.pending_action !== 'consent' && ( - // The v5-upgrade and portal buttons below are disabled by the same - // pending-packet guard the backend enforces (command() refuses a - // second packet while one is still unacknowledged) — without this - // note the buttons just look broken, and "Retry" above them isn't - // obviously the fix. + {active && data.pending_action && data.pending_action !== 'consent' && ( + // The portal button below (and the v5-upgrade button above, when + // present) are disabled by the same pending-packet guard the + // backend enforces (command() refuses a second packet while one is + // still unacknowledged) — without this note they just look broken, + // and "Retry" above isn't obviously the fix. Gated on `active`: + // outside that status the portal renders as a plain, un-gated link + // and no v5-upgrade section exists, so pending_action blocks + // nothing this note could correctly describe (e.g. + // activation_pending/deletion_pending with their own packet still + // in flight). Which controls it names depends on whether the + // v5-upgrade section is actually on screen.

- {t('productUsage.pendingBlocksActions')} + {t(data.consent_update_available ? 'productUsage.pendingBlocksActions' : 'productUsage.pendingBlocksPortal')}

)}
diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index 4cd92837..e5c78611 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -19,6 +19,7 @@ "upgradeExplanation": "Deine bestehende Teilnahme behält ihren bisherigen Umfang. Sieh dir den erweiterten Katalog und die beiden Bestandszahlen an, bevor du dich entscheidest. Ablehnen beendet die Teilnahme nicht.", "upgradePending": "Die signierte Erweiterung wartet auf die Bestätigung des Collectors. Bis dahin wird nur der bisher bestätigte Umfang erfasst. Versuche es erneut, wenn der Collector erreichbar ist, oder deaktiviere die Teilnahme, um zu stoppen und zu löschen.", "pendingBlocksActions": "Das v5-Upgrade und das Nutzungsportal sind gesperrt, bis der ausstehende Bericht erneut gesendet wurde oder zugestellt ist — nutze oben „Jetzt senden“.", + "pendingBlocksPortal": "Das Nutzungsportal ist gesperrt, bis der ausstehende Bericht erneut gesendet wurde oder zugestellt ist — nutze oben „Jetzt senden“.", "catalog": { "crm": { "name": "Kundenverwaltung", diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 5e453864..8b50a119 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -19,6 +19,7 @@ "upgradeExplanation": "Your existing participation keeps its current scope. Review the expanded catalog and the two inventory totals before deciding whether to upgrade. Declining does not end participation.", "upgradePending": "The signed consent upgrade is pending confirmation. Only the previously accepted scope is collected. Retry when the collector is available, or disable participation to stop and delete.", "pendingBlocksActions": "The v5 upgrade and the usage portal are unavailable until the pending report is retried or delivered — use \"Retry / send if due\" above.", + "pendingBlocksPortal": "The usage portal is unavailable until the pending report is retried or delivered — use \"Retry / send if due\" above.", "catalog": { "crm": { "name": "Client management",