Two wave fixes found while checking whether the webapp had been updated
for v1.6.8.
- #65 keep ticket waves when a client edits an event without them. A
client that rebuilt the `extra` envelope rather than round-tripping it
destroyed every wave: the list arrived empty, a single primary wave was
synthesized from the event-level `amount_tickets`, and a multi-wave
event silently collapsed into one tier. `promo_codes` had carried the
same guard since the v1.6.8 merge; `ticket_waves` is the same class of
organiser state and now carries it too.
- #66 expose `ticket_waves` on public event responses. A buyer cannot
choose a tier without its id, and nothing public carried one — the
NIP-52 tags describe the active wave but name no id. Waves move to
`EventExtraBase`, leaving promo codes as the only organiser-private
field in `extra`. Buyers can now see upcoming tiers and their prices,
which is what #61 recorded as the cost of the flat-tag decision.
No schema change; both are model/serialisation only.
Verified against bohm's dev LNbits before tagging: the exact PUT that
collapsed a two-wave event now leaves both intact with the roll-up
unchanged, the public endpoint carries the waves while still hiding
promo codes, and a webapp-shaped edit that writes through to the primary
wave takes effect (139 = 99 + 40) where it was previously discarded.
#66 is a prerequisite for aiolabs/webapp#176, which is merged to dev and
needs this deployed before its wave picker works for anyone but the
organiser.
Rebase onto upstream v1.6.8, so the upstream segment moves and the aio
patch counter resets to 1.
- #63 merge upstream v1.6.8: ticket waves (per-wave price/currency/
stock/fiat), paginated tickets, the organiser ticket-image template.
Pricing and inventory now follow the wave the buyer selected rather
than the primary-wave roll-up; npub checkout and DM delivery kept
where upstream removed them; `_parse_date` accepts the ISO datetimes
our closing dates carry.
- #63 drop the SatsPay/watchonly on-chain surface — on-chain will go
through native LndRest once aiolabs/lnbits#53 lands (#41).
- #63 publish the active wave over Nostr, with `nostr_published_wave_id`
(fork migration m004) so the reconciliation sweep republishes at wave
boundaries (#61).
- #64 require a capacity on every ticket wave (#34, #62).
`migrations.py` stays byte-identical to upstream. v1.6.8 adds no
upstream migrations over v1.6.1 — waves live in `extra` JSON — so no
`dbversions` surgery is needed on existing installs; only the fork
namespace advances, `events_fork` 3 -> 4.
- #62 `tickets_available` is always published, including zero. Omitting
it used to mean "unlimited", which nothing else agreed with:
api_get_event and api_ticket_create both read `amount_tickets < 1` as
sold out, so a zero-capacity event advertised unlimited tickets on the
card and returned 410 to every purchase. Three such events were live
on aio-demo. The admin form no longer offers 0 either.
No schema change. Existing zero-capacity events are not migrated — we
cannot infer whether the organiser meant unlimited or forgot to set a
number — and they will read as sold out once republished. The #55 sweep
will not flag them on its own, so /republish-all is the way to refresh.
Ticket availability and publish delivery.
- #58 publishes are confirmed against the relay's NIP-01 `OK` instead of
the send queue. A publish that never reaches a relay now leaves the row
flagged for the sweep rather than reporting success and clearing it —
which had silently reverted a completed repair on cfaun. Verified end
to end on aio-demo: delivery confirmed, the no-relay failure caught
with the relay's own diagnostic in the log, and the retry recovering
once the relay returned.
- #59 `amount_tickets` is the remaining count; `api_ticket_create` no
longer subtracts `sold` from it. Every event was locking itself as sold
out at half capacity. 16 of 24 live events on aio-demo were affected,
3 already refusing sales with stock remaining.
No schema change. Affected events start selling again on upgrade; worth
re-running an availability check per host afterwards.
Nostr publish reliability.
- #54 the two paths that skip a NIP-52 publish now log at WARNING
instead of silently (bare return / debug); publish failures moved to
ERROR
- #55 `events.nostr_publish_pending` marks a row from before each
publish attempt until a confirmed success, and a 5-minute sweep
republishes whatever is still flagged, so drift recovers on its own
instead of waiting for an operator who knows to run /republish-all
- #55 the NostrClient send loop holds and retries a dequeued req
(bounded at 3) rather than dropping it
Adds migration m003 (events.nostr_publish_pending). Verified on bohm:
events_fork 1 -> 3, column present, event created and published to a
relay with the flag clearing on success.
Since v1.6.1-aio.14: promo codes enforced — active flag, max_uses with
derived used_count, codes hidden from public event records, anonymous
POST /api/v1/promo/validate/{event_id} preview, single basket_totals
pricing path, Stripe metadata.promo_code (#45).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo
The LNbits admin form lagged the webapp's CreateEventDialog:
- Payment methods never rendered. c2d9a96 wired the template to
`paymentMethodOptions` / `acceptsFiat` but never defined them, so the
q-option-group got `options=undefined` and the fiat-currency select
was gated on `undefined`. Rails are now two q-checkboxes; Card is
disabled with an explanatory tooltip when `g.user.fiat_providers` is
empty (same rule as the webapp) and names the providers otherwise.
- Location (NIP-52 `location` tag) and Categories (NIP-52 `t` tags,
same 25-item list as the webapp's category.ts) were missing from the
form even though the model, CRUD and publisher already carry them.
- Datetimes are stamped with the browser's UTC offset on submit, as the
webapp does; `_to_unix` treats naive values as UTC, so 18:00 CEST
entered here went out on Nostr as 18:00 UTC. Table columns render
"YYYY-MM-DD HH:MM" instead of the raw ISO string.
- Validation: title + start date required, end >= start on the folded
date+time, fiat currency required when a sat-priced event accepts
card. Create is enabled once wallet + title + start are set; info,
closing date, tickets and price were all effectively required before
because the disable check compared undefined fields to null.
- Labels follow the payment-rails vocabulary: "Unit" -> "Price
currency", "Fiat checkout currency" -> "Fiat currency"; ticket
closing date and end date explain their defaults.
- A fiat-priced event mirrors `fiat_currency = currency` on save so the
payload and the `tickets_fiat_currency` tag stay coherent.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018b1bDExMX7W3a47wcgFUjb
The v1.6.1-aio.9 mail still scored 8.4/10 on mail-tester: the remaining
deduction was HTML_IMAGE_ONLY (1.8) — an HTML part whose only content of
note is a remote <img>. Remote images are also blocked by default in most
clients until the reader opts in, and a bare QR saved from that mail says
nothing about what it opens.
- New `qr.py` module (QR + logo helpers moved out of views_api) with
`render_ticket_card`: site title, event name, when/where, the branded
QR, name on ticket, ticket id and the door instruction, laid out with
the bundled DejaVu Sans; `format_event_when` gives "Fri 19 Feb 2027,
16:00 - 20:00"; filenames are `ticket-<event-slug>-<id8>.png`.
- `GET /events/api/v1/ticket-card/{ticket_id}` serves the same PNG
(anonymous, like the QR endpoint); the email's "Ticket image" link
now points there.
- The ticket email becomes multipart/mixed: text + HTML alternatives
(URLs as links, no <img>) plus the card as a PNG attachment, which
clients show inline at the end of the message and which works offline
at the door.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo
A tester's ticket email landed in spam. A mail-tester run against demo
scored 8.3/10 with SPF, DKIM and DMARC all passing through the VPS relay,
so the deductions were all in the message: MISSING_DATE (1.4),
HTML_IMAGE_ONLY_04 (0.3), MISSING_MID (0.1) — and Gmail/Outlook weigh a
missing Date/Message-ID as "machine-generated" far more than that.
- `build_ticket_email` sets Date, a Message-ID under the sender domain,
and a From display name from `lnbits_site_title`.
- The body now carries the event name, dates, location, name on ticket,
ticket id and the door instruction, so the HTML part is no longer a
QR with a handful of words.
Upstream candidate: lnbits core `send_email` has the same omissions.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo
Marks the monotonic created_at fix (#26). aio semver stays ahead of the
upstream 1.6.1 tag per fork versioning rules.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closesaiolabs/events#23. Pre-cascade prerequisite for aiolabs/lnbits#17
(signer abstraction phase 1), which lands an m002 startup job that
NULLs the legacy `accounts.prvkey` column. After this migration, the
events extension reads no plaintext nsec and works with any
NostrSigner backend (LocalSigner / RemoteBunkerSigner / ClientSideOnlySigner).
## What changed
### nostr_hooks.py — publish_or_delete_nostr_event
Was: pulled `(account.pubkey, account.prvkey)` from the wallet owner,
passed both to `publish_event_to_nostr`. Hard-skipped publish when
`account.prvkey` was None.
Now: calls `await resolve_for_wallet(event.wallet)` (the DRY helper
from aiolabs/lnbits#23 — wallet → account → signer → can_sign-check
in one call, returns None on any soft-fail). Passes the resolved
`NostrSigner` to the publisher. Soft-skip on None (wallet missing,
account unclassified, or ClientSideOnlySigner where the server has
no signing authority) — matching previous "no prvkey" behavior.
### nostr_publisher.py — publish_event_to_nostr
Was: accepted `(account_pubkey, account_prvkey)` and signed via a
local `sign_nostr_event` helper that called `coincurve.PrivateKey
.sign_schnorr` directly on the plaintext nsec.
Now: accepts `signer: NostrSigner`. Builds the unsigned event dict
(`kind`/`created_at`/`tags`/`content`), hands it to
`await signer.sign_event(...)`, reconstructs the local `NostrEvent`
model from the signed dict (`id`/`pubkey`/`sig` fields). The signer
backend (LocalSigner / RemoteBunkerSigner) is transparent.
Removed the `sign_nostr_event` helper entirely — the signer abstraction
handles all signing now.
Dropped the `coincurve` import; no direct crypto in this extension.
## Acceptance
- [x] keypair helper replaced (nostr_hooks no longer touches account.prvkey)
- [x] publish_event_to_nostr accepts NostrSigner instead of (pubkey, prvkey)
- [x] extension-local Schnorr code removed (sign_nostr_event gone)
- [x] re-grep `events/`: zero `account.prvkey` references
- [x] version bumped: 1.6.1-aio.3 → 1.6.1-aio.4
Manual smoke testing + tag + catalog entry follow the migration
landing; will run against the regtest stack with lnbits on
`issue-18-phase-2.3` (which validates both LocalSigner and
RemoteBunkerSigner signing paths end-to-end).
## Cross-references
- aiolabs/events#23 — issue this commit closes
- aiolabs/lnbits#17 — the cascading signer-abstraction PR
- aiolabs/lnbits#23 — the resolve_for_wallet helper this uses
- aiolabs/lnbits#26 — phase 2.3 (sign_event over bunker, validated against
aiolabs/nsecbunkerd@fb1c239)
- aiolabs/lnbits#21 — umbrella audit identifying 5 affected extensions
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rebases the aio fork onto upstream v1.6.1 (4bf867e), pulling in:
- fiat checkout + email/Nostr DM ticket notifications (PR #50)
- currency-conversion fix (v1.5.0)
- custom notification subject/body (v1.6.0)
- resend-email button on the ticket list (PR #51)
Notable merges:
- views_api.api_event_update keeps the explicit-field-list gating from
the aio.4 security fix, with allow_fiat + fiat_currency added so an
owner editing a fiat-enabled event keeps the fiat config.
- models.PublicEvent now exposes both upstream's fiat fields and our
location / categories / status fields.
- migrations.py reverts to byte-identical to upstream v1.6.1 (no aio
entries); fork schema lives in migrations_fork.py (per aiolabs/lnbits#8).
- Lint reformatted with black + ruff to match upstream style.
Contributors entry adds `padreug` (aio fork maintainer).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>