diff --git a/apps/machine/src/lib/utils.ts b/apps/machine/src/lib/utils.ts index abba253..fc76a41 100644 --- a/apps/machine/src/lib/utils.ts +++ b/apps/machine/src/lib/utils.ts @@ -5,3 +5,26 @@ import { twMerge } from 'tailwind-merge' export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } + +/** + * Render a denomination/count list for the journal. + * + * Electron's console bridge stringifies every console argument on its way to + * the journal, so passing the array itself arrives as `[object Object]` and + * the numbers are lost. Interpolate one of these instead. See CLAUDE.md, + * "Useful invariants when debugging". + */ +export function formatBays(rows: { denomination: number; count?: number }[]): string { + if (!rows?.length) return '(none)' + // `count` is optional on a device-config cassette: a preset can declare the + // denomination a bay holds without claiming how many notes are in it. Show + // that as unknown rather than as zero, which would read as a drained bay. + return rows.map((r) => `${r.denomination}x${r.count ?? '?'}`).join(' ') +} + +/** Same, for a denomination-keyed count map as `getInventory()` returns. */ +export function formatInventory(inv: Record): string { + const entries = Object.entries(inv ?? {}) + if (!entries.length) return '(none)' + return entries.map(([denom, count]) => `${denom}x${count}`).join(' ') +} diff --git a/apps/machine/src/services/hal.ts b/apps/machine/src/services/hal.ts index f48fda8..723bbc3 100644 --- a/apps/machine/src/services/hal.ts +++ b/apps/machine/src/services/hal.ts @@ -15,6 +15,7 @@ */ import type { ATMServices } from '@bitSpire/state-machine' +import { formatBays } from '@/lib/utils' export interface CassetteConfig { denomination: number @@ -126,7 +127,7 @@ export async function initializeHalServices(config: HalConfig): Promise = { dispenseCash: async (amounts) => { - console.log('[HAL] Dispensing:', amounts) + console.log(`[HAL] Dispensing: ${formatBays(amounts)}`) // Re-initialize dispenser if it was closed after a previous error if (!dispenser.initialized) { diff --git a/apps/machine/src/services/lightning.ts b/apps/machine/src/services/lightning.ts index ee9a441..8140791 100644 --- a/apps/machine/src/services/lightning.ts +++ b/apps/machine/src/services/lightning.ts @@ -21,6 +21,7 @@ import type { ATMServices, ATMContext } from '@bitSpire/state-machine' // Import Electron types import type {} from '@/types/electron' +import { formatBays, formatInventory } from '@/lib/utils' // Check if we're running in Electron (electronAPI is exposed via preload) const isElectron = typeof window !== 'undefined' && window.electronAPI !== undefined @@ -1092,7 +1093,7 @@ export function createATMServices( * Dispense cash (mock for development) */ dispenseCash: async (amounts) => { - console.log('[ATM Service] Dispensing cash:', amounts) + console.log(`[ATM Service] Dispensing cash: ${formatBays(amounts)}`) // In production, this would interface with the Rust HAL // For now, simulate dispense delay @@ -1269,7 +1270,7 @@ export function createATMServices( 20: 50, // 50 x $20 bills = $1000 capacity } - console.log('[ATM Service] Inventory:', inventory) + console.log(`[ATM Service] Inventory: ${formatInventory(inventory)}`) return inventory }, } diff --git a/apps/machine/src/stores/atm.ts b/apps/machine/src/stores/atm.ts index 23fcd2e..1a13349 100644 --- a/apps/machine/src/stores/atm.ts +++ b/apps/machine/src/stores/atm.ts @@ -15,6 +15,7 @@ import { initializeLightningServices, fetchBtcPrice } from '@/services/lightning import { classifyInitError } from '@/services/init-error' import { startOperatorConfigService, type OperatorConfigService } from '@/services/operator-config' import { startOperatorFeesService, type OperatorFeesService } from '@/services/operator-fees' +import { formatBays, formatInventory } from '@/lib/utils' import type { HalConfig, HalServices } from '@/services/hal' import type { MachineModel } from '@/config' import type { LightningBackend } from '@/services/lightning' @@ -179,7 +180,7 @@ async function loadInventoryFromDb(): Promise | null> { if (isElectron && window.electronAPI) { try { const inv = await window.electronAPI.getInventory() - console.log('[ATM] Loaded inventory from DB:', inv) + console.log(`[ATM] Loaded inventory from DB: ${formatInventory(inv)}`) return inv } catch (e) { console.warn('[ATM] Failed to load inventory from DB:', e) @@ -245,7 +246,7 @@ const mockServices: ATMServices = { }, dispenseCash: async (amounts) => { - console.log('[Mock] Dispensing cash:', amounts) + console.log(`[Mock] Dispensing cash: ${formatBays(amounts)}`) await new Promise((resolve) => setTimeout(resolve, 2000)) return { bills: amounts.map((a) => ({ @@ -474,7 +475,7 @@ export const useAtmStore = defineStore('atm', () => { // the availability beacon kept advertising a full cassette. if (inv === null) return persistedInventory.value = inv - console.log('[ATM] Persisted inventory updated:', inv) + console.log(`[ATM] Persisted inventory updated: ${formatInventory(inv)}`) } /** Detect Bitcoin network from a BOLT-11 invoice prefix (called once, persisted) */ @@ -1589,7 +1590,7 @@ export const useAtmStore = defineStore('atm', () => { console.log('[ATM] Fiat currency:', devConfig.fiatCode) console.log('[ATM] Validator device:', devConfig.validator.device) console.log('[ATM] Dispenser device:', devConfig.dispenser.device) - console.log('[ATM] Cassettes:', devConfig.dispenser.cassettes) + console.log(`[ATM] Cassettes: ${formatBays(devConfig.dispenser.cassettes)}`) const halConfig = toHalConfig(devConfig) await initializeWithHalIpc(halConfig) @@ -1662,7 +1663,7 @@ export const useAtmStore = defineStore('atm', () => { // HAL dispenseCash via IPC const halAtmServices: Pick = { dispenseCash: async (amounts) => { - console.log('[ATM] Dispensing via IPC:', amounts) + console.log(`[ATM] Dispensing via IPC: ${formatBays(amounts)}`) return await api.halDispense(amounts) }, getInventory: async () => { @@ -1850,15 +1851,15 @@ export const useAtmStore = defineStore('atm', () => { credentialIdHash: string reason?: string }) { - console.info('[Access] audit', { - result: outcome.result, - role: outcome.role ?? null, - // Truncate the hash in logs — it's already non-reversible, but no need to - // splash the full value across the journal. - credentialIdHash: outcome.credentialIdHash.slice(0, 12), - reason: outcome.reason ?? null, - at: Date.now(), - }) + // Truncate the hash — it's already non-reversible, but no need to splash + // the full value across the journal. Interpolated rather than passed as an + // object: the console bridge would stringify it to `[object Object]` and + // this line is the audit trail until #90 persists it to state.db. + console.info( + `[Access] audit result=${outcome.result} role=${outcome.role ?? 'none'} ` + + `hash=${outcome.credentialIdHash.slice(0, 12)} ` + + `reason=${outcome.reason ?? 'none'} at=${Date.now()}` + ) } /** Grant terminal access after a credential (and any PIN) is authorized. */