Finish the console object-argument sweep #108

Merged
padreug merged 1 commit from fix/log-object-args-sweep into dev 2026-09-24 13:09:56 +00:00
4 changed files with 43 additions and 17 deletions

View file

@ -5,3 +5,26 @@ import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) 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<number, number>): string {
const entries = Object.entries(inv ?? {})
if (!entries.length) return '(none)'
return entries.map(([denom, count]) => `${denom}x${count}`).join(' ')
}

View file

@ -15,6 +15,7 @@
*/ */
import type { ATMServices } from '@bitSpire/state-machine' import type { ATMServices } from '@bitSpire/state-machine'
import { formatBays } from '@/lib/utils'
export interface CassetteConfig { export interface CassetteConfig {
denomination: number denomination: number
@ -126,7 +127,7 @@ export async function initializeHalServices(config: HalConfig): Promise<HalServi
const atmServices: Pick<ATMServices, 'dispenseCash' | 'getInventory'> = { const atmServices: Pick<ATMServices, 'dispenseCash' | 'getInventory'> = {
dispenseCash: async (amounts) => { 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 // Re-initialize dispenser if it was closed after a previous error
if (!dispenser.initialized) { if (!dispenser.initialized) {

View file

@ -21,6 +21,7 @@ import type { ATMServices, ATMContext } from '@bitSpire/state-machine'
// Import Electron types // Import Electron types
import type {} from '@/types/electron' import type {} from '@/types/electron'
import { formatBays, formatInventory } from '@/lib/utils'
// Check if we're running in Electron (electronAPI is exposed via preload) // Check if we're running in Electron (electronAPI is exposed via preload)
const isElectron = typeof window !== 'undefined' && window.electronAPI !== undefined const isElectron = typeof window !== 'undefined' && window.electronAPI !== undefined
@ -1092,7 +1093,7 @@ export function createATMServices(
* Dispense cash (mock for development) * Dispense cash (mock for development)
*/ */
dispenseCash: async (amounts) => { 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 // In production, this would interface with the Rust HAL
// For now, simulate dispense delay // For now, simulate dispense delay
@ -1269,7 +1270,7 @@ export function createATMServices(
20: 50, // 50 x $20 bills = $1000 capacity 20: 50, // 50 x $20 bills = $1000 capacity
} }
console.log('[ATM Service] Inventory:', inventory) console.log(`[ATM Service] Inventory: ${formatInventory(inventory)}`)
return inventory return inventory
}, },
} }

View file

@ -15,6 +15,7 @@ import { initializeLightningServices, fetchBtcPrice } from '@/services/lightning
import { classifyInitError } from '@/services/init-error' import { classifyInitError } from '@/services/init-error'
import { startOperatorConfigService, type OperatorConfigService } from '@/services/operator-config' import { startOperatorConfigService, type OperatorConfigService } from '@/services/operator-config'
import { startOperatorFeesService, type OperatorFeesService } from '@/services/operator-fees' import { startOperatorFeesService, type OperatorFeesService } from '@/services/operator-fees'
import { formatBays, formatInventory } from '@/lib/utils'
import type { HalConfig, HalServices } from '@/services/hal' import type { HalConfig, HalServices } from '@/services/hal'
import type { MachineModel } from '@/config' import type { MachineModel } from '@/config'
import type { LightningBackend } from '@/services/lightning' import type { LightningBackend } from '@/services/lightning'
@ -179,7 +180,7 @@ async function loadInventoryFromDb(): Promise<Record<number, number> | null> {
if (isElectron && window.electronAPI) { if (isElectron && window.electronAPI) {
try { try {
const inv = await window.electronAPI.getInventory() 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 return inv
} catch (e) { } catch (e) {
console.warn('[ATM] Failed to load inventory from DB:', e) console.warn('[ATM] Failed to load inventory from DB:', e)
@ -245,7 +246,7 @@ const mockServices: ATMServices = {
}, },
dispenseCash: async (amounts) => { dispenseCash: async (amounts) => {
console.log('[Mock] Dispensing cash:', amounts) console.log(`[Mock] Dispensing cash: ${formatBays(amounts)}`)
await new Promise((resolve) => setTimeout(resolve, 2000)) await new Promise((resolve) => setTimeout(resolve, 2000))
return { return {
bills: amounts.map((a) => ({ bills: amounts.map((a) => ({
@ -474,7 +475,7 @@ export const useAtmStore = defineStore('atm', () => {
// the availability beacon kept advertising a full cassette. // the availability beacon kept advertising a full cassette.
if (inv === null) return if (inv === null) return
persistedInventory.value = inv 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) */ /** 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] Fiat currency:', devConfig.fiatCode)
console.log('[ATM] Validator device:', devConfig.validator.device) console.log('[ATM] Validator device:', devConfig.validator.device)
console.log('[ATM] Dispenser device:', devConfig.dispenser.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) const halConfig = toHalConfig(devConfig)
await initializeWithHalIpc(halConfig) await initializeWithHalIpc(halConfig)
@ -1662,7 +1663,7 @@ export const useAtmStore = defineStore('atm', () => {
// HAL dispenseCash via IPC // HAL dispenseCash via IPC
const halAtmServices: Pick<ATMServices, 'dispenseCash' | 'getInventory'> = { const halAtmServices: Pick<ATMServices, 'dispenseCash' | 'getInventory'> = {
dispenseCash: async (amounts) => { dispenseCash: async (amounts) => {
console.log('[ATM] Dispensing via IPC:', amounts) console.log(`[ATM] Dispensing via IPC: ${formatBays(amounts)}`)
return await api.halDispense(amounts) return await api.halDispense(amounts)
}, },
getInventory: async () => { getInventory: async () => {
@ -1850,15 +1851,15 @@ export const useAtmStore = defineStore('atm', () => {
credentialIdHash: string credentialIdHash: string
reason?: string reason?: string
}) { }) {
console.info('[Access] audit', { // Truncate the hash — it's already non-reversible, but no need to splash
result: outcome.result, // the full value across the journal. Interpolated rather than passed as an
role: outcome.role ?? null, // object: the console bridge would stringify it to `[object Object]` and
// Truncate the hash in logs — it's already non-reversible, but no need to // this line is the audit trail until #90 persists it to state.db.
// splash the full value across the journal. console.info(
credentialIdHash: outcome.credentialIdHash.slice(0, 12), `[Access] audit result=${outcome.result} role=${outcome.role ?? 'none'} ` +
reason: outcome.reason ?? null, `hash=${outcome.credentialIdHash.slice(0, 12)} ` +
at: Date.now(), `reason=${outcome.reason ?? 'none'} at=${Date.now()}`
}) )
} }
/** Grant terminal access after a credential (and any PIN) is authorized. */ /** Grant terminal access after a credential (and any PIN) is authorized. */