Commit graph

473 commits

Author SHA1 Message Date
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
Patrick Mulligan
7afcc6fc5d feat(deploy): add eGalax touchscreen support for Dell 9030 AIO
The Dell OptiPlex 9030 AIO's built-in eGalax touchscreen is
misidentified by libinput as a touchpad. Fix:

1. Unbind from usbhid at boot via udev, bind to usbtouchscreen
   kernel module which handles it as absolute input
2. Apply calibration matrix via systemd service after X11 starts
   (swap+invert axes, scale to active panel area 238-1853 x 197-1869)

Also updates hardware comments for Dell 9030 (was HP ProDesk).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 22:04:59 -04:00
Patrick Mulligan
d71815a15d feat(machine): add markdown-driven support pages
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>
2026-03-28 19:24:09 -04:00
Patrick Mulligan
ca6d8c4f68 feat: add VITE_MAINTENANCE_MODE env var for service screen
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>
2026-03-28 00:50:34 -04:00
Patrick Mulligan
d3befc11c1 feat: add fund-atm CLI for generating ATM funding invoices
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>
2026-03-28 00:45:45 -04:00
Patrick Mulligan
6cf372055f feat(deploy): add stable USB serial symlinks for BATM3
USB-serial adapters enumerate in unpredictable order on reboot,
causing ttyUSB0/1/2 to swap between the F56 dispenser, MEI
validator, and NFC module. Add udev rules that create stable
symlinks by adapter serial number:

- /dev/ttyF56 → Prolific DDDLb103Y23 (F56 dispenser)
- /dev/ttyMEI → FTDI A9YW78OC (MEI cash recycler)
- /dev/ttyNFC → FTDI A9ZF8ELY (NFC module)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 20:35:53 -04:00
Patrick Mulligan
7dfc5ffc98 chore: close stale issue — scripts already reorganized
The .mjs scripts were moved from packages/nostr-client/ root to
packages/nostr-client/dev/ in commit 5448a62.

Closes #13

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 10:25:11 -04:00
Patrick Mulligan
44f8f803ae feat(state-machine): auto-clear confirmAbandon after 60s
If a customer walks away from the abandon confirmation screen,
the machine now returns to idle after 60 seconds instead of
hanging indefinitely. Cash stays in the cashbox as operator funds.

Closes #31

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 10:20:58 -04:00
Patrick Mulligan
bebe987b60 fix(state-store): handle FK constraints in v4→v5 migration
The table recreation migration failed on machines with existing
transaction_bills/cassette_bills rows due to FK constraints on
transactions(txid). Also clean up leftover transactions_new table
from any previous failed migration attempt.

Closes #38

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 15:05:03 -04:00
Patrick Mulligan
44c27ed0c6 fix(ui): force cursor visible on all elements in kiosk mode
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>
2026-03-26 14:24:16 -04:00
Patrick Mulligan
10fab86371 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 03:10:28 -04:00
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
0833e7029a chore(deploy): add batm3-installed to push-cache all target
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 00:53:23 -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
88a52cf769 feat(deploy): add batm3-installed NixOS config
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>
2026-03-25 17:13:02 -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
98b17073d8 Merge branch 'feat/batm3-support' 2026-03-23 20:41:57 -04:00
Patrick Mulligan
cc7eefbf31 chore: update atm-tui flake input (remediate UX fix)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 03:56:34 -04:00
Patrick Mulligan
179e9a835a chore(deploy): add batm3 target to push-cache.sh
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 03:20:46 -04:00
Patrick Mulligan
dc5f4f8aa6 feat(deploy): add batm3 ISO build target
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>
2026-03-23 03:20:46 -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
2cc0acfdef feat: include atm-tui in NixOS ATM deployments
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>
2026-03-23 03:15:54 -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
0395aab3d1 feat(docker): streamline regtest dev environment
- Fix EXTENSION_SERVICE_URL default to localhost (was hardcoded LAN IP)
- Fix strfry healthcheck (BusyBox nc lacks -z, use /proc/net/tcp)
- Add LP healthcheck + depends_on strfry
- Add regtest-bootstrap.sh: funds LND, opens channels, creates LP app
- Add regtest.sh: single management script (up/down/reset/status/logs/lncli/bitcoin)
- Minimal start uses only 3 containers (bitcoind, lnd-1, lnd-4) instead of 17

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 01:31:51 -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
9e6ee4813c test: add LNURL-withdraw Nostr RPC test scripts
- test-withdraw-rpc.mjs: basic withdraw.createLink via kind 21000
- test-full-withdraw.mjs: end-to-end create + LNURL redeem
- test-update-delete.mjs: update/delete lifecycle tests (TEST 1 + TEST 2)

TEST 1 payment blocked by app balance (see script header comment).
TEST 2 (delete + reject) fully passes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 13:02:48 -05:00
Patrick Mulligan
25e5dfe46c fix(docker): update regtest network/volume names
Regtest stack renamed from lnbits_ prefix to regtest_ prefix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 13:02:22 -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
e48073397f fix(state-machine): add 2-minute safety timeout to dispensingCash state
If the dispenseCash promise hangs (hardware jam, serial port freeze,
waitForBillsRemoved stuck), the machine was trapped in dispensingCash
forever with no way to recover. Now it transitions to dispenseError
after 2 minutes, which then auto-returns to idle after 30 seconds.

This ensures the ATM always recovers to a usable state, even when
hardware fails mid-dispense.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 10:25:51 -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
143ae9ff0b feat(nix): add tejo hardware support and per-model auto-upgrade
- 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>
2026-03-06 08:54:50 -05:00