From 4e36b985347e9af09926e87b627b2116bae1012f Mon Sep 17 00:00:00 2001 From: Padreug Date: Thu, 2 Jul 2026 00:16:46 +0200 Subject: [PATCH] feat(pairing-ui): pre-fill the pair dialog relay with the default endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The backend now defaults an omitted relay to the nostrclient proxy endpoint, but the pair dialog still showed an empty, client-side-required field — so the operator saw "no default". Wire it through: - GET /api/v1/dca/default-relay returns default_relay_endpoint() (the derived ws(s):///nostrclient/api/v1/relay). - openPairDialog pre-fills the relay textarea with it; the operator can override or clear it (blank → same server-side default). Drops the client-side "at least one relay is required" guard. - Hint updated to explain the default. Co-Authored-By: Claude Opus 4.8 --- static/js/index.js | 23 +++++++++++++++-------- templates/spirekeeper/index.html | 2 +- tests/test_pair_endpoint.py | 12 ++++++++++++ views_api.py | 11 +++++++++++ 4 files changed, 39 insertions(+), 9 deletions(-) diff --git a/static/js/index.js b/static/js/index.js index 053133d..a0659b5 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -839,12 +839,21 @@ window.app = Vue.createApp({ // ----------------------------------------------------------------- // Pair / revoke spire (S0 / #9, #12) // ----------------------------------------------------------------- - openPairDialog(machine) { + async openPairDialog(machine) { this.pairDialog.machine = machine this.pairDialog.relays = '' this.pairDialog.durationHours = null this.pairDialog.result = null this.pairDialog.show = true + // Pre-fill with this lnbits' default relay (the nostrclient proxy + // endpoint derived from lnbits_baseurl). The operator can override or + // clear it; if left blank the backend fills in the same default. + try { + const {data} = await LNbits.api.request('GET', `${API}/default-relay`) + if (data && data.relay) this.pairDialog.relays = data.relay + } catch (e) { + // Non-fatal: leave blank; the backend still defaults on submit. + } }, async submitPair() { @@ -852,14 +861,12 @@ window.app = Vue.createApp({ .split(/[\s,]+/) .map(s => s.trim()) .filter(Boolean) - if (!relays.length) { - Quasar.Notify.create({ - type: 'negative', - message: 'At least one relay is required' - }) - return + // relays is optional — blank falls back to the nostrclient endpoint + // server-side (the pre-filled default), so no client-side requirement. + const body = {} + if (relays.length) { + body.relays = relays } - const body = {relays} if (this.pairDialog.durationHours) { body.duration_hours = Number(this.pairDialog.durationHours) } diff --git a/templates/spirekeeper/index.html b/templates/spirekeeper/index.html index 01bb973..8399529 100644 --- a/templates/spirekeeper/index.html +++ b/templates/spirekeeper/index.html @@ -868,7 +868,7 @@ diff --git a/tests/test_pair_endpoint.py b/tests/test_pair_endpoint.py index 775fd4f..c123a81 100644 --- a/tests/test_pair_endpoint.py +++ b/tests/test_pair_endpoint.py @@ -175,3 +175,15 @@ def test_revoke_failure_maps_to_bad_gateway(monkeypatch): _call_revoke() assert ei.value.status_code == 502 assert state["unpaired"] is None # not persisted on failure + + +def test_default_relay_endpoint_returns_nostrclient_url(): + from lnbits.settings import settings + + prev = settings.lnbits_baseurl + try: + settings.lnbits_baseurl = "https://lnbits.example.com/" + result = asyncio.run(views_api.api_default_relay(SimpleNamespace(id="op1"))) + assert result == {"relay": "wss://lnbits.example.com/nostrclient/api/v1/relay"} + finally: + settings.lnbits_baseurl = prev diff --git a/views_api.py b/views_api.py index ebaf53b..86748eb 100644 --- a/views_api.py +++ b/views_api.py @@ -33,6 +33,7 @@ from .pairing import ( PairResult, PairingError, RevokeResult, + default_relay_endpoint, pair_spire, revoke_spire, ) @@ -297,6 +298,16 @@ async def api_create_machine( return machine +@spirekeeper_api_router.get("/api/v1/dca/default-relay") +async def api_default_relay(user: User = Depends(check_user_exists)) -> dict: + """The relay a pairing seed defaults to when the operator leaves it blank — + this lnbits' nostrclient proxy endpoint, derived from lnbits_baseurl + (bitspire#70). The pair dialog pre-fills it. `None` if lnbits_baseurl is + unset.""" + _ = user + return {"relay": default_relay_endpoint()} + + @spirekeeper_api_router.post( "/api/v1/dca/machines/{machine_id}/pair", response_model=PairResult )