From 8d0c32902dd78324286a7585188d0c292692c8ec Mon Sep 17 00:00:00 2001
From: Paul Nothaft
Date: Tue, 8 Sep 2026 15:58:24 +0200
Subject: [PATCH 1/3] feat(setup): add anonymous usage-reporting opt-in to the
first-run wizard
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Adds a step between the SMTP/site-URL config step and the final
thank-you screen, asking whether to participate in the existing
Product usage & feedback reporting. Kept short — three points on how
it differs from typical telemetry (one-way only, no personal data,
and participants can browse the same shared feature-adoption dataset
other installations report) plus an explicit consent checkbox — and
calls the same enable() endpoint the post-install Settings page uses.
Skipping does nothing; participation can be toggled anytime from
Settings → Product usage.
---
frontend/src/i18n/locales/de.json | 15 +++++
frontend/src/i18n/locales/en.json | 15 +++++
frontend/src/pages/SetupPage.tsx | 98 +++++++++++++++++++++++++++++--
3 files changed, 122 insertions(+), 6 deletions(-)
diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json
index cff4c6c8..551fc184 100644
--- a/frontend/src/i18n/locales/de.json
+++ b/frontend/src/i18n/locales/de.json
@@ -5043,6 +5043,21 @@
"saveFailed": "Einige Änderungen konnten nicht gespeichert werden — Sie können sie unter Einstellungen → Veranstaltungsarten abschließen.",
"deleteFailed": "Eine Löschung ist fehlgeschlagen — die Liste wurde neu geladen. Mitgelieferte Arten können nur hier gelöscht werden; versuchen Sie es erneut oder fahren Sie mit ihnen fort."
},
+ "usageReporting": {
+ "subtitle": "Noch eine Sache",
+ "intro": "Möchten Sie helfen, die Weiterentwicklung von PicPeak mitzugestalten? Die anonyme Nutzungsstatistik ist freiwillig — und funktioniert anders als das Tracking, das Sie sonst gewohnt sind abzulehnen.",
+ "oneWayTitle": "Anders als Google Analytics & Co.",
+ "oneWayDesc": "Ein reiner Einwegkanal: PicPeak sendet signierte Berichte, kann aber niemals Befehle zurückempfangen. Keine Fotos, Namen, E-Mails oder Klickverfolgung — niemals.",
+ "mutualTitle": "Sehen, was alle anderen nutzen",
+ "mutualDesc": "Als Teilnehmer sehen Sie denselben geteilten Datensatz — welche Funktionen andere Installationen wirklich verwenden, nicht nur das, was Sie selbst senden.",
+ "feedbackTitle": "Feedback direkt teilen",
+ "feedbackDesc": "Senden Sie kurzes Feedback oder Funktionswünsche direkt an den Entwickler — ganz einfach aus den Einstellungen.",
+ "consentCheck": "Ich bin einverstanden — jederzeit wieder abschaltbar unter Einstellungen → Produktnutzung.",
+ "enable": "Ja, ich möchte teilnehmen",
+ "skip": "Vielleicht später",
+ "enabled": "Danke für Ihre Teilnahme!",
+ "enableFailed": "Konnte gerade nicht aktiviert werden — Sie können es jederzeit unter Einstellungen → Produktnutzung erneut versuchen."
+ },
"community": {
"subtitle": "Alles bereit",
"mission": "PicPeak gibt es, damit Fotografinnen und Fotografen ihre Galerien und Kundendaten selbst besitzen — auf dem eigenen Server, ohne monatliche SaaS-Gebühren. Danke, dass Sie es ausprobieren.",
diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json
index aaa4ddc4..8f4b087d 100644
--- a/frontend/src/i18n/locales/en.json
+++ b/frontend/src/i18n/locales/en.json
@@ -4925,6 +4925,21 @@
"saveFailed": "Some event type changes could not be saved — you can finish them in Settings → Event Types.",
"deleteFailed": "A deletion failed — the list has been reloaded. Built-in types can only be deleted here, so retry or continue with them kept."
},
+ "usageReporting": {
+ "subtitle": "One more thing",
+ "intro": "Want to help shape what PicPeak builds next? Anonymous usage reporting is optional, and it works differently from the analytics you're used to declining.",
+ "oneWayTitle": "Not like Google Analytics",
+ "oneWayDesc": "A one-way channel only: PicPeak sends signed reports but can never receive commands back. No photos, names, emails or click tracking — ever.",
+ "mutualTitle": "See what everyone else uses",
+ "mutualDesc": "As a participant, you can browse the same shared dataset — which features other installations actually use, not just what you send.",
+ "feedbackTitle": "Share feedback directly",
+ "feedbackDesc": "Send quick feedback or feature requests straight to the maintainer, right from Settings.",
+ "consentCheck": "I agree to participate — I can turn this off anytime in Settings → Product usage.",
+ "enable": "Yes, I'll participate",
+ "skip": "Maybe later",
+ "enabled": "Thanks for participating!",
+ "enableFailed": "Couldn't enable it just now — you can try again anytime in Settings → Product usage."
+ },
"community": {
"subtitle": "You're all set",
"mission": "PicPeak exists so photographers can own their galleries and client data — on their own server, without monthly SaaS fees. Thanks for giving it a try.",
diff --git a/frontend/src/pages/SetupPage.tsx b/frontend/src/pages/SetupPage.tsx
index c5c35eea..8d186d00 100644
--- a/frontend/src/pages/SetupPage.tsx
+++ b/frontend/src/pages/SetupPage.tsx
@@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { Navigate, useNavigate } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
-import { Key, Mail, Lock, Eye, EyeOff, AlertCircle, ArrowLeft, ArrowRight, Copy, Check, ExternalLink, Bug, Lightbulb, Star, Coffee } from 'lucide-react';
+import { Key, Mail, Lock, Eye, EyeOff, AlertCircle, ArrowLeft, ArrowRight, Copy, Check, ExternalLink, Bug, Lightbulb, Star, Coffee, ShieldOff, Users, MessageSquare } from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
import { toast } from 'react-toastify';
import { useTranslation } from 'react-i18next';
@@ -11,6 +11,7 @@ import { useAdminAuth } from '../contexts';
import { setupService } from '../services/setup.service';
import { settingsService } from '../services/settings.service';
import { featureFlagsService, type FeatureFlags, type FeatureKey } from '../services/featureFlags.service';
+import { productUsageService } from '../services/productUsage.service';
import { PicpeakRestoreCard } from '../components/admin/PicpeakBackupCard';
import { SetupConfigStep } from '../components/admin/SetupConfigStep';
import { SetupEventTypesStep } from '../components/admin/SetupEventTypesStep';
@@ -36,6 +37,17 @@ const COMMUNITY_LINKS: {
{ key: 'support', href: 'https://www.buymeacoffee.com/theluap', icon: Coffee },
];
+// Anonymous usage-reporting opt-in, one step before the final thank-you
+// screen. Kept deliberately short — most people reflexively decline "send us
+// data" prompts, so this leads with what makes PicPeak's reporting different
+// from typical analytics rather than repeating the full disclosure the
+// Settings → Product usage tab already shows in detail.
+const USAGE_REPORTING_POINTS: { key: string; icon: LucideIcon }[] = [
+ { key: 'oneWay', icon: ShieldOff },
+ { key: 'mutual', icon: Users },
+ { key: 'feedback', icon: MessageSquare },
+];
+
// "How will you use PicPeak?" — the opt-in feature groups shown after the admin
// account is created. galleries/analytics/userManagement are always on and not
// listed. Labels/descriptions reuse the existing Settings→Features i18n keys
@@ -70,7 +82,7 @@ export const SetupPage: React.FC = () => {
staleTime: Infinity,
});
- const [step, setStep] = useState<'token' | 'account' | 'usage' | 'eventTypes' | 'restore' | 'config' | 'community'>('token');
+ const [step, setStep] = useState<'token' | 'account' | 'usage' | 'eventTypes' | 'restore' | 'config' | 'usageReporting' | 'community'>('token');
const [form, setForm] = useState({ token: '', email: '', password: '', confirm: '' });
const [showPassword, setShowPassword] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
@@ -79,6 +91,8 @@ export const SetupPage: React.FC = () => {
const [errors, setErrors] = useState>({});
const [selectedFeatures, setSelectedFeatures] = useState>(new Set());
const [isSavingFeatures, setIsSavingFeatures] = useState(false);
+ const [usageReportingConsent, setUsageReportingConsent] = useState(false);
+ const [isEnablingUsageReporting, setIsEnablingUsageReporting] = useState(false);
if (statusLoading) {
return ;
@@ -275,6 +289,22 @@ export const SetupPage: React.FC = () => {
setStep('config');
};
+ // Best-effort, same as the feature-flag save above: a collector hiccup on a
+ // fresh install must not trap the admin here. They can always opt in later
+ // from Settings → Product usage, where the full disclosure lives.
+ const enableUsageReporting = async () => {
+ setIsEnablingUsageReporting(true);
+ try {
+ await productUsageService.enable();
+ toast.success(t('setup.usageReporting.enabled'));
+ } catch (_) {
+ toast.warn(t('setup.usageReporting.enableFailed'));
+ } finally {
+ setIsEnablingUsageReporting(false);
+ setStep('community');
+ }
+ };
+
const stepNumber = step === 'token' ? 1 : step === 'account' ? 2 : 3;
return (
@@ -305,9 +335,11 @@ export const SetupPage: React.FC = () => {
? t('setup.restoreStepSubtitle')
: step === 'config'
? t('setup.config.subtitle')
- : step === 'community'
- ? t('setup.community.subtitle')
- : t('setup.usageSubtitle')}
+ : step === 'usageReporting'
+ ? t('setup.usageReporting.subtitle')
+ : step === 'community'
+ ? t('setup.community.subtitle')
+ : t('setup.usageSubtitle')}