Commit graph

893 commits

Author SHA1 Message Date
a59712327f feat(activities): useOwnedTickets composable + ActivityCard ticket badge
Module-level singleton so the badge on every ActivityCard, the
owned-tickets section on ActivityDetailPage, and the (forthcoming)
"My tickets" filter chip on the activity feed all share one fetch
of the user's tickets rather than each instance hitting the
backend.

useOwnedTickets exposes:
- ticketsByActivity: Map<activityId, ActivityTicket[]> for O(1)
  lookup from the card/detail surfaces
- ownedActivityIds: Set used by the feed filter
- paidCount(id) / getTickets(id) for ergonomic per-activity reads
- refresh() for consumers that just mutated the user's ticket set
  (a successful purchase) to update every surface atomically

Auto-loads on first use after auth is ready, re-fetches when the
current user id changes (login/logout/switch).

ActivityCard grows a primary-colored "You have N tickets" row that
sits next to the existing "X tickets remaining" line — buyer can
see at a glance whether they've already bought in for any activity
in the feed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 00:38:25 +02:00
6a35e8e0cb feat(activities): parse ticket inventory tags from NIP-52 events
The aiolabs/events extension publishes six AIO custom tags on every
kind 31922/31923 calendar event (tickets_available, _sold, _price,
_currency, _allow_fiat, _fiat_currency — see aiolabs/events#15) and
republishes the event on every ticket sale. Connected clients pick
up the new state via their existing relay subscription, no REST
polling.

- New TicketTags shape on CalendarTimeEvent + CalendarDateEvent.
  parseTicketTags reads the six tags off the raw event; tickets_
  currency is the discriminator so non-AIO calendar events (which
  don't have these tags) cleanly produce undefined.
- ActivityTicketInfo grows `sold` + `allowFiat` + `fiatCurrency`
  for the buyer surfaces, drops the never-populated `total` field,
  makes `available` optional (undefined = unlimited capacity).
- Both calendar→Activity converters now populate ticketInfo via
  ticketTagsToInfo so Nostr-sourced activities carry the inventory
  info that was previously only on LNbits drafts.
- ActivityCard handles the three-state available display
  (unlimited / count / sold-out) instead of just truthy/sold-out.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 00:38:25 +02:00
9f38611f4f feat(activities): notification config on event create + edit
CreateEventDialog gains a collapsible "Buyer notifications" section
exposing the EventExtra fields added upstream in v1.4.0 / v1.6.0:

- email_notifications + nostr_notifications switches — opt buyers
  into email and NIP-04 Nostr DM ticket confirmations.
- notification_subject + notification_body inputs — let organizers
  customize the message. Empty falls back to extension defaults.

Submit handler builds `extra` by overlaying onto the existing
event.extra so unrelated fields the LNbits admin UI sets
(promo_codes, conditional, min_tickets) survive the round-trip
through the webapp. Populate-from-event mirrors the same.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 00:38:25 +02:00
a4200749ae fix(activities): normalize 'sat' vs 'sats' across fiat conditionals
TicketApiService.getCurrencies() returns 'sats' (plural) while the
schema, initialValues, and existing comparisons used 'sat' (singular)
— a pre-existing inconsistency in the events extension surface. The
new payment-rails conditionals tripped on it: as soon as the user
picked the populated 'sats' option from the price-currency dropdown,
form.values.currency became 'sats', the `=== 'sat'` check failed, and
the Fiat currency dropdown stayed hidden even with the toggle on.

Normalize all the new comparisons to accept both spellings:

- FiatToggleField: isSatDenomination(d) helper drives both the
  v-show and the auto-mirror watch.
- CreateEventDialog Zod superRefine: same accept-both rule on the
  require-fiat_currency branch.
- PurchaseTicketDialog: isPriceInSats computed drives the
  Lightning-sats badge AND the PriceConversionPreview render
  condition AND the inverse conversion watcher's bail-out.

Also flip FiatToggleField to drive dropdown visibility from the
outer FormField's slot value rather than useFormContext — slot
bindings are guaranteed reactive, sidesteps the public-form-context
indirection that earlier left allowFiat stale in the child's
template.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 00:38:25 +02:00
d6efbd2c65 fix(base): FiatToggleField reads form state via useFormContext
The previous version called useField directly with a getter for the
field name. That created a child-local field rather than connecting
to the parent form's allow_fiat / fiat_currency state — so the
Switch's on/off visually toggled but the form never knew, and the
conditional Fiat currency dropdown never appeared.

Rewrite around the proven pattern used elsewhere in the dialog: bind
the inputs through FormField (the shadcn-vue / vee-validate Field
component) and reach for cross-field state via useFormContext.
showCurrencyDropdown now reads form.values[allowFiatField] directly,
which mirrors the parent's actual state, and the denomination-mirror
watch goes through form.setFieldValue.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 00:38:25 +02:00
574c178d89 feat(activities): provider-aware checkout labels and conversion preview
The buyer-side payment-method block now surfaces one button per
configured fiat provider — Stripe, PayPal, Square, SEPA — rather than
a single bare "Fiat" catch-all. Buttons read in provider names so the
buyer never has to guess what rail backs each choice; the dispatch on
click forwards both `rail` and `provider` to the existing
`ticketApi.requestTicket` signature.

PaymentMethodSelector + useFiatProviders from the base module drive
the list. The Lightning button picks up a "≈ N sats" badge whenever
the event price is denominated in fiat, so the buyer sees the live
sat charge alongside the headline price. A new conversion-preview
line under the headline shows the sat→fiat estimate in the inverse
case (sat-denominated event with fiat enabled), giving the rail-vs-
unit asymmetry an explicit place in the UI.

Explanatory copy makes the equivalence explicit: both methods charge
the same amount, rates are estimates, exact amount locks in at
checkout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 00:38:25 +02:00
985c10939d refactor(activities): adopt shared payment-rails pattern in CreateEventDialog
Split the bottom of the create/edit form into two semantic sections
that read in the canonical vocabulary:

  Pricing
    Tickets · Price · Price currency   ← renamed from bare "Currency"
  Payment methods
    Lightning — always on (informational chip)
    <FiatToggleField/>                  ← replaces the inline switch
                                          + raw fiat_currency dropdown

The toggle field handles the conditional dropdown (hide + auto-mirror
when the price denomination IS the fiat currency) and the disabled-
with-tooltip state when the user has no configured provider, so the
parent form just supplies field names + the denomination value.

Zod superRefine grows a check that requires `fiat_currency` only in
the surface where the toggle exposes the dropdown — `allow_fiat &&
currency === 'sat'`. Submit-time payload drops `fiat_currency` when
`allow_fiat` is off so we don't persist a rail-currency the backend
won't use.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 00:38:25 +02:00
caec8eddcc feat(base): payment-rails composables + components shared across modules
Activities is the first module to mix Lightning + fiat rails; restaurant
and marketplace will follow. Extract the cross-cutting bits to the base
module so the next adoption is a wiring exercise:

- useFiatProviders: reactive `User.fiat_providers` (today the same list
  for organizer + buyer because LNbits configures providers globally),
  plus `providerMeta()` for label/icon hints.
- usePriceConversion: `convert()` + reactive `useLivePreview()` over
  the existing `/api/v1/conversion` endpoint, 60s cache, null on
  transient failure.
- PaymentMethodSelector: buyer-side rail picker. `PaymentMethod.id`
  enumerates rails (`lightning | fiat | cash | internal | …`) with
  `provider` for the fiat case so a multi-provider instance shows one
  button per provider instead of a bare "Fiat" catch-all.
- FiatToggleField: organizer-side switch + conditional fiat-currency
  dropdown. Auto-disables with a setup-instructions tooltip when the
  user has no providers; silently mirrors fiat_currency to a non-sat
  price denomination to keep the backend payload consistent.
- PriceConversionPreview: muted "≈ X.XX USD" line for surfaces where
  the price denomination differs from the chosen rail's currency.

LnbitsAPI.getConversion wraps the conversion endpoint so the composable
goes through the existing API service rather than raw fetch. CLAUDE.md
gains a "Payment rails pattern" section documenting the canonical
vocabulary ("Price currency" / "Fiat currency" / "Payment method" /
"Also accept fiat" — bare "Currency" and "Pay in fiat" are banned in
payment-context UI labels) and the fiat-providers-are-global note.

The pre-existing `prvkey` comment on User picks up an inline allowlist
marker so the secret scanner stops flagging this file on every commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 00:38:25 +02:00
ec0dbf727b feat(activities): expose fiat checkout on event create + purchase
Both sides of the fiat-payment surface introduced by events v1.4.0:

CreateEventDialog — organizer-side opt-in:
- New "Accept fiat payments" switch (allow_fiat) + fiat currency
  picker (USD/EUR/GBP/CHF). Toggle is always shipped on
  create/edit so a true→false flip propagates correctly.
- Hint copy notes that the host's LNbits admin needs a configured
  fiat provider (Stripe etc.) for the toggle to actually work at
  purchase time.

PurchaseTicketDialog — buyer-side method selector:
- Two-button selector (Lightning / Fiat) shown only when the event
  has `allow_fiat=true`. Hidden entirely for Lightning-only events.
- Lightning path: unchanged (uses useTicketPurchase composable).
- Fiat path: posts to the API with `payment_method=fiat`, then
  surfaces a "Open <provider> checkout" button that opens the
  returned fiat_payment_request URL in a new tab. Payment
  confirmation happens via webhook on the backend; ticket appears
  in My Tickets on next reload.

EventsPage threads the new fiat fields through `selectedEvent` so
the dialog sees them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 00:38:25 +02:00
73aee75b5b feat(activities): align types + API service with events v1.6.1
The backend rebase brought in PR #50 (fiat checkout + email/Nostr
ticket notifications), v1.6.0 (custom notification subject/body), and
PR #51 (resend-email endpoint). The webapp types lagged.

Aligns the type surface in src/modules/activities/types/ticket.ts:

- EventExtra (with notification toggles + custom subject/body), promo
  codes, conditional event config.
- ActivityTicketExtra mirroring backend's TicketExtra (nostr_identifier,
  email/nostr notification_sent flags, refund state).
