feat(pairing): slim the spire seed + carry lnbits_npub (bitspire-#70) #37

Merged
padreug merged 7 commits from bitspire-70-seed-lnbits-npub into main 2026-07-02 17:45:02 +00:00
4 changed files with 39 additions and 9 deletions
Showing only changes of commit 4e36b98534 - Show all commits

feat(pairing-ui): pre-fill the pair dialog relay with the default endpoint
Some checks failed
ci.yml / feat(pairing-ui): pre-fill the pair dialog relay with the default endpoint (pull_request) Failing after 0s

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)://<host>/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 <noreply@anthropic.com>
Padreug 2026-07-02 00:16:46 +02:00

View file

@ -839,12 +839,21 @@ window.app = Vue.createApp({
// ----------------------------------------------------------------- // -----------------------------------------------------------------
// Pair / revoke spire (S0 / #9, #12) // Pair / revoke spire (S0 / #9, #12)
// ----------------------------------------------------------------- // -----------------------------------------------------------------
openPairDialog(machine) { async openPairDialog(machine) {
this.pairDialog.machine = machine this.pairDialog.machine = machine
this.pairDialog.relays = '' this.pairDialog.relays = ''
this.pairDialog.durationHours = null this.pairDialog.durationHours = null
this.pairDialog.result = null this.pairDialog.result = null
this.pairDialog.show = true 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() { async submitPair() {
@ -852,14 +861,12 @@ window.app = Vue.createApp({
.split(/[\s,]+/) .split(/[\s,]+/)
.map(s => s.trim()) .map(s => s.trim())
.filter(Boolean) .filter(Boolean)
if (!relays.length) { // relays is optional — blank falls back to the nostrclient endpoint
Quasar.Notify.create({ // server-side (the pre-filled default), so no client-side requirement.
type: 'negative', const body = {}
message: 'At least one relay is required' if (relays.length) {
}) body.relays = relays
return
} }
const body = {relays}
if (this.pairDialog.durationHours) { if (this.pairDialog.durationHours) {
body.duration_hours = Number(this.pairDialog.durationHours) body.duration_hours = Number(this.pairDialog.durationHours)
} }

View file

@ -868,7 +868,7 @@
<q-input <q-input
v-model="pairDialog.relays" v-model="pairDialog.relays"
label="Relay(s) for the spire's events" label="Relay(s) for the spire's events"
hint="One per line. The same relay the spire publishes to (its VITE_RELAY_URL), e.g. wss://your-host/nostrrelay/<id>" hint="Pre-filled with this instance's nostrclient relay endpoint (derived from your Base URL). Leave as-is unless the spire must use a different relay. One per line; blank uses the default."
type="textarea" autogrow type="textarea" autogrow
class="q-mb-md" class="q-mb-md"
dense outlined></q-input> dense outlined></q-input>

View file

@ -175,3 +175,15 @@ def test_revoke_failure_maps_to_bad_gateway(monkeypatch):
_call_revoke() _call_revoke()
assert ei.value.status_code == 502 assert ei.value.status_code == 502
assert state["unpaired"] is None # not persisted on failure 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

View file

@ -33,6 +33,7 @@ from .pairing import (
PairResult, PairResult,
PairingError, PairingError,
RevokeResult, RevokeResult,
default_relay_endpoint,
pair_spire, pair_spire,
revoke_spire, revoke_spire,
) )
@ -297,6 +298,16 @@ async def api_create_machine(
return 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( @spirekeeper_api_router.post(
"/api/v1/dca/machines/{machine_id}/pair", response_model=PairResult "/api/v1/dca/machines/{machine_id}/pair", response_model=PairResult
) )