diff --git a/static/js/index.js b/static/js/index.js
index 50b0fce..f1b20a8 100644
--- a/static/js/index.js
+++ b/static/js/index.js
@@ -1,773 +1,286 @@
+// Satoshi Machine v2 — operator dashboard (P9a foundation).
+//
+// Vue 3 + Quasar UMD app. Talks to the v2 satmachineadmin REST surface
+// (machines / clients / deposits / settlements / commission-splits /
+// super-config). All endpoints are operator-scoped via the LNbits session.
+//
+// LNbits UMD/Quasar conventions in play:
+// - Vue delimiters are `${ ... }` because Jinja owns `{{ }}` in the
+// template file. Use v-text / :attr binding rather than mustache.
+// - For per-element typography overrides, prefer :style — Quasar's
+// .text-grey-* / .text-caption utilities collide with LNbits' theme.
+// - For pale backgrounds (bg-*-1), pair with explicit dark text class
+// so dark-mode users don't get unreadable white-on-cream.
+
+const SUPER_FEE_PATH = '/satmachineadmin/api/v1/dca/super-config'
+const MACHINES_PATH = '/satmachineadmin/api/v1/dca/machines'
+const STUCK_PATH = '/satmachineadmin/api/v1/dca/settlements/stuck'
+
window.app = Vue.createApp({
el: '#vue',
mixins: [windowMixin],
delimiters: ['${', '}'],
- data: function () {
+
+ data() {
return {
- // DCA Admin Data
- dcaClients: [],
- deposits: [],
- lamassuTransactions: [],
+ activeTab: 'fleet',
+ refreshing: false,
- // Table configurations
- clientsTable: {
+ // Server state ---------------------------------------------------
+ superConfig: null,
+ machines: [],
+ worklistCount: 0,
+
+ // UI configuration -----------------------------------------------
+ machinesTable: {
columns: [
- { name: 'username', align: 'left', label: 'Username', field: 'username' },
- { name: 'user_id', align: 'left', label: 'User ID', field: 'user_id' },
- { name: 'wallet_id', align: 'left', label: 'Wallet ID', field: 'wallet_id' },
- { name: 'dca_mode', align: 'left', label: 'DCA Mode', field: 'dca_mode' },
- { name: 'remaining_balance', align: 'right', label: 'Remaining Balance', field: 'remaining_balance' },
- { name: 'fixed_mode_daily_limit', align: 'left', label: 'Daily Limit', field: 'fixed_mode_daily_limit' },
- { name: 'status', align: 'left', label: 'Status', field: 'status' }
+ {name: 'status', label: '', field: 'is_active', align: 'center'},
+ {name: 'name', label: 'Name / Location', field: 'name', align: 'left'},
+ {name: 'machine_npub', label: 'npub', field: 'machine_npub', align: 'left'},
+ {name: 'wallet_id', label: 'Wallet', field: 'wallet_id', align: 'left'},
+ {name: 'fiat_code', label: 'Fiat', field: 'fiat_code', align: 'left'},
+ {
+ name: 'fallback_commission_pct',
+ label: 'Fallback %',
+ field: 'fallback_commission_pct',
+ align: 'right'
+ },
+ {name: 'actions', label: 'Actions', field: 'id', align: 'right'}
],
- pagination: {
- rowsPerPage: 10
- }
- },
- depositsTable: {
- columns: [
- { name: 'client_id', align: 'left', label: 'Client', field: 'client_id' },
- { name: 'amount', align: 'left', label: 'Amount', field: 'amount' },
- { name: 'currency', align: 'left', label: 'Currency', field: 'currency' },
- { name: 'status', align: 'left', label: 'Status', field: 'status' },
- { name: 'created_at', align: 'left', label: 'Created', field: 'created_at' },
- { name: 'notes', align: 'left', label: 'Notes', field: 'notes' }
- ],
- pagination: {
- rowsPerPage: 10
- }
- },
- lamassuTransactionsTable: {
- columns: [
- { name: 'lamassu_transaction_id', align: 'left', label: 'Transaction ID', field: 'lamassu_transaction_id' },
- { name: 'transaction_time', align: 'left', label: 'Time', field: 'transaction_time' },
- { name: 'fiat_amount', align: 'right', label: 'Fiat Amount', field: 'fiat_amount' },
- { name: 'crypto_amount', align: 'right', label: 'Total Sats', field: 'crypto_amount' },
- { name: 'commission_amount_sats', align: 'right', label: 'Commission', field: 'commission_amount_sats' },
- { name: 'base_amount_sats', align: 'right', label: 'Base Amount', field: 'base_amount_sats' },
- { name: 'distributions_total_sats', align: 'right', label: 'Distributed', field: 'distributions_total_sats' },
- { name: 'clients_count', align: 'center', label: 'Clients', field: 'clients_count' }
- ],
- pagination: {
- rowsPerPage: 10
- }
- },
- distributionDetailsTable: {
- columns: [
- { name: 'client_username', align: 'left', label: 'Client', field: 'client_username' },
- { name: 'amount_sats', align: 'right', label: 'Amount (sats)', field: 'amount_sats' },
- { name: 'amount_fiat', align: 'right', label: 'Amount (fiat)', field: 'amount_fiat' },
- { name: 'status', align: 'center', label: 'Status', field: 'status' },
- { name: 'created_at', align: 'left', label: 'Created', field: 'created_at' }
- ]
+ pagination: {rowsPerPage: 10, sortBy: 'name'}
},
- // Dialog states
- depositFormDialog: {
+ // Dialog state ---------------------------------------------------
+ addMachineDialog: {
show: false,
- data: {
- currency: 'GTQ'
- }
+ saving: false,
+ data: this._emptyMachineForm()
},
- clientDetailsDialog: {
+ editMachineDialog: {
show: false,
- data: null,
- balance: null
- },
- distributionDialog: {
- show: false,
- transaction: null,
- distributions: []
- },
-
- // Quick deposit form
- quickDepositForm: {
- selectedClient: null,
- amount: null,
- notes: ''
- },
-
- // Polling status
- lastPollTime: null,
- testingConnection: false,
- runningManualPoll: false,
- runningTestTransaction: false,
- processingSpecificTransaction: false,
- lamassuConfig: null,
-
- // Manual transaction processing
- manualTransactionDialog: {
- show: false,
- transactionId: ''
- },
-
- // Config dialog
- configDialog: {
- show: false,
- data: {
- host: '',
- port: 5432,
- database_name: '',
- username: '',
- password: '',
- selectedWallet: null,
- selectedCommissionWallet: null,
- // DCA Client Limits
- max_daily_limit_gtq: 2000,
- // SSH Tunnel settings
- use_ssh_tunnel: false,
- ssh_host: '',
- ssh_port: 22,
- ssh_username: '',
- ssh_password: '',
- ssh_private_key: ''
- }
- },
-
- // Options
- currencyOptions: [
- { label: 'GTQ', value: 'GTQ' },
- { label: 'USD', value: 'USD' }
- ]
+ saving: false,
+ data: {}
+ }
}
},
- ///////////////////////////////////////////////////
- ////////////////METHODS FUNCTIONS//////////////////
- ///////////////////////////////////////////////////
-
- methods: {
- // Utility Methods
- formatCurrency(amount) {
- if (!amount) return 'Q 0.00';
-
- // Amount is now stored as GTQ directly in database
- return new Intl.NumberFormat('es-GT', {
- style: 'currency',
- currency: 'GTQ',
- }).format(amount);
- },
-
- formatDate(dateString) {
- if (!dateString) return ''
- return new Date(dateString).toLocaleDateString()
- },
-
- formatDateTime(dateString) {
- if (!dateString) return ''
- const date = new Date(dateString)
- return date.toLocaleDateString() + ' ' + date.toLocaleTimeString('en-US', { hour12: false })
- },
-
- formatSats(amount) {
- if (!amount) return '0 sats'
- return new Intl.NumberFormat('en-US').format(amount) + ' sats'
- },
-
- getClientUsername(clientId) {
- const client = this.dcaClients.find(c => c.id === clientId)
- return client ? (client.username || client.user_id.substring(0, 8) + '...') : clientId
- },
-
-
- // Configuration Methods
- async getLamassuConfig() {
- try {
- const { data } = await LNbits.api.request(
- 'GET',
- '/satmachineadmin/api/v1/dca/config',
- null
- )
- this.lamassuConfig = data
-
- // When opening config dialog, populate the selected wallets if they exist
- if (data && data.source_wallet_id && this.g.user?.wallets) {
- const wallet = this.g.user.wallets.find(w => w.id === data.source_wallet_id)
- if (wallet) {
- this.configDialog.data.selectedWallet = wallet
- }
- }
- if (data && data.commission_wallet_id && this.g.user?.wallets) {
- const commissionWallet = this.g.user.wallets.find(w => w.id === data.commission_wallet_id)
- if (commissionWallet) {
- this.configDialog.data.selectedCommissionWallet = commissionWallet
- }
- }
-
- // Populate other configuration fields
- if (data) {
- this.configDialog.data.max_daily_limit_gtq = data.max_daily_limit_gtq || 2000
- }
- } catch (error) {
- // It's OK if no config exists yet
- this.lamassuConfig = null
- }
- },
-
- async saveConfiguration() {
- try {
- const data = {
- host: this.configDialog.data.host,
- port: this.configDialog.data.port,
- database_name: this.configDialog.data.database_name,
- username: this.configDialog.data.username,
- password: this.configDialog.data.password,
- source_wallet_id: this.configDialog.data.selectedWallet?.id,
- commission_wallet_id: this.configDialog.data.selectedCommissionWallet?.id,
- // SSH Tunnel settings
- max_daily_limit_gtq: this.configDialog.data.max_daily_limit_gtq,
- use_ssh_tunnel: this.configDialog.data.use_ssh_tunnel,
- ssh_host: this.configDialog.data.ssh_host,
- ssh_port: this.configDialog.data.ssh_port,
- ssh_username: this.configDialog.data.ssh_username,
- ssh_password: this.configDialog.data.ssh_password,
- ssh_private_key: this.configDialog.data.ssh_private_key
- }
-
- const { data: config } = await LNbits.api.request(
- 'POST',
- '/satmachineadmin/api/v1/dca/config',
- null,
- data
- )
-
- this.lamassuConfig = config
- this.closeConfigDialog()
-
- this.$q.notify({
- type: 'positive',
- message: 'Database configuration saved successfully',
- timeout: 5000
- })
- } catch (error) {
- LNbits.utils.notifyApiError(error)
- }
- },
-
- closeConfigDialog() {
- this.configDialog.show = false
- this.configDialog.data = {
- host: '',
- port: 5432,
- database_name: '',
- username: '',
- password: '',
- selectedWallet: null,
- selectedCommissionWallet: null,
- // DCA Client Limits
- max_daily_limit_gtq: 2000,
- // SSH Tunnel settings
- use_ssh_tunnel: false,
- ssh_host: '',
- ssh_port: 22,
- ssh_username: '',
- ssh_password: '',
- ssh_private_key: ''
- }
- },
-
- // DCA Client Methods
- async getDcaClients() {
- try {
- const { data } = await LNbits.api.request(
- 'GET',
- '/satmachineadmin/api/v1/dca/clients',
- null
- )
-
- // Fetch balance data for each client
- const clientsWithBalances = await Promise.all(
- data.map(async (client) => {
- try {
- const { data: balance } = await LNbits.api.request(
- 'GET',
- `/satmachineadmin/api/v1/dca/clients/${client.id}/balance`,
- null
- )
- return {
- ...client,
- remaining_balance: balance.remaining_balance
- }
- } catch (error) {
- console.error(`Error fetching balance for client ${client.id}:`, error)
- return {
- ...client,
- remaining_balance: 0
- }
- }
- })
- )
-
- this.dcaClients = clientsWithBalances
- } catch (error) {
- LNbits.utils.notifyApiError(error)
- }
- },
-
-
- // Quick Deposit Methods
- async sendQuickDeposit() {
- try {
- const data = {
- client_id: this.quickDepositForm.selectedClient?.value,
- amount: this.quickDepositForm.amount, // Send GTQ directly - now stored as GTQ
- currency: 'GTQ',
- notes: this.quickDepositForm.notes
- }
-
- const { data: newDeposit } = await LNbits.api.request(
- 'POST',
- '/satmachineadmin/api/v1/dca/deposits',
- null,
- data
- )
-
- this.deposits.unshift(newDeposit)
-
- // Reset form
- this.quickDepositForm = {
- selectedClient: null,
- amount: null,
- notes: ''
- }
-
- this.$q.notify({
- type: 'positive',
- message: 'Deposit created successfully',
- timeout: 5000
- })
- } catch (error) {
- LNbits.utils.notifyApiError(error)
- }
- },
-
- async viewClientDetails(client) {
- try {
- const { data: balance } = await LNbits.api.request(
- 'GET',
- `/satmachineadmin/api/v1/dca/clients/${client.id}/balance`,
- null
- )
- this.clientDetailsDialog.data = client
- this.clientDetailsDialog.balance = balance
- this.clientDetailsDialog.show = true
- } catch (error) {
- LNbits.utils.notifyApiError(error)
- }
- },
-
- // Deposit Methods
- async getDeposits() {
- try {
- const { data } = await LNbits.api.request(
- 'GET',
- '/satmachineadmin/api/v1/dca/deposits',
- null
- )
- this.deposits = data
- } catch (error) {
- LNbits.utils.notifyApiError(error)
- }
- },
-
- addDepositDialog(client) {
- this.depositFormDialog.data = {
- client_id: client.id,
- client_name: client.username || `${client.user_id.substring(0, 8)}...`,
- currency: 'GTQ'
- }
- this.depositFormDialog.show = true
- },
-
- async sendDepositData() {
- try {
- const data = {
- client_id: this.depositFormDialog.data.client_id,
- amount: this.depositFormDialog.data.amount, // Send GTQ directly - now stored as GTQ
- currency: this.depositFormDialog.data.currency,
- notes: this.depositFormDialog.data.notes
- }
-
- if (this.depositFormDialog.data.id) {
- // Update existing pending deposit
- const { data: updatedDeposit } = await LNbits.api.request(
- 'PUT',
- `/satmachineadmin/api/v1/dca/deposits/${this.depositFormDialog.data.id}`,
- null,
- { amount: data.amount, currency: data.currency, notes: data.notes }
- )
- const index = this.deposits.findIndex(d => d.id === updatedDeposit.id)
- if (index !== -1) {
- this.deposits.splice(index, 1, updatedDeposit)
- }
- } else {
- // Create new deposit
- const { data: newDeposit } = await LNbits.api.request(
- 'POST',
- '/satmachineadmin/api/v1/dca/deposits',
- null,
- data
- )
- this.deposits.unshift(newDeposit)
- }
-
- this.closeDepositFormDialog()
- this.$q.notify({
- type: 'positive',
- message: this.depositFormDialog.data.id ? 'Deposit updated successfully' : 'Deposit created successfully',
- timeout: 5000
- })
- } catch (error) {
- LNbits.utils.notifyApiError(error)
- }
- },
-
- closeDepositFormDialog() {
- this.depositFormDialog.show = false
- this.depositFormDialog.data = {
- currency: 'GTQ'
- }
- },
-
- async confirmDeposit(deposit) {
- try {
- await LNbits.utils
- .confirmDialog('Confirm that this deposit has been physically placed in the ATM machine?')
- .onOk(async () => {
- const { data: updatedDeposit } = await LNbits.api.request(
- 'PUT',
- `/satmachineadmin/api/v1/dca/deposits/${deposit.id}/status`,
- null,
- { status: 'confirmed', notes: 'Confirmed by admin - money placed in machine' }
- )
- const index = this.deposits.findIndex(d => d.id === deposit.id)
- if (index !== -1) {
- this.deposits.splice(index, 1, updatedDeposit)
- }
- this.$q.notify({
- type: 'positive',
- message: 'Deposit confirmed! DCA is now active for this client.',
- timeout: 5000
- })
- })
- } catch (error) {
- LNbits.utils.notifyApiError(error)
- }
- },
-
- editDeposit(deposit) {
- this.depositFormDialog.data = { ...deposit }
- this.depositFormDialog.show = true
- },
-
- async deleteDeposit(deposit) {
- try {
- await LNbits.utils
- .confirmDialog('Are you sure you want to delete this pending deposit?')
- .onOk(async () => {
- await LNbits.api.request(
- 'DELETE',
- `/satmachineadmin/api/v1/dca/deposits/${deposit.id}`,
- null
- )
- this.deposits = this.deposits.filter(d => d.id !== deposit.id)
- this.$q.notify({
- type: 'positive',
- message: 'Deposit deleted successfully',
- timeout: 5000
- })
- })
- } catch (error) {
- LNbits.utils.notifyApiError(error)
- }
- },
-
- // Export Methods
- async exportClientsCSV() {
- await LNbits.utils.exportCSV(this.clientsTable.columns, this.dcaClients)
- },
-
- async exportDepositsCSV() {
- await LNbits.utils.exportCSV(this.depositsTable.columns, this.deposits)
- },
-
- async exportLamassuTransactionsCSV() {
- await LNbits.utils.exportCSV(this.lamassuTransactionsTable.columns, this.lamassuTransactions)
- },
-
- // Polling Methods
- async testDatabaseConnection() {
- this.testingConnection = true
- try {
- const { data } = await LNbits.api.request(
- 'POST',
- '/satmachineadmin/api/v1/dca/test-connection',
- null
- )
-
- // Show detailed results in a dialog
- const stepsList = data.steps ? data.steps.join('\n') : 'No detailed steps available'
-
- let dialogContent = `Connection Test Results
`
-
- if (data.ssh_tunnel_used) {
- dialogContent += `SSH Tunnel: ${data.ssh_tunnel_success ? '✅ Success' : '❌ Failed'}
`
- }
-
- dialogContent += `Database: ${data.database_connection_success ? '✅ Success' : '❌ Failed'}
`
- dialogContent += `Detailed Steps:
`
- dialogContent += stepsList.replace(/\n/g, '
')
-
- this.$q.dialog({
- title: data.success ? 'Connection Test Passed' : 'Connection Test Failed',
- message: dialogContent,
- html: true,
- ok: {
- color: data.success ? 'positive' : 'negative',
- label: 'Close'
- }
- })
-
- // Also show a brief notification
- this.$q.notify({
- type: data.success ? 'positive' : 'negative',
- message: data.message,
- timeout: 3000
- })
-
- } catch (error) {
- LNbits.utils.notifyApiError(error)
- } finally {
- this.testingConnection = false
- }
- },
-
- async manualPoll() {
- this.runningManualPoll = true
- try {
- const { data } = await LNbits.api.request(
- 'POST',
- '/satmachineadmin/api/v1/dca/manual-poll',
- null
- )
-
- this.lastPollTime = new Date().toLocaleString()
- this.$q.notify({
- type: 'positive',
- message: `Manual poll completed. Found ${data.transactions_processed} new transactions.`,
- timeout: 5000
- })
-
- // Refresh data
- await this.getDcaClients() // Refresh to show updated balances
- await this.getDeposits()
- await this.getLamassuTransactions()
- await this.getLamassuConfig()
- } catch (error) {
- LNbits.utils.notifyApiError(error)
- } finally {
- this.runningManualPoll = false
- }
- },
-
- async testTransaction() {
- this.runningTestTransaction = true
- try {
- const { data } = await LNbits.api.request(
- 'POST',
- '/satmachineadmin/api/v1/dca/test-transaction',
- null
- )
-
- // Show detailed results in a dialog
- const details = data.transaction_details
-
- let dialogContent = `Test Transaction Results
`
- dialogContent += `Transaction ID: ${details.transaction_id}
`
- dialogContent += `Total Amount: ${details.total_amount_sats} sats
`
- dialogContent += `Base Amount: ${details.base_amount_sats} sats
`
- dialogContent += `Commission: ${details.commission_amount_sats} sats (${details.commission_percentage}%)
`
- if (details.discount > 0) {
- dialogContent += `Discount: ${details.discount}%
`
- dialogContent += `Effective Commission: ${details.effective_commission}%
`
- }
- dialogContent += `
Check your wallets to see the distributions!`
-
- this.$q.dialog({
- title: 'Test Transaction Completed',
- message: dialogContent,
- html: true,
- ok: {
- color: 'positive',
- label: 'Great!'
- }
- })
-
- // Also show a brief notification
- this.$q.notify({
- type: 'positive',
- message: `Test transaction processed: ${details.total_amount_sats} sats distributed`,
- timeout: 5000
- })
-
- // Refresh data
- await this.getDcaClients() // Refresh to show updated balances
- await this.getDeposits()
- await this.getLamassuTransactions()
- await this.getLamassuConfig()
- } catch (error) {
- LNbits.utils.notifyApiError(error)
- } finally {
- this.runningTestTransaction = false
- }
- },
-
- openManualTransactionDialog() {
- this.manualTransactionDialog.transactionId = ''
- this.manualTransactionDialog.show = true
- },
-
- async processSpecificTransaction() {
- if (!this.manualTransactionDialog.transactionId) {
- this.$q.notify({
- type: 'warning',
- message: 'Please enter a transaction ID',
- timeout: 3000
- })
- return
- }
-
- this.processingSpecificTransaction = true
- try {
- const { data } = await LNbits.api.request(
- 'POST',
- `/satmachineadmin/api/v1/dca/process-transaction/${this.manualTransactionDialog.transactionId}`,
- null
- )
-
- if (data.already_processed) {
- this.$q.notify({
- type: 'warning',
- message: `Transaction already processed with ${data.payment_count} distributions`,
- timeout: 5000
- })
- this.manualTransactionDialog.show = false
- return
- }
-
- // Show detailed results
- const details = data.transaction_details
- let dialogContent = `Manual Transaction Processing Results
`
- dialogContent += `Transaction ID: ${details.transaction_id}
`
- dialogContent += `Status: ${details.status}
`
- dialogContent += `Dispense: ${details.dispense ? 'Yes' : 'No'}
`
- dialogContent += `Dispense Confirmed: ${details.dispense_confirmed ? 'Yes' : 'No'}
`
- dialogContent += `Crypto Amount: ${details.crypto_amount} sats
`
- dialogContent += `Fiat Amount: ${details.fiat_amount}
`
- dialogContent += `
Transaction processed successfully!`
-
- this.$q.dialog({
- title: 'Transaction Processed',
- message: dialogContent,
- html: true,
- ok: {
- color: 'positive',
- label: 'Great!'
- }
- })
-
- this.$q.notify({
- type: 'positive',
- message: `Transaction ${details.transaction_id} processed successfully`,
- timeout: 5000
- })
-
- // Close dialog and refresh data
- this.manualTransactionDialog.show = false
- await this.getDcaClients()
- await this.getDeposits()
- await this.getLamassuTransactions()
- await this.getLamassuConfig()
-
- } catch (error) {
- LNbits.utils.notifyApiError(error)
- } finally {
- this.processingSpecificTransaction = false
- }
- },
-
- // Lamassu Transaction Methods
- async getLamassuTransactions() {
- try {
- const { data } = await LNbits.api.request(
- 'GET',
- '/satmachineadmin/api/v1/dca/transactions',
- null
- )
- this.lamassuTransactions = data
- } catch (error) {
- LNbits.utils.notifyApiError(error)
- }
- },
-
- async viewTransactionDistributions(transaction) {
- try {
- const { data: distributions } = await LNbits.api.request(
- 'GET',
- `/satmachineadmin/api/v1/dca/transactions/${transaction.id}/distributions`,
- null
- )
-
- this.distributionDialog.transaction = transaction
- this.distributionDialog.distributions = distributions
- this.distributionDialog.show = true
- } catch (error) {
- LNbits.utils.notifyApiError(error)
- }
- },
-
- },
- ///////////////////////////////////////////////////
- //////LIFECYCLE FUNCTIONS RUNNING ON PAGE LOAD/////
- ///////////////////////////////////////////////////
- async created() {
- // Load DCA admin data
- await Promise.all([
- this.getLamassuConfig(),
- this.getDcaClients(),
- this.getDeposits(),
- this.getLamassuTransactions()
- ])
- },
-
computed: {
- isConfigFormValid() {
- const data = this.configDialog.data
+ walletOptions() {
+ // g.user is sometimes null on initial mount in LNbits 1.4 — guard it.
+ const wallets = this.g?.user?.wallets || []
+ return wallets.map(w => ({label: w.name, value: w.id}))
+ }
+ },
- // Basic database fields are required
- const basicValid = data.host && data.database_name && data.username && data.selectedWallet
+ async created() {
+ await this.refreshAll()
+ },
- // If SSH tunnel is enabled, validate SSH fields
- if (data.use_ssh_tunnel) {
- const sshValid = data.ssh_host && data.ssh_username &&
- (data.ssh_password || data.ssh_private_key)
- return basicValid && sshValid
+ methods: {
+ // -----------------------------------------------------------------
+ // Loaders
+ // -----------------------------------------------------------------
+ async refreshAll() {
+ this.refreshing = true
+ try {
+ await Promise.all([
+ this.loadSuperConfig(),
+ this.loadMachines(),
+ this.loadWorklistCount()
+ ])
+ } finally {
+ this.refreshing = false
}
-
- return basicValid
},
- clientOptions() {
- return this.dcaClients.map(client => ({
- label: `${client.username || client.user_id.substring(0, 8) + '...'} (${client.dca_mode})`,
- value: client.id
- }))
+ async loadSuperConfig() {
+ try {
+ const {data} = await LNbits.api.request('GET', SUPER_FEE_PATH)
+ this.superConfig = data
+ } catch (e) {
+ this.superConfig = null
+ }
},
- totalDcaBalance() {
- return this.deposits
- .filter(d => d.status === 'confirmed')
- .reduce((total, deposit) => total + deposit.amount, 0)
+ async loadMachines() {
+ try {
+ const {data} = await LNbits.api.request('GET', MACHINES_PATH)
+ this.machines = data || []
+ } catch (e) {
+ this.machines = []
+ this._notifyError(e, 'Failed to load machines')
+ }
+ },
+
+ async loadWorklistCount() {
+ // Light read used to badge the Worklist tab. The full worklist
+ // panel lives in P9g; here we just count for the badge.
+ try {
+ const {data} = await LNbits.api.request('GET', STUCK_PATH)
+ this.worklistCount =
+ (data?.errored?.length || 0) +
+ (data?.stuck_pending?.length || 0) +
+ (data?.stuck_processing?.length || 0)
+ } catch (e) {
+ this.worklistCount = 0
+ }
+ },
+
+ // -----------------------------------------------------------------
+ // Add machine
+ // -----------------------------------------------------------------
+ openAddMachineDialog() {
+ this.addMachineDialog.data = this._emptyMachineForm()
+ this.addMachineDialog.show = true
+ },
+
+ async submitAddMachine() {
+ const body = this._cleanMachineForm(this.addMachineDialog.data)
+ if (!body.machine_npub || !body.wallet_id) {
+ Quasar.Notify.create({
+ type: 'negative',
+ message: 'machine_npub and wallet_id are required'
+ })
+ return
+ }
+ this.addMachineDialog.saving = true
+ try {
+ const {data} = await LNbits.api.request('POST', MACHINES_PATH, null, body)
+ this.machines.unshift(data)
+ this.addMachineDialog.show = false
+ Quasar.Notify.create({
+ type: 'positive',
+ message: `Machine ${data.name || data.machine_npub.slice(0, 12)} added`
+ })
+ } catch (e) {
+ this._notifyError(e, 'Failed to add machine')
+ } finally {
+ this.addMachineDialog.saving = false
+ }
+ },
+
+ // -----------------------------------------------------------------
+ // Edit / delete machine
+ // -----------------------------------------------------------------
+ openEditMachineDialog(machine) {
+ this.editMachineDialog.data = {
+ id: machine.id,
+ name: machine.name || '',
+ location: machine.location || '',
+ wallet_id: machine.wallet_id,
+ fiat_code: machine.fiat_code,
+ fallback_commission_pct: machine.fallback_commission_pct,
+ is_active: machine.is_active
+ }
+ this.editMachineDialog.show = true
+ },
+
+ async submitEditMachine() {
+ const d = this.editMachineDialog.data
+ this.editMachineDialog.saving = true
+ try {
+ const {data} = await LNbits.api.request(
+ 'PUT',
+ `${MACHINES_PATH}/${d.id}`,
+ null,
+ {
+ name: d.name,
+ location: d.location,
+ wallet_id: d.wallet_id,
+ fiat_code: d.fiat_code,
+ fallback_commission_pct: d.fallback_commission_pct,
+ is_active: d.is_active
+ }
+ )
+ const idx = this.machines.findIndex(m => m.id === data.id)
+ if (idx >= 0) this.machines[idx] = data
+ this.editMachineDialog.show = false
+ Quasar.Notify.create({type: 'positive', message: 'Machine updated'})
+ } catch (e) {
+ this._notifyError(e, 'Failed to update machine')
+ } finally {
+ this.editMachineDialog.saving = false
+ }
+ },
+
+ confirmDeleteMachine(machine) {
+ Quasar.Dialog.create({
+ title: 'Delete machine?',
+ message:
+ `This removes ${machine.name || machine.machine_npub.slice(0, 12)}` +
+ ' from your fleet. Existing settlements and payment history are preserved' +
+ ' — only the machine row itself is removed. Continue?',
+ html: true,
+ cancel: true,
+ persistent: true
+ }).onOk(async () => {
+ try {
+ await LNbits.api.request('DELETE', `${MACHINES_PATH}/${machine.id}`)
+ this.machines = this.machines.filter(m => m.id !== machine.id)
+ Quasar.Notify.create({type: 'positive', message: 'Machine deleted'})
+ } catch (e) {
+ this._notifyError(e, 'Failed to delete machine')
+ }
+ })
+ },
+
+ // -----------------------------------------------------------------
+ // Future: drill into a machine's detail view. Stub for P9a; P9b
+ // wires the actual settlement / telemetry / commission-splits panel.
+ // -----------------------------------------------------------------
+ viewMachine(machine) {
+ Quasar.Notify.create({
+ type: 'info',
+ message: `Machine detail view lands in P9b. (selected ${machine.id})`
+ })
+ },
+
+ // -----------------------------------------------------------------
+ // Helpers
+ // -----------------------------------------------------------------
+ shortNpub(npub) {
+ if (!npub) return ''
+ if (npub.length <= 16) return npub
+ return npub.slice(0, 8) + '…' + npub.slice(-6)
+ },
+
+ shortId(id) {
+ if (!id) return ''
+ return id.length <= 12 ? id : id.slice(0, 8) + '…'
+ },
+
+ copy(text) {
+ if (!text) return
+ Quasar.copyToClipboard(text).then(() => {
+ Quasar.Notify.create({type: 'info', message: 'Copied', timeout: 800})
+ })
+ },
+
+ _emptyMachineForm() {
+ return {
+ machine_npub: '',
+ wallet_id: null,
+ name: '',
+ location: '',
+ fiat_code: 'GTQ',
+ fallback_commission_pct: 0.05
+ }
+ },
+
+ _cleanMachineForm(d) {
+ return {
+ machine_npub: (d.machine_npub || '').trim(),
+ wallet_id: d.wallet_id,
+ name: (d.name || '').trim() || null,
+ location: (d.location || '').trim() || null,
+ fiat_code: (d.fiat_code || 'GTQ').trim(),
+ fallback_commission_pct: Number(d.fallback_commission_pct ?? 0.05)
+ }
+ },
+
+ _notifyError(err, fallback) {
+ const msg = err?.response?.data?.detail || err?.message || fallback
+ Quasar.Notify.create({type: 'negative', message: msg, timeout: 5000})
}
}
})
diff --git a/templates/satmachineadmin/index.html b/templates/satmachineadmin/index.html
index 9c0f3a9..dc1fd8d 100644
--- a/templates/satmachineadmin/index.html
+++ b/templates/satmachineadmin/index.html
@@ -1,851 +1,317 @@
-
-
-
+{% extends "base.html" %}
+{% from "macros.jinja" import window_vars with context %}
-{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
-%} {% block scripts %} {{ window_vars(user) }}
-
-{% endblock %} {% block page %}
+{% block scripts %}
+ {{ window_vars(user) }}
+
+{% endblock %}
+
+{% block page %}
Manage fiat deposits for existing DCA clients
-Clients registered via the DCA client extension
-+ Manage your bitSpire fleet, liquidity providers, and commission distribution. +
+Add a new deposit for an existing client
- -+ Each ATM is paired with one dedicated wallet. Inbound payments to + that wallet trigger automatic distribution. +
ATM transactions processed through DCA distribution
-
- Dollar Cost Averaging administration for Lamassu ATM integration.
- Manage client deposits and DCA distribution settings.
-
Database: ${ lamassuConfig.host }:${ lamassuConfig.port }/${ lamassuConfig.database_name }
-Status:
-
Last Poll: ${ lamassuConfig.last_poll_time ? formatDateTime(lamassuConfig.last_poll_time) : 'Not yet run' }
-Last Success: ${ lamassuConfig.last_successful_poll ? formatDateTime(lamassuConfig.last_successful_poll) : 'Never' }
-Status:
+
+ + 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. +
+ +