feat: scan-modal polish, Pine 1.2.0, bitcoind RPC creds off argv #102
@ -35,6 +35,9 @@ app:
|
||||
fi;
|
||||
RPC_USER="$(printenv BITCOIN_RPC_USER)";
|
||||
RPC_PASS="$(printenv BITCOIN_RPC_PASS)";
|
||||
RPC_CONF="/tmp/rpc.conf";
|
||||
umask 077;
|
||||
printf "rpcuser=%s\nrpcpassword=%s\n" "$RPC_USER" "$RPC_PASS" > "$RPC_CONF";
|
||||
RPC_TXRELAY_AUTH="$(printenv BITCOIN_RPC_TXRELAY_RPCAUTH || true)";
|
||||
DISK_GB_VALUE="$(printenv DISK_GB || true)";
|
||||
RPC_HEADROOM="-rpcthreads=16 -rpcworkqueue=256";
|
||||
@ -43,9 +46,9 @@ app:
|
||||
RPC_TXRELAY_FLAGS="$RPC_TXRELAY_FLAGS -rpcauth=$RPC_TXRELAY_AUTH -rpcwhitelist=txrelay:sendrawtransaction,submitpackage,testmempoolaccept,getmempoolinfo,getrawmempool,getmempoolentry,getnetworkinfo,getblockchaininfo,getblockcount,getblockhash,getblock,getblockheader,getrawtransaction,gettxout,gettxspendingprevout,decoderawtransaction,decodescript,estimatesmartfee,uptime,ping,getconnectioncount,getpeerinfo,getindexinfo,getdeploymentinfo,getchaintips";
|
||||
fi;
|
||||
if [ "${DISK_GB_VALUE:-0}" -lt 1000 ]; then
|
||||
exec "$BITCOIND" -datadir=/home/bitcoin/.bitcoin -noconf -printtoconsole=0 -server=1 -prune=550 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=1024 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS";
|
||||
exec "$BITCOIND" -datadir=/home/bitcoin/.bitcoin -conf="$RPC_CONF" -printtoconsole=0 -server=1 -prune=550 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=1024 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS;
|
||||
else
|
||||
exec "$BITCOIND" -datadir=/home/bitcoin/.bitcoin -noconf -printtoconsole=0 -server=1 -txindex=1 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=4096 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS";
|
||||
exec "$BITCOIND" -datadir=/home/bitcoin/.bitcoin -conf="$RPC_CONF" -printtoconsole=0 -server=1 -txindex=1 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=4096 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS;
|
||||
fi
|
||||
derived_env:
|
||||
- key: DISK_GB
|
||||
|
||||
@ -35,6 +35,9 @@ app:
|
||||
fi;
|
||||
RPC_USER="$(printenv BITCOIN_RPC_USER)";
|
||||
RPC_PASS="$(printenv BITCOIN_RPC_PASS)";
|
||||
RPC_CONF="/tmp/rpc.conf";
|
||||
umask 077;
|
||||
printf "rpcuser=%s\nrpcpassword=%s\n" "$RPC_USER" "$RPC_PASS" > "$RPC_CONF";
|
||||
RPC_TXRELAY_AUTH="$(printenv BITCOIN_RPC_TXRELAY_RPCAUTH || true)";
|
||||
DISK_GB_VALUE="$(printenv DISK_GB || true)";
|
||||
RPC_HEADROOM="-rpcthreads=16 -rpcworkqueue=256";
|
||||
@ -43,9 +46,9 @@ app:
|
||||
RPC_TXRELAY_FLAGS="$RPC_TXRELAY_FLAGS -rpcauth=$RPC_TXRELAY_AUTH -rpcwhitelist=txrelay:sendrawtransaction,submitpackage,testmempoolaccept,getmempoolinfo,getrawmempool,getmempoolentry,getnetworkinfo,getblockchaininfo,getblockcount,getblockhash,getblock,getblockheader,getrawtransaction,gettxout,gettxspendingprevout,decoderawtransaction,decodescript,estimatesmartfee,uptime,ping,getconnectioncount,getpeerinfo,getindexinfo,getdeploymentinfo,getchaintips";
|
||||
fi;
|
||||
if [ "${DISK_GB_VALUE:-0}" -lt 1000 ]; then
|
||||
exec "$BITCOIND" -datadir=/home/bitcoin/.bitcoin -noconf -printtoconsole=0 -server=1 -prune=550 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=2048 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS";
|
||||
exec "$BITCOIND" -datadir=/home/bitcoin/.bitcoin -conf="$RPC_CONF" -printtoconsole=0 -server=1 -prune=550 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=2048 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS;
|
||||
else
|
||||
exec "$BITCOIND" -datadir=/home/bitcoin/.bitcoin -noconf -printtoconsole=0 -server=1 -txindex=1 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=4096 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS";
|
||||
exec "$BITCOIND" -datadir=/home/bitcoin/.bitcoin -conf="$RPC_CONF" -printtoconsole=0 -server=1 -txindex=1 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=4096 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS;
|
||||
fi
|
||||
derived_env:
|
||||
- key: DISK_GB
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
app:
|
||||
id: pine
|
||||
name: Pine
|
||||
version: "1.1.1"
|
||||
version: "1.2.0"
|
||||
description: A private voice assistant for your home. Pine runs speech-to-text (Whisper) and text-to-speech (Piper) on your own node and pairs with a PineVoice satellite speaker, so Home Assistant Assist works locally with nothing sent to the cloud.
|
||||
category: home
|
||||
|
||||
@ -174,10 +174,15 @@ app:
|
||||
<div id="log">Ready. Click the button, then pick “PineVoice” in the Bluetooth popup.</div>
|
||||
</div>
|
||||
|
||||
<p class="ha">After WiFi joins, add the speaker to Home Assistant:
|
||||
<b>Settings → Devices & services → Add Wyoming Protocol</b>, host =
|
||||
the speaker’s IP, port <b>10700</b>, then pick an Assist pipeline using
|
||||
Whisper + Piper. Wake word: <b>“Hey Jarvis.”</b></p>
|
||||
<p class="ha">After WiFi joins, one manual step remains — pair the
|
||||
speaker in Home Assistant: <b>Settings → Devices & services →
|
||||
Add Wyoming Protocol</b>, host = the speaker’s IP, port
|
||||
<b>10700</b>. Whisper, Piper and the Assist pipeline are wired up
|
||||
automatically when Pine installs. Wake word: <b>“Hey Jarvis.”</b>
|
||||
You can also ask node things like <i>“what’s the block height?”</i></p>
|
||||
<p class="ha">Troubleshooting: if it hears you (LED reacts) but answers
|
||||
are silent, unplug and replug the speaker — an interrupted answer can
|
||||
wedge its audio output until it reboots.</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
@ -44,12 +44,22 @@
|
||||
<!-- ============ SCAN PANE ============ -->
|
||||
<div v-if="pane === 'scan'" key="scan">
|
||||
<div class="relative w-full aspect-square rounded-xl overflow-hidden bg-black/40 border border-white/10 mb-4">
|
||||
<video ref="videoElement" class="w-full h-full object-cover" autoplay muted playsinline></video>
|
||||
<!-- opacity (not v-if/v-show): the scanner needs the element, and a
|
||||
source-less <video> flashes a native play glyph in Android WebViews -->
|
||||
<video ref="videoElement" class="w-full h-full object-cover transition-opacity duration-200" :class="isScanning ? 'opacity-100' : 'opacity-0'" autoplay muted playsinline></video>
|
||||
<!-- Viewfinder -->
|
||||
<div class="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||
<div class="scan-viewfinder"></div>
|
||||
</div>
|
||||
<div v-if="!isScanning" class="absolute inset-0 flex flex-col items-center justify-center gap-3 bg-black/50">
|
||||
<!-- While auto-start is settling, show only a quiet spinner — the
|
||||
Start/Take-photo buttons are the FALLBACK, not a splash screen -->
|
||||
<div v-if="!isScanning && autoStarting" class="absolute inset-0 flex flex-col items-center justify-center gap-3 bg-black/50">
|
||||
<svg class="w-8 h-8 text-white/40 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" />
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v3a5 5 0 00-5 5H4z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div v-else-if="!isScanning" class="absolute inset-0 flex flex-col items-center justify-center gap-3 bg-black/50">
|
||||
<svg class="w-10 h-10 text-white/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
@ -272,7 +282,7 @@ function goBack() {
|
||||
if (pane.value === 'amount') {
|
||||
error.value = ''
|
||||
goTo('scan', 'back')
|
||||
nextTick(() => startScanning())
|
||||
nextTick(() => { if (!liveCameraUnavailable.value) startScanning() })
|
||||
} else if (pane.value === 'success') {
|
||||
close()
|
||||
}
|
||||
@ -281,6 +291,10 @@ function goBack() {
|
||||
// --- Scanner ---
|
||||
const videoElement = ref<HTMLVideoElement | null>(null)
|
||||
const isScanning = ref(false)
|
||||
// True while a camera-start attempt is in flight (incl. the automatic one on
|
||||
// open) — the fallback buttons stay hidden until it resolves, so they don't
|
||||
// flash for a second on every open.
|
||||
const autoStarting = ref(false)
|
||||
const scanStatus = ref('')
|
||||
const scanStatusIsError = ref(false)
|
||||
const cameraError = ref(false)
|
||||
@ -291,6 +305,7 @@ const animatedDecoder = useAnimatedQRDecoder()
|
||||
async function startScanning() {
|
||||
cameraError.value = false
|
||||
scanStatusIsError.value = false
|
||||
autoStarting.value = true
|
||||
try {
|
||||
if (!videoElement.value) return
|
||||
if (!navigator.mediaDevices?.getUserMedia) {
|
||||
@ -307,7 +322,9 @@ async function startScanning() {
|
||||
returnDetailedScanResult: true,
|
||||
highlightScanRegion: false,
|
||||
preferredCamera: 'environment',
|
||||
maxScansPerSecond: 10,
|
||||
// 4/s decodes plenty fast for a hand-held QR while keeping the
|
||||
// preview smooth on phone WebViews (10/s visibly lagged the video).
|
||||
maxScansPerSecond: 4,
|
||||
}
|
||||
)
|
||||
await qrScanner.value.start()
|
||||
@ -317,6 +334,8 @@ async function startScanning() {
|
||||
cameraError.value = true
|
||||
scanStatusIsError.value = true
|
||||
scanStatus.value = err instanceof Error ? err.message : 'Unable to access camera'
|
||||
} finally {
|
||||
autoStarting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@ -465,7 +484,8 @@ function acceptDetected(raw: string) {
|
||||
action.value = 'pay-invoice'
|
||||
dest.value = text
|
||||
const invoiceAmount = parseBolt11AmountSats(text)
|
||||
amount.value = invoiceAmount ?? 0
|
||||
// Zero-amount invoice inside a unified BIP21 URI: prefill from amount=
|
||||
amount.value = invoiceAmount ?? bip21AmountSats ?? 0
|
||||
amountLocked.value = invoiceAmount !== null
|
||||
} else if (isCashuToken(text)) {
|
||||
rail.value = 'cashu'
|
||||
@ -491,6 +511,16 @@ function acceptDetected(raw: string) {
|
||||
dest.value = text
|
||||
amount.value = bip21AmountSats ?? 0
|
||||
amountLocked.value = false
|
||||
} else if (text.toLowerCase().startsWith('lno1')) {
|
||||
// BOLT12 offer — LND (this node's Lightning backend) can't pay offers yet
|
||||
scanStatusIsError.value = true
|
||||
scanStatus.value = 'BOLT12 offers aren\'t supported yet — ask the recipient for a BOLT11 invoice'
|
||||
return
|
||||
} else if (text.toLowerCase().startsWith('lnurl1') || /^[\w.+-]+@[\w-]+(\.[\w-]+)+$/.test(text)) {
|
||||
// LNURL-pay / lightning address — needs an HTTP callback flow we don't do yet
|
||||
scanStatusIsError.value = true
|
||||
scanStatus.value = 'LNURL and lightning addresses aren\'t supported yet — ask for a BOLT11 invoice'
|
||||
return
|
||||
} else {
|
||||
scanStatusIsError.value = true
|
||||
scanStatus.value = 'Not a recognised invoice, address or token'
|
||||
@ -656,7 +686,7 @@ function close() {
|
||||
watch(() => props.show, (open) => {
|
||||
if (open) {
|
||||
resetAll()
|
||||
nextTick(() => startScanning())
|
||||
nextTick(() => { if (!liveCameraUnavailable.value) startScanning() })
|
||||
} else {
|
||||
stopScanning()
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user