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>
326 lines
12 KiB
JavaScript
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
|
|
}))
|
|
}
|
|
}
|
|
})
|