fix(usage): let an operator clear a participation the collector never accepted

Probing the live collector to settle the delete-sequence question turned up
something else: usage.picpeak.app answers a valid usage.v2 registration with
INVALID_PACKET while the identical v1 flow is accepted. It does not speak v2
yet — which the deployment notes already require, but the consequence of
getting that order wrong was worse than "reports do not send".

Opting in to v2 against a v1-only collector left the installation stuck.
Registration was refused, so nothing existed at the collector at all; the row
sat in activation_pending, disable moved it to deletion_pending, retry was
futile forever, and enable refused because the row was not `disabled`. The
abandon hatch added earlier did not apply: it was gated on
SIGNING_KEY_UNREADABLE. So the most harmless possible failure — nothing
registered anywhere — was the one an operator could not clear.

The gate is now the property that actually matters: a participation the
collector has provably never accepted (sequence 0, no receipt) with a failing
delivery can be discarded, from activation_pending as well as
deletion_pending. Its receipt records `never-registered` rather than an
unconfirmed deletion, because nothing remote exists to be unsure about. A
participation the collector *did* accept keeps the old narrow gate and its
explicit warning — clearing local state while the collector still holds
reports must stay a deliberate, warned-about act.

A collector that rejects a registration or a deletion outright now reports
SCHEMA_NOT_ACCEPTED instead of DELIVERY_FAILED, and the settings page says the
collector does not accept this report version yet. Retrying cannot fix that,
and sending the operator to look for a network fault they do not have was
wrong.

Verified end to end against the live collector: v2 opt-in reports
SCHEMA_NOT_ACCEPTED, the exit is offered immediately, the receipt says
never-registered, and joining again on v1 registers, reports and withdraws
with a collector-confirmed deletion.
This commit is contained in:
Paul Nothaft
2026-09-06 18:54:58 +02:00
parent c741dc22c5
commit e40bc474bc
7 changed files with 186 additions and 22 deletions
+14 -1
View File
@@ -77,7 +77,20 @@ 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
again afterwards with a fresh identity.
The same exit covers the other way a participation can become impossible to
finish: a collector that rejects the packet outright. Opting in to usage.v2
against a collector that still only speaks usage.v1 — the deployment order
this document warns about above — is answered with `INVALID_PACKET`, which is
surfaced as `SCHEMA_NOT_ACCEPTED` rather than a generic delivery failure,
because retrying cannot resolve it. Nothing is registered in that case, so
**Discard local identity** is offered immediately and its receipt records
`never-registered` rather than an unconfirmed deletion. The exit is never
offered while a participation the collector *did* accept could still be
deleted remotely; that case keeps the explicit warning.
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.