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 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: { 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 () { restartNostrConnection: async function () {
LNbits.utils LNbits.utils
.confirmDialog( .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 () => { .onOk(async () => {
try { try {
@ -208,6 +258,8 @@ window.app = Vue.createApp({
'/nostrmarket/api/v1/restart', '/nostrmarket/api/v1/restart',
this.g.user.wallets[0].adminkey this.g.user.wallets[0].adminkey
) )
// Check status after restart
setTimeout(() => this.checkNostrStatus(), 2000)
} catch (error) { } catch (error) {
LNbits.utils.notifyApiError(error) LNbits.utils.notifyApiError(error)
} }
@ -216,6 +268,7 @@ window.app = Vue.createApp({
}, },
created: async function () { created: async function () {
await this.getMerchant() await this.getMerchant()
await this.checkNostrStatus()
setInterval(async () => { setInterval(async () => {
if ( if (
!this.wsConnection || !this.wsConnection ||

View file

@ -1,44 +1,61 @@
<q-card> <q-card>
<q-card-section> <q-card-section>
<p> <p class="text-body1">
Nostr Market<br /> 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> <small>
Created by, Created by
<a <a
class="text-secondary" class="text-secondary"
target="_blank" target="_blank"
style="color: unset" style="color: unset"
href="https://github.com/talvasconcelos" href="https://github.com/talvasconcelos"
>Tal Vasconcelos</a >Tal Vasconcelos</a
> >,
<a <a
class="text-secondary" class="text-secondary"
target="_blank" target="_blank"
style="color: unset" style="color: unset"
href="https://github.com/benarc" href="https://github.com/benarc"
>Ben Arc</a >Ben Arc</a
> >,
<a <a
class="text-secondary" class="text-secondary"
target="_blank" target="_blank"
style="color: unset" style="color: unset"
href="https://github.com/motorina0" href="https://github.com/motorina0"
>motorina0</a >motorina0</a
></small >
> </small>
</p> </p>
<a
class="text-secondary"
target="_blank"
href="/docs#/nostrmarket"
class="text-white"
>Swagger REST API Documentation</a
>
</q-card-section> </q-card-section>
<q-card-section> <q-card-section class="q-pt-none">
<a class="text-secondary" target="_blank" href="/nostrmarket/market" <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
><q-icon name="storefront" class="q-mr-sm"></q-icon>Market client</a <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-section>
</q-card> </q-card>

View file

@ -151,16 +151,66 @@
<div v-if="g.user.admin" class="col-12 q-mb-lg"> <div v-if="g.user.admin" class="col-12 q-mb-lg">
<q-card> <q-card>
<q-card-section class="q-pa-md"> <q-card-section class="q-pa-md">
<q-btn <q-btn-dropdown
label="Restart Nostr Connection" :color="nostrStatusColor"
color="grey" :label="nostrStatusLabel"
outline icon="sync"
split
@click="restartNostrConnection" @click="restartNostrConnection"
> >
<q-tooltip> <q-list>
Restart the connection to the nostrclient extension <q-item clickable v-close-popup @click="restartNostrConnection">
</q-tooltip> <q-item-section avatar>
</q-btn> <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-section>
</q-card> </q-card>
</div> </div>
@ -168,7 +218,7 @@
<q-card> <q-card>
<q-card-section> <q-card-section>
<h6 class="text-subtitle1 q-my-none"> <h6 class="text-subtitle1 q-my-none">
{{SITE_TITLE}} Nostr Market Extension Nostr Market
</h6> </h6>
</q-card-section> </q-card-section>
<q-card-section class="q-pa-none"> <q-card-section class="q-pa-none">

View file

@ -1,10 +1,12 @@
import json import json
from http import HTTPStatus from http import HTTPStatus
import httpx
from fastapi import Depends from fastapi import Depends
from fastapi.exceptions import HTTPException from fastapi.exceptions import HTTPException
from lnbits.core.models import WalletTypeInfo from lnbits.core.models import WalletTypeInfo
from lnbits.core.services import websocket_updater from lnbits.core.services import websocket_updater
from lnbits.settings import settings
from lnbits.decorators import ( from lnbits.decorators import (
require_admin_key, require_admin_key,
require_invoice_key, require_invoice_key,
@ -1105,6 +1107,40 @@ async def api_list_currencies_available():
return list(currencies.keys()) 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") @nostrmarket_ext.put("/api/v1/restart")
async def restart_nostr_client(wallet: WalletTypeInfo = Depends(require_admin_key)): async def restart_nostr_client(wallet: WalletTypeInfo = Depends(require_admin_key)):
try: try: