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).
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.
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.
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.
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.
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
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>
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>
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>
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>
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>
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>
The page header used py-4, putting the logo/banner ~6px below the fixed
top-right profile icon's center. Use pt-2.5 so the header sits centered
on the same horizontal axis as the profile icon (both centers at 30px).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment out the profile sheet's "Back to hub" link (and its hubRootUrl)
since only the events app ships first — nothing to go back to yet. The
commented markup swaps the old Home icon for the HUB's brand-kit logo
(noted as a @brand-hub-logo alias to add when re-enabled — the hub logo,
not the per-standalone @brand-app-logo) so it's ready to re-enable when
the hub launches.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The top-right menu trigger showed a LogIn (arrow-into-door) icon when
logged out; use the generic User icon instead — it reads as "your
account / profile" and matches the avatar shown when logged in. Still
opens the same profile/menu sheet (with the login CTA).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
useEventDetail.load() early-returned when the event was already in the
store, so arriving from the feed (cached) set up no live subscription.
NIP-52 calendar events are replaceable and the events extension
republishes them when a ticket sells (updating tickets_sold/available),
but with no subscription the detail page never received the update —
counts went stale until a manual reload.
Always open the dTag-scoped subscription (only the one-shot query +
loading state are skipped on a cache hit), and unsubscribe a prior sub
before re-subscribing so reload() can't leak one. The reactive `event`
computed then reflects republished counts without a reload.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The calendar popup already narrows its day-dots to the active category
filter; surface those categories inside the popup so the user can see —
and loosen — what's narrowing it without closing. Renders only the
selected categories as removable chips; clicking one emits toggle-category
to the parent, which reactively re-widens the dots in place.
- EventCalendarPopup: optional selectedCategories prop (defaults to none
for callers like My Tickets) + toggle-category emit; chip row between
the header and the month grid.
- EventsPage: wire selectedCategories + toggleCategory through.
- i18n: events.filters.filteringBy + removeCategory (en/fr/es + schema).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "strictly-monotonic created_at per coord" section named useRSVP.ts as
canonical, but that file no longer exists. monotonicCreatedAt() in
src/lib/nostr/timestamp.ts is now the single implementation — make the
doc reference it and show both the per-coord-Map and single-field
tracking shapes. Keeps doc and code aligned per the docs discipline.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Relays only push a replaceable-event update to OPEN subscriptions when
its created_at is strictly newer than the held version. created_at is
second-resolution, so useBookmarks' `Math.floor(Date.now()/1000)` lets
two rapid toggles collide in the same second — the second is treated as
not-newer and never reaches live subscribers (only a reload shows it).
This is the same root cause found while debugging the live ticket count.
- Add `monotonicCreatedAt(lastCreatedAt, now?)` = max(now, last+1), a
reusable helper for any replaceable-event publisher.
- Use it in `toggleBookmark`; track `lastCreatedAt` as a typed field on
BookmarkState (drops the `(state as any)` casts).
Unit tests cover no-prior, same-second bump, wall-clock tracking,
future-dated prior, and a strictly-increasing same-second burst.
The aiolabs/events extension's nostr_publisher uses int(time.time()) the
same way — flagged in #122 for a follow-up on the backend.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
NIP-52 calendar events (kinds 31922/31923) are addressable: their d-tag
is author-scoped, so the replacement key is kind:pubkey:d-tag, not the
bare d-tag. The store keyed `eventsMap` by `event.id` (d-tag) and
replaced on newer `created_at` ignoring pubkey, so a different author
republishing the same d-tag could overwrite a legit event in the store
(cross-author hijack). NDK (`event.coordinate()`) and welshman
(`eventsByAddress`) both key addressable events by the full coordinate.
- Key `eventsMap` by `eventCoordinate()` = `${kind}:${pubkey}:${dtag}`;
same-coordinate-newer-wins replacement, different authors stored apart.
- Keep the d-tag as the route identifier: `getEventById(dtag)` scans and
returns the newest match (single-publisher in practice). Add
`getByCoordinate()` for precise, author-known lookups.
- `removeEvent(dtag)` deletes every coordinate sharing that d-tag.
Client-side only — the store is rebuilt from relays each session, so no
demo-DB surgery. Covered by vitest unit tests including the cross-author
no-overwrite case.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
No test runner existed in the repo. Add vitest (node env, *.spec.ts
discovery) with a minimal config mirroring only the `@`→src alias, plus
`test`/`test:watch` scripts and a smoke test as a known-good baseline.
Precursor for the nostr-patterns review fixes (events store coordinate
keying #121, monotonic created_at #122), which ship with unit tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The date-picker popup showed dots for all events regardless of the
active category filter. Feed it a category-filtered set so its per-day
dots reflect what the user is browsing (temporal/day filters still don't
apply — the calendar is for picking any date). No categories selected
behaves as before (all events).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rebuild the calendar popup on the reka-ui dialog primitives instead of
the shared DialogContent, so it can use a light, blurred overlay
(bg-background/20 + backdrop-blur-md) instead of the usual opaque dark
dim. The panel itself is translucent + blurred (bg-background/70 +
backdrop-blur-xl). Result: the feed stays visible, softly blurred,
behind the frosted glass. Scoped to this popup — other dialogs keep
their solid dark overlay.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The like count only watched bookmark lists by #a, so when someone removed
an event their new (replaceable) list no longer matched the filter and
never arrived — the count stayed stale until reload. Also watch known
likers by `authors` and track each author's current liked-coords, diffing
prev vs next on every update so a dropped coord decrements live. Verified
end-to-end against a relay: a like incremented the count and the same
key's updated list (coord dropped) decremented it with no reload.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The wallet WebSocket handler assumed LNbits sends the PRE-payment balance
for outgoing payments and subtracted the amount again. LNbits actually
re-fetches the wallet AFTER the payment settles before emitting the
notification (core payments.py `_send_payment_notification_in_background`
→ "fetch balance again" → notifications.py `send_ws_payment_notification`
→ `wallet.balance`), so `wallet_balance` is already POST-payment for both
directions. The extra subtraction double-deducted: 100 sats, send 10 →
balance showed 80, then a refresh (which reads the authoritative
/api/v1/wallet balance) corrected it to 90.
Use `wallet_balance` as-is for all messages, matching the polling
fallback which already trusts the API balance directly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Display how many people have favorited (liked) an event next to its
heart, updating in real time. A like == the event appearing in someone's
NIP-51 bookmark list (kind 10003) — the same action the heart performs.
New useEventLikes composable keeps ONE batched subscription over every
mounted heart's event coordinate (filtered by #a). It stays open after
EOSE, so a like published by anyone is pushed live and the count ticks
up for everyone — verified end-to-end against a relay (a like from a
fresh key bumped the shown count 2→3 with no reload). The heart also
pops on a live increment (gated past the initial historical-load
window), and the user's own like/un-like reflects instantly via the
optimistic heart state.
Caveat: an un-like by another user only reflects on next load — a
replaceable list that no longer contains the coord stops matching the #a
filter, so the removal isn't pushed. Counts are correct on fresh load.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
After a successful ticket purchase, the feed/calendar "My tickets" filter
and EventCard owned badges didn't update until a full page reload — the
shared useOwnedTickets singleton was never refreshed. Its own docs note a
successful purchase should call refresh(); wire that in at the payment-
confirmed point in useTicketPurchase so every surface reflects the new
ticket immediately.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The heart took ~1s to fill because toggleBookmark awaited the remote
LNbits signer + relay publish before updating state. Flip local state
optimistically so the heart responds on tap, then sign/publish in the
background and roll back (with an error toast) if it fails. Add a brief
scale pop on the heart when a favorite is added for tactile feedback.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Defensive guard so the date-picker popup can never linger across
navigation (reported: it appeared open on the feed after returning from
an event detail page). Force calendarOpen=false in onBeforeRouteLeave on
both the feed and My Tickets, the two popup hosts. Modals shouldn't
survive a route change regardless of how the close/navigation interleave.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the My-tickets filter that lived on the removed calendar page.
A calendar button opens the date-picker popup with per-day dots over the
user's own events; picking a day filters the ticket list to it (a
removable date chip overrides the upcoming/past toggle while active).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The calendar is now a date-picker popup on the feed (and, next, a visual
on My Tickets), so the dedicated /events/calendar page and route are no
longer needed. Delete EventsCalendarPage.vue, drop its route, and update
the stale openCreate comment.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The calendar button now opens the date-picker popup instead of
navigating to a separate page. Picking a day filters the feed to that
day (button highlights while active) and shows a removable date chip
whose ✕ clears ONLY the date selection — distinct from category
clearing, which keeps its own clear in the filter dropdown. Dots in the
popup come from the full (unfiltered) event set.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bring back selectedDate filtering, now driven by the calendar popup
instead of the removed week-strip: when a day is picked it takes
priority over the temporal pills and bypasses the past/upcoming split.
Add a dedicated clearSelectedDate() so the date selection can be cleared
independently of categories (a preset pill also clears it).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a pickerMode to EventCalendarView (month grid only, emits selectDate
on every day tap, no selected-day events panel) and a new
EventCalendarPopup dialog wrapping it. Picking a day emits the date and
closes. Groundwork for replacing the standalone calendar page with an
on-feed date-picker popup and a My-Tickets event-date visual.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The My-tickets filter chip sat on its own row below the Back link,
wasting vertical space. Move it onto the Back-button row (still
left-aligned to clear the fixed top-right hamburger), reclaiming a row.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The aspect-square day cells made the month grid ~350px tall on mobile,
pushing the selected-day event list well below the fold. Switch cells to
a fixed h-12 (still a comfortable 48px tap target) and tighten the
section spacing (space-y-4 → space-y-2), so the grid + day's events fit
with less scrolling.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The toggle sat in the header's top-right corner, where it collided with
the fixed top-right hamburger menu (the Past button rendered behind it).
Move it to its own left-aligned row above the tabs — same approach the
calendar page uses to clear the hamburger.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
My Tickets listed every ticket with no way to separate events that
already happened. Add an Upcoming/Past segmented toggle (defaults to
upcoming) that filters the grouped tickets by their event's date, with
the tab counts (All/Paid/Pending/Registered) derived from the visible
set so badges match what's shown. Events not yet resolved from relays
stay visible under Upcoming until their date is known.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
With the DatePickerStrip gone, selectedDate/selectDate in useEventFilters
were unreachable — nothing set a specific date anymore (the calendar page
only navigates to event detail). Delete the orphaned DatePickerStrip
component and strip the now-dead date-filter branch, state, and actions.
The feed filters purely by temporal pills + past/upcoming + categories.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The feed had two redundant day controls — the DatePickerStrip week strip
and the temporal preset pills — on top of the calendar page. Remove the
week strip (the calendar already covers picking a specific date) and move
the calendar shortcut to the end of the temporal-filter row, next to the
pills. Frees a row and keeps coarse windows (Today/This Week/…) inline
with one-tap access to the full calendar.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The calendar page had no way back to the feed except the browser/back
gesture. Add a top-bar ghost Back link (ArrowLeft + "Back" → events feed)
mirroring EventDetailPage's pattern for navigation consistency.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The month-grid calendar opened with no day selected, so the events panel
below was empty until the user clicked a day. Initialise the selection to
today (currentMonth already starts on the current month) so it opens
showing today's events.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>