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:
parent
f6a1c2f9f6
commit
e63f8ab9f6
3 changed files with 189 additions and 0 deletions
|
|
@ -230,6 +230,50 @@ ipcMain.handle('hal:cleanup', async () => {
|
||||||
// App lifecycle
|
// App lifecycle
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
initDatabase()
|
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<string, { denomination: number; count: number }[]> = {
|
||||||
|
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()
|
createWindow()
|
||||||
|
|
||||||
app.on('activate', () => {
|
app.on('activate', () => {
|
||||||
|
|
|
||||||
141
deploy/nixos/atm-transactions.sh
Normal file
141
deploy/nixos/atm-transactions.sh
Normal 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;"
|
||||||
|
|
@ -109,6 +109,10 @@
|
||||||
# Camera support
|
# Camera support
|
||||||
v4l-utils
|
v4l-utils
|
||||||
fswebcam
|
fswebcam
|
||||||
|
|
||||||
|
# ATM operations
|
||||||
|
sqlite
|
||||||
|
(writeShellScriptBin "atm-transactions" (builtins.readFile ./atm-transactions.sh))
|
||||||
];
|
];
|
||||||
|
|
||||||
# Enable SSH for remote administration
|
# Enable SSH for remote administration
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue