Some checks failed
ci.yml / feat(cash-in): super_config.max_cash_in_sats per-tx cap + UI (#31) (pull_request) Failing after 0s
Wires the server-side per-transaction cash-in ceiling the `create_withdraw` handler already enforces (it read the value defensively via getattr; this makes it a first-class config field). - migrations.py m012: ADD COLUMN super_config.max_cash_in_sats INTEGER (NULL = no cap). - models.py: SuperConfig.max_cash_in_sats + UpdateSuperConfigData field with a >= 0 validator. - super-fee dialog: a "Max cash-in per transaction (sats)" input; blank sends null (the PUT skips null, preserving the current value — set 0 to reject every cash-in). crud `update_super_config` and the PUT endpoint flow the field through automatically (dynamic dict update; check_super_user gated). Why a sats cap and not the bunker ACL: the ACL / usage caps (#28) gate call *rate*, not *sats*, and `principal_sats` is necessarily ATM-attested — so a single in-rate call could request an arbitrarily large payout. This bounds a compromised/buggy machine to one capped transaction. Verified on the dev stack: m012 runs, the model round-trips the column (GET returns the set value), and a negative value is rejected.
1659 lines
75 KiB
HTML
1659 lines
75 KiB
HTML
{% extends "base.html" %}
|
||
{% from "macros.jinja" import window_vars with context %}
|
||
|
||
{% block scripts %}
|
||
{{ window_vars(user) }}
|
||
<script src="{{ static_url_for('spirekeeper/static', path='js/index.js') }}"></script>
|
||
{% endblock %}
|
||
|
||
{% block page %}
|
||
<div class="row q-col-gutter-md" id="dcaAdmin">
|
||
<div class="col-12">
|
||
|
||
<!-- Header bar -->
|
||
<div class="row items-center q-mb-md">
|
||
<div class="col">
|
||
<h4 class="q-my-none">Satoshi Machine — Operator</h4>
|
||
<p class="text-caption q-my-none" :style="{opacity: 0.7}">
|
||
Manage your bitSpire fleet, liquidity providers, and commission distribution.
|
||
</p>
|
||
</div>
|
||
<div class="col-auto">
|
||
<q-btn
|
||
flat dense round icon="refresh" color="grey"
|
||
@click="refreshAll" :loading="refreshing">
|
||
<q-tooltip>Refresh all data</q-tooltip>
|
||
</q-btn>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Platform fee banner (read-only for operators; editable inline if super) -->
|
||
<q-banner
|
||
v-if="superConfig"
|
||
class="q-mb-md"
|
||
:class="superAnyFee > 0 ? 'bg-blue-1 text-grey-9' : 'bg-grey-2 text-grey-9'">
|
||
<template v-slot:avatar>
|
||
<q-icon name="account_balance" :color="superAnyFee > 0 ? 'blue' : 'grey'"></q-icon>
|
||
</template>
|
||
<span :style="{fontWeight: 500}">
|
||
LNbits platform fee:
|
||
<span :style="{color: '#1976d2'}">cash-in ${ (superConfig.super_cash_in_fee_fraction * 100).toFixed(2) }%</span>
|
||
·
|
||
<span :style="{color: '#1976d2'}">cash-out ${ (superConfig.super_cash_out_fee_fraction * 100).toFixed(2) }%</span>
|
||
of each transaction's principal.
|
||
</span>
|
||
<span :style="{opacity: 0.7, fontSize: '0.85em'}" class="q-ml-sm">
|
||
Operator's per-machine fee rides on top of these.
|
||
</span>
|
||
<template v-slot:action>
|
||
<q-btn v-if="g?.user?.super_user"
|
||
flat dense color="blue" icon="edit"
|
||
label="Edit"
|
||
@click="openSuperFeeDialog">
|
||
<q-tooltip>Super-only: set platform fee + destination wallet</q-tooltip>
|
||
</q-btn>
|
||
</template>
|
||
</q-banner>
|
||
|
||
<!-- Main tab strip -->
|
||
<q-card flat bordered>
|
||
<q-tabs
|
||
v-model="activeTab"
|
||
dense
|
||
align="left"
|
||
class="text-grey-7"
|
||
active-color="primary"
|
||
indicator-color="primary"
|
||
narrow-indicator>
|
||
<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>
|
||
</q-tabs>
|
||
<q-separator ></q-separator>
|
||
|
||
<q-tab-panels v-model="activeTab" animated>
|
||
|
||
<!-- ============================================================= -->
|
||
<!-- FLEET TAB -->
|
||
<!-- ============================================================= -->
|
||
<q-tab-panel name="fleet">
|
||
<div class="row items-center q-mb-md">
|
||
<div class="col">
|
||
<h6 class="q-my-none">Your machines</h6>
|
||
<p class="text-caption q-my-none" :style="{opacity: 0.7}">
|
||
Each ATM is paired with one dedicated wallet. Inbound payments to
|
||
that wallet trigger automatic distribution.
|
||
</p>
|
||
</div>
|
||
<div class="col-auto">
|
||
<q-btn
|
||
color="primary" icon="add"
|
||
label="Add machine"
|
||
@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>
|
||
</template>
|
||
You haven't registered any machines yet. Click <b>Add machine</b> to
|
||
register a bitSpire ATM by its Nostr npub.
|
||
</q-banner>
|
||
|
||
<q-table
|
||
v-else
|
||
dense flat
|
||
:rows="machines"
|
||
row-key="id"
|
||
:columns="machinesTable.columns"
|
||
:pagination="machinesTable.pagination"
|
||
:rows-per-page-options="[10, 25, 50]">
|
||
<template v-slot:body="props">
|
||
<q-tr :props="props">
|
||
<q-td key="status">
|
||
<q-icon
|
||
:name="props.row.is_active ? 'check_circle' : 'pause_circle'"
|
||
:color="props.row.is_active ? 'green' : 'grey'"
|
||
size="sm">
|
||
<q-tooltip>${ props.row.is_active ? 'Active' : 'Paused' }</q-tooltip>
|
||
</q-icon>
|
||
</q-td>
|
||
<q-td key="name">
|
||
<div :style="{fontWeight: 500}" v-text="props.row.name || 'Unnamed'"></div>
|
||
<div :style="{fontSize: '0.8em', opacity: 0.6}"
|
||
v-text="props.row.location || 'No location set'"></div>
|
||
<q-chip v-if="props.row.paired_at"
|
||
dense size="sm" color="green-2" text-color="green-9"
|
||
icon="link" :style="{marginTop: '2px'}">
|
||
paired
|
||
<q-tooltip>Bunker key minted; paired ${ new Date(props.row.paired_at).toLocaleString() }</q-tooltip>
|
||
</q-chip>
|
||
<q-chip v-else
|
||
dense size="sm" color="grey-3" text-color="grey-8"
|
||
icon="link_off" :style="{marginTop: '2px'}">
|
||
not paired
|
||
</q-chip>
|
||
</q-td>
|
||
<q-td key="machine_npub">
|
||
<code :style="{fontSize: '0.85em'}"
|
||
v-text="shortNpub(props.row.machine_npub)"></code>
|
||
<q-btn flat dense round size="xs" icon="content_copy"
|
||
@click="copy(props.row.machine_npub)">
|
||
<q-tooltip>Copy full npub</q-tooltip>
|
||
</q-btn>
|
||
</q-td>
|
||
<q-td key="wallet_id">
|
||
<code :style="{fontSize: '0.85em'}"
|
||
v-text="shortId(props.row.wallet_id)"></code>
|
||
</q-td>
|
||
<q-td key="fiat_code" v-text="props.row.fiat_code"></q-td>
|
||
<q-td key="actions" auto-width>
|
||
<q-btn flat dense round size="sm" icon="visibility"
|
||
color="primary"
|
||
@click="viewMachine(props.row)">
|
||
<q-tooltip>View settlements</q-tooltip>
|
||
</q-btn>
|
||
<q-btn flat dense round size="sm" icon="edit"
|
||
color="grey"
|
||
@click="openEditMachineDialog(props.row)">
|
||
<q-tooltip>Edit</q-tooltip>
|
||
</q-btn>
|
||
<q-btn flat dense round size="sm" icon="qr_code_2"
|
||
color="teal"
|
||
@click="openPairDialog(props.row)">
|
||
<q-tooltip>${ props.row.paired_at ? 'Re-pair (new seed URL)' : 'Pair (seed URL)' }</q-tooltip>
|
||
</q-btn>
|
||
<q-btn v-if="props.row.paired_at"
|
||
flat dense round size="sm" icon="link_off"
|
||
color="orange-8"
|
||
@click="confirmRevokeMachine(props.row)">
|
||
<q-tooltip>Revoke spire access</q-tooltip>
|
||
</q-btn>
|
||
<q-btn flat dense round size="sm" icon="delete"
|
||
color="red-7"
|
||
@click="confirmDeleteMachine(props.row)">
|
||
<q-tooltip>Delete</q-tooltip>
|
||
</q-btn>
|
||
</q-td>
|
||
</q-tr>
|
||
</template>
|
||
</q-table>
|
||
</q-tab-panel>
|
||
|
||
<!-- ============================================================= -->
|
||
<!-- PLACEHOLDERS for tabs that land in P9b–P9g -->
|
||
<!-- ============================================================= -->
|
||
<q-tab-panel name="clients">
|
||
<div class="row items-center q-mb-md">
|
||
<div class="col">
|
||
<h6 class="q-my-none">Liquidity providers</h6>
|
||
<p class="text-caption q-my-none" :style="{opacity: 0.7}">
|
||
LPs receive proportional DCA distributions from your machines.
|
||
Balances reflect deposits less the sats they've been paid.
|
||
</p>
|
||
</div>
|
||
<div class="col-auto">
|
||
<q-btn color="primary" icon="person_add"
|
||
label="Register LP"
|
||
:disable="!machines.length"
|
||
@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>
|
||
</template>
|
||
Register at least one machine before adding LPs — an LP is scoped
|
||
to a specific machine.
|
||
</q-banner>
|
||
|
||
<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>
|
||
</template>
|
||
No LPs yet. Use <b>Register LP</b> to add one at any of your machines.
|
||
</q-banner>
|
||
|
||
<q-table v-else
|
||
dense flat
|
||
:rows="clients"
|
||
row-key="id"
|
||
:columns="clientsTable.columns"
|
||
:rows-per-page-options="[10, 25, 50]"
|
||
:pagination="clientsTable.pagination">
|
||
<template v-slot:body="props">
|
||
<q-tr :props="props">
|
||
<q-td key="machine">
|
||
<span :style="{fontWeight: 500}"
|
||
v-text="machineNameById(props.row.machine_id)"></span>
|
||
</q-td>
|
||
<q-td key="username">
|
||
<span v-text="props.row.username || shortId(props.row.user_id)"></span>
|
||
</q-td>
|
||
<q-td key="onboarded">
|
||
<q-icon v-if="props.row.lp_onboarded"
|
||
name="check_circle" color="green" size="sm">
|
||
<q-tooltip>
|
||
LP has onboarded via satmachineclient.
|
||
Deposits and DCA distributions can proceed.
|
||
</q-tooltip>
|
||
</q-icon>
|
||
<q-icon v-else name="pending" color="deep-orange" size="sm">
|
||
<q-tooltip>
|
||
LP hasn't installed/opened satmachineclient yet.
|
||
Deposits will be refused until they register and
|
||
select a DCA wallet.
|
||
</q-tooltip>
|
||
</q-icon>
|
||
</q-td>
|
||
<q-td key="remaining_balance" class="text-right">
|
||
<span v-if="clientBalances[props.row.id]"
|
||
:style="{color: clientBalances[props.row.id].remaining_balance > 0 ? '#2e7d32' : '#9e9e9e', fontWeight: 500}"
|
||
v-text="formatFiat(clientBalances[props.row.id].remaining_balance, clientBalances[props.row.id].currency)"></span>
|
||
<span v-else :style="{opacity: 0.5}">…</span>
|
||
</q-td>
|
||
<q-td key="status">
|
||
<q-badge
|
||
:color="props.row.status === 'active' ? 'green' : 'grey'"
|
||
: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-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>
|
||
</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>
|
||
</q-item-section>
|
||
<q-item-section>Delete</q-item-section>
|
||
</q-item>
|
||
</q-list>
|
||
</q-btn-dropdown>
|
||
</q-td>
|
||
</q-tr>
|
||
</template>
|
||
</q-table>
|
||
</q-tab-panel>
|
||
<q-tab-panel name="deposits">
|
||
<div class="row items-center q-mb-md">
|
||
<div class="col">
|
||
<h6 class="q-my-none">Deposits</h6>
|
||
<p class="text-caption q-my-none" :style="{opacity: 0.7}">
|
||
Record fiat handed in by LPs. Confirmed deposits increase the
|
||
LP's balance and feed proportional DCA distribution.
|
||
</p>
|
||
</div>
|
||
<div class="col-auto">
|
||
<q-btn color="primary" icon="add"
|
||
label="Record deposit"
|
||
:disable="!clients.length"
|
||
@click="openAddDepositDialog"></q-btn>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row q-col-gutter-sm q-mb-md">
|
||
<div class="col-12 col-md-3">
|
||
<q-select v-model="depositsFilter.status"
|
||
:options="[
|
||
{label: 'All statuses', value: null},
|
||
{label: 'Pending', value: 'pending'},
|
||
{label: 'Confirmed', value: 'confirmed'},
|
||
{label: 'Rejected', value: 'rejected'}]"
|
||
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></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>
|
||
</template>
|
||
Register at least one LP before recording deposits.
|
||
</q-banner>
|
||
|
||
<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>
|
||
</template>
|
||
No deposits yet. Use <b>Record deposit</b> to log a new one.
|
||
</q-banner>
|
||
|
||
<q-banner v-else-if="!filteredDeposits.length"
|
||
class="bg-grey-2 text-grey-9">
|
||
No deposits match the current filters.
|
||
</q-banner>
|
||
|
||
<q-table v-else
|
||
dense flat
|
||
:rows="filteredDeposits"
|
||
row-key="id"
|
||
:columns="depositsTable.columns"
|
||
:rows-per-page-options="[10, 25, 50]"
|
||
:pagination="depositsTable.pagination">
|
||
<template v-slot:body="props">
|
||
<q-tr :props="props">
|
||
<q-td key="status">
|
||
<q-badge :color="depositStatusColor(props.row.status)"
|
||
:label="props.row.status"></q-badge>
|
||
</q-td>
|
||
<q-td key="client">
|
||
<span v-text="clientUsernameById(props.row.client_id)"></span>
|
||
<div :style="{fontSize: '0.75em', opacity: 0.6}"
|
||
v-text="machineNameById(props.row.machine_id)"></div>
|
||
</q-td>
|
||
<q-td key="amount" class="text-right">
|
||
<span :style="{fontWeight: 500}"
|
||
v-text="formatFiat(props.row.amount, props.row.currency)"></span>
|
||
</q-td>
|
||
<q-td key="created_at">
|
||
<span :style="{fontSize: '0.85em'}"
|
||
v-text="formatTime(props.row.created_at)"></span>
|
||
</q-td>
|
||
<q-td key="confirmed_at">
|
||
<span v-if="props.row.confirmed_at"
|
||
:style="{fontSize: '0.85em'}"
|
||
v-text="formatTime(props.row.confirmed_at)"></span>
|
||
<span v-else :style="{opacity: 0.4}">—</span>
|
||
</q-td>
|
||
<q-td key="notes">
|
||
<span v-if="props.row.notes"
|
||
:style="{fontSize: '0.85em'}"
|
||
v-text="props.row.notes"></span>
|
||
<span v-else :style="{opacity: 0.4}">—</span>
|
||
</q-td>
|
||
<q-td key="actions" auto-width>
|
||
<q-btn-dropdown flat dense size="sm" icon="more_vert">
|
||
<q-list dense>
|
||
<q-item v-if="props.row.status === 'pending'"
|
||
clickable v-close-popup
|
||
@click="confirmDepositStatus(props.row, 'confirmed')">
|
||
<q-item-section avatar>
|
||
<q-icon name="check_circle" color="green"></q-icon>
|
||
</q-item-section>
|
||
<q-item-section>Confirm</q-item-section>
|
||
</q-item>
|
||
<q-item v-if="props.row.status === 'pending'"
|
||
clickable v-close-popup
|
||
@click="openRejectDepositDialog(props.row)">
|
||
<q-item-section avatar>
|
||
<q-icon name="cancel" color="red"></q-icon>
|
||
</q-item-section>
|
||
<q-item-section>Reject…</q-item-section>
|
||
</q-item>
|
||
<q-item v-if="props.row.status === 'pending'"
|
||
clickable v-close-popup
|
||
@click="openEditDepositDialog(props.row)">
|
||
<q-item-section avatar>
|
||
<q-icon name="edit"></q-icon>
|
||
</q-item-section>
|
||
<q-item-section>Edit</q-item-section>
|
||
</q-item>
|
||
<q-item v-if="props.row.status === 'pending'"
|
||
clickable v-close-popup
|
||
@click="confirmDeleteDeposit(props.row)">
|
||
<q-item-section avatar>
|
||
<q-icon name="delete" color="red-7"></q-icon>
|
||
</q-item-section>
|
||
<q-item-section>Delete</q-item-section>
|
||
</q-item>
|
||
</q-list>
|
||
</q-btn-dropdown>
|
||
</q-td>
|
||
</q-tr>
|
||
</template>
|
||
</q-table>
|
||
</q-tab-panel>
|
||
<q-tab-panel name="commission">
|
||
<div class="row items-center q-mb-md">
|
||
<div class="col">
|
||
<h6 class="q-my-none">Commission splits</h6>
|
||
<p class="text-caption q-my-none" :style="{opacity: 0.7}">
|
||
After the LNbits platform fee is taken, the remainder is
|
||
distributed across the wallets you configure here. Per-machine
|
||
overrides take precedence over your default rules.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row q-col-gutter-md q-mb-md">
|
||
<div class="col-12 col-md-6">
|
||
<q-select v-model="commissionScope"
|
||
:options="commissionScopeOptions"
|
||
label="Scope being edited"
|
||
emit-value map-options
|
||
dense outlined
|
||
@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
|
||
explicit override.
|
||
</span>
|
||
<span v-else>
|
||
Per-machine override for
|
||
<b v-text="machineNameById(commissionScope)"></b>.
|
||
Empty/cleared rows fall back to the default.
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<q-card flat bordered>
|
||
<q-card-section>
|
||
<div class="row items-center q-mb-sm">
|
||
<div class="col">
|
||
<span :style="{fontWeight: 500}">Legs</span>
|
||
<span class="q-ml-md text-caption">
|
||
Sum:
|
||
<span :style="{
|
||
color: commissionSumValid ? '#2e7d32' : '#c62828',
|
||
fontWeight: 500
|
||
}"
|
||
v-text="(commissionSum * 100).toFixed(2) + '%'"></span>
|
||
<q-icon v-if="commissionSumValid"
|
||
name="check_circle" color="green" size="xs"
|
||
class="q-ml-xs"></q-icon>
|
||
<q-icon v-else
|
||
name="error" color="red" size="xs"
|
||
class="q-ml-xs">
|
||
<q-tooltip>Must sum to 100% before saving</q-tooltip>
|
||
</q-icon>
|
||
</span>
|
||
</div>
|
||
<div class="col-auto">
|
||
<q-btn flat dense color="primary" icon="add"
|
||
label="Add leg"
|
||
@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>
|
||
</template>
|
||
<span v-if="commissionScope === null">
|
||
No default rules. Without a default, all operator
|
||
commission stays in the machine wallet (audit visible).
|
||
</span>
|
||
<span v-else>
|
||
No override for this machine. The default ruleset applies.
|
||
</span>
|
||
</q-banner>
|
||
|
||
<div v-for="(leg, idx) in commissionLegs" :key="idx"
|
||
class="q-mb-md q-pa-sm"
|
||
:style="{border: '1px solid rgba(255,255,255,0.08)', borderRadius: '4px'}">
|
||
<div class="row q-col-gutter-sm items-center q-mb-sm">
|
||
<div class="col">
|
||
<q-btn-toggle v-model="leg.targetKind"
|
||
:options="[
|
||
{label: 'My wallet', value: 'wallet'},
|
||
{label: 'Lightning address / LNURL / invoice key', value: 'external'}
|
||
]"
|
||
no-caps dense flat
|
||
toggle-color="primary"
|
||
color="grey-8"></q-btn-toggle>
|
||
</div>
|
||
<div class="col-auto">
|
||
<q-btn flat dense round icon="delete" color="red-7"
|
||
@click="commissionLegs.splice(idx, 1)">
|
||
<q-tooltip>Remove leg</q-tooltip>
|
||
</q-btn>
|
||
</div>
|
||
</div>
|
||
<div class="row q-col-gutter-sm items-start">
|
||
<div class="col-12 col-md-6">
|
||
<q-select v-if="leg.targetKind === 'wallet'"
|
||
v-model="leg.target"
|
||
:options="walletOptions"
|
||
label="Wallet (one of yours)"
|
||
emit-value map-options dense outlined></q-select>
|
||
<q-input v-else
|
||
v-model.trim="leg.target"
|
||
label="LN address, LNURL, or invoice key"
|
||
hint="user@domain · LNURL1... · or an LP's invoice key"
|
||
dense outlined></q-input>
|
||
</div>
|
||
<div class="col-7 col-md-3">
|
||
<q-input v-model="leg.label"
|
||
label="Label (employee, maintenance, ...)"
|
||
dense outlined></q-input>
|
||
</div>
|
||
<div class="col-5 col-md-3">
|
||
<q-input v-model.number="leg.fraction"
|
||
label="% (0..1)"
|
||
type="number" step="0.01" min="0" max="1"
|
||
dense outlined>
|
||
<template v-slot:append>
|
||
<span :style="{fontSize: '0.75em', opacity: 0.6}"
|
||
v-text="((leg.fraction || 0) * 100).toFixed(1) + '%'"></span>
|
||
</template>
|
||
</q-input>
|
||
</div>
|
||
</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>
|
||
</template>
|
||
Preview against
|
||
<b v-text="formatSats(commissionPreviewInput)"></b>
|
||
sats operator commission →
|
||
<span v-for="(prev, idx) in commissionPreview" :key="idx"
|
||
class="q-mr-md">
|
||
<span :style="{opacity: 0.7}"
|
||
v-text="prev.label || ('leg ' + (idx + 1))"></span>:
|
||
<b v-text="formatSats(prev.sats)"></b>
|
||
</span>
|
||
</q-banner>
|
||
</q-card-section>
|
||
|
||
<q-card-actions align="right">
|
||
<q-btn v-if="commissionScope !== null && commissionLegs.length"
|
||
flat color="red"
|
||
label="Remove override"
|
||
@click="confirmDeleteCommissionOverride"></q-btn>
|
||
<q-btn flat label="Reload"
|
||
:disable="commissionSaving"
|
||
@click="loadCommissionSplits"></q-btn>
|
||
<q-btn color="primary"
|
||
label="Save"
|
||
:disable="!commissionSumValid"
|
||
:loading="commissionSaving"
|
||
@click="saveCommissionSplits"></q-btn>
|
||
</q-card-actions>
|
||
</q-card>
|
||
</q-tab-panel>
|
||
<q-tab-panel name="worklist">
|
||
<div class="row items-center q-mb-md">
|
||
<div class="col">
|
||
<h6 class="q-my-none">Worklist</h6>
|
||
<p class="text-caption q-my-none" :style="{opacity: 0.7}">
|
||
Settlements that didn't process cleanly. Errored ones need
|
||
retry; stuck ones may need force-reset (processor crashed
|
||
mid-flight).
|
||
</p>
|
||
</div>
|
||
<div class="col-auto row items-center q-gutter-sm">
|
||
<q-input v-model.number="worklistThreshold"
|
||
label="Threshold (min)" dense outlined
|
||
:style="{width: '120px'}"
|
||
type="number" min="1"></q-input>
|
||
<q-btn flat dense color="primary" icon="refresh"
|
||
:loading="worklistLoading"
|
||
@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>
|
||
</template>
|
||
All clear — no errored or stuck settlements.
|
||
</q-banner>
|
||
|
||
<div v-for="bucket in worklistBuckets" :key="bucket.key"
|
||
v-show="bucket.rows.length"
|
||
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"></q-icon>
|
||
<span :style="{fontWeight: 500}" v-text="bucket.label"></span>
|
||
<q-chip dense
|
||
:color="bucket.color" text-color="white"
|
||
class="q-ml-sm"
|
||
v-text="bucket.rows.length"></q-chip>
|
||
</div>
|
||
<q-table dense flat
|
||
:rows="bucket.rows"
|
||
row-key="id"
|
||
:columns="worklistTable.columns"
|
||
hide-pagination
|
||
:pagination="{rowsPerPage: 0}">
|
||
<template v-slot:body="props">
|
||
<q-tr :props="props">
|
||
<q-td key="machine">
|
||
<span v-text="machineNameById(props.row.machine_id)"></span>
|
||
</q-td>
|
||
<q-td key="created_at">
|
||
<span :style="{fontSize: '0.85em'}"
|
||
v-text="formatTime(props.row.created_at)"></span>
|
||
</q-td>
|
||
<q-td key="wire_sats" class="text-right">
|
||
<span v-text="formatSats(props.row.wire_sats)"></span>
|
||
</q-td>
|
||
<q-td key="error_message">
|
||
<span :style="{fontSize: '0.85em', opacity: 0.8}"
|
||
v-text="props.row.error_message || '—'"></span>
|
||
</q-td>
|
||
<q-td key="actions" auto-width>
|
||
<q-btn flat dense size="sm" icon="open_in_new"
|
||
color="primary"
|
||
@click="viewMachineFromWorklist(props.row)">
|
||
<q-tooltip>Open machine detail</q-tooltip>
|
||
</q-btn>
|
||
<q-btn v-if="bucket.key === 'errored'"
|
||
flat dense size="sm" icon="restart_alt"
|
||
color="primary"
|
||
@click="confirmRetryFromWorklist(props.row)">
|
||
<q-tooltip>Retry distribution</q-tooltip>
|
||
</q-btn>
|
||
<q-btn v-if="bucket.key === 'stuck_pending' || bucket.key === 'stuck_processing'"
|
||
flat dense size="sm" icon="local_fire_department"
|
||
color="red"
|
||
@click="confirmForceResetFromWorklist(props.row)">
|
||
<q-tooltip>Force-reset (stuck)</q-tooltip>
|
||
</q-btn>
|
||
</q-td>
|
||
</q-tr>
|
||
</template>
|
||
</q-table>
|
||
</div>
|
||
</q-tab-panel>
|
||
|
||
<q-tab-panel name="reports">
|
||
<div class="row items-center q-mb-md">
|
||
<div class="col">
|
||
<h6 class="q-my-none">Reports</h6>
|
||
<p class="text-caption q-my-none" :style="{opacity: 0.7}">
|
||
Client-side CSV exports of the data currently loaded in the
|
||
dashboard. For larger date ranges or server-side filters,
|
||
use the LNbits API directly.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<div class="row q-col-gutter-md">
|
||
<div class="col-12 col-md-6 col-lg-4">
|
||
<q-card flat bordered>
|
||
<q-card-section>
|
||
<div :style="{fontWeight: 500}">Machines</div>
|
||
<div class="text-caption" :style="{opacity: 0.7}">
|
||
<span v-text="machines.length"></span> rows
|
||
</div>
|
||
</q-card-section>
|
||
<q-card-actions>
|
||
<q-btn flat color="primary" icon="download"
|
||
label="machines.csv"
|
||
@click="downloadMachinesCsv"></q-btn>
|
||
</q-card-actions>
|
||
</q-card>
|
||
</div>
|
||
<div class="col-12 col-md-6 col-lg-4">
|
||
<q-card flat bordered>
|
||
<q-card-section>
|
||
<div :style="{fontWeight: 500}">Clients (LPs)</div>
|
||
<div class="text-caption" :style="{opacity: 0.7}">
|
||
<span v-text="clients.length"></span> rows, balances included
|
||
</div>
|
||
</q-card-section>
|
||
<q-card-actions>
|
||
<q-btn flat color="primary" icon="download"
|
||
label="clients.csv"
|
||
@click="downloadClientsCsv"></q-btn>
|
||
</q-card-actions>
|
||
</q-card>
|
||
</div>
|
||
<div class="col-12 col-md-6 col-lg-4">
|
||
<q-card flat bordered>
|
||
<q-card-section>
|
||
<div :style="{fontWeight: 500}">Deposits</div>
|
||
<div class="text-caption" :style="{opacity: 0.7}">
|
||
<span v-text="deposits.length"></span> rows
|
||
</div>
|
||
</q-card-section>
|
||
<q-card-actions>
|
||
<q-btn flat color="primary" icon="download"
|
||
label="deposits.csv"
|
||
@click="downloadDepositsCsv"></q-btn>
|
||
</q-card-actions>
|
||
</q-card>
|
||
</div>
|
||
<div class="col-12 col-md-6 col-lg-4">
|
||
<q-card flat bordered>
|
||
<q-card-section>
|
||
<div :style="{fontWeight: 500}">Payments (legs)</div>
|
||
<div class="text-caption" :style="{opacity: 0.7}">
|
||
Distribution audit (dca / super_fee / operator_split / etc)
|
||
</div>
|
||
</q-card-section>
|
||
<q-card-actions>
|
||
<q-btn flat color="primary" icon="download"
|
||
label="payments.csv"
|
||
:loading="reportsBusy"
|
||
@click="downloadPaymentsCsv"></q-btn>
|
||
</q-card-actions>
|
||
</q-card>
|
||
</div>
|
||
</div>
|
||
</q-tab-panel>
|
||
|
||
</q-tab-panels>
|
||
</q-card>
|
||
|
||
<!-- =============================================================== -->
|
||
<!-- ADD MACHINE DIALOG -->
|
||
<!-- =============================================================== -->
|
||
<q-dialog v-model="addMachineDialog.show" persistent>
|
||
<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-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}">
|
||
Register an ATM by its Nostr public key. Choose the LNbits wallet that
|
||
will receive cash-out payments from this machine — settlements there
|
||
trigger the automatic distribution chain.
|
||
</p>
|
||
|
||
<q-input
|
||
v-model="addMachineDialog.data.name"
|
||
label="Machine name"
|
||
hint="Operator-friendly label (e.g. ATM-Antigua-1)"
|
||
class="q-mb-md"
|
||
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></q-input>
|
||
|
||
<q-input
|
||
v-model="addMachineDialog.data.machine_npub"
|
||
label="Machine npub — DEVELOPMENT ONLY (blank = normal bunker pairing)"
|
||
hint="⚠ Leave blank for normal operation: the bunker mints this machine's key when you pair it (no nsec ever lands on the machine). Only fill this to register a machine that holds its OWN signing key — development / self-signing. Hex or npub1…"
|
||
color="orange"
|
||
class="q-mb-md"
|
||
dense outlined
|
||
:rules="[
|
||
v => !v || v.length >= 32 || 'Looks too short — use a full hex/npub, or leave blank'
|
||
]"></q-input>
|
||
|
||
<q-select
|
||
v-model="addMachineDialog.data.wallet_id"
|
||
:options="walletOptions"
|
||
label="Wallet (receives ATM payments)"
|
||
emit-value map-options
|
||
class="q-mb-md"
|
||
dense outlined
|
||
:rules="[v => !!v || 'Pick a wallet']"></q-select>
|
||
|
||
<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></q-input>
|
||
|
||
<q-input
|
||
v-model.number="addMachineDialog.data.operator_cash_in_fee_fraction"
|
||
label="Operator cash-in fee (decimal fraction, 0-0.15)"
|
||
hint="Your per-machine cut on cash-in. Sits on top of the platform fee; cap is 15% total per direction."
|
||
type="number" step="0.0001" min="0" max="0.15"
|
||
class="q-mb-md"
|
||
dense outlined></q-input>
|
||
|
||
<q-input
|
||
v-model.number="addMachineDialog.data.operator_cash_out_fee_fraction"
|
||
label="Operator cash-out fee (decimal fraction, 0-0.15)"
|
||
hint="Your per-machine cut on cash-out. Sits on top of the platform fee; cap is 15% total per direction."
|
||
type="number" step="0.0001" min="0" max="0.15"
|
||
class="q-mb-md"
|
||
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>
|
||
<q-btn
|
||
color="primary" label="Add machine"
|
||
:loading="addMachineDialog.saving"
|
||
@click="submitAddMachine"></q-btn>
|
||
</q-card-actions>
|
||
</q-card>
|
||
</q-dialog>
|
||
|
||
<!-- =============================================================== -->
|
||
<!-- PAIR SPIRE DIALOG — mint bunker key + one-shot seed URL (S0/#9) -->
|
||
<!-- =============================================================== -->
|
||
<q-dialog v-model="pairDialog.show" persistent>
|
||
<q-card :style="{minWidth: '480px', maxWidth: '95vw'}">
|
||
<q-card-section class="row items-center q-pb-none">
|
||
<div class="text-h6">Pair spire</div>
|
||
<q-space></q-space>
|
||
<q-btn icon="close" flat round dense v-close-popup></q-btn>
|
||
</q-card-section>
|
||
|
||
<!-- Step 1 — configure + generate -->
|
||
<template v-if="!pairDialog.result">
|
||
<q-card-section>
|
||
<p class="text-caption q-mb-md" :style="{opacity: 0.7}">
|
||
Mints a dedicated signing key for
|
||
<b v-text="(pairDialog.machine && pairDialog.machine.name) || 'this spire'"></b>
|
||
inside the operator bunker and issues a one-shot seed URL. The
|
||
spire's key never touches its disk; its cash-outs route to this
|
||
machine's wallet. Re-pairing issues a fresh seed.
|
||
</p>
|
||
|
||
<q-input
|
||
v-model="pairDialog.relays"
|
||
label="Relay(s) for the spire's events"
|
||
hint="One per line. The same relay the spire publishes to (its VITE_RELAY_URL), e.g. wss://your-host/nostrrelay/<id>"
|
||
type="textarea" autogrow
|
||
class="q-mb-md"
|
||
dense outlined></q-input>
|
||
|
||
<q-input
|
||
v-model.number="pairDialog.durationHours"
|
||
label="Token lifetime in hours (optional)"
|
||
hint="Blank = non-expiring. Set e.g. 720 (30 days) to force periodic re-pairing."
|
||
type="number" min="1"
|
||
class="q-mb-md"
|
||
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>
|
||
<q-btn
|
||
color="primary" label="Generate seed URL" icon="vpn_key"
|
||
:loading="pairDialog.saving"
|
||
@click="submitPair"></q-btn>
|
||
</q-card-actions>
|
||
</template>
|
||
|
||
<!-- Step 2 — show the seed URL -->
|
||
<template v-else>
|
||
<q-card-section>
|
||
<q-banner dense rounded class="bg-green-1 text-grey-9 q-mb-md">
|
||
<template v-slot:avatar>
|
||
<q-icon name="check_circle" color="green"></q-icon>
|
||
</template>
|
||
Paired. Scan this on the spire at first boot, or paste the seed URL
|
||
into <code>provision-atm</code>. Shown once — copy it now.
|
||
</q-banner>
|
||
|
||
<div class="row justify-center q-mb-md">
|
||
<lnbits-qrcode
|
||
:value="pairDialog.result.seed_url"
|
||
:options="{width: 280}"></lnbits-qrcode>
|
||
</div>
|
||
|
||
<q-input
|
||
v-model="pairDialog.result.seed_url"
|
||
label="Seed URL"
|
||
type="textarea" autogrow readonly
|
||
class="q-mb-sm"
|
||
dense outlined>
|
||
<template v-slot:append>
|
||
<q-btn flat dense round icon="content_copy"
|
||
@click="copy(pairDialog.result.seed_url)">
|
||
<q-tooltip>Copy seed URL</q-tooltip>
|
||
</q-btn>
|
||
</template>
|
||
</q-input>
|
||
|
||
<div class="text-caption" :style="{opacity: 0.7}">
|
||
Spire identity:
|
||
<code :style="{fontSize: '0.85em'}"
|
||
v-text="shortNpub(pairDialog.result.spire_npub)"></code>
|
||
<q-btn flat dense round size="xs" icon="content_copy"
|
||
@click="copy(pairDialog.result.spire_npub)">
|
||
<q-tooltip>Copy npub</q-tooltip>
|
||
</q-btn>
|
||
</div>
|
||
</q-card-section>
|
||
<q-card-actions align="right" class="text-primary">
|
||
<q-btn flat label="Done" color="primary" v-close-popup></q-btn>
|
||
</q-card-actions>
|
||
</template>
|
||
</q-card>
|
||
</q-dialog>
|
||
|
||
<!-- =============================================================== -->
|
||
<!-- MACHINE DETAIL DIALOG — settlements list + per-row actions -->
|
||
<!-- =============================================================== -->
|
||
<q-dialog v-model="machineDetail.show" maximized
|
||
transition-show="slide-up" transition-hide="slide-down">
|
||
<q-card v-if="machineDetail.machine">
|
||
<q-bar class="bg-primary text-white">
|
||
<div class="text-h6 q-mr-md"
|
||
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-btn flat dense round icon="refresh"
|
||
@click="reloadMachineDetail"
|
||
:loading="machineDetail.loading">
|
||
<q-tooltip>Reload</q-tooltip>
|
||
</q-btn>
|
||
<q-btn flat dense round icon="close" v-close-popup>
|
||
<q-tooltip>Close</q-tooltip>
|
||
</q-btn>
|
||
</q-bar>
|
||
|
||
<q-card-section>
|
||
<div class="row q-col-gutter-md q-mb-md">
|
||
<div class="col-12 col-md-4">
|
||
<div class="text-caption" :style="{opacity: 0.6}">npub</div>
|
||
<code :style="{fontSize: '0.85em', wordBreak: 'break-all'}"
|
||
v-text="machineDetail.machine.machine_npub"></code>
|
||
</div>
|
||
<div class="col-12 col-md-3">
|
||
<div class="text-caption" :style="{opacity: 0.6}">Wallet</div>
|
||
<code :style="{fontSize: '0.85em'}"
|
||
v-text="machineDetail.machine.wallet_id"></code>
|
||
</div>
|
||
<div class="col-6 col-md-2">
|
||
<div class="text-caption" :style="{opacity: 0.6}">Location</div>
|
||
<span v-text="machineDetail.machine.location || '—'"></span>
|
||
</div>
|
||
</div>
|
||
|
||
<q-separator class="q-mb-md"></q-separator>
|
||
|
||
<q-tabs v-model="machineDetail.activeTab" dense
|
||
align="left" class="text-grey-7"
|
||
active-color="primary" indicator-color="primary"
|
||
narrow-indicator>
|
||
<q-tab name="settlements" icon="receipt_long"
|
||
label="Settlements"></q-tab>
|
||
<q-tab name="cassettes" icon="precision_manufacturing"
|
||
label="Cassettes"></q-tab>
|
||
</q-tabs>
|
||
<q-separator></q-separator>
|
||
|
||
<q-tab-panels v-model="machineDetail.activeTab" animated>
|
||
<q-tab-panel name="settlements" class="q-px-none">
|
||
|
||
<div class="row items-center q-mt-md q-mb-sm">
|
||
<div class="col">
|
||
<h6 class="q-my-none">Settlements</h6>
|
||
<p class="text-caption q-my-none" :style="{opacity: 0.7}">
|
||
Every bitSpire transaction lands here. Click a row's menu for
|
||
retry / partial-dispense / notes.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<q-banner v-if="!machineDetail.settlements.length"
|
||
class="bg-grey-2 text-grey-9">
|
||
No settlements yet. They'll appear when bitSpire pays this machine's
|
||
wallet.
|
||
</q-banner>
|
||
|
||
<q-table v-else
|
||
dense flat
|
||
:rows="machineDetail.settlements"
|
||
row-key="id"
|
||
:columns="settlementsTable.columns"
|
||
:rows-per-page-options="[10, 25, 50]"
|
||
:pagination="settlementsTable.pagination">
|
||
<template v-slot:body="props">
|
||
<q-tr :props="props">
|
||
<q-td key="status">
|
||
<q-badge :color="settlementStatusColor(props.row.status)"
|
||
:label="props.row.status"></q-badge>
|
||
</q-td>
|
||
<q-td key="tx_type">
|
||
<q-chip dense square size="sm"
|
||
:color="txTypeChip(props.row.tx_type).color"
|
||
text-color="white"
|
||
:icon="txTypeChip(props.row.tx_type).icon">
|
||
<span v-text="txTypeChip(props.row.tx_type).label"></span>
|
||
<q-tooltip>
|
||
<span v-text="txTypeChip(props.row.tx_type).tooltip"></span>
|
||
</q-tooltip>
|
||
</q-chip>
|
||
</q-td>
|
||
<q-td key="created_at">
|
||
<span :style="{fontSize: '0.85em'}"
|
||
v-text="formatTime(props.row.created_at)"></span>
|
||
</q-td>
|
||
<q-td key="wire_sats" class="text-right">
|
||
<span v-text="formatSats(props.row.wire_sats)"></span>
|
||
</q-td>
|
||
<q-td key="principal_sats" class="text-right">
|
||
<span v-text="formatSats(props.row.principal_sats)"></span>
|
||
</q-td>
|
||
<q-td key="fee_sats" class="text-right">
|
||
<span v-text="formatSats(props.row.fee_sats)"></span>
|
||
<div :style="{fontSize: '0.75em', opacity: 0.6}">
|
||
super
|
||
<span v-text="formatSats(props.row.platform_fee_sats)"></span>
|
||
/ op
|
||
<span v-text="formatSats(props.row.operator_fee_sats)"></span>
|
||
</div>
|
||
</q-td>
|
||
<q-td key="fiat_amount" class="text-right">
|
||
<span v-text="formatFiat(props.row.fiat_amount, props.row.fiat_code)"></span>
|
||
</q-td>
|
||
<q-td key="payment_hash">
|
||
<code :style="{fontSize: '0.8em'}"
|
||
v-text="shortId(props.row.payment_hash)"></code>
|
||
</q-td>
|
||
<q-td key="actions" auto-width>
|
||
<q-btn-dropdown flat dense size="sm" label="" icon="more_vert">
|
||
<q-list dense>
|
||
<q-item clickable v-close-popup
|
||
@click="openSettlementNote(props.row)">
|
||
<q-item-section avatar>
|
||
<q-icon name="edit_note"></q-icon>
|
||
</q-item-section>
|
||
<q-item-section>Add note</q-item-section>
|
||
</q-item>
|
||
<q-item v-if="props.row.status === 'errored'"
|
||
clickable v-close-popup
|
||
@click="confirmRetrySettlement(props.row)">
|
||
<q-item-section avatar>
|
||
<q-icon name="restart_alt" color="primary"></q-icon>
|
||
</q-item-section>
|
||
<q-item-section>Retry distribution</q-item-section>
|
||
</q-item>
|
||
<q-item
|
||
v-if="['pending','errored'].includes(props.row.status)"
|
||
clickable v-close-popup
|
||
@click="openPartialDispense(props.row)">
|
||
<q-item-section avatar>
|
||
<q-icon name="precision_manufacturing" color="warning"></q-icon>
|
||
</q-item-section>
|
||
<q-item-section>Partial dispense…</q-item-section>
|
||
</q-item>
|
||
<q-item
|
||
v-if="['pending','processing'].includes(props.row.status)"
|
||
clickable v-close-popup
|
||
@click="confirmForceReset(props.row)">
|
||
<q-item-section avatar>
|
||
<q-icon name="local_fire_department" color="red"></q-icon>
|
||
</q-item-section>
|
||
<q-item-section>Force-reset (stuck)…</q-item-section>
|
||
</q-item>
|
||
</q-list>
|
||
</q-btn-dropdown>
|
||
</q-td>
|
||
</q-tr>
|
||
<q-tr v-if="props.row.notes" :props="props" no-hover>
|
||
<q-td colspan="100%" class="bg-grey-2">
|
||
<pre :style="{whiteSpace: 'pre-wrap', fontSize: '0.8em', margin: 0}"
|
||
v-text="props.row.notes"></pre>
|
||
</q-td>
|
||
</q-tr>
|
||
</template>
|
||
</q-table>
|
||
|
||
</q-tab-panel>
|
||
<q-tab-panel name="cassettes" class="q-px-none">
|
||
|
||
<div class="row items-center q-mt-md q-mb-sm">
|
||
<div class="col">
|
||
<h6 class="q-my-none">Cassettes</h6>
|
||
<p class="text-caption q-my-none" :style="{opacity: 0.7}">
|
||
Per-cassette count and physical bay position. Denomination
|
||
set is hardware-determined (re-provision via atm-tui to
|
||
change). "Publish to ATM" encrypts + signs + sends the new
|
||
config to the machine via Nostr.
|
||
</p>
|
||
</div>
|
||
<div class="col-auto">
|
||
<q-btn flat dense icon="undo" label="Revert"
|
||
:disable="!machineDetail.cassettesDirty"
|
||
@click="revertCassetteEdits">
|
||
<q-tooltip>Discard unsaved edits</q-tooltip>
|
||
</q-btn>
|
||
<q-btn color="primary" icon="cloud_upload"
|
||
label="Publish to ATM"
|
||
:disable="!machineDetail.cassettesDirty"
|
||
:loading="machineDetail.cassettesPublishing"
|
||
@click="openCassettePublishConfirm"></q-btn>
|
||
</div>
|
||
</div>
|
||
|
||
<q-banner v-if="machineDetail.cassettesError"
|
||
class="bg-red-1 text-grey-9 q-mb-md">
|
||
<template v-slot:avatar>
|
||
<q-icon name="warning" color="negative"></q-icon>
|
||
</template>
|
||
<span v-text="machineDetail.cassettesError"></span>
|
||
</q-banner>
|
||
|
||
<q-banner v-if="!machineDetail.cassetteEdits.length
|
||
&& !machineDetail.cassettesLoading"
|
||
class="bg-blue-1 text-grey-9">
|
||
<template v-slot:avatar>
|
||
<q-icon name="hourglass_empty" color="blue"></q-icon>
|
||
</template>
|
||
Waiting for the ATM's bootstrap state event. Power on the ATM
|
||
and confirm it has reached the configured relay; cassette
|
||
rows will auto-populate on receipt.
|
||
</q-banner>
|
||
|
||
<q-table v-if="machineDetail.cassetteEdits.length"
|
||
dense flat
|
||
:rows="machineDetail.cassetteEdits"
|
||
row-key="position"
|
||
:columns="cassettesTable.columns"
|
||
:pagination="cassettesTable.pagination"
|
||
hide-pagination>
|
||
<template v-slot:body="props">
|
||
<q-tr :props="props"
|
||
:style="props.row._dirty
|
||
? {boxShadow: 'inset 4px 0 0 0 #fdd835'}
|
||
: {}">
|
||
<q-td key="position" class="text-right">
|
||
<b v-text="'Bay ' + props.row.position"></b>
|
||
</q-td>
|
||
<q-td key="denomination" class="text-right">
|
||
<q-input v-model.number="props.row.denomination"
|
||
type="number" min="1" step="1" dense outlined
|
||
:suffix="machineDetail.machine.fiat_code || ''"
|
||
:style="{width: '140px', display: 'inline-block'}"
|
||
@update:model-value="markCassetteDirty(props.row)"></q-input>
|
||
</q-td>
|
||
<q-td key="count" class="text-right">
|
||
<q-input v-model.number="props.row.count" type="number"
|
||
min="0" step="1" dense outlined
|
||
:style="{width: '120px', display: 'inline-block'}"
|
||
@update:model-value="markCassetteDirty(props.row)"></q-input>
|
||
</q-td>
|
||
<q-td key="state" class="text-right">
|
||
<span v-if="props.row.state_denomination !== null"
|
||
:style="{fontSize: '0.85em', opacity: 0.7}">
|
||
<span v-text="props.row.state_denomination"></span>
|
||
<span :style="{opacity: 0.6}"
|
||
v-text="' ' + (machineDetail.machine.fiat_code || '')"></span>
|
||
<span :style="{opacity: 0.6}"> · </span>
|
||
<span v-text="'×' + props.row.state_count"></span>
|
||
</span>
|
||
<span v-else :style="{opacity: 0.4}">—</span>
|
||
</q-td>
|
||
<q-td key="updated_at">
|
||
<span :style="{fontSize: '0.85em', opacity: 0.7}"
|
||
v-text="formatTime(props.row.updated_at)"></span>
|
||
</q-td>
|
||
</q-tr>
|
||
</template>
|
||
</q-table>
|
||
|
||
</q-tab-panel>
|
||
</q-tab-panels>
|
||
</q-card-section>
|
||
</q-card>
|
||
</q-dialog>
|
||
|
||
<!-- =============================================================== -->
|
||
<!-- CASSETTE PUBLISH CONFIRM DIALOG -->
|
||
<!-- =============================================================== -->
|
||
<q-dialog v-model="cassettePublishConfirm.show" persistent>
|
||
<q-card :style="{minWidth: '480px', maxWidth: '95vw'}">
|
||
<q-card-section class="row items-center q-pb-none">
|
||
<div class="text-h6">Publish cassette config to ATM</div>
|
||
<q-space ></q-space>
|
||
<q-btn icon="close" flat round dense v-close-popup></q-btn>
|
||
</q-card-section>
|
||
<q-card-section>
|
||
<q-banner class="bg-orange-1 text-grey-9 q-mb-md">
|
||
<template v-slot:avatar>
|
||
<q-icon name="warning" color="warning"></q-icon>
|
||
</template>
|
||
<b>This publish will overwrite the ATM's currently-tracked
|
||
counts.</b> If the ATM has dispensed cash since your last
|
||
refill or count baseline, those decrements will be lost.
|
||
Publish only after a physical refill (a known total), not to
|
||
"tweak" counts mid-day. v2 reconciliation will replace this
|
||
modal with reconciled state display.
|
||
</q-banner>
|
||
<p class="q-mb-sm">Sending to ATM:</p>
|
||
<q-list dense bordered>
|
||
<q-item v-for="row in machineDetail.cassetteEdits"
|
||
:key="row.position">
|
||
<q-item-section>
|
||
<q-item-label>
|
||
<b v-text="'Bay ' + row.position"></b>
|
||
</q-item-label>
|
||
</q-item-section>
|
||
<q-item-section side>
|
||
<q-item-label caption>
|
||
<b v-text="row.denomination + ' ' +
|
||
(machineDetail.machine.fiat_code || '')"></b>
|
||
· count
|
||
<b v-text="row.count"></b>
|
||
</q-item-label>
|
||
</q-item-section>
|
||
</q-item>
|
||
</q-list>
|
||
</q-card-section>
|
||
<q-card-actions align="right">
|
||
<q-btn flat label="Cancel" v-close-popup></q-btn>
|
||
<q-btn color="primary"
|
||
label="Publish to ATM"
|
||
:loading="machineDetail.cassettesPublishing"
|
||
@click="submitCassettePublish"></q-btn>
|
||
</q-card-actions>
|
||
</q-card>
|
||
</q-dialog>
|
||
|
||
<!-- =============================================================== -->
|
||
<!-- PARTIAL-DISPENSE DIALOG -->
|
||
<!-- =============================================================== -->
|
||
<q-dialog v-model="partialDispenseDialog.show" persistent>
|
||
<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-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>
|
||
</template>
|
||
Original gross:
|
||
<b v-text="formatSats(partialDispenseDialog.settlement.wire_sats)"></b>.
|
||
Provide what was actually dispensed. Sat amounts will scale linearly,
|
||
the commission split will recompute, and distribution will re-run.
|
||
</q-banner>
|
||
<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>
|
||
<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">
|
||
<q-input v-model.number="partialDispenseDialog.dispensed_fraction"
|
||
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></q-input>
|
||
</q-tab-panel>
|
||
<q-tab-panel name="sats" class="q-pa-sm">
|
||
<q-input v-model.number="partialDispenseDialog.dispensed_sats"
|
||
label="Dispensed sats"
|
||
hint="Exact sat amount actually dispensed (≤ original gross)"
|
||
type="number" step="1" min="0"
|
||
:max="partialDispenseDialog.settlement.wire_sats"
|
||
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></q-input>
|
||
</q-card-section>
|
||
<q-card-actions align="right">
|
||
<q-btn flat label="Cancel" v-close-popup></q-btn>
|
||
<q-btn color="warning"
|
||
label="Apply partial dispense"
|
||
:loading="partialDispenseDialog.saving"
|
||
@click="submitPartialDispense"></q-btn>
|
||
</q-card-actions>
|
||
</q-card>
|
||
</q-dialog>
|
||
|
||
<!-- =============================================================== -->
|
||
<!-- ADD-NOTE DIALOG -->
|
||
<!-- =============================================================== -->
|
||
<q-dialog v-model="noteDialog.show" persistent>
|
||
<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-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}">
|
||
Notes are append-only and timestamped. Use for reconciliation context,
|
||
off-LN refund records, dispute narrative, etc.
|
||
</p>
|
||
<q-input v-model="noteDialog.note"
|
||
label="Note"
|
||
type="textarea" autogrow
|
||
:rules="[v => (v && v.trim().length > 0) || 'Note cannot be empty']"
|
||
dense outlined></q-input>
|
||
</q-card-section>
|
||
<q-card-actions align="right">
|
||
<q-btn flat label="Cancel" v-close-popup></q-btn>
|
||
<q-btn color="primary" label="Save note"
|
||
:loading="noteDialog.saving"
|
||
@click="submitNote"></q-btn>
|
||
</q-card-actions>
|
||
</q-card>
|
||
</q-dialog>
|
||
|
||
<!-- =============================================================== -->
|
||
<!-- SUPER-FEE EDIT DIALOG (super-only) -->
|
||
<!-- =============================================================== -->
|
||
<q-dialog v-model="superFeeDialog.show" persistent>
|
||
<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-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}">
|
||
Charged on every transaction's principal across the LNbits
|
||
instance. Independent per direction. Each direction's total
|
||
(super + operator) is capped at 15%. Operators see these as a
|
||
read-only banner. Wallet ID is where the collected fee lands;
|
||
typically a wallet you (the super) own.
|
||
</p>
|
||
<q-input v-model.number="superFeeDialog.data.super_cash_in_fee_fraction"
|
||
label="Cash-in fee (decimal fraction, 0-0.15)"
|
||
hint="0.03 = 3% of principal on cash-in transactions"
|
||
type="number" step="0.0001" min="0" max="0.15"
|
||
class="q-mb-md" dense outlined></q-input>
|
||
<q-input v-model.number="superFeeDialog.data.super_cash_out_fee_fraction"
|
||
label="Cash-out fee (decimal fraction, 0-0.15)"
|
||
hint="0.03 = 3% of principal on cash-out transactions"
|
||
type="number" step="0.0001" min="0" max="0.15"
|
||
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></q-input>
|
||
<q-input v-model.number="superFeeDialog.data.max_cash_in_sats"
|
||
label="Max cash-in per transaction (sats — blank = no cap)"
|
||
hint="Server-side ceiling on a single cash-in's principal. The ATM attests the amount; this bounds a compromised/buggy machine to one capped tx."
|
||
type="number" step="1" min="0"
|
||
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>
|
||
<q-btn color="primary" label="Save"
|
||
:loading="superFeeDialog.saving"
|
||
@click="submitSuperFee"></q-btn>
|
||
</q-card-actions>
|
||
</q-card>
|
||
</q-dialog>
|
||
|
||
<!-- =============================================================== -->
|
||
<!-- ADD / EDIT DEPOSIT DIALOG -->
|
||
<!-- =============================================================== -->
|
||
<q-dialog v-model="depositDialog.show" persistent>
|
||
<q-card :style="{minWidth: '460px', maxWidth: '95vw'}">
|
||
<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-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'"
|
||
v-model="depositDialog.data.client_id"
|
||
:options="depositClientOptions"
|
||
label="Liquidity provider"
|
||
emit-value map-options
|
||
class="q-mb-md" dense outlined
|
||
:rules="[v => !!v || 'Pick an LP']"></q-select>
|
||
|
||
<q-banner v-if="depositDialog.mode === 'add' && selectedDepositClient && !selectedDepositClient.lp_onboarded"
|
||
class="bg-deep-orange-1 text-grey-9 q-mb-md">
|
||
<template v-slot:avatar>
|
||
<q-icon name="pending" color="deep-orange"></q-icon>
|
||
</template>
|
||
This LP hasn't onboarded via <b>satmachineclient</b> yet, so
|
||
their DCA wallet isn't configured. Ask them to open the
|
||
satmachineclient extension once and the deposit will be
|
||
accepted next time.
|
||
</q-banner>
|
||
|
||
<q-input v-model.number="depositDialog.data.amount"
|
||
:label="depositMachineFiatCode ? `Amount (${depositMachineFiatCode})` : 'Amount (fiat)'"
|
||
type="number" step="0.01" min="0"
|
||
class="q-mb-md" dense outlined
|
||
:rules="[v => v > 0 || 'Must be > 0']">
|
||
<template v-slot:append>
|
||
<q-chip v-if="depositMachineFiatCode"
|
||
dense color="grey-3" text-color="grey-9"
|
||
:style="{fontWeight: 500}">
|
||
<span v-text="depositMachineFiatCode"></span>
|
||
<q-tooltip>
|
||
Currency is set by the selected LP's machine
|
||
(<code v-text="depositMachineFiatCode"></code>) — not
|
||
operator-editable. See aiolabs/satmachineadmin#26.
|
||
</q-tooltip>
|
||
</q-chip>
|
||
</template>
|
||
</q-input>
|
||
|
||
<q-input v-model="depositDialog.data.notes"
|
||
label="Notes (optional)"
|
||
type="textarea" autogrow
|
||
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>
|
||
<q-btn color="primary"
|
||
:label="depositDialog.mode === 'add' ? 'Record' : 'Save'"
|
||
:loading="depositDialog.saving"
|
||
:disable="depositDialog.mode === 'add' && (!selectedDepositClient || !selectedDepositClient.lp_onboarded)"
|
||
@click="submitDeposit"></q-btn>
|
||
</q-card-actions>
|
||
</q-card>
|
||
</q-dialog>
|
||
|
||
<!-- =============================================================== -->
|
||
<!-- REJECT DEPOSIT DIALOG (status update with notes) -->
|
||
<!-- =============================================================== -->
|
||
<q-dialog v-model="rejectDepositDialog.show" persistent>
|
||
<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-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}">
|
||
The deposit will be marked rejected and won't count toward the LP's
|
||
balance. Optional reason for the audit trail.
|
||
</p>
|
||
<q-input v-model="rejectDepositDialog.notes"
|
||
label="Reason (optional)"
|
||
type="textarea" autogrow
|
||
dense outlined></q-input>
|
||
</q-card-section>
|
||
<q-card-actions align="right">
|
||
<q-btn flat label="Cancel" v-close-popup></q-btn>
|
||
<q-btn color="red" label="Reject"
|
||
:loading="rejectDepositDialog.saving"
|
||
@click="submitRejectDeposit"></q-btn>
|
||
</q-card-actions>
|
||
</q-card>
|
||
</q-dialog>
|
||
|
||
<!-- =============================================================== -->
|
||
<!-- ADD / EDIT CLIENT DIALOGS -->
|
||
<!-- =============================================================== -->
|
||
<q-dialog v-model="clientDialog.show" persistent>
|
||
<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-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}"
|
||
v-if="clientDialog.mode === 'add'">
|
||
Enrol an LP at one of your machines. Wallet, DCA mode, and
|
||
autoforward are configured by the LP themselves via the
|
||
<b>satmachineclient</b> extension — you can't set them here.
|
||
Deposits are refused until the LP has registered.
|
||
</p>
|
||
|
||
<q-select
|
||
v-if="clientDialog.mode === 'add'"
|
||
v-model="clientDialog.data.machine_id"
|
||
:options="machineOptions"
|
||
label="At machine"
|
||
emit-value map-options
|
||
class="q-mb-md"
|
||
dense outlined
|
||
:rules="[v => !!v || 'Required']"></q-select>
|
||
|
||
<q-input v-if="clientDialog.mode === 'add'"
|
||
v-model="clientDialog.data.user_id"
|
||
label="LP's LNbits user_id"
|
||
hint="The LP shares this from their LNbits account settings"
|
||
class="q-mb-md" dense outlined
|
||
:rules="[v => !!v || 'Required']"></q-input>
|
||
|
||
<q-input v-model="clientDialog.data.username"
|
||
label="Display name (optional)"
|
||
hint="Operator-facing label only; doesn't affect distribution"
|
||
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></q-select>
|
||
</q-card-section>
|
||
<q-card-actions align="right">
|
||
<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"></q-btn>
|
||
</q-card-actions>
|
||
</q-card>
|
||
</q-dialog>
|
||
|
||
<!-- =============================================================== -->
|
||
<!-- SETTLE BALANCE DIALOG (closes spirekeeper#2) -->
|
||
<!-- =============================================================== -->
|
||
<q-dialog v-model="settleBalanceDialog.show" persistent>
|
||
<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-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>
|
||
</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
|
||
otherwise shrink forever via proportional shares.
|
||
</q-banner>
|
||
|
||
<div v-if="settleBalanceDialog.balance" class="q-mb-md">
|
||
<div class="text-caption" :style="{opacity: 0.6}">Remaining balance</div>
|
||
<div :style="{fontSize: '1.2em', fontWeight: 500}"
|
||
v-text="formatFiat(settleBalanceDialog.balance.remaining_balance, settleBalanceDialog.balance.currency)"></div>
|
||
</div>
|
||
|
||
<q-select v-model="settleBalanceDialog.data.funding_wallet_id"
|
||
:options="walletOptions"
|
||
label="Funding wallet (yours)"
|
||
emit-value map-options
|
||
class="q-mb-md" dense outlined
|
||
:rules="[v => !!v || 'Pick a wallet']"></q-select>
|
||
|
||
<q-input v-model.number="settleBalanceDialog.data.exchange_rate"
|
||
label="Exchange rate (sats per 1 fiat unit)"
|
||
hint="You set the rate. Use exchange spot, midpoint, or a favourable gesture."
|
||
type="number" step="0.0001" min="0"
|
||
class="q-mb-md" dense outlined
|
||
:rules="[v => v > 0 || 'Must be > 0']"></q-input>
|
||
|
||
<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></q-input>
|
||
|
||
<q-input v-model="settleBalanceDialog.data.notes"
|
||
label="Notes (optional, audit memo)"
|
||
type="textarea" autogrow
|
||
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>
|
||
<q-btn color="primary" label="Settle balance"
|
||
:loading="settleBalanceDialog.saving"
|
||
@click="submitSettleBalance"></q-btn>
|
||
</q-card-actions>
|
||
</q-card>
|
||
</q-dialog>
|
||
|
||
<!-- =============================================================== -->
|
||
<!-- EDIT MACHINE DIALOG (same fields; PUT instead of POST) -->
|
||
<!-- =============================================================== -->
|
||
<q-dialog v-model="editMachineDialog.show" persistent>
|
||
<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-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></q-input>
|
||
<q-input v-model="editMachineDialog.data.location"
|
||
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></q-select>
|
||
<q-input v-model="editMachineDialog.data.fiat_code"
|
||
label="Fiat code" class="q-mb-md" dense outlined></q-input>
|
||
<q-input v-model.number="editMachineDialog.data.operator_cash_in_fee_fraction"
|
||
label="Operator cash-in fee (decimal fraction, 0-0.15)"
|
||
hint="Sits on top of the platform cash-in fee. Cap 15% total per direction."
|
||
type="number" step="0.0001" min="0" max="0.15"
|
||
class="q-mb-md" dense outlined></q-input>
|
||
<q-input v-model.number="editMachineDialog.data.operator_cash_out_fee_fraction"
|
||
label="Operator cash-out fee (decimal fraction, 0-0.15)"
|
||
hint="Sits on top of the platform cash-out fee. Cap 15% total per direction."
|
||
type="number" step="0.0001" min="0" max="0.15"
|
||
class="q-mb-md" dense outlined></q-input>
|
||
<q-toggle v-model="editMachineDialog.data.is_active"
|
||
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>
|
||
<q-btn
|
||
color="primary" label="Save"
|
||
:loading="editMachineDialog.saving"
|
||
@click="submitEditMachine"></q-btn>
|
||
</q-card-actions>
|
||
</q-card>
|
||
</q-dialog>
|
||
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|