Commit graph

1,040 commits

Author SHA1 Message Date
7dbabfaa81 feat(chatelet): availability calendar with Airbnb-style stay picking
Replace the two date pickers and the manual 'Check availability' button
with one range calendar fed by the extension's keyless unavailable-ranges
endpoint (chatelet ≥ v0.5.0): occupied nights are struck through, a stay
can never straddle one, and check-out may land on the morning another
guest arrives (half-open nights). Two months on sm+, one on phones.

reka-ui only guards clicks with isDateDisabled and hides selected
styling on isDateUnavailable days, so the matchers are selection-aware:
once a check-in is chosen the first occupied night after it stops being
unavailable (it is the last legal check-out) and every later day is
disabled; min_nights is enforced the same way. Both matchers close over
refs because reka reads them once at setup.

Picking a complete stay quotes it automatically; the quote card shows
rate × nights and the total. A quote that contradicts the local calendar
refreshes it (someone else's hold appeared), as does a confirmed booking.
The /login?redirect round-trip of dates is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 11:53:22 +02:00
6ea94692a1 feat(ui): shadcn range-calendar primitive over reka-ui RangeCalendarRoot
Port of ui/calendar onto reka-ui's RangeCalendar* parts, same slot shape.
The cell trigger gains range states — accent tint for the nights inside
the range, primary pills on the two ends — and a real unavailable look
(struck through, muted, not a pointer target).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 11:53:22 +02:00
ded2fd10f8 Merge pull request 'feat(chatelet): book a room and pay by Lightning (slice 2)' (#154) from feat/chatelet-booking into dev
Reviewed-on: #154
2026-09-15 21:52:07 +00:00
56e1d7a780 feat(chatelet): poll the booking itself, not the invoice
Switch the invoice screen's poll from LNbits core's payment status to
chatelet's new keyless GET /api/v1/public/bookings/{id} (chatelet#18).
The dialog now waits for the booking to reach confirmed — meaning the
extension's paid listener has actually run — and shows a 'Dates
released' screen when the hold expires or is declined before payment
instead of spinning forever.

Needs chatelet >= the build that ships the endpoint on the target
instance (staging must be upgraded before this reaches dev).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 23:44:13 +02:00
c369beab92 feat(chatelet): book a room — hold, Lightning invoice, paid confirmation
Slice 2 of the guest UI. The Book button on the room page opens a
dialog that collects guest count, optional contact and a note, asks
the extension to hold the dates, then shows the bolt11 (QR, copy,
open-in-wallet, pay from the LNbits wallet) and polls settlement
until it is paid. useBookingFlow mirrors events' useTicketPurchase.

Booking needs a Nostr identity (the extension DMs check-in details to
the guest pubkey), so anonymous guests bounce through
/login?redirect= and land back on the room with their dates in the
query. Guest checkout without an account is a later slice.

Settlement is polled on LNbits core's anonymous payment status until
the extension grows a public booking-status endpoint; the paid-invoice
listener on the extension is what confirms the booking server-side.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 23:19:05 +02:00
3e55decfad feat(chatelet): booking wire types + requestBooking / payment-status client
Mirror the extension's BookingRequestData / Booking / BookingQuote
models and add the two calls slice 2 needs: POST /bookings (hold +
invoice) and an anonymous settlement poll on LNbits core's
GET /api/v1/payments/{hash}. Chatelet's own booking read needs a
wallet key, so the invoice is what the guest can watch until a public
booking-status endpoint lands on the extension.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 23:10:59 +02:00
718d6e76e1 feat(auth): send the user back to ?redirect= after login
Both login pages always landed on '/'. A page that needs an identity
(chatelet's Book button) can now bounce an anonymous user through
/login?redirect=<path> and get them back. Only same-origin absolute
paths are honoured; anything else falls back to home.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 23:10:26 +02:00
3eb45b04a7 Merge pull request 'feat(chatelet): guest booking UI — slice 1 (browse + availability)' (#142) from feat/chatelet-guest-ui into dev
Reviewed-on: #142
2026-09-15 21:07:51 +00:00
59a5046437 fix(chatelet): stack the date pickers on narrow screens
Side by side at phone width the DatePicker's long-form date truncates
to "September 20t…"; one column below the sm breakpoint keeps the
full date readable.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 22:54:14 +02:00
65770ad9be feat(chatelet): pick check-in/check-out with the shared DatePicker
Replace the two native <input type="date"> fields on the room detail
page with the base DatePicker (Popover + shadcn Calendar), the same
component the events editor uses. Check-in is floored at today and
check-out at the morning after check-in; moving check-in past the
current check-out clears it, and any change resets the last quote.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 22:41:14 +02:00
3720dbc35a feat(chatelet): standalone app scaffold (second Vite entry)
chatelet.html + src/chatelet-app/{main,app,App,app.config,views/SettingsPage}
+ vite.chatelet.config.ts (port 5188, cacheDir .vite-chatelet, dist-chatelet,
manifest id aiolabs-chatelet), mirroring the events standalone. package.json
gains dev:/build:/preview:chatelet + entries in dev:all and build:demo
(VITE_BASE_PATH=/chatelet/). Verified: build:chatelet typechecks + bundles clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019VUQCfdqiLSsFS2jcGnaFD
2026-09-15 22:33:29 +02:00
654be4bf8a feat(chatelet): guest module — ChateletApiService + browse/availability views
Slice-1 part B of the chatelet guest booking UI (aiolabs/webapp #141).

- ChateletApiService: plain HTTP wrapper (events TicketApiService pattern) →
  chatelet's public guest endpoints (/public/rooms, /availability). Unauth by
  default; swaps HTTP→kind-21000 RPC once aiolabs/lnbits#64 lands, no view change.
- useChatelet composable + pinia store (DI via tryInjectService, never direct import).
- Views: ChateletPage (room grid) + ChateletDetailPage (details + date-range
  availability check with sats/fiat quote; Book button stubbed for slice 2).
- SERVICE_TOKENS.CHATELET_API.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019VUQCfdqiLSsFS2jcGnaFD
2026-09-15 22:33:29 +02:00
8b5baeb06d Merge pull request 'feat(events): promo codes — organizer editor and server-validated checkout discount' (#153) from feat/events-promo-codes into dev
Reviewed-on: #153
2026-09-13 16:55:14 +00:00
7dbe04afa3 feat(events): show the redeemed promo code in the scanner roster
Maps applied_promo_code from the stats endpoint onto the roster rows as a
small outline badge and includes it in the fuzzy search keys.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ByAwHU4pRnyE58YocQvAas
2026-09-10 12:22:13 +02:00
a1b9aaf2a2 feat(events): promo code entry with server-validated total at checkout
usePromoCode drives an Apply flow against the validate endpoint (states
idle / checking / applied / invalid / unverified; quantity changes
re-validate with a debounce; stale responses are ignored; 404 from an
older extension keeps the code and sends it unverified). The dialog shows
the struck-through original and the discounted total in the header, price
block, conversion preview and invoice amount; the code rides on both the
Lightning and card requests. A 100 %-off code on the card rail lands on the
success screen via finalizePurchasedTickets instead of the "no checkout URL"
error, and a guest whose total becomes zero gets an inline log-in prompt
(free claims stay login-gated, events#29) rather than a swapped form.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ByAwHU4pRnyE58YocQvAas
2026-09-10 12:22:13 +02:00
4162e0af99 feat(events): promo codes editor in the create/edit event dialog
Collapsible "Promo codes" section: rows of code (upper-cased as typed),
discount %, max uses (blank = unlimited), active switch, remove; per-row
errors from validatePromoRows gate submit alongside the vee-validate form.
extra.promo_codes is always sent explicitly (empty editor = clear); edit
mode prefills from the authenticated record.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ByAwHU4pRnyE58YocQvAas
2026-09-10 12:22:13 +02:00
262e674f9f fix(events): let the organizer's own record win when merging My Events
The authenticated fetch returns the full Event (incl. extra.promo_codes,
which public responses omit as of events ext v1.6.1-aio.12) and is what
the edit dialog must prefill from. Public records still win for events the
user does not own.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ByAwHU4pRnyE58YocQvAas
2026-09-10 12:22:13 +02:00
a68fed916d i18n(events): promo code strings for the editor and the purchase dialog
New events.create.promo.* (first key under a create namespace; the rest
of the create dialog remains hardcoded English) and events.purchase.promo.*
in en/es/fr + the types interface.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ByAwHU4pRnyE58YocQvAas
2026-09-10 12:22:13 +02:00
0a21c092ff feat(events): promo code wire types, validate endpoint client, pure helpers
Types follow upstream v2 names (PromoCode.max_uses / used_count,
PromoValidateRequest, BasketDiscount, BasketTotals) as served by the
events extension >= v1.6.1-aio.12; EventExtra.promo_codes becomes
optional because public event responses no longer carry it.

TicketApiService.validatePromoCode posts the upper-cased code + quantity
to POST /events/api/v1/promo/validate/{event_id}; request() now attaches
the HTTP status to thrown errors so a 404 from an older extension can be
told apart from a 400. lib/promoCodes.ts holds the pure helpers shared by
the organizer editor and the buyer dialog (normalise, editor validation
returning i18n keys, wire <-> row mapping, display-only percent rounding).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ByAwHU4pRnyE58YocQvAas
2026-09-10 12:22:12 +02:00
9c0abfae1f fix(events): let the event card shrink below its truncated rows (min-w-0)
On phone widths the event list is a one-column grid, and a grid item
defaults to min-width:auto (= min-content). The card's nowrap truncate
rows (location, organizer) push that min-content past the viewport for
any event with a long address, so the card spilled off screen and the
text was clipped by the window instead of ellipsised. min-w-0 on the
Card root lets the item shrink; overflow-hidden would have clipped the
pending/rejected badge that deliberately hangs past the bottom edge.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KVxfxB2sDFrdwnXrjhEp1b
2026-09-10 06:09:10 +02:00
4faa57f87a fix(events): resolve the ticket QR logo through @brand-app-logo
ticketQr.ts imported @brand/logo.png by literal filename, so any brand
kit that ships its logo as SVG only (cfaun's does) failed the events
standalone build with ENOENT. Every other logo consumer goes through
the @brand-app-logo alias, which resolves the per-app or brand logo in
SVG-then-PNG order; use the same alias here.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nx87UjQtd1HS5u4DwCMve8
2026-09-09 14:48:31 +02:00
b080d802c5 Merge pull request 'fix(events): pin the sticky buy bar flush above the nav (--bottom-nav-height)' (#150) from fix/events-sticky-bar-gap into dev
Reviewed-on: #150
2026-09-09 11:19:46 +00:00
6e4e2f3d0c fix(events): pin the sticky buy bar flush above the nav
Two causes of the visible gap under the bar on phone and desktop: the
bar lived inside the page's `space-y-6` column, whose child margin also
offsets a fixed box (24px), and its bottom offset was hand-derived. It
now teleports to <body> and uses `--bottom-nav-height`. Measured in a
headless run: bar bottom == nav top on 390px and 1280px viewports.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ByAwHU4pRnyE58YocQvAas
2026-09-09 13:19:05 +02:00
82f7e5659a feat(layout): publish the bottom nav's rendered height as --bottom-nav-height
BottomNav measures itself (ResizeObserver) and writes
`--bottom-nav-height` on <html>; index.css declares the 0px default. Any
element that must sit flush above the nav pins with
`bottom: var(--bottom-nav-height)` instead of re-deriving
`3.5rem + env(safe-area-inset-bottom)` and drifting when the device's
inset, border or font scaling differs from that guess. The restaurant
item page's add-to-cart bar is migrated to it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ByAwHU4pRnyE58YocQvAas
2026-09-09 13:19:05 +02:00
9eb75a2017 feat(events): surface the buy-ticket CTA above the description + sticky bar
The ticket block (owned banner, Buy ticket + availability, past/sold-out
notices, and the host's Scan CTA) moved from below the description to
directly under the date and location. A long description was pushing
the primary action below the fold — on the Château du Faune event the
button sat under ~1.7k characters of text.

A sticky bar (title, price, tickets left, the same CTA) pins above the
bottom nav whenever the inline block is scrolled out of view
(useElementVisibility), and hides again when it is back, or while the
purchase / return dialogs are open. Container gets extra bottom padding
while the bar is up so the organizer card is never covered.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ByAwHU4pRnyE58YocQvAas
2026-09-09 12:47:38 +02:00
cd70e65425 feat(events): warn buyers to check their spam folder for the ticket email
A tester's ticket landed in spam. A mail-tester run of the demo mailer
scored 8.3/10 with authentication fully green, so the sender setup is
not at fault and the mailer is being hardened separately (aiolabs/events
fix/ticket-email-headers). Until sender reputation builds, tell buyers
where to look: a red hint under the email field.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo
2026-09-08 16:24:19 +02:00
1e43e8ed0a fix(events): let guests default to the card rail in the purchase dialog
`selectedMethodId` started as 'lightning', so the "keep the current
selection" guard in the default-rail watcher always found a match and the
guest preference for fiat never applied — verified on demo, where an
anonymous buyer saw "Proceed" (Lightning) preselected with Card on offer.
Start empty and reset to empty on close so each open re-evaluates:
guests land on Card when it exists, everyone else on the first enabled
rail, exactly as the comment already promised.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo
2026-09-08 13:02:31 +02:00
411ac0c93c Merge pull request 'feat(events): guest checkout with email delivery, Stripe return flow, public ticket page' (#144) from feat/events-guest-checkout into dev
Reviewed-on: #144
2026-09-08 10:25:56 +00:00
cbd4159f72 docs(events): guest checkout, fiat return flow and ticket delivery contract
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo
2026-09-06 19:46:16 +02:00
89a1fcb030 feat(events): use the shared logo QR renderer in My Tickets
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo
2026-09-06 19:43:11 +02:00
5d3ebcc5e4 feat(events): per-event payment methods in the create/edit dialog
Replace the "Also accept fiat" switch with a Lightning / Card checkbox
group so an organizer can switch Lightning off for a card-only sale.
Sends extra.payment_methods (upstream v2 shape) and keeps allow_fiat in
sync (fiat in list ⇒ allow_fiat=true) for older readers. Card stays
disabled with a tooltip when the organizer has no fiat provider; the
fiat-currency picker appears when Card is on and the price is in sats.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo
2026-09-06 19:43:10 +02:00
b853fbce4b feat(events): public ticket page at /events/ticket/:ticketId
Target of the emailed ticket link the extension now builds under the
webapp root. Anonymous, like the LNbits UI's own /events/ticket/{id}:
reads the PublicTicket, resolves the event name (Nostr store → scoped
relay query → public REST as a last resort), and renders the logo QR
with paid / pending / checked-in state.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo
2026-09-06 19:39:42 +02:00
bd78e3227d feat(events): handle the return from fiat checkout on the event page
Stripe now brings the buyer back to the event page with
?checkout=success&tickets=<ids> or ?checkout=cancelled. The page strips
the query, then opens CheckoutReturnDialog, which polls the anonymous
ticket endpoint until every row is paid (60 s budget with a retry), and
renders one logo QR per ticket with a link to its public ticket page and
the "emailed to" note from the pending-checkout record. Logged-in buyers
also get their owned tickets refreshed and a My Tickets shortcut.

The login toast on "Buy ticket" now only fires for free events; paid
events open the dialog for guests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo
2026-09-06 19:37:52 +02:00
891b5072f8 feat(events): guest checkout in the purchase dialog with delivery email
Buyers no longer need an account to buy a paid ticket. The dialog gains a
"Your details" block (name for guests, email + confirm-email for everyone,
prefilled from the account email) as a vee-validate/zod form, and builds
its rail buttons from the event's published payment methods so an
organizer can run card-only events.

- Fiat: same-tab redirect to the provider right after the POST, with the
  pending checkout (ticket ids, email) remembered in localStorage for the
  return page; the two-step "open in new tab" panel is gone.
- Lightning: guests get the bolt11 QR + "Open in wallet" and settle via
  the existing poll; the LNbits-wallet button stays login-only.
- Free tickets remain login-gated (events#29).
- Guest success state renders the ticket QRs inline with the "emailed
  to" note, since guests have no My Tickets.

i18n keys added under events.purchase / checkout / ticket (en, es, fr).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo
2026-09-06 19:35:45 +02:00
f85848a50b feat(events): guest identity, frontend_url and public ticket read in the ticket API
Prepare the webapp for account-less ticket purchases against events ext
v1.6.1-aio.8:

- requestTicket takes an options object: user_id (+ optional email) for
  logged-in buyers, name + email for guests, Authorization only when a
  token exists, and frontend_url so the backend builds the Stripe return
  URLs and the emailed ticket link under this app's root.
- getTicket (anonymous PublicTicket) and getPublicEvent for the return
  dialog and the public ticket page.
- resendTicketEmail maps the structured TicketResendResult (legacy bare
  ticket tolerated).
- NIP-52 tickets_payment_methods tag parsed into ticketInfo.paymentMethods;
  effectivePaymentMethods() mirrors the backend rule.
- renderTicketQr: level-H ticket:// QR with the brand logo centred, shared
  by every ticket surface; useTicketPurchase accepts a guest contact.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo
2026-09-06 19:30:47 +02:00
0c5b577499 fix(events): read buyer id from auth service in fiat checkout
The fiat path pulled `currentUser` off the LNbits API client, which
never exposed it — only `getAccessToken()`. `userId` was therefore
always undefined and every Stripe/fiat "Continue" click failed with
"Missing user id" before the request was ever sent.

Use `useAuth()`'s `currentUser`, the same source the Lightning path
already goes through.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YLt6zWfXA1hM8zYQdpbeyn
2026-09-05 19:51:04 +02:00
c035fc2bfc feat(libra): flag the "Other" account hint on the account step
Add a compact inline amber warning (icon + text) next to the
"Select the account…" instruction on both the expense and income
account-selection steps, so users know to fall back to the "Other"
account when unsure.

Splits the income i18n into selectAccount + a new otherAccountHint
key (en/fr/es + types).
2026-06-26 11:07:37 +02:00
9f8f2a233b Merge pull request 'feat(libra): expense/income form UI fixes' (#139) from feat/libra-expense-ui-fixes into dev
Reviewed-on: #139
2026-06-25 20:43:35 +00:00
8a9ea269de feat(libra): trim amount/currency/reference helper text
Remove the "Amount in selected currency" and "Currency for this
income/expense" helper lines on both forms, drop the Reference helper
line, and mark the field as "Reference (optional)" in the label
instead. Also drops the now-unused FormDescription import from the
income form.
2026-06-25 22:39:51 +02:00
87f86647e7 feat(libra): match income Description field to the expense form
Fold the description guidance into the placeholder and drop the
separate FormDescription helper line, so the income Description field
mirrors the expense form's structure.
2026-06-25 22:37:29 +02:00
3ffb3bf983 feat(libra): mention the "Other" account on the income account step
Append the same "Other" account guidance to the income form's Step 1
(libra.income.selectAccount) that the expense form already shows, for
consistency. Updated across en / fr / es locales.
2026-06-25 22:21:55 +02:00
49e7afb0ab feat(libra): put Amount and Currency on the same row
Wrap the Amount and Currency fields in a two-column grid on both the
expense (AddExpense.vue) and income (AddIncome.vue) forms, and add
w-full to the currency SelectTrigger so the dropdown fills its column.
Tightens the vertical layout of the entry dialogs.
2026-06-25 22:16:44 +02:00
807f8200b1 feat(libra): guide expense account choice + description copy
Step 1 (account selection) now tells the user to pick the "Other"
account if they're not sure. The Description field's placeholder is
rewritten to prompt for a detailed description of the purchase /
invoice / bill and what it was used for (event/project/etc), replacing
the redundant helper line.

Client feedback: with no fixed category taxonomy yet, steering unsure
users to the Other account plus a richer free-text description lets us
reconcile/recategorize later.

Refs aiolabs/webapp#137
2026-06-25 22:10:41 +02:00
eb9ae54215 Merge pull request 'feat(events): handle free tickets in the purchase flow' (#131) from feat/free-tickets-client into dev
Reviewed-on: #131
2026-06-20 09:58:39 +00:00
afb57a3918 feat(events): handle free tickets in the purchase flow
Companion to aiolabs/events#31. Free events (price 0 / 100%-off promo)
now come back from POST /tickets/{event_id} as paid=true with the row
ids inline and no payment_request — the backend issued them
already-paid, no invoice to settle.

Previously the composable's `!paymentRequest` guard treated any
invoice-less response as fiat and threw "This event uses fiat
checkout", so free tickets were unbuyable.

- TicketPurchaseInvoice gains `paid` + `ticketIds`; TicketApiService
  maps them.
- purchaseTicketForEvent short-circuits on `invoice.paid`: skip the QR /
  payment-poll and go straight to the ticket-QR success state. The fiat
  error now only fires for an actual fiat (not-paid) response.
- The ticket-QR rendering (refresh owned tickets, one QR per row, toast)
  is extracted into a shared finalizePurchasedTickets() used by both the
  Lightning-poll path and the free path.
- PurchaseTicketDialog: for free events drop the payment-method selector
  and price line, show "Free", and label the CTA "Get ticket".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 09:09:46 +02:00
36791c8121 feat(hub): hide standalones not provisioned on this deploy (aiolabs/webapp#129)
The hub rendered all 8 standalones statically and greyed out any whose
VITE_HUB_<APP>_URL was unset — so an events-only deploy showed 7 dead
"coming soon" tiles. The greyed path was overloaded: hubLink() returned
null for both "not deployed" and "logged out", which the template then
couldn't tell apart.

Replace that with a registry → resolver → view-model pipeline:

- availabilityOf(m) resolves an explicit state from deploy config + auth:
  available | auth-locked | inactive | unavailable.
- A `tiles` computed maps the catalog to view-models and drops
  'unavailable' (not provisioned) entirely — no more ghost tiles.
- 'auth-locked' keeps today's greyed + login-prompt behavior for
  deployed-but-logged-out apps (wallet, chat, tasks, libra).
- 'inactive' (active:false) is wired as a greyed, inert state — an unused
  seam for a future install/disable model; no app sets it yet.

The view treats the catalog as an opaque list, so the source can later
move from this in-code array to a runtime feed (LNbits /hub/apps or a
NIP-78 event) without touching the render path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 08:50:07 +02:00
2cc8e34b9d feat(layout): re-enable "Back to hub" with a sticky sheet footer
Reverses the events-only hide (c037d45) now that the link has a real
home. Three parts:

- Add a @brand-hub-logo alias (brandHubLogoAliasEntry) resolving to the
  brand's primary/global logo — the HUB's logo, never the per-standalone
  @brand-app-logo. Wired into all 9 app vite configs since the shared
  ProfileSheetContent renders it.
- Restructure the profile sheet into a fixed-height flex column: a
  flex-1 min-h-0 overflow-y-auto scroll region over a shrink-0 footer,
  so "Back to hub" + the log-in/out bar stay pinned to the bottom while
  the identity/preferences area scrolls.
- Move the edit-profile Dialog out of the flex root (it portals to body,
  so it's not part of the sheet flow).

Logo bumped to w-8 h-8, centered, with tightened footer padding.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 00:26:52 +02:00
8419ca4660 fix(events): collapse own-event draft + relay copy into one card
Coordinate-keying the events store (kind:pubkey:d-tag) regressed the
own-events merge: a creator's own event showed up twice in the feed.

`loadOwnEvents` surfaces the caller's own LNbits events via REST
(`ticketedEventToEvent`) so drafts appear before their NIP-52 event is
on a relay. That adapter stamps an empty `organizer.pubkey`, `isMine`,
and no ticket info. The relay-published copy lands under the real
publisher pubkey (`resolve_for_wallet` — NOT the user's Nostr login
key) with full ticket counts. Pre-coordinate-keying both collapsed on
the bare d-tag so the relay copy replaced the draft; under
`kind:pubkey:d-tag` the empty-pubkey draft and the real-pubkey copy are
distinct keys, so both render — the empty "..."/no-tickets card next to
the real one. Only the logged-in owner sees it, since only own events
get the REST merge.

upsertEvent now reconciles by d-tag: the published copy supersedes the
provisional draft and inherits its `isMine`, so the creator keeps the
Yours badge + Hosting filter even though the publisher key differs from
their login key. Handles both arrival orderings; a draft with no
published copy yet (pending review) still shows alone; genuinely
distinct authors sharing a d-tag are untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 00:46:12 +02:00
a7fd686cdd fix(nix): regenerate pnpmDeps hash for the vitest lockfile change
Adding vitest (#124) updated pnpm-lock.yaml (pulled in @standard-schema/spec
and other vitest deps) but the fixed-output pnpmDeps.hash in mkWebapp was
not regenerated, so the build reused the stale offline store and failed
with ERR_PNPM_NO_OFFLINE_TARBALL during `pnpm install --offline`. Rehash
so the deps FOD is refetched from the current lockfile.

Verified: `nix build .#chat` succeeds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 14:50:36 +02:00
f3612f82ac fix(events): clarify the scan-tickets "Remaining" stat as "Not scanned"
The scan station's third stat card (sold minus scanned) was labeled
"Remaining", which read like leftover sales capacity. Relabel it "Not
scanned" so the trio reads unambiguously: Scanned + Not scanned = Sold.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 12:45:49 +00:00