- TicketedEvent + CreateEventRequest gain allow_fiat, fiat_currency, extra.
- TicketPurchaseInvoice extended for fiat: paymentRequest now optional,
  fiatPaymentRequest + fiatProvider + isFiat added. **Closes a latent
  blocker**: a backend response with is_fiat=true would have lost the
  fiat URL during deserialization (silent crash on QR generation).
- New CreateTicketRequest type for the POST /tickets/{id} body, with
  the v1.6.1 payment_method + fiat_provider + nostr_identifier fields.

TicketApiService:
- requestTicket() accepts the new optional fields (paymentMethod,
  fiatProvider, promoCode, refundAddress, nostrIdentifier) and
  deserializes the full TicketPurchaseInvoice shape including fiat.
- fetchUserTickets() / validateTicket() / new resendTicketEmail()
  thread the extra metadata through.

useTicketPurchase composable rejects fiat responses with a clear
error (the QR-and-bolt11 path doesn't know fiat); the eventual UI
selector will live in PurchaseTicketDialog.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 00:38:25 +02:00
6cd420d9cb fix(activities): stamp local tz offset on event datetimes before submit
The form sent naive "YYYY-MM-DDTHH:MM" to the LNbits events backend,
where _to_unix (nostr_publisher.py) assumes UTC when tzinfo is None.
So 08:00 entered in CEST got stored as 08:00 UTC, and the NIP-52 start
tag landed on the relays at the wrong instant — the detail page then
re-localized it to 10:00 (offset doubly applied).

Stamp the wall-clock value with the user's UTC offset before sending so
the backend builds the correct unix and the detail page renders the
intended wall-clock. Seconds (`:00`) included for pre-3.11 Python
fromisoformat compatibility. Round-trips through edit mode unchanged:
splitDateTime trims to "HH:MM" so the suffix drops cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 00:38:25 +02:00
8c09fbdc18 fix(activities): toast on logged-out Create tap instead of opening dialog
BottomNav fires onClick regardless of tab.disabled — the opacity gate
was visual only. Mirror BookmarkButton/RSVPButton: show a toast.info
with a Log in action and bail before opening the create dialog.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 00:38:25 +02:00
cf1740d025 chore(deps): bump nostr-tools to ^2.23.3 to match lnbits
The only breaking surface in webapp code is SimplePool.subscribeMany —
2.23 dropped the Filter[] form: a single subscription now takes one
Filter, and multi-filter REQs go through subscribeMap. RelayHub gets
an internal poolSubscribe() adapter that routes single-filter to
pool.subscribe() and multi-filter to pool.subscribeMap(), preserving
the external RelayHub.subscribe() API so no downstream modules change.

Peer-dep bump (@noble/* and @scure/* → 2.x) is contained: nostr-tools
is the only consumer in the lockfile, so the major version shift
doesn't conflict with anything else.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 00:38:25 +02:00
2bc0b9d57b chore(mcp): add shadcn-vue MCP server alongside playwright 2026-05-24 00:37:16 +02:00
80a213e984 build: add workbox-window as explicit devDependency
Required for pnpm strict-hoisting builds (used by aiolabs/server-deploy
NixOS builds). Without it as a direct dep, Rollup can't resolve
`workbox-window` from vite-plugin-pwa's virtual:pwa-register module —
npm's flat hoisting masked this previously.
2026-05-23 11:23:53 +02:00
d6e0019fca build: switch from npm to pnpm
- Replace package-lock.json with pnpm-lock.yaml
- Add packageManager: pnpm@10.33.0
- Allowlist postinstall scripts for esbuild, sharp, vue-demi, electron,
  electron-winstaller via pnpm.onlyBuiltDependencies
- Pin nostr-tools to 2.15.0 (was ^2.10.4 resolving to 2.15.0 via npm).
  A fresh pnpm resolve drifted to 2.23.5, which the regtest nostrrelay
  extension can't parse; upgrade deferred to a follow-up issue covering
  the matching server-side fix.
2026-05-23 11:23:53 +02:00
b3db5e81ef fix(events): inline-arrow multi-statement @update:open handler
Vue's template expression parser doesn't accept multi-line statement
bodies separated by newlines alone — it errors with 'Unexpected token,
expected ","' as it tries to parse the second statement as a
continuation of the first. Convert to the inline-arrow form already
used elsewhere in the codebase (WalletPage.vue, TaskCard.vue, etc.).
2026-05-21 17:00:06 +02:00
63fc7b3ab8 feat(activities): pending-aware toast + unified pending gate
Rename willGoToPending → willLandInPending, drop the (now-redundant)
isEditMode predicate from the gate so create can reuse it. Toast
copy now confirms the destination explicitly:

  create + pending : "Submitted! Awaiting admin approval — your
                     draft is visible on your feed with a Pending
                     badge."
  edit + pending   : "Updated — awaiting re-approval. Hidden from
                     the public feed until reviewed."

Closes the surprise where a non-admin user under auto_approve=off
got a generic "Event submitted!" and then couldn't tell whether
their post had been accepted or was just waiting.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 16:53:55 +02:00
556b9e5cfe feat(activities): ownership + status badges on cards & detail
ActivityCard now renders a "Yours" badge (top-right corner of the
image) when activity.isMine, and a "Pending review" / "Rejected"
badge (bottom-left) when activity.lnbitsStatus is non-approved. The
creator can spot their own posts at a glance on the main feed —
particularly important for pending drafts that don't exist on
Nostr yet.

ActivityDetailPage echoes both badges next to the category row so
users landing on the detail link of their own draft have the same
signal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 16:53:42 +02:00
dbc8b7abf4 feat(activities): merge own LNbits drafts into the feed
The /activities feed is Nostr-driven, so an event that hasn't been
published yet (typically `proposed` under auto_approve=off) silently
vanishes from the creator's view. Add ticketedEventToActivity (the
LNbits → NIP-52-shape adapter) and call it from useActivities so own
drafts surface alongside Nostr-published activities. Once approved
and published, the relay-sourced Activity has a newer createdAt and
wins on upsert (and lacks lnbitsStatus, so any badge disappears).

Also fire loadOwnEvents from the shell after event-created /
event-updated so a fresh draft shows up immediately, not on the next
subscribe cycle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 16:53:24 +02:00
9b5f1273b3 fix(activities): multi-day time-based events show end date too
ActivityDetailPage.dateDisplay rendered the end of a time-based event
as time-only ("19:00 — 21:45"), implying a single calendar day even
when the event actually spanned multiple days. The end date was
silently lost in the display.

Detect whether start and end share the same calendar day; if not,
repeat the full date on the end side so a multi-day event reads as
"Fri May 29 • 19:00 — Sat May 30 • 21:45".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 16:31:01 +02:00
79be46c33d fix(activities): tighter populate race + log silent feed-fetch fail
CreateEventDialog.populateFromEvent released the watcher guard via
setTimeout(0). Switch to nextTick so the release waits for Vue's
microtask flush — setTimeout(0) only schedules a macrotask, which
can run before vee-validate's batched setValues lands, briefly
unguarding the auto-mirror during populate.

useEvents.fetchAll silently swallowed fetchMyEvents failures so a
flaky probe degraded to "public events only" with no signal in the
console. Add a console.warn so the degradation is debuggable without
toast-spamming users on transient errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 16:13:34 +02:00
e540feba44 refactor(activities): useApprovalState composable
Both the activities-app shell and EventsPage probed isAdmin /
autoApprove with slightly different code, both needed by the edit
dialog's warning copy. Extract into a single composable so the probe
sequence + re-probe-on-auth-flip behavior lives in one place.

Also clear editingEvent eagerly when the bottom-nav Create tab fires
so a Create tap never inherits a stale Edit selection from a prior
close path that didn't run for any reason.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 16:13:22 +02:00
2b376bb244 refactor(base): expose extractFileId, dedupe URL→file-id parsing
Promote ImageUploadService.extractFileId from private to public so
edit-flow consumers don't re-implement the `/image/original/<id>`
parse. Used by market's CreateProductDialog and activities'
CreateEventDialog when re-populating <ImageUpload> from a stored URL.

Also clarify ImageUpload.removeImage: the `delete_token: ''` placeholder
on re-populated images intentionally skips the server-side DELETE.
We don't own the original upload's one-time token, and removing
client-side shouldn't reach back and wipe shared files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 16:13:08 +02:00
b9bca36b50 feat(activities): edit button on activity detail page
The NIP-52 d-tag we publish for an event is the LNbits event id (set
in nostr_publisher.build_nip52_event), so a single fetchMyEvents call
can tell us whether the displayed activity belongs to the caller.
When it does, show an Edit button next to Bookmark; clicking sets
the store's editingEvent and opens the shell-mounted dialog in edit
mode.

This was the missing surface — users land on /activities/:id when
they tap a posting, not on /events.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 16:01:55 +02:00
345ca073af feat(activities-app): wire shell dialog for edit + approval probes
Probe isAdmin / autoApprove once at auth-ready (re-probe on login)
and feed them plus the store's editingEvent into the shell-mounted
CreateEventDialog. Add handleUpdateEvent that picks the right wallet
admin key from the editing event's wallet id.

Without this the Activities standalone app could only Create — the
existing dialog was create-only at shell level even though the
dialog component itself already supported edit mode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 16:01:55 +02:00
a77bf7ff6c feat(activities): editingEvent in activities store
Carry the LNbits event being edited at store level so the
shell-mounted CreateEventDialog (activities-app/App.vue) can open in
edit mode from anywhere a user surfaces their own event — most
importantly the activity detail page, which is where they actually
land when fixing a posting.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 16:01:55 +02:00
9b1b56e05d feat(activities): status badge + buy-disabled on own pending events
Show a "Pending review" (or "Rejected") badge on the user's own
non-approved events, and disable the Buy Ticket button on any
non-approved event with a "Not yet available" label. Probe
auto_approve via the public endpoint with inkey, not adminkey, so the
warning copy works for non-admin owners.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 15:55:19 +02:00
01b871e7fa feat(activities): merge own events into the feed
When authenticated, parallel-fetch the caller's own events (any
status) alongside the public approved feed and merge with public-wins
dedup. Without this, an event that drops to `proposed` after a
non-admin edit disappears from the user's view — they couldn't find
it to make a follow-up edit or watch for re-approval.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 15:55:19 +02:00
3047565920 feat(activities): fetchMyEvents + invoice-key auto_approve probe
`fetchMyEvents` hits the existing all_wallets=true endpoint to surface
the caller's own events regardless of status. `getAutoApprove` now
calls the public probe (invoice-key-gated) added in events extension
v1.3.0-aio.5 so non-admin webapp users get accurate edit-flow copy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 15:55:19 +02:00
af3c9853c0 feat(activities): edit button on user-owned events
Pencil button in the card footer of upcoming events the current user
owns (event.wallet ∈ currentUser.wallets). Clicking opens the same
CreateEventDialog in edit mode, pre-populated with the event.

Probe `is_admin` and `auto_approve` once at mount so the dialog can
render the "going back to pending" warning copy accurately for
non-admin owners.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 12:30:00 +02:00
cd35fae674 feat(activities): dual-mode CreateEventDialog supports edit
Accept optional `event` prop and `onUpdateEvent` handler. Dialog
toggles title, description, submit button text, and a warning Alert
based on edit mode plus an `isAdmin`/`autoApprove` pair the parent
supplies.

On open in edit mode, populate the form from the event — split stored
"YYYY-MM-DD[THH:MM]" back into date+time inputs, restore categories,
and seed bannerImages from the stored URL by extracting the pict-rs
file ID (same pattern as market's CreateProductDialog).

A clearing-the-banner action during edit sends `banner: null` so the
backend wipes the field instead of keeping the old image. Auto-mirror
watcher is guarded against firing during the initial population.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 12:29:49 +02:00
4bea1a6592 feat(activities): TicketApiService.updateEvent + admin/auto_approve probes
`updateEvent` calls PUT /events/{id} with the event's wallet admin key
— mirrors the backend's `require_admin_key` decorator (different key
than the inkey used by createEvent).

Add `isAdmin` and `getAutoApprove` probes so the dialog can decide
whether to show "edit will go back to pending approval" copy. Both
degrade to `false` on failure, which biases the warning toward being
shown when in doubt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 12:29:37 +02:00
e5f0202a4a feat(base): default ImageUpload compression on
Flip ImageUploadOptions.compress so undefined/true → compress with
default knobs, false → skip, object → compress with merged knobs. A
future <ImageUpload> consumer that forgets the prop now gets safe
behavior (small WebP) instead of dumping a 5–8 MB phone photo to
pict-rs.

Drop the now-redundant :compress="true" from the events banner and
market product call sites. Profile keeps its object override since it
tunes maxWidthOrHeight: 512 / maxSizeMB: 0.2 for avatars.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 00:26:12 +02:00
67dbfb16e1 fix(preferences): null-guard DropdownMenuRadioGroup handlers
Reka UI tightened model-value's type to AcceptableValue, which
includes null. The four inline (v: string) => ... handlers in
PreferencesRow.vue no longer satisfied the prop's expected signature,
breaking TS at the standalone-app build step (forum-app, others).

Drop the string annotation, guard the null case, and cast on the
forward call to preserve the intended narrowing.
2026-05-21 00:02:26 +02:00
a815442990 feat(base/profile): compress avatar uploads with tight 512px cap
Pass tuned compress options to <ImageUpload>: 512px max edge / 200 KB
target. Avatars don't need 1920px and the smaller cap meaningfully
reduces pict-rs storage for a high-volume content type. Closes #59
for the profile consumer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 23:48:48 +02:00
c1194dadbb feat(market): client-side compress product image uploads
Pass `:compress="true"` to <ImageUpload> so the up-to-5 product images
get resized to 1920px max edge and re-encoded as WebP before hitting
pict-rs. Closes #59 for the market consumer. Default knobs match the
events banner use case — both are gallery-scale images.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 23:48:48 +02:00
1c35bcb9d1 chore: add .mcp.json and ignore .playwright-mcp/
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 19:48:15 +02:00
012f364a7a feat(activities): themed date/time pickers + end-after-start guard
Swap the native <input type="date|time"> controls in CreateEventDialog
for the shared DatePicker / TimePicker components so the form looks
like the rest of the shadcn UI instead of browser chrome.

While there:

- End date auto-mirrors start date on pick (and re-mirrors if the
  existing end has fallen behind the new start), so a one-day event
  needs no extra clicks.
- Zod superRefine rejects end < start, comparing the folded date+time
  string so equal-date / later-time is enforced too.
- Move End date/time out of the "More options" collapsible into the
  main form flow (drops Collapsible / Separator / ChevronDown / the
  showMoreOptions ref).
- End time label reads "End time (optional)" to make the field's
  status obvious.
- Banner image label is plain markup (not <FormItem>) since it's
  managed via bannerImages ref outside vee-validate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 19:36:52 +02:00
93c05104df feat(base): themed DatePicker + TimePicker for activity forms
Scaffolds shadcn-vue Calendar + adds @internationalized/date, then
wraps them in two small shared components living alongside ImageUpload
for reuse across activity / market / future forms.

DatePicker — Popover + Calendar, bridges the wire format (YYYY-MM-DD
string) to reka-ui's CalendarDate. Closes on pick (the Calendar
primitive doesn't auto-close). Optional min-date for forward-only
selection.

TimePicker — two shadcn <Select> dropdowns (HH 00–23, MM at minuteStep
granularity, default 15). Bound to a single "HH:MM" string externally,
clearable. Mobile-first: a tap opens the native sheet / wheel — no
typeahead overlay (reka-ui's Select doesn't expose typeahead on the
closed trigger and the workaround proved brittle against its internal
document-level key handling).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 19:36:38 +02:00
1727a4cbf0 feat(activities): banner image upload to img.ariege.io
Replace the plain "Image URL" text input on the event-creation form with
the shared <ImageUpload> component, single-file mode with :compress="true".
Files are resized + re-encoded to WebP client-side before hitting pict-rs
so phone-sized posters don't bloat the image server.

The stored `banner` is the canonical pict-rs original URL — the same
shape market uses — so existing display paths (thumbnail/resize URL
builders, NIP-52 "image" tag publishing) need no changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 17:13:05 +02:00
16608e0d60 feat(base): client-side image compression in ImageUploadService
Optional opt-in resize + re-encode of files before the pict-rs POST,
behind a new `compress` option on ImageUploadOptions / `<ImageUpload
:compress>` prop. Pict-rs stores originals at full resolution forever
and `process.webp?resize=…` URLs only shape *delivery* — without
compression, a 5–8 MB phone photo lands on disk untouched.

Defaults when enabled: 1920 px max edge, WebP, q=0.85, target ~1 MB,
Web Worker. Skips compression for files already comfortably under the
size target, and keeps the original if re-encoding would make it larger.

Uses browser-image-compression (MIT, ~50 KB, mature) — handles EXIF
orientation internally (canvas drawImage doesn't auto-rotate, which is
the classic "portrait photo lands sideways" bug we'd otherwise own) and
falls back gracefully on encoder failure (HEIC, etc.).

Default is `compress: false` so existing market and profile call sites
keep current behavior; rollout tracked in #59.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:55:47 +02:00
691f8df830 feat(activities): capture optional start/end time on event creation
Fold a time into the existing event_start_date / event_end_date strings
("2026-05-25" or "2026-05-25T10:00") rather than introducing parallel
fields. Presence of "T" toggles which NIP-52 kind the events-extension
publisher emits (31922 date-only vs 31923 time-based).

CreateEventDialog gets optional HH:MM inputs next to the start date and
the (already-collapsible) end date — stacked below sm breakpoint so the
iPhone SE doesn't get the time pushed off-screen by the native date
input's intrinsic min-width.

EventsPage.formatDate shows the time portion when present.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 01:24:15 +02:00
9ac31de49f fix(activities): reject malformed NIP-52 kind 31922 events at parse time
parseCalendarDateEvent accepted any non-empty `start` string, letting
events with embedded times (e.g. "2026-05-25T10:00") through. Downstream
parseIsoDate then split on "-" and produced an Invalid Date, crashing
the renderer with "RangeError: Invalid time value".

Validate `start` and `end` against YYYY-MM-DD at parse time so bad events
are dropped before reaching the view — symmetric with how the time-event
parser rejects unparseable timestamps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 01:24:01 +02:00
124cad1249 feat(libra/balance): split Net Balance card per direction per currency
When a user has entries in multiple currencies that go in opposite
directions — e.g. an income entry in EUR (user owes the org) and an
expense entry in CAD (org owes user) — the previous Net Balance hero
collapsed both into a single "You owe" / "Owed to you" label driven
by the net sats. The fiat amounts were displayed via Math.abs(),
hiding the per-currency signs the backend already returns, so the
hero was actively misleading: it showed €200 and CA$300 under one
direction when in reality they point in opposite directions.

Render up to two grouped sections — "You owe" with the user-owes
currencies, "Owed to you" with the libra-owes currencies — using new
youOweFiatEntries / libraOwesFiatEntries computeds that filter the
signed fiat_balances dict by sign. Net sats moves to a small caption
labelled "Net at current rates", since sats can be netted but
distinct fiat currencies can't without a spot rate. Falls back to
the old single-amount sats display when there are no fiat balances.
2026-05-17 20:15:01 +02:00
9e3de6ce16 feat(libra/record): permission-gated Add Expense / Add Income buttons
Check the user's permitted accounts on mount; disable the card and
show a lock-icon caption directing them to contact an administrator
when they have no SUBMIT_EXPENSE / SUBMIT_INCOME access. Greys the
icon and badge background when disabled so the card reads inactive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 19:15:08 +02:00
c5d943a991 feat(libra/record): always show Add Income — drop stub-era env-var gate
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 19:05:54 +02:00
bfa5118fbe feat(libra/balance): clarify income/expenses cards with info captions
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 16:14:17 +02:00
30ad4cf512 feat(libra/balance): show lifetime income vs expenses breakdown
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 16:14:17 +02:00
26a89c58dd feat(libra/balance): show fiat balances alongside sats
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 16:14:17 +02:00