Commit graph

185 commits

Author SHA1 Message Date
Patrick Mulligan
de6a9559ff feat(ui): replace hourglass animation with pickaxe mining swing
Replace the dead hourglass CSS animation with a thematic pickaxe mining
animation. The pickaxe swings from resting position up to -45deg then
strikes down, mimicking a mining motion. Used in the generatingNdebit
loading state; other loading states still use BounceDots pending review.

Refs #33

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 01:47:45 -04:00
Patrick Mulligan
902c8ab6cb fix: use fiat code from env in command queue poller
The command poller hardcoded 'GTQ' as the currency for manual dispense
transactions. Now reads VITE_LAMASSU_FIAT_CODE from env, defaulting
to 'USD'.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 00:47:07 -04:00
Patrick Mulligan
c10e3239b5 fix: only remediate original tx when manual dispense fully succeeds
If the manual dispense itself partially fails (e.g., cassette jam during
remediation), the original failed transaction must stay in error state
so the operator knows it still needs attention. Only mark as
'remediated' when result.dispensed === true (all requested bills out).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 00:37:40 -04:00
Patrick Mulligan
89b0ceaa73 feat: operator command queue for local TUI dispense
Add operator_commands table and polling loop so the TUI (or other local
tools) can trigger manual dispenses by inserting a command row into
SQLite. The Electron main process polls every 2s, executes pending
commands via HAL, records the transaction, and updates the command
status with the result.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 00:37:40 -04:00
Patrick Mulligan
01e71b0954 security: add replay protection, timestamp validation, and input checks
Addresses security audit findings for the operator command channel:

1. Replay protection: track processed management event IDs in a Set,
   reject duplicates. Caps at 1000 entries to prevent unbounded growth.

2. Timestamp validation: reject events created before machine startup
   (prevents processing stale events on relay reconnect) and events
   older than 60 seconds (limits replay window).

3. Input validation: validate bill denomination/count in
   handleManagementCommand (defense in depth — IPC path also validates
   but direct HAL path did not). Caps count at 100 per denomination.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 00:37:40 -04:00
Patrick Mulligan
e03782803b feat: operator command channel via Nostr (manual dispense)
Add a Nostr-native operator command channel using Kind 21003 (CLINK
Manage) events. Operators listed in OPERATOR_PUBKEYS can send encrypted
commands to the machine.

Phase 1 implements manual dispense: operator sends a dispense command,
machine verifies sender, checks it's idle, performs a direct HAL
dispense (bypassing state machine), and records the transaction.

When ref_txid is provided, the referenced failed transaction is updated
to status 'remediated', closing the loop on dispense errors.

Changes:
- CLINK types: add 'machine' resource, MachineDispenseRequest type
- CLINK client: support operator pubkey list (string | string[])
- Runtime config: VITE_OPERATOR_PUBKEYS env var
- Schema v4→v5: manual_dispense type, remediated_by column
- Lightning services: wire onManagement callback
- ATM store: handleManagementCommand with idle check + remediation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 00:37:40 -04:00
Patrick Mulligan
c8f76af113 fix(hal): make validator optional for dispenser-only operation
The HAL init hung indefinitely when the validator device didn't exist
or failed to respond — blocking the entire init including the dispenser
and Lightning connection.

Now the validator is optional:
- Checks device exists (fs.existsSync) before attempting to open
- 15s timeout on validator.run() to prevent hanging
- On failure, logs warning and proceeds with dispenser-only mode
- All validator methods guarded with null checks

This enables the BATM3 to run cash-out only when the MEI validator
is not connected (e.g., during initial setup or testing).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 16:20:51 -04:00
Patrick Mulligan
71eff33f1e feat(hal): add EBDS bill validator driver for BATM3 support
Port MEI CashFlow SC / BNR Advance EBDS protocol from lamassu-machine
to TypeScript HAL. Adds 'batm3' machine model preset (EBDS validator +
F56 dispenser). Fixes hardcoded 'id003' validator type in device config
overrides so model presets correctly propagate their validator type.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 03:20:46 -04:00
Patrick Mulligan
a21865ff20 feat(machine): record failed dispenses and per-cassette tracking
Failed dispenses (sats debited, cash not dispensed) were invisible —
transactions only recorded on 'complete'. Now records on 'dispenseError'
with status ('dispense_error'|'partial'|'complete'), error message, and
per-cassette detail.

