diff --git a/static/js/index.js b/static/js/index.js index 483df51..d11a526 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -67,6 +67,40 @@ window.app = Vue.createApp({ // across the current legs (purely visual; doesn't hit the server). commissionPreviewInput: 1000, + // Worklist (P9g) + worklist: { + errored: [], + stuck_pending: [], + stuck_processing: [], + totalCount: 0 + }, + worklistLoading: false, + worklistThreshold: 30, + worklistTable: { + columns: [ + {name: 'machine', label: 'Machine', field: 'machine_id', align: 'left'}, + {name: 'created_at', label: 'Created', field: 'created_at', align: 'left'}, + {name: 'gross_sats', label: 'Gross', field: 'gross_sats', align: 'right'}, + { + name: 'error_message', + label: 'Error', + field: 'error_message', + align: 'left' + }, + {name: 'actions', label: '', field: 'id', align: 'right'} + ] + }, + + // Reports + reportsBusy: false, + + // Super-fee edit dialog (super-only) + superFeeDialog: { + show: false, + saving: false, + data: {super_fee_pct: 0, super_fee_wallet_id: ''} + }, + // UI configuration ----------------------------------------------- machinesTable: { columns: [ @@ -226,6 +260,32 @@ window.app = Vue.createApp({ value: c.id })) }, + worklistBuckets() { + return [ + { + key: 'errored', + label: 'Errored — needs retry', + icon: 'error', + color: 'red', + rows: this.worklist.errored + }, + { + key: 'stuck_pending', + label: 'Stuck pending — listener crashed before processing?', + icon: 'hourglass_top', + color: 'orange', + rows: this.worklist.stuck_pending + }, + { + key: 'stuck_processing', + label: 'Stuck processing — processor crashed mid-flight?', + icon: 'sync_problem', + color: 'purple', + rows: this.worklist.stuck_processing + } + ] + }, + commissionScopeOptions() { const opts = [{label: 'Default ruleset (operator-wide)', value: null}] for (const m of this.machines) { @@ -284,6 +344,7 @@ window.app = Vue.createApp({ async created() { await this.refreshAll() await this.loadCommissionSplits() + await this.loadWorklist() }, methods: { @@ -377,8 +438,8 @@ window.app = Vue.createApp({ }, async loadWorklistCount() { - // Light read used to badge the Worklist tab. The full worklist - // panel lives in P9g; here we just count for the badge. + // Light read for the tab badge — Worklist tab fetches the full + // payload via loadWorklist when opened. try { const {data} = await LNbits.api.request('GET', STUCK_PATH) this.worklistCount = @@ -390,6 +451,162 @@ window.app = Vue.createApp({ } }, + async loadWorklist() { + this.worklistLoading = true + try { + const {data} = await LNbits.api.request( + 'GET', `${STUCK_PATH}?threshold_minutes=${this.worklistThreshold}` + ) + this.worklist.errored = data?.errored || [] + this.worklist.stuck_pending = data?.stuck_pending || [] + this.worklist.stuck_processing = data?.stuck_processing || [] + this.worklist.totalCount = + this.worklist.errored.length + + this.worklist.stuck_pending.length + + this.worklist.stuck_processing.length + this.worklistCount = this.worklist.totalCount + } catch (e) { + this._notifyError(e, 'Failed to load worklist') + } finally { + this.worklistLoading = false + } + }, + + async viewMachineFromWorklist(settlement) { + const machine = this.machinesById[settlement.machine_id] + if (!machine) return + await this.viewMachine(machine) + }, + + confirmRetryFromWorklist(settlement) { + this.confirmRetrySettlement(settlement) + // Drop from worklist on success (optimistic; reload covers re-eval). + setTimeout(() => this.loadWorklist(), 500) + }, + + confirmForceResetFromWorklist(settlement) { + this.confirmForceReset(settlement) + setTimeout(() => this.loadWorklist(), 500) + }, + + // ----------------------------------------------------------------- + // Super-fee edit (P9f — super-only) + // ----------------------------------------------------------------- + openSuperFeeDialog() { + this.superFeeDialog.data = { + super_fee_pct: this.superConfig?.super_fee_pct ?? 0, + super_fee_wallet_id: this.superConfig?.super_fee_wallet_id || '' + } + this.superFeeDialog.show = true + }, + + async submitSuperFee() { + const d = this.superFeeDialog.data + this.superFeeDialog.saving = true + try { + const {data} = await LNbits.api.request( + 'PUT', SUPER_FEE_PATH, null, + { + super_fee_pct: Number(d.super_fee_pct), + super_fee_wallet_id: (d.super_fee_wallet_id || '').trim() || null + } + ) + this.superConfig = data + this.superFeeDialog.show = false + Quasar.Notify.create({type: 'positive', message: 'Platform fee updated'}) + } catch (e) { + this._notifyError(e, 'Save failed') + } finally { + this.superFeeDialog.saving = false + } + }, + + // ----------------------------------------------------------------- + // Reports / CSV exports (P9g) + // ----------------------------------------------------------------- + downloadMachinesCsv() { + this._downloadCsv( + 'machines.csv', + ['id', 'machine_npub', 'wallet_id', 'name', 'location', 'fiat_code', + 'is_active', 'fallback_commission_pct', 'created_at'], + this.machines + ) + }, + + downloadClientsCsv() { + const rows = this.clients.map(c => { + const bal = this.clientBalances[c.id] || {} + return { + ...c, + machine_name: this.machineNameById(c.machine_id), + remaining_balance: bal.remaining_balance ?? '', + total_deposits: bal.total_deposits ?? '', + total_payments: bal.total_payments ?? '', + balance_currency: bal.currency ?? '' + } + }) + this._downloadCsv( + 'clients.csv', + ['id', 'machine_id', 'machine_name', 'user_id', 'wallet_id', + 'username', 'dca_mode', 'status', 'autoforward_enabled', + 'autoforward_ln_address', 'total_deposits', 'total_payments', + 'remaining_balance', 'balance_currency', 'created_at'], + rows + ) + }, + + downloadDepositsCsv() { + this._downloadCsv( + 'deposits.csv', + ['id', 'client_id', 'machine_id', 'creator_user_id', 'amount', + 'currency', 'status', 'notes', 'created_at', 'confirmed_at'], + this.deposits + ) + }, + + async downloadPaymentsCsv() { + // Payments are not pre-loaded; fetch on demand. + this.reportsBusy = true + try { + const {data} = await LNbits.api.request('GET', `${API}/payments`) + this._downloadCsv( + 'payments.csv', + ['id', 'settlement_id', 'client_id', 'machine_id', 'leg_type', + 'destination_wallet_id', 'destination_ln_address', 'amount_sats', + 'amount_fiat', 'exchange_rate', 'status', 'external_payment_hash', + 'transaction_time', 'created_at', 'error_message'], + data || [] + ) + } catch (e) { + this._notifyError(e, 'Failed to fetch payments') + } finally { + this.reportsBusy = false + } + }, + + _downloadCsv(filename, columns, rows) { + const escape = v => { + if (v == null) return '' + const s = String(v) + if (/[",\n\r]/.test(s)) return '"' + s.replace(/"/g, '""') + '"' + return s + } + const header = columns.join(',') + const body = rows.map( + row => columns.map(col => escape(row[col])).join(',') + ).join('\n') + const csv = header + '\n' + body + const blob = new Blob([csv], {type: 'text/csv;charset=utf-8'}) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = filename + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + }, + // ----------------------------------------------------------------- // Add machine // ----------------------------------------------------------------- diff --git a/templates/satmachineadmin/index.html b/templates/satmachineadmin/index.html index 74e38b3..eb763e2 100644 --- a/templates/satmachineadmin/index.html +++ b/templates/satmachineadmin/index.html @@ -43,6 +43,14 @@ Your remainder splits per the rules below. + @@ -548,14 +556,167 @@ - - Worklist (stuck / errored settlements) — pending P9g. +
+
+
Worklist
+

+ Settlements that didn't process cleanly. Errored ones need + retry; stuck ones may need force-reset (processor crashed + mid-flight). +

+
+
+ + +
+
+ + + + All clear — no errored or stuck settlements. + +
+
+ + + +
+ + + +
+ - - Reports / CSV exports — pending P9g. - +
+
+
Reports
+

+ 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. +

+
+
+
+
+ + +
Machines
+
+ rows +
+
+ + + +
+
+
+ + +
Clients (LPs)
+
+ rows, balances included +
+
+ + + +
+
+
+ + +
Deposits
+
+ rows +
+
+ + + +
+
+
+ + +
Payments (legs)
+
+ Distribution audit (dca / super_fee / operator_split / etc) +
+
+ + + +
+
+
@@ -889,6 +1050,41 @@ + + + + + + +
Platform fee (super-only)
+ + +
+ +

+ Charged on every operator's commission across the LNbits instance. + Operators see this as a read-only banner. Wallet ID is where the + collected fee lands; typically a wallet you (the super) own. +

+ + +
+ + + + +
+
+