feat: add nostrclient status indicator and connection button

- Add /api/v1/status endpoint to check nostrclient availability
- Add color-coded "Connect" button (red/orange/green) based on status
- Show dropdown with connection details (relays count, websocket status)
- Add warning banner when nostrclient extension is not available
- Update info card with description of extension functionality

Closes #132

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ben Weeks 2025-12-22 10:15:11 +00:00
parent 17d13dbe6b
commit 9911a03575
4 changed files with 186 additions and 30 deletions

View file

@ -16,7 +16,29 @@ window.app = Vue.createApp({
privateKey: null
}
},
wsConnection: null
wsConnection: null,
nostrStatus: {
nostrclient_available: false,
nostrclient_relays: [],
nostrclient_error: null,
nostrmarket_running: false,
websocket_connected: false
}
}
},
computed: {
nostrStatusColor: function () {
if (!this.nostrStatus.nostrclient_available) {
return 'red'
} else if (this.nostrStatus.websocket_connected) {
return 'green'
} else if (this.nostrStatus.nostrmarket_running) {
return 'orange'
}
return 'red'
},
nostrStatusLabel: function () {
return 'Connect'
}
},
methods: {
@ -196,10 +218,38 @@ window.app = Vue.createApp({
})
}
},
checkNostrStatus: async function () {
try {
const {data} = await LNbits.api.request(
'GET',
'/nostrmarket/api/v1/status',
this.g.user.wallets[0].inkey
)
this.nostrStatus = data
if (!data.nostrclient_available) {
this.$q.notify({
timeout: 5000,
type: 'warning',
message: 'Nostrclient extension not available',
caption:
data.nostrclient_error ||
'Please install and configure the nostrclient extension'
})
}
} catch (error) {
this.nostrStatus = {
nostrclient_available: false,
nostrclient_relays: [],
nostrclient_error: 'Failed to check status',
nostrmarket_running: false,
websocket_connected: false
}
}
},
restartNostrConnection: async function () {
LNbits.utils
.confirmDialog(
'Are you sure you want to reconnect to the nostrcient extension?'
'Are you sure you want to reconnect to the nostrclient extension?'
)
.onOk(async () => {
try {
@ -208,6 +258,8 @@ window.app = Vue.createApp({
'/nostrmarket/api/v1/restart',
this.g.user.wallets[0].adminkey
)
// Check status after restart
setTimeout(() => this.checkNostrStatus(), 2000)
} catch (error) {
LNbits.utils.notifyApiError(error)
}
@ -216,6 +268,7 @@ window.app = Vue.createApp({
},
created: async function () {
await this.getMerchant()
await this.checkNostrStatus()
setInterval(async () => {
if (
!this.wsConnection ||

View file

@ -1,44 +1,61 @@
<q-card>
<q-card-section>
<p>
Nostr Market<br />
<p class="text-body1">
Create, edit and publish products to your Nostr relays. Customers can
browse your stalls and pay with Lightning.
</p>
<p class="q-mb-none">
<small>
Created by,
Created by
<a
class="text-secondary"
target="_blank"
style="color: unset"
href="https://github.com/talvasconcelos"
>Tal Vasconcelos</a
>
>,
<a
class="text-secondary"
target="_blank"
style="color: unset"
href="https://github.com/benarc"
>Ben Arc</a
>
>,
<a
class="text-secondary"
target="_blank"
style="color: unset"
href="https://github.com/motorina0"
>motorina0</a
></small
>
>
</small>
</p>
<a
class="text-secondary"
target="_blank"
href="/docs#/nostrmarket"
class="text-white"
>Swagger REST API Documentation</a
>
</q-card-section>
<q-card-section>
<a class="text-secondary" target="_blank" href="/nostrmarket/market"
><q-tooltip>Visit the market client</q-tooltip
><q-icon name="storefront" class="q-mr-sm"></q-icon>Market client</a
>
<q-card-section class="q-pt-none">
<a class="text-secondary" target="_blank" href="/nostrmarket/market">
<q-icon name="storefront" class="q-mr-sm"></q-icon>Market client
<q-tooltip>Visit the market client</q-tooltip>
</a>
<br />
<a class="text-secondary" target="_blank" href="/docs#/nostrmarket">
<q-icon name="code" class="q-mr-sm"></q-icon>API Documentation
<q-tooltip>Swagger REST API Documentation</q-tooltip>
</a>
</q-card-section>
<q-card-section class="q-pt-none">
<q-banner dense class="bg-orange-8 text-white" rounded>
<template v-slot:avatar>
<q-icon name="info" color="white"></q-icon>
</template>
<strong>Requires:</strong>&nbsp;
<a
href="https://github.com/lnbits/nostrclient"
target="_blank"
class="text-white"
style="text-decoration: underline"
>nostrclient</a
>
extension to be installed and configured with relays.
</q-banner>
</q-card-section>
</q-card>

View file

@ -151,16 +151,66 @@
<div v-if="g.user.admin" class="col-12 q-mb-lg">
<q-card>
<q-card-section class="q-pa-md">
<q-btn
label="Restart Nostr Connection"
color="grey"
outline
<q-btn-dropdown
:color="nostrStatusColor"
:label="nostrStatusLabel"
icon="sync"
split
@click="restartNostrConnection"
>
<q-tooltip>
Restart the connection to the nostrclient extension
</q-tooltip>
</q-btn>
<q-list>
<q-item clickable v-close-popup @click="restartNostrConnection">
<q-item-section avatar>
<q-icon name="refresh" color="primary"></q-icon>
</q-item-section>
<q-item-section>
<q-item-label>Restart Connection</q-item-label>
<q-item-label caption>
Reconnect to the nostrclient extension
</q-item-label>
</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="checkNostrStatus">
<q-item-section avatar>
<q-icon name="wifi_find" color="primary"></q-icon>
</q-item-section>
<q-item-section>
<q-item-label>Check Status</q-item-label>
<q-item-label caption>
Check connection to nostrclient
</q-item-label>
</q-item-section>
</q-item>
<q-separator></q-separator>
<q-item>
<q-item-section>
<q-item-label caption>
<strong>Nostrclient:</strong>
<q-badge
:color="nostrStatus.nostrclient_available ? 'green' : 'red'"
class="q-ml-xs"
v-text="nostrStatus.nostrclient_available ? 'Available' : 'Not Available'"
></q-badge>
<br />
<strong>Relays:</strong>
<span v-text="(nostrStatus.nostrclient_relays || []).length"></span> configured<br />
<strong>WebSocket:</strong>
<q-badge
:color="nostrStatus.websocket_connected ? 'green' : 'orange'"
class="q-ml-xs"
v-text="nostrStatus.websocket_connected ? 'Connected' : 'Disconnected'"
></q-badge>
</q-item-label>
<q-item-label
v-if="nostrStatus.nostrclient_error"
caption
class="text-negative q-mt-xs"
v-text="nostrStatus.nostrclient_error"
></q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
</q-card-section>
</q-card>
</div>
@ -168,7 +218,7 @@
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">
{{SITE_TITLE}} Nostr Market Extension
Nostr Market
</h6>
</q-card-section>
<q-card-section class="q-pa-none">

View file

@ -1,10 +1,12 @@
import json
from http import HTTPStatus
import httpx
from fastapi import Depends
from fastapi.exceptions import HTTPException
from lnbits.core.models import WalletTypeInfo
from lnbits.core.services import websocket_updater
from lnbits.settings import settings
from lnbits.decorators import (
require_admin_key,
require_invoice_key,
@ -1105,6 +1107,40 @@ async def api_list_currencies_available():
return list(currencies.keys())
@nostrmarket_ext.get("/api/v1/status")
async def api_get_nostr_status(
wallet: WalletTypeInfo = Depends(require_invoice_key),
) -> dict:
"""Get the status of the nostrclient extension."""
nostrclient_available = False
nostrclient_relays = []
nostrclient_error = None
try:
async with httpx.AsyncClient() as client:
response = await client.get(
f"http://localhost:{settings.port}/nostrclient/api/v1/relays",
timeout=5.0,
)
if response.status_code == 200:
nostrclient_available = True
nostrclient_relays = response.json()
except httpx.ConnectError:
nostrclient_error = "Cannot connect to nostrclient extension"
except httpx.TimeoutException:
nostrclient_error = "Timeout connecting to nostrclient"
except Exception as ex:
nostrclient_error = str(ex)
return {
"nostrclient_available": nostrclient_available,
"nostrclient_relays": nostrclient_relays,
"nostrclient_error": nostrclient_error,
"nostrmarket_running": nostr_client.running,
"websocket_connected": nostr_client.is_websocket_connected,
}
@nostrmarket_ext.put("/api/v1/restart")
async def restart_nostr_client(wallet: WalletTypeInfo = Depends(require_admin_key)):
try: