Add circular outline to the ? help button on idle screen and scale
support page navigation buttons for touchscreen kiosk use.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
In maintenance mode, establish a minimal Nostr connection (no
Lightning.Pub) and publish Kind 30078 heartbeat with
maintenance: true. Monitors show yellow dot + "under service"
instead of appearing offline.
Only the Nostr client is initialized — no payment infrastructure.
Same one-shot private key security model as normal operation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The LNURL-withdraw session had a fixed 5-minute expiry timer that
raced with the state machine's displayingQR timeout (also 5 min).
If the session timer fired first, the withdraw link was deleted
while the customer could still retry from confirmAbandon.
Now LNURL sessions are cleaned up by the state machine on idle
transition instead of a fixed timer. A 15-minute safety timeout
remains as a fallback in case the state machine doesn't clean up.
Flow: displayingQR (5min) → confirmAbandon (60s) → idle → cleanup.
The withdraw link stays alive the entire time the customer can
interact with it.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Publish cash level (none/low/good/full) in the Kind 30078 event
based on total bill count across all cassettes. Enables monitoring
dashboards to show cash availability without revealing exact amounts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Kind 30078 availability event was using 'atm' as the model
placeholder. Now reads the actual model from runtime config
(batm3, douro, sintra, etc.) so monitoring dashboards can
distinguish between different ATM types.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace cursor: default (which showed pointer on buttons) with
cursor: none !important on all elements. Touchscreen ATMs don't
need a visible cursor — taps register via touch coordinates.
Reverts the earlier cursor: default addition and fixes the
pre-existing issue of pointer cursor showing over buttons.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wire up the availability broadcast composable to publish the ATM's
status as a replaceable Kind 30078 Nostr event. Publishes on
availability change (debounced) and as a 5-minute heartbeat so
monitors can detect offline machines.
Also adds WiFi auto-connect for BATM3: reads SSID/PSK from
/var/lib/lamassu-atm/wifi.conf at boot. Credentials stay local.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add support/help pages accessible via a ? button on the idle screen.
Pages are driven by .md files in /var/lib/lamassu-atm/support/ —
operators can customize content without rebuilding the app.
Features:
- Tabbed view with markdown rendering (via marked)
- Standalone URLs auto-render as QR codes (scannable from phone)
- Table URLs: click-to-reveal QR codes (prevents accidental scans)
- Yes/No rendered as green checkmarks / red X marks
- Wallet comparison table with download QR codes
- FAQ with Lightning-only clarification
- Support page with operator Nostr QR placeholder
- Custodial vs non-custodial footnote
- Large text for touchscreen accessibility
- Centered layout for short-content pages
Closes#36
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Set VITE_MAINTENANCE_MODE=true in .env to show an "Under Service"
screen and block all transactions. No hardware init, no Lightning
connection — just a static screen.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Electron hides the cursor over non-interactive elements when running
without a desktop environment. Add cursor: default to html/body so
the mouse pointer is always visible when using an external mouse or
touchscreen.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>