Also fixes a bug in both HAL services where dispense results were mapped
by amounts-array index instead of cassette position, causing swapped
denomination counts when cassette order differs from request order.

Schema v3→v4: adds status/error columns to transactions, new
cassette_bills table for per-cassette provisioned/dispensed/rejected.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 17:53:10 -04:00
Patrick Mulligan
3ab03d05ac feat(machine): add renderer watchdog for kiosk resilience
After 6 days of uptime the Electron renderer silently crashed while the
main process kept running (blank screen, no recovery). Three-layer
detection: render-process-gone (instant), unresponsive (Chromium), and
IPC heartbeat (30s ping, 2 missed = reload). Reloads renderer via
loadFile/loadURL preserving HAL hardware state in main process.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 17:27:58 -04:00
Patrick Mulligan
5182b3634e fix(machine): enable/disable validator on state transitions (direct HAL)
The IPC path already had a watcher to enable the bill validator when
entering insertingBills and disable it when leaving. The direct HAL
path (initializeWithHal) was missing this, meaning the validator would
accept bills in any state. Matches brain.js pattern (lines 193-196).

Closes #28

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 00:14:45 -04:00
Patrick Mulligan
ab2ea0b811 fix(hal): re-initialize dispenser after errors
After a dispense error, the F56/Puloon drivers call close() which sets
initialized=false. The next dispense would fail on a closed serial port.
Now checks dispenser.initialized before each dispense and re-inits if
needed, matching the lazy re-init pattern from brain.js (line 4072).

Closes #29

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 00:14:31 -04:00
Patrick Mulligan
310d0edb99 feat(machine): persist exchange rate and currency in transactions
Add exchange_rate (sats per fiat unit) and currency columns to the
transactions table so transaction economics can be audited after the
fact. Includes schema migration v2 (fee columns) and v3 (rate/currency),
updated IPC types, and atm-transactions CLI output.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 01:30:18 -04:00
Patrick Mulligan
d64f6cd9ab fix(machine): persist transactions stuck in waitingForCashTaken
The IPC HAL path (production) never set halServices.value, so the
auto-advance from waitingForCashTaken → complete never fired. The
machine hung on "Cash Ready!" indefinitely — no transaction persisted.

Three fixes:
- Auto-advance now checks `isElectron` (covers IPC path)
- waitingForCashTaken has a 30s after-timeout as safety net
- Fix unscoped lightningPub refs in LNURL session helpers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 21:26:58 -05:00
Patrick Mulligan
473834a363 refactor: rename fiatAmount to fiatCents for clarity
The field was always stored in cents but the name was ambiguous.
Rename to fiatCents across state machine, store, and views.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 13:02:12 -05:00
Patrick Mulligan
99ae5ab3de feat(lightning): add withdraw link lifecycle management (delete/update/invalidate)
- Add deleteWithdrawLink and updateWithdrawLink RPC methods to LightningPubClient
- Extract shared WithdrawLink type, add Delete/Update request/response types
- Track linkId in LNURL sessions for server-side cleanup
- Invalidate previous LNURL session on new link creation
- Auto-delete expired links on the server

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 13:01:40 -05:00
Patrick Mulligan
adbfffa0c3 security(M1): verify Nostr event signatures on kind 21000
Add verifyEvent() check before processing kind 21000 events from
Lightning.Pub. While NIP-44v1 encryption provides implicit
authentication (relay can't forge encrypted content without the
shared secret), verifying signatures adds defense-in-depth against
any future changes that might weaken the encryption assumption.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 09:56:25 -05:00
Patrick Mulligan
f1011e7cee security(H5): hardcode allowMockFallback=false in production
Only allow mock fallback when running in development mode (isDev).
In production (packaged Electron app), the VITE_ALLOW_MOCK_FALLBACK
env var is ignored entirely. This prevents an attacker with file
access from enabling mock services (fake payments, fake hardware)
by editing .env on the ATM.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 09:44:13 -05:00
Patrick Mulligan
46f12e5629 security(H4): fix bill escrow race condition
Guard hal:stack-bill and hal:reject-bill IPC handlers against being
called when no bill is in escrow (pendingBillDenomination === null).
Previously, rapid-fire calls could double-accept or misattribute
bill denominations. Now the handlers silently ignore calls when
no bill is pending, preventing the race.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 09:41:09 -05:00
Patrick Mulligan
1ac50b6add security(H1): add Content Security Policy
Add CSP in two layers:
1. Meta tag in index.html (works for all builds)
2. HTTP header via Electron session API (defense-in-depth)

