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
Showing only changes of commit d66f50dbdf - Show all commits

fix(logging): finish the object-argument sweep

#107 caught three sites by grepping for an object literal as the second
console argument. That pattern misses the more common form, a variable
holding an object, so five more were still landing in the journal as
[object Object]. This time the list came from the machine itself: every
distinct such line in three days of sintra's journal.

The five: the bay list at HAL init, the inventory loaded from state.db,
the inventory pushed to the renderer, the amounts sent to a dispense, and
the access-control audit record. Three sibling sites in the mock and
service paths are fixed too; they had not run recently enough to appear
in the journal but carry the same shapes.

The audit line is the one that mattered. It is the entire record of who
was granted or denied terminal access until #90 persists it to state.db,
and every field of it was being discarded.

formatBays and formatInventory render the denomination/count shapes these
sites share. `count` is optional on a device-config cassette, so an
absent one prints as unknown rather than as zero, which would read as a
drained bay.
Padreug 2026-09-24 12:26:05 +02:00

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. */