satmachineclient/static/js/index.js
Padreug 18010de363 fix(v2)(ui): rewire LP dashboard JS to call the new dca_lp endpoints
The static JS was orphaned at a prior backend refactor: it called
/registration-status, /register, /dashboard/summary, /dashboard/
transactions, /dashboard/analytics — none of which exist in views_api.
First render hit `Failed to check registration status` and the
"Welcome to DCA" form's submit failed with `Failed to register for DCA`.

This commit rewires the data layer to the v2 endpoints:
  - GET  /api/v1/dca-client/preferences  (auto-onboards the LP on load
    — the act of opening this dashboard is what creates the LP's
    dca_lp row, which unlocks deposit creation on the operator side)
  - GET  /api/v1/dca-client/positions    (404 → empty-state, not error)
  - GET  /api/v1/dca-client/transactions

The legacy "Welcome / register" wizard in index.html is now dead
(`isRegistered` defaults to true and `loadPreferences` always succeeds
because the backend auto-creates). The chart panel is also dead (no
backend /analytics endpoint). Stubs added for `registerClient`,
`loadChartData`, plus `chartLoading` / `chartTimeRange` /
`analyticsData` data fields so the template renders without
undefined-binding warnings even though those branches never execute.

Future cleanup: the registration card + chart panel HTML can be
deleted from the template, and a preferences-editor card (PUT
/preferences) added. Out of scope here — the priority was unblocking
E2E testing on v2-bitspire.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 15:29:18 +02:00

326 lines
12 KiB
JavaScript

