fix(cassettes): republish after a dispense the renderer didn't run
Two dispense paths bypassed the refresh-and-publish step that cash-out does. The kind-21003 management command persisted the transaction and stopped there, and the operator-command poller runs entirely in the main process, where the renderer cannot see the bays move at all. In both cases the renderer kept serving a stale inventory and the operator's cassette view stayed frozen until the next customer cash-out. The management handler takes an after-hook, and the main process emits 'cassettes:changed' when it mutates the table so the renderer can catch up. Both land on one helper that reloads the inventory and republishes the state document. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
0d43c4e033
commit
db68e6e244
4 changed files with 52 additions and 3 deletions
|
|
@ -841,6 +841,11 @@ function startCommandPoller(): void {
|
|||
error: result.error,
|
||||
})
|
||||
|
||||
// This dispense happened entirely in the main process, so the renderer
|
||||
// has no idea the bays moved — it would keep serving a stale inventory
|
||||
// and would never republish the operator's view. Tell it.
|
||||
mainWindow?.webContents.send('cassettes:changed')
|
||||
|
||||
// Only remediate the original tx if ALL requested bills were dispensed
|
||||
let refRemediated = false
|
||||
if (parsed.ref_txid && result.dispensed) {
|
||||
|
|
|
|||
|
|
@ -222,6 +222,13 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||
// Bolt Card reader (main process → renderer). removeAllListeners first: a
|
||||
// renderer reload re-runs this, and a duplicated card-tap listener would
|
||||
// trigger the LNURL-withdraw twice.
|
||||
// The main process changed the cassettes table (an operator-command dispense,
|
||||
// boot seeding). The renderer reloads its inventory and republishes state.
|
||||
onCassettesChanged: (callback: () => void) => {
|
||||
ipcRenderer.removeAllListeners('cassettes:changed')
|
||||
ipcRenderer.on('cassettes:changed', () => callback())
|
||||
},
|
||||
|
||||
onNfcCardTapped: (callback: (lnurlw: string) => void) => {
|
||||
ipcRenderer.removeAllListeners('nfc:card-tapped')
|
||||
ipcRenderer.on('nfc:card-tapped', (_event, lnurlw) => callback(lnurlw))
|
||||
|
|
|
|||
|
|
@ -72,7 +72,13 @@ async function handleManagementCommand(
|
|||
request: ManagementRequest,
|
||||
dispenseFn: (amounts: { denomination: number; count: number }[]) => Promise<any>,
|
||||
machineIdle: boolean,
|
||||
currency: string
|
||||
currency: string,
|
||||
/**
|
||||
* Called once the dispense has been persisted. Bills left the bays whether or
|
||||
* not the dispense completed, so the caller refreshes its inventory and
|
||||
* republishes the operator's view — this path used to do neither.
|
||||
*/
|
||||
onCassettesChanged?: () => Promise<void>
|
||||
): Promise<ManagementResponse | null> {
|
||||
if (!isMachineDispenseRequest(request)) return null
|
||||
|
||||
|
|
@ -130,6 +136,7 @@ async function handleManagementCommand(
|
|||
cassettes: result.cassettes,
|
||||
error: result.error,
|
||||
})
|
||||
await onCassettesChanged?.()
|
||||
|
||||
// Only remediate the original tx if ALL requested bills were dispensed
|
||||
let refRemediated = false
|
||||
|
|
@ -450,6 +457,16 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
*/
|
||||
const persistedInventory = ref<Record<number, number>>({})
|
||||
|
||||
/**
|
||||
* The bays moved outside the normal cash-out flow — an operator-command
|
||||
* dispense, a main-process seed. Refresh the renderer's view and push the
|
||||
* operator's. Best-effort: a publish failure must not fail the dispense.
|
||||
*/
|
||||
async function refreshAndPublishCassettes() {
|
||||
await reloadPersistedInventory()
|
||||
await operatorConfigSvc?.publishCassettesState()
|
||||
}
|
||||
|
||||
async function reloadPersistedInventory() {
|
||||
const inv = await loadInventoryFromDb()
|
||||
// Only a failed read is ignored. An empty map used to be skipped too,
|
||||
|
|
@ -709,6 +726,7 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
// Start the machine
|
||||
actor.value.start()
|
||||
setupNfcListener()
|
||||
setupCassettesChangedListener()
|
||||
console.log('[ATM] State machine initialized')
|
||||
}
|
||||
|
||||
|
|
@ -922,6 +940,21 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The main process can move the bays without the renderer knowing — an
|
||||
* operator-command dispense runs entirely there, and boot seeding writes the
|
||||
* table before the store exists. Listen for that and catch up, otherwise the
|
||||
* renderer serves a stale inventory and the operator's view never updates.
|
||||
* Idempotent via preload removeAllListeners.
|
||||
*/
|
||||
function setupCassettesChangedListener() {
|
||||
if (!isElectron || !window.electronAPI?.onCassettesChanged) return
|
||||
window.electronAPI.onCassettesChanged(() => {
|
||||
console.log('[ATM] Cassettes changed in the main process — refreshing')
|
||||
void refreshAndPublishCassettes()
|
||||
})
|
||||
}
|
||||
|
||||
/** Wire the main-process reader once (idempotent via preload removeAllListeners). */
|
||||
function setupNfcListener() {
|
||||
if (!isElectron || !window.electronAPI?.onNfcCardTapped) return
|
||||
|
|
@ -1319,7 +1352,8 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
request,
|
||||
(amounts) => hal.atmServices.dispenseCash(amounts),
|
||||
isIdle.value,
|
||||
fiatCode.value
|
||||
fiatCode.value,
|
||||
refreshAndPublishCassettes
|
||||
)
|
||||
})
|
||||
|
||||
|
|
@ -1651,7 +1685,8 @@ export const useAtmStore = defineStore('atm', () => {
|
|||
request,
|
||||
(amounts) => api.halDispense(amounts),
|
||||
isIdle.value,
|
||||
fiatCode.value
|
||||
fiatCode.value,
|
||||
refreshAndPublishCassettes
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
|||
2
apps/machine/src/types/electron.d.ts
vendored
2
apps/machine/src/types/electron.d.ts
vendored
|
|
@ -217,6 +217,8 @@ declare global {
|
|||
onHalBillInserted: (callback: (denomination: number) => void) => void
|
||||
onHalBillRejected: (callback: (reason: string) => void) => void
|
||||
onHalError: (callback: (error: string) => void) => void
|
||||
/** The main process mutated the cassettes table; reload + republish. */
|
||||
onCassettesChanged: (callback: () => void) => void
|
||||
/** Bolt Card reader: a tapped card's lnurlw voucher. */
|
||||
onNfcCardTapped: (callback: (lnurlw: string) => void) => void
|
||||
/** Bolt Card reader status (ready / reading / error / unavailable). */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue