From e63f8ab9f69f25c0060280b37ed02c6336c4dd0a Mon Sep 17 00:00:00 2001 From: Patrick Mulligan Date: Sat, 28 Feb 2026 11:55:00 -0500 Subject: [PATCH] fix(machine): seed cassettes on first boot + add atm-transactions script Cassette inventory was never initialized in SQLite, so recordTransaction's UPDATE decrements were no-ops against an empty table. Now the Electron main process seeds cassettes from VITE_LAMASSU_CASSETTES or the model preset on first boot. Also adds an atm-transactions CLI script (with --summary, --inventory, --type, --today, --last, --since filters) and sqlite to the NixOS system packages. Co-Authored-By: Claude Opus 4.6 --- apps/machine/electron/main.ts | 44 ++++++++++ deploy/nixos/atm-transactions.sh | 141 +++++++++++++++++++++++++++++++ deploy/nixos/configuration.nix | 4 + 3 files changed, 189 insertions(+) create mode 100644 deploy/nixos/atm-transactions.sh diff --git a/apps/machine/electron/main.ts b/apps/machine/electron/main.ts index 03d7abd..6f3970f 100644 --- a/apps/machine/electron/main.ts +++ b/apps/machine/electron/main.ts @@ -230,6 +230,50 @@ ipcMain.handle('hal:cleanup', async () => { // App lifecycle app.whenReady().then(() => { initDatabase() + + // Seed cassettes from env/preset if DB table is empty. + // This ensures recordTransaction() can decrement cassette counts + // even before the operator explicitly sets them via the UI. + const existing = loadCassettes() + if (existing.length === 0) { + let seedCassettes: { denomination: number; count: number }[] = [] + + // Priority 1: explicit VITE_LAMASSU_CASSETTES env var + const cassettesJson = process.env.VITE_LAMASSU_CASSETTES + if (cassettesJson) { + try { + seedCassettes = JSON.parse(cassettesJson) + } catch (e) { + console.warn('[Electron] Failed to parse VITE_LAMASSU_CASSETTES:', e) + } + } + + // Priority 2: default presets per model + if (seedCassettes.length === 0) { + const model = process.env.VITE_LAMASSU_MACHINE_MODEL || 'sintra' + const presets: Record = { + douro: [ + { denomination: 100, count: 50 }, + { denomination: 200, count: 50 }, + ], + sintra: [{ denomination: 20, count: 50 }], + tejo: [ + { denomination: 5, count: 500 }, + { denomination: 20, count: 500 }, + { denomination: 50, count: 500 }, + { denomination: 100, count: 500 }, + ], + gaia: [{ denomination: 20, count: 50 }], + } + seedCassettes = presets[model] || [] + } + + if (seedCassettes.length > 0) { + setCassettes(seedCassettes) + console.log('[Electron] Seeded cassettes from config:', seedCassettes) + } + } + createWindow() app.on('activate', () => { diff --git a/deploy/nixos/atm-transactions.sh b/deploy/nixos/atm-transactions.sh new file mode 100644 index 0000000..a81c651 --- /dev/null +++ b/deploy/nixos/atm-transactions.sh @@ -0,0 +1,141 @@ +#!/usr/bin/env bash +# atm-transactions — Query ATM transaction history from SQLite +# +# Usage: +# atm-transactions # Show all transactions +# atm-transactions --today # Today's transactions +# atm-transactions --type cash_in # Only cash-in +# atm-transactions --type cash_out # Only cash-out +# atm-transactions --last 5 # Last 5 transactions +# atm-transactions --since 2026-02-28 # Since a date +# atm-transactions --summary # Summary totals only +# atm-transactions --inventory # Current cassette & cashbox state + +set -euo pipefail + +DB="/var/lib/lamassu-atm/state.db" + +if [ ! -f "$DB" ]; then + echo "ERROR: Database not found at $DB" >&2 + exit 1 +fi + +sql() { + sqlite3 -column -header "$DB" "$1" +} + +# Parse arguments +TYPE="" +LIMIT="" +SINCE="" +SUMMARY=false +INVENTORY=false +TODAY=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --type) + TYPE="$2"; shift 2 ;; + --last) + LIMIT="$2"; shift 2 ;; + --since) + SINCE="$2"; shift 2 ;; + --today) + TODAY=true; shift ;; + --summary) + SUMMARY=true; shift ;; + --inventory) + INVENTORY=true; shift ;; + -h|--help) + echo "Usage: atm-transactions [OPTIONS]" + echo "" + echo "Options:" + echo " --type cash_in|cash_out Filter by transaction type" + echo " --last N Show last N transactions" + echo " --since YYYY-MM-DD Show transactions since date" + echo " --today Show today's transactions" + echo " --summary Show summary totals" + echo " --inventory Show cassette & cashbox state" + echo " -h, --help Show this help" + exit 0 ;; + *) + echo "Unknown option: $1" >&2; exit 1 ;; + esac +done + +# Inventory mode +if $INVENTORY; then + echo "=== Cassettes ===" + sql "SELECT denomination AS 'Denom', count AS 'Bills', denomination * count AS 'Value' FROM cassettes ORDER BY denomination;" + echo "" + echo "=== Cashbox ===" + sql "SELECT total_bills AS 'Bills', total_fiat_cents / 100 AS 'Total', CASE WHEN last_emptied_at IS NOT NULL THEN datetime(last_emptied_at / 1000, 'unixepoch') ELSE 'never' END AS 'Last Emptied' FROM cashbox;" + exit 0 +fi + +# Summary mode +if $SUMMARY; then + echo "=== Transaction Summary ===" + sql "SELECT + type AS 'Type', + COUNT(*) AS 'Count', + SUM(fiat_cents) / 100 AS 'Total Fiat', + SUM(sats) AS 'Total Sats', + MIN(fiat_cents) / 100 AS 'Min Fiat', + MAX(fiat_cents) / 100 AS 'Max Fiat' + FROM transactions + GROUP BY type;" + echo "" + echo "=== Daily Breakdown ===" + sql "SELECT + date(created_at / 1000, 'unixepoch') AS 'Date', + type AS 'Type', + COUNT(*) AS 'Txns', + SUM(fiat_cents) / 100 AS 'Fiat', + SUM(sats) AS 'Sats' + FROM transactions + GROUP BY date(created_at / 1000, 'unixepoch'), type + ORDER BY 1 DESC, 2;" + exit 0 +fi + +# Build WHERE clause +WHERE="" +CONDITIONS=() + +if [ -n "$TYPE" ]; then + CONDITIONS+=("type = '$TYPE'") +fi + +if $TODAY; then + CONDITIONS+=("date(created_at / 1000, 'unixepoch') = date('now')") +fi + +if [ -n "$SINCE" ]; then + CONDITIONS+=("date(created_at / 1000, 'unixepoch') >= '$SINCE'") +fi + +if [ ${#CONDITIONS[@]} -gt 0 ]; then + WHERE="WHERE $(IFS=' AND '; echo "${CONDITIONS[*]}")" +fi + +# Build LIMIT clause +LIMIT_SQL="" +if [ -n "$LIMIT" ]; then + LIMIT_SQL="LIMIT $LIMIT" +fi + +# Main query +sql "SELECT + t.txid AS 'TX ID', + t.type AS 'Type', + t.fiat_cents / 100 AS 'Fiat', + t.sats AS 'Sats', + datetime(t.created_at / 1000, 'unixepoch') AS 'Time (UTC)', + group_concat('Q' || tb.denomination || 'x' || tb.count) AS 'Bills' +FROM transactions t +LEFT JOIN transaction_bills tb ON t.txid = tb.txid +$WHERE +GROUP BY t.txid +ORDER BY t.created_at DESC +$LIMIT_SQL;" diff --git a/deploy/nixos/configuration.nix b/deploy/nixos/configuration.nix index 2d61501..4f207b3 100644 --- a/deploy/nixos/configuration.nix +++ b/deploy/nixos/configuration.nix @@ -109,6 +109,10 @@ # Camera support v4l-utils fswebcam + + # ATM operations + sqlite + (writeShellScriptBin "atm-transactions" (builtins.readFile ./atm-transactions.sh)) ]; # Enable SSH for remote administration