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

View file

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