fix(logging): interpolate log fields instead of passing an object

Electron's console bridge stringifies each console argument on its way to
the journal, so `console.log('msg:', { a, b })` arrives as
`msg: [object Object]` and every field is lost.

That cost a debugging session today: a cassette-state publish that had
in fact applied an operator refill correctly looked from the journal like
nothing had happened, because the d-tag, event id and stamp were all
inside the object.

Three call sites, the only ones in the renderer passing an object. The
publish line now also carries seq and the applied-op count, which are the
two things worth knowing when an operation seems not to have landed.

Recorded in CLAUDE.md's debugging invariants so it does not come back.
This commit is contained in:
Padreug 2026-09-23 23:55:13 +02:00
commit a68e462462
3 changed files with 7 additions and 3 deletions

View file

@ -219,6 +219,7 @@ UP Board enumerates its eMMC controller via ACPI, not PCI. `upboard.nix` force-l
## Useful invariants when debugging
- The renderer logs prefix every line with a tag: `[Lightning]`, `[ATM]`, `[ATM Service]`, `[LNURL Session]`, `[CLINK]`, `[StateStore]`. `journalctl -u bitspire | grep '\['` is your friend.
- **Never pass an object as a console argument in the renderer.** Electron's console bridge stringifies each argument, so `console.log('msg:', { a, b })` reaches the journal as `msg: [object Object]` and every field is lost. Interpolate instead. Cost a debugging session on 2026-09-23, when a cassette publish that had worked looked like it had done nothing.
- `bitspire.service` runs as the `lamassu` user; `/var/lib/bitspire` is its `dataDir` (ReadWritePaths). DB lives at `/var/lib/bitspire/state.db` (we previously had `/var/lib/lamassu-atm` — that path is gone on dev, see commit `9c455d6`).
- The `lightning.lightningPub` field on `LightningServices` is a `LightningBackend` *adapter*, not a `LightningPubClient`. Don't try to call LP-only methods on it.

View file

@ -147,7 +147,7 @@ export async function startOperatorConfigService(
},
}
)
console.log('[OperatorConfig] Subscribed:', { dTag, subscriptionId })
console.log(`[OperatorConfig] Subscribed: d=${dTag} sub=${subscriptionId}`)
const heartbeat = setInterval(() => {
publishCassettesState(cfg, api, machineId).catch((err) =>
@ -342,6 +342,9 @@ async function publishCassettesState(
await cfg.nostrClient.publish(event)
await api.markStatePublished(createdAt)
console.log('[OperatorConfig] cassettes-state published:', { dTag, eventId: event.id, createdAt })
console.log(
`[OperatorConfig] cassettes-state published: id=${event.id.slice(0, 12)}… ` +
`created_at=${createdAt} seq=${seq} applied_ops=${appliedOps.length} d=${dTag}`
)
return true
}

View file

@ -132,7 +132,7 @@ export async function startOperatorFeesService(
},
}
)
console.log('[Fees] Subscribed:', { dTag, subscriptionId })
console.log(`[Fees] Subscribed: d=${dTag} sub=${subscriptionId}`)
return {
stop: () => cfg.nostrClient.unsubscribe(subscriptionId),