diff --git a/package.json b/package.json
index d7db78c..89785e6 100644
--- a/package.json
+++ b/package.json
@@ -30,11 +30,8 @@
"dev:forum": "vite --host --config vite.forum.config.ts",
"build:forum": "vue-tsc -b && vite build --config vite.forum.config.ts",
"preview:forum": "vite preview --host --config vite.forum.config.ts",
- "dev:restaurant": "vite --host --config vite.restaurant.config.ts",
- "build:restaurant": "vue-tsc -b && vite build --config vite.restaurant.config.ts",
- "preview:restaurant": "vite preview --host --config vite.restaurant.config.ts",
- "dev:all": "concurrently -n hub,libra,sortir,wallet,chat,forum,market,tasks,restaurant -c blue,magenta,cyan,yellow,green,blue,red,gray,green \"npm:dev\" \"npm:dev:libra\" \"npm:dev:activities\" \"npm:dev:wallet\" \"npm:dev:chat\" \"npm:dev:forum\" \"npm:dev:market\" \"npm:dev:tasks\" \"npm:dev:restaurant\"",
- "build:demo": "npm run build && VITE_BASE_PATH=/sortir/ npm run build:activities && VITE_BASE_PATH=/libra/ npm run build:libra && VITE_BASE_PATH=/wallet/ npm run build:wallet && VITE_BASE_PATH=/chat/ npm run build:chat && VITE_BASE_PATH=/forum/ npm run build:forum && VITE_BASE_PATH=/market/ npm run build:market && VITE_BASE_PATH=/tasks/ npm run build:tasks && VITE_BASE_PATH=/restaurant/ npm run build:restaurant",
+ "dev:all": "concurrently -n hub,libra,sortir,wallet,chat,forum,market,tasks -c blue,magenta,cyan,yellow,green,blue,red,gray \"npm:dev\" \"npm:dev:libra\" \"npm:dev:activities\" \"npm:dev:wallet\" \"npm:dev:chat\" \"npm:dev:forum\" \"npm:dev:market\" \"npm:dev:tasks\"",
+ "build:demo": "npm run build && VITE_BASE_PATH=/sortir/ npm run build:activities && VITE_BASE_PATH=/libra/ npm run build:libra && VITE_BASE_PATH=/wallet/ npm run build:wallet && VITE_BASE_PATH=/chat/ npm run build:chat && VITE_BASE_PATH=/forum/ npm run build:forum && VITE_BASE_PATH=/market/ npm run build:market && VITE_BASE_PATH=/tasks/ npm run build:tasks",
"electron:dev": "concurrently \"vite --host\" \"electron-forge start\"",
"electron:build": "vue-tsc -b && vite build && electron-builder",
"electron:package": "electron-builder",
diff --git a/restaurant.html b/restaurant.html
deleted file mode 100644
index e910921..0000000
--- a/restaurant.html
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
- Restaurant — Order
-
-
-
-
-
-
-
-
diff --git a/src/core/di-container.ts b/src/core/di-container.ts
index 15aa084..411ebad 100644
--- a/src/core/di-container.ts
+++ b/src/core/di-container.ts
@@ -173,10 +173,6 @@ export const SERVICE_TOKENS = {
// Expenses services
EXPENSES_API: Symbol('expensesAPI'),
-
- // Restaurant services
- RESTAURANT_API: Symbol('restaurantAPI'),
- RESTAURANT_NOSTR_SYNC: Symbol('restaurantNostrSync'),
} as const
// Type-safe injection helpers
diff --git a/src/modules/restaurant/components/CartLineItem.vue b/src/modules/restaurant/components/CartLineItem.vue
deleted file mode 100644
index cd27b60..0000000
--- a/src/modules/restaurant/components/CartLineItem.vue
+++ /dev/null
@@ -1,94 +0,0 @@
-
-
-
-
-
-
-
- {{ line.name }}
-
-
- {{ lineTotal }}
-
-
-
- {{ modifierSummary }}
-
-
- Note: {{ line.note }}
-
-
-
-
-
- {{ line.quantity }}
-
-
-
-
-
-
-
-
diff --git a/src/modules/restaurant/components/CategoryNav.vue b/src/modules/restaurant/components/CategoryNav.vue
deleted file mode 100644
index 21b1dff..0000000
--- a/src/modules/restaurant/components/CategoryNav.vue
+++ /dev/null
@@ -1,59 +0,0 @@
-
-
-
-
-
-
-
diff --git a/src/modules/restaurant/components/MenuItemCard.vue b/src/modules/restaurant/components/MenuItemCard.vue
deleted file mode 100644
index 4f00537..0000000
--- a/src/modules/restaurant/components/MenuItemCard.vue
+++ /dev/null
@@ -1,149 +0,0 @@
-
-
-
-
-
diff --git a/src/modules/restaurant/components/MenuTree.vue b/src/modules/restaurant/components/MenuTree.vue
deleted file mode 100644
index d53885a..0000000
--- a/src/modules/restaurant/components/MenuTree.vue
+++ /dev/null
@@ -1,82 +0,0 @@
-
-
-
-
-
diff --git a/src/modules/restaurant/components/ModifierSelector.vue b/src/modules/restaurant/components/ModifierSelector.vue
deleted file mode 100644
index b556b3d..0000000
--- a/src/modules/restaurant/components/ModifierSelector.vue
+++ /dev/null
@@ -1,221 +0,0 @@
-
-
-
-
-
-
-
- {{ grp.name }}
-
-
- Required
- Optional
- · pick one
-
- · up to {{ grp.max_selections }}
-
- · pick any
-
-
-
- v && toggleOne(grp.id, String(v))"
- class="space-y-1.5"
- >
-
-
-
-
-
-
-
-
-
diff --git a/src/modules/restaurant/components/OrderInvoiceCard.vue b/src/modules/restaurant/components/OrderInvoiceCard.vue
deleted file mode 100644
index 64d613b..0000000
--- a/src/modules/restaurant/components/OrderInvoiceCard.vue
+++ /dev/null
@@ -1,104 +0,0 @@
-
-
-
-
-
-
-
Lightning invoice
-
- expires in {{ fmtCountdown(expiresInSec) }}
- expired
-
-
-
-
![bolt11 invoice QR code]()
-
-
- Amount
-
- {{ Math.ceil(invoice.amount_msat / 1000) }} sat
-
-
-
-
-
-
diff --git a/src/modules/restaurant/components/RestaurantHeader.vue b/src/modules/restaurant/components/RestaurantHeader.vue
deleted file mode 100644
index 8a4af4f..0000000
--- a/src/modules/restaurant/components/RestaurantHeader.vue
+++ /dev/null
@@ -1,81 +0,0 @@
-
-
-
-
-
-
![]()
-
-
-
-
-
-
-
![]()
-
-
-
-
-
-
-
-
- {{ restaurant.name }}
-
-
- {{ restaurant.description }}
-
-
- {{ openBadge.label }}
-
- {{ restaurant.currency || 'sat' }}
-
-
-
- {{ restaurant.location }}
-
-
-
-
-
-
-
diff --git a/src/modules/restaurant/composables/useCheckout.ts b/src/modules/restaurant/composables/useCheckout.ts
deleted file mode 100644
index f42e6c8..0000000
--- a/src/modules/restaurant/composables/useCheckout.ts
+++ /dev/null
@@ -1,309 +0,0 @@
-/**
- * useCheckout — drives the customer's place-and-pay flow against
- * the restaurant extension.
- *
- * v1 ships REST-only:
- * 1. quote (msat required)
- * 2. balance pre-check (sum across all restaurants in the cart)
- * 3. POST /orders → { order, invoice }
- * 4. (optional, customer choice) POST /api/v1/payments to settle
- * the bolt11 from the customer's LNbits wallet. They can also
- * skip this step and scan the QR with any other wallet — the
- * extension's invoice listener marks the order paid either way.
- *
- * Split into two distinct actions so the UI can render the QR codes
- * between place and pay, giving the customer the option to scan
- * with an external wallet rather than auto-paying from LNbits:
- *
- * placeOrders() — runs steps 1-3, populates `state.value.placedOrders`
- * payOrder(idx) — runs step 4 for one placed-order index
- *
- * The festival aggregator (aiolabs/restaurant#8) exercises this same
- * path with N > 1 restaurants in the cart. v1 happens to ship a UI
- * where N == 1, but the orchestration is multi-restaurant ready.
- *
- * NIP-17 transport (aiolabs/restaurant#9) plugs in via the
- * `buildCreateOrder` helper — single point both REST and Nostr
- * transports construct CreateOrder. Loyalty (#5) injects its
- * pass-through fields the same way.
- */
-import { ref, type Ref } from 'vue'
-import { injectService, SERVICE_TOKENS } from '@/core/di-container'
-import { useAuth } from '@/composables/useAuthService'
-import appConfig from '@/app.config'
-import type { RestaurantAPI } from '../services/RestaurantAPI'
-import { useCartStore, type CartLine } from '../stores/cart'
-import type {
- CreateOrder,
- CreateOrderItem,
- Order,
- OrderInvoice,
- PlaceOrderResponse,
-} from '../types/restaurant'
-
-export interface PlacedOrder {
- restaurantId: string
- restaurantSlug: string
- order: Order
- invoice: OrderInvoice | null
-}
-
-export interface CheckoutState {
- step:
- | 'idle'
- | 'quoting'
- | 'placing'
- | 'placed'
- | 'paying'
- | 'paid'
- | 'error'
- progress: { current: number; total: number }
- currentRestaurantSlug: string | null
- placedOrders: PlacedOrder[]
- /** Set of `placedOrders[i].order.id` that have been auto-paid
- * via LNbits in this session. External-wallet payments don't
- * populate this — they're detected via the per-order poller in
- * CheckoutPage. */
- paidOrderIds: Set
- error: string | null
-}
-
-export interface UseCheckoutReturn {
- state: Ref
- /** Run quote → balance precheck → POST /orders for every cart
- * bucket. Returns the placed orders (also persisted in state). */
- placeOrders: () => Promise
- /** Pay one already-placed order's bolt11 from the customer's
- * LNbits wallet. Idempotent — calling twice on the same order
- * is a no-op after the first success. */
- payOrder: (placedIndex: number) => Promise
- /** Pay every unpaid placed-order in sequence. Best-effort: if
- * one fails, earlier successes stay paid. */
- payAll: () => Promise
- /** Reset to idle and drop placed/paid state. */
- reset: () => void
-}
-
-/**
- * The single point CreateOrder is built — keeps loyalty (#5), NIP-17
- * transport (#9), and tip overrides one-place changes rather than
- * touching the whole flow.
- */
-function buildCreateOrder(
- restaurantId: string,
- customerPubkey: string | undefined,
- lines: CartLine[]
-): CreateOrder {
- const items: CreateOrderItem[] = lines.map((l) => ({
- menu_item_id: l.menu_item_id,
- quantity: l.quantity,
- selected_modifiers: l.selected_modifiers,
- note: l.note ?? undefined,
- }))
- // Loyalty (#5) future-extension point: when implemented, inject
- // { loyalty_credits_msat, loyalty_pubkey } into extra.fields here.
- return {
- restaurant_id: restaurantId,
- customer_pubkey: customerPubkey || null,
- items,
- channel: 'rest',
- payment_method: 'lightning',
- }
-}
-
-function blankState(): CheckoutState {
- return {
- step: 'idle',
- progress: { current: 0, total: 0 },
- currentRestaurantSlug: null,
- placedOrders: [],
- paidOrderIds: new Set(),
- error: null,
- }
-}
-
-export function useCheckout(): UseCheckoutReturn {
- const api = injectService(SERVICE_TOKENS.RESTAURANT_API)
- const cart = useCartStore()
- const { user } = useAuth()
-
- // We talk to LNbits's payments endpoint directly rather than
- // pulling in the whole `wallet` module — the restaurant-app
- // bundle is a customer surface, not a wallet UI.
- const apiBaseUrl =
- (
- appConfig.modules.restaurant as
- | { config?: { apiBaseUrl?: string } }
- | undefined
- )?.config?.apiBaseUrl || ''
-
- const state = ref(blankState())
-
- function reset(): void {
- state.value = blankState()
- }
-
- // ----------------------------------------------------------------- //
- // place //
- // ----------------------------------------------------------------- //
-
- async function placeOrders(): Promise {
- const buckets = cart.restaurantsInCart.map((rid) => ({
- restaurantId: rid,
- restaurantSlug: cart.linesFor(rid)[0]?.restaurant_slug ?? '',
- lines: cart.linesFor(rid),
- }))
- if (!buckets.length) {
- throw new Error('Cart is empty')
- }
-
- state.value = {
- ...blankState(),
- step: 'quoting',
- progress: { current: 0, total: buckets.length },
- }
-
- // 1. Quote per restaurant.
- const quotes: Array<{
- restaurantId: string
- restaurantSlug: string
- lines: CartLine[]
- msat: number
- }> = []
- for (let i = 0; i < buckets.length; i++) {
- const b = buckets[i]
- state.value.currentRestaurantSlug = b.restaurantSlug
- state.value.progress = { current: i, total: buckets.length }
- const quote = await api.quoteOrder(
- b.lines.map((l) => ({
- menu_item_id: l.menu_item_id,
- quantity: l.quantity,
- selected_modifiers: l.selected_modifiers,
- note: l.note ?? undefined,
- }))
- )
- quotes.push({ ...b, msat: quote.required_msat })
- }
-
- // 2. Balance pre-check.
- const totalMsatRequired = quotes.reduce((s, q) => s + q.msat, 0)
- const wallet0 = user.value?.wallets?.[0]
- if (wallet0 && typeof wallet0.balance_msat === 'number') {
- if (wallet0.balance_msat < totalMsatRequired) {
- const needSat = Math.ceil(totalMsatRequired / 1000)
- const haveSat = Math.floor(wallet0.balance_msat / 1000)
- // Not fatal — the customer may still want to scan the QR
- // and pay from an external wallet. Surface a warning but
- // continue.
- console.warn(
- `[restaurant] LNbits wallet balance is below the cart total ` +
- `(have ${haveSat} sat, need ${needSat} sat). Auto-pay will ` +
- `fail; scan the QR with an external wallet to settle.`
- )
- }
- }
-
- // 3. Place orders.
- state.value.step = 'placing'
- const placed: PlacedOrder[] = []
- for (let i = 0; i < quotes.length; i++) {
- const q = quotes[i]
- state.value.currentRestaurantSlug = q.restaurantSlug
- state.value.progress = { current: i, total: quotes.length }
- const payload = buildCreateOrder(
- q.restaurantId,
- user.value?.pubkey,
- q.lines
- )
- const result: PlaceOrderResponse = await api.placeOrder(payload)
- placed.push({
- restaurantId: q.restaurantId,
- restaurantSlug: q.restaurantSlug,
- order: result.order,
- invoice: result.invoice,
- })
- }
-
- state.value.step = 'placed'
- state.value.placedOrders = placed
- state.value.currentRestaurantSlug = null
- return placed
- }
-
- // ----------------------------------------------------------------- //
- // pay //
- // ----------------------------------------------------------------- //
-
- async function payBolt11Raw(
- bolt11: string,
- adminkey: string
- ): Promise {
- const response = await fetch(`${apiBaseUrl}/api/v1/payments`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-Api-Key': adminkey,
- },
- body: JSON.stringify({ out: true, bolt11 }),
- })
- if (!response.ok) {
- let detail = response.statusText
- try {
- const body = await response.json()
- if (body?.detail) detail = body.detail
- } catch {
- /* body wasn't JSON */
- }
- throw new Error(`Payment failed: ${response.status} ${detail}`)
- }
- }
-
- async function payOrder(placedIndex: number): Promise {
- const placed = state.value.placedOrders[placedIndex]
- if (!placed) throw new Error(`No placed order at index ${placedIndex}`)
- if (!placed.invoice) return // cash orders skip payment
- if (state.value.paidOrderIds.has(placed.order.id)) return // already paid
-
- const adminkey = user.value?.wallets?.[0]?.adminkey
- if (!adminkey) {
- throw new Error('No wallet available — please log in first.')
- }
-
- state.value.step = 'paying'
- state.value.currentRestaurantSlug = placed.restaurantSlug
- try {
- await payBolt11Raw(placed.invoice.bolt11, adminkey)
- // Set semantics keeps `paidOrderIds` from re-renders; rebuild
- // it on update so Vue picks up the change.
- state.value.paidOrderIds = new Set([
- ...state.value.paidOrderIds,
- placed.order.id,
- ])
- // Bump to 'paid' only when every placed order is paid.
- if (
- state.value.placedOrders.every((p) =>
- state.value.paidOrderIds.has(p.order.id)
- )
- ) {
- state.value.step = 'paid'
- } else {
- state.value.step = 'placed'
- }
- } catch (err) {
- state.value.step = 'error'
- state.value.error = err instanceof Error ? err.message : String(err)
- throw err
- }
- }
-
- async function payAll(): Promise {
- for (let i = 0; i < state.value.placedOrders.length; i++) {
- const p = state.value.placedOrders[i]
- if (!p.invoice) continue
- if (state.value.paidOrderIds.has(p.order.id)) continue
- await payOrder(i)
- }
- }
-
- return { state, placeOrders, payOrder, payAll, reset }
-}
diff --git a/src/modules/restaurant/composables/useMenu.ts b/src/modules/restaurant/composables/useMenu.ts
deleted file mode 100644
index 2c5d706..0000000
--- a/src/modules/restaurant/composables/useMenu.ts
+++ /dev/null
@@ -1,124 +0,0 @@
-/**
- * useMenu — fetches a restaurant's menu via REST.
- *
- * v1: REST-only via RestaurantAPI.getMenu(). The Nostr live-overlay
- * merge lands in commit 8 (subscribes to kind-30402 listings for the
- * restaurant's pubkey and patches `items` reactively).
- *
- * Usage:
- * const { restaurant, tree, items, isLoading, error, refresh }
- * = useMenu(slugOrId)
- *
- * Pass a slug or an id — the composable picks the right endpoint
- * based on the format. Slugs are kebab-case strings; ids are
- * urlsafe-short-hash from the extension (alphanumeric, ~22 chars).
- */
-
-import { ref, computed, onScopeDispose, watch, type Ref } from 'vue'
-import {
- injectService,
- tryInjectService,
- SERVICE_TOKENS,
-} from '@/core/di-container'
-import type { RestaurantAPI } from '../services/RestaurantAPI'
-import type { RestaurantNostrSync } from '../services/RestaurantNostrSync'
-import type {
- EnrichedMenuItem,
- MenuNode,
- Restaurant,
-} from '../types/restaurant'
-
-// Heuristic: ids from urlsafe_short_hash are 22-char base64url. Slugs
-// allow dashes and are typically shorter. Anything containing a dash
-// or shorter than 20 chars is treated as a slug.
-function looksLikeId(value: string): boolean {
- return !value.includes('-') && value.length >= 20 && /^[A-Za-z0-9_-]+$/.test(value)
-}
-
-export interface UseMenuReturn {
- restaurant: Ref
- tree: Ref
- /** Items with the Nostr live overlay merged in. */
- items: Ref
- isLoading: Ref
- error: Ref
- refresh: () => Promise
-}
-
-export function useMenu(slugOrId: Ref | string): UseMenuReturn {
- const api = injectService(SERVICE_TOKENS.RESTAURANT_API)
- const sync = tryInjectService(
- SERVICE_TOKENS.RESTAURANT_NOSTR_SYNC
- )
-
- const restaurant = ref(null)
- const tree = ref([])
- const baseItems = ref([])
- const isLoading = ref(false)
- const error = ref(null)
-
- let abortController: AbortController | null = null
-
- const target = computed(() =>
- typeof slugOrId === 'string' ? slugOrId : slugOrId.value
- )
-
- /**
- * `items` exposes the base REST snapshot patched with the live
- * overlay from RestaurantNostrSync. Operator edits on the
- * extension side surface here within ~1s of arriving at the
- * relay, without a refetch.
- */
- const items = computed(() => {
- const overlay = sync?.overlay
- const deleted = sync?.deleted
- if (!overlay && !deleted) return baseItems.value
- return baseItems.value
- .filter((it) => !(deleted && deleted.has(it.id)))
- .map((it) => {
- const patch = overlay?.get(it.id)
- return patch ? ({ ...it, ...patch } as EnrichedMenuItem) : it
- })
- })
-
- async function load(value: string): Promise {
- if (!value) return
- abortController?.abort()
- abortController = new AbortController()
- const my = abortController
-
- isLoading.value = true
- error.value = null
- try {
- const r = looksLikeId(value)
- ? await api.getRestaurantById(value)
- : await api.getRestaurantBySlug(value)
- if (my.signal.aborted) return
-
- const menu = await api.getMenu(r.id)
- if (my.signal.aborted) return
-
- restaurant.value = menu.restaurant
- tree.value = menu.tree
- baseItems.value = menu.items
- } catch (err) {
- if (my.signal.aborted) return
- error.value = err instanceof Error ? err : new Error(String(err))
- } finally {
- if (!my.signal.aborted) isLoading.value = false
- }
- }
-
- async function refresh(): Promise {
- if (target.value) await load(target.value)
- }
-
- // React to slug/id changes (Vue Router param updates).
- watch(target, (value) => load(value), { immediate: true })
-
- onScopeDispose(() => {
- abortController?.abort()
- })
-
- return { restaurant, tree, items, isLoading, error, refresh }
-}
diff --git a/src/modules/restaurant/composables/useOrder.ts b/src/modules/restaurant/composables/useOrder.ts
deleted file mode 100644
index 00347da..0000000
--- a/src/modules/restaurant/composables/useOrder.ts
+++ /dev/null
@@ -1,154 +0,0 @@
-/**
- * useOrder(orderId) — polls the restaurant extension for status
- * updates on a single order.
- *
- * Polls every `orderPollMs` (from app.config.modules.restaurant)
- * while status is in a non-terminal state. Resets to an immediate
- * fetch on `VisibilityService.onVisible` so a backgrounded tab
- * catches up the moment it comes back. Cleans the interval on
- * scope dispose.
- *
- * Status is treated as an open string (see KNOWN_ORDER_STATUSES)
- * — production / kitchen workflow (aiolabs/restaurant#4) may
- * introduce new states.
- */
-import { onScopeDispose, ref, watch, type Ref } from 'vue'
-import {
- injectService,
- tryInjectService,
- SERVICE_TOKENS,
-} from '@/core/di-container'
-import appConfig from '@/app.config'
-import type { VisibilityService } from '@/core/services/VisibilityService'
-import type { RestaurantAPI } from '../services/RestaurantAPI'
-import type {
- Order,
- OrderItemRow,
- OrderStatus,
-} from '../types/restaurant'
-
-const TERMINAL_STATUSES: OrderStatus[] = [
- 'completed',
- 'canceled',
- 'refunded',
-]
-
-export interface UseOrderReturn {
- order: Ref
- items: Ref
- isLoading: Ref
- error: Ref
- refresh: () => Promise
-}
-
-export function useOrder(orderId: Ref | string): UseOrderReturn {
- const api = injectService(SERVICE_TOKENS.RESTAURANT_API)
- const visibility = tryInjectService(
- SERVICE_TOKENS.VISIBILITY_SERVICE
- )
-
- const pollMs =
- (
- appConfig.modules.restaurant as
- | { config?: { orderPollMs?: number } }
- | undefined
- )?.config?.orderPollMs ?? 5000
-
- const order = ref(null)
- const items = ref([])
- const isLoading = ref(false)
- const error = ref(null)
-
- let timer: ReturnType | null = null
- let unregisterVisibility: (() => void) | null = null
-
- function targetId(): string {
- return typeof orderId === 'string' ? orderId : orderId.value
- }
-
- async function fetchOnce(): Promise {
- const id = targetId()
- if (!id) return
- isLoading.value = true
- try {
- const data = await api.getOrder(id)
- order.value = data.order
- items.value = data.items
- error.value = null
- } catch (err) {
- error.value = err instanceof Error ? err : new Error(String(err))
- } finally {
- isLoading.value = false
- }
- }
-
- function isTerminal(status: OrderStatus | undefined): boolean {
- return !!status && TERMINAL_STATUSES.includes(status)
- }
-
- function startPolling(): void {
- if (timer) return
- timer = setInterval(async () => {
- if (isTerminal(order.value?.status)) {
- stopPolling()
- return
- }
- await fetchOnce()
- }, pollMs)
- }
-
- function stopPolling(): void {
- if (timer) {
- clearInterval(timer)
- timer = null
- }
- }
-
- // Refetch immediately when the tab becomes visible again — useful
- // for mobile where polling pauses during background. The
- // VisibilityService takes (name, onResume, onPause) and returns
- // an unregister fn.
- if (visibility) {
- unregisterVisibility = visibility.registerService(
- `useOrder-${typeof orderId === 'string' ? orderId : 'ref'}`,
- async () => {
- await fetchOnce()
- if (
- !isTerminal((order.value as Order | null)?.status)
- ) {
- startPolling()
- }
- },
- async () => {
- // pause polling while hidden — saves battery on mobile
- stopPolling()
- }
- )
- }
-
- watch(
- () => targetId(),
- async (id) => {
- stopPolling()
- order.value = null
- items.value = []
- if (!id) return
- await fetchOnce()
- if (!isTerminal((order.value as Order | null)?.status)) startPolling()
- },
- { immediate: true }
- )
-
- onScopeDispose(() => {
- stopPolling()
- unregisterVisibility?.()
- })
-
- return {
- order,
- items,
- isLoading,
- error,
- refresh: fetchOnce,
- }
-}
diff --git a/src/modules/restaurant/index.ts b/src/modules/restaurant/index.ts
deleted file mode 100644
index 3b57b9b..0000000
--- a/src/modules/restaurant/index.ts
+++ /dev/null
@@ -1,139 +0,0 @@
-import type { App } from 'vue'
-import type { RouteRecordRaw } from 'vue-router'
-import type { ModulePlugin } from '@/core/types'
-import { container, SERVICE_TOKENS } from '@/core/di-container'
-
-import { RestaurantAPI } from './services/RestaurantAPI'
-import { RestaurantNostrSync } from './services/RestaurantNostrSync'
-
-// v1 skeleton — REST-only, single-venue, URL-driven (/r/:slug).
-//
-// Feature-roadmap context (do NOT build in v1; see issues on
-// aiolabs/restaurant):
-// #1 PDF menu, #2 tier modes, #3 inventory, #4 kitchen workflow,
-// #5 loyalty, #6 cost-of-goods, #7 deployment/monetization,
-// #8 festival/aggregator (NIP-51), #9 NIP-17 order transport.
-//
-// Future-compatibility scaffolding baked in even at v1:
-// • Cart store keys by restaurant_id (multi-restaurant ready
-// for #8 without a refactor).
-// • OrderStatus is an open string type (#4 may add states).
-// • MenuItem.extra carries forward-compatible metadata for
-// inventory (#3), cost-of-goods (#6), loyalty (#5),
-// mode-gated badges (#2).
-// • Module config has a `features: Record`
-// slot reserved for tier gating (#2).
-// • useCheckout builds CreateOrder through a single
-// buildCreateOrder() helper so loyalty (#5) can inject
-// loyalty fields without rewriting the flow.
-
-export interface RestaurantModuleConfig {
- apiBaseUrl: string
- defaultSlug: string
- orderPollMs: number
- currencyDisplay: 'sats' | 'msat'
- features: Record
-}
-
-/**
- * Restaurant Module Plugin (v1 skeleton).
- *
- * The real surface — types/RestaurantAPI/views/cart/checkout/Nostr —
- * lands across commits 3–8. This file is the lifecycle anchor and
- * the route table.
- */
-export const restaurantModule: ModulePlugin = {
- name: 'restaurant',
- version: '0.1.0',
- dependencies: ['base'],
-
- async install(_app: App, options?: { config?: RestaurantModuleConfig }) {
- console.log('🍴 Installing restaurant module…')
-
- if (!options?.config) {
- throw new Error('Restaurant module requires configuration')
- }
-
- // REST client. Initialized lazily — onInitialize() is a no-op
- // (no async dependencies); failures here would only fire if
- // the appConfig is malformed and we want to know about that.
- const restaurantAPI = new RestaurantAPI()
- container.provide(SERVICE_TOKENS.RESTAURANT_API, restaurantAPI)
- await restaurantAPI
- .initialize({ waitForDependencies: true, maxRetries: 1 })
- .catch((error) => {
- console.warn('🍴 RestaurantAPI init deferred:', error)
- })
-
- // Nostr live-overlay sync. Requires RelayHub from baseModule;
- // BaseService.waitForDependencies handles the timing if base
- // initialization hasn't quite landed by the time we get here.
- const restaurantNostrSync = new RestaurantNostrSync()
- container.provide(
- SERVICE_TOKENS.RESTAURANT_NOSTR_SYNC,
- restaurantNostrSync
- )
- await restaurantNostrSync
- .initialize({ waitForDependencies: true, maxRetries: 3 })
- .catch((error) => {
- // No-overlay mode is fine: REST still works, the menu just
- // doesn't reflect operator edits without a page refresh.
- console.warn('🍴 RestaurantNostrSync init deferred:', error)
- })
-
- console.log('✅ Restaurant module installed')
- },
-
- routes: [
- {
- path: '/',
- name: 'restaurant-home',
- component: () => import('./views/HomePage.vue'),
- meta: { requiresAuth: false, title: 'Restaurant' },
- },
- {
- path: '/r/:slug',
- name: 'restaurant-menu',
- component: () => import('./views/RestaurantPage.vue'),
- meta: { requiresAuth: false, title: 'Menu' },
- },
- {
- path: '/r/:slug/item/:itemId',
- name: 'restaurant-item',
- component: () => import('./views/ItemPage.vue'),
- meta: { requiresAuth: false, title: 'Item' },
- },
- {
- path: '/cart',
- name: 'restaurant-cart',
- component: () => import('./views/CartPage.vue'),
- meta: { requiresAuth: false, title: 'Cart' },
- },
- {
- path: '/checkout',
- name: 'restaurant-checkout',
- component: () => import('./views/CheckoutPage.vue'),
- meta: { requiresAuth: false, title: 'Checkout' },
- },
- {
- path: '/orders/:id',
- name: 'restaurant-order',
- component: () => import('./views/OrderStatusPage.vue'),
- meta: { requiresAuth: false, title: 'Order' },
- },
- {
- path: '/orders',
- name: 'restaurant-orders',
- component: () => import('./views/OrdersListPage.vue'),
- meta: { requiresAuth: false, title: 'Orders' },
- },
- {
- path: '/settings',
- name: 'restaurant-settings',
- component: () => import('./views/SettingsPage.vue'),
- meta: { requiresAuth: false, title: 'Settings' },
- },
- ] as RouteRecordRaw[],
-}
-
-export default restaurantModule
diff --git a/src/modules/restaurant/services/RestaurantAPI.ts b/src/modules/restaurant/services/RestaurantAPI.ts
deleted file mode 100644
index 788a342..0000000
--- a/src/modules/restaurant/services/RestaurantAPI.ts
+++ /dev/null
@@ -1,170 +0,0 @@
-/**
- * Typed REST client for the LNbits "restaurant" extension.
- *
- * Mirrors the surface in ~/dev/shared/extensions/restaurant/views_api.py.
- * Public read endpoints (`/restaurants/by-slug/{slug}`,
- * `/restaurants/{id}/menu`, `/menu_items/{id}`) and customer order
- * endpoints (`/orders/quote`, `/orders`, `/orders/{id}`) need no API
- * key; `customer_pubkey` rides in the request body as optional
- * metadata.
- */
-
-import { BaseService } from '@/core/base/BaseService'
-import appConfig from '@/app.config'
-import type {
- CreateOrder,
- CreateOrderItem,
- MenuResponse,
- MenuItem,
- Order,
- OrderInvoice,
- OrderQuote,
- OrderWithItems,
- PlaceOrderResponse,
- Restaurant,
-} from '../types/restaurant'
-
-export class RestaurantAPI extends BaseService {
- protected readonly metadata = {
- name: 'RestaurantAPI',
- version: '1.0.0',
- dependencies: [] as string[],
- }
-
- private baseUrl: string
-
- constructor() {
- super()
- const config = (
- appConfig.modules.restaurant as
- | { config?: { apiBaseUrl?: string } }
- | undefined
- )?.config
- if (!config?.apiBaseUrl) {
- throw new Error(
- 'RestaurantAPI: Missing apiBaseUrl in restaurant module config'
- )
- }
- this.baseUrl = config.apiBaseUrl
- }
-
- protected async onInitialize(): Promise {
- this.debug('RestaurantAPI initialized with base URL:', this.baseUrl)
- }
-
- // ----------------------------------------------------------------- //
- // request helper //
- // ----------------------------------------------------------------- //
-
- private async request(
- endpoint: string,
- options: RequestInit = {}
- ): Promise {
- const url = `${this.baseUrl}/restaurant/api/v1${endpoint}`
-
- const headers: Record = {
- 'Content-Type': 'application/json',
- }
- if (options.headers) {
- Object.assign(headers, options.headers as Record)
- }
-
- const response = await fetch(url, { ...options, headers })
-
- if (!response.ok) {
- let detail = response.statusText
- try {
- const body = await response.json()
- if (body?.detail) detail = body.detail
- } catch {
- // body wasn't JSON, fall through
- }
- throw new Error(
- `RestaurantAPI ${options.method || 'GET'} ${endpoint} ` +
- `failed: ${response.status} ${detail}`
- )
- }
-
- if (response.status === 204) {
- return undefined as T
- }
- return (await response.json()) as T
- }
-
- // ----------------------------------------------------------------- //
- // Restaurants //
- // ----------------------------------------------------------------- //
-
- /** Resolve a URL slug → Restaurant payload. Used by /r/:slug. */
- async getRestaurantBySlug(slug: string): Promise {
- return this.request(
- `/restaurants/by-slug/${encodeURIComponent(slug)}`
- )
- }
-
- async getRestaurantById(id: string): Promise {
- return this.request(
- `/restaurants/${encodeURIComponent(id)}`
- )
- }
-
- /**
- * Full hydrated menu — returns `{restaurant, tree, items}` where
- * `tree` is the rooted MenuNode tree with children + items attached
- * and `items` is the flat enriched list (modifier groups + options
- * + availability windows pre-joined).
- */
- async getMenu(restaurantId: string): Promise {
- return this.request(
- `/restaurants/${encodeURIComponent(restaurantId)}/menu`
- )
- }
-
- async getMenuItem(itemId: string): Promise