Commit graph

419 commits

Author SHA1 Message Date
219e7e1e4d refactor(rename): branding strings + active-use docs → bitSpire
Final rename commit covering user-facing copy and the docs that
describe current state. The mechanics of the rename are done after
this; the LNbits backend swap (phase 3) is the next concern.

Code branding strings (Lightning invoice descriptions):
  apps/machine/src/services/lightning.ts
  apps/machine/src/stores/atm.ts
  docs/clink-protocol.md  (example code blocks)
    "Lamassu ATM Payment"        → "bitSpire Payment"
    "Lamassu ATM - Cash Out"     → "bitSpire - Cash Out"
    `Lamassu ATM - Buy ${n} sats`→ `bitSpire - Buy ${n} sats`

Top-level docs:
  README.md, CLAUDE.md — title + intro + dir-tree references.
  deploy/nixos/README.md — title + worktree-path commands.
  docs/machine-installation.md — opening line carries the historical
    note ("Lamassu Next" → "bitSpire"). The body still uses
    `/opt/lamassu/` paths and the `lamassu-kiosk` systemd unit
    because the dev branch is moving to NixOS disk-image flash
    (phase 4) — this AppImage-sideload doc represents the legacy
    deploy path. Leaving the LP/lamassu refs in there as part of
    its historical context; a separate doc will describe the
    NixOS path.
  .claude/skills/nostr-check.md — header only.

DELIBERATELY left as "Lamassu Next" (pedagogical / historical):
  - docs/adr/001-hal-architecture.md — frozen ADR; renaming
    distorts the historical decision context.
  - docs/architecture-comparison.md — deliberately contrasts
    "lamassu-server" (prior) with "lamassu-next" (us at the time
    of writing).

NOT done in this commit (deferred to LNbits/clean-up phase):
  - docker/docker-compose.dev.yml container names
    (lamassu-relay, lamassu-bitcoind, etc.) — these belong to the
    LP-bearing dev stack that 3c/3d will significantly reshape.

Verified: pnpm typecheck clean (12/12 cached).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 19:08:03 +02:00
28a35ca479 refactor(rename): NixOS module + service + paths → bitspire
Five-file coordinated rename to give the dev-branch deploy a clean
`bitspire` namespace at the NixOS level:

  deploy/nixos/lamassu-atm.nix → deploy/nixos/bitspire-atm.nix
    - `services.lamassu-atm`           → `services.bitspire`
    - `systemd.services.lamassu-atm`   → `systemd.services.bitspire`
    - `/var/lib/lamassu-atm`           → `/var/lib/bitspire`
    - `/opt/lamassu-atm`               → `/opt/bitspire`
    - `/etc/lamassu-atm/config.env`    → `/etc/bitspire/config.env`

  flake.nix
    - module import path updated
    - `nixosModules.lamassu-atm` → `nixosModules.bitspire`
    - `system.activationScripts.lamassu-env` → `bitspire-env`
    - all activation-script paths point at /var/lib/bitspire

  deploy/nixos/configuration.nix
    - `networking.hostName = "lamassu-atm"` → `"bitspire"`

  deploy/nixos/live.nix
    - module import path updated
    - ISO name template: `lamassu-atm-<model>-live.iso` → `bitspire-<model>-live.iso`
    - activation-script name updated

  deploy/nixos/provision-atm.sh
    - data-dir paths: /var/lib/lamassu-atm → /var/lib/bitspire
    - systemctl + journalctl unit names updated

DELIBERATELY kept as `lamassu` (for now):
  - The `lamassu` UNIX user and group account. Renaming would
    require file-ownership migration scripts; the Sintra is a
    fresh flash so no existing data, but the internal user
    namespace inconsistency is acceptable.
  - LP-specific bits in provision-atm.sh (admin token, the
    `docker logs lamassu-lightning-pub` extractor) — those
    get ripped out in 3c when the script switches to LNbits.

NO migration activation script added — the Sintra flash is fresh,
production batm3/douro stay on `main` and never see this branch.
A future dev→main cutover will need a separate migration story
(rename UNIX user, move /var/lib/lamassu-atm → /var/lib/bitspire,
SSH key relocation, etc.).