// Satoshi Machine Client v2 — LP dashboard JS.
//
// Maintenance-mode reminder: the rich LP UI is moving to ~/dev/webapp.
// This file is the lightweight in-LNbits dashboard kept functional for
// dev / E2E testing on the v2-bitspire branch. Endpoints:
// GET /satmachineclient/api/v1/dca-client/preferences (auto-onboards)
// PUT /satmachineclient/api/v1/dca-client/preferences
// GET /satmachineclient/api/v1/dca-client/positions
// GET /satmachineclient/api/v1/dca-client/transactions
//
// The "registration / welcome" wizard in the template is dead — every
// call to GET /preferences auto-creates the LP's dca_lp row on first
// hit, so `isRegistered` is always true after the initial load and the
// wizard never shows. Cleanup of the template is deferred.
window.app = Vue.createApp({
el: '#vue',
mixins: [windowMixin],
delimiters: ['${', '}'],
data: function () {
return {
// Registration / onboarding (legacy; always true after initial
// /preferences call because the endpoint auto-creates).
isRegistered: true,
registrationChecked: true,
registrationForm: {
selectedWallet: null,
dca_mode: 'flow',
fixed_mode_daily_limit: null,
username: ''
},
// LP preferences (loaded from dca_lp on first call).
preferences: null,
// Admin configuration (legacy: no longer fetched — kept so the
// legacy template doesn't undefined-error on `adminConfig.*`).
adminConfig: {
max_daily_limit_gtq: 2000,
currency: 'GTQ'
},
// Dashboard state
dashboardData: null,
transactions: [],
loading: true,
error: null,
showFiatValues: false,
// Stubs for legacy template bindings (chart + registration form
// are dead branches but still in the .html — keep these defined
// so Vue doesn't emit warnings on initial render).
chartLoading: false,
chartTimeRange: '30d',
analyticsData: null,
transactionColumns: [
{name: 'date', label: 'Date', align: 'left',
field: row => row.transaction_time || row.created_at, sortable: false},
{name: 'amount_sats', label: 'Bitcoin', align: 'right',
field: 'amount_sats', sortable: false},
{name: 'amount_fiat', label: 'Fiat Amount', align: 'right',
field: 'amount_fiat', sortable: false},
{name: 'type', label: 'Type', align: 'center',
field: 'leg_type', sortable: false},
{name: 'status', label: 'Status', align: 'center',
field: 'status', sortable: false}
],
transactionPagination: {
sortBy: 'date',
descending: true,
page: 1,
rowsPerPage: 10
}
}
},
methods: {
// -----------------------------------------------------------------
// Onboarding + preferences
// -----------------------------------------------------------------
async loadPreferences() {
// GET /preferences auto-creates the LP's dca_lp row with the
// authenticated wallet as the default DCA destination. This is
// the structural enforcement of the "LP must onboard before
// deposits work" gate on the operator side.
try {
const {data} = await LNbits.api.request(
'GET',
'/satmachineclient/api/v1/dca-client/preferences',
this.g.user.wallets[0].adminkey
)
this.preferences = data
this.isRegistered = true
this.registrationChecked = true
} catch (error) {
console.error('Error loading preferences:', error)
this.error = 'Failed to load DCA preferences'
this.registrationChecked = true
}
},
// -----------------------------------------------------------------
// Formatting helpers
// -----------------------------------------------------------------
formatCurrency(amount) {
if (!amount) return 'Q 0.00'
return new Intl.NumberFormat('es-GT', {
style: 'currency', currency: 'GTQ'
}).format(amount)
},
formatCurrencyWithCode(amount, currencyCode) {
if (!amount) return `${currencyCode} 0.00`
try {
return new Intl.NumberFormat('en-US', {
style: 'currency', currency: currencyCode
}).format(amount)
} catch (error) {
return `${currencyCode} ${amount.toFixed(2)}`
}
},
formatDate(dateString) {
if (!dateString) return ''
const date = new Date(dateString)
if (isNaN(date.getTime())) return 'Invalid Date'
return date.toLocaleDateString()
},
formatTime(dateString) {
if (!dateString) return ''
const date = new Date(dateString)
if (isNaN(date.getTime())) return 'Invalid Time'
return date.toLocaleTimeString('en-US', {hour: '2-digit', minute: '2-digit'})
},
formatSats(amount) {
if (!amount) return '0 sats'
const formatted = new Intl.NumberFormat('en-US').format(amount)
if (amount >= 100000000) return formatted + ' sats 🏆'
if (amount >= 50000000) return formatted + ' sats 🎆'
if (amount >= 10000000) return formatted + ' sats 👑'
if (amount >= 5000000) return formatted + ' sats 🏆'
if (amount >= 1000000) return formatted + ' sats 🌟'
if (amount >= 500000) return formatted + ' sats 🔥'
if (amount >= 100000) return formatted + ' sats 🚀'
if (amount >= 50000) return formatted + ' sats ⚡'
if (amount >= 10000) return formatted + ' sats 🎯'
return formatted + ' sats'
},
// -----------------------------------------------------------------
// Dashboard data
// -----------------------------------------------------------------
async loadDashboardData() {
try {
const {data} = await LNbits.api.request(
'GET',
'/satmachineclient/api/v1/dca-client/positions',
this.g.user.wallets[0].adminkey
)
// Backend returns ClientDashboardSummary with no dca_mode field
// at the top level any more (it's LP-wide and lives on
// preferences); echo `default_dca_mode` from prefs so the
// legacy template renderers (`dashboardData.dca_mode`) keep
// working until the template is rewritten.
data.dca_mode = this.preferences?.default_dca_mode || 'flow'
data.dca_status = 'active'
this.dashboardData = data
} catch (error) {
// 404 from /positions = "LP isn't enrolled at any machine yet",
// which is a valid state, not an error. Show an empty dashboard.
if (error?.response?.status === 404) {
this.dashboardData = {
user_id: this.g.user.id,
total_sats_accumulated: 0,
total_fiat_invested: 0,
current_fiat_balance: 0,
pending_fiat_deposits: 0,
average_cost_basis: 0,
current_sats_fiat_value: 0,
total_transactions: 0,
total_machines: 0,
last_transaction_date: null,
currency: this.adminConfig.currency,
positions: [],
dca_mode: this.preferences?.default_dca_mode || 'flow',
dca_status: 'awaiting_enrolment'
}
} else {
console.error('Error loading dashboard data:', error)
this.error = 'Failed to load dashboard data'
}
}
},
async loadTransactions() {
try {
const {data} = await LNbits.api.request(
'GET',
'/satmachineclient/api/v1/dca-client/transactions?limit=50',
this.g.user.wallets[0].adminkey
)
this.transactions = data.sort((a, b) => {
const dateA = new Date(a.transaction_time || a.created_at)
const dateB = new Date(b.transaction_time || b.created_at)
return dateB - dateA
})
} catch (error) {
console.error('Error loading transactions:', error)
this.$q.notify({
type: 'negative',
message: 'Failed to load transactions',
position: 'top'
})
}
},
async refreshAllData() {
try {
this.loading = true
await Promise.all([
this.loadPreferences(),
this.loadDashboardData(),
this.loadTransactions()
])
this.$q.notify({
type: 'positive',
message: 'Dashboard refreshed!',
icon: 'refresh',
position: 'top'
})
} catch (error) {
console.error('Error refreshing data:', error)
this.$q.notify({
type: 'negative',
message: 'Failed to refresh data',
position: 'top'
})
} finally {
this.loading = false
}
},
// -----------------------------------------------------------------
// Milestone widget (purely cosmetic)
// -----------------------------------------------------------------
getNextMilestone() {
if (!this.dashboardData) return {target: 10000, name: '10k sats'}
const sats = this.dashboardData.total_sats_accumulated
if (sats < 10000) return {target: 10000, name: '10k sats'}
if (sats < 50000) return {target: 50000, name: '50k sats'}
if (sats < 100000) return {target: 100000, name: '100k sats'}
if (sats < 500000) return {target: 500000, name: '500k sats'}
if (sats < 1000000) return {target: 1000000, name: '1M sats'}
if (sats < 5000000) return {target: 5000000, name: '5M sats'}
if (sats < 10000000) return {target: 10000000, name: '10M sats'}
if (sats < 50000000) return {target: 50000000, name: '50M sats'}
if (sats < 100000000) return {target: 100000000, name: '100M sats (1 BTC!)'}
return {target: 500000000, name: '500M sats (5 BTC)'}
},
getMilestoneProgress() {
if (!this.dashboardData) return 0
const sats = this.dashboardData.total_sats_accumulated
const milestone = this.getNextMilestone()
const progress = (sats / milestone.target) * 100
return Math.min(Math.max(progress, 0), 100)
},
// -----------------------------------------------------------------
// Stubs for the legacy registration wizard + chart panel.
// Those template branches are dead (the wizard never shows because
// isRegistered is true after auto-onboard; the chart panel has no
// backend analytics endpoint to feed it) but the .html still
// references these handlers. Keep stubs so a stray click doesn't
// throw an uncaught error.
// -----------------------------------------------------------------
async registerClient() {
// Old "register / pick wallet & mode" form is gone. Preferences
// are auto-created and editable via a separate path (TODO: add
// an editor card to this dashboard once the template gets a
// proper rewrite).
await this.loadPreferences()
this.$q.notify({
type: 'info',
message: 'Your DCA account is already set up — refreshed.',
position: 'top'
})
},
loadChartData() {
// No backend analytics endpoint; chart panel is dead.
}
},
async created() {
try {
this.loading = true
// Auto-onboard on load (creates dca_lp row if missing).
await this.loadPreferences()
// Then load dashboard + transactions.
await Promise.all([
this.loadDashboardData(),
this.loadTransactions()
])
} catch (error) {
console.error('Error initializing dashboard:', error)
this.error = 'Failed to initialize dashboard'
} finally {
this.loading = false
}
},
computed: {
hasData() {
return this.dashboardData && !this.loading && this.isRegistered
},
walletOptions() {
if (!this.g.user?.wallets) return []
return this.g.user.wallets.map(wallet => ({
label: `${wallet.name} (${Math.round(wallet.balance_msat / 1000)} sats)`,
value: wallet.id
}))
}
}
})