satmachineclient/templates/myextension/index.html
padreug 75dd03b15a Refactor MyExtension to DCA Admin: Update extension name and description in config.json, remove legacy MyExtension CRUD operations and related API endpoints, and adjust router tags. Clean up unused files and methods to streamline the codebase for DCA administration functionality.
Refactor DCA Admin page endpoints: Update description, remove unused CRUD operations and API endpoints related to MyExtension, and streamline the code for improved clarity and functionality.

Remove QR Code dialog from MyExtension index.html: streamline the UI by eliminating unused dialog components, enhancing code clarity and maintainability.
2025-06-20 22:00:41 +02:00

771 lines
29 KiB
HTML

<!--/////////////////////////////////////////////////-->
<!--//PAGE FOR THE DCA ADMIN EXTENSION IN LNBITS//////-->
<!--/////////////////////////////////////////////////-->
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block scripts %} {{ window_vars(user) }}
<script src="{{ static_url_for('myextension/static', path='js/index.js') }}"></script>
{% endblock %} {% block page %}
<div class="row q-col-gutter-md" id="dcaAdmin">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<!-- Deposit Management Section -->
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">DCA Deposit Management</h5>
<p class="text-caption q-my-none">Manage fiat deposits for existing DCA clients</p>
</div>
</div>
</q-card-section>
</q-card>
<!-- DCA Clients Table -->
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h6 class="text-subtitle2 q-my-none">Registered DCA Clients</h6>
<p class="text-caption q-my-none">Clients registered via the DCA client extension</p>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportClientsCSV">Export to CSV</q-btn>
</div>
</div>
<q-table
dense
flat
:rows="dcaClients"
row-key="id"
:columns="clientsTable.columns"
v-model:pagination="clientsTable.pagination"
>
<template v-slot:body="props">
<q-tr :props="props">
<q-td v-for="col in props.cols" :key="col.name" :props="props">
<div v-if="col.field == 'username'">${ col.value || 'No username' }</div>
<div v-else-if="col.field == 'user_id'">${ col.value.substring(0, 8) }...</div>
<div v-else-if="col.field == 'wallet_id'">${ col.value.substring(0, 8) }...</div>
<div v-else-if="col.field == 'status'">
<q-badge :color="col.value === 'active' ? 'green' : 'red'">
${ col.value }
</q-badge>
</div>
<div v-else-if="col.field == 'remaining_balance'">
<span :class="col.value > 0 ? 'text-green-8 text-weight-bold' : 'text-grey-6'">
${ formatCurrency(col.value || 0) }
</span>
</div>
<div v-else-if="col.field == 'fixed_mode_daily_limit' && col.value">
${ formatCurrency(col.value) }
</div>
<div v-else>${ col.value || '-' }</div>
</q-td>
<q-td auto-width>
<q-btn
flat dense size="sm" icon="account_balance_wallet"
color="primary" class="q-mr-sm"
@click="addDepositDialog(props.row)"
>
<q-tooltip>Add Deposit</q-tooltip>
</q-btn>
<q-btn
flat dense size="sm" icon="visibility"
color="blue"
@click="viewClientDetails(props.row)"
>
<q-tooltip>View Balance & Details</q-tooltip>
</q-btn>
</q-td>
</q-tr>
</template>
</q-table>
</q-card-section>
</q-card>
<!-- Quick Add Deposit Section -->
<q-card>
<q-card-section>
<h6 class="text-subtitle2 q-my-none">Quick Add Deposit</h6>
<p class="text-caption q-my-none">Add a new deposit for an existing client</p>
<div v-if="dcaClients.length === 0" class="q-mt-md">
<q-banner class="bg-orange-1 text-orange-9">
<template v-slot:avatar>
<q-icon name="info" color="orange" />
</template>
No DCA clients registered yet. Clients must first install and configure the DCA client extension.
<template v-slot:action>
<q-btn
flat
color="orange"
label="Create Test Client"
@click="createTestClient"
size="sm"
/>
</template>
</q-banner>
</div>
<q-form v-else @submit="sendQuickDeposit" class="q-gutter-md q-mt-md">
<div class="row q-gutter-md">
<div class="col">
<q-select
filled
dense
v-model="quickDepositForm.selectedClient"
:options="clientOptions"
label="Select Client *"
option-label="label"
option-value="value"
></q-select>
</div>
<div class="col">
<q-input
filled
dense
type="number"
v-model.number="quickDepositForm.amount"
label="Amount (GTQ) *"
placeholder="Amount in centavos (GTQ * 100)"
hint="Enter amount in centavos"
></q-input>
</div>
<div class="col-auto">
<q-btn
unelevated
color="primary"
type="submit"
:disable="!quickDepositForm.selectedClient || !quickDepositForm.amount"
>Add Deposit</q-btn
>
</div>
</div>
<div class="row">
<div class="col">
<q-input
filled
dense
type="textarea"
v-model.trim="quickDepositForm.notes"
label="Notes (Optional)"
placeholder="Optional notes about this deposit"
rows="2"
></q-input>
</div>
</div>
</q-form>
</q-card-section>
</q-card>
<!-- Deposits Management Section -->
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h6 class="text-subtitle2 q-my-none">Recent Deposits</h6>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportDepositsCSV">Export to CSV</q-btn>
</div>
</div>
<q-table
dense
flat
:rows="deposits"
row-key="id"
:columns="depositsTable.columns"
v-model:pagination="depositsTable.pagination"
>
<template v-slot:body="props">
<q-tr :props="props">
<q-td v-for="col in props.cols" :key="col.name" :props="props">
<div v-if="col.field == 'client_id'">${ getClientUsername(col.value) }</div>
<div v-else-if="col.field == 'amount'">${ formatCurrency(col.value) }</div>
<div v-else-if="col.field == 'status'">
<q-badge :color="col.value === 'confirmed' ? 'green' : 'orange'">
${ col.value }
</q-badge>
</div>
<div v-else-if="col.field == 'created_at'">${ formatDate(col.value) }</div>
<div v-else>${ col.value }</div>
</q-td>
<q-td auto-width>
<q-btn
v-if="props.row.status === 'pending'"
flat dense size="sm" icon="check_circle"
color="green" class="q-mr-sm"
@click="confirmDeposit(props.row)"
>
<q-tooltip>Confirm Deposit</q-tooltip>
</q-btn>
<q-btn
flat dense size="sm" icon="edit"
color="orange"
@click="editDeposit(props.row)"
>
<q-tooltip>Edit Deposit</q-tooltip>
</q-btn>
</q-td>
</q-tr>
</template>
</q-table>
</q-card-section>
</q-card>
<!-- Lamassu Transactions Section -->
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h6 class="text-subtitle2 q-my-none">Processed Lamassu Transactions</h6>
<p class="text-caption q-my-none">ATM transactions processed through DCA distribution</p>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportLamassuTransactionsCSV">Export to CSV</q-btn>
</div>
</div>
<q-table
dense
flat
:rows="lamassuTransactions"
row-key="id"
:columns="lamassuTransactionsTable.columns"
v-model:pagination="lamassuTransactionsTable.pagination"
>
<template v-slot:body="props">
<q-tr :props="props" class="cursor-pointer" @click="viewTransactionDistributions(props.row)">
<q-td v-for="col in props.cols" :key="col.name" :props="props">
<div v-if="col.field == 'lamassu_transaction_id'">${ col.value }</div>
<div v-else-if="col.field == 'fiat_amount'">${ formatCurrency(col.value) }</div>
<div v-else-if="col.field == 'crypto_amount'">${ formatSats(col.value) }</div>
<div v-else-if="col.field == 'commission_amount_sats'">${ formatSats(col.value) }</div>
<div v-else-if="col.field == 'base_amount_sats'">${ formatSats(col.value) }</div>
<div v-else-if="col.field == 'distributions_total_sats'">${ formatSats(col.value) }</div>
<div v-else-if="col.field == 'commission_percentage'">${ (col.value * 100).toFixed(1) }%</div>
<div v-else-if="col.field == 'effective_commission'">${ (col.value * 100).toFixed(1) }%</div>
<div v-else-if="col.field == 'discount'">${ col.value }%</div>
<div v-else-if="col.field == 'exchange_rate'">${ col.value.toLocaleString() }</div>
<div v-else-if="col.field == 'transaction_time'">${ formatDateTime(col.value) }</div>
<div v-else-if="col.field == 'processed_at'">${ formatDateTime(col.value) }</div>
<div v-else>${ col.value || '-' }</div>
</q-td>
<q-td auto-width>
<q-btn
flat dense size="sm" icon="visibility"
color="primary"
@click.stop="viewTransactionDistributions(props.row)"
>
<q-tooltip>View Distribution Details</q-tooltip>
</q-btn>
</q-td>
</q-tr>
</template>
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">
{{SITE_TITLE}} DCA Admin Extension
</h6>
<p>
Dollar Cost Averaging administration for Lamassu ATM integration. <br />
Manage client deposits and DCA distribution settings.
</p>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list>
<q-expansion-item
group="api"
icon="info"
label="DCA System Status"
:content-inset-level="0.5"
>
<q-card-section class="text-caption">
<div class="row">
<div class="col-6">Active Clients:</div>
<div class="col-6">${ dcaClients.filter(c => c.status === 'active').length }</div>
</div>
<div class="row">
<div class="col-6">Pending Deposits:</div>
<div class="col-6">${ deposits.filter(d => d.status === 'pending').length }</div>
</div>
<div class="row">
<div class="col-6">Total DCA Balance:</div>
<div class="col-6">${ formatCurrency(totalDcaBalance) }</div>
</div>
</q-card-section>
</q-expansion-item>
<q-separator></q-separator>
<q-expansion-item
group="api"
icon="settings"
label="Lamassu Database Config"
:content-inset-level="0.5"
>
<q-card-section class="text-caption">
<div v-if="lamassuConfig">
<p><strong>Database:</strong> ${ lamassuConfig.host }:${ lamassuConfig.port }/${ lamassuConfig.database_name }</p>
<p><strong>Status:</strong>
<q-badge v-if="lamassuConfig.test_connection_success === true" color="green">Connected</q-badge>
<q-badge v-else-if="lamassuConfig.test_connection_success === false" color="red">Failed</q-badge>
<q-badge v-else color="grey">Not tested</q-badge>
</p>
<p><strong>Last Poll:</strong> ${ lamassuConfig.last_poll_time ? formatDateTime(lamassuConfig.last_poll_time) : 'Not yet run' }</p>
<p><strong>Last Success:</strong> ${ lamassuConfig.last_successful_poll ? formatDateTime(lamassuConfig.last_successful_poll) : 'Never' }</p>
</div>
<div v-else>
<p><strong>Status:</strong> <q-badge color="orange">Not configured</q-badge></p>
</div>
<div class="q-mt-md">
<q-btn
size="sm"
color="primary"
@click="configDialog.show = true"
icon="settings"
>
Configure Database
</q-btn>
<q-btn
v-if="lamassuConfig"
size="sm"
color="accent"
@click="testDatabaseConnection"
:loading="testingConnection"
class="q-ml-sm"
>
Test Connection
</q-btn>
<q-btn
v-if="lamassuConfig"
size="sm"
color="secondary"
@click="manualPoll"
:loading="runningManualPoll"
class="q-ml-sm"
>
Manual Poll
</q-btn>
<q-btn
v-if="lamassuConfig && lamassuConfig.source_wallet_id"
size="sm"
color="warning"
@click="testTransaction"
:loading="runningTestTransaction"
class="q-ml-sm"
>
Test Transaction
</q-btn>
</div>
</q-card-section>
</q-expansion-item>
<q-separator></q-separator>
{% include "myextension/_api_docs.html" %}
</q-list>
</q-card-section>
</q-card>
</div>
<!--/////////////////////////////////////////////////-->
<!--//////////////DEPOSIT FORM DIALOG////////////////-->
<!--/////////////////////////////////////////////////-->
<q-dialog v-model="depositFormDialog.show" position="top" @hide="closeDepositFormDialog">
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
<q-form @submit="sendDepositData" class="q-gutter-md">
<div v-if="depositFormDialog.data.client_name" class="text-h6 q-mb-md">
Deposit for: ${ depositFormDialog.data.client_name }
</div>
<q-input
filled
dense
type="number"
v-model.number="depositFormDialog.data.amount"
label="Deposit Amount (GTQ) *"
placeholder="Amount in centavos (GTQ * 100)"
hint="Enter amount in centavos (1 GTQ = 100 centavos)"
></q-input>
<q-select
filled
dense
emit-value
v-model="depositFormDialog.data.currency"
:options="currencyOptions"
label="Currency"
></q-select>
<q-input
filled
dense
type="textarea"
v-model.trim="depositFormDialog.data.notes"
label="Notes"
placeholder="Optional notes about this deposit"
rows="3"
></q-input>
<div class="row q-mt-lg">
<q-btn
v-if="depositFormDialog.data.id"
unelevated
color="primary"
type="submit"
>Update Deposit</q-btn
>
<q-btn
v-else
unelevated
color="primary"
:disable="!depositFormDialog.data.amount"
type="submit"
>Create Deposit</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</q-card>
</q-dialog>
<!--/////////////////////////////////////////////////-->
<!--//////////////CLIENT DETAILS DIALOG//////////////-->
<!--/////////////////////////////////////////////////-->
<q-dialog v-model="clientDetailsDialog.show" position="top">
<q-card class="q-pa-lg" style="width: 600px; max-width: 90vw">
<div class="text-h6 q-mb-md">Client Details</div>
<div v-if="clientDetailsDialog.data">
<q-list>
<q-item v-if="clientDetailsDialog.data.username">
<q-item-section>
<q-item-label caption>Username</q-item-label>
<q-item-label>${ clientDetailsDialog.data.username }</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>User ID</q-item-label>
<q-item-label>${ clientDetailsDialog.data.user_id }</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>Wallet ID</q-item-label>
<q-item-label>${ clientDetailsDialog.data.wallet_id }</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>DCA Mode</q-item-label>
<q-item-label>${ clientDetailsDialog.data.dca_mode }</q-item-label>
</q-item-section>
</q-item>
<q-item v-if="clientDetailsDialog.data.fixed_mode_daily_limit">
<q-item-section>
<q-item-label caption>Daily Limit</q-item-label>
<q-item-label>${ formatCurrency(clientDetailsDialog.data.fixed_mode_daily_limit) }</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>Balance Summary</q-item-label>
<q-item-label v-if="clientDetailsDialog.balance">
Deposits: ${ formatCurrency(clientDetailsDialog.balance.total_deposits) } |
Payments: ${ formatCurrency(clientDetailsDialog.balance.total_payments) } |
Remaining: ${ formatCurrency(clientDetailsDialog.balance.remaining_balance) }
</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
<div class="row q-mt-lg">
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
</q-card>
</q-dialog>
<!--/////////////////////////////////////////////////-->
<!--//////////////LAMASSU CONFIG DIALOG//////////////-->
<!--/////////////////////////////////////////////////-->
<q-dialog v-model="configDialog.show" position="top" @hide="closeConfigDialog">
<q-card class="q-pa-lg q-pt-xl" style="width: 600px; max-width: 90vw">
<div class="text-h6 q-mb-md">Lamassu Database Configuration</div>
<q-form @submit="saveConfiguration" class="q-gutter-md">
<q-input
filled
dense
v-model.trim="configDialog.data.host"
label="Database Host *"
placeholder="e.g., localhost or 192.168.1.100"
hint="Hostname or IP address of the Lamassu Postgres server"
></q-input>
<q-input
filled
dense
type="number"
v-model.number="configDialog.data.port"
label="Database Port *"
placeholder="5432"
hint="Postgres port (usually 5432)"
></q-input>
<q-input
filled
dense
v-model.trim="configDialog.data.database_name"
label="Database Name *"
placeholder="lamassu"
hint="Name of the Lamassu database"
></q-input>
<q-input
filled
dense
v-model.trim="configDialog.data.username"
label="Username *"
placeholder="postgres"
hint="Database username with read access"
></q-input>
<q-input
filled
dense
type="password"
v-model.trim="configDialog.data.password"
label="Password *"
placeholder="Enter database password"
hint="Database password"
></q-input>
<q-separator class="q-my-md"></q-separator>
<div class="text-h6 q-mb-md">DCA Source Wallet</div>
<q-select
filled
dense
:options="g.user.wallets"
v-model="configDialog.data.selectedWallet"
label="Source Wallet for DCA Distributions *"
option-label="name"
hint="Wallet that holds Bitcoin for distribution to DCA clients"
></q-select>
<q-select
filled
dense
:options="g.user.wallets"
v-model="configDialog.data.selectedCommissionWallet"
label="Commission Wallet (Optional)"
option-label="name"
hint="Wallet where commission earnings will be sent (leave empty to keep in source wallet)"
></q-select>
<q-separator class="q-my-md"></q-separator>
<div class="text-h6 q-mb-md">SSH Tunnel (Recommended)</div>
<div class="row items-center q-mb-md">
<q-toggle
v-model="configDialog.data.use_ssh_tunnel"
color="primary"
@click.stop
/>
<span class="q-ml-sm">Use SSH Tunnel</span>
</div>
<div v-if="configDialog.data.use_ssh_tunnel" class="q-mt-md" @click.stop>
<q-input
filled
dense
v-model.trim="configDialog.data.ssh_host"
label="SSH Host *"
placeholder="e.g., your-server.com or 192.168.1.100"
hint="SSH server hostname or IP address"
@click.stop
></q-input>
<q-input
filled
dense
type="number"
v-model.number="configDialog.data.ssh_port"
label="SSH Port *"
placeholder="22"
hint="SSH port (usually 22)"
@click.stop
></q-input>
<q-input
filled
dense
v-model.trim="configDialog.data.ssh_username"
label="SSH Username *"
placeholder="ubuntu"
hint="SSH username"
@click.stop
></q-input>
<q-input
filled
dense
type="password"
v-model.trim="configDialog.data.ssh_password"
label="SSH Password"
placeholder="SSH password (if not using key)"
hint="SSH password or leave empty to use private key"
@click.stop
></q-input>
<q-input
filled
dense
type="textarea"
v-model.trim="configDialog.data.ssh_private_key"
label="SSH Private Key"
placeholder="-----BEGIN OPENSSH PRIVATE KEY-----"
hint="SSH private key content (alternative to password)"
rows="4"
@click.stop
></q-input>
<q-banner class="bg-green-1 text-green-9 q-mt-md">
<template v-slot:avatar>
<q-icon name="security" color="green" />
</template>
SSH tunneling keeps your database secure by avoiding direct internet exposure.
The database connection will be routed through the SSH server.
</q-banner>
</div>
<q-banner v-if="!configDialog.data.id" class="bg-blue-1 text-blue-9">
<template v-slot:avatar>
<q-icon name="info" color="blue" />
</template>
This configuration will be securely stored and used for hourly polling.
Only read access to the Lamassu database is required.
</q-banner>
<div class="row q-mt-lg">
<q-btn
unelevated
color="primary"
type="submit"
:disable="!isConfigFormValid"
@click.stop
>Save Configuration</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto" @click.stop
>Cancel</q-btn
>
</div>
</q-form>
</q-card>
</q-dialog>
<!--/////////////////////////////////////////////////-->
<!--//////////////TRANSACTION DISTRIBUTIONS DIALOG////-->
<!--/////////////////////////////////////////////////-->
<q-dialog v-model="distributionDialog.show" position="top" maximized>
<q-card class="q-pa-lg">
<div class="text-h6 q-mb-md">Transaction Distribution Details</div>
<div v-if="distributionDialog.transaction" class="q-mb-lg">
<q-list>
<q-item>
<q-item-section>
<q-item-label caption>Lamassu Transaction ID</q-item-label>
<q-item-label>${ distributionDialog.transaction.lamassu_transaction_id }</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>Transaction Time</q-item-label>
<q-item-label>${ formatDateTime(distributionDialog.transaction.transaction_time) }</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>Total Amount</q-item-label>
<q-item-label>
${ formatCurrency(distributionDialog.transaction.fiat_amount) }
(${ formatSats(distributionDialog.transaction.crypto_amount) })
</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>Commission</q-item-label>
<q-item-label>
${ (distributionDialog.transaction.commission_percentage * 100).toFixed(1) }%
<span v-if="distributionDialog.transaction.discount > 0">
(with ${ distributionDialog.transaction.discount }% discount = ${ (distributionDialog.transaction.effective_commission * 100).toFixed(1) }% effective)
</span>
= ${ formatSats(distributionDialog.transaction.commission_amount_sats) }
</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>Available for Distribution</q-item-label>
<q-item-label>${ formatSats(distributionDialog.transaction.base_amount_sats) }</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>Total Distributed</q-item-label>
<q-item-label>${ formatSats(distributionDialog.transaction.distributions_total_sats) } to ${ distributionDialog.transaction.clients_count } clients</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
<q-separator class="q-my-md"></q-separator>
<div class="text-h6 q-mb-md">Client Distributions</div>
<q-table
dense
flat
:rows="distributionDialog.distributions"
row-key="payment_id"
:columns="distributionDetailsTable.columns"
>
<template v-slot:body="props">
<q-tr :props="props">
<q-td v-for="col in props.cols" :key="col.name" :props="props">
<div v-if="col.field == 'client_username'">${ col.value || 'No username' }</div>
<div v-else-if="col.field == 'client_user_id'">${ col.value.substring(0, 8) }...</div>
<div v-else-if="col.field == 'amount_sats'">${ formatSats(col.value) }</div>
<div v-else-if="col.field == 'amount_fiat'">${ formatCurrency(col.value) }</div>
<div v-else-if="col.field == 'exchange_rate'">${ col.value.toLocaleString() }</div>
<div v-else-if="col.field == 'status'">
<q-badge :color="col.value === 'confirmed' ? 'green' : col.value === 'failed' ? 'red' : 'orange'">
${ col.value }
</q-badge>
</div>
<div v-else-if="col.field == 'created_at'">${ formatDateTime(col.value) }</div>
<div v-else>${ col.value || '-' }</div>
</q-td>
</q-tr>
</template>
</q-table>
<div class="row q-mt-lg">
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
</q-card>
</q-dialog>
</div>
{% endblock %}