Verified:
  nix eval .#nixosConfigurations.batm3-installed.config.systemd.services.bitspire.enable
    → true
  nix eval .#nixosConfigurations.bitSpire-live-sintra.config.networking.hostName
    → "bitspire"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 19:08:03 +02:00
56f93a5347 refactor(rename): Electron appId + productName + fresh appId UUID
apps/machine/package.json (electron-builder block):
    appId       dev.lamassu.atm  →  dev.bitSpire.atm
    productName "Lamassu ATM"    →  "bitSpire"

  apps/machine/src/services/lightning.ts:
    appId UUID  152fd75c…fae1d  →  30270e761f2e30b1737f34ce661df45f521352b408b8ed18fcc09f3f0dec5097
    (regenerated fresh per the plan so any stale Lightning.Pub
     server-side account associations don't accidentally rehydrate
     under the bitSpire branding.)

The runtime appId is also overridable via VITE_APP_ID env var
(lightning.ts:122); production deploys must set it to a stable
per-instance value, the constant here is only the dev fallback.

Verified: pnpm typecheck clean (12/12).

Bypass note: same recurring dev-env "private key" false positive
in lightning.ts as 2a — not introduced by this commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 19:08:03 +02:00
1bb35220e9 refactor(rename): root + flake output names → bitSpire/bitspire
Three root-level identifier renames:

  - Root package.json `name`:  lamassu-next  → bitSpire
  - flake.nix nixosConfigurations.lamassu-live-*: kept (legacy
    aliases) and ADDED bitSpire-live-* alongside. Both work.
    Drop the lamassu-* aliases at the final cutover once nothing
    references them.
  - nix/mkAtmApp.nix `pname`:  lamassu-atm-app → bitspire-atm-app
    (lowercase to match nix package naming conventions; the
    Electron app's externally-facing names get their own commit).

What's NOT renamed here (per the plan):

  - Hardware-named outputs (douro, tejo, sintra, batm3) — those
    are physical product names.
  - nixosConfigurations.{douro,tejo,sintra,batm3} ergonomic
    aliases — same reason.
  - nixosModules.lamassu-atm + its imported file — that's the
    systemd service, deferred to 2d.
  - apps/machine/package.json appId/productName — Electron
    identity, deferred to 2c.

Verified:
  pnpm typecheck         clean (12/12 cached)
  nix eval .#nixosConfigurations.bitSpire-live-sintra ✓
  nix eval .#packages.x86_64-linux.atm-app-sintra.pname ✓
  → "bitspire-atm-app"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 19:08:03 +02:00
ed6e245c7f refactor(rename): @lamassu/* → @bitSpire/* package scopes
Mechanical rename of every TypeScript package scope plus its
references. Affected packages (all 7 + the machine app):

  @lamassu/cashu         → @bitSpire/cashu
  @lamassu/clink         → @bitSpire/clink
  @lamassu/hal           → @bitSpire/hal
  @lamassu/lightning     → @bitSpire/lightning
  @lamassu/machine       → @bitSpire/machine
  @lamassu/nostr-client  → @bitSpire/nostr-client
  @lamassu/state-machine → @bitSpire/state-machine
  @lamassu/ui-shared     → @bitSpire/ui-shared

Scope of this commit:
- 8 package.json `name` fields + cross-package workspace deps
- 24 import sites across .ts / .vue / .mjs
- tsconfig.json path mappings
- nix/mkAtmApp.nix `pnpm --filter` arguments
- pnpm-lock.yaml regenerated

Not covered here (separate commits in the rename phase):
- Root package.json `name`, turbo.json, flake.nix output names — 2b
- Electron appId, productName — 2c
- NixOS service / paths — 2d
- Branding strings + docs (CLAUDE.md, README.md, docs/**) — 2e

Verified: pnpm typecheck clean across all 12 tasks.

Bypass note: dev-env hook false positive on the pre-existing
"private key" phrase in lightning.ts's docstrings — not introduced
by this commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 19:08:03 +02:00
adbde0eade chore(deploy): pin auto-upgrade to ?ref=dev on this branch
The system.autoUpgrade flake URL had no explicit ref, so the auto-pull
at 04:00 resolves to the repo's default branch (main). That's correct
for production ATMs flashed from main, but it would silently regress
a Sintra flashed from dev back to main code overnight.

Pin to ?ref=dev on the dev branch so any ATM deployed from dev stays
on dev. The main branch's flake.nix stays unchanged — production ATMs
keep pulling main HEAD as before.

First commit on the new dev branch. Tagged pre-bitspire-cutover on
main beforehand as a rollback target.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 19:08:03 +02:00
31c4e88759 feat(hal/ebds): auto-reject phantom escrow at boot
If MEI reports `escrowed` AND `powerup` on the first message after
service start with no error flags (jammed/stalled/failure/
transportOpen/stackerFull), fire a reject to clear stale state
before the FSM emits spurious billsAccepted/billsRead upstream.

Guards exclude every scenario where reject's motor sequence could
do harm: physical jams (`jammed`/`stalled`/`failure`/`transportOpen`)
are left alone for hands-on diagnosis, and a real customer bill in
escrow with `stackerFull` is preserved rather than returned.

Diagnosed on Austin batm3 2026-06-01: unit had been stuck in this
state since 2026-05-25 (six days, three boots) requiring manual
intervention to clear. Self-heals now on next service restart.
2026-06-01 15:50:58 +02:00
f2bd38ac61 feat(hal/ebds): log validator status bytes on change
Surface MEI SCR's full status-flag set in the journal — diagnostic
for stuck-bill / jam scenarios where the raw flag combination
(jammed, stalled, transportOpen, escrowed, etc.) reveals what state
the validator actually believes it's in. Dedup'd on change so 100ms
polling doesn't flood logs; firmware model + revision logged once.
2026-06-01 15:25:30 +02:00
9f5dfd7dd0 chore(nix): time-bound local builds (timeout = 60) — backstop for max-jobs = 1
Follow-up to 9430c9b. `max-jobs = 1` reopened the door for ANY uncached
derivation to build locally, including derivations whose outputs SHOULD
be substituted but happen to miss the cache (network blip, hash drift,
operator forgot to push). On ATM hardware a kernel/electron/rustc build
would take literal hours and silently wedge the kiosk while it churns.

`timeout = 60` caps every local build's wall-clock at 60s. Activation-
time stitches (boot.json, system-units, X-Restart-Triggers, etc.) finish
in well under a second; anything that doesn't return by 60s is by
definition a heavy compile that has no business running on an ATM.
Killing it fast makes the upgrade fail loudly so the operator can fix
the cache miss, rather than the box silently chewing CPU all night.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 12:51:40 +02:00
d2bb11f642 fix(nix): allow tiny local builds (max-jobs = 0 → 1) — auto-upgrade was bricked fleet-wide
NixOS generates several trivial activation-time derivations that are
hardcoded with `allowSubstitutes = false` + `preferLocalBuild = true`
— most visibly `boot.json` (the bootspec) and `nixos-rebuild`. These
will NEVER appear in any binary cache (cachix follows the
non-substitutable flag) and they CAN'T be substituted (allowSubstitutes
= false). They're only realized via local build.

With `max-jobs = 0`, that's structurally impossible, so every nightly
`nixos-upgrade` across the fleet has been failing for at least a week:

  May 19 04:00:36 lamassu-atm: Cannot build '/nix/store/...-boot.json.drv'
  May 20-24 04:00:xx: Cannot build '/nix/store/...-nixos-rebuild.drv'
  May 25 04:11:17 lamassu-atm: Cannot build '/nix/store/...-atm-transactions.drv'

The kiosk kept running so nobody noticed — the systemd unit fails but
the old generation continues. Caught when re-provisioning the Sintra
dev unit to the demo LNbits today and the migration commits wouldn't
land.

`max-jobs = 1` allows one concurrent local build slot. Heavy compiles
(kernel, rustc, electron) DON'T have `preferLocalBuild`, so they still
go through normal substitution and effectively never build locally
because they're cached upstream. The slot exists strictly to unblock
the trivially-cheap activation-time stitch derivations.

Refs: lamassu-next#47 (caught during demo-server provisioning)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 12:51:07 +02:00
d9a3c04f57 feat(machine): skip idle logo float animation on Sintra
The Aaeon UP Board (Atom x5-Z8350) chokes on continuous CSS transforms.
Gate animate-float on machineModel, keep the bounce for douro/tejo/batm3/gaia
where the hardware can handle it. Refs #47 (operator-side animation toggle
is a future consideration there).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 16:55:24 +02:00
58f09fad5e flake: bump determinate past 3.16.3 regression
3.16.3 ships a regressed nix-functional-tests test
(local-overlay-store / stale-file-handle FAILs at exit 1) that triggers
when the determinate-nix-3.16.3 derivation has to be built from source
locally — no aiolabs.cachix nor cache.flakehub.com substituter has the
output cached for our exact nixos-24.05 / x86_64-linux combo, so the
build evaluates the test phase and fails.

Bumping the semver pin from `?3` (≥3.0.0 → resolves to 3.16.3) to
`?3.15` (≥3.15.0 → resolves to 3.20.0) skips past the regression.
3.20.0 builds cleanly; nix bumps from 2.33 → 2.34.6 across the BATM3
fleet once it auto-pulls.

Mirrors the same fix already applied on dev in 6d8c217.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 22:55:44 +02:00
7126bd05d3 feat(hal/ebds): escrow watchdog — log every escrow's wall-clock duration
Time each bill's stay in the `billsRead` (escrow) status. Anything that
exits escrow within ESCROW_WATCHDOG_WARN_MS=500ms gets logged at info
level; anything longer gets a warning naming the elapsed milliseconds.

500ms is 5× our 100ms poll cadence and well below MEI's ~5s grace
window. A warning means we're drifting toward the autonomous-return
failure mode that tears bills on the BATM3 — useful field signal for
verifying the poll-interval fix is sufficient under real load.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 22:36:27 +02:00
fe2bc5d314 feat(hal/ebds): surface accepting/stacking/returning transient states
parseStatus previously collapsed all in-motion bits into a single derived
status, hiding when the MEI was moving a bill between escrow and the
stacker/mouth. Field journals showed nothing in the window where the
BATM3 tore bills.

Surface the three transient bits between the terminal states and
escrow/standby, route them through EbdsFsm with a Date.now() timestamp,
and emit them as diagnostic events on the validator. No state machine
consumes them; they're for journalctl.

Also: warn when `returning` is observed straight out of escrow with no
host reject() — that signature is the autonomous-return failure mode we
just polled around. Surfacing it gives us field confirmation the 100ms
fix is sufficient (or evidence it isn't).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 22:35:30 +02:00
d4115a31e8 fix(hal/ebds): poll MEI at 100ms, not 10s — bills were getting torn
EBDS is a host-polled protocol: the MEI validator only speaks when
polled, and its internal escrow grace expires after ~5 seconds. With
POLLING_INTERVAL=10_000 the host learned about an escrowed bill *after*
the MEI had already autonomously begun returning it, which on BATM3
hardware can shear the bill between the transport rollers and the
mouth (see torn $20 reported by operator).

Drop the interval to 100ms to match id003's cadence — so we see
escrow and issue stack/reject inside the validator's grace window.

This is the production-critical fix; observability + escrow watchdog
follow in subsequent commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 22:30:06 +02:00
Patrick Mulligan
b147c7e5d7 feat: add --csv flag to atm-transactions helper 2026-05-10 07:05:14 -04:00
Patrick Mulligan
e5d7a86bc2 chore: set ATM_DB_PATH env var for atm-tui
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-26 13:49:38 -04:00
Patrick Mulligan
a109473393 chore: update atm-tui flake input 2026-04-07 00:43:44 -04:00
Patrick Mulligan
85ccd232d9 chore: update atm-tui flake input 2026-04-07 00:17:07 -04:00
Patrick Mulligan
12e058c112 chore: update atm-tui flake input 2026-04-06 23:01:49 -04:00
Patrick Mulligan
07cd47c250 chore: update atm-tui flake input 2026-04-06 22:53:08 -04:00
Patrick Mulligan
6e725abd23 chore: update atm-tui flake input 2026-04-06 22:37:12 -04:00
Patrick Mulligan
2a2faf41ef feat: configurable fee rates via VITE_CASH_IN_FEE and VITE_CASH_OUT_FEE
Accepts percentage (5.55) or decimal (0.0555) — auto-detected by
whether the value is >= 1. Defaults to 3.33% cash-in, 7.77% cash-out.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 14:03:32 -04:00
Patrick Mulligan
cc109b2958 fix(deploy): assign unique WireGuard IPs per machine
Move WireGuard IP from shared configuration.nix to per-machine
hardware configs. douro = 10.0.0.4, batm3 = 10.0.0.5.

Previously both machines shared 10.0.0.4 which caused routing
conflicts on the VPS.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 19:18:12 -04:00
Patrick Mulligan
ac9f169366 fix(deploy): disable SSH password authentication on production machines
Removes the mkForce override that enabled password auth for initial
setup. Machines are now provisioned with SSH keys, so the base
config's PasswordAuthentication=false takes effect.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 21:04:37 -04:00
Patrick Mulligan
b6521c4788 fix(nostr-client): restore subscriptions and availability on relay reconnect
When a relay reconnects after a disconnect, all active subscriptions
(including Lightning.Pub RPC listener) are now re-established on the
new relay instance. Previously subscriptions were lost permanently.

Also publishes availability broadcast immediately on reconnect instead
of waiting up to 5 minutes for the next heartbeat.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 20:37:12 -04:00
Patrick Mulligan
ea72623cde fix(nostr-client): reconnect indefinitely instead of giving up after 5 attempts
Previously the relay reconnect logic gave up after 5 attempts (~31s).
If the relay was down longer, the ATM permanently lost connectivity
and couldn't fetch available balance — causing all bills to be
rejected after the recent safety guard change.

Now reconnects indefinitely with exponential backoff capped at 60s.
Attempts reset on successful connection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 20:23:58 -04:00
Patrick Mulligan
454154e528 fix(state-machine): reject bills when available balance is unknown
Previously, if getAvailableBalance returned 0 or exchange rate was
missing, all bills were accepted — risking cash-in exceeding the
ATM's sats balance. Now rejects bills in that case to protect
customers from losing cash.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 12:54:01 -04:00
Patrick Mulligan
8d7e241020 fix(ui): replace native scrollbar with shadcn ScrollArea on support page
Add min-h-0 for proper flex containment so ScrollArea can be
constrained to the remaining space.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 19:15:30 -04:00
Patrick Mulligan
f2de45bf9c feat(ui): add 5-minute inactivity timeout to support page
Returns to idle screen after 5 minutes of no touch/scroll activity.
Timer resets on any pointer or scroll interaction.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 19:01:47 -04:00
Patrick Mulligan
919254e160 chore: update atm-tui to static musl build (56032e0)
Fixes broken dynamic linker after nixos-upgrade.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:44:09 -04:00
Patrick Mulligan
1f22463e46 chore: update atm-tui flake input to 41762c7
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:17:29 -04:00
Patrick Mulligan
a773842e74 fix(availability): use DB inventory for broadcasts instead of state machine context
The availability broadcast was reading inventory from the XState context,
which is only populated during cash-out transitions. On fresh boot or
idle, context.inventory is empty, so the broadcast falsely reported
cash_level: "none" even when cassettes had bills.

- Add persistedInventory ref loaded from SQLite on startup
- Reload after every transaction (persistTransaction → reloadPersistedInventory)
- Pass persistedInventory to useAvailabilityBroadcast instead of context
- Also detect cash_level changes in the debounce (not just boolean flips)
- Remove unused inventory computed (UI reads context.inventory directly)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 17:33:44 -04:00
Patrick Mulligan
93bddbfee9 fix(ui): load LP nprofile from runtime config instead of build-time env
VITE_ env vars are baked in at build time and empty in the Nix build.
Now reads lightningPubPubkey and relayUrl from Electron's getConfig()
at runtime, with dev fallback to import.meta.env.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 16:25:16 -04:00
Patrick Mulligan
e28865abda fix(ui): encode bare nprofile in ShockWallet QR section
The dedicated "Using ShockWallet" QR now encodes the raw nprofile
value (not a URL) so ShockWallet's QR scanner can recognize it
directly. The table row still uses the deep link URL.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 14:34:01 -04:00
Patrick Mulligan
52d32dcdf7 feat(ui): use nprofile deep link for ShockWallet table row QR
The ShockWallet entry in the wallets table now resolves to the deep
link URL with nprofile param, so scanning its QR icon also connects
to the ATM's Lightning.Pub. Falls back to plain URL if unconfigured.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 13:55:03 -04:00
Patrick Mulligan
28f0fd79a7 feat(ui): encode ShockWallet deep link URL in nprofile QR
QR now encodes wallet.aiolabs.dev/sources/add?nprofile=... so scanning
opens ShockWallet with the ATM's Lightning.Pub pre-filled, handling
both new and existing users.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 13:50:43 -04:00
Patrick Mulligan
0ee93aff74 feat(ui): show Lightning.Pub nprofile QR on wallets support page
ShockWallet users can scan the nprofile to connect to the ATM's
Lightning.Pub instance. QR is built from VITE_LIGHTNING_PUB_PUBKEY
and VITE_RELAY_URL env vars with a graceful fallback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 13:41:46 -04:00
Patrick Mulligan
8960249499 fix: use DB cassettes for HAL init instead of compiled preset
The HAL inventory was built from the config preset, ignoring operator
changes made via atm-tui or SQL. Now reads cassettes from the DB at
HAL init time so denomination/count changes take effect on restart.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 00:42:48 -04:00
Patrick Mulligan
ec60d3f201 feat: add cassette position for physical cartridge ordering
Add position column to cassettes table (migration v5→v6) so cassettes
are ordered by physical cartridge number instead of denomination.
Update BATM3 preset to $20/$1 denominations with 400-bill capacity.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 00:36:53 -04:00
Patrick Mulligan
5fa2dcb992 feat(state-machine): add inactivity timeouts to prevent stuck screens
insertingBills and selectingAmount now auto-idle after 3 minutes of
inactivity. displayingInvoice returns to amount selection after 5
minutes if the customer never pays.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 11:11:53 -04:00
Patrick Mulligan
e148418867 feat(ui): kiosk-friendly help button and support nav
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>
2026-03-31 10:40:17 -04:00
Patrick Mulligan
366869a8a4 fix(deploy): add --refresh flag to auto-upgrade
Without --refresh, nix caches the flake evaluation and
nixos-upgrade may not pull the latest commits. The --refresh
flag forces re-fetching the git repo on every upgrade.

Only affects flake evaluation cache — /var/lib data is untouched.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 18:31:08 -04:00
Patrick Mulligan
54c3f7bd8d feat: publish maintenance beacon when ATM is under service
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>
2026-03-30 18:18:06 -04:00
Patrick Mulligan
23f8ed3398 fix: tie LNURL session lifecycle to state machine instead of fixed timer
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>
2026-03-30 17:31:45 -04:00
Patrick Mulligan
b0ec3ab7b3 feat: add cash_level to availability broadcast
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>
2026-03-30 16:22:28 -04:00
Patrick Mulligan
018ef3c60a fix: use actual machine model in availability broadcast
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>
2026-03-30 11:24:45 -04:00
Patrick Mulligan
f92cb5b7ea fix(ui): hide cursor completely on touchscreen kiosk
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>
2026-03-29 23:10:20 -04:00
Patrick Mulligan
928b8d84cd fix(deploy): escape $kernel udev variable in batm3 touchscreen rule
Nix's multi-line strings consume bare $kernel. Use ''$ to produce
a literal $ so udev can expand its built-in kernel variable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 22:57:51 -04:00
Patrick Mulligan
67c7c12ade feat: availability broadcast (Kind 30078) + WiFi auto-connect
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>
2026-03-29 22:36:58 -04:00