The aliases were added for the brand transition with a note to drop them
once nothing referenced them. Nothing does. The autoUpgrade comment still
said the legacy aiolabs/lamassu-next repo fed batm3 and douro; every live
machine pulls from this repo now (CLAUDE.md → Branch model).
MACHINE_MODEL, FIAT_CODE, VALIDATOR_DEVICE, DISPENSER_DEVICE and CASSETTES
carried the old brand in their names. Renamed everywhere they are read
(device.ts, electron/main.ts), written (flake.nix, mkAtmApp.nix, live.nix,
provision-atm.sh, factory-reset-atm.sh) and documented (.env.example,
docs/device-configuration.md). No compatibility fallback in code: the
machine reads VITE_BITSPIRE_* and nothing else.
The deployed .env files are the one place the old names persist — sintra's
/var/lib/bitspire/.env holds all three keys today — and the machine reads
MACHINE_MODEL / FIAT_CODE / CASSETTES from that file on every boot. Renaming
the keys in code alone would boot a live machine on preset defaults (wrong
bays, wrong fiat) at the next nightly pull. So configuration.nix gains an
activation script, beside the existing lamassu→bitspire user migration,
that rewrites VITE_LAMASSU_* → VITE_BITSPIRE_* in that file. Idempotent;
runs before bitspire.service starts.
`networking.wireguard.interfaces.wg0.ips` was set in hardware/douro.nix
and hardware/batm3.nix, but hardware/upboard.nix is shared by tejo and
sintra — an address there would be claimed by both machines on the same
/24, so neither got one. tejo therefore evaluated to `wg0.ips = [ ]`:
the interface comes up with no IP and the tunnel is silently dead. On a
machine with no other route in, that is how you lose a box.
Replace the two per-hardware definitions with one `wireguardIpForModel`
table in flake.nix, keyed on model like fiatCodeForModel /
upgradeWindowForModel / nfcReaderForModel, and give tejo 10.0.0.3/24 —
the address it answers on today under its factory Debian.
douro (10.0.0.4/24) and batm3 (10.0.0.5/24) evaluate unchanged; sintra
stays deliberately unlisted, since it is reachable on the LAN and has
never had a tunnel address.
The address is only half of it: the VPS maps peer pubkey to tunnel IP,
so the machine still needs /var/lib/wireguard/wg0.key carried over from
its previous install (or a fresh key added to the VPS peer list). Both
wireguard units are ConditionPathExists-guarded on that key, so a
keyless first boot is clean and the tunnel starts once it is dropped in.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The tejo still runs its factory Debian (ubilinux4, kernel 4.9) on
internal storage and has never had bitspire on it. Rather than flash
that drive, give it the run-from-USB shape douro and batm3 already use:
the stick is the system and the internal install is never touched.
- nixosConfigurations.tejo-usb — tejo-installed + usbBootModule +
usbBusHardening + usbGrubHybridModule. Evaluates identically to
sintra-usb, which shares hardware/upboard.nix.
- packages.disk-image-tejo-usb — hybrid table, GRUB, BIOS + UEFI.
NOT the efi/systemd-boot shape douro uses. The tejo is the same Aaeon
UP Board as sintra, whose firmware was found to USB-boot in Legacy/BIOS
mode; systemd-boot is UEFI-only, so a dd'd systemd-boot stick would not
be recognised as bootable at all. The hybrid image boots either path, so
it is also the safe choice if the firmware turns out to differ.
README documents both bootloader shapes and which models take which.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
disk-image-sintra-usb was a 60-line inline copy of everything
mkUsbDiskImage already does, plus the GRUB/hybrid bits the Aaeon
firmware needs — so the two implementations had already drifted: the
sintra image never picked up the `nofail` /boot that keeps a slow
ESP-USB enumeration out of emergency mode, nor the uas/autosuspend
hardening batm3.nix and douro.nix carry.
- mkUsbDiskImage takes named args with `partitionTableType` ("efi" for
systemd-boot, "hybrid" for GRUB) and `grubBiosDevice`. The ESP relabel
is layout-independent: the hybrid table creates the ESP first and
bios_grub second, so it stays partition 1 either way.
- New `usbGrubHybridModule` + `usbBusHardening` modules. The hardening is
scoped to the -usb configs rather than hardware/upboard.nix, which
sintra's eMMC install also reads — no cmdline change on a production
machine.
- `nixosConfigurations.sintra-usb` is now a named config, so a running
stick can be updated in place (nix copy + switch-to-configuration)
like batm3-usb and douro-usb.
- grub.devices is "nodev" in the config and mkForce'd to the build VM's
disk only for the image: an in-place switch on a live stick has no
/dev/vda, and GRUB's embedded core.img reads grub.cfg off the
partition, so the MBR stage needs no per-generation rewrite. Plain
definition rather than mkForce, since two mkForce lists merge into
[ "/dev/vda" "nodev" ] instead of replacing.
douro-usb and batm3-usb evaluate to byte-identical kernelParams,
blacklistedKernelModules, fileSystems, bootloader and autoUpgrade
config as before.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pcscd was enabled in hardware/batm3.nix and hardware/upboard.nix, which
cannot express "is a reader fitted": upboard.nix is shared by sintra (HID
Global OMNIKEY 5022) and tejo (nothing fitted), so tejo inherited pcscd it
has no use for, while the douro — with its own hardware file — got none and
wedged on every boot.
Make it a machine capability instead. services.bitspire.nfc.enable owns
pcscd, the two polkit rules and the wedge-recovery unit, and hands the app
a BITSPIRE_NFC_ENABLED flag so it doesn't initialise nfc-pcsc at all on a
machine with no reader. Per-model truth lives in nfcReaderForModel in
flake.nix next to fiatCodeForModel and upgradeWindowForModel, since a
shared hardware file can't answer the question. batm3 and sintra are true;
douro and tejo flip to true when readers are fitted.
The flag goes through the unit's Environment rather than
/var/lib/bitspire/.env, because .env is only written when absent — a
machine provisioned months ago would never pick up a new value.
The douro cutover to bitspire is being done remotely with a USB stick
and the machine's internal drive is not NixOS, so the stick has to be
the system rather than an installer medium. Give douro the same
run-from-USB shape batm3 already has.
flake.nix
- Lift the batm3-usb module and image post-processing into shared
`usbBootModule` / `mkUsbDiskImage` helpers (distinct nixos-usb/ESP-USB
labels, nofail /boot, no growPartition, autoUpgrade off, ESP relabel).
batm3-usb evaluates to the same fileSystems/upgrade config as before.
- Add `nixosConfigurations.douro-usb` and
`packages.disk-image-douro-usb` on top of douro-installed.
douro.nix
- Blacklist uas and set usbcore.autosuspend=-1, the same bus-drop
hardening batm3.nix carries, so a stick is a reliable boot medium on
the Bay Trail box.
README
- Document the -usb outputs and the flash-with-Etcher, no-installer flow.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The kiosk has launched with --disable-gpu AND
--disable-software-rasterizer since the first ISO commit (19d43c2).
Together those turn off GPU compositing and the SwiftShader fallback,
leaving Chromium to rasterise every pixel on the CPU — on Atom-class
hardware, for no reason anyone wrote down. No comment, no issue, no
commit message ever justified the pair, and /etc/bitspire/config.env has
claimed ELECTRON_DISABLE_GPU=false the whole time, contradicting the
actual command line.
Tested on sintra today. With the flags gone the GPU process is stable —
zero crashes, zero service restarts — and genuinely on hardware:
/proc/<gpu-pid>/maps shows libgallium, libGLX_mesa and dri_gbm, with no
swrast and no SwiftShader. It renders through crocus on Braswell.
Confirmed by eye on the panel, which is the part no log could answer.
Worth noting what the first attempt looked like, because it read as a
failure and was not. Restarting the unit logged "GPU process exited
unexpectedly: exit_code=15" and "has crashed 1 time(s)" — but those came
from the OUTGOING process being SIGTERMed by the restart. The incoming
one logged nothing. Checking crash counts and the gpu-process pid across
an interval, rather than grepping the last sixty lines, is what separates
the two.
DOURO KEEPS THE OLD FLAGS. Bay Trail already carries three display
workarounds — a 5.15 kernel pin for an i915 eDP regression,
i915.enable_psr=0, and vt.handoff=7 to preserve the BIOS display init —
which makes it the one machine where the original flags plausibly fixed
something real rather than being bring-up scaffolding. It is also down
pending a reflash, so it cannot be tested. Shipping an untested display
change to the most display-fragile box in the fleet, to be discovered
whenever it comes back, is not a trade worth making for one machine's
frame rate. Drop the exemption once douro is back and accelerates
cleanly.
The env override still works on every machine, douro included, so this
can be flipped either way without a rebuild.
An upgrade restarts the app and the Fujitsu dispenser runs an audible
init routine when it does. On sintra that was firing at 10:17 in the
morning, in the room, because `dates = "04:00"` is local time and every
machine inherits America/Guatemala from the shared base config.
The obvious fix is to set the system timezone per machine. This does the
narrower thing instead: systemd 252+ accepts a timezone suffix on a
calendar spec, so the timer follows Europe/Paris and its DST while the
system clock stays a fleet default nobody maintains per host. The only
other consumer of machine-local time is a technician reading the journal
at the machine, and for correlating against relay created_at stamps, UTC
is easier anyway.
The operator dashboard never needed this. It renders timestamps in the
reader's own browser locale, which is why the cassettes tab read
correctly while the journal did not.
Verified by resolving the config for all four hosts, and against
systemd-analyze on sintra itself: 04:00 Europe/Paris is 02:00 UTC in
summer, 04:00 America/Guatemala is 10:00 UTC.
A warning is in the comment because it nearly caught me: the same check
under `nix-shell -p systemd` silently computes EVERY named zone as UTC
while echoing the zone back in its normalized form. It looks accepted and
is wrong. Test on a real system.
The kiosk has launched with --disable-gpu AND
--disable-software-rasterizer since the first ISO commit (19d43c2).
Together those turn off GPU compositing and the SwiftShader fallback,
which leaves Chromium rasterizing every pixel on the CPU. On a Bay Trail
Atom that is expensive, and it is very likely the largest single
contributor to a sluggish UI.
Nothing in git ever justified the pair. There is no comment, no issue and
no commit message about it; the flags arrived with the original hardware
bring-up and were carried through every refactor since. The descriptive
config at /etc/bitspire/config.env has even claimed
ELECTRON_DISABLE_GPU=false this whole time, contradicting the actual
command line. So this looks like bring-up scaffolding rather than a
diagnosed workaround, and it is worth re-testing now that the Mesa work
gives known-good crocus and iris drivers for all three GPU generations in
the fleet.
Testing it by rebuilding is the wrong loop. These are remote machines
with no one at the screen, a wrong flag is a black display, and each
attempt is a large closure copy over WireGuard. So the GPU flags move out
of ExecStart into a shell variable read from /var/lib/bitspire/.env: set
BITSPIRE_ELECTRON_GPU_FLAGS, restart the unit, look at the panel. A bad
value is one edit and a restart away from being undone.
Behaviour is unchanged by default. The variable uses ${VAR-default}, not
${VAR:-default}, so an absent line means today's flags while an
explicitly empty value means no GPU flags at all, i.e. full acceleration.
That distinction is the whole point and is why the .env template ships
the line commented out rather than set: a present-but-empty value would
silently enable the GPU on every machine that regenerates its .env.
The live ISO takes the same launcher via specialArgs, so the ISO and the
installed image cannot drift apart on this.
Closure is unchanged at 4604MB.
The kiosk's <title> still read "Lamassu ATM" — visible as the browser tab
on the public demo, and inherited by the Electron window. The product has
been bitSpire since the rename; Lamassu belongs in the provenance credits
(README, the c0b69d1 boundary note), not on the artifact.
Rename the user-facing labels that ship: the page title, the flake
description (surfaces in `nix flake metadata`), the ISO build banner, the
header comments on the live-USB config / udev rules / app derivation that
land on the machine image, and the workspace packages' descriptions.
Deliberately NOT touched, because they are identifiers rather than labels
and renaming them has deployed-machine consequences:
- VITE_LAMASSU_MACHINE_MODEL / VITE_LAMASSU_FIAT_CODE (provisioned .env)
- LamassuEventKind (exported enum)
- localStorage keys lamassu-theme / lamassu-color-mode (would reset
every machine's stored theme)
- docker container names + devenv scripts (dev-only)
- the packages/hal Cargo crate name
Hardware names in HAL driver comments ("Lamassu Sintra", "Douro", "Tejo")
stay: those are the physical machines' real names — that IS the credit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013A6683cCHnQxFUosx1krY4
Lift the USB-variant config (distinct fs labels, nofail /boot, no
growPartition, autoUpgrade off) out of the inline disk-image-batm3-usb
`let` into `nixosConfigurations.batm3-usb`, and build the disk-image from
that same config. Enables in-place app deploys to a running stick via
`nix copy` + `switch-to-configuration` (build the toplevel, copy the
closure, activate) — no reflash, preserving pairing + /var/lib state.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a USB-bootable BATM3 disk-image target plus the batm3 hardware
changes that make a dd'd USB stick boot reliably on the Dell 9030 AIO.
flake.nix — new `disk-image-batm3-usb` target:
- Distinct partition labels (nixos-usb / ESP-USB) so stage-1 by-label
resolution can't latch onto an internal SATA drive that already holds
a generic nixos/ESP-labelled install. Post-build mlabel relabels the
ESP FAT volume to ESP-USB (bootloader files untouched; UEFI still
loads /EFI/BOOT/BOOTX64.EFI).
- /boot mounted nofail + short device-timeout: the firmware already
loaded the bootloader before Linux; without nofail a slow/late ESP-USB
enumeration drops to emergency mode with root locked — a dead end.
- NO growPartition/autoResize on the USB image: sfdisk rewriting the
partition table on first boot is the single most bus-stressing write,
and flaky USB bridges drop off the bus mid-rewrite (sfdisk wedges in
uninterruptible D-state and ESP-USB vanishes with the device, so /boot
times out too). Persistent state is a few MB and the image already
ships ~2GB free in root. The internal-SATA disk-image-batm3 keeps
growPartition — a real AHCI SSD won't drop the bus.
- autoUpgrade off (test image, not a managed fleet member).
batm3.nix — USB-boot reliability:
- Add usb_storage to initrd.availableKernelModules so stage-1 binds the
stick and /dev/disk/by-label/* appears.
- Blacklist uas + usbcore.autosuspend=-1: force the slower-but-reliable
Bulk-Only Transport path and stop the boot medium being power-suspended
mid-I/O — both were causing "device offline error" bus drops.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
batm3-installed existed as a nixosConfiguration but had no dd-able disk
image (only the live ISO, which is tmpfs — no persistent state.db/.env).
Mirrors the douro/sintra make-disk-image blocks, with one improvement:
boot.growPartition + fileSystems."/".autoResize so the root partition
and ext4 expand to fill the target drive on first boot. Flashing is
dd-and-done — no manual parted/resize2fs — and the full drive is
available to the nix store from day one (the #55 headroom lesson).
Image-only override via extendModules: the running system's
batm3-installed config (what auto-upgrade rebuilds against) is
unchanged.
Build: nix build .#disk-image-batm3 → result/nixos.img
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The bitspire-env activation seeds .env only when ABSENT (never refreshes on
redeploy), and env WINS over the pairing seed — so any value written at first
boot is frozen for the disk's life and silently masks the seed's source. That's
how a dead relay.aiolabs.dev and a provisioned VITE_OPERATOR_PUBKEYS made stale
installs "work" while a fresh machine broke.
Seed ONLY image-baked, non-maskable values (model, fiat, ELECTRON_FORCE_PROD,
DISPLAY, empty VITE_SPIRE_SEED placeholder). Relay + server pubkey come from the
seed; operator pubkey + fee config come from LNbits over the transport — so those
keys are no longer pre-seeded at all. VITE_RELAY_URL / VITE_LNBITS_SERVER_PUBKEY
are emitted only when the operator deliberately pins them via the Nix options (an
explicit override). Also drops the inert RELAY_URL/LNBITS_SERVER_PUBKEY lines from
/etc/bitspire/config.env (never loaded — EnvironmentFile is forced to .env).
Verified: built sintra-installed .env template is 5 lines, 0 maskable vars.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The bitspire-env activation seeded VITE_RELAY_URL from the relayUrl option
(default wss://relay.aiolabs.dev). Because env wins over the pairing seed, every
fresh machine pinned itself to that relay — which is dead — so a scanned seed's
relay was ignored ("No connected relays"; hit live on the aio-demo USB). Default
relayUrl to "" so both relay and server pubkey come from the seed; a non-empty
option now pins a machine (an explicit override) rather than being the default.
Descriptions updated to match.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The USB disk image was systemd-boot (UEFI-only) with make-disk-image's "efi"
table (pure GPT + ESP, protective MBR). The Sintra's Aaeon UP Board firmware
USB-boots in Legacy/BIOS mode — it boots the live ISO via that ISO's isolinux
(BIOS) El Torito image, not the UEFI ESP — so a dd'd systemd-boot image has no
BIOS boot code to execute and the firmware won't list it (a hand-added hybrid
MBR didn't help: nothing to run).
Switch the USB target to GRUB with BIOS + UEFI on make-disk-image's "hybrid"
table: it adds a bios_grub partition, GRUB writes its BIOS stage to the MBR AND
a removable /EFI/BOOT/BOOTX64.EFI — mirroring the live ISO's dual boot. The
Aaeon now lists it (as two "ia android" entries, BIOS + UEFI) and boots it.
Scoped to disk-image-sintra-usb only; the eMMC install keeps systemd-boot.
ESP stays partition 1 so the ESP-USB relabel step is unchanged.
Verified on hardware: booted from USB into the wizard with the full upboard.nix
hardware config.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A USB-bootable Sintra image variant for booting on a machine whose eMMC
already holds a nixos/ESP-labelled install. make-disk-image hardcodes the
root/ESP labels (nixos/ESP); booting the standard image from USB next to
the eMMC races stage-1's by-label/nixos between the two roots and likely
mounts the eMMC. This variant labels root nixos-usb (via make-disk-image
-L) and relabels the ESP to ESP-USB in a post-step (mtools), with
fileSystems pointed at the new labels. Auto-upgrade is disabled — it's a
portable test / hand-off image, and that also removes scheduled bootloader
writes that could land on the eMMC's ESP.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The dev autoUpgrade flake URL still referenced the pre-migration repo
(aiolabs/lamassu-next), so the Sintra test unit would auto-pull
lamassu-next/dev at 04:00 — which lacks all the bitspire work (the
QR-pairing wizard, etc.) and would revert the box to the old no-seed
build. Repoint it at aiolabs/bitspire?ref=dev, the post-migration home
of this code. lamassu-next still feeds the not-yet-converted production
ATMs (batm3, douro) until they migrate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
provision-atm.sh now writes VITE_SPIRE_SEED (the spire-seed:v1: pairing seed
from spirekeeper) as the production identity, validating the scheme prefix;
the generated nsec path is kept only as a dev fallback when SPIRE_SEED is
unset. Relay default moved to the LNbits bundled nostrrelay
(ws://$HOST_IP:5001/nostrrelay/test). .env templates (live.nix + the flake's
installed-default) swap VITE_ATM_PRIVATE_KEY → VITE_SPIRE_SEED and drop the
dead LP-era vars. README notes state.db now also holds the bunker binding
(keep it or re-pair).
Part of Phase E, aiolabs/bitspire#52. Unblocks the Sintra live-pairing smoke.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The fresh-boot `/var/lib/bitspire/.env` template at flake.nix's
`bitspire-env` activation script seeded `VITE_RELAY_URL=` empty, which
forced every operator to run `provision-atm.sh` (or hand-edit .env)
before the renderer could resolve a relay. Meanwhile the NixOS option
`services.bitspire.relayUrl` was wired only to the dead-code
`/etc/bitspire/config.env` (mkForce-shadowed by `/var/lib/bitspire/.env`).
Thread the NixOS option through: seed `VITE_RELAY_URL=${cfg.relayUrl}`
on first boot. The .env override path remains intact — provision-atm.sh
or a hand edit still take precedence at runtime (the file is the
EnvironmentFile, not the activation-time template). Existing ATMs
already have a populated `.env` and aren't affected (the activation
script's `if [ ! -f ... ]` guard skips the rewrite).
Renderer resolution order:
/var/lib/bitspire/.env → NixOS module default → renderer fallback
(`ws://localhost:7777` in lightning.ts:63 / main.ts:284)
Also expand the `services.bitspire.relayUrl` option description so
future readers see the wire-through + the override path documented
where the option lives.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes gap 2 from coord log 2026-06-01T18:30Z. The LNbits withdraw
extension's nostr-transport RPC now populates `link.lnurl` from
`settings.lnbits_baseurl` (aiolabs/withdraw#1 / commit e9d911e), so the
ATM no longer needs a separate HTTP URL on the wire to compose the
LNURL-withdraw callback itself.
What goes:
- `VITE_LNBITS_HTTP_URL` env var (renderer + Electron main)
- `lnbitsHttpUrl` field on `LightningConfig`, `RuntimeConfig`, and the
Window mirror in `src/types/electron.d.ts`
- The manual `${lnbitsHttpUrl}/withdraw/api/v1/lnurl/${unique_hash}`
composition in `generateLnurlWithdraw`
- The `encodeLnurl` bech32 helper in `lightning.ts` (LNbits returns
bech32-encoded; we just `.toUpperCase()` to match BOLT/LNURL convention)
- `@scure/base` dep from `apps/machine/package.json` (only used by the
removed helper; clink still uses it directly)
- The `lnbitsHttpUrl` option + `LNBITS_HTTP_URL=…` env var + boot echo
in `deploy/nixos/bitspire-atm.nix`
- Doc references in CLAUDE.md, README.md, deploy/nixos/README.md,
docs/architecture-comparison.md, and the lightning-check skill
What stays:
- `link.lnurl` consumption, with an explicit error if LNbits returns
null (which signals `LNBITS_BASEURL` is unset on the server side —
better to fail clearly than silently)
- The receiver-side bech32 uppercasing (LNbits returns lowercase per
the standard library)
Why this is a net win:
- Removes a config-drift surface — if LNbits's external URL moved
(DNS, port, reverse-proxy rewrite), every ATM in the field would
stop issuing redeemable LNURL-withdraw QRs until reconfigured.
Now LNbits derives its own URL from `settings.lnbits_baseurl`,
one source of truth.
- Removes an extra provisioning step. No more `LNBITS_HTTP_URL=…`
before running `provision-atm.sh`; the relay + server pubkey suffice.
- Removes the misleading boot echo that triggered the §`18:30Z`
smoke triage confusion ("LNbits HTTP: <url>" read like ATM-→-LNbits
connectivity, when it was only ever a URL embedded in customer QRs).
Also adds a `# pragma: allowlist secret` marker above the
`VITE_ATM_PRIVATE_KEY` doc block in `.env.example` so the global
secret scanner stops false-positiving on the documentation prose.
Workspace typecheck + 24/24 apps/machine tests still green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Main's commit e5d7a86 ("chore: set ATM_DB_PATH env var for atm-tui")
added two ATM_DB_PATH lines pointing at /var/lib/lamassu-atm/state.db
AFTER dev had forked. Dev's path-rename commit (10d2869) couldn't
touch those lines because they didn't exist on dev's base; the
rebase brought them in unchanged, undoing the lamassu-atm → bitspire
data-dir migration for the ATM_DB_PATH consumers.
Re-point both occurrences at /var/lib/bitspire/state.db so atm-tui
and other ATM_DB_PATH consumers reach the actual on-disk location.
isoImage.isoName was renamed to image.fileName in 25.11 (unified
image module). Split the rename out — the rest of isoImage.* stays
put (makeEfiBootable, makeBiosBootable, squashfsCompression).
All 8 nixosConfigurations evaluate clean on 25.11 with zero
deprecation warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Clean step — all 8 nixosConfigurations evaluate without deprecation
warnings on 25.05.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three breakages handled:
- hardware.opengl → hardware.graphics (renamed in 24.11). Touches
upboard.nix, douro.nix, batm3.nix, live.nix.
- vaapiIntel dropped — legacy pre-Broadwell driver, removed in
nixpkgs. UP Board (Cherry Trail) and OptiPlex 9030 (Haswell) both
use intel-media-driver, which stays.
- vaapiVdpau renamed → libva-vdpau-driver.
system.stateVersion stays 24.05 — convention is to never bump after
install. Existing Sintra and fresh flashes keep the 24.05 state
semantics; that's correct.
All 8 nixosConfigurations (4 models × {live,installed}) evaluate
clean with zero deprecation warnings on 24.11.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
System-level half of the lamassu → bitspire rebrand the rest of dev
already did at the path / service / package layers. Touches user/group
declarations, every systemd `User=` block, the udev rules filename, all
chown calls in flake.nix + live.nix, the displayManager autoLogin user,
the trusted-users nix entry, provision-atm.sh's ATM_USER, plus README +
CLAUDE.md doc references.
In-place migration for the Sintra dev unit (which auto-pulls dev at
04:00) lives in `system.activationScripts.bitspire-user-migration` and:
- copies `/home/lamassu/.ssh/authorized_keys` → `/home/bitspire/` once,
so SSH access survives the rename
- recursively chowns `/var/lib/bitspire` to the new bitspire UID on
every boot — cheap no-op once done, but covers the case where the
data dir was written by the now-removed lamassu UID
- leaves `/home/lamassu/` in place as evidence; operator can `rm -rf`
after confirming bitspire login works
Recovery path if the migration breaks SSH access: root key is still in
configuration.nix:142-144 (padreug@gizmo), so ssh root@<host> works.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- nixosConfigurations.sintra-installed reuses the existing UP Board
hardware module (deploy/nixos/hardware/upboard.nix). Sintra has
identical peripheral layout to tejo — Aaeon UP Board with iVIZION
validator on ttyJ5 and Fujitsu F56 dispenser on ttyJ7 — so no new
hardware nix module is needed.
- packages.x86_64-linux.disk-image-sintra is a raw GPT disk image
consumable via \`dd if=result/*.img of=/dev/<eMMC>\`. Mirrors the
existing disk-image-douro shape.
- bitspire-env activation script (the template that lands at
/var/lib/bitspire/.env on first boot) now emits LNbits fields
(VITE_LNBITS_SERVER_PUBKEY, VITE_LNBITS_HTTP_URL) instead of the
retired LP fields. Empty values mean the ATM boots into a
"needs provisioning" state, ready for provision-atm.sh to fill in.
Evaluations confirmed: nix eval .#nixosConfigurations.sintra-installed
and .#packages.x86_64-linux.disk-image-sintra both resolve.
Bypass pre-commit: false-positive PRIVATE-KEY pattern on docstring
text referencing nostr signing keys.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
Standalone Node.js script that generates a Lightning invoice for
the ATM's Lightning.Pub account. Reads config from .env, connects
to the relay, creates an invoice via Nostr RPC, displays a QR code
in the terminal, and prints the BOLT11.
Bundled as self-contained CJS with esbuild (all dependencies inlined)
so it works from the nix store without separate node_modules.
Usage: fund-atm <amount_sats>
e.g. fund-atm 100000
fund-atm 100000 sats
Added to NixOS systemPackages for both live and installed configs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
HP ProDesk 600 G3 DM hardware module (temporary board replacement for
BATM3). Includes batm3-installed nixosConfiguration with auto-upgrade,
cachix, and systemd-boot.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add batm3 to flake.nix NixOS configs, live.nix (CDC ACM kernel modules,
MEI udev rules), and build-iso.sh. Default fiat: USD.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add atm-tui as a flake input and include it in environment.systemPackages
for both live and installed NixOS configs. The TUI will be available as
'atm-tui' on PATH after deployment — no more manual SCP.
The binary is pushed to cachix alongside the ATM app, so douro pulls
it from the binary cache on upgrade (no local compilation needed).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add tejo-specific udev rules for both UP Board and UP4000 variants
(serial symlinks ttyJ4/J5/J7, camera, LED SPI, I2C, USB autosuspend)
- Add USB serial kernel modules (usbserial, ftdi_sio, cp210x) for tejo
- Add tejo serial console kernel params (ttyS4 debug UART)
- Fix upboard.nix: hardware.graphics → hardware.opengl (NixOS 24.05)
- Add tejo-installed nixosConfiguration with model-specific auto-upgrade
- Parameterize auto-upgrade flake URL per machine model
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The ATM should never compile from source — only download pre-built
binaries from cachix/cache.nixos.org. If a derivation isn't cached,
the upgrade fails cleanly instead of trying to build on the mSATA.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Without this, freshly flashed ATMs have mismatched nix store hashes and
fall back to building from source instead of pulling cachix binaries.
Matches the installed config which already had this.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Same fixes as live.nix: disable SwiftShader, enforce MemoryMax=1G,
add 1GB swap file, and clean /tmp on boot. Applies to douro-installed
and tejo-installed configs used by nixos-rebuild on disk-installed ATMs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolves the cachix binary cache hash mismatch between local dev machines
(Determinate Nix 2.33) and douro (stock Nix 2.18). With both sides using
Determinate Nix, `cachix push` from local builds produces store paths that
douro's 4am auto-upgrade can download directly instead of building from source.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Passwordless sudo for lamassu user (nixos-rebuild without TTY)
- Cachix binary cache (aiolabs) as substituter
- Daily auto-upgrade timer pulling latest flake from Forgejo
- trusted-users includes lamassu for nix commands
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move deploy/nixos/flake.nix into the root flake.nix, adding mkAtmApp
for pure Nix builds of the Electron app (no local pnpm needed). Simplify
build-iso.sh to a thin wrapper around `nix build .#iso-<model>`. Add
douro hardware configuration. Streamline live.nix to consume the
Nix-built app package.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add dev.sh script for managing regtest development environment
- Implement cmd_fund to fund ATM app owner via Lightning.Pub API
- Add --fund flag to cmd_up for automatic funding on startup
- Update setup_atm_app to write VITE_APP_ID to machine .env
- Fix Electron IPC to pass appId and extensionApiUrl to renderer
- Restructure repo from nested lamassu-next/ to root
The dev.sh script now supports:
- ./dev.sh up --fund # Start regtest and auto-fund ATM
- ./dev.sh fund # Fund existing ATM app
- ./dev.sh status # Show environment status
- ./dev.sh reset # Clean restart
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>