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 <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-02-28 11:55:00 -05:00
commit e63f8ab9f6
3 changed files with 189 additions and 0 deletions

View file

@ -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;"