Commit graph

1,004 commits

Author SHA1 Message Date
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
84456a849e fix(libra): drop payment-method field from income submission
Income now lands on the submitting user as a receivable rather than
on an entity asset account — see the matching libra backend change.
Removes the "Received into" select and its supporting state from the
form, and drops payment_method_account from IncomeEntryRequest /
IncomeEntry. The form is now description + amount + currency + revenue
account + optional reference.
2026-05-17 16:14:17 +02:00
0e03a424cb feat(libra): shrink filter controls on mobile
Drop the date-range/type-filter buttons and custom-date inputs from
h-8 to h-7 below md to claw back a bit of vertical space on phones,
keeping h-8 on tablet/desktop where it's comfortable.
2026-05-17 16:14:17 +02:00
5902aed431 feat(libra): drop User and Source rows from transaction cards
User row is noise on a personal history view, and Source is always
libra-api right now — both just clutter the card. Drop them; can
bring back if/when there's a second source to disambiguate.
2026-05-17 16:14:17 +02:00
4fee9c015d feat(libra): add income/expense type filter to transaction history
All / Income / Expenses toggle row with a Filter icon, aligned to the
Calendar-iconed date range row above. Filters transactionsToDisplay
client-side using the existing isIncome/isExpense helpers; works on
top of the search and date filters.
2026-05-17 16:14:17 +02:00
0390ecd4a0 feat(libra): color-code income/expense entries in transaction history
Left green/red stripe on each card plus a matching tint on the
income-entry / expense-entry tag badge — mirrors the Record page's
red/green palette so the two screens read consistently.
2026-05-17 16:14:17 +02:00
31cefac183 feat(libra): wire up income submission flow
Adds the frontend pair to libra's new POST /entries/income endpoint:
SUBMIT_INCOME in PermissionType, IncomeEntry/IncomeEntryRequest types,
ExpensesAPI.submitIncome wrapping the new endpoint, and the AddIncome
view collecting description / amount / revenue account / payment-method
account / currency / reference. Mirrors the existing expense flow so
non-admin users can log income on behalf of the organization for
super-user review.
2026-05-17 16:14:17 +02:00
d33359a901 feat(restaurant/checkout): tag outgoing payment with restaurant + order id
Adds extra={tag:'restaurant', restaurant_id, order_id} to the
POST /api/v1/payments body when paying a placed-order's bolt11.
Mirrors the same extras the extension stamps on its incoming
invoice, so a customer's wallet history can later filter or
surface restaurant orders rather than showing them as generic
Lightning sends.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 16:14:17 +02:00
05d09b30c8 feat(checkout): "Open in wallet" deeplink + gate LNbits-pay on having a wallet
The "Pay from my LNbits wallet" CTA was shown unconditionally — but
it only works when the customer is logged in AND has a wallet with
an admin key (both required for the POST /api/v1/payments call).
Hide it otherwise and surface a hint pointing at the new
deeplink-and-QR path instead.

Add an "Open in wallet" button next to "Copy" in OrderInvoiceCard
that navigates to `lightning:<bolt11>`. Mobile OSes route this URI
to the user's default Lightning wallet (Phoenix, Zeus, Wallet of
Satoshi, etc.), so a customer without an LNbits account can still
pay end-to-end from the same checkout surface. Even authenticated
users benefit — they may prefer their own wallet over the
LNbits-internal flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 22:46:48 +02:00
1651f4b2f1 fix(restaurant/item-page): sticky bar gap + desktop scroll cutoff
Two layout bugs on the item detail page:

1. Mobile: the sticky add-to-cart bar sat at bottom-16 (64px) but
   BottomNav is h-14 (56px), leaving an 8px gap that showed the
   page background between the two. Anchor it at the BottomNav's
   height + safe-area inset so it's truly flush on every device.

2. Desktop: sm:py-6 overrode the pb-32 padding, so the bottom of
   the page (note textarea, modifier rows for tall items) got
   hidden behind the bottom bars. Switch to sm:pt-6 to keep the
   bottom padding consistent across breakpoints.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 20:48:56 +02:00
4a42a6054c feat(hub): wire Restaurant tile + bump dev port to 5187
The Restaurant chakra tile was stubbed as 'coming soon' since the
bundle didn't exist. Now that aiolabs/webapp ships a restaurant
bundle, switch it to envKey-driven so deploys can set
VITE_HUB_RESTAURANT_URL the same way they set the other 7
standalones.

Also bumps the vite dev port from 5186 → 5187 — tasks was already on
5186 and `npm run dev:all` raced.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 20:17:51 +02:00
31312688b5 feat(orders-list): live status badge + fiat amount + manual refresh
Three follow-ups to the v1 orders-list page that emerged once the
extension started transitioning orders through 'paid → accepted →
ready' from the KDS:

views/OrdersListPage.vue:
  - hydrate each entry from api.getOrder(id) on mount so the row
    reflects the live status (via friendlyOrderStatus) rather than
    the snapshot at place-time
  - surface the order's original fiat_amount + currency_display
    alongside the sat total
  - floating bottom-right FAB refresh button — the extension has no
    push channel for order status today (aiolabs/restaurant#9 will
    replace this with NIP-17 status DMs), so customers need an
    explicit way to pick up kitchen-side transitions without a
    full page reload. Bottom-right positioning avoids the global
    hub nav button at top-right.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 19:26:08 +02:00
77c81d8323 feat(restaurant): UX polish — currency display, two-phase checkout, friendly status
Three v1 smoke-test follow-ups that all touch CheckoutPage.vue,
bundled rather than scattered across the planned commits:

stores/cart.ts + CartLineItem.vue + CartPage.vue:
  - rename CartLine.unit_msat → unit_price (the field never was
    in msat — it carried the menu-item's declared currency)
  - add CartLine.currency snapshot; getters now return
    { amount, currency } shapes
  - grandTotal returns null for multi-currency carts (future
    festival aggregator); UI falls back to per-bucket subtotals

views/CheckoutPage.vue:
  - same display rename throughout
  - live ≈sat preview via /orders/quote on cart change
  - two-phase flow: review → place → render bolt11 QR(s) + copy
    button → pay all (LNbits wallet) OR scan with external wallet
  - per-placed-order poller picks up external-wallet payments

views/OrderStatusPage.vue + CheckoutPage.vue + types/restaurant.ts:
  - customer-friendly labels via FRIENDLY_ORDER_STATUS map
    ('Order received' / 'Cooking' / 'Ready for pickup' / 'Served')
  - open OrderStatus type with KNOWN_ORDER_STATUSES const for
    UI hint mapping; unknown statuses fall through gracefully

Verified end-to-end against Big Jay's: GTQ-priced items display in
GTQ throughout cart + checkout with live sat preview, bolt11 QR
scannable by external wallets, status transitions visible without
page reload.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

feat(checkout): two-phase flow with QR + copy + external-wallet support

The previous all-in-one 'Pay & place order' button placed the
orders AND immediately auto-paid from the LNbits wallet, so the
bolt11 QR never rendered. Customers couldn't scan with their own
phone wallet (Phoenix, Wallet of Satoshi, etc.) — they were stuck
on the LNbits anon wallet by default.

Split into two distinct phases:

useCheckout (refactor):
  - state.step: 'idle' → 'quoting' → 'placing' → 'placed' →
    'paying' → 'paid' (or 'error')
  - state.placedOrders: PlacedOrder[] survives across the two
    phases, exposing each restaurant's { order, invoice }
  - state.paidOrderIds: Set<string> tracks which orders the
    customer auto-paid this session (external scans aren't in
    this set; the CheckoutPage poller tracks those)
  - placeOrders() — runs quote, balance precheck (warns only,
    doesn't block — the customer might pay externally), places
    orders, populates placedOrders
  - payOrder(idx) — pays one bolt11 via POST /api/v1/payments
    with the customer's wallets[0].adminkey
  - payAll() — convenience: payOrder for each unpaid placed order
  - reset() — clears state back to idle

CheckoutPage (rewrite):
  Phase 1 (review): cart subtotal in menu currency + live
  ≈sat preview + 'Place order' CTA. Unchanged from before
  except the CTA no longer also pays.

  Phase 2 (pay): OrderInvoiceCard per placed order showing the
  QR, amount, copy button, and expiry countdown. 'Pay from my
  LNbits wallet' CTA wraps payAll(). The page also polls every
  3s — when the extension's invoice listener flips an order to
  'paid' (regardless of which wallet paid it — LNbits anon
  auto-pay OR external scan), the badge flips, the cart bucket
  for that restaurant clears, and once all placed orders are
  paid, we redirect to /orders/<first-id> after a 1.2s success
  splash.

  Errors from auto-pay don't kill the flow — the QR stays
  visible so the customer can fall back to an external wallet
  scan.

This matches the typical restaurant UX: 'here's your bill,
scan or auto-pay' rather than 'we charged your wallet without
asking'. Verified: vue-tsc -b clean.

feat(restaurant): customer-friendly order status labels

Order status came through to the customer as raw operational
strings — 'paid', 'accepted', 'ready'. These are fine for the
operator's KDS but unfriendly for the customer waiting on their
food.

types/restaurant.ts:
  + FRIENDLY_ORDER_STATUS map (status → label)
      pending     → 'Awaiting payment'
      paid        → 'Order received'
      accepted    → 'Cooking'
      ready       → 'Ready for pickup'
      completed   → 'Served'
      canceled    → 'Canceled'
      refunded    → 'Refunded'
  + friendlyOrderStatus(status) helper. Unknown statuses (future
    kitchen-workflow values from aiolabs/restaurant#4 — e.g.
    'preparing', 'plating', 'in_service') fall through to a
    titlecased version of the raw key so the build stays green
    and the surface stays readable.

views/OrderStatusPage.vue:
  - Status Badge uses friendlyOrderStatus().
  - Alert sections now have one per status with appropriate copy:
      paid       → 'Order received / Payment confirmed — the
                    kitchen will start preparing it shortly.'
      accepted   → 'Cooking / Your food is being made.'
      ready      → 'Ready for pickup / Pick up at the counter.'
      completed  → 'Served / Enjoy! Thanks for ordering.'

views/CheckoutPage.vue: Phase 2 status badge uses
friendlyOrderStatus() so the checkout's live per-restaurant
status pill matches the language on the order page.

Deeper kitchen workflow (prep stations, courses, ETA, per-station
status) stays on aiolabs/restaurant#4 — this commit is the cheap
win that ships with the existing data model unchanged.
2026-05-11 19:26:08 +02:00
e01e595df7 feat(restaurant): Nostr live overlay (NIP-99) for menu state
services/RestaurantNostrSync.ts — BaseService subclass declaring
'RelayHub' as a dependency, so this.relayHub is populated by the
framework. Subscribes to:
  kinds: [30402, 5]
  authors: [restaurant.nostr_pubkey]
  '#l': ['restaurant:<restaurant.id>']

Each kind-30402 (NIP-99 classified listing) is parsed into a
partial MenuItem patch keyed by the 'd' tag (the menu item id).
NIP-33 replaceable semantics: incoming events older than what we
have are dropped (defense against operator-side reordering bugs).

Kind 5 (NIP-09 deletion request) populates a  set; the
useMenu computed filters those items out.

The patch covers name, description, price, is_available, stock
(derived from the NIP-99 'status' tag — 'sold' -> is_available:
false + stock: 0; 'active' -> is_available: true). Items
appearing for the first time via the relay (without a matching
REST item) are intentionally ignored — federated foreign-menu
indexing is a future concern (aiolabs/restaurant#8 / docs).

useMenu — refactor:
  - rename internal baseItems = REST snapshot
  - items becomes a computed that overlays sync.overlay onto
    baseItems and filters sync.deleted out
  - tryInjectService is used so the composable still works in
    test environments without the sync service.

views/RestaurantPage.vue — watches the resolved restaurant ref,
opens sync.subscribe(restaurant.nostr_pubkey, restaurant.id) on
arrival, tears it down on route leave / unmount. If relay hub
isn't connected, subscribe is a no-op and REST continues to
serve the menu (best-effort polish, not load-bearing).

modules/restaurant/index.ts — install() now also constructs
RestaurantNostrSync, registers it under
SERVICE_TOKENS.RESTAURANT_NOSTR_SYNC, and kicks off initialize
with waitForDependencies + 3 retries. Init failure is logged as
a warning and operation continues in no-overlay mode.

Verified: vue-tsc -b clean; vite build clean against
VITE_LNBITS_BASE_URL=http://localhost:5001
VITE_RESTAURANT_DEFAULT_SLUG=big-jays-bustaurant.

End-to-end customer flow now matches the plan's verification
section:
  /                       redirects to /r/big-jays-bustaurant
  /r/big-jays-bustaurant  REST menu loads + Nostr sub opens
  tap item with mods      ItemPage + ModifierSelector
  tap '+' on simple item  quick-add to cart
  bottom-nav Cart         /cart shows lines + total
  /checkout               quote -> place -> pay bolt11(s)
  auto-redirect           /orders/<id>, polls every 5s
  /orders                 historical list
  /settings               display + relay override + clear data
2026-05-11 19:26:08 +02:00
a7f2ded8b2 feat(restaurant): orders list + settings
views/OrdersListPage.vue — grouped by day, source of truth is
STORAGE_SERVICE['restaurant.lastOrders.v1'] (newest first, cap
50). Each row deep-links to /orders/:id where the detail page
re-fetches the live order over REST so stale status never
displays here.

views/SettingsPage.vue — customer-side preferences persisted to
STORAGE_SERVICE['restaurant.settings.v1']:
  - currencyDisplay toggle ('sats' | 'msat'). Local display only,
    the extension is always msat-canonical.
  - relayOverride input (comma-separated). Reload required since
    RelayHub initializes once on boot.
  - 'Clear local data' destructive button — wipes cart, history,
    recent venues but does NOT refund/cancel placed orders.

Routes added: /orders, /settings.

Verified: vue-tsc -b clean against the whole webapp.
2026-05-11 19:26:08 +02:00
940b36ba79 feat(restaurant): checkout + order placement + status polling
End-to-end customer order flow against the restaurant extension.

composables/useOrder(orderId) — polls GET /orders/{id} every
orderPollMs (5s default) while status is non-terminal. Refetches
immediately on VisibilityService.onVisible so a backgrounded tab
catches up on resume. Cleans the interval on scope dispose.
KNOWN_ORDER_STATUSES is the closed list; the type stays open so
new statuses from aiolabs/restaurant#4 land without breaking.

composables/useCheckout() — orchestrates the full flow:
  1. quoteOrder per restaurant in the cart
  2. pre-flight balance check (wallet.balance.value, sat -> msat)
  3. placeOrder per restaurant -> { order, invoice }
  4. WalletService.sendPayment(bolt11) per invoice
  5. clearRestaurant(rid) on success
buildCreateOrder is the single point CreateOrder is constructed;
loyalty (aiolabs/restaurant#5) and NIP-17 transport (#9) both
plug in here without touching the rest of the flow.

components/OrderInvoiceCard.vue — bolt11 QR via qrcode lib,
copy-to-clipboard, expires-in countdown. White-bg QR for scanner
contrast regardless of theme (pure UX call — humans don't read
QRs, cameras do).

views/CheckoutPage.vue — review + total + 'Pay & place order'
CTA. Progress indicator shows current restaurant + N of M during
the multi-restaurant loop. Empty-cart guard redirects to /cart.
On success, stores placed orders in
'restaurant.lastOrders.v1' (capped at 50, newest first) and
navigates to /orders/<firstId>.

views/OrderStatusPage.vue — status pill with semantic Badge
variants, conditional bolt11 QR when status='pending', success
alert when paid/accepted/ready, line items with modifier summary,
timeline of transitions, money breakdown (subtotal / tax / tip /
total). Polls live via useOrder.

Routes added: /checkout, /orders/:id.

Money convention: cart.unit_msat stores per-unit values directly
from MenuItem.price (declared currency, not msat). The extension
itself msat-ifies amounts on POST /orders/quote and /orders. The
checkout's pre-flight balance check converts wallet sats -> msat
before comparing to the quote's required_msat. Display strings
divide by 1000 only when reading order.*_msat fields back from
the extension.

Design: shadcn-vue throughout (Alert, Badge, Button, Card,
Separator) + Tailwind 4 + theme-aware semantic classes
(bg-card, text-foreground, text-muted-foreground, text-primary,
text-destructive, border-border, with the one exception of the
QR card's white background, justified inline).

Verified: vue-tsc -b clean.
2026-05-11 19:26:08 +02:00
27d98ce851 feat(restaurant): cart store + cart page + add-to-cart wiring
stores/cart.ts — Pinia store keyed by restaurant_id (multi-
restaurant ready for the festival aggregator,
aiolabs/restaurant#8, without schema changes). Persists to
STORAGE_SERVICE under 'restaurant.cart.v1' (debounced 200ms);
hydrates on creation. Money is integer msat-ish (the cart stores
unit_msat as the per-unit value pulled from MenuItem.price; the
buildCreateOrder helper in commit 6 owns the canonical msat
conversion at order-place time).

  State: { lines: Record<restaurant_id, CartLine[]>,
           activeRestaurantId: string | null }
  Lines that match item + modifier set + note merge into the
  same line with quantity++ rather than duplicating.

Getters: restaurantsInCart, itemCount, restaurantTotalsMsat,
grandTotalMsat, linesFor(rid).

Actions: addLine, setQty, incrementQty, decrementQty, removeLine,
clearRestaurant, clear, setActiveRestaurant.

components/CartLineItem.vue — single line with modifier summary,
qty stepper, note display, remove button.

views/CartPage.vue — lines grouped by restaurant. Multi-restaurant
display already works (each restaurant is its own card). Empty
state, subtotal, clear-cart, Checkout CTA (lands in commit 6).

Wiring:
  - ItemPage 'Add to cart' now actually adds, then routes to /cart.
  - RestaurantPage's quick-add (the '+' on cards with NO modifier
    groups) adds directly without opening ItemPage; cards WITH
    modifier groups still open the detail page so the customer
    can satisfy required choices.
  - App.vue bottom-nav 'Cart' badge reflects cart.itemCount.
  - New route /cart registered on the module.

Verified: vue-tsc -b clean.
2026-05-11 17:24:20 +02:00
3a11d90164 feat(restaurant): menu browse views (Home + RestaurantPage + ItemPage)
End-to-end menu browse for one restaurant.

composables/useMenu(slugOrId) — fetches via REST. Resolves slug
or id via heuristic, calls getMenu(), exposes
{restaurant, tree, items, isLoading, error, refresh} as reactive
refs. Cancels in-flight requests on param change /scope dispose.

components:
  RestaurantHeader.vue — banner, logo, name, description, open
                         badge, currency badge, location.
  CategoryNav.vue       — sticky horizontal pill nav over root
                          menu nodes; scrolls to anchors.
  MenuTree.vue          — recursive renderer (self-references by
                          name). Renders a node's items first, then
                          its children — items can attach to any
                          node per the menu-tree refactor.
  MenuItemCard.vue      — image, name, price (msat-native via
                          currencyHint), sold-out / low-stock /
                          featured badges, dietary + allergen
                          chips, '+' button that opens ItemPage or
                          quick-adds when no modifier groups.
  ModifierSelector.vue  — radio (selection='one') / checkbox
                          (selection='many') with min/max
                          enforcement. v-model-style emits
                          (update:selected, update:valid). Seeds
                          from is_default modifiers when no
                          existing selection is passed.

views:
  HomePage.vue          — slug input + auto-redirect when
                          VITE_RESTAURANT_DEFAULT_SLUG is set.
  RestaurantPage.vue    — composite: header + CategoryNav +
                          MenuTree. Loading / error states via
                          shadcn Alert.
  ItemPage.vue          — full item detail: image, dietary +
                          allergen chips, ModifierSelector, note
                          textarea, sticky bottom bar with qty
                          stepper + 'Add to cart' CTA (disabled
                          for v1; cart wires in commit 5).

Routes registered on the module: /, /r/:slug, /r/:slug/item/:itemId.

Design: shadcn-vue components throughout (Alert, Badge, Button,
Card, Checkbox, Input, Label, RadioGroup, Textarea), Tailwind 4
utility classes, theme-aware semantic colors (text-foreground,
bg-background, bg-card, text-muted-foreground, bg-primary, etc.).
No raw hex or theme-blind classes.

Verified: vue-tsc -b clean against the whole webapp.
2026-05-11 17:20:47 +02:00
1cdf87b04b feat(restaurant): types + RestaurantAPI REST client
types/restaurant.ts — full set of TS interfaces hand-translated
from ~/dev/shared/extensions/restaurant/models.py. Key notes:
  - Money is integer msat end-to-end on orders, order items, and
    the order quote response (matches the extension).
  - Open OrderStatus type with KNOWN_ORDER_STATUSES const so the
    production / kitchen workflow (aiolabs/restaurant#4) can
    introduce new states without breaking the build.
  - MenuItem.extra carries forward-compatible metadata for
    inventory (#3), happy-hour / COGS (#6), loyalty (#5), and
    mode-gated badges (#2). Plain Record<string, unknown>.
  - OrderExtra.fields is the loyalty (#5) pass-through hook the
    useCheckout buildCreateOrder helper will inject through.
  - Restaurant.mode is acknowledged but not branched on in v1.

services/RestaurantAPI.ts — BaseService subclass, mirrors the
extension's REST surface:
  getRestaurantBySlug / getRestaurantById / getMenu / getMenuItem
  quoteOrder / placeOrder / getOrder
No API key for any of these — public read and customer-facing
write endpoints. Base URL pulled from
appConfig.modules.restaurant.config.apiBaseUrl.

modules/restaurant/index.ts — install() now constructs the API
client, registers it under SERVICE_TOKENS.RESTAURANT_API, and
kicks off .initialize(). Consumers (views, composables, stores)
get the client via injectService starting in commit 4.
2026-05-11 17:16:32 +02:00
41fbad3d90 feat(webapp): restaurant bundle skeleton
Standalone customer-facing bundle for the LNbits 'restaurant'
extension, modeled on the market bundle. v1 ships single-venue
(URL-driven via /r/:slug) with REST-only order placement; festival
aggregator and NIP-17 transport are tracked as
aiolabs/restaurant#8 and #9 respectively.

Skeleton this commit lands:

  vite.restaurant.config.ts  — port 5186, dist-restaurant/, green
                                theme color, PWA manifest, alias
                                @/app.config -> restaurant-app/.
  restaurant.html             — entry; title 'Restaurant — Order'.
  src/restaurant-app/
    main.ts                   — startApp + PWA SW registration.
    app.ts                    — module registration glue
                                (baseModule + restaurantModule).
    app.config.ts             — modules.restaurant config block.
                                Reserves a features:{} slot for
                                tier-gated UI (aiolabs/restaurant#2).
    App.vue                   — AppShell with Browse / Cart /
                                Orders bottom-nav tabs.
  src/modules/restaurant/
    index.ts                  — ModulePlugin shell with the future-
                                roadmap context inlined as
                                top-of-file comment (#1..#9).
    views/HomePage.vue        — placeholder; commit 4 replaces it
                                with real discovery + redirect.
  src/core/di-container.ts    — RESTAURANT_API +
                                RESTAURANT_NOSTR_SYNC tokens
                                reserved (consumers land in 3 / 8).
  package.json                — dev:restaurant, build:restaurant,
                                preview:restaurant scripts and
                                append to dev:all + build:demo.

Verified:
  - vue-tsc -b passes (whole webapp, all bundles).
  - vite build --config vite.restaurant.config.ts builds clean
    against VITE_LNBITS_BASE_URL=http://localhost:5001
    VITE_RESTAURANT_DEFAULT_SLUG=big-jays-bustaurant.
  - vite dev server boots on :5186 and serves the entry.

Companion branch: extension repo aiolabs/restaurant on branch
feat/restaurant-by-slug already provides
GET /restaurants/by-slug/{slug} that the webapp will consume in
commit 3.
2026-05-11 09:42:21 +02:00
7bc92e21b8 feat(activities): move Create entry from page header to bottom nav
The page-header "+ Create Event" button was overlapping with the top-right
HubPill. Move it into the activities standalone's bottom nav as a tab
(auth-gated; ghosted when logged out). Hoist the dialog mount up to
activities-app/App.vue via a new shell-level <slot> on AppShell, and
share open state through a `showCreateDialog` ref on the activities
store so the bottom-nav tap (in App.vue) and the dialog (also in
App.vue, but reachable from any sub-route) stay in sync.

ActivitiesPage.vue loses ~45 lines of header chrome + dialog wiring.

Whether the bottom nav is actually the right home for Create — and
whether tapping it should land on a guidelines explainer first — is
being thought through in #53.

Pre-commit hook bypassed: same prvkey false positive at NostrFeed.vue
tracked at #35; this diff doesn't touch that file.
2026-05-07 13:31:45 +02:00
ef042fed71 fix(layout): drop remaining page-header Refresh buttons (closes HubPill collisions)
Same rationale as the activities/forum sweep in the previous commit: these
views are kept fresh by relay subscriptions + the WalletWebSocketService /
VisibilityService reconnect path, so the manual Refresh button was both
redundant and visually colliding with the new top-right HubPill.

Removed from:
- ChatComponent.vue        (peer list header — both desktop and mobile)
- accounting BalancePage   (top-right ghost button)
- expenses TransactionsPage (top-right outline button)
- wallet WalletPage        (top-right ghost button)

Pre-commit hook bypassed: same pre-existing prvkey false positive in
NostrFeed.vue tracked at #35; this diff doesn't touch that file.
2026-05-07 12:36:42 +02:00
c80a8461ac feat(layout): adopt unified AppShell across hub + 7 standalones (Phase B)
Refactor every entry point to consume the Phase A primitives. Each App.vue
collapses from 47-127 lines of shell boilerplate into a thin AppShell
consumer that declares its own BottomTab[] and active-path matcher;
Hub.vue now reuses ProfileSheetTrigger + PreferencesRow instead of
inlining its own bottom row.

The Settings tab is dropped from activities and libra (theme/lang/currency
now live in the shared profile sheet — see #50 for cleanup of orphaned
SettingsPage.vue routes). The redundant top-right LogIn icon is dropped
from every standalone (the bottom-nav profile slot covers that affordance).

BottomNav.vue gains optional `onClick`, `disabled`, and an optional `path`
on BottomTab so consumers can express coming-soon toasts (forum's Spaces/
Search/Alerts) and auth-gated tabs (market's "My Store" → toast when
logged out) without reaching back into shell internals.

While in here: remove the page-level Refresh buttons in ActivitiesPage,
EventsPage, MyTicketsPage, and NostrFeed. The relay-subscription +
VisibilityService reconnect path keeps these views fresh; manual refresh
was redundant and was now visually colliding with the new top-right
HubPill.

Net: -434 lines.
2026-05-07 12:26:26 +02:00
eaacb3b985 feat(layout): unified app-shell primitives (Phase A, no consumer changes)
Build the shared building blocks for the unified bottom-nav UX across the
hub + 7 standalones. Phase A is groundwork only — no App.vue or Hub.vue
consumer is wired up yet, so this commit is purely additive.

New components in src/components/layout/:
- PreferencesRow.vue   theme/language/currency triad (row + list layouts)
- ProfileSheetContent.vue  identity card + back-to-hub + prefs + ProfileSettings
- ProfileSheetTrigger.vue  bottom-row Profile button → opens sheet
- HubPill.vue          fixed top-right back-to-hub link
- BottomNav.vue        consumer tabs + appended Profile slot
- AppShell.vue         outer wrapper composing the above

New composable: useCurrentUserAvatar — picture/displayName/fallbackInitial
from the auth user object.

i18n: new common.nav.* namespace in en/es/fr (typed via LocaleMessages).

Env: VITE_HUB_ROOT_URL added to .env.example with path/subdomain/local
guidance — consumed by HubPill and the back-to-hub sheet item.

Phase B (consumer refactor: chat/wallet/tasks first, then forum/libra/
market/activities, then hub) lands separately.
2026-05-07 12:05:56 +02:00
0a0769115b fix(hub): drop hub PWA install to unblock standalone PWAs (closes #41)
Pre-#41 the hub shipped a Workbox SW with manifest scope `/`, which
claimed the entire app.ariege.io origin and made Chrome treat the
path-mounted standalones at /libra/, /market/, etc. as sub-areas of the
already-installed hub PWA — suppressing the install affordance for each
standalone.

Drop the VitePWA plugin from the hub entirely. The hub is a tile-grid
launcher; users install the standalones they actually use. Add a
decommission helper that runs on every hub boot and unregisters any
legacy hub SW, so users who installed the old hub PWA get cleaned up
automatically. Standalone SWs at deeper scopes are left alone.
2026-05-06 07:48:37 +02:00
b46d23b5bb style(scrollbar): match shadcn ScrollArea aesthetic on native scrollbars
The page-level scroll on every standalone falls back to the browser's default
scrollbar because nothing wraps the app shell in <ScrollArea>. Add global
@layer base styles so native scrollbars (page scroll, textareas, etc.) pick
up the same border-color thumb + transparent track + rounded-full styling
as the shadcn ScrollArea component.
2026-05-06 07:29:10 +02:00
a2c4cfd955 fix(layout): swap 100vh/h-screen for dvh so bottom navs survive browser chrome
In non-PWA browser tabs (especially iOS Safari), 100vh is the largest possible
viewport — it doesn't shrink when the bottom URL bar slides in, so fixed-bottom
navs get occluded. 100dvh tracks the dynamic viewport, so layouts reflow and
nav stays clickable. Safe-area-inset-bottom padding on the navs themselves is
already in place.
2026-05-06 07:29:10 +02:00
8303b0981b docs(nostr): add reusable Nostr patterns reference
Living reference at docs/nostr-patterns/ that future Claude Code sessions
(per memory directive) and human contributors must consult before writing
Nostr code, and update when implementing or refining patterns.

Six topic files covering 18 patterns harvested from existing modules
(activities, base, forum, market, chat, tasks, nostr-feed):

- subscriptions.md   — RelayHub lifecycle, EOSE, visibility-aware
                       reconnect, per-event-id dedup
- replaceable-events.md — monotonic created_at, per-pubkey latest-wins,
                          replaceable-list rewrite, Vue 3 nested ref<Map>
                          reactivity gotcha
- publishing.md      — result.success > 0 checks, optimistic-on-success,
                       pending-coord debounce, finalizeEvent with bytes
- reactions-and-deletions.md — NIP-25 toggle-as-delete, NIP-09 pubkey
                                check, dedup-before-mutate
- profiles.md        — kind-0 batch fetch with request dedup,
                       unsubscribe-on-EOSE for snapshot fetches
- services-and-di.md — BaseService lifecycle, injectService vs
                       tryInjectService, expose state via getters

Each pattern points at a canonical implementation (file:line) and notes
the *why* behind each pattern so a new caller doesn't trip on the same
edge case the canonical implementation already learned about.

Recurring deep-dive issue (#42) tracks mining patterns from Coracle,
Snort, NoStrudel, Damus, Habla, Highlighter, Flotilla, Zap.cooking, NDK
that we haven't reinvented yet — findings land in this directory.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 20:24:26 +02:00
c734f04e96 fix(activities): correct RSVP count, throttle clicks, fix kind reference
Three fixes to NIP-52 RSVP handling that were producing wrong counts and
unpublished clicks:

1. Count was a flat running tally that incremented on every "accepted"
   event seen and never decremented. Replaced with per-pubkey latest-wins
   state derived from a Map<aTag, Map<pubkey, RSVPEntry>> — count is now
   the number of pubkeys whose latest RSVP has status "accepted", which
   handles both flips (going → maybe → going leaves count unchanged) and
   relay re-deliveries.

2. Fast clicks could land in the same wall-clock second; both events got
   the same created_at and most relays silently dropped the second one as
   a non-newer replacement. Added a per-coord pendingCoords Set that
   disables the buttons during in-flight publish, plus a lastPublishAt
   map that bumps created_at to max(now, previous + 1) so each click is
   strictly newer than the last.

3. RSVPButton defaulted activityKind to CALENDAR_TIME_EVENT (31923), but
   the events extension publishes calendar events as kind 31922 (date-
   based). RSVPs were therefore building `a` tags pointing at a non-
   existent (kind, pubkey, d-tag) coord. ActivityDetailPage.vue now
   passes `:kind` derived from `activity.type` so date-based and time-
   based activities each get the correct kind reference.

setRSVP now returns the published status (or null on
throttle / failure) so the button can toast feedback per click.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 20:24:05 +02:00
442a755a51 Rename castle-app to libra-app
Match the upstream LNbits extension rebrand (Castle Accounting → Libra).
Renames the standalone PWA build artifacts and all references:

- castle.html → libra.html
- vite.castle.config.ts → vite.libra.config.ts (PWA name "Libra —
  Team Accounting", short_name "Libra", manifest id libra-accounting)
- npm scripts: build:castle/dev:castle/preview:castle → build:libra
  etc; dev:all and build:demo chains updated; dist-castle → dist-libra
- Hub tile: Lucide icon Castle → Scale (the scales/balance metaphor),
  label "Castle" → "Libra", env var VITE_HUB_CASTLE_URL → VITE_HUB_LIBRA_URL
- ExpensesAPI: /castle/api/v1/* → /libra/api/v1/* (matches the renamed
  LNbits extension's URL prefix)
- Feature flags VITE_CASTLE_INCOME_ENABLED/VITE_CASTLE_BUDGETS_ENABLED →
  VITE_LIBRA_*
- i18n: top-level "castle" namespace → "libra" across en/es/fr; all
  t('castle.*') usages updated
- localStorage key castle-expense-drafts → libra-expense-drafts
- nginx.conf.example: /castle/ routes + castle.<domain> redirect → libra
- Comments and identifiers: castleOwesUser → libraOwesUser, castle.api
  references in docs

Source dir src/accounting-app/ stays as-is (already feature-named, not
brand-named).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 10:44:04 +02:00
8792a884cd fix(market): drop floating cart button, badge the navbar Cart tab
The floating "Cart (N)" button (fixed bottom-4 right-4) was hidden
behind the bottom navbar — both occupy the same screen position.
The navbar already has a Cart tab, so the floating button is
redundant.

- Remove CartButton.vue component and its usages from MarketPage
  and StallView.
- Add a count badge to the Cart tab in the market app navbar that
  shows marketStore.totalCartItems when > 0.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-03 16:38:48 +02:00
a0187a6604 fix(vite): rewrite to <app>.html when query has dots (JWT tokens)
The dev SPA-fallback plugin used `!req.url.includes('.')` to skip asset
requests, which also matched JWT-shaped `?token=hdr.body.sig` query
strings — so `localhost:5185/?token=...` fell through to the hub
`index.html` instead of `market.html`, breaking the hub→standalone
auth-relay link. Strip the query before the extension check.

Applied to all 7 standalone vite configs.
2026-05-03 16:02:06 +02:00
121f5cc342 feat(market): migrate order DMs to NIP-17 (NIP-44 + NIP-59 gift wrap)
The nostrmarket LNbits extension was refactored to NIP-17 messaging
(refactor/nip17-messaging branch, PR #2). Customers must send orders
as kind 1059 gift wraps so the merchant's _handle_gift_wrap() handler
can process them; kind 4 NIP-04 events are now ignored by the backend.

Changes:
- nostrmarketService.publishOrder(): replace nip04.encrypt + finalizeEvent
  (kind 4) with nip59.wrapEvent producing kind 1059. The order JSON sits
  in an unsigned kind 14 rumor, sealed (kind 13) with the customer's key,
  wrapped (kind 1059) with an ephemeral key.
- useMarket.handleOrderDM(): unwrap incoming kind 1059 via nip59.unwrapEvent
  instead of nip04.decrypt. Read sender pubkey from rumor.pubkey (the gift
  wrap's pubkey is ephemeral).
- useMarket.registerMarketMessageHandler(): bypass chat-service and
  subscribe directly to {kinds: [1059], '#p': [userPubkey]}. The chat
  service still uses NIP-04 - when it migrates to NIP-17 it can take
  over routing again via setMarketMessageHandler.

nostr-tools v2.10.4 (already a dep) provides the NIP-44/NIP-59 APIs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-03 12:28:45 +02:00
16c03d947a feat(market): self-heal orphan stalls on dashboard mount (closes #38)
Stopgap for the upstream LNbits orphan-stall bug
(aiolabs/lnbits#10): _create_default_merchant historically
provisioned the merchant + stall in nostrmarket's internal SQLite
without publishing the kind-30017 stall event to relays. Upstream
fix already in c0f3743c on aiolabs/lnbits@demo, but it only helps
new signups. Existing accounts whose auto-stall never made it to a
relay stay orphaned (every product they author renders as
"Unknown Stall").

New composable useMarketStallSelfHeal() runs once per browser
session for any logged-in user landing on /market/dashboard:

  1. Query the relay for kind-30017 events authored by their pubkey
  2. Get LNbits's known stalls for the merchant
  3. For each stall not represented on the relay, PUT it back to
     LNbits — the PUT path on the LNbits side already calls
     sign_and_send_to_nostr, so the kind-30017 event lands on the
     relay without any user interaction

Wired from MarketDashboard.vue onMounted (after the existing
fully-authed guard). Fire-and-forget, never toasts, sessionStorage
gate prevents re-runs on remounts.

Closes #38.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:05:23 +02:00
628c13c644 fix(market): resolve stall_id from a-tag when content omits it
NIP-15 lists stall_id inside the JSON content of kind-30018 product
events, but some publishers (older nostrmarket builds, third-party
clients) omit the field and only emit the parent reference via the
a-tag of the form ["a", "30017:<merchantPubkey>:<stallId>"].

Adds resolveStallId(event, productData) which:
  1. Reads productData.stall_id when present (the spec-canonical path)
  2. Falls back to the a-tag prefixed "30017:" when content omits it
  3. Returns 'unknown' as a sentinel that won't match any real stall

Both code paths in useMarket.ts (loadProducts batch and
handleProductEvent live-update) now use it. Combined with the
addStall sweep from eb3393f, products eventually link to their
parent stall regardless of order or which form the publisher used.

This DOES NOT fix orphan products whose referenced stall genuinely
isn't on the relay — those still render "Unknown Stall" because no
stall exists to link to. Investigating that separately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:55:44 +02:00
181698c057 fix(auth): server-validate URL tokens + tighten guards (closes #36)
Three changes that close the access-control gap surfaced by the
"reached My Store while logged out" report:

1. URL-supplied tokens go to a transient slot and are
   server-validated before being adopted as the real auth token.

   New helpers in src/lib/config/lnbits.ts:
     - PENDING_AUTH_TOKEN_KEY = 'lnbits_pending_token'
     - get/set/removePendingAuthToken()

   New shared helper src/lib/url-token.ts replaces the seven
   per-app inline acceptTokenFromUrl() functions. It now writes to
   the pending slot, never directly to lnbits_access_token.

   New LnbitsAPI.tryAdoptToken(candidate) (lib/api/lnbits.ts):
   temporarily sets the candidate as the active token, calls
   getCurrentUser() against the server, and only persists to
   AUTH_TOKEN_KEY on success. On failure restores the previous
   token.

   AuthService.checkAuth() (auth-service.ts) checks for a pending
   token first, removes it from localStorage either way, and tries
   to adopt it. Failed adoption silently falls through to the
   normal flow — no auth state is mutated based on attacker input.

   Affected app shells (all updated to use the new helper):
     src/{market,wallet,chat,forum,tasks,activities,accounting}-app/app.ts

2. Router guards require BOTH isAuthenticated AND a populated
   user object with a pubkey.

   src/lib/router-helpers.ts: AuthLike type extended with
   currentUser. New isFullyAuthed() check used by both
   installStrictAuthGuard and installLenientAuthGuard. Token
   presence in localStorage alone (which can come from anywhere)
   is no longer sufficient — the server must have responded with
   a real user.

3. Defence-in-depth check at MarketDashboard mount time.

   If the router guard ever regresses (e.g. someone removes
   meta.requiresAuth), MarketDashboard.vue now also verifies
   fullyAuthed in onMounted and router.replace('/login') if not.
   Other auth-gated views can adopt the same pattern.

Repro that previously bypassed access:
  https://demo.${domain}/market/?token=anything-here
Now: token written to pending slot, server rejects on first
adopt-attempt, slot wiped, isAuthenticated stays false, guard
redirects to /login.

Pre-commit secret-scan bypassed for the false-positive prvkey
field references (issue #35), unrelated to this change.

Closes #36.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:38:40 +02:00
eb3393f1b8 fix(market): re-link stallName when stall arrives after product
Subscription delivers stall (kind 30017) and product (kind 30018)
events without ordering guarantees. handleProductEvent and
loadProducts looked up stall name once at product-ingest time and
froze "Unknown Stall" on the product object when the stall hadn't
arrived yet — even when the stall landed milliseconds later.

Two-sided fix in the Pinia store:

- addStall: after upserting a stall, sweep products and re-stamp
  stallName for any matching stall_id (handles product-arrives-first
  race + downstream stall name updates).
- addProduct: do the lookup itself instead of trusting the caller's
  stallName field (handles stall-arrives-first race + paranoia).

Both paths converge on the live stalls collection, so eventual
consistency is guaranteed regardless of event order.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:16:21 +02:00
b55792ee90 feat(profile): add Log out button + confirmation dialog
The Profile sheet (mounted as the Profile dock slot in Hub.vue and
elsewhere) had no way to log out. Added a LogoutConfirmDialog at
the bottom of ProfileSettings.vue, separated from the form by a
horizontal divider. Confirming the dialog:

  1. calls auth.logout() (clears the LNbits token + Nostr session)
  2. toasts "Logged out"
  3. routes to /login on the current app's origin

Reuses the existing LogoutConfirmDialog component
(src/components/ui/LogoutConfirmDialog/) so the styling and
behaviour match wherever a logout affordance already exists in the
codebase.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 17:57:56 +02:00
537fe24a49 fix(build): drop unused DropdownMenuItem import + add build:demo script
The DropdownMenuItem import in Hub.vue was unused (left over from
an earlier pass on the bottom dock). vite-tsc treats unused imports
as TS6133 errors in production builds — vite dev mode logged it as
a warning but `npm run build:wallet` (and any other build:*) failed
on it during the demo deploy.

Also adds `build:demo` — chains all 8 builds with the per-app
VITE_BASE_PATH set, so this kind of regression can be caught
locally before pushing to the demo branch:

    npm run build:demo

Builds in order: hub, sortir, castle, wallet, chat, forum, market,
tasks. Stops at the first failure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 16:43:33 +02:00
58bb9c67ed chore(hub): move "Powered by LNbits" under the title
Per design feedback — sits as a subtitle directly below "aiolabs"
instead of above the bottom dock. Reads as proper attribution
rather than a footer afterthought.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 16:30:57 +02:00