Files
picpeak/docs/PRODUCT_USAGE.md
T
Paul Nothaft c043897b0e fix(usage): name the unreadable-key failure, unpin the collector default, align the tab
Review follow-ups on #1304.

SIGNING_KEY_UNREADABLE. USAGE_ENCRYPTION_KEY defaults to JWT_SECRET, so
rotating JWT_SECRET — the correct response to a suspected compromise —
makes the stored Ed25519 key undecryptable. That surfaced as a generic
DELIVERY_FAILED which retried forever, and it silently blocks the DELETE
packet too: an operator who withdraws has their local state cleared
while the collector keeps its copy. decrypt() now tags its own failure
and deliver() reports it under its own name, without flagging an
identity conflict — an unreadable key is not evidence of a clone. The
docs already warned that losing the key breaks deletion signing; they
now name the trigger and the error.

The collector default is no longer an inline string in the constructor.
It is a declared DEFAULT_COLLECTOR_URL, since it is a deployment choice:
self-hosters point USAGE_COLLECTOR_URL at their own collector and the UI
already derives every link from whatever is configured. schema.cjs is
deliberately untouched — it is vendored byte-identical with
picpeak-usage, and its $id is a schema identity, not a delivery address.

Links in the consent dialog. It named the collector inside prose but
never linked it, so an operator deciding whether to opt in could not
open the destination or the public schema without retyping a URL. Both
are links now, built from the configured collector.

UI standards. The tab hand-rolled its surfaces as
`<section className="rounded-xl border border-theme …">` and imported
Button from a deep path; every other settings tab uses `<Card
padding="md">` from the components/common barrel. Converted, with the
feedback <form> wrapped rather than replaced so its semantics survive,
and headings given the same colour tokens as ImageSecurityTab. The
barrel pulls ErrorBoundary -> i18n/config, so the tab's test needed the
initReactI18next shim the FaceRecognitionCard test already uses.

Not changed: the delete packet reusing the current sequence. The
collector handles delete before any sequence check — "possession proof
is sufficient for deletion, including when a restored backup has a
stale sequence" (picpeak-usage server/collector.js) — so deletion is
deliberately sequence-exempt and the client is correct as written.

Refs #1110
2026-09-05 21:23:22 +02:00

4.8 KiB

Optional product usage and feedback (#1110)

Tracking is disabled by default. After updating, settings editors see a dismissible invitation in the admin shell. Only explicit consent in Settings → Product usage & feedback registers an installation. Public galleries never load the usage UI chunk or trigger product reports.

The backend builds and signs allowlisted feature packets, sending them at most daily on authenticated admin use. It does not send gallery visitors, click histories, photo/gallery counts, names, emails, domains, filenames, or secrets. The settings page provides disclosure, raw preview/export, a private lookup hash, delivery status, feedback, and a short-lived voting connection.

Deployment

Run the normal core migrations; migration 201 creates dedicated state/marker tables. No identity or key is created by the migration. No extra browser tracker or CORS policy is required. The collector is the separate picpeak-usage app from #1110.

Backend variable Default Meaning
USAGE_COLLECTOR_URL https://usage.picpeak.app Fixed operator-configured collector origin, HTTPS in production
USAGE_ENCRYPTION_KEY JWT_SECRET 32+ characters, encrypts the local Ed25519 key with AES-256-GCM

Local development can use an HTTP loopback collector outside production. The collector URL is never writable through generic settings or request payloads. Keep the encryption material stable and protected; losing it makes the old identity unable to sign deletion requests. Note that USAGE_ENCRYPTION_KEY defaults to JWT_SECRET, so rotating JWT_SECRET without setting a dedicated USAGE_ENCRYPTION_KEY first loses it. The settings page then reports SIGNING_KEY_UNREADABLE rather than a generic delivery failure, because the consequence is specific: reports stop and the deletion request can no longer be signed either. Keys live in a dedicated database table, not the generic readable settings. A random mode-0600 file at getStoragePath()/usage-instance.key binds the database to its local storage.

Disabled → activation pending → active. Registration/delivery failures are durable and retried. Multiple admin tabs/processes share a database lease; only accepted receipts advance the sequence and report date. Re-signed retries reuse the immutable packet ID so lost acknowledgements do not duplicate data.

Opt-out immediately stops collection, clears markers/previews/feedback preferences, and enters deletion pending. It keeps only credentials and the deletion operation until the collector confirms deletion. The collector removes reports, projections, feedback/publications, votes, and sessions. PicPeak then erases the local fingerprint, private key and binding. A later join generates a fresh identity. Repeated deletion handles lost receipts safely.

A missing/mismatched storage binding or conflicting collector sequence stops reporting with identity conflict. A full clone of a signing identity cannot be distinguished cryptographically. Do not run the same participation identity in two deployments; disable/delete the old participation and rejoin. Deletion affects any other copy that shared the same identity.

Feedback and permissions

Only settings.edit can inspect identity/packets or change participation and feedback preferences. Any authenticated admin may trigger the fixed daily report; the activity endpoint accepts no telemetry input. Every usage endpoint uses adminAuth, including token-type checks. Gallery tokens cannot use it.

Feedback is sent only on explicit submission. Each item defaults anonymous and private; names, publication permission, and testimonial marketing permission are separate choices. Published requests/testimonials require maintainer review. Public voting uses a backend-authorized 15-minute session, never the lookup hash.

Contract

The closed schema is in backend/src/usage/schema.cjs, with signing in protocol.cjs. Keep both byte-identical to the collector's protocol/ copies. The collector serves the schema, complete source archive, public projections, and full raw exports. Feature semantics and retention are documented in its docs/PROTOCOL.md and docs/OPERATIONS.md.

Used flags represent successful allowlisted admin capability calls since joining, not visitor behavior or counts. OAuth marks successful admin SSO; applied CSS is observed during report generation. Gallery layouts are controlled enums extracted from event themes without IDs or counts. Other signals use the explicit rules in middleware/productUsage.js and usage/UsageService.js.

Tests: backend/__tests__/routes/adminUsage.test.js, frontend features/settings/__tests__/ProductUsageTab.test.tsx, and the collector's cross-repository integration suite with isolated databases and real HTTP.