Policy: script-src 'self' blocks XSS from loading external scripts
or executing inline scripts. style-src allows 'unsafe-inline' for
Vue's style injection. connect-src allows ws/wss/http/https for
configurable relay and API endpoints.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 09:36:59 -05:00
Patrick Mulligan
1e3de32c51 security(H2): validate IPC dispense input from renderer
Add input validation to hal:dispense IPC handler:
- Reject non-array or empty amounts
- Validate denomination and count are numbers
- Reject non-positive or non-integer counts
- Verify denomination exists in loaded cassettes
- Verify requested count does not exceed available inventory

Prevents a compromised renderer from sending crafted dispense
requests (negative counts, unknown denominations, over-capacity).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 09:34:20 -05:00
Patrick Mulligan
2273303b13 security(C1): remove private key from get-config IPC response
Move atmPrivateKey and adminToken out of the general get-config IPC
handler into a dedicated one-shot get-atm-secrets handler that returns
secrets only once per app lifecycle. Subsequent calls return empty
strings. This prevents XSS or DevTools from repeatedly querying
getConfig() to steal the ATM's Nostr private key.

TODO: Move signing/encryption to main process entirely (Phase 2)
so the private key never crosses the IPC boundary.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 09:32:50 -05:00
Patrick Mulligan
d8841f7fe9 fix(hal): return DispenseResult from IPC dispense handler
The hal:dispense IPC handler in main.ts did not return the result of
dispenseCash(), causing the state machine guard to crash on undefined
output. This left the UI stuck on "Dispensing cash..." after successful
dispense.

- hal-service.ts: return DispenseResult instead of void/throwing
- main.ts: add missing return in IPC handler
- machine.ts: defensive guard (?. instead of .) as safety net

Bug found with the aid of Seoyoung at Trece Cielos.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 15:50:12 -05:00
Patrick Mulligan
e51f462876 fix(machine): align dispense error handling with legacy brain.js
dispenseCash now always resolves with a DispenseCashResult (per-bill
dispensed/rejected counts, overall success flag, optional error) instead
of throwing. dispenseError is a 30s timed state that auto-returns to
idle, matching brain.js _timedState('outOfCash'). The dead-end retry
loop (which the UI never exposed) is removed.

The Vue dispenseError screen now shows partial dispense info, the
transaction ID as a QR code, and a 30s countdown.

Closes #30

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:01:07 -05:00
Patrick Mulligan
45224d2aaf fix(machine): show price and balance badges on mobile
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 09:17:32 -05:00
Patrick Mulligan
b7858a08c7 perf: reduce CPU usage on Bay Trail ATM hardware
- Remove infinite box-shadow glow animations from Buy/Sell buttons
  on idle screen (expensive on CPU without GPU)
- Reduce BTC price polling from 10s to 30s
- Reduce LNURL-withdraw polling from 2s to 5s

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 14:26:42 -05:00
Patrick Mulligan
e3f376b462 feat: support VITE_FORCE_MOCK for demo/dev environments
When VITE_FORCE_MOCK=true, initialize with mock services directly
instead of requiring Electron or a live Lightning.Pub connection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 13:35:25 -05:00
Patrick Mulligan
c2880b187b fix(machine): fix TDZ error accessing isElectron before initialization
loadTheme() and loadColorMode() were called during module evaluation
before the isElectron const was declared, causing a ReferenceError in
Firefox's strict TDZ enforcement. Move isElectron above the ref() calls.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 11:31:32 -05:00
Patrick Mulligan
7a42380f0e fix(machine): add mobile responsive layout for atm.aiolabs.dev
The kiosk UI was optimized for 1920px touchscreens, breaking mobile web.
Uses Tailwind lg: breakpoints (mobile-first) so mobile works naturally
while kiosk sizing applies at 1024px+. No JS branching — pure CSS.

