feat: availability broadcast (Kind 30078) + WiFi auto-connect

Wire up the availability broadcast composable to publish the ATM's
status as a replaceable Kind 30078 Nostr event. Publishes on
availability change (debounced) and as a 5-minute heartbeat so
monitors can detect offline machines.

Also adds WiFi auto-connect for BATM3: reads SSID/PSK from
/var/lib/lamassu-atm/wifi.conf at boot. Credentials stay local.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-03-29 22:36:58 -04:00
commit 67c7c12ade
3 changed files with 82 additions and 1 deletions

View file

@ -93,9 +93,22 @@ export function useAvailabilityBroadcast(options: UseAvailabilityBroadcastOption
{ deep: true }
)
// Heartbeat: republish every 5 minutes so monitors know the machine is alive.
// If the latest Kind 30078 event is older than ~10 minutes, the machine is offline.
const heartbeatInterval = setInterval(
() => {
publish(computeAvailability())
},
5 * 60 * 1000
)
// Publish immediately on setup
const initial = computeAvailability()
publish(initial)
return { computeAvailability }
function stop() {
clearInterval(heartbeatInterval)
}
return { computeAvailability, stop }
}

View file

@ -21,6 +21,7 @@ import {
type ManagementResponse,
} from '@lamassu/clink'
import type { TransactionRecord } from '@/types/state'
import { useAvailabilityBroadcast } from '@/composables/useAvailabilityBroadcast'
// Check if we're running in Electron
const isElectron = typeof window !== 'undefined' && window.electronAPI !== undefined
@ -308,6 +309,10 @@ export const useAtmStore = defineStore('atm', () => {
return snapshot.value?.context ?? null
})
const inventory = computed((): Record<number, number> => {
return context.value?.inventory ?? {}
})
const fiatSymbol = computed(() => {
const symbols: Record<string, string> = { USD: '$', GTQ: 'Q', EUR: '€', GBP: '£', MXN: '$' }
return symbols[fiatCode.value] || fiatCode.value
@ -557,6 +562,9 @@ export const useAtmStore = defineStore('atm', () => {
// Initialize state machine with real services
initialize(servicesWithInventory)
// Start broadcasting availability (Kind 30078) with 5-minute heartbeat
startAvailabilityBroadcast(services.nostrClient, services.identity, 'atm')
} catch (error) {
console.error('[ATM] Failed to connect to Lightning.Pub:', error)
connectionStatus.value = 'error'
@ -847,6 +855,9 @@ export const useAtmStore = defineStore('atm', () => {
}
})
// Start broadcasting availability (Kind 30078)
startAvailabilityBroadcast(lightning.nostrClient, lightning.identity, 'atm')
console.log('[ATM] Fully initialized with HAL + Lightning')
} catch (error) {
console.error('[ATM] HAL initialization failed:', error)
@ -1099,6 +1110,9 @@ export const useAtmStore = defineStore('atm', () => {
// Real hardware connected — disable mock bill simulator
debugMode.value = false
// Start broadcasting availability (Kind 30078)
startAvailabilityBroadcast(lightning.nostrClient, lightning.identity, 'atm')
console.log('[ATM] Fully initialized with HAL (IPC) + Lightning')
} catch (error) {
console.error('[ATM] HAL initialization failed:', error)
@ -1225,6 +1239,23 @@ export const useAtmStore = defineStore('atm', () => {
}
}
/** Start broadcasting ATM availability (Kind 30078) with 5-minute heartbeat */
let stopAvailabilityBroadcast: (() => void) | null = null
function startAvailabilityBroadcast(nostrClient: any, identity: any, model: string) {
if (stopAvailabilityBroadcast) return
const { stop } = useAvailabilityBroadcast({
nostrClient,
identity,
inventory,
balanceSats,
fiatCode: fiatCode.value,
model,
})
stopAvailabilityBroadcast = stop
}
return {
// State
actor,