From 4ac640a49919a7ff55e61225123d017ef4cf3efa Mon Sep 17 00:00:00 2001 From: Padreug Date: Sat, 13 Jun 2026 22:31:21 +0200 Subject: [PATCH] docs: update functional identifier refs to spirekeeper Runbook SQL (spirekeeper.dca_*), ext URL paths, code-location paths, and the DB-schema name in docs/CLAUDE/README move to the new identity. Rewrites the placeholder description.md with a real one. Historical aiolabs/satmachineadmin#N issue/repo links stay pointing at the original repo. Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 4 +- README.md | 2 +- description.md | 13 ++- docs/security-pathway-v1.md | 22 ++--- misc-docs/EMERGENCY_PROTOCOLS.md | 122 ++++++++++++------------- misc-docs/EMERGENCY_PROTOCOLS_PRINT.md | 122 ++++++++++++------------- 6 files changed, 142 insertions(+), 143 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index acf23cc..35a690e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -213,7 +213,7 @@ commission_amount = 266800 - 258835 = 7,965 sats (to commission wallet) ### Security Considerations - **Superuser Authentication**: Admin extension requires LNBits superuser login - **Wallet Admin Keys**: Client extension uses wallet admin keys for user operations -- **Database Access**: Only superusers can write to satoshimachine database +- **Database Access**: Only superusers can write to spirekeeper database - SSH tunnel encryption for database connectivity - Read-only database permissions for Lamassu access - Input sanitization and type validation @@ -230,7 +230,7 @@ on existing installs: ```sql SELECT a.id, a.username, a.pubkey, m.id, m.machine_npub FROM accounts a -JOIN ext_satoshimachine.dca_machines m +JOIN ext_spirekeeper.dca_machines m ON LOWER(a.pubkey) = LOWER(m.machine_npub); ``` diff --git a/README.md b/README.md index 725355a..5ac8906 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,7 @@ The extension creates several tables: ├── config.json # Extension configuration ├── manifest.json # Extension manifest ├── templates/ -│ └── satmachineadmin/ +│ └── spirekeeper/ │ └── index.html # Main UI template └── static/ └── js/ diff --git a/description.md b/description.md index a748691..2a93f78 100644 --- a/description.md +++ b/description.md @@ -1,10 +1,9 @@ -SatMachineAdmin can be used as a template for building new extensions, it includes a bunch of functions that can be edited/deleted as you need them. +spirekeeper is the operator-side administration extension for bitSpire — it manages one or more bitSpire ATMs and runs the Dollar Cost Averaging (DCA) distribution that pays registered clients out of their confirmed deposit balances. -This is a longform description that will be used in the advanced description when users click on the "more" button on the extension cards. +It listens for settlement events published by each ATM over Nostr (kind-21000, NIP-44 v2 encrypted), pays the resulting invoices through LNbits, and splits the proceeds across the principal, operator-fee, and commission legs according to the per-machine and super-config fee settings. -Adding some bullets is nice covering: +- **ATM fleet management** — register and configure each bitSpire by its machine npub; publish fee config and cassette state over Nostr. +- **DCA distribution** — proportional ("flow") allocation across clients based on their remaining deposit balances, with a full per-leg audit trail. +- **Operator administration** — deposit confirmation workflow, client balance tracking, settlement history with drill-down, and CSV export for accounting. -- Functionality -- Use cases - -...and some other text about just how great this etension is. +Requires LNbits superuser access for administration. The Lightning ATM side is served by the bitSpire device; this extension is the back office. diff --git a/docs/security-pathway-v1.md b/docs/security-pathway-v1.md index 2b7cc8b..5d57414 100644 --- a/docs/security-pathway-v1.md +++ b/docs/security-pathway-v1.md @@ -7,7 +7,7 @@ ## 0 · Why this document exists -Today the satoshi‑machine code lives at `~/dev/shared/extensions/satmachineadmin` on branch `v2-bitspire`. v2 swapped the legacy Lamassu SSH/PostgreSQL polling model for a Nostr‑native one: bitSpire publishes invoices over kind‑21000 NIP‑44 v2 events, LNbits pays them, and our extension hooks the resulting `Payment` object. +Today the satoshi‑machine code lives at `~/dev/shared/extensions/spirekeeper` on branch `v2-bitspire`. v2 swapped the legacy Lamassu SSH/PostgreSQL polling model for a Nostr‑native one: bitSpire publishes invoices over kind‑21000 NIP‑44 v2 events, LNbits pays them, and our extension hooks the resulting `Payment` object. The hard truth: the *settlement* itself uses Lightning (so it can't be forged once a preimage lands), but everything *around* the settlement — who the ATM is, what operator it belongs to, what the principal/commission split was, and what fiat was dispensed — currently rides on **mutable, unauthenticated metadata** (`Payment.extra`) plus a **stopgap that has the ATM hold the operator's own Nostr private key**. The latter means physical possession of the ATM = total compromise of the operator's LNbits account. @@ -66,7 +66,7 @@ Lamassu's old answer here was TLS cert pinning. We have a richer toolbox — Nos │ ▼ register_invoice_listener fires - satmachineadmin/tasks.py:_handle_payment + spirekeeper/tasks.py:_handle_payment │ ┌─────────────────────────────────┴────────────────────────────┐ ▼ ▼ @@ -144,7 +144,7 @@ T3, T5, T6 are the ones that keep the hardware honest. T3 + T6 are *the* reason ## 4 · Audit findings — current state inventory -Pulled from the two recent code‑level audits of `~/dev/shared/extensions/satmachineadmin` (operator‑scoping inventory) and `~/dev/lnbits/nostr-transport` (transport primitives). +Pulled from the two recent code‑level audits of `~/dev/shared/extensions/spirekeeper` (operator‑scoping inventory) and `~/dev/lnbits/nostr-transport` (transport primitives). ### 4.1 What's already strong @@ -307,7 +307,7 @@ None of those need to change. The new layers slot in *above* them. | **S4 — NIP‑78 per‑machine config + fleet roster** | Operator publishes `kind:30078` config + `kind:30000` fleet list. Handler cross‑checks ATM npub ∈ fleet; reads max‑withdraw/fee policy from config. | G1, G9 | 1 week | Define config schema; backwards‑compat path for pre‑NIP‑78 machines. | | **S5 — `sender_pubkey` persistence + signed metadata in Payment.extra** | When the dispatcher writes a Payment row, it stamps `Payment.extra.sender_pubkey`, `delegation_root`, and an HMAC over the key fields keyed by the LNbits server's own secret. Mutation post‑write breaks the HMAC. | G2 (DB‑side), G5, G6 | 3–5 days | LNbits PR — fairly localised. | | **S6 — Rate limiting + roster‑gated auto‑account** | Auto‑account‑from‑npub only fires if the npub appears in some operator's NIP‑78 fleet OR if an explicit "open enrollment" flag is set. Relay/handler‑level rate limit per pubkey. | G8, G9 | 1 week | LNbits PR. | -| **S7 — NIP‑46 bunker option** | Operator can pair satmachineadmin with a Bunker (Amber, Nunchuk Custody, etc.). Operator's nsec leaves LNbits' DB; LNbits stores only the bunker connection. | G6, partial G5 | 4–6 weeks | Largest. Defer until S0–S5 land. | +| **S7 — NIP‑46 bunker option** | Operator can pair spirekeeper with a Bunker (Amber, Nunchuk Custody, etc.). Operator's nsec leaves LNbits' DB; LNbits stores only the bunker connection. | G6, partial G5 | 4–6 weeks | Largest. Defer until S0–S5 land. | | **S8 — Cash‑in path** | Wire `is_out=True` cash‑in handling: LNURL‑withdraw with expiration matching the kind‑21000 invoice TTL, attestation receipt on settle, refund queue for stale links. | G10 | 2 weeks | Out of scope for this security doc but tracked here for completeness. | Recommended sequencing for the *next sprint*: **S0 + S1 + S5**. They give us the biggest security delta with no upstream LNbits dependency for S0/S1 and a small, well‑scoped LNbits patch for S5. S2/S3/S4 are the proper Nostr‑native layer and should land in the sprint after. @@ -359,12 +359,12 @@ For an auditor or new contributor doing a walk‑through: | File | Role | Note | |---|---|---| -| `~/dev/shared/extensions/satmachineadmin/tasks.py` | LNbits invoice listener. Entry point for all settlements today. | `_handle_payment:56-95` — load‑bearing routing. | -| `~/dev/shared/extensions/satmachineadmin/bitspire.py` | Parses Payment.extra. The trust boundary. | `parse_settlement:68-92` — happy vs fallback path. | -| `~/dev/shared/extensions/satmachineadmin/distribution.py` | Three‑leg distribution chain. | `process_settlement` — uses claim pattern. | -| `~/dev/shared/extensions/satmachineadmin/crud.py` | Operator‑scoped DB layer. | `claim_settlement_for_processing`, `_machine_owned_by`. | -| `~/dev/shared/extensions/satmachineadmin/views_api.py` | 33 routes, all `check_user_exists` except super‑config PUT. | `_assert_wallet_owned_by` is the wallet‑IDOR fix. | -| `~/dev/shared/extensions/satmachineadmin/migrations.py` | Schema. | `dca_settlements` is the audit row; `dca_payments` is the leg row. | +| `~/dev/shared/extensions/spirekeeper/tasks.py` | LNbits invoice listener. Entry point for all settlements today. | `_handle_payment:56-95` — load‑bearing routing. | +| `~/dev/shared/extensions/spirekeeper/bitspire.py` | Parses Payment.extra. The trust boundary. | `parse_settlement:68-92` — happy vs fallback path. | +| `~/dev/shared/extensions/spirekeeper/distribution.py` | Three‑leg distribution chain. | `process_settlement` — uses claim pattern. | +| `~/dev/shared/extensions/spirekeeper/crud.py` | Operator‑scoped DB layer. | `claim_settlement_for_processing`, `_machine_owned_by`. | +| `~/dev/shared/extensions/spirekeeper/views_api.py` | 33 routes, all `check_user_exists` except super‑config PUT. | `_assert_wallet_owned_by` is the wallet‑IDOR fix. | +| `~/dev/shared/extensions/spirekeeper/migrations.py` | Schema. | `dca_settlements` is the audit row; `dca_payments` is the leg row. | | `~/dev/shocknet/lamassu-next/deploy/nixos/provision-atm.sh` | Where keys land on the ATM today. | `:81-99` — `VITE_ATM_PRIVATE_KEY` and the Option‑1 stopgap. | | `~/dev/lnbits/nostr-transport/lnbits/core/services/nostr_transport/` | LNbits transport handler (upstream we depend on). | NIP‑44 v2 crypto here; G5/G6/G7 fixes will live here. | | `~/dev/nostr-protocol/nips/26.md` | Delegation. | Source for S2. | @@ -397,7 +397,7 @@ How we'd test the proposed design end‑to‑end, once S0–S5 land: Once approved: -1. The PDF for printing will be generated post‑plan‑mode (requires shell exec). Recommended path: render the markdown via `pandoc` to `~/dev/shared/extensions/satmachineadmin/docs/security-pathway-v1.pdf`; the markdown source will live at `~/dev/shared/extensions/satmachineadmin/docs/security-pathway-v1.md` so future contributors edit it in‑repo. +1. The PDF for printing will be generated post‑plan‑mode (requires shell exec). Recommended path: render the markdown via `pandoc` to `~/dev/shared/extensions/spirekeeper/docs/security-pathway-v1.pdf`; the markdown source will live at `~/dev/shared/extensions/spirekeeper/docs/security-pathway-v1.md` so future contributors edit it in‑repo. 2. Open Forgejo epics on `aiolabs/satmachineadmin` linking back to existing `#9/#11/#12` and adding a new one for "Security pathway hardening (S0–S7)." 3. Open a tracking issue on `aiolabs/lnbits` against the `nostr-transport` branch for the LNbits‑side primitives (S2, S5, S6). 4. Sequence sprint: **S0 + S1 + S5 first** (highest ratio of security delta to upstream coupling). S2/S3/S4 in the following sprint. diff --git a/misc-docs/EMERGENCY_PROTOCOLS.md b/misc-docs/EMERGENCY_PROTOCOLS.md index e408774..1b94049 100644 --- a/misc-docs/EMERGENCY_PROTOCOLS.md +++ b/misc-docs/EMERGENCY_PROTOCOLS.md @@ -38,7 +38,7 @@ -- Find duplicate transactions SELECT transaction_id, COUNT(*) as count, STRING_AGG(id::text, ', ') as record_ids -FROM satoshimachine.lamassu_transactions +FROM spirekeeper.lamassu_transactions GROUP BY transaction_id HAVING COUNT(*) > 1; ``` @@ -60,16 +60,16 @@ HAVING COUNT(*) > 1; -- Step 1: Identify duplicate distributions SELECT lt.transaction_id, lt.id, lt.created_at, lt.base_amount, COUNT(dp.id) as distribution_count -FROM satoshimachine.lamassu_transactions lt -LEFT JOIN satoshimachine.dca_payments dp ON dp.lamassu_transaction_id = lt.id +FROM spirekeeper.lamassu_transactions lt +LEFT JOIN spirekeeper.dca_payments dp ON dp.lamassu_transaction_id = lt.id GROUP BY lt.id -HAVING COUNT(dp.id) > (SELECT COUNT(*) FROM satoshimachine.dca_clients WHERE remaining_balance > 0); +HAVING COUNT(dp.id) > (SELECT COUNT(*) FROM spirekeeper.dca_clients WHERE remaining_balance > 0); -- Step 2: Calculate over-distributed amounts per client SELECT client_id, SUM(amount_sats) as total_received, -- Manual calculation of expected amount needed here -FROM satoshimachine.dca_payments +FROM spirekeeper.dca_payments WHERE lamassu_transaction_id IN (SELECT id FROM duplicates_table) GROUP BY client_id; ``` @@ -94,7 +94,7 @@ if existing: **Required Database Change**: ```sql -ALTER TABLE satoshimachine.lamassu_transactions +ALTER TABLE spirekeeper.lamassu_transactions ADD CONSTRAINT unique_transaction_id UNIQUE (transaction_id); ``` @@ -117,7 +117,7 @@ ADD CONSTRAINT unique_transaction_id UNIQUE (transaction_id); -- Check last successful poll SELECT MAX(created_at) as last_transaction, EXTRACT(EPOCH FROM (NOW() - MAX(created_at)))/3600 as hours_since_last -FROM satoshimachine.lamassu_transactions; +FROM spirekeeper.lamassu_transactions; -- If hours_since_last > 24, investigate immediately ``` @@ -194,8 +194,8 @@ ssh-keygen -t ed25519 -f ~/.ssh/satmachine_lamassu -C "satmachine-polling" -- Find stuck/failed payments (older than 1 hour, not completed) SELECT dp.id, dp.client_id, dp.amount_sats, dp.status, dp.created_at, c.username, dp.payment_hash -FROM satoshimachine.dca_payments dp -JOIN satoshimachine.dca_clients c ON dp.client_id = c.id +FROM spirekeeper.dca_payments dp +JOIN spirekeeper.dca_clients c ON dp.client_id = c.id WHERE dp.status != 'completed' AND dp.created_at < NOW() - INTERVAL '1 hour' ORDER BY dp.created_at DESC; @@ -207,7 +207,7 @@ ORDER BY dp.created_at DESC; SELECT SUM(commission_amount) as total_commission_expected, -- Manually check actual wallet balance in LNBits -FROM satoshimachine.lamassu_transactions; +FROM spirekeeper.lamassu_transactions; ``` #### Immediate Response @@ -258,9 +258,9 @@ SELECT c.remaining_balance, COALESCE(SUM(d.amount), 0) as total_deposits, COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0) as total_payments -FROM satoshimachine.dca_clients c -LEFT JOIN satoshimachine.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed' -LEFT JOIN satoshimachine.dca_payments p ON c.id = p.client_id +FROM spirekeeper.dca_clients c +LEFT JOIN spirekeeper.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed' +LEFT JOIN spirekeeper.dca_payments p ON c.id = p.client_id GROUP BY c.id; ``` @@ -319,9 +319,9 @@ SELECT COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0) as total_distributed, COALESCE(SUM(d.amount), 0) - COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0) as calculated_balance, c.remaining_balance - (COALESCE(SUM(d.amount), 0) - COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0)) as discrepancy -FROM satoshimachine.dca_clients c -LEFT JOIN satoshimachine.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed' -LEFT JOIN satoshimachine.dca_payments p ON c.id = p.client_id +FROM spirekeeper.dca_clients c +LEFT JOIN spirekeeper.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed' +LEFT JOIN spirekeeper.dca_payments p ON c.id = p.client_id GROUP BY c.id, c.username, c.remaining_balance HAVING ABS(c.remaining_balance - (COALESCE(SUM(d.amount), 0) - COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0))) > 1; ``` @@ -341,11 +341,11 @@ HAVING ABS(c.remaining_balance - (COALESCE(SUM(d.amount), 0) - COALESCE(SUM(CASE ```sql -- Get complete transaction history for client SELECT 'DEPOSIT' as type, id, amount, status, created_at, confirmed_at -FROM satoshimachine.dca_deposits +FROM spirekeeper.dca_deposits WHERE client_id = UNION ALL SELECT 'PAYMENT' as type, id, amount_sats, status, created_at, NULL -FROM satoshimachine.dca_payments +FROM spirekeeper.dca_payments WHERE client_id = ORDER BY created_at; ``` @@ -361,18 +361,18 @@ ORDER BY created_at; **Option A: Adjustment Entry** (Recommended) ```sql -- Create compensating deposit for positive discrepancy -INSERT INTO satoshimachine.dca_deposits (client_id, amount, status, note) +INSERT INTO spirekeeper.dca_deposits (client_id, amount, status, note) VALUES (, , 'confirmed', 'Balance correction - reconciliation 2025-10-19'); -- OR Create compensating payment for negative discrepancy -INSERT INTO satoshimachine.dca_payments (client_id, amount_sats, status, note) +INSERT INTO spirekeeper.dca_payments (client_id, amount_sats, status, note) VALUES (, , 'completed', 'Balance correction - reconciliation 2025-10-19'); ``` **Option B: Direct Balance Update** (Use with extreme caution) ```sql -- ONLY if audit trail is complete and discrepancy is unexplained -UPDATE satoshimachine.dca_clients +UPDATE spirekeeper.dca_clients SET remaining_balance = , updated_at = NOW() WHERE id = ; @@ -396,11 +396,11 @@ async def daily_reconciliation_check(): **Database Constraints**: ```sql -- Prevent negative balances -ALTER TABLE satoshimachine.dca_clients +ALTER TABLE spirekeeper.dca_clients ADD CONSTRAINT positive_balance CHECK (remaining_balance >= 0); -- Prevent confirmed deposits with zero amount -ALTER TABLE satoshimachine.dca_deposits +ALTER TABLE spirekeeper.dca_deposits ADD CONSTRAINT positive_deposit CHECK (amount > 0); ``` @@ -435,7 +435,7 @@ SELECT -- Calculate differences base_amount - ROUND(crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100))) as base_difference, commission_amount - ROUND(crypto_atoms - (crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100)))) as commission_difference -FROM satoshimachine.lamassu_transactions +FROM spirekeeper.lamassu_transactions WHERE ABS(base_amount - ROUND(crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100)))) > 1 OR ABS(commission_amount - ROUND(crypto_atoms - (crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100))))) > 1; ``` @@ -485,9 +485,9 @@ SELECT SUM(lt.base_amount) as total_distributed, SUM(expected_base) as should_have_distributed, SUM(expected_base - lt.base_amount) as client_impact -FROM satoshimachine.lamassu_transactions lt -JOIN satoshimachine.dca_payments dp ON dp.lamassu_transaction_id = lt.id -JOIN satoshimachine.dca_clients c ON dp.client_id = c.id +FROM spirekeeper.lamassu_transactions lt +JOIN spirekeeper.dca_payments dp ON dp.lamassu_transaction_id = lt.id +JOIN spirekeeper.dca_clients c ON dp.client_id = c.id WHERE -- filter for affected transactions GROUP BY c.id; ``` @@ -505,7 +505,7 @@ GROUP BY c.id; - Create compensating payments to affected clients: ```sql -- Add to client balances -UPDATE satoshimachine.dca_clients c +UPDATE spirekeeper.dca_clients c SET remaining_balance = remaining_balance + adjustment.amount FROM ( -- Calculate adjustment per client @@ -572,7 +572,7 @@ assert abs(calculated_total - crypto_atoms) <= 1, "Commission calculation error SELECT SUM(commission_amount) as expected_total_commission, -- Compare to actual wallet balance in LNBits dashboard -FROM satoshimachine.lamassu_transactions +FROM spirekeeper.lamassu_transactions WHERE created_at > ''; ``` @@ -607,7 +607,7 @@ curl -X GET https:///api/v1/wallet \ SELECT commission_wallet_id, LEFT(commission_wallet_adminkey, 10) || '...' as key_preview, updated_at -FROM satoshimachine.lamassu_config +FROM spirekeeper.lamassu_config ORDER BY updated_at DESC LIMIT 1; ``` @@ -783,9 +783,9 @@ WITH client_financials AS ( COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0) as total_payments, COUNT(DISTINCT d.id) as deposit_count, COUNT(DISTINCT p.id) as payment_count - FROM satoshimachine.dca_clients c - LEFT JOIN satoshimachine.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed' - LEFT JOIN satoshimachine.dca_payments p ON c.id = p.client_id + FROM spirekeeper.dca_clients c + LEFT JOIN spirekeeper.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed' + LEFT JOIN spirekeeper.dca_payments p ON c.id = p.client_id GROUP BY c.id ) SELECT @@ -813,7 +813,7 @@ SELECT SUM(commission_amount) as total_commission, MIN(created_at) as first_transaction, MAX(created_at) as last_transaction -FROM satoshimachine.lamassu_transactions; +FROM spirekeeper.lamassu_transactions; -- 3. Failed/Pending Payments Check SELECT @@ -822,7 +822,7 @@ SELECT SUM(amount_sats) as total_amount, MIN(created_at) as oldest, MAX(created_at) as newest -FROM satoshimachine.dca_payments +FROM spirekeeper.dca_payments GROUP BY status ORDER BY CASE status @@ -839,7 +839,7 @@ SELECT status, created_at, EXTRACT(EPOCH FROM (NOW() - created_at))/3600 as hours_pending -FROM satoshimachine.dca_deposits +FROM spirekeeper.dca_deposits WHERE status = 'pending' AND created_at < NOW() - INTERVAL '48 hours' ORDER BY created_at; @@ -855,7 +855,7 @@ SELECT commission_amount, ROUND(crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100))) as expected_base, base_amount - ROUND(crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100))) as difference -FROM satoshimachine.lamassu_transactions +FROM spirekeeper.lamassu_transactions ORDER BY created_at DESC LIMIT 20; ``` @@ -876,15 +876,15 @@ psql -h localhost -U lnbits -d lnbits 2. **Direct Configuration Update**: ```sql -- Update Lamassu config directly -UPDATE satoshimachine.lamassu_config +UPDATE spirekeeper.lamassu_config SET polling_enabled = false -WHERE id = (SELECT MAX(id) FROM satoshimachine.lamassu_config); +WHERE id = (SELECT MAX(id) FROM spirekeeper.lamassu_config); ``` 3. **Manual Client Balance Update**: ```sql -- ONLY in emergency when dashboard unavailable -UPDATE satoshimachine.dca_clients +UPDATE spirekeeper.dca_clients SET remaining_balance = WHERE id = ; -- MUST document this action in incident log @@ -910,19 +910,19 @@ uv run lnbits ```bash # Export all DCA-related tables to CSV sqlite3 -header -csv /path/to/lnbits/database.sqlite \ - "SELECT * FROM satoshimachine.lamassu_transactions;" \ + "SELECT * FROM spirekeeper.lamassu_transactions;" \ > lamassu_transactions_export_$(date +%Y%m%d_%H%M%S).csv sqlite3 -header -csv /path/to/lnbits/database.sqlite \ - "SELECT * FROM satoshimachine.dca_payments;" \ + "SELECT * FROM spirekeeper.dca_payments;" \ > dca_payments_export_$(date +%Y%m%d_%H%M%S).csv sqlite3 -header -csv /path/to/lnbits/database.sqlite \ - "SELECT * FROM satoshimachine.dca_deposits;" \ + "SELECT * FROM spirekeeper.dca_deposits;" \ > dca_deposits_export_$(date +%Y%m%d_%H%M%S).csv sqlite3 -header -csv /path/to/lnbits/database.sqlite \ - "SELECT * FROM satoshimachine.dca_clients;" \ + "SELECT * FROM spirekeeper.dca_clients;" \ > dca_clients_export_$(date +%Y%m%d_%H%M%S).csv ``` @@ -946,9 +946,9 @@ SELECT dp.amount_sats as client_received, dp.status as payment_status, dp.payment_hash -FROM satoshimachine.lamassu_transactions lt -LEFT JOIN satoshimachine.dca_payments dp ON dp.lamassu_transaction_id = lt.id -LEFT JOIN satoshimachine.dca_clients c ON dp.client_id = c.id +FROM spirekeeper.lamassu_transactions lt +LEFT JOIN spirekeeper.dca_payments dp ON dp.lamassu_transaction_id = lt.id +LEFT JOIN spirekeeper.dca_clients c ON dp.client_id = c.id ORDER BY lt.created_at DESC, c.username; ``` @@ -981,18 +981,18 @@ async def process_lamassu_transaction(txn_data: dict) -> Optional[LamassuTransac ```sql -- Add unique constraint on transaction_id -ALTER TABLE satoshimachine.lamassu_transactions +ALTER TABLE spirekeeper.lamassu_transactions ADD CONSTRAINT unique_transaction_id UNIQUE (transaction_id); -- Prevent negative balances -ALTER TABLE satoshimachine.dca_clients +ALTER TABLE spirekeeper.dca_clients ADD CONSTRAINT positive_balance CHECK (remaining_balance >= 0); -- Ensure positive amounts -ALTER TABLE satoshimachine.dca_deposits +ALTER TABLE spirekeeper.dca_deposits ADD CONSTRAINT positive_deposit CHECK (amount > 0); -ALTER TABLE satoshimachine.dca_payments +ALTER TABLE spirekeeper.dca_payments ADD CONSTRAINT positive_payment CHECK (amount_sats > 0); ``` @@ -1196,7 +1196,7 @@ grep "wallet.*api\|payment_hash" lnbits.log | tail -50 ### System Access **LNBits Admin Dashboard**: -- URL: `https:///satoshimachine` +- URL: `https:///spirekeeper` - Requires superuser authentication **Database Access**: @@ -1205,7 +1205,7 @@ grep "wallet.*api\|payment_hash" lnbits.log | tail -50 sqlite3 /home/padreug/AioLabs/Git/lnbits-extensions/lnbits/data/database.sqlite # Direct table access -sqlite3 /path/to/db "SELECT * FROM satoshimachine.;" +sqlite3 /path/to/db "SELECT * FROM spirekeeper.;" ``` **Log Files**: @@ -1274,7 +1274,7 @@ pkill -f lnbits ```sql -- Disable automatic polling -UPDATE satoshimachine.lamassu_config +UPDATE spirekeeper.lamassu_config SET polling_enabled = false; ``` @@ -1283,7 +1283,7 @@ SET polling_enabled = false; ```sql -- All client balances summary SELECT id, username, remaining_balance, created_at -FROM satoshimachine.dca_clients +FROM spirekeeper.dca_clients ORDER BY remaining_balance DESC; ``` @@ -1291,7 +1291,7 @@ ORDER BY remaining_balance DESC; ```sql SELECT id, transaction_id, created_at, crypto_atoms, base_amount, commission_amount -FROM satoshimachine.lamassu_transactions +FROM spirekeeper.lamassu_transactions ORDER BY created_at DESC LIMIT 10; ``` @@ -1299,7 +1299,7 @@ LIMIT 10; ### Failed Payments ```sql -SELECT * FROM satoshimachine.dca_payments +SELECT * FROM spirekeeper.dca_payments WHERE status != 'completed' ORDER BY created_at DESC; ``` @@ -1314,19 +1314,19 @@ BACKUP_DIR="emergency_backup_${DATE}" mkdir -p $BACKUP_DIR sqlite3 -header -csv /path/to/database.sqlite \ - "SELECT * FROM satoshimachine.lamassu_transactions;" \ + "SELECT * FROM spirekeeper.lamassu_transactions;" \ > ${BACKUP_DIR}/lamassu_transactions.csv sqlite3 -header -csv /path/to/database.sqlite \ - "SELECT * FROM satoshimachine.dca_payments;" \ + "SELECT * FROM spirekeeper.dca_payments;" \ > ${BACKUP_DIR}/dca_payments.csv sqlite3 -header -csv /path/to/database.sqlite \ - "SELECT * FROM satoshimachine.dca_deposits;" \ + "SELECT * FROM spirekeeper.dca_deposits;" \ > ${BACKUP_DIR}/dca_deposits.csv sqlite3 -header -csv /path/to/database.sqlite \ - "SELECT * FROM satoshimachine.dca_clients;" \ + "SELECT * FROM spirekeeper.dca_clients;" \ > ${BACKUP_DIR}/dca_clients.csv echo "Backup complete in ${BACKUP_DIR}/" diff --git a/misc-docs/EMERGENCY_PROTOCOLS_PRINT.md b/misc-docs/EMERGENCY_PROTOCOLS_PRINT.md index eb5b6b9..d67ed17 100644 --- a/misc-docs/EMERGENCY_PROTOCOLS_PRINT.md +++ b/misc-docs/EMERGENCY_PROTOCOLS_PRINT.md @@ -38,7 +38,7 @@ -- Find duplicate transactions SELECT transaction_id, COUNT(*) as count, STRING_AGG(id::text, ', ') as record_ids -FROM satoshimachine.lamassu_transactions +FROM spirekeeper.lamassu_transactions GROUP BY transaction_id HAVING COUNT(*) > 1; ``` @@ -60,16 +60,16 @@ HAVING COUNT(*) > 1; -- Step 1: Identify duplicate distributions SELECT lt.transaction_id, lt.id, lt.created_at, lt.base_amount, COUNT(dp.id) as distribution_count -FROM satoshimachine.lamassu_transactions lt -LEFT JOIN satoshimachine.dca_payments dp ON dp.lamassu_transaction_id = lt.id +FROM spirekeeper.lamassu_transactions lt +LEFT JOIN spirekeeper.dca_payments dp ON dp.lamassu_transaction_id = lt.id GROUP BY lt.id -HAVING COUNT(dp.id) > (SELECT COUNT(*) FROM satoshimachine.dca_clients WHERE remaining_balance > 0); +HAVING COUNT(dp.id) > (SELECT COUNT(*) FROM spirekeeper.dca_clients WHERE remaining_balance > 0); -- Step 2: Calculate over-distributed amounts per client SELECT client_id, SUM(amount_sats) as total_received, -- Manual calculation of expected amount needed here -FROM satoshimachine.dca_payments +FROM spirekeeper.dca_payments WHERE lamassu_transaction_id IN (SELECT id FROM duplicates_table) GROUP BY client_id; ``` @@ -94,7 +94,7 @@ if existing: **Required Database Change**: ```sql -ALTER TABLE satoshimachine.lamassu_transactions +ALTER TABLE spirekeeper.lamassu_transactions ADD CONSTRAINT unique_transaction_id UNIQUE (transaction_id); ``` @@ -117,7 +117,7 @@ ADD CONSTRAINT unique_transaction_id UNIQUE (transaction_id); -- Check last successful poll SELECT MAX(created_at) as last_transaction, EXTRACT(EPOCH FROM (NOW() - MAX(created_at)))/3600 as hours_since_last -FROM satoshimachine.lamassu_transactions; +FROM spirekeeper.lamassu_transactions; -- If hours_since_last > 24, investigate immediately ``` @@ -194,8 +194,8 @@ ssh-keygen -t ed25519 -f ~/.ssh/satmachine_lamassu -C "satmachine-polling" -- Find stuck/failed payments (older than 1 hour, not completed) SELECT dp.id, dp.client_id, dp.amount_sats, dp.status, dp.created_at, c.username, dp.payment_hash -FROM satoshimachine.dca_payments dp -JOIN satoshimachine.dca_clients c ON dp.client_id = c.id +FROM spirekeeper.dca_payments dp +JOIN spirekeeper.dca_clients c ON dp.client_id = c.id WHERE dp.status != 'completed' AND dp.created_at < NOW() - INTERVAL '1 hour' ORDER BY dp.created_at DESC; @@ -207,7 +207,7 @@ ORDER BY dp.created_at DESC; SELECT SUM(commission_amount) as total_commission_expected, -- Manually check actual wallet balance in LNBits -FROM satoshimachine.lamassu_transactions; +FROM spirekeeper.lamassu_transactions; ``` #### Immediate Response @@ -258,9 +258,9 @@ SELECT c.remaining_balance, COALESCE(SUM(d.amount), 0) as total_deposits, COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0) as total_payments -FROM satoshimachine.dca_clients c -LEFT JOIN satoshimachine.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed' -LEFT JOIN satoshimachine.dca_payments p ON c.id = p.client_id +FROM spirekeeper.dca_clients c +LEFT JOIN spirekeeper.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed' +LEFT JOIN spirekeeper.dca_payments p ON c.id = p.client_id GROUP BY c.id; ``` @@ -319,9 +319,9 @@ SELECT COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0) as total_distributed, COALESCE(SUM(d.amount), 0) - COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0) as calculated_balance, c.remaining_balance - (COALESCE(SUM(d.amount), 0) - COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0)) as discrepancy -FROM satoshimachine.dca_clients c -LEFT JOIN satoshimachine.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed' -LEFT JOIN satoshimachine.dca_payments p ON c.id = p.client_id +FROM spirekeeper.dca_clients c +LEFT JOIN spirekeeper.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed' +LEFT JOIN spirekeeper.dca_payments p ON c.id = p.client_id GROUP BY c.id, c.username, c.remaining_balance HAVING ABS(c.remaining_balance - (COALESCE(SUM(d.amount), 0) - COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0))) > 1; ``` @@ -341,11 +341,11 @@ HAVING ABS(c.remaining_balance - (COALESCE(SUM(d.amount), 0) - COALESCE(SUM(CASE ```sql -- Get complete transaction history for client SELECT 'DEPOSIT' as type, id, amount, status, created_at, confirmed_at -FROM satoshimachine.dca_deposits +FROM spirekeeper.dca_deposits WHERE client_id = UNION ALL SELECT 'PAYMENT' as type, id, amount_sats, status, created_at, NULL -FROM satoshimachine.dca_payments +FROM spirekeeper.dca_payments WHERE client_id = ORDER BY created_at; ``` @@ -361,18 +361,18 @@ ORDER BY created_at; **Option A: Adjustment Entry** (Recommended) ```sql -- Create compensating deposit for positive discrepancy -INSERT INTO satoshimachine.dca_deposits (client_id, amount, status, note) +INSERT INTO spirekeeper.dca_deposits (client_id, amount, status, note) VALUES (, , 'confirmed', 'Balance correction - reconciliation 2025-10-19'); -- OR Create compensating payment for negative discrepancy -INSERT INTO satoshimachine.dca_payments (client_id, amount_sats, status, note) +INSERT INTO spirekeeper.dca_payments (client_id, amount_sats, status, note) VALUES (, , 'completed', 'Balance correction - reconciliation 2025-10-19'); ``` **Option B: Direct Balance Update** (Use with extreme caution) ```sql -- ONLY if audit trail is complete and discrepancy is unexplained -UPDATE satoshimachine.dca_clients +UPDATE spirekeeper.dca_clients SET remaining_balance = , updated_at = NOW() WHERE id = ; @@ -396,11 +396,11 @@ async def daily_reconciliation_check(): **Database Constraints**: ```sql -- Prevent negative balances -ALTER TABLE satoshimachine.dca_clients +ALTER TABLE spirekeeper.dca_clients ADD CONSTRAINT positive_balance CHECK (remaining_balance >= 0); -- Prevent confirmed deposits with zero amount -ALTER TABLE satoshimachine.dca_deposits +ALTER TABLE spirekeeper.dca_deposits ADD CONSTRAINT positive_deposit CHECK (amount > 0); ``` @@ -435,7 +435,7 @@ SELECT -- Calculate differences base_amount - ROUND(crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100))) as base_difference, commission_amount - ROUND(crypto_atoms - (crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100)))) as commission_difference -FROM satoshimachine.lamassu_transactions +FROM spirekeeper.lamassu_transactions WHERE ABS(base_amount - ROUND(crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100)))) > 1 OR ABS(commission_amount - ROUND(crypto_atoms - (crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100))))) > 1; ``` @@ -485,9 +485,9 @@ SELECT SUM(lt.base_amount) as total_distributed, SUM(expected_base) as should_have_distributed, SUM(expected_base - lt.base_amount) as client_impact -FROM satoshimachine.lamassu_transactions lt -JOIN satoshimachine.dca_payments dp ON dp.lamassu_transaction_id = lt.id -JOIN satoshimachine.dca_clients c ON dp.client_id = c.id +FROM spirekeeper.lamassu_transactions lt +JOIN spirekeeper.dca_payments dp ON dp.lamassu_transaction_id = lt.id +JOIN spirekeeper.dca_clients c ON dp.client_id = c.id WHERE -- filter for affected transactions GROUP BY c.id; ``` @@ -505,7 +505,7 @@ GROUP BY c.id; - Create compensating payments to affected clients: ```sql -- Add to client balances -UPDATE satoshimachine.dca_clients c +UPDATE spirekeeper.dca_clients c SET remaining_balance = remaining_balance + adjustment.amount FROM ( -- Calculate adjustment per client @@ -572,7 +572,7 @@ assert abs(calculated_total - crypto_atoms) <= 1, "Commission calculation error SELECT SUM(commission_amount) as expected_total_commission, -- Compare to actual wallet balance in LNBits dashboard -FROM satoshimachine.lamassu_transactions +FROM spirekeeper.lamassu_transactions WHERE created_at > ''; ``` @@ -607,7 +607,7 @@ curl -X GET https:///api/v1/wallet \ SELECT commission_wallet_id, LEFT(commission_wallet_adminkey, 10) || '...' as key_preview, updated_at -FROM satoshimachine.lamassu_config +FROM spirekeeper.lamassu_config ORDER BY updated_at DESC LIMIT 1; ``` @@ -783,9 +783,9 @@ WITH client_financials AS ( COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0) as total_payments, COUNT(DISTINCT d.id) as deposit_count, COUNT(DISTINCT p.id) as payment_count - FROM satoshimachine.dca_clients c - LEFT JOIN satoshimachine.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed' - LEFT JOIN satoshimachine.dca_payments p ON c.id = p.client_id + FROM spirekeeper.dca_clients c + LEFT JOIN spirekeeper.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed' + LEFT JOIN spirekeeper.dca_payments p ON c.id = p.client_id GROUP BY c.id ) SELECT @@ -813,7 +813,7 @@ SELECT SUM(commission_amount) as total_commission, MIN(created_at) as first_transaction, MAX(created_at) as last_transaction -FROM satoshimachine.lamassu_transactions; +FROM spirekeeper.lamassu_transactions; -- 3. Failed/Pending Payments Check SELECT @@ -822,7 +822,7 @@ SELECT SUM(amount_sats) as total_amount, MIN(created_at) as oldest, MAX(created_at) as newest -FROM satoshimachine.dca_payments +FROM spirekeeper.dca_payments GROUP BY status ORDER BY CASE status @@ -839,7 +839,7 @@ SELECT status, created_at, EXTRACT(EPOCH FROM (NOW() - created_at))/3600 as hours_pending -FROM satoshimachine.dca_deposits +FROM spirekeeper.dca_deposits WHERE status = 'pending' AND created_at < NOW() - INTERVAL '48 hours' ORDER BY created_at; @@ -855,7 +855,7 @@ SELECT commission_amount, ROUND(crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100))) as expected_base, base_amount - ROUND(crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100))) as difference -FROM satoshimachine.lamassu_transactions +FROM spirekeeper.lamassu_transactions ORDER BY created_at DESC LIMIT 20; ``` @@ -876,15 +876,15 @@ psql -h localhost -U lnbits -d lnbits 2. **Direct Configuration Update**: ```sql -- Update Lamassu config directly -UPDATE satoshimachine.lamassu_config +UPDATE spirekeeper.lamassu_config SET polling_enabled = false -WHERE id = (SELECT MAX(id) FROM satoshimachine.lamassu_config); +WHERE id = (SELECT MAX(id) FROM spirekeeper.lamassu_config); ``` 3. **Manual Client Balance Update**: ```sql -- ONLY in emergency when dashboard unavailable -UPDATE satoshimachine.dca_clients +UPDATE spirekeeper.dca_clients SET remaining_balance = WHERE id = ; -- MUST document this action in incident log @@ -910,19 +910,19 @@ uv run lnbits ```bash # Export all DCA-related tables to CSV sqlite3 -header -csv /path/to/lnbits/database.sqlite \ - "SELECT * FROM satoshimachine.lamassu_transactions;" \ + "SELECT * FROM spirekeeper.lamassu_transactions;" \ > lamassu_transactions_export_$(date +%Y%m%d_%H%M%S).csv sqlite3 -header -csv /path/to/lnbits/database.sqlite \ - "SELECT * FROM satoshimachine.dca_payments;" \ + "SELECT * FROM spirekeeper.dca_payments;" \ > dca_payments_export_$(date +%Y%m%d_%H%M%S).csv sqlite3 -header -csv /path/to/lnbits/database.sqlite \ - "SELECT * FROM satoshimachine.dca_deposits;" \ + "SELECT * FROM spirekeeper.dca_deposits;" \ > dca_deposits_export_$(date +%Y%m%d_%H%M%S).csv sqlite3 -header -csv /path/to/lnbits/database.sqlite \ - "SELECT * FROM satoshimachine.dca_clients;" \ + "SELECT * FROM spirekeeper.dca_clients;" \ > dca_clients_export_$(date +%Y%m%d_%H%M%S).csv ``` @@ -946,9 +946,9 @@ SELECT dp.amount_sats as client_received, dp.status as payment_status, dp.payment_hash -FROM satoshimachine.lamassu_transactions lt -LEFT JOIN satoshimachine.dca_payments dp ON dp.lamassu_transaction_id = lt.id -LEFT JOIN satoshimachine.dca_clients c ON dp.client_id = c.id +FROM spirekeeper.lamassu_transactions lt +LEFT JOIN spirekeeper.dca_payments dp ON dp.lamassu_transaction_id = lt.id +LEFT JOIN spirekeeper.dca_clients c ON dp.client_id = c.id ORDER BY lt.created_at DESC, c.username; ``` @@ -981,18 +981,18 @@ async def process_lamassu_transaction(txn_data: dict) -> Optional[LamassuTransac ```sql -- Add unique constraint on transaction_id -ALTER TABLE satoshimachine.lamassu_transactions +ALTER TABLE spirekeeper.lamassu_transactions ADD CONSTRAINT unique_transaction_id UNIQUE (transaction_id); -- Prevent negative balances -ALTER TABLE satoshimachine.dca_clients +ALTER TABLE spirekeeper.dca_clients ADD CONSTRAINT positive_balance CHECK (remaining_balance >= 0); -- Ensure positive amounts -ALTER TABLE satoshimachine.dca_deposits +ALTER TABLE spirekeeper.dca_deposits ADD CONSTRAINT positive_deposit CHECK (amount > 0); -ALTER TABLE satoshimachine.dca_payments +ALTER TABLE spirekeeper.dca_payments ADD CONSTRAINT positive_payment CHECK (amount_sats > 0); ``` @@ -1196,7 +1196,7 @@ grep "wallet.*api\|payment_hash" lnbits.log | tail -50 ### System Access **LNBits Admin Dashboard**: -- URL: `https:///satoshimachine` +- URL: `https:///spirekeeper` - Requires superuser authentication **Database Access**: @@ -1205,7 +1205,7 @@ grep "wallet.*api\|payment_hash" lnbits.log | tail -50 sqlite3 /home/padreug/AioLabs/Git/lnbits-extensions/lnbits/data/database.sqlite # Direct table access -sqlite3 /path/to/db "SELECT * FROM satoshimachine.;" +sqlite3 /path/to/db "SELECT * FROM spirekeeper.;" ``` **Log Files**: @@ -1274,7 +1274,7 @@ pkill -f lnbits ```sql -- Disable automatic polling -UPDATE satoshimachine.lamassu_config +UPDATE spirekeeper.lamassu_config SET polling_enabled = false; ``` @@ -1283,7 +1283,7 @@ SET polling_enabled = false; ```sql -- All client balances summary SELECT id, username, remaining_balance, created_at -FROM satoshimachine.dca_clients +FROM spirekeeper.dca_clients ORDER BY remaining_balance DESC; ``` @@ -1291,7 +1291,7 @@ ORDER BY remaining_balance DESC; ```sql SELECT id, transaction_id, created_at, crypto_atoms, base_amount, commission_amount -FROM satoshimachine.lamassu_transactions +FROM spirekeeper.lamassu_transactions ORDER BY created_at DESC LIMIT 10; ``` @@ -1299,7 +1299,7 @@ LIMIT 10; ### Failed Payments ```sql -SELECT * FROM satoshimachine.dca_payments +SELECT * FROM spirekeeper.dca_payments WHERE status != 'completed' ORDER BY created_at DESC; ``` @@ -1314,19 +1314,19 @@ BACKUP_DIR="emergency_backup_${DATE}" mkdir -p $BACKUP_DIR sqlite3 -header -csv /path/to/database.sqlite \ - "SELECT * FROM satoshimachine.lamassu_transactions;" \ + "SELECT * FROM spirekeeper.lamassu_transactions;" \ > ${BACKUP_DIR}/lamassu_transactions.csv sqlite3 -header -csv /path/to/database.sqlite \ - "SELECT * FROM satoshimachine.dca_payments;" \ + "SELECT * FROM spirekeeper.dca_payments;" \ > ${BACKUP_DIR}/dca_payments.csv sqlite3 -header -csv /path/to/database.sqlite \ - "SELECT * FROM satoshimachine.dca_deposits;" \ + "SELECT * FROM spirekeeper.dca_deposits;" \ > ${BACKUP_DIR}/dca_deposits.csv sqlite3 -header -csv /path/to/database.sqlite \ - "SELECT * FROM satoshimachine.dca_clients;" \ + "SELECT * FROM spirekeeper.dca_clients;" \ > ${BACKUP_DIR}/dca_clients.csv echo "Backup complete in ${BACKUP_DIR}/"