From 306549a6560d101153a65e00e223da350245d369 Mon Sep 17 00:00:00 2001 From: padreug Date: Thu, 26 Jun 2025 14:17:19 +0200 Subject: [PATCH 01/10] wrap q-icon in div to prevent text rendering problem --- templates/satmachineclient/index.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/templates/satmachineclient/index.html b/templates/satmachineclient/index.html index af2fa20..6bbc1b2 100644 --- a/templates/satmachineclient/index.html +++ b/templates/satmachineclient/index.html @@ -527,7 +527,9 @@
- +
+ +
Welcome to DCA!
Let's set up your Bitcoin Dollar Cost Averaging account
From 03179647ec7c0e38e6fa89872e85d559c5121196 Mon Sep 17 00:00:00 2001 From: padreug Date: Fri, 27 Jun 2025 00:28:49 +0200 Subject: [PATCH 02/10] 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. --- static/js/index.js | 134 ++++++++++++++------------ templates/satmachineclient/index.html | 40 +++----- 2 files changed, 85 insertions(+), 89 deletions(-) diff --git a/static/js/index.js b/static/js/index.js index c6e6209..42ee57c 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -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 + })) } }, diff --git a/templates/satmachineclient/index.html b/templates/satmachineclient/index.html index 6bbc1b2..b5600c1 100644 --- a/templates/satmachineclient/index.html +++ b/templates/satmachineclient/index.html @@ -7,7 +7,7 @@ {% endblock %} {% block page %} -
+
@@ -542,49 +542,32 @@ label="Username (Optional)" placeholder="Enter a friendly name" hint="How you'd like to be identified in the system" - /> + > - - + > - - + > + >
- Flow Mode: Your Bitcoin purchases are proportional to your deposit balance.
+ Flow Mode: Your Bitcoin purchases come at 0% fee when people cash ou at the machine.
Fixed Mode: Set a daily limit for consistent Bitcoin accumulation.
From 16db140bb6a574edcc6787c93652929db97f4d0c Mon Sep 17 00:00:00 2001 From: padreug Date: Fri, 27 Jun 2025 23:31:56 +0200 Subject: [PATCH 04/10] Enhance milestone rewards display: Add new milestones for 5M, 20M, and 100M sats with corresponding labels and icons. Update progress calculation to reflect these new thresholds in the dashboard. --- static/js/index.js | 14 ++++++++---- templates/satmachineclient/index.html | 33 +++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/static/js/index.js b/static/js/index.js index 42ee57c..2924342 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -195,9 +195,12 @@ window.app = Vue.createApp({ if (!amount) return '0 sats' const formatted = new Intl.NumberFormat('en-US').format(amount) // Add some excitement for larger amounts - if (amount >= 1000000) return formatted + ' sats 💎' - if (amount >= 100000) return formatted + ' sats 🚀' - if (amount >= 10000) return formatted + ' sats ⚡' + if (amount >= 100000000) return formatted + ' sats 🏆' // Full coiner (1 BTC) + if (amount >= 20000000) return formatted + ' sats 👑' // Bitcoin royalty + if (amount >= 5000000) return formatted + ' sats 🌟' // Rising star + if (amount >= 1000000) return formatted + ' sats 💎' // Diamond hands + if (amount >= 100000) return formatted + ' sats 🚀' // Rocket fuel + if (amount >= 10000) return formatted + ' sats ⚡' // Lightning return formatted + ' sats' }, @@ -280,7 +283,10 @@ window.app = Vue.createApp({ if (sats < 500000) return { target: 500000, name: '500k sats' } if (sats < 1000000) return { target: 1000000, name: '1M sats' } if (sats < 2100000) return { target: 2100000, name: '2.1M sats' } - return { target: 21000000, name: '21M sats' } + if (sats < 5000000) return { target: 5000000, name: '5M sats' } + if (sats < 20000000) return { target: 20000000, name: '20M sats' } + if (sats < 100000000) return { target: 100000000, name: '100M sats (1 BTC!)' } + return { target: 210000000, name: '210M sats (2.1 BTC)' } }, getMilestoneProgress() { diff --git a/templates/satmachineclient/index.html b/templates/satmachineclient/index.html index 3334efb..19097e5 100644 --- a/templates/satmachineclient/index.html +++ b/templates/satmachineclient/index.html @@ -475,6 +475,39 @@ True HODLer 💎 + + + +
+
+
+ + 5,000,000 sats + Rising star 🌟 + +
+ + + +
+
+
+ + 20,000,000 sats + Bitcoin royalty 👑 + +
+ + + +
+
+
+ + 100,000,000 sats + Full coiner! 🏆 + +
From 340dc22c20ca222ea7742dadc6ebddc95db684e1 Mon Sep 17 00:00:00 2001 From: padreug Date: Fri, 27 Jun 2025 23:21:36 +0200 Subject: [PATCH 05/10] Refactor client configuration access: Remove direct access to sensitive admin config and local client-limits endpoint. Implement fetching of client limits via a secure public API. Update registration form to reflect changes and enhance user experience. --- crud.py | 6 +- static/js/index.js | 55 ++++++-- templates/satmachineclient/index.html | 173 ++++++++++++-------------- views_api.py | 4 + 4 files changed, 134 insertions(+), 104 deletions(-) diff --git a/crud.py b/crud.py index 982594a..79a31dd 100644 --- a/crud.py +++ b/crud.py @@ -514,4 +514,8 @@ async def get_client_by_user_id(user_id: str) -> Optional[dict]: ) return dict(client) if client else None except Exception: - return None \ No newline at end of file + return None + + +# Removed get_active_lamassu_config - client should not access sensitive admin config +# Client limits are now fetched via secure public API endpoint \ No newline at end of file diff --git a/static/js/index.js b/static/js/index.js index 2924342..f0f3c5f 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -7,7 +7,6 @@ window.app = Vue.createApp({ // Registration state isRegistered: false, registrationChecked: false, - showRegistrationDialog: false, registrationForm: { selectedWallet: null, dca_mode: 'flow', @@ -15,6 +14,12 @@ window.app = Vue.createApp({ username: '' }, + // Admin configuration + adminConfig: { + max_daily_limit_gtq: 2000, + currency: 'GTQ' + }, + // Dashboard state dashboardData: null, transactions: [], @@ -72,6 +77,23 @@ window.app = Vue.createApp({ }, methods: { + // Configuration Methods + async loadClientLimits() { + try { + const { data } = await LNbits.api.request( + 'GET', + '/satmachineadmin/api/v1/dca/client-limits' + // No authentication required - public endpoint with safe data only + ) + + this.adminConfig = data + console.log('Client limits loaded:', this.adminConfig) + } catch (error) { + console.error('Error loading client limits:', error) + // Keep default values if client limits fail to load + } + }, + // Registration Methods async checkRegistrationStatus() { try { @@ -85,9 +107,8 @@ window.app = Vue.createApp({ this.registrationChecked = true if (!this.isRegistered) { - this.showRegistrationDialog = true - // Pre-fill username and default wallet if available - this.registrationForm.username = this.g.user.username || '' + // Fetch current user info to get the username + await this.loadCurrentUser() this.registrationForm.selectedWallet = this.g.user.wallets[0]?.id || null } @@ -99,13 +120,29 @@ window.app = Vue.createApp({ } }, + async loadCurrentUser() { + try { + const { data } = await LNbits.api.getAuthenticatedUser() + + // Set username from API response with priority: display_name > username > email > fallback + const username = data.extra?.display_name || data.username || data.email + this.registrationForm.username = (username !== null && username !== undefined && username !== '') + ? username + : `user_${this.g.user.id.substring(0, 8)}` + } catch (error) { + console.error('Error loading current user:', error) + // Fallback to generated username + this.registrationForm.username = `user_${this.g.user.id.substring(0, 8)}` + } + }, + async registerClient() { try { - // Prepare registration data similar to the admin test client creation + // Prepare registration data using the form's username (already loaded from API) const registrationData = { dca_mode: this.registrationForm.dca_mode, fixed_mode_daily_limit: this.registrationForm.fixed_mode_daily_limit, - username: this.registrationForm.username || this.g.user.username || `user_${this.g.user.id.substring(0, 8)}` + username: this.registrationForm.username || `user_${this.g.user.id.substring(0, 8)}` } // Find the selected wallet object to get the adminkey @@ -122,7 +159,6 @@ window.app = Vue.createApp({ ) this.isRegistered = true - this.showRegistrationDialog = false this.$q.notify({ type: 'positive', @@ -754,7 +790,10 @@ window.app = Vue.createApp({ try { this.loading = true - // Check registration status first + // Load client limits first + await this.loadClientLimits() + + // Check registration status await this.checkRegistrationStatus() // Only load dashboard data if registered diff --git a/templates/satmachineclient/index.html b/templates/satmachineclient/index.html index 19097e5..a36b596 100644 --- a/templates/satmachineclient/index.html +++ b/templates/satmachineclient/index.html @@ -21,19 +21,87 @@ - + ${error} - - - - -
Welcome to Bitcoin DCA!
-
- Please complete your registration to start your Dollar Cost Averaging journey. + + + +
+
+ +
+
Welcome to DCA!
+
Let's set up your Bitcoin Dollar Cost Averaging account
+ + + + + + + + + + +
+ Flow Mode: Your Bitcoin purchases come at 0% fee when people cash ou at the machine.
+ Fixed Mode: Set a daily limit for consistent Bitcoin accumulation. +
+
+ +
+ + + Start My DCA Journey 🚀 + +
+
@@ -179,7 +247,7 @@
${formatCurrency(dashboardData.pending_fiat_deposits)} ready to DCA @@ -363,7 +431,7 @@