fix(usage): close the QA findings on opt-in product usage
A QA exploration of this branch against an isolated rig — own stub collector, SQLite and PostgreSQL — turned up one dead end and a set of signals and controls that did not hold up. This closes all of them. Rotating JWT_SECRET, the documented response to a suspected compromise, made the signing key unreadable. That was already named and documented, but it left no way out: the delete packet can never be signed, so the row stays deletion_pending forever, and enable() refuses because it is not `disabled`. An operator who rotated precisely because the secret was compromised cannot restore it, so the feature was bricked with no control left. POST /usage/abandon is offered only in that state; it drops the local identity and records the receipt as `collector-unconfirmed` rather than claiming a deletion that did not happen. Every failed delivery was retried on the next admin request, and /activity is open to any authenticated admin while the settings ticker fires it every five minutes per open tab — 30 activity calls against a rejecting collector produced 30 outbound requests. Migration 206 adds attempts/next_attempt_at and the unattended sender honours the gate; Retry and opt-out still send immediately, and the tab names the time of the next automatic attempt. Feedback, votes and portal sessions now share an installation-wide budget of 30/hour. They are the only endpoints whose effect is outbound traffic carrying operator-written free text, and the general limiter skips authenticated requests by design. Reading status and withdrawing stay unthrottled. gallery_image_protection was true on a bare install with no galleries: PicPeak ships default_protection_level='standard' and enable_devtools_protection=true, so it reported fleet-wide 100% and could never separate a decision from an untouched default. It now reads only what deviates from the shipped defaults, and the devtools flag is not read at all — being on by default, its only informative state is off, which is the opposite of what the key claims. Also: - the export receipt counted every packet and called the total "usage reports"; reports and participant operations are now counted and named separately - GET /usage/preview no longer persists the custom_css marker, so the transparency view stops changing what will be sent - the feedback route requires every field the packet schema requires, so an API caller gets the missing field named instead of a bare INVALID_PACKET from inside signing - the German strings for this feature use "Sie" throughout, matching the rest of the admin UI; the ignore hint says what ignoring will do rather than stating it as already true - the consent dialog returns focus to the control that opened it - the long buttons wrap instead of running off a 390px viewport - a deletion receipt is labelled as belonging to an earlier participation while a new one is active Regression tests cover each of these, including the delete packet's reuse of the last accepted sequence, which was an unwritten assumption about the collector rather than a defect.
This commit is contained in:
@@ -133,7 +133,7 @@ Legacy v1 semantics remain documented separately in the protocol reference.
|
||||
| `download_resolution_picker` — Download resolution picker enabled / Download-Auflösungswahl aktiviert | usage.v2 | Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts. | **Not collected. Configuration only.** |
|
||||
| `gallery_client_access` — Client access enabled / Client-Zugang aktiviert | usage.v2 | Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts. | **Not collected. Configuration only.** |
|
||||
| `gallery_watermarks` — Watermarks enabled / Wasserzeichen aktiviert | usage.v2 | Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts. | **Not collected. Configuration only.** |
|
||||
| `gallery_image_protection` — Image protection enabled / Bildschutz aktiviert | usage.v2 | Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts. | **Not collected. Configuration only.** |
|
||||
| `gallery_image_protection` — Image protection enabled / Bildschutz aktiviert | usage.v2 | Enabled beyond the shipped defaults — a stronger protection level, canvas rendering, or right-click disabled — globally or on at least one gallery; only existence across the installation, never gallery IDs or counts. | **Not collected. Configuration only.** |
|
||||
| `gallery_reveal` — Gallery reveal enabled / Galerie-Enthüllung aktiviert | usage.v2 | Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts. | **Not collected. Configuration only.** |
|
||||
| `gallery_expiration` — Gallery expiration configured / Galerieablauf konfiguriert | usage.v2 | At least one gallery has an expiry configured; no dates, gallery IDs or counts. | **Not collected. Configuration only.** |
|
||||
|
||||
|
||||
+33
-4
@@ -45,7 +45,15 @@ 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
|
||||
be signed either. Restoring the original key material is the correct fix and
|
||||
completes the pending deletion. When it is genuinely gone — a rotation done
|
||||
because the secret was compromised — the settings page offers **Discard local
|
||||
identity** (`POST /api/admin/usage/abandon`), which is available in no other
|
||||
state. It erases the local identity, key material and markers and records an
|
||||
abandonment receipt marked `collector-unconfirmed`: the collector was never
|
||||
told, so it keeps the reports already accepted, and the receipt says so rather
|
||||
than claiming a deletion that did not happen. Participation can be started
|
||||
again afterwards with a fresh identity. 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.
|
||||
|
||||
@@ -75,6 +83,16 @@ 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.
|
||||
|
||||
Retries are paced (migration 206). Consecutive failures set `attempts` and
|
||||
`next_attempt_at`, and the unattended sender — the activity endpoint and the
|
||||
settings ticker — waits for that gate: 2, 4, 8, 16, 32 minutes, then hourly.
|
||||
Without it a packet the collector rejects permanently produced one collector
|
||||
request per admin action, because any authenticated admin reaches the activity
|
||||
endpoint and every open admin tab fires it every five minutes. Explicit
|
||||
operator actions are not paced: **Retry** and opt-out send immediately, and the
|
||||
settings page names the time of the next automatic attempt so a waiting
|
||||
installation does not read as a broken one.
|
||||
|
||||
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
|
||||
@@ -84,9 +102,12 @@ a fresh identity. Repeated deletion handles lost receipts safely.
|
||||
|
||||
Migration 204 adds bounded, local-only privacy receipts and removes any legacy
|
||||
plaintext voting token from the last collector receipt. A completed export
|
||||
records its time and report count; confirmed opt-out replaces this with a
|
||||
deletion receipt containing only a random receipt ID, time, status and fixed
|
||||
scope. It retains no old installation hash, key, payload or credential. The
|
||||
records its time, the number of accepted reports and the total number of
|
||||
accepted packets separately — feedback, votes and portal sessions are
|
||||
participant operations, not reports, and a receipt that folded them into one
|
||||
"reports" figure stated something untrue about its own contents. Confirmed
|
||||
opt-out replaces this with a deletion receipt containing only a random receipt
|
||||
ID, time, status and fixed scope. It retains no old installation hash, key, payload or credential. The
|
||||
settings page can download these receipts even after opt-out. They are local
|
||||
records of the collector acknowledgement, not independent proof of storage
|
||||
erasure. Downloaded exports carry their own dated receipt; the collector does
|
||||
@@ -105,6 +126,14 @@ 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, votes and portal sessions share one installation-wide budget of 30
|
||||
per hour. They are the only endpoints whose effect is an outbound request
|
||||
carrying operator-written free text, and the platform's general limiter skips
|
||||
authenticated requests by design — correct for endpoints that touch only this
|
||||
installation, wrong for a relay. Reading status, retrying and opting out are
|
||||
never throttled: those are how an operator sees what is happening and how they
|
||||
leave.
|
||||
|
||||
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.
|
||||
|
||||
@@ -1218,7 +1218,7 @@
|
||||
"adminUsage.js": {
|
||||
"decision": "excluded",
|
||||
"signals": [],
|
||||
"reason": "Consent, inspection, export, feedback, voting and deletion are explicit protocol operations; not product-use signals. Activity only triggers a due fixed report.",
|
||||
"reason": "Consent, inspection, export, feedback, voting, deletion and abandoning an unsignable deletion are explicit protocol operations; not product-use signals. Activity only triggers a due fixed report.",
|
||||
"route_signatures": [
|
||||
"POST /activity",
|
||||
"GET /",
|
||||
@@ -1226,6 +1226,7 @@
|
||||
"POST /enable",
|
||||
"POST /consent",
|
||||
"POST /disable",
|
||||
"POST /abandon",
|
||||
"POST /retry",
|
||||
"GET /preview",
|
||||
"GET /export",
|
||||
|
||||
Reference in New Issue
Block a user