- Viewport: width=1920 → width=device-width
- Kiosk-only: overflow:hidden and cursor:none behind @media (min-width: 1024px)
- IdleView: stack buttons vertically on mobile, justify-around for even spacing
- CashInView/CashOutView: stack split panels on mobile (flex-col-reverse lg:flex-row)
- QRCode: SVG scales down on small screens via max-w-full
- App.vue: hide verbose badges on mobile, debug bar collapses theme/color selectors
- Connection/network badges always visible (not gated by isIdle)
- Reduce zone-glow animation intensity by half (20px/4px vs 40px/8px)
- All lg: values verified to match original kiosk rendering

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 23:41:18 -05:00
Patrick Mulligan
624650d3be fix(machine): hide theme toggle in dev mode when debug panel available
The debug panel already has full theme/color mode controls, so the
floating light/dark toggle is redundant in dev. Keep it for production.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 22:50:45 -05:00
Patrick Mulligan
254fbd26f2 fix(machine): UI polish — fiat rate display, 15s receipt with QR, remove CLINK
- Show exchange rate as fiat/BTC (e.g. Q615,000/BTC) instead of sats/fiat
- Show USD/BTC rate when currency != USD
- Complete screen stays 15s (was 3s) with txid QR code for receipt photo
- Add "Done" button for manual dismiss on complete screen
- Remove CLINK ndebit UI (mode selector, pubkey entry) pending k1 fix (#23)
- Remove askForReceipt/sendingReceipt screens (npub scan not active, #36)
- Remove negative sign on commission display
- Set timezone to America/Guatemala

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 15:36:18 -05:00
Patrick Mulligan
88689a7fd3 fix(machine): hide status badges during active transactions
The fixed-position BTC price, balance, and connection badges
overlapped with the CashIn/CashOut headers. Now only visible on idle.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 14:09:48 -05:00
Patrick Mulligan
398c1d76c6 feat(machine): show tx details on completion screen (principal, commission, total)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 13:58:39 -05:00
Patrick Mulligan
73feb2b0d2 feat(machine): omnipresent light/dark toggle and dynamic network badge
Move light/dark toggle with Lucide Sun/Moon icons from IdleView to
App.vue so it's visible on all screens. Detect Bitcoin network
(mainnet/testnet/regtest) from BOLT-11 invoice prefix and persist
in localStorage. Hide network badge on mainnet (implied).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 12:22:42 -05:00
Patrick Mulligan
3d9d85ba7f feat(machine): redesign cash-out with split-screen layout
Split-screen for displayingInvoice (info left, QR right), full-width
for selectingAmount. Add StepIndicator with warning flow accent header.
Replace Skeleton with BounceDots, remove ScrollArea and Card wrappers,
wrap states in Transition for fade animations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 12:18:46 -05:00
Patrick Mulligan
d3e652bce3 feat(machine): redesign cash-in with split-screen layout
Split-screen for insertingBills (amounts left, bill visual right) and
displayingQR (info left, QR right). Add StepIndicator with success
flow accent header. Replace Skeleton with BounceDots, remove ScrollArea
and Card wrappers, wrap states in Transition for fade animations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 12:17:28 -05:00
Patrick Mulligan
263165b495 feat(machine): redesign idle screen with circular touch zones
Full-canvas layout: brand zone with logo/badges/balance at top,
two circular touch zones (Buy=bitcoin orange, Sell=green) with
zone-glow animation. Show per-flow commission rates (Buy/Sell).
Move light/dark toggle to App.vue for omnipresence.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 12:12:09 -05:00
Patrick Mulligan
9cfd2f2907 feat(machine): add scanning frame to QR code component
Wrap QrcodeVue in animated scanning frame with 4 pulsing corner
brackets and a sweep line. Increase default size from 300 to 380
for split-screen contexts. Use font-mono-code for URI preview.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 12:11:28 -05:00
Patrick Mulligan
acb4a8ccaf feat(machine): add BounceDots and StepIndicator components
BounceDots: three animated dots replacing Skeleton shimmer loaders.
StepIndicator: dot-line-dot progress with active/complete states,
split into separate dots and labels rows for proper alignment.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 12:10:14 -05:00
Patrick Mulligan
80bc2bfd8a feat(machine): add Ubuntu font and kiosk animations
Self-host Ubuntu Regular/Bold and Ubuntu Mono woff2 for offline kiosk.
Add @font-face declarations, font-mono-code utility, dot-bounce loader,
QR scan line, corner pulse, zone glow, state-fade transition, and step
indicator CSS styles.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 12:09:42 -05:00
Patrick Mulligan
cea774c3ad feat(machine): add kiosk touch targets and viewport for 1920px
Scale up button sizes (kiosk, kiosk-lg, kiosk-icon variants), add
active:scale feedback, set viewport to 1920px fixed width, hide cursor,
and bump card title/description sizes for touch-screen readability.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 12:08:06 -05:00
Patrick Mulligan
e63f8ab9f6 fix(machine): seed cassettes on first boot + add atm-transactions script
Cassette inventory was never initialized in SQLite, so
recordTransaction's UPDATE decrements were no-ops against an empty
table. Now the Electron main process seeds cassettes from
VITE_LAMASSU_CASSETTES or the model preset on first boot.

Also adds an atm-transactions CLI script (with --summary, --inventory,
--type, --today, --last, --since filters) and sqlite to the NixOS
system packages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 11:55:00 -05:00
Patrick Mulligan
f6a1c2f9f6 fix(machine): fix TypeScript cast in useTheme isElectron check
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 20:22:58 -05:00
Patrick Mulligan
09611c0958 feat(machine): add light/dark toggle on idle screen
Subtle sun/moon button in bottom-left corner lets users switch
between light and dark mode. Persists via localStorage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 20:02:10 -05:00
Patrick Mulligan
a6d69a385c feat(machine): hide debug toggle in production, default to Catppuccin light
- Debug toggle button only shown when allowMockFallback is true
- Electron defaults to Catppuccin Latte (light) instead of Gruvbox dark
- Browser dev mode keeps Gruvbox dark as default

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 20:00:24 -05:00
Patrick Mulligan
e460765355 feat(machine): production safety — disable mock fallback and ndebit
When VITE_ALLOW_MOCK_FALLBACK is unset (production default), the ATM
now shows a maintenance screen instead of silently falling back to mock
services when hardware or Lightning initialization fails. Also disables
ndebit/CLINK in production since the static ndebit pointer is replayable
— cash-in uses LNURL-withdraw only (single-use by design).

- Add allowMockFallback config field (Electron IPC + types)
- Add strict config validation (no localhost, require private key)
- Gate all catch-block fallbacks behind allowMockFallback
- Disable debit approval service and ndebit generation in production
- Add maintenance screen in App.vue when initError is set

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 17:39:12 -05:00
Patrick Mulligan
03b5720883 fix(machine): clarify cash-in QR screen is for receiving, not paying
The displayingQR state showed "Waiting for payment..." with an hourglass,
which confused users into thinking they needed to pay. This is an
LNURL-withdraw flow — the user scans to *receive* sats.

- "Preparing payment code" → "Preparing your withdraw code"
- Add prominent "Scan to receive your sats" heading above QR
- "Waiting for payment..." → "Open your wallet and scan to claim"
- "Manual payment options" → "Manual withdraw options"
- "Payment Sent!" → "Sats Sent!"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 12:40:59 -05:00
Patrick Mulligan
1a452fe7ec fix(machine): replace remaining hardcoded colors with semantic classes
Replace bg-orange-500 lightning bolt overlay with bg-bitcoin, bg-white
receipt placeholders with bg-qr/text-qr-foreground, and remove redundant
bg-white QR wrappers in IdleView overlays (QRCode.vue already has bg-qr).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 11:04:19 -05:00
Patrick Mulligan
f5f00108aa feat(machine): add light/dark mode toggle with semantic color styling
Replace hardcoded Tailwind colors (bg-white/5, text-green-400, bg-black/30,
etc.) with theme-aware semantic classes (bg-card, text-success, text-bitcoin,
bg-destructive/20) across CashInView and CashOutView. Add colorMode support
to useTheme composable with localStorage persistence and system preference
detection. QRCode component now reacts to theme/mode changes via
MutationObserver. Debug panel includes Light/Dark/System toggle.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 10:57:33 -05:00
Patrick Mulligan
0406a21b0e fix(cash-in): restore cancel button and add displayingQR timeout
Cancel button is now always visible during the cash-in flow. The state
machine routes CANCEL to confirmAbandon when bills are present, so the
user always has an exit path with appropriate warnings. Also adds a
5-minute auto-timeout on displayingQR and allows cancel during
generatingNdebit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 20:19:01 -05:00