feat(deploy): services.bitspire.nfc.enable — declare the reader per machine

pcscd was enabled in hardware/batm3.nix and hardware/upboard.nix, which
cannot express "is a reader fitted": upboard.nix is shared by sintra (HID
Global OMNIKEY 5022) and tejo (nothing fitted), so tejo inherited pcscd it
has no use for, while the douro — with its own hardware file — got none and
wedged on every boot.

Make it a machine capability instead. services.bitspire.nfc.enable owns
pcscd, the two polkit rules and the wedge-recovery unit, and hands the app
a BITSPIRE_NFC_ENABLED flag so it doesn't initialise nfc-pcsc at all on a
machine with no reader. Per-model truth lives in nfcReaderForModel in
flake.nix next to fiatCodeForModel and upgradeWindowForModel, since a
shared hardware file can't answer the question. batm3 and sintra are true;
douro and tejo flip to true when readers are fitted.

The flag goes through the unit's Environment rather than
/var/lib/bitspire/.env, because .env is only written when absent — a
machine provisioned months ago would never pick up a new value.
This commit is contained in:
Padreug 2026-09-29 22:49:59 +02:00
commit bb2ad39628
5 changed files with 137 additions and 95 deletions

View file

@ -952,22 +952,29 @@ app.whenReady().then(() => {
startWatchdog()
startCommandPoller()
// Bolt Card reader — forwards taps (lnurlw) + status to the renderer. Fully
// best-effort: if the reader/pcscd is absent it just reports 'unavailable'
// and the cash-out QR path is unaffected.
void startNfcReader(
(lnurlw) => {
// Don't log the value — it carries the card's single-use SUN p/c.
console.log(`[NFC] card tapped — lnurlw (${lnurlw.length} chars) → renderer`)
mainWindow?.webContents.send('nfc:card-tapped', lnurlw)
},
(status: NfcStatus) => {
console.log(
`[NFC] status=${status.state}${status.reader ? ` reader="${status.reader}"` : ''}${status.message ? ` — ${status.message}` : ''}`
)
mainWindow?.webContents.send('nfc:status', status)
}
)
// Bolt Card reader — forwards taps (lnurlw) + status to the renderer. Opt-in
// per machine via services.bitspire.nfc.enable, which is off unless a CCID
// reader is actually fitted: nfc-pcsc's pcsclite binding busy-spins this very
// thread when pcscd is absent and wedges the whole app (see nfc-service.ts).
// nfc-service also re-checks the pcscd socket, so this flag is the coarse
// gate, not the only defence. Cash-out via QR never depends on any of it.
if (process.env.BITSPIRE_NFC_ENABLED === 'true') {
void startNfcReader(
(lnurlw) => {
// Don't log the value — it carries the card's single-use SUN p/c.
console.log(`[NFC] card tapped — lnurlw (${lnurlw.length} chars) → renderer`)
mainWindow?.webContents.send('nfc:card-tapped', lnurlw)
},
(status: NfcStatus) => {
console.log(
`[NFC] status=${status.state}${status.reader ? ` reader="${status.reader}"` : ''}${status.message ? ` — ${status.message}` : ''}`
)
mainWindow?.webContents.send('nfc:status', status)
}
)
} else {
console.log('[NFC] no reader configured (BITSPIRE_NFC_ENABLED not "true") — skipping init')
}
app.on('activate', () => {
// macOS: re-create window when dock icon clicked