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:
@@ -278,7 +278,9 @@ export default function ProductUsageTab() {
|
||||
{t(
|
||||
data.last_error === 'SIGNING_KEY_UNREADABLE'
|
||||
? 'productUsage.signingKeyUnreadable'
|
||||
: 'productUsage.deliveryProblem'
|
||||
: data.last_error === 'SCHEMA_NOT_ACCEPTED'
|
||||
? 'productUsage.schemaNotAccepted'
|
||||
: 'productUsage.deliveryProblem'
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
@@ -296,7 +298,13 @@ export default function ProductUsageTab() {
|
||||
// The one dead end the operator cannot retry out of. Offered only
|
||||
// here, and worded so nobody mistakes it for a confirmed deletion.
|
||||
<div className="rounded border border-amber-300 dark:border-amber-700 p-3 space-y-2">
|
||||
<p>{t('productUsage.abandonExplanation')}</p>
|
||||
<p>
|
||||
{t(
|
||||
data.abandon_never_registered
|
||||
? 'productUsage.abandonExplanationUnregistered'
|
||||
: 'productUsage.abandonExplanation'
|
||||
)}
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={WRAPPING_BUTTON}
|
||||
@@ -305,7 +313,11 @@ export default function ProductUsageTab() {
|
||||
if (
|
||||
await confirm({
|
||||
title: t('productUsage.abandon'),
|
||||
message: t('productUsage.abandonConfirm'),
|
||||
message: t(
|
||||
data.abandon_never_registered
|
||||
? 'productUsage.abandonConfirmUnregistered'
|
||||
: 'productUsage.abandonConfirm'
|
||||
),
|
||||
confirmLabel: t('productUsage.abandon'),
|
||||
variant: 'danger'
|
||||
})
|
||||
|
||||
@@ -402,6 +402,7 @@
|
||||
"deliveryProblem": "Die Übertragung benötigt Aufmerksamkeit. Bei Löschung oder Identitätskonflikt ist die Erfassung gestoppt. Versuchen Sie es erneut oder deaktivieren Sie die Teilnahme, um die Daten zu löschen.",
|
||||
"invalidCollectorUrl": "Die konfigurierte Collector-URL ist ungültig, daher kann die Teilnahme weder gestartet noch übermittelt werden. Setzen Sie USAGE_COLLECTOR_URL auf einen https-Origin ohne Pfad, Query oder Zugangsdaten (oder lassen Sie sie leer, um den Standard zu verwenden).",
|
||||
"signingKeyUnreadable": "Der Signaturschlüssel für die Nutzungsdaten kann nicht gelesen werden. Meist wurde USAGE_ENCRYPTION_KEY — oder das als Rückfallwert genutzte JWT_SECRET — geändert. Berichte können nicht gesendet und auch die Löschanfrage kann nicht signiert werden. Stellen Sie das ursprüngliche Schlüsselmaterial wieder her, um die Löschung abzuschließen; erneutes Senden oder Deaktivieren allein behebt dies nicht.",
|
||||
"schemaNotAccepted": "Der Collector hat das Paket rundheraus abgelehnt — er nimmt diese Berichtsversion also noch nicht an, meist weil er nicht aktualisiert wurde. Erneutes Senden ändert daran nichts. Es wurde nichts registriert; Sie können die Teilnahme verwerfen und erneut beitreten, sobald der Collector sie unterstützt.",
|
||||
"inspect": "Genau sehen, was geteilt wird",
|
||||
"preview": "Nächsten Bericht ansehen",
|
||||
"lastPacket": "Zuletzt angenommener signierter Nutzungsbericht",
|
||||
@@ -443,7 +444,9 @@
|
||||
"retryScheduled": "Der nächste automatische Versuch erfolgt um {{time}}. „Erneut versuchen“ sendet sofort.",
|
||||
"abandon": "Lokale Identität verwerfen",
|
||||
"abandonExplanation": "Die Löschanfrage kann ohne das ursprüngliche Schlüsselmaterial nicht signiert werden. Wenn Sie es nicht wiederherstellen können, lässt sich die lokale Identität verwerfen: Erfassung und Schlüssel werden hier entfernt, der Collector bestätigt die Löschung dabei aber nicht.",
|
||||
"abandonExplanationUnregistered": "Diese Teilnahme wurde vom Collector nie angenommen, dort ist also nichts gespeichert und es gibt nichts zu löschen. Sie können sie hier verwerfen und jederzeit neu beginnen.",
|
||||
"abandonConfirm": "Installationsidentität, Schlüsselmaterial und alle lokalen Marker werden gelöscht. Der Collector wird nicht benachrichtigt und behält die bisher gesendeten Berichte — die Quittung hält das als unbestätigt fest. Danach ist eine neue Teilnahme wieder möglich.",
|
||||
"abandonConfirmUnregistered": "Installationsidentität, Schlüsselmaterial und alle lokalen Marker werden gelöscht. Der Collector hat diese Teilnahme nie angenommen, es wird also nirgendwo sonst etwas entfernt. Danach ist eine neue Teilnahme wieder möglich.",
|
||||
"auditPreviousParticipation": "Löschbestätigungen beziehen sich auf eine frühere Teilnahme, nicht auf die aktuelle."
|
||||
},
|
||||
"userManagement": {
|
||||
|
||||
@@ -402,6 +402,7 @@
|
||||
"deliveryProblem": "Delivery needs attention. Collection stops during deletion or an identity conflict. Use retry, or disable participation to delete its data.",
|
||||
"invalidCollectorUrl": "The configured usage collector URL is not valid, so participation cannot be started or delivered. Set USAGE_COLLECTOR_URL to an https origin with no path, query or credentials (or leave it unset to use the default).",
|
||||
"signingKeyUnreadable": "The usage signing key cannot be read, which usually means USAGE_ENCRYPTION_KEY — or JWT_SECRET, which it falls back to — was changed. Reports cannot be sent and the deletion request cannot be signed either. Restore the original encryption material to finish deletion; retrying or disabling will not resolve it on its own.",
|
||||
"schemaNotAccepted": "The collector rejected the packet outright, which means it does not accept this report version yet — usually a collector that has not been upgraded. Retrying will not change that. Nothing has been registered, so you can discard the participation and join again once the collector supports it.",
|
||||
"inspect": "See exactly what is shared",
|
||||
"preview": "Preview next report",
|
||||
"lastPacket": "Last accepted signed usage report",
|
||||
@@ -443,7 +444,9 @@
|
||||
"retryScheduled": "The next automatic attempt is at {{time}}. \"Retry\" sends immediately.",
|
||||
"abandon": "Discard local identity",
|
||||
"abandonExplanation": "The deletion request cannot be signed without the original encryption material. If you cannot restore it, you can discard the local identity: collection and keys are removed here, but the collector does not confirm the deletion.",
|
||||
"abandonExplanationUnregistered": "This participation was never accepted by the collector, so nothing is stored there and there is nothing to delete. You can discard it here and start again at any time.",
|
||||
"abandonConfirm": "This deletes the installation identity, the key material and every local marker. The collector is not notified and keeps the reports already sent — the receipt records that as unconfirmed. You can join again afterwards.",
|
||||
"abandonConfirmUnregistered": "This deletes the local installation identity, the key material and every local marker. The collector never accepted this participation, so nothing is removed anywhere else. You can join again afterwards.",
|
||||
"auditPreviousParticipation": "Deletion confirmations refer to an earlier participation, not the current one."
|
||||
},
|
||||
"userManagement": {
|
||||
|
||||
@@ -19,8 +19,10 @@ export interface UsageStatus {
|
||||
last_error: string | null;
|
||||
/** Epoch ms the paced sender is waiting for, or null when nothing is paced. */
|
||||
retry_after?: number | null;
|
||||
/** True only for a withdrawal whose delete packet can never be signed. */
|
||||
/** True when the participation cannot be completed and the only exit is to discard it. */
|
||||
can_abandon?: boolean;
|
||||
/** True when the collector never accepted anything, so discarding deletes nothing remote. */
|
||||
abandon_never_registered?: boolean;
|
||||
pending_action: string | null;
|
||||
last_packet: unknown;
|
||||
privacy_receipts?: Record<string, unknown>;
|
||||
|
||||
Reference in New Issue
Block a user