Closes the last T2 plan item. Off by default; toggling on requires
the operator to confirm a collector URL (an empty URL is "armed but
silent"). The toggle lives on the admin Overview page next to the
public-key card — the right place for a privacy-affecting choice
since it's where operators actually live.
What's sent (per the in-card "Show me exactly what gets sent"
disclosure, and pinned by the test):
- install_uuid: random UUIDv4 generated on first opt-in. NOT
derived from operator_name, store id, public URL, or any
other identifier. Wipeable via the Reset button.
- daemon_version (CARGO_PKG_VERSION).
- tier (creator/pro/patron/unlicensed) — the same string the
admin tier endpoint already exposes.
- counts: products, active_licenses, settled_invoices — each
floored to the nearest 5 (anti-fingerprinting; an exact license
count uniquely identifies an operator over time).
- uptime_bucket: <1d / 1-7d / 1-4w / >4w (bucketed, not exact).
What's NOT sent (test asserts none of these strings appear in the
preview heartbeat): operator_name, public_url, store_id, api_key,
buyer_email, btcpay_url. Also no product/policy slugs or names, no
license/invoice ids, no fingerprints, no webhook secrets.
Backend:
- src/analytics.rs — heartbeat builder, opt-in check, daily
background tick (5min initial grace period after boot).
- src/api/community.rs — GET / POST / reset admin endpoints.
- main.rs spawns the background tick unconditionally; the tick
is a no-op if disabled OR no collector URL configured.
Frontend (web/index.html, Overview page):
- Toggle + collector URL input + privacy disclosure showing the
EXACT JSON shape that would be sent (renders the live preview
heartbeat from /v1/admin/community-analytics).
- "Reset install_uuid" button so an operator who's been beaconing
under one identifier can start fresh.
Also includes the configureBtcpay.ts idempotency change from
v0.1.0:46 (already committed; touched again here only because the
diff includes the .ts file in the same dirty-tree push).
Test count: 32 (was 31; +1 community_analytics_opt_in_and_privacy_contract
which seeds 23 licenses and verifies the heartbeat reports 20 —
proves the floor-to-5 anti-fingerprinting is in effect).
The /v1/admin/webhook-deliveries endpoints from v0.1.0:43 were
operator-actionable via curl but invisible in the dashboard. Adds a
"Delivery history" section to the Webhooks page showing recent
deliveries with a status filter (defaults to "Failed (DLQ)" so the
problem case is what an operator sees first).
Each row shows created-at, event type, status badge (delivered /
failed / pending), attempt count, last status code, and last_error
inline beneath the status when present (so operators don't have to
chase a separate "details" view to know why a delivery failed).
Non-delivered rows get a Retry button that re-queues via the
existing POST /v1/admin/webhook-deliveries/:id/retry; the worker
picks up the retried row on its next 5s tick.
No backend changes. The endpoints landed in :43; this commit is
just the front-end surface.