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

@ -125,6 +125,10 @@ class Event(BaseModel):
status: str = "approved"
nostr_event_id: str | None = None
nostr_event_created_at: int | None = None
# Set before every publish attempt, cleared on confirmed success.
# True means the relay's copy may be behind this row — see
# migrations_fork.m003 and the sweep in __init__.events_start.
nostr_publish_pending: bool = False
@validator("categories", pre=True)
def parse_categories(cls, v):