fix(v2): m005-m007 idempotency + SQLite CREATE INDEX syntax; template self-closing tags

Three live-test bugs caught while wiring the v2 frontend to a real
LNbits regtest instance.

1. **SQLite parse error on CREATE INDEX with schema-prefixed table.**
   `CREATE INDEX foo ON satoshimachine.bar (col)` errors with
   "near '.': syntax error" on SQLite. PG accepts the prefix on the
   table; SQLite expects the schema prefix on the INDEX NAME only,
   not on the table. Cleanest portable fix (libra extension pattern):
   drop `satoshimachine.` from the table reference inside CREATE INDEX.
   The index lands in the same schema as the table regardless.

2. **m005 non-idempotent after partial failure.** The previous bug
   above tripped m005 mid-flight (CREATE TABLE super_config + CREATE
   TABLE dca_machines succeeded, then the first CREATE INDEX errored
   and aborted). LNbits doesn't mark partial migrations done, so the
   next boot re-ran m005 — and CREATE TABLE super_config now errored
   with "table already exists". To make recovery clean:
   - CREATE TABLE IF NOT EXISTS on every table (13 tables)
   - CREATE INDEX IF NOT EXISTS on every index (10 indexes)
   - super_config seed INSERT wrapped in check-then-insert so the
     PK conflict on 'default' on re-run is avoided

3. **Vue compiler error code 30 — self-closing tags on non-void
   elements in templates/satmachineadmin/index.html.** The previous
   commit `98f82be` on satmachineclient called this out as a known
   LNbits UMD gotcha: Vue 3 UMD's compiler doesn't auto-expand `<q-icon />`
   the way SFCs do — the browser HTML parser sees the malformed self-
   closing tag and aborts compilation. 118 tags expanded from
   `<q-foo … />` to `<q-foo …></q-foo>` via mechanical rewrite.

Verified end-to-end against docker regtest-lnbits-1:
  - All three migrations (m005, m006, m007) ran cleanly
  - Schema has all 8 v2 tables + 10 indexes
  - "satmachineadmin v2 loaded" + invoice listener registered
  - /satmachineadmin/ returns 200; JS loads; super-config + machines
    endpoints respond

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-14 19:12:51 +02:00
commit cb19ba3675
2 changed files with 160 additions and 154 deletions

View file

@ -10,7 +10,7 @@ async def m001_initial_dca_schema(db):
# DCA Clients table
await db.execute(
f"""
CREATE TABLE satoshimachine.dca_clients (
CREATE TABLE IF NOT EXISTS satoshimachine.dca_clients (
id TEXT PRIMARY KEY NOT NULL,
user_id TEXT NOT NULL,
wallet_id TEXT NOT NULL,
@ -27,7 +27,7 @@ async def m001_initial_dca_schema(db):
# DCA Deposits table
await db.execute(
f"""
CREATE TABLE satoshimachine.dca_deposits (
CREATE TABLE IF NOT EXISTS satoshimachine.dca_deposits (
id TEXT PRIMARY KEY NOT NULL,
client_id TEXT NOT NULL,
amount INTEGER NOT NULL,
@ -43,7 +43,7 @@ async def m001_initial_dca_schema(db):
# DCA Payments table
await db.execute(
f"""
CREATE TABLE satoshimachine.dca_payments (
CREATE TABLE IF NOT EXISTS satoshimachine.dca_payments (
id TEXT PRIMARY KEY NOT NULL,
client_id TEXT NOT NULL,
amount_sats INTEGER NOT NULL,
@ -61,7 +61,7 @@ async def m001_initial_dca_schema(db):
# Lamassu Configuration table
await db.execute(
f"""
CREATE TABLE satoshimachine.lamassu_config (
CREATE TABLE IF NOT EXISTS satoshimachine.lamassu_config (
id TEXT PRIMARY KEY NOT NULL,
host TEXT NOT NULL,
port INTEGER NOT NULL DEFAULT 5432,
@ -90,7 +90,7 @@ async def m001_initial_dca_schema(db):
# Lamassu Transactions table (for audit trail)
await db.execute(
f"""
CREATE TABLE satoshimachine.lamassu_transactions (
CREATE TABLE IF NOT EXISTS satoshimachine.lamassu_transactions (
id TEXT PRIMARY KEY NOT NULL,
lamassu_transaction_id TEXT NOT NULL UNIQUE,
fiat_amount INTEGER NOT NULL,
@ -200,7 +200,7 @@ async def m005_satmachine_v2_overhaul(db):
# The only thing the LNbits super has direct DB control over in this extension.
await db.execute(
f"""
CREATE TABLE satoshimachine.super_config (
CREATE TABLE IF NOT EXISTS satoshimachine.super_config (
id TEXT PRIMARY KEY,
super_fee_pct DECIMAL(10,4) NOT NULL DEFAULT 0.0000,
super_fee_wallet_id TEXT,
@ -208,17 +208,23 @@ async def m005_satmachine_v2_overhaul(db):
);
"""
)
await db.execute(
"INSERT INTO satoshimachine.super_config (id, super_fee_pct) "
"VALUES ('default', 0.0000)"
# Idempotent seed: check before insert so re-runs after a partial-
# failure recovery don't trip the PK conflict.
existing = await db.fetchone(
"SELECT id FROM satoshimachine.super_config WHERE id = 'default'"
)
if not existing:
await db.execute(
"INSERT INTO satoshimachine.super_config (id, super_fee_pct) "
"VALUES ('default', 0.0000)"
)
# dca_machines — one row per bitSpire ATM, owned by exactly one operator.
# fallback_commission_pct kicks in only when bitSpire's settlement Payment.extra
# is missing the (net_sats, fee_sats) split — see plan's lamassu-next ask #1.
await db.execute(
f"""
CREATE TABLE satoshimachine.dca_machines (
CREATE TABLE IF NOT EXISTS satoshimachine.dca_machines (
id TEXT PRIMARY KEY,
operator_user_id TEXT NOT NULL,
machine_npub TEXT NOT NULL UNIQUE,
@ -234,15 +240,15 @@ async def m005_satmachine_v2_overhaul(db):
"""
)
await db.execute(
"CREATE INDEX dca_machines_operator_idx "
"ON satoshimachine.dca_machines (operator_user_id)"
"CREATE INDEX IF NOT EXISTS dca_machines_operator_idx "
"ON dca_machines (operator_user_id)"
)
# dca_clients — LP registrations scoped per (machine, user). One LP can hold
# positions across many machines (and many operators) on the same instance.
await db.execute(
f"""
CREATE TABLE satoshimachine.dca_clients (
CREATE TABLE IF NOT EXISTS satoshimachine.dca_clients (
id TEXT PRIMARY KEY,
machine_id TEXT NOT NULL,
user_id TEXT NOT NULL,
@ -259,19 +265,19 @@ async def m005_satmachine_v2_overhaul(db):
"""
)
await db.execute(
"CREATE UNIQUE INDEX dca_clients_machine_user_uq "
"ON satoshimachine.dca_clients (machine_id, user_id)"
"CREATE UNIQUE INDEX IF NOT EXISTS dca_clients_machine_user_uq "
"ON dca_clients (machine_id, user_id)"
)
await db.execute(
"CREATE INDEX dca_clients_user_idx "
"ON satoshimachine.dca_clients (user_id)"
"CREATE INDEX IF NOT EXISTS dca_clients_user_idx "
"ON dca_clients (user_id)"
)
# dca_deposits — fiat the operator (or super) records against an LP at a machine.
# creator_user_id preserves audit trail (resolves a v1 tech-debt finding).
await db.execute(
f"""
CREATE TABLE satoshimachine.dca_deposits (
CREATE TABLE IF NOT EXISTS satoshimachine.dca_deposits (
id TEXT PRIMARY KEY,
client_id TEXT NOT NULL,
machine_id TEXT NOT NULL,
@ -286,8 +292,8 @@ async def m005_satmachine_v2_overhaul(db):
"""
)
await db.execute(
"CREATE INDEX dca_deposits_client_idx "
"ON satoshimachine.dca_deposits (client_id, created_at DESC)"
"CREATE INDEX IF NOT EXISTS dca_deposits_client_idx "
"ON dca_deposits (client_id, created_at DESC)"
)
# dca_settlements — idempotency table for bitSpire-driven settlements.
@ -306,7 +312,7 @@ async def m005_satmachine_v2_overhaul(db):
# section "Customer discounts".
await db.execute(
f"""
CREATE TABLE satoshimachine.dca_settlements (
CREATE TABLE IF NOT EXISTS satoshimachine.dca_settlements (
id TEXT PRIMARY KEY,
machine_id TEXT NOT NULL,
payment_hash TEXT NOT NULL UNIQUE,
@ -332,8 +338,8 @@ async def m005_satmachine_v2_overhaul(db):
"""
)
await db.execute(
"CREATE INDEX dca_settlements_machine_idx "
"ON satoshimachine.dca_settlements (machine_id, created_at DESC)"
"CREATE INDEX IF NOT EXISTS dca_settlements_machine_idx "
"ON dca_settlements (machine_id, created_at DESC)"
)
# payment_hash UNIQUE already creates a lookup index — no extra index needed.
@ -344,7 +350,7 @@ async def m005_satmachine_v2_overhaul(db):
# scope must equal 1.0 — enforced at write-time in crud.py.
await db.execute(
f"""
CREATE TABLE satoshimachine.dca_commission_splits (
CREATE TABLE IF NOT EXISTS satoshimachine.dca_commission_splits (
id TEXT PRIMARY KEY,
machine_id TEXT,
operator_user_id TEXT NOT NULL,
@ -357,8 +363,8 @@ async def m005_satmachine_v2_overhaul(db):
"""
)
await db.execute(
"CREATE INDEX dca_commission_splits_lookup_idx "
"ON satoshimachine.dca_commission_splits (operator_user_id, machine_id)"
"CREATE INDEX IF NOT EXISTS dca_commission_splits_lookup_idx "
"ON dca_commission_splits (operator_user_id, machine_id)"
)
# dca_payments — every leg of every distribution. The leg_type discriminator
@ -367,7 +373,7 @@ async def m005_satmachine_v2_overhaul(db):
# autoforward (see satmachineadmin#8) | refund.
await db.execute(
f"""
CREATE TABLE satoshimachine.dca_payments (
CREATE TABLE IF NOT EXISTS satoshimachine.dca_payments (
id TEXT PRIMARY KEY,
settlement_id TEXT,
client_id TEXT,
@ -388,16 +394,16 @@ async def m005_satmachine_v2_overhaul(db):
"""
)
await db.execute(
"CREATE INDEX dca_payments_client_idx "
"ON satoshimachine.dca_payments (client_id, created_at DESC)"
"CREATE INDEX IF NOT EXISTS dca_payments_client_idx "
"ON dca_payments (client_id, created_at DESC)"
)
await db.execute(
"CREATE INDEX dca_payments_settlement_idx "
"ON satoshimachine.dca_payments (settlement_id)"
"CREATE INDEX IF NOT EXISTS dca_payments_settlement_idx "
"ON dca_payments (settlement_id)"
)
await db.execute(
"CREATE INDEX dca_payments_operator_idx "
"ON satoshimachine.dca_payments (operator_user_id, leg_type)"
"CREATE INDEX IF NOT EXISTS dca_payments_operator_idx "
"ON dca_payments (operator_user_id, leg_type)"
)
# dca_telemetry — latest replaceable kind-30078 (public availability beacon)
@ -408,7 +414,7 @@ async def m005_satmachine_v2_overhaul(db):
# lands. Ingest opportunistically; render absent fields gracefully in the UI.
await db.execute(
"""
CREATE TABLE satoshimachine.dca_telemetry (
CREATE TABLE IF NOT EXISTS satoshimachine.dca_telemetry (
machine_id TEXT PRIMARY KEY,
beacon_cash_in BOOLEAN,
beacon_cash_out BOOLEAN,
@ -473,6 +479,6 @@ async def m007_settlement_claim_and_machine_wallet_unique(db):
"ADD COLUMN processing_claim TEXT"
)
await db.execute(
"CREATE UNIQUE INDEX dca_machines_wallet_id_uq "
"ON satoshimachine.dca_machines (wallet_id)"
"CREATE UNIQUE INDEX IF NOT EXISTS dca_machines_wallet_id_uq "
"ON dca_machines (wallet_id)"
)