feat(nostr): flag events whose NIP-52 publish didn't land

Inventory reaches clients only through the republished calendar event,
and until now a publish that failed or was skipped left no durable
trace — only a log line, if that. Twice the drift was caught by a human
reading a wrong number on a public page (#35 on aio-demo, #51 on cfaun,
where an event's relay copy sat 14 days behind the DB).

Adds `events.nostr_publish_pending`, set before every attempt and
cleared only on a confirmed success. Ordering it that way is what makes
"the attempt was never made" — no signer resolved, no NostrClient, the
process died mid-flight — as discoverable as "the attempt raised". Both
shapes have now been observed in production; only the second one was
ever visible.

`set_ticket_paid` raises the flag inside its own update so the counters
and "the relay doesn't know about them yet" commit atomically, and the
sale path pays no extra write.

`publish_or_delete_nostr_event` now returns a bool so callers can
branch. The flag, not the return value, is the durable record — the
existing call sites stay correct ignoring it.

Publish failures move from WARNING to ERROR: the published ticket count
has stopped tracking reality, which is not routine journal noise.

Refs #35
This commit is contained in:
Padreug 2026-09-26 23:45:47 +02:00
commit 5d52a231d3
6 changed files with 240 additions and 6 deletions

View file

@ -127,3 +127,32 @@ async def m002_ticket_payment_hash(db):
"UPDATE events.ticket SET payment_hash = id "
"WHERE payment_hash IS NULL OR payment_hash = ''"
)
async def m003_event_nostr_publish_pending(db):
"""
Add `events.nostr_publish_pending` — the marker that makes NIP-52
publish drift queryable instead of invisible.
Inventory reaches clients only through the republished calendar
event. When that publish doesn't land, the relay keeps serving the
counts it last saw and nothing anywhere records the divergence; it
has twice been caught only by a human reading a public page
(aiolabs/events#35, #51).
The flag is set before each publish attempt and cleared only on a
confirmed success, so it covers *both* observed failure shapes:
an attempt that raised (a signer outage) and an attempt that was
never made at all (no signer resolved, no NostrClient). A periodic
sweep republishes whatever is still marked.
Existing rows default to FALSE rather than TRUE: on upgrade we have
no evidence they're stale, and marking the whole table pending would
stampede the signer with a full-table republish on first boot.
`/republish-all` is the deliberate way to force that.
"""
await _alter_add_column_safe(
db,
"ALTER TABLE events.events "
"ADD COLUMN nostr_publish_pending BOOLEAN NOT NULL DEFAULT FALSE",
)