diff --git a/frontend/src/features/settings/__tests__/ProductUsageTab.test.tsx b/frontend/src/features/settings/__tests__/ProductUsageTab.test.tsx
index 32515507..1a306b3c 100644
--- a/frontend/src/features/settings/__tests__/ProductUsageTab.test.tsx
+++ b/frontend/src/features/settings/__tests__/ProductUsageTab.test.tsx
@@ -317,3 +317,30 @@ it('states in the consent dialog that the connection only runs outwards', async
await screen.findByText('productUsage.sectionOneWay');
await screen.findByText('productUsage.oneWay');
});
+
+// The portal is public. Reaching it must not depend on having joined or on
+// the 15-minute voting session that `connect` creates — an operator deciding
+// whether to participate should be able to look at it first.
+describe('the plain usage portal link', () => {
+ it('opens the collector in a new tab before participation, without a session', async () => {
+ mount();
+ const link = await screen.findByRole('link', { name: 'productUsage.openUsagePortal' });
+ expect(link).toHaveAttribute('href', 'https://usage.picpeak.app');
+ expect(link).toHaveAttribute('target', '_blank');
+ expect(link).toHaveAttribute('rel', 'noopener noreferrer');
+ expect(service.portalSession).not.toHaveBeenCalled();
+ // The session-bound link is a different thing and stays behind `connect`.
+ expect(screen.queryByText('productUsage.openPortal')).toBeNull();
+ });
+
+ it('is not offered when the collector URL is unusable', async () => {
+ vi.mocked(service.status).mockResolvedValue({
+ ...status,
+ collector_url: null,
+ collector_error: 'INVALID_COLLECTOR_URL'
+ });
+ mount();
+ await screen.findByText('productUsage.invalidCollectorUrl');
+ expect(screen.queryByRole('link', { name: 'productUsage.openUsagePortal' })).toBeNull();
+ });
+});
diff --git a/frontend/src/features/settings/tabs/ProductUsageTab.tsx b/frontend/src/features/settings/tabs/ProductUsageTab.tsx
index fc731ed6..fb571e73 100644
--- a/frontend/src/features/settings/tabs/ProductUsageTab.tsx
+++ b/frontend/src/features/settings/tabs/ProductUsageTab.tsx
@@ -7,6 +7,7 @@ import {
} from '../../../services/productUsage.service';
import {
ArrowUpFromLine,
+ ExternalLink,
Globe,
ListChecks,
MessageSquare,
@@ -377,14 +378,29 @@ export default function ProductUsageTab() {
>
)}
{data.collector_url && (
-
- {t('productUsage.transparency')}
-
+ <>
+ {/* The public portal itself, not the session-bound link below:
+ it needs neither participation nor a voting session, so an
+ operator can look at the portal before deciding to join.
+ Styled as a button so it reads as an action, not a footnote. */}
+
+ {t('productUsage.openUsagePortal')}
+
+
+
+ {t('productUsage.transparency')}
+
+ >
)}
diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json
index 1d450d62..1dfc1e1a 100644
--- a/frontend/src/i18n/locales/de.json
+++ b/frontend/src/i18n/locales/de.json
@@ -435,7 +435,7 @@
"notice": "Optionale Nutzungsberichte zeigen, welche Funktionen für die Community wichtig sind. Die Übermittlung ist aus, bis Sie sich aktiv dafür entscheiden.",
"ignore": "Ignorieren",
"ignoreHint": "Wenn Sie ignorieren, erscheint dieser Hinweis nicht erneut — Sie können weiterhin unter Einstellungen → Produktnutzung teilnehmen.",
- "review": "Teilnahme prüfen",
+ "review": "Details ansehen",
"cancel": "Abbrechen",
"loading": "Teilnahmeeinstellungen werden geladen…",
"failed": "Der Vorgang konnte nicht abgeschlossen werden. Prüfen Sie den Status und versuchen Sie es erneut.",
@@ -458,6 +458,7 @@
"enable": "Produktnutzung aktivieren",
"disable": "Deaktivieren & Daten löschen",
"retry": "Erneut versuchen / fälligen Bericht senden",
+ "openUsagePortal": "Nutzungsportal öffnen",
"transparency": "Öffentliches Schema & Datenschutzhinweise",
"linkCollector": "Wohin Berichte gesendet werden",
"hash": "Ihr vertraulicher Abfrage-Hash",
diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json
index 1b09a0ec..3a953470 100644
--- a/frontend/src/i18n/locales/en.json
+++ b/frontend/src/i18n/locales/en.json
@@ -458,6 +458,7 @@
"enable": "Enable product usage",
"disable": "Disable & delete my data",
"retry": "Retry / send if due",
+ "openUsagePortal": "Open usage portal",
"transparency": "Read the public schema & privacy details",
"linkCollector": "Where reports are sent",
"hash": "Your private lookup hash",