diff --git a/src/modules/market/composables/useMarketStallSelfHeal.ts b/src/modules/market/composables/useMarketStallSelfHeal.ts new file mode 100644 index 0000000..6a0eb65 --- /dev/null +++ b/src/modules/market/composables/useMarketStallSelfHeal.ts @@ -0,0 +1,99 @@ +import { useAuth } from '@/composables/useAuthService' +import { injectService, SERVICE_TOKENS } from '@/core/di-container' +import type { NostrmarketAPI } from '../services/nostrmarketAPI' + +const SESSION_FLAG = 'market-stall-self-heal-checked' +const STALL_EVENT_KIND = 30017 + +/** + * Detect-and-recover from the LNbits orphan-stall bug + * (aiolabs/lnbits#10): _create_default_merchant provisions the merchant + * + stall in nostrmarket's internal SQLite but historically never + * published the kind-30017 stall event to relays. The upstream fix is + * 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 until somebody republishes — which manifests in our + * webapp as "Unknown Stall" on every product authored by them. + * + * This composable runs once per browser session (sessionStorage gate) + * for any logged-in user who lands on the merchant dashboard: + * + * 1. Ask the relay for kind-30017 events authored by their pubkey. + * 2. Ask LNbits for the merchant's known stalls. + * 3. For each stall in (2) whose id isn't represented in (1), PUT the + * stall 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. + * + * Silent on success. Logs to console.info on republish; console.warn on + * failure. Never toasts — this is supposed to be invisible. + * + * Tracked in aiolabs/webapp#38. + */ +export function useMarketStallSelfHeal() { + const { user } = useAuth() + const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB) as any + const nostrmarketAPI = injectService(SERVICE_TOKENS.NOSTRMARKET_API) as NostrmarketAPI + + async function selfHealOnce(): Promise { + if (sessionStorage.getItem(SESSION_FLAG)) return + + const currentUser = user.value + if (!currentUser?.pubkey) return + + const wallets = (currentUser as any).wallets as Array<{ adminkey?: string; inkey?: string }> | undefined + if (!wallets?.length) return + + const adminWallet = wallets.find(w => w.adminkey) || wallets[0] + if (!adminWallet?.adminkey || !adminWallet?.inkey) return + + // Mark checked early — even on failure we don't want to retry on every + // dashboard mount during the same tab session. + sessionStorage.setItem(SESSION_FLAG, '1') + + if (!relayHub || !nostrmarketAPI) { + console.warn('[market-self-heal] Required services unavailable, skipping') + return + } + + try { + const relayEvents: Array<{ tags?: Array<[string, string?]> }> = await relayHub.queryEvents([ + { kinds: [STALL_EVENT_KIND], authors: [currentUser.pubkey] }, + ]) + const publishedStallIds = new Set() + for (const ev of relayEvents) { + const dTag = ev.tags?.find(t => Array.isArray(t) && t[0] === 'd') + const stallId = dTag?.[1] + if (stallId) publishedStallIds.add(stallId) + } + + const lnbitsStalls = await nostrmarketAPI.getStalls(adminWallet.inkey) + const orphans = lnbitsStalls.filter(s => !publishedStallIds.has(s.id)) + + if (orphans.length === 0) { + console.info( + `[market-self-heal] All ${lnbitsStalls.length} stall(s) have a relay event — no recovery needed.`, + ) + return + } + + console.info( + `[market-self-heal] Republishing ${orphans.length} orphan stall(s):`, + orphans.map(s => `${s.id} (${s.name})`), + ) + + for (const stall of orphans) { + try { + await nostrmarketAPI.updateStall(adminWallet.adminkey, stall) + console.info(`[market-self-heal] Republished ${stall.id} (${stall.name})`) + } catch (err) { + console.warn(`[market-self-heal] Failed to republish ${stall.id}:`, err) + } + } + } catch (err) { + console.warn('[market-self-heal] Self-heal check failed:', err) + } + } + + return { selfHealOnce } +} diff --git a/src/modules/market/views/MarketDashboard.vue b/src/modules/market/views/MarketDashboard.vue index ded7018..5e08423 100644 --- a/src/modules/market/views/MarketDashboard.vue +++ b/src/modules/market/views/MarketDashboard.vue @@ -77,6 +77,7 @@ import OrderHistory from '../components/OrderHistory.vue' import MerchantStore from '../components/MerchantStore.vue' import MarketSettings from '../components/MarketSettings.vue' import { auth } from '@/composables/useAuthService' +import { useMarketStallSelfHeal } from '../composables/useMarketStallSelfHeal' const route = useRoute() const marketStore = useMarketStore() @@ -139,6 +140,7 @@ const tabs = computed(() => [ ]) const router = useRouter() +const { selfHealOnce } = useMarketStallSelfHeal() // Lifecycle onMounted(() => { @@ -153,6 +155,9 @@ onMounted(() => { return } console.log('Market Dashboard mounted') + // Self-heal orphan stalls (issue #38) once per browser session. + // Fire-and-forget — never blocks the dashboard render. + void selfHealOnce() })