Refactor DCA client registration form: Update wallet selection to use walletOptions computed property for better data handling. Change element ID from 'dcaClient' to 'vue' for consistency. Improve error handling and data validation in chart loading and registration processes.

This commit is contained in:
padreug 2025-06-27 00:28:49 +02:00
parent 306549a656
commit 03179647ec
2 changed files with 85 additions and 89 deletions

View file

@ -1,5 +1,5 @@
window.app = Vue.createApp({
el: '#dcaClient',
el: '#vue',
mixins: [windowMixin],
delimiters: ['${', '}'],
data: function () {
@ -14,7 +14,7 @@ window.app = Vue.createApp({
fixed_mode_daily_limit: null,
username: ''
},
// Dashboard state
dashboardData: null,
transactions: [],
@ -80,17 +80,17 @@ window.app = Vue.createApp({
'/satmachineclient/api/v1/registration-status',
this.g.user.wallets[0].adminkey
)
this.isRegistered = data.is_registered
this.registrationChecked = true
if (!this.isRegistered) {
this.showRegistrationDialog = true
// Pre-fill username and default wallet if available
this.registrationForm.username = this.g.user.username || ''
this.registrationForm.selectedWallet = this.g.user.wallets[0] || null
this.registrationForm.selectedWallet = this.g.user.wallets[0]?.id || null
}
return data
} catch (error) {
console.error('Error checking registration status:', error)
@ -108,26 +108,32 @@ window.app = Vue.createApp({
username: this.registrationForm.username || this.g.user.username || `user_${this.g.user.id.substring(0, 8)}`
}
// Find the selected wallet object to get the adminkey
const selectedWallet = this.g.user.wallets.find(w => w.id === this.registrationForm.selectedWallet)
if (!selectedWallet) {
throw new Error('Selected wallet not found')
}
const { data } = await LNbits.api.request(
'POST',
'/satmachineclient/api/v1/register',
this.registrationForm.selectedWallet.adminkey,
selectedWallet.adminkey,
registrationData
)
this.isRegistered = true
this.showRegistrationDialog = false
this.$q.notify({
type: 'positive',
message: data.message || 'Successfully registered for DCA!',
icon: 'check_circle',
position: 'top'
})
// Load dashboard data after successful registration
await this.loadDashboardData()
} catch (error) {
console.error('Error registering client:', error)
this.$q.notify({
@ -297,18 +303,18 @@ window.app = Vue.createApp({
console.log('Chart already loading, ignoring request')
return
}
try {
this.chartLoading = true
// Destroy existing chart immediately to prevent conflicts
if (this.dcaChart) {
console.log('Destroying existing chart before loading new data')
this.dcaChart.destroy()
this.dcaChart = null
}
const {data} = await LNbits.api.request(
const { data } = await LNbits.api.request(
'GET',
`/satmachineclient/api/v1/dashboard/analytics?time_range=${this.chartTimeRange}`,
this.g.user.wallets[0].adminkey
@ -321,10 +327,10 @@ window.app = Vue.createApp({
}
this.analyticsData = data
// Wait for DOM update and ensure we're still in loading state
await this.$nextTick()
// Double-check we're still the active loading request
if (this.chartLoading) {
this.initDCAChart()
@ -343,13 +349,13 @@ window.app = Vue.createApp({
console.log('analyticsData:', this.analyticsData)
console.log('dcaChart ref:', this.$refs.dcaChart)
console.log('chartLoading state:', this.chartLoading)
// Skip if we're not in a loading state (indicates this is a stale call)
if (!this.chartLoading && this.dcaChart) {
console.log('Chart already exists and not loading, skipping initialization')
return
}
if (!this.analyticsData) {
console.log('No analytics data available')
return
@ -371,7 +377,7 @@ window.app = Vue.createApp({
console.error('Chart.js is not loaded')
return
}
console.log('Chart.js version:', Chart.version || 'unknown')
console.log('Chart.js available:', typeof Chart)
@ -383,45 +389,45 @@ window.app = Vue.createApp({
}
const ctx = this.$refs.dcaChart.getContext('2d')
// Use accumulation_timeline data which is already grouped by day
const timelineData = this.analyticsData.accumulation_timeline || []
console.log('Timeline data sample:', timelineData.slice(0, 2)) // Debug first 2 records
// If we have timeline data, use it (already grouped by day)
if (timelineData.length > 0) {
// Calculate running totals from daily data
let runningSats = 0
const labels = []
const cumulativeSats = []
timelineData.forEach(point => {
// Ensure sats is a valid number
const sats = point.sats || 0
const validSats = typeof sats === 'number' ? sats : parseFloat(sats) || 0
runningSats += validSats
const date = new Date(point.date)
if (!isNaN(date.getTime())) {
labels.push(date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric'
labels.push(date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric'
}))
cumulativeSats.push(runningSats)
}
})
console.log('Timeline chart data:', { labels, cumulativeSats })
this.createChart(labels, cumulativeSats)
return
}
// Fallback to cost_basis_history but group by date to avoid duplicates
console.log('No timeline data, using cost_basis_history as fallback')
const chartData = this.analyticsData.cost_basis_history || []
console.log('Chart data sample:', chartData.slice(0, 2)) // Debug first 2 records
// Handle empty data case
if (chartData.length === 0) {
console.log('No chart data available')
@ -429,7 +435,7 @@ window.app = Vue.createApp({
const placeholderGradient = ctx.createLinearGradient(0, 0, 0, 300)
placeholderGradient.addColorStop(0, 'rgba(255, 149, 0, 0.3)')
placeholderGradient.addColorStop(1, 'rgba(255, 149, 0, 0.05)')
// Show placeholder chart with enhanced styling
this.dcaChart = new Chart(ctx, {
type: 'line',
@ -463,7 +469,7 @@ window.app = Vue.createApp({
borderWidth: 2,
cornerRadius: 8,
callbacks: {
label: function(context) {
label: function (context) {
return `${context.parsed.y.toLocaleString()} sats`
}
}
@ -486,7 +492,7 @@ window.app = Vue.createApp({
ticks: {
color: '#666666',
font: { size: 12, weight: '500' },
callback: function(value) {
callback: function (value) {
return value.toLocaleString() + ' sats'
}
}
@ -498,7 +504,7 @@ window.app = Vue.createApp({
this.chartLoading = false
return
}
// Group cost_basis_history by date to eliminate duplicates
const groupedData = new Map()
chartData.forEach(point => {
@ -513,30 +519,30 @@ window.app = Vue.createApp({
}
}
})
const uniqueChartData = Array.from(groupedData.values()).sort((a, b) =>
const uniqueChartData = Array.from(groupedData.values()).sort((a, b) =>
new Date(a.date).getTime() - new Date(b.date).getTime()
)
const labels = uniqueChartData.map(point => {
// Handle different date formats with enhanced timezone handling
let date;
if (point.date) {
console.log('Raw date from API:', point.date); // Debug the actual date string
// If it's an ISO string with timezone info, parse it correctly
if (typeof point.date === 'string' && point.date.includes('T')) {
// ISO string - parse and convert to local date
date = new Date(point.date);
// For display purposes, use the date part only to avoid timezone shifts
const localDateStr = date.getFullYear() + '-' +
String(date.getMonth() + 1).padStart(2, '0') + '-' +
String(date.getDate()).padStart(2, '0');
const localDateStr = date.getFullYear() + '-' +
String(date.getMonth() + 1).padStart(2, '0') + '-' +
String(date.getDate()).padStart(2, '0');
date = new Date(localDateStr + 'T00:00:00'); // Force local midnight
} else {
date = new Date(point.date);
}
// Check if date is valid
if (isNaN(date.getTime())) {
date = new Date();
@ -544,9 +550,9 @@ window.app = Vue.createApp({
} else {
date = new Date();
}
console.log('Formatted date:', date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }));
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric'
@ -557,22 +563,22 @@ window.app = Vue.createApp({
const sats = point.cumulative_sats || 0
return typeof sats === 'number' ? sats : parseFloat(sats) || 0
})
console.log('Final chart data:', { labels, cumulativeSats })
console.log('Labels array:', labels)
console.log('CumulativeSats array:', cumulativeSats)
// Validate data before creating chart
if (labels.length === 0 || cumulativeSats.length === 0) {
console.warn('No valid data for chart, skipping creation')
return
}
if (labels.length !== cumulativeSats.length) {
console.warn('Mismatched data arrays:', { labelsLength: labels.length, dataLength: cumulativeSats.length })
return
}
// Check for any invalid values in cumulativeSats
const hasInvalidValues = cumulativeSats.some(val => val === null || val === undefined || isNaN(val))
if (hasInvalidValues) {
@ -585,34 +591,34 @@ window.app = Vue.createApp({
createChart(labels, cumulativeSats) {
console.log('createChart called with loading state:', this.chartLoading)
if (!this.$refs.dcaChart) {
console.log('Chart ref not available for createChart')
return
}
// Skip if we're not in a loading state (indicates this is a stale call)
if (!this.chartLoading) {
console.log('Not in loading state, skipping createChart')
return
}
// Destroy existing chart
if (this.dcaChart) {
console.log('Destroying existing chart in createChart')
this.dcaChart.destroy()
this.dcaChart = null
}
const ctx = this.$refs.dcaChart.getContext('2d')
try {
// Create gradient for the area fill
const gradient = ctx.createLinearGradient(0, 0, 0, 300)
gradient.addColorStop(0, 'rgba(255, 149, 0, 0.4)')
gradient.addColorStop(0.5, 'rgba(255, 149, 0, 0.2)')
gradient.addColorStop(1, 'rgba(255, 149, 0, 0.05)')
// Small delay to ensure Chart.js is fully initialized
setTimeout(() => {
try {
@ -621,7 +627,7 @@ window.app = Vue.createApp({
console.log('Loading state changed during timeout, aborting chart creation')
return
}
this.dcaChart = new Chart(ctx, {
type: 'line',
data: {
@ -662,7 +668,7 @@ window.app = Vue.createApp({
cornerRadius: 8,
displayColors: false,
callbacks: {
title: function(context) {
title: function (context) {
return `📅 ${context[0].label}`
},
label: function (context) {
@ -741,10 +747,10 @@ window.app = Vue.createApp({
async created() {
try {
this.loading = true
// Check registration status first
await this.checkRegistrationStatus()
// Only load dashboard data if registered
if (this.isRegistered) {
await Promise.all([
@ -768,7 +774,7 @@ window.app = Vue.createApp({
console.log('Loading state:', this.loading)
console.log('Chart ref available:', !!this.$refs.dcaChart)
console.log('Analytics data available:', !!this.analyticsData)
if (this.analyticsData && this.$refs.dcaChart) {
console.log('Initializing chart from mounted hook')
this.initDCAChart()
@ -781,6 +787,14 @@ window.app = Vue.createApp({
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
}))
}
},

View file

@ -7,7 +7,7 @@
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>
<script src="{{ static_url_for('satmachineclient/static', path='js/index.js') }}"></script>
{% endblock %} {% block page %}
<div class="row q-col-gutter-md" id="dcaClient">
<div class="row q-col-gutter-md" id="vue">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<!-- Loading State -->
@ -542,49 +542,32 @@
label="Username (Optional)"
placeholder="Enter a friendly name"
hint="How you'd like to be identified in the system"
/>
></q-input>
<q-select
filled
dense
emit-value
v-model="registrationForm.selectedWallet"
:options="g.user.wallets"
option-label="name"
:options="walletOptions"
label="DCA Wallet *"
hint="Choose which wallet will receive your Bitcoin DCA purchases"
>
<template v-slot:option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section>
<q-item-label>
${scope.opt.name} (ID: ${scope.opt.id.substring(0, 8)}... • ${Math.round(scope.opt.balance_msat / 1000)} sats)
</q-item-label>
</q-item-section>
</q-item>
</template>
</q-select>
></q-select>
<q-select
filled
dense
emit-value
v-model="registrationForm.dca_mode"
:options="[
{ label: 'Flow Mode (Recommended)', value: 'flow', description: 'Proportional distribution based on your balance' },
{ label: 'Fixed Mode', value: 'fixed', description: 'Set daily purchase limits' }
{ label: 'Flow Mode (Recommended)', value: 'flow' },
{ label: 'Fixed Mode', value: 'fixed' }
]"
option-label="label"
option-value="value"
label="DCA Strategy *"
hint="Choose how your Bitcoin purchases will be distributed"
>
<template v-slot:option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section>
<q-item-label>${scope.opt.label} - ${scope.opt.description}</q-item-label>
</q-item-section>
</q-item>
</template>
</q-select>
></q-select>
<q-input
v-if="registrationForm.dca_mode === 'fixed'"
@ -598,7 +581,7 @@
:rules="[
val => registrationForm.dca_mode !== 'fixed' || (val && val > 0) || 'Daily limit is required for fixed mode'
]"
/>
></q-input>
<q-banner class="bg-blue-1 text-blue-9 q-mt-md">
<template v-slot:avatar>
@ -619,8 +602,7 @@
class="full-width"
size="lg"
>
<q-icon name="rocket_launch" class="q-mr-sm" />
Start My DCA Journey
🚀 Start My DCA Journey
</q-btn>
</div>
</q-form>