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,6 +208,12 @@ async def m005_satmachine_v2_overhaul(db):
);
"""
)
# 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)"
@ -218,7 +224,7 @@ async def m005_satmachine_v2_overhaul(db):
# 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)"
)

View file

@ -63,19 +63,19 @@
active-color="primary"
indicator-color="primary"
narrow-indicator>
<q-tab name="fleet" icon="precision_manufacturing" label="Fleet" />
<q-tab name="clients" icon="group" label="Clients" />
<q-tab name="deposits" icon="receipt_long" label="Deposits" />
<q-tab name="commission" icon="call_split" label="Commission" />
<q-tab name="fleet" icon="precision_manufacturing" label="Fleet"></q-tab>
<q-tab name="clients" icon="group" label="Clients"></q-tab>
<q-tab name="deposits" icon="receipt_long" label="Deposits"></q-tab>
<q-tab name="commission" icon="call_split" label="Commission"></q-tab>
<q-tab name="worklist" icon="warning" label="Worklist">
<q-badge
v-if="worklistCount > 0"
color="red" floating>${ worklistCount }
</q-badge>
</q-tab>
<q-tab name="reports" icon="download" label="Reports" />
<q-tab name="reports" icon="download" label="Reports"></q-tab>
</q-tabs>
<q-separator />
<q-separator ></q-separator>
<q-tab-panels v-model="activeTab" animated>
@ -95,13 +95,13 @@
<q-btn
color="primary" icon="add"
label="Add machine"
@click="openAddMachineDialog" />
@click="openAddMachineDialog"></q-btn>
</div>
</div>
<q-banner v-if="!machines.length" class="bg-blue-1 text-grey-9">
<template v-slot:avatar>
<q-icon name="info" color="blue" />
<q-icon name="info" color="blue"></q-icon>
</template>
You haven't registered any machines yet. Click <b>Add machine</b> to
register a bitSpire ATM by its Nostr npub.
@ -187,13 +187,13 @@
<q-btn color="primary" icon="person_add"
label="Register LP"
:disable="!machines.length"
@click="openAddClientDialog" />
@click="openAddClientDialog"></q-btn>
</div>
</div>
<q-banner v-if="!machines.length" class="bg-orange-1 text-grey-9">
<template v-slot:avatar>
<q-icon name="warning" color="orange" />
<q-icon name="warning" color="orange"></q-icon>
</template>
Register at least one machine before adding LPs — an LP is scoped
to a specific machine.
@ -201,7 +201,7 @@
<q-banner v-else-if="!clients.length" class="bg-blue-1 text-grey-9">
<template v-slot:avatar>
<q-icon name="info" color="blue" />
<q-icon name="info" color="blue"></q-icon>
</template>
No LPs yet. Use <b>Register LP</b> to add one at any of your machines.
</q-banner>
@ -228,7 +228,7 @@
</q-td>
<q-td key="dca_mode">
<q-badge :color="props.row.dca_mode === 'flow' ? 'blue' : 'purple'"
:label="props.row.dca_mode" />
:label="props.row.dca_mode"></q-badge>
</q-td>
<q-td key="remaining_balance" class="text-right">
<span v-if="clientBalances[props.row.id]"
@ -251,27 +251,27 @@
<q-td key="status">
<q-badge
:color="props.row.status === 'active' ? 'green' : 'grey'"
:label="props.row.status" />
:label="props.row.status"></q-badge>
</q-td>
<q-td key="actions" auto-width>
<q-btn-dropdown flat dense size="sm" icon="more_vert">
<q-list dense>
<q-item clickable v-close-popup
@click="openEditClientDialog(props.row)">
<q-item-section avatar><q-icon name="edit" /></q-item-section>
<q-item-section avatar><q-icon name="edit"></q-icon></q-item-section>
<q-item-section>Edit</q-item-section>
</q-item>
<q-item clickable v-close-popup
@click="openSettleBalanceDialog(props.row)">
<q-item-section avatar>
<q-icon name="payments" color="primary" />
<q-icon name="payments" color="primary"></q-icon>
</q-item-section>
<q-item-section>Settle balance…</q-item-section>
</q-item>
<q-item clickable v-close-popup
@click="confirmDeleteClient(props.row)">
<q-item-section avatar>
<q-icon name="delete" color="red-7" />
<q-icon name="delete" color="red-7"></q-icon>
</q-item-section>
<q-item-section>Delete</q-item-section>
</q-item>
@ -295,7 +295,7 @@
<q-btn color="primary" icon="add"
label="Record deposit"
:disable="!clients.length"
@click="openAddDepositDialog" />
@click="openAddDepositDialog"></q-btn>
</div>
</div>
@ -307,18 +307,18 @@
{label: 'Pending', value: 'pending'},
{label: 'Confirmed', value: 'confirmed'},
{label: 'Rejected', value: 'rejected'}]"
label="Status" emit-value map-options dense outlined />
label="Status" emit-value map-options dense outlined></q-select>
</div>
<div class="col-12 col-md-4">
<q-select v-model="depositsFilter.client_id"
:options="depositClientOptions"
label="LP" emit-value map-options dense outlined clearable />
label="LP" emit-value map-options dense outlined clearable></q-select>
</div>
</div>
<q-banner v-if="!clients.length" class="bg-orange-1 text-grey-9">
<template v-slot:avatar>
<q-icon name="warning" color="orange" />
<q-icon name="warning" color="orange"></q-icon>
</template>
Register at least one LP before recording deposits.
</q-banner>
@ -326,7 +326,7 @@
<q-banner v-else-if="!filteredDeposits.length && !deposits.length"
class="bg-blue-1 text-grey-9">
<template v-slot:avatar>
<q-icon name="info" color="blue" />
<q-icon name="info" color="blue"></q-icon>
</template>
No deposits yet. Use <b>Record deposit</b> to log a new one.
</q-banner>
@ -347,7 +347,7 @@
<q-tr :props="props">
<q-td key="status">
<q-badge :color="depositStatusColor(props.row.status)"
:label="props.row.status" />
:label="props.row.status"></q-badge>
</q-td>
<q-td key="client">
<span v-text="clientUsernameById(props.row.client_id)"></span>
@ -381,7 +381,7 @@
clickable v-close-popup
@click="confirmDepositStatus(props.row, 'confirmed')">
<q-item-section avatar>
<q-icon name="check_circle" color="green" />
<q-icon name="check_circle" color="green"></q-icon>
</q-item-section>
<q-item-section>Confirm</q-item-section>
</q-item>
@ -389,7 +389,7 @@
clickable v-close-popup
@click="openRejectDepositDialog(props.row)">
<q-item-section avatar>
<q-icon name="cancel" color="red" />
<q-icon name="cancel" color="red"></q-icon>
</q-item-section>
<q-item-section>Reject…</q-item-section>
</q-item>
@ -397,7 +397,7 @@
clickable v-close-popup
@click="openEditDepositDialog(props.row)">
<q-item-section avatar>
<q-icon name="edit" />
<q-icon name="edit"></q-icon>
</q-item-section>
<q-item-section>Edit</q-item-section>
</q-item>
@ -405,7 +405,7 @@
clickable v-close-popup
@click="confirmDeleteDeposit(props.row)">
<q-item-section avatar>
<q-icon name="delete" color="red-7" />
<q-icon name="delete" color="red-7"></q-icon>
</q-item-section>
<q-item-section>Delete</q-item-section>
</q-item>
@ -435,7 +435,7 @@
label="Scope being edited"
emit-value map-options
dense outlined
@update:model-value="loadCommissionSplits" />
@update:model-value="loadCommissionSplits"></q-select>
<div class="text-caption q-mt-xs" :style="{opacity: 0.7}">
<span v-if="commissionScope === null">
Default ruleset — applies to every machine without an
@ -464,7 +464,7 @@
v-text="(commissionSum * 100).toFixed(2) + '%'"></span>
<q-icon v-if="commissionSumValid"
name="check_circle" color="green" size="xs"
class="q-ml-xs" />
class="q-ml-xs"></q-icon>
<q-icon v-else
name="error" color="red" size="xs"
class="q-ml-xs">
@ -475,14 +475,14 @@
<div class="col-auto">
<q-btn flat dense color="primary" icon="add"
label="Add leg"
@click="addCommissionLeg" />
@click="addCommissionLeg"></q-btn>
</div>
</div>
<q-banner v-if="!commissionLegs.length"
class="bg-blue-1 text-grey-9">
<template v-slot:avatar>
<q-icon name="info" color="blue" />
<q-icon name="info" color="blue"></q-icon>
</template>
<span v-if="commissionScope === null">
No default rules. Without a default, all operator
@ -499,12 +499,12 @@
<q-select v-model="leg.wallet_id"
:options="walletOptions"
label="Wallet"
emit-value map-options dense outlined />
emit-value map-options dense outlined></q-select>
</div>
<div class="col-7 col-md-4">
<q-input v-model="leg.label"
label="Label (e.g. employee, maintenance)"
dense outlined />
dense outlined></q-input>
</div>
<div class="col-4 col-md-3">
<q-input v-model.number="leg.pct"
@ -519,13 +519,13 @@
</div>
<div class="col-1 col-md-1">
<q-btn flat dense round icon="delete" color="red-7"
@click="commissionLegs.splice(idx, 1)" />
@click="commissionLegs.splice(idx, 1)"></q-btn>
</div>
</div>
<q-banner v-if="commissionPreview" class="bg-grey-2 text-grey-9 q-mt-md">
<template v-slot:avatar>
<q-icon name="visibility" color="grey" />
<q-icon name="visibility" color="grey"></q-icon>
</template>
Preview against
<b v-text="formatSats(commissionPreviewInput)"></b>
@ -543,15 +543,15 @@
<q-btn v-if="commissionScope !== null && commissionLegs.length"
flat color="red"
label="Remove override"
@click="confirmDeleteCommissionOverride" />
@click="confirmDeleteCommissionOverride"></q-btn>
<q-btn flat label="Reload"
:disable="commissionSaving"
@click="loadCommissionSplits" />
@click="loadCommissionSplits"></q-btn>
<q-btn color="primary"
label="Save"
:disable="!commissionSumValid"
:loading="commissionSaving"
@click="saveCommissionSplits" />
@click="saveCommissionSplits"></q-btn>
</q-card-actions>
</q-card>
</q-tab-panel>
@ -569,17 +569,17 @@
<q-input v-model.number="worklistThreshold"
label="Threshold (min)" dense outlined
:style="{width: '120px'}"
type="number" min="1" />
type="number" min="1"></q-input>
<q-btn flat dense color="primary" icon="refresh"
:loading="worklistLoading"
@click="loadWorklist" />
@click="loadWorklist"></q-btn>
</div>
</div>
<q-banner v-if="worklist.totalCount === 0"
class="bg-green-1 text-grey-9">
<template v-slot:avatar>
<q-icon name="check_circle" color="green" />
<q-icon name="check_circle" color="green"></q-icon>
</template>
All clear — no errored or stuck settlements.
</q-banner>
@ -589,7 +589,7 @@
class="q-mb-lg">
<div class="row items-center q-mb-sm">
<q-icon :name="bucket.icon" :color="bucket.color" size="sm"
class="q-mr-sm" />
class="q-mr-sm"></q-icon>
<span :style="{fontWeight: 500}" v-text="bucket.label"></span>
<q-chip dense
:color="bucket.color" text-color="white"
@ -666,7 +666,7 @@
<q-card-actions>
<q-btn flat color="primary" icon="download"
label="machines.csv"
@click="downloadMachinesCsv" />
@click="downloadMachinesCsv"></q-btn>
</q-card-actions>
</q-card>
</div>
@ -681,7 +681,7 @@
<q-card-actions>
<q-btn flat color="primary" icon="download"
label="clients.csv"
@click="downloadClientsCsv" />
@click="downloadClientsCsv"></q-btn>
</q-card-actions>
</q-card>
</div>
@ -696,7 +696,7 @@
<q-card-actions>
<q-btn flat color="primary" icon="download"
label="deposits.csv"
@click="downloadDepositsCsv" />
@click="downloadDepositsCsv"></q-btn>
</q-card-actions>
</q-card>
</div>
@ -712,7 +712,7 @@
<q-btn flat color="primary" icon="download"
label="payments.csv"
:loading="reportsBusy"
@click="downloadPaymentsCsv" />
@click="downloadPaymentsCsv"></q-btn>
</q-card-actions>
</q-card>
</div>
@ -729,8 +729,8 @@
<q-card :style="{minWidth: '480px', maxWidth: '95vw'}">
<q-card-section class="row items-center q-pb-none">
<div class="text-h6">Add bitSpire machine</div>
<q-space />
<q-btn icon="close" flat round dense v-close-popup />
<q-space ></q-space>
<q-btn icon="close" flat round dense v-close-popup></q-btn>
</q-card-section>
<q-card-section>
<p class="text-caption q-mb-md" :style="{opacity: 0.7}">
@ -744,14 +744,14 @@
label="Machine name"
hint="Operator-friendly label (e.g. ATM-Antigua-1)"
class="q-mb-md"
dense outlined />
dense outlined></q-input>
<q-input
v-model="addMachineDialog.data.location"
label="Location (optional)"
hint="Physical address or city; shown on operator dashboard"
class="q-mb-md"
dense outlined />
dense outlined></q-input>
<q-input
v-model="addMachineDialog.data.machine_npub"
@ -779,21 +779,21 @@
hint="Only used if bitSpire doesn't supply a per-tx split (lamassu-next#44)."
type="number" step="0.0001" min="0" max="1"
class="q-mb-md"
dense outlined />
dense outlined></q-input>
<q-input
v-model="addMachineDialog.data.fiat_code"
label="Fiat code"
hint="Currency the ATM dispenses (GTQ / USD / MXN / etc.)"
class="q-mb-md"
dense outlined />
dense outlined></q-input>
</q-card-section>
<q-card-actions align="right" class="text-primary">
<q-btn flat label="Cancel" v-close-popup />
<q-btn flat label="Cancel" v-close-popup></q-btn>
<q-btn
color="primary" label="Add machine"
:loading="addMachineDialog.saving"
@click="submitAddMachine" />
@click="submitAddMachine"></q-btn>
</q-card-actions>
</q-card>
</q-dialog>
@ -809,7 +809,7 @@
v-text="machineDetail.machine.name || 'Unnamed machine'"></div>
<q-chip dense color="white" text-color="primary"
v-text="machineDetail.machine.fiat_code"></q-chip>
<q-space />
<q-space ></q-space>
<q-btn flat dense round icon="refresh"
@click="reloadMachineDetail"
:loading="machineDetail.loading">
@ -844,7 +844,7 @@
</div>
</div>
<q-separator class="q-mb-md" />
<q-separator class="q-mb-md"></q-separator>
<div class="row items-center q-mb-sm">
<div class="col">
@ -873,7 +873,7 @@
<q-tr :props="props">
<q-td key="status">
<q-badge :color="settlementStatusColor(props.row.status)"
:label="props.row.status" />
:label="props.row.status"></q-badge>
<q-icon v-if="props.row.used_fallback_split"
name="warning_amber" color="orange" size="sm"
class="q-ml-xs">
@ -915,7 +915,7 @@
<q-item clickable v-close-popup
@click="openSettlementNote(props.row)">
<q-item-section avatar>
<q-icon name="edit_note" />
<q-icon name="edit_note"></q-icon>
</q-item-section>
<q-item-section>Add note</q-item-section>
</q-item>
@ -923,7 +923,7 @@
clickable v-close-popup
@click="confirmRetrySettlement(props.row)">
<q-item-section avatar>
<q-icon name="restart_alt" color="primary" />
<q-icon name="restart_alt" color="primary"></q-icon>
</q-item-section>
<q-item-section>Retry distribution</q-item-section>
</q-item>
@ -932,7 +932,7 @@
clickable v-close-popup
@click="openPartialDispense(props.row)">
<q-item-section avatar>
<q-icon name="precision_manufacturing" color="warning" />
<q-icon name="precision_manufacturing" color="warning"></q-icon>
</q-item-section>
<q-item-section>Partial dispense…</q-item-section>
</q-item>
@ -941,7 +941,7 @@
clickable v-close-popup
@click="confirmForceReset(props.row)">
<q-item-section avatar>
<q-icon name="local_fire_department" color="red" />
<q-icon name="local_fire_department" color="red"></q-icon>
</q-item-section>
<q-item-section>Force-reset (stuck)…</q-item-section>
</q-item>
@ -968,13 +968,13 @@
<q-card :style="{minWidth: '480px', maxWidth: '95vw'}">
<q-card-section class="row items-center q-pb-none">
<div class="text-h6">Apply partial dispense</div>
<q-space />
<q-btn icon="close" flat round dense v-close-popup />
<q-space ></q-space>
<q-btn icon="close" flat round dense v-close-popup></q-btn>
</q-card-section>
<q-card-section v-if="partialDispenseDialog.settlement">
<q-banner class="bg-blue-1 text-grey-9 q-mb-md">
<template v-slot:avatar>
<q-icon name="info" color="blue" />
<q-icon name="info" color="blue"></q-icon>
</template>
Original gross:
<b v-text="formatSats(partialDispenseDialog.settlement.gross_sats)"></b>.
@ -984,8 +984,8 @@
<q-tabs v-model="partialDispenseDialog.mode" dense
active-color="primary" indicator-color="primary"
align="justify">
<q-tab name="fraction" label="By fraction (0..1)" />
<q-tab name="sats" label="By exact sats" />
<q-tab name="fraction" label="By fraction (0..1)"></q-tab>
<q-tab name="sats" label="By exact sats"></q-tab>
</q-tabs>
<q-tab-panels v-model="partialDispenseDialog.mode" animated>
<q-tab-panel name="fraction" class="q-pa-sm">
@ -993,7 +993,7 @@
label="Dispensed fraction"
hint="e.g. 0.6 means 60% of the original tx was dispensed"
type="number" step="0.01" min="0" max="1"
dense outlined />
dense outlined></q-input>
</q-tab-panel>
<q-tab-panel name="sats" class="q-pa-sm">
<q-input v-model.number="partialDispenseDialog.dispensed_sats"
@ -1001,21 +1001,21 @@
hint="Exact sat amount actually dispensed (≤ original gross)"
type="number" step="1" min="0"
:max="partialDispenseDialog.settlement.gross_sats"
dense outlined />
dense outlined></q-input>
</q-tab-panel>
</q-tab-panels>
<q-input v-model="partialDispenseDialog.notes"
label="Reason (recorded in audit memo)"
type="textarea" autogrow
class="q-mt-md"
dense outlined />
dense outlined></q-input>
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="Cancel" v-close-popup />
<q-btn flat label="Cancel" v-close-popup></q-btn>
<q-btn color="warning"
label="Apply partial dispense"
:loading="partialDispenseDialog.saving"
@click="submitPartialDispense" />
@click="submitPartialDispense"></q-btn>
</q-card-actions>
</q-card>
</q-dialog>
@ -1027,8 +1027,8 @@
<q-card :style="{minWidth: '420px', maxWidth: '95vw'}">
<q-card-section class="row items-center q-pb-none">
<div class="text-h6">Add note to settlement</div>
<q-space />
<q-btn icon="close" flat round dense v-close-popup />
<q-space ></q-space>
<q-btn icon="close" flat round dense v-close-popup></q-btn>
</q-card-section>
<q-card-section>
<p class="text-caption q-mb-sm" :style="{opacity: 0.7}">
@ -1042,10 +1042,10 @@
dense outlined />
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="Cancel" v-close-popup />
<q-btn flat label="Cancel" v-close-popup></q-btn>
<q-btn color="primary" label="Save note"
:loading="noteDialog.saving"
@click="submitNote" />
@click="submitNote"></q-btn>
</q-card-actions>
</q-card>
</q-dialog>
@ -1057,8 +1057,8 @@
<q-card :style="{minWidth: '460px', maxWidth: '95vw'}">
<q-card-section class="row items-center q-pb-none">
<div class="text-h6">Platform fee (super-only)</div>
<q-space />
<q-btn icon="close" flat round dense v-close-popup />
<q-space ></q-space>
<q-btn icon="close" flat round dense v-close-popup></q-btn>
</q-card-section>
<q-card-section>
<p class="text-caption q-mb-md" :style="{opacity: 0.7}">
@ -1070,17 +1070,17 @@
label="Fee % (decimal, 0..1)"
hint="0.30 = 30% of every operator's commission"
type="number" step="0.0001" min="0" max="1"
class="q-mb-md" dense outlined />
class="q-mb-md" dense outlined></q-input>
<q-input v-model="superFeeDialog.data.super_fee_wallet_id"
label="Super fee destination wallet_id"
hint="LNbits wallet that collects the platform fee"
class="q-mb-md" dense outlined />
class="q-mb-md" dense outlined></q-input>
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="Cancel" v-close-popup />
<q-btn flat label="Cancel" v-close-popup></q-btn>
<q-btn color="primary" label="Save"
:loading="superFeeDialog.saving"
@click="submitSuperFee" />
@click="submitSuperFee"></q-btn>
</q-card-actions>
</q-card>
</q-dialog>
@ -1093,8 +1093,8 @@
<q-card-section class="row items-center q-pb-none">
<div class="text-h6"
v-text="depositDialog.mode === 'add' ? 'Record deposit' : 'Edit deposit'"></div>
<q-space />
<q-btn icon="close" flat round dense v-close-popup />
<q-space ></q-space>
<q-btn icon="close" flat round dense v-close-popup></q-btn>
</q-card-section>
<q-card-section>
<q-select v-if="depositDialog.mode === 'add'"
@ -1113,19 +1113,19 @@
<q-input v-model="depositDialog.data.currency"
label="Currency"
class="q-mb-md" dense outlined />
class="q-mb-md" dense outlined></q-input>
<q-input v-model="depositDialog.data.notes"
label="Notes (optional)"
type="textarea" autogrow
class="q-mb-md" dense outlined />
class="q-mb-md" dense outlined></q-input>
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="Cancel" v-close-popup />
<q-btn flat label="Cancel" v-close-popup></q-btn>
<q-btn color="primary"
:label="depositDialog.mode === 'add' ? 'Record' : 'Save'"
:loading="depositDialog.saving"
@click="submitDeposit" />
@click="submitDeposit"></q-btn>
</q-card-actions>
</q-card>
</q-dialog>
@ -1137,8 +1137,8 @@
<q-card :style="{minWidth: '420px', maxWidth: '95vw'}">
<q-card-section class="row items-center q-pb-none">
<div class="text-h6">Reject deposit</div>
<q-space />
<q-btn icon="close" flat round dense v-close-popup />
<q-space ></q-space>
<q-btn icon="close" flat round dense v-close-popup></q-btn>
</q-card-section>
<q-card-section>
<p class="text-caption q-mb-sm" :style="{opacity: 0.7}">
@ -1148,13 +1148,13 @@
<q-input v-model="rejectDepositDialog.notes"
label="Reason (optional)"
type="textarea" autogrow
dense outlined />
dense outlined></q-input>
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="Cancel" v-close-popup />
<q-btn flat label="Cancel" v-close-popup></q-btn>
<q-btn color="red" label="Reject"
:loading="rejectDepositDialog.saving"
@click="submitRejectDeposit" />
@click="submitRejectDeposit"></q-btn>
</q-card-actions>
</q-card>
</q-dialog>
@ -1166,8 +1166,8 @@
<q-card :style="{minWidth: '520px', maxWidth: '95vw'}">
<q-card-section class="row items-center q-pb-none">
<div class="text-h6" v-text="clientDialog.mode === 'add' ? 'Register liquidity provider' : 'Edit LP'"></div>
<q-space />
<q-btn icon="close" flat round dense v-close-popup />
<q-space ></q-space>
<q-btn icon="close" flat round dense v-close-popup></q-btn>
</q-card-section>
<q-card-section>
<p class="text-caption q-mb-md" :style="{opacity: 0.7}"
@ -1203,43 +1203,43 @@
<q-input v-model="clientDialog.data.username"
label="Display name (optional)"
class="q-mb-md" dense outlined />
class="q-mb-md" dense outlined></q-input>
<q-select v-model="clientDialog.data.dca_mode"
:options="[{label: 'Flow (proportional)', value: 'flow'},
{label: 'Fixed (daily limit)', value: 'fixed'}]"
label="DCA mode"
emit-value map-options
class="q-mb-md" dense outlined />
class="q-mb-md" dense outlined></q-select>
<q-input v-if="clientDialog.data.dca_mode === 'fixed'"
v-model.number="clientDialog.data.fixed_mode_daily_limit"
label="Fixed mode daily limit (fiat)"
type="number" step="0.01"
class="q-mb-md" dense outlined />
class="q-mb-md" dense outlined></q-input>
<q-toggle v-model="clientDialog.data.autoforward_enabled"
label="Auto-forward DCA to external LN address"
class="q-mb-md" />
class="q-mb-md"></q-toggle>
<q-input v-if="clientDialog.data.autoforward_enabled"
v-model="clientDialog.data.autoforward_ln_address"
label="LN address (e.g. user@walletofsatoshi.com)"
hint="LP-controlled; failures leave sats safely in LP's LNbits wallet"
class="q-mb-md" dense outlined />
class="q-mb-md" dense outlined></q-input>
<q-select v-if="clientDialog.mode === 'edit'"
v-model="clientDialog.data.status"
:options="['active', 'paused', 'closed']"
label="Status"
class="q-mb-md" dense outlined />
class="q-mb-md" dense outlined></q-select>
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="Cancel" v-close-popup />
<q-btn flat label="Cancel" v-close-popup></q-btn>
<q-btn color="primary"
:label="clientDialog.mode === 'add' ? 'Register' : 'Save'"
:loading="clientDialog.saving"
@click="submitClient" />
@click="submitClient"></q-btn>
</q-card-actions>
</q-card>
</q-dialog>
@ -1251,13 +1251,13 @@
<q-card :style="{minWidth: '480px', maxWidth: '95vw'}">
<q-card-section class="row items-center q-pb-none">
<div class="text-h6">Settle LP balance</div>
<q-space />
<q-btn icon="close" flat round dense v-close-popup />
<q-space ></q-space>
<q-btn icon="close" flat round dense v-close-popup></q-btn>
</q-card-section>
<q-card-section v-if="settleBalanceDialog.client">
<q-banner class="bg-blue-1 text-grey-9 q-mb-md">
<template v-slot:avatar>
<q-icon name="payments" color="blue" />
<q-icon name="payments" color="blue"></q-icon>
</template>
Pay the LP's remaining fiat balance in sats from your wallet at the
rate you choose. Useful to zero out small balances that would
@ -1287,18 +1287,18 @@
<q-input v-model.number="settleBalanceDialog.data.amount_fiat"
label="Amount (fiat) — leave blank to settle full remaining"
type="number" step="0.01"
class="q-mb-md" dense outlined />
class="q-mb-md" dense outlined></q-input>
<q-input v-model="settleBalanceDialog.data.notes"
label="Notes (optional, audit memo)"
type="textarea" autogrow
class="q-mb-md" dense outlined />
class="q-mb-md" dense outlined></q-input>
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="Cancel" v-close-popup />
<q-btn flat label="Cancel" v-close-popup></q-btn>
<q-btn color="primary" label="Settle balance"
:loading="settleBalanceDialog.saving"
@click="submitSettleBalance" />
@click="submitSettleBalance"></q-btn>
</q-card-actions>
</q-card>
</q-dialog>
@ -1310,36 +1310,36 @@
<q-card :style="{minWidth: '480px', maxWidth: '95vw'}">
<q-card-section class="row items-center q-pb-none">
<div class="text-h6">Edit machine</div>
<q-space />
<q-btn icon="close" flat round dense v-close-popup />
<q-space ></q-space>
<q-btn icon="close" flat round dense v-close-popup></q-btn>
</q-card-section>
<q-card-section>
<q-input v-model="editMachineDialog.data.name"
label="Machine name" class="q-mb-md" dense outlined />
label="Machine name" class="q-mb-md" dense outlined></q-input>
<q-input v-model="editMachineDialog.data.location"
label="Location" class="q-mb-md" dense outlined />
label="Location" class="q-mb-md" dense outlined></q-input>
<q-select
v-model="editMachineDialog.data.wallet_id"
:options="walletOptions"
label="Wallet"
emit-value map-options
class="q-mb-md"
dense outlined />
dense outlined></q-select>
<q-input v-model.number="editMachineDialog.data.fallback_commission_pct"
label="Fallback commission %"
type="number" step="0.0001" min="0" max="1"
class="q-mb-md" dense outlined />
class="q-mb-md" dense outlined></q-input>
<q-input v-model="editMachineDialog.data.fiat_code"
label="Fiat code" class="q-mb-md" dense outlined />
label="Fiat code" class="q-mb-md" dense outlined></q-input>
<q-toggle v-model="editMachineDialog.data.is_active"
label="Active (receives settlements)" class="q-mb-md" />
label="Active (receives settlements)" class="q-mb-md"></q-toggle>
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="Cancel" v-close-popup />
<q-btn flat label="Cancel" v-close-popup></q-btn>
<q-btn
color="primary" label="Save"
:loading="editMachineDialog.saving"
@click="submitEditMachine" />
@click="submitEditMachine"></q-btn>
</q-card-actions>
</q-card>
</q-dialog>