Compare commits

..

No commits in common. "8d442b7c6f5ef986b5646ec16e21322a2e919b92" and "315bcae4cacb0a3557ea662ca7f3629ed17efb6d" have entirely different histories.

5 changed files with 206 additions and 332 deletions

View file

@ -103,7 +103,7 @@ async def get_client_dashboard_summary(user_id: str) -> Optional[ClientDashboard
# Calculate metrics # Calculate metrics
total_invested = confirmed_deposits # Total invested = all confirmed deposits total_invested = confirmed_deposits # Total invested = all confirmed deposits
remaining_balance = confirmed_deposits - dca_spent # Remaining = deposits - DCA spending remaining_balance = confirmed_deposits - dca_spent # Remaining = deposits - DCA spending
avg_cost_basis = total_sats / dca_spent if dca_spent > 0 else 0 # Cost basis = sats / GTQ avg_cost_basis = total_sats / dca_spent if dca_spent > 0 else 0 # Cost basis = sats / fiat spent
# Calculate current fiat value of total sats # Calculate current fiat value of total sats
current_sats_fiat_value = 0.0 current_sats_fiat_value = 0.0
@ -248,7 +248,7 @@ async def get_client_analytics(user_id: str, time_range: str = "30d") -> Optiona
# Build cost basis history # Build cost basis history
cost_basis_history = [] cost_basis_history = []
for record in cost_basis_data: for record in cost_basis_data:
avg_cost_basis = record["cumulative_sats"] / record["cumulative_fiat"] if record["cumulative_fiat"] > 0 else 0 # Cost basis = sats / GTQ avg_cost_basis = record["cumulative_sats"] / record["cumulative_fiat"] if record["cumulative_fiat"] > 0 else 0
# Use transaction_date (which is COALESCE(transaction_time, created_at)) # Use transaction_date (which is COALESCE(transaction_time, created_at))
date_to_use = record["transaction_date"] date_to_use = record["transaction_date"]
if date_to_use is None: if date_to_use is None:
@ -515,7 +515,3 @@ async def get_client_by_user_id(user_id: str) -> Optional[dict]:
return dict(client) if client else None return dict(client) if client else None
except Exception: except Exception:
return None return None
# Removed get_active_lamassu_config - client should not access sensitive admin config
# Client limits are now fetched via secure public API endpoint

View file

@ -6,46 +6,16 @@ from typing import List, Optional
from pydantic import BaseModel from pydantic import BaseModel
# API Models for Client Dashboard (Frontend communication in GTQ) # Client Dashboard Data Models
class ClientDashboardSummaryAPI(BaseModel):
"""API model - client dashboard summary in GTQ"""
user_id: str
total_sats_accumulated: int
total_fiat_invested_gtq: float # Confirmed deposits in GTQ
pending_fiat_deposits_gtq: float # Pending deposits in GTQ
current_sats_fiat_value_gtq: float # Current fiat value of total sats in GTQ
average_cost_basis: float # Average sats per GTQ
current_fiat_balance_gtq: float # Available balance for DCA in GTQ
total_transactions: int
dca_mode: str # 'flow' or 'fixed'
dca_status: str # 'active' or 'inactive'
last_transaction_date: Optional[datetime]
currency: str = "GTQ"
class ClientTransactionAPI(BaseModel):
"""API model - client transaction in GTQ"""
id: str
amount_sats: int
amount_fiat_gtq: float # Amount in GTQ
exchange_rate: float
transaction_type: str # 'flow', 'fixed', 'manual'
status: str
created_at: datetime
transaction_time: Optional[datetime] = None # Original ATM transaction time
lamassu_transaction_id: Optional[str] = None
# Internal Models for Client Dashboard (Database storage in GTQ)
class ClientDashboardSummary(BaseModel): class ClientDashboardSummary(BaseModel):
"""Internal model - client dashboard summary stored in GTQ""" """Summary metrics for client dashboard overview"""
user_id: str user_id: str
total_sats_accumulated: int total_sats_accumulated: int
total_fiat_invested: float # Confirmed deposits in GTQ total_fiat_invested: int # Confirmed deposits
pending_fiat_deposits: float # Pending deposits awaiting confirmation in GTQ pending_fiat_deposits: int # Pending deposits awaiting confirmation
current_sats_fiat_value: float # Current fiat value of total sats in GTQ current_sats_fiat_value: float # Current fiat value of total sats
average_cost_basis: float # Average sats per GTQ average_cost_basis: float # Average sats per fiat unit
current_fiat_balance: float # Available balance for DCA in GTQ current_fiat_balance: int # Available balance for DCA
total_transactions: int total_transactions: int
dca_mode: str # 'flow' or 'fixed' dca_mode: str # 'flow' or 'fixed'
dca_status: str # 'active' or 'inactive' dca_status: str # 'active' or 'inactive'
@ -54,10 +24,10 @@ class ClientDashboardSummary(BaseModel):
class ClientTransaction(BaseModel): class ClientTransaction(BaseModel):
"""Internal model - client transaction stored in GTQ""" """Read-only view of client's DCA transactions"""
id: str id: str
amount_sats: int amount_sats: int
amount_fiat: float # Amount in GTQ (e.g., 150.75) amount_fiat: int
exchange_rate: float exchange_rate: float
transaction_type: str # 'flow', 'fixed', 'manual' transaction_type: str # 'flow', 'fixed', 'manual'
status: str status: str

View file

@ -1,5 +1,5 @@
window.app = Vue.createApp({ window.app = Vue.createApp({
el: '#vue', el: '#dcaClient',
mixins: [windowMixin], mixins: [windowMixin],
delimiters: ['${', '}'], delimiters: ['${', '}'],
data: function () { data: function () {
@ -7,6 +7,7 @@ window.app = Vue.createApp({
// Registration state // Registration state
isRegistered: false, isRegistered: false,
registrationChecked: false, registrationChecked: false,
showRegistrationDialog: false,
registrationForm: { registrationForm: {
selectedWallet: null, selectedWallet: null,
dca_mode: 'flow', dca_mode: 'flow',
@ -14,12 +15,6 @@ window.app = Vue.createApp({
username: '' username: ''
}, },
// Admin configuration
adminConfig: {
max_daily_limit_gtq: 2000,
currency: 'GTQ'
},
// Dashboard state // Dashboard state
dashboardData: null, dashboardData: null,
transactions: [], transactions: [],
@ -77,23 +72,6 @@ window.app = Vue.createApp({
}, },
methods: { 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 // Registration Methods
async checkRegistrationStatus() { async checkRegistrationStatus() {
try { try {
@ -107,9 +85,10 @@ window.app = Vue.createApp({
this.registrationChecked = true this.registrationChecked = true
if (!this.isRegistered) { if (!this.isRegistered) {
// Fetch current user info to get the username this.showRegistrationDialog = true
await this.loadCurrentUser() // Pre-fill username and default wallet if available
this.registrationForm.selectedWallet = this.g.user.wallets[0]?.id || null this.registrationForm.username = this.g.user.username || ''
this.registrationForm.selectedWallet = this.g.user.wallets[0] || null
} }
return data return data
@ -120,45 +99,24 @@ 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() { async registerClient() {
try { try {
// Prepare registration data using the form's username (already loaded from API) // Prepare registration data similar to the admin test client creation
const registrationData = { const registrationData = {
dca_mode: this.registrationForm.dca_mode, dca_mode: this.registrationForm.dca_mode,
fixed_mode_daily_limit: this.registrationForm.fixed_mode_daily_limit, fixed_mode_daily_limit: this.registrationForm.fixed_mode_daily_limit,
username: this.registrationForm.username || `user_${this.g.user.id.substring(0, 8)}` 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( const { data } = await LNbits.api.request(
'POST', 'POST',
'/satmachineclient/api/v1/register', '/satmachineclient/api/v1/register',
selectedWallet.adminkey, this.registrationForm.selectedWallet.adminkey,
registrationData registrationData
) )
this.isRegistered = true this.isRegistered = true
this.showRegistrationDialog = false
this.$q.notify({ this.$q.notify({
type: 'positive', type: 'positive',
@ -183,26 +141,24 @@ window.app = Vue.createApp({
// Dashboard Methods // Dashboard Methods
formatCurrency(amount) { formatCurrency(amount) {
if (!amount) return 'Q 0.00'; if (!amount) return 'Q 0.00';
// Amount is already in GTQ // Values are already in full currency units, not centavos
const gtqAmount = amount;
return new Intl.NumberFormat('es-GT', { return new Intl.NumberFormat('es-GT', {
style: 'currency', style: 'currency',
currency: 'GTQ', currency: 'GTQ',
}).format(gtqAmount); }).format(amount);
}, },
formatCurrencyWithCode(amount, currencyCode) { formatCurrencyWithCode(amount, currencyCode) {
if (!amount) return `${currencyCode} 0.00`; if (!amount) return `${currencyCode} 0.00`;
// Amount is already in GTQ // Format with the provided currency code
const currencyAmount = amount;
try { try {
return new Intl.NumberFormat('en-US', { return new Intl.NumberFormat('en-US', {
style: 'currency', style: 'currency',
currency: currencyCode, currency: currencyCode,
}).format(currencyAmount); }).format(amount);
} catch (error) { } catch (error) {
// Fallback if currency code is not supported // Fallback if currency code is not supported
return `${currencyCode} ${currencyAmount.toFixed(2)}`; return `${currencyCode} ${amount.toFixed(2)}`;
} }
}, },
@ -232,16 +188,10 @@ window.app = Vue.createApp({
formatSats(amount) { formatSats(amount) {
if (!amount) return '0 sats' if (!amount) return '0 sats'
const formatted = new Intl.NumberFormat('en-US').format(amount) const formatted = new Intl.NumberFormat('en-US').format(amount)
// Add some excitement for larger amounts with consistent 5x→2x progression // Add some excitement for larger amounts
if (amount >= 100000000) return formatted + ' sats 🏆' // Full coiner (1 BTC) if (amount >= 1000000) return formatted + ' sats 💎'
if (amount >= 50000000) return formatted + ' sats 🎆' // Bitcoin baron if (amount >= 100000) return formatted + ' sats 🚀'
if (amount >= 10000000) return formatted + ' sats 👑' // Bitcoin royalty if (amount >= 10000) return formatted + ' sats ⚡'
if (amount >= 5000000) return formatted + ' sats 🏆' // Verified bag holder
if (amount >= 1000000) return formatted + ' sats 🌟' // Millionaire
if (amount >= 500000) return formatted + ' sats 🔥' // Half million
if (amount >= 100000) return formatted + ' sats 🚀' // Getting serious
if (amount >= 50000) return formatted + ' sats ⚡' // Lightning quick
if (amount >= 10000) return formatted + ' sats 🎯' // First milestone
return formatted + ' sats' return formatted + ' sats'
}, },
@ -316,20 +266,15 @@ window.app = Vue.createApp({
}, },
getNextMilestone() { getNextMilestone() {
if (!this.dashboardData) return { target: 10000, name: '10k sats' } if (!this.dashboardData) return { target: 100000, name: '100k sats' }
const sats = this.dashboardData.total_sats_accumulated const sats = this.dashboardData.total_sats_accumulated
// Consistent 5x→2x progression pattern
if (sats < 10000) return { target: 10000, name: '10k sats' } 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 < 100000) return { target: 100000, name: '100k sats' }
if (sats < 500000) return { target: 500000, name: '500k sats' } if (sats < 500000) return { target: 500000, name: '500k sats' }
if (sats < 1000000) return { target: 1000000, name: '1M sats' } if (sats < 1000000) return { target: 1000000, name: '1M sats' }
if (sats < 5000000) return { target: 5000000, name: '5M sats' } if (sats < 2100000) return { target: 2100000, name: '2.1M sats' }
if (sats < 10000000) return { target: 10000000, name: '10M sats' } return { target: 21000000, name: '21M 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() { getMilestoneProgress() {
@ -797,10 +742,7 @@ window.app = Vue.createApp({
try { try {
this.loading = true this.loading = true
// Load client limits first // Check registration status first
await this.loadClientLimits()
// Check registration status
await this.checkRegistrationStatus() await this.checkRegistrationStatus()
// Only load dashboard data if registered // Only load dashboard data if registered
@ -839,14 +781,6 @@ window.app = Vue.createApp({
computed: { computed: {
hasData() { hasData() {
return this.dashboardData && !this.loading && this.isRegistered 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="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> <script src="{{ static_url_for('satmachineclient/static', path='js/index.js') }}"></script>
{% endblock %} {% block page %} {% endblock %} {% block page %}
<div class="row q-col-gutter-md" id="vue"> <div class="row q-col-gutter-md" id="dcaClient">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md"> <div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<!-- Loading State --> <!-- Loading State -->
@ -21,87 +21,19 @@
<!-- Error State --> <!-- Error State -->
<q-card v-if="error && !loading" class="bg-negative text-white"> <q-card v-if="error && !loading" class="bg-negative text-white">
<q-card-section> <q-card-section>
<q-icon name="error" class="q-mr-sm"></q-icon> <q-icon name="error" class="q-mr-sm" />
${error} ${error}
</q-card-section> </q-card-section>
</q-card> </q-card>
<!-- Registration Form Card --> <!-- Not Registered State -->
<q-card v-if="registrationChecked && !isRegistered && !loading" class="q-mb-md"> <q-card v-if="registrationChecked && !isRegistered && !loading" class="bg-orange-1">
<q-card-section> <q-card-section class="text-center">
<div class="text-center q-mb-lg"> <q-icon name="account_circle" size="3em" color="orange" />
<div> <div class="text-h6 q-mt-md text-orange-8">Welcome to Bitcoin DCA!</div>
<q-icon name="account_circle" size="4em" color="orange"></q-icon> <div class="text-body2 text-grey-7 q-mt-sm">
Please complete your registration to start your Dollar Cost Averaging journey.
</div> </div>
<div class="text-h5 q-mt-md text-orange-8">Welcome to DCA!</div>
<div class="text-body2 text-grey-7">Let's set up your Bitcoin Dollar Cost Averaging account</div>
</div>
<q-form @submit="registerClient" class="q-gutter-md">
<q-select
filled
dense
emit-value
v-model="registrationForm.selectedWallet"
:options="walletOptions"
label="DCA Wallet *"
hint="Choose which wallet will receive your Bitcoin DCA purchases"
></q-select>
<q-select
filled
dense
emit-value
v-model="registrationForm.dca_mode"
:options="[
{ 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"
></q-select>
<q-input
v-if="registrationForm.dca_mode === 'fixed'"
filled
dense
type="number"
v-model.number="registrationForm.fixed_mode_daily_limit"
label="Daily Limit (GTQ)"
placeholder="Enter daily purchase limit"
:hint="`Maximum amount to purchase per day (Admin limit: ${adminConfig.max_daily_limit_gtq} GTQ)`"
:rules="[
val => registrationForm.dca_mode !== 'fixed' || (val && val > 0) || 'Daily limit is required for fixed mode',
val => registrationForm.dca_mode !== 'fixed' || val <= adminConfig.max_daily_limit_gtq || `Daily limit cannot exceed ${adminConfig.max_daily_limit_gtq} GTQ (admin maximum)`
]"
></q-input>
<q-banner class="bg-blue-1 text-blue-9 q-mt-md">
<template v-slot:avatar>
<q-icon name="info" color="blue"></q-icon>
</template>
<div class="text-caption">
<strong>Flow Mode:</strong> Your Bitcoin purchases come at 0% fee when people cash ou at the machine.<br>
<strong>Fixed Mode:</strong> Set a daily limit for consistent Bitcoin accumulation.
</div>
</q-banner>
<div class="row q-mt-lg">
<q-btn
unelevated
color="primary"
type="submit"
:disable="!registrationForm.selectedWallet || !registrationForm.dca_mode"
class="full-width"
size="lg"
>
<q-icon name="flash_on" class="q-mr-sm"></q-icon>
Start My DCA Journey 🚀
</q-btn>
</div>
</q-form>
</q-card-section> </q-card-section>
</q-card> </q-card>
@ -247,7 +179,7 @@
<div v-if="dashboardData.pending_fiat_deposits > 0" class="q-mt-md"> <div v-if="dashboardData.pending_fiat_deposits > 0" class="q-mt-md">
<q-banner rounded class="bg-orange-1 text-orange-9"> <q-banner rounded class="bg-orange-1 text-orange-9">
<template v-slot:avatar> <template v-slot:avatar>
<q-icon name="schedule" color="orange" size="md"></q-icon> <q-icon name="schedule" color="orange" size="md" />
</template> </template>
<div class="text-subtitle2"> <div class="text-subtitle2">
<strong>${formatCurrency(dashboardData.pending_fiat_deposits)}</strong> ready to DCA <strong>${formatCurrency(dashboardData.pending_fiat_deposits)}</strong> ready to DCA
@ -431,7 +363,7 @@
<template v-slot:no-data="{ message }"> <template v-slot:no-data="{ message }">
<div class="full-width row flex-center q-pa-lg"> <div class="full-width row flex-center q-pa-lg">
<div class="text-center"> <div class="text-center">
<q-icon name="rocket_launch" size="3em" class="text-orange-5 q-mb-md"></q-icon> <q-icon name="rocket_launch" size="3em" class="text-orange-5 q-mb-md" />
<div class="text-h6 text-grey-7 q-mb-sm">${message}</div> <div class="text-h6 text-grey-7 q-mb-sm">${message}</div>
<div class="text-caption text-grey-5"> <div class="text-caption text-grey-5">
Visit your nearest Lamassu ATM to begin stacking sats automatically Visit your nearest Lamassu ATM to begin stacking sats automatically
@ -511,17 +443,6 @@
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item>
<q-item-section avatar>
<div v-if="dashboardData.total_sats_accumulated >= 50000" class="text-positive text-h6"></div>
<div v-else class="text-grey-5 text-h6"></div>
</q-item-section>
<q-item-section>
<q-item-label class="text-body2">50,000 sats</q-item-label>
<q-item-label caption>Lightning quick ⚡</q-item-label>
</q-item-section>
</q-item>
<q-item> <q-item>
<q-item-section avatar> <q-item-section avatar>
<div v-if="dashboardData.total_sats_accumulated >= 100000" class="text-positive text-h6"></div> <div v-if="dashboardData.total_sats_accumulated >= 100000" class="text-positive text-h6"></div>
@ -551,51 +472,7 @@
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label class="text-body2">1,000,000 sats</q-item-label> <q-item-label class="text-body2">1,000,000 sats</q-item-label>
<q-item-label caption>Millionaire! 🌟</q-item-label> <q-item-label caption>True HODLer 💎</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section avatar>
<div v-if="dashboardData.total_sats_accumulated >= 5000000" class="text-positive text-h6"></div>
<div v-else class="text-grey-5 text-h6"></div>
</q-item-section>
<q-item-section>
<q-item-label class="text-body2">5,000,000 sats</q-item-label>
<q-item-label caption>Verified Bag Holder 🏆</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section avatar>
<div v-if="dashboardData.total_sats_accumulated >= 10000000" class="text-positive text-h6"></div>
<div v-else class="text-grey-5 text-h6"></div>
</q-item-section>
<q-item-section>
<q-item-label class="text-body2">10,000,000 sats</q-item-label>
<q-item-label caption>Bitcoin royalty 👑</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section avatar>
<div v-if="dashboardData.total_sats_accumulated >= 50000000" class="text-positive text-h6"></div>
<div v-else class="text-grey-5 text-h6"></div>
</q-item-section>
<q-item-section>
<q-item-label class="text-body2">50,000,000 sats</q-item-label>
<q-item-label caption>Bitcoin baron 🎆</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section avatar>
<div v-if="dashboardData.total_sats_accumulated >= 100000000" class="text-positive text-h6"></div>
<div v-else class="text-grey-5 text-h6"></div>
</q-item-section>
<q-item-section>
<q-item-label class="text-body2">100,000,000 sats</q-item-label>
<q-item-label caption>Legendary Full coiner! 🏆</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
</q-list> </q-list>
@ -646,5 +523,106 @@
</q-card> </q-card>
</div> </div>
<!-- Registration Dialog -->
<q-dialog v-model="showRegistrationDialog" persistent position="top">
<q-card class="q-pa-lg" style="width: 500px; max-width: 90vw">
<div class="text-center q-mb-lg">
<q-icon name="account_circle" size="4em" color="orange" />
<div class="text-h5 q-mt-md text-orange-8">Welcome to DCA!</div>
<div class="text-body2 text-grey-7">Let's set up your Bitcoin Dollar Cost Averaging account</div>
</div>
<q-form @submit="registerClient" class="q-gutter-md">
<q-input
filled
dense
v-model.trim="registrationForm.username"
label="Username (Optional)"
placeholder="Enter a friendly name"
hint="How you'd like to be identified in the system"
/>
<q-select
filled
dense
v-model="registrationForm.selectedWallet"
:options="g.user.wallets"
option-label="name"
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
filled
dense
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' }
]"
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-input
v-if="registrationForm.dca_mode === 'fixed'"
filled
dense
type="number"
v-model.number="registrationForm.fixed_mode_daily_limit"
label="Daily Limit (GTQ)"
placeholder="Enter daily purchase limit in centavos"
hint="Maximum amount to purchase per day (in centavos: 1 GTQ = 100 centavos)"
:rules="[
val => registrationForm.dca_mode !== 'fixed' || (val && val > 0) || 'Daily limit is required for fixed mode'
]"
/>
<q-banner class="bg-blue-1 text-blue-9 q-mt-md">
<template v-slot:avatar>
<q-icon name="info" color="blue" />
</template>
<div class="text-caption">
<strong>Flow Mode:</strong> Your Bitcoin purchases are proportional to your deposit balance.<br>
<strong>Fixed Mode:</strong> Set a daily limit for consistent Bitcoin accumulation.
</div>
</q-banner>
<div class="row q-mt-lg">
<q-btn
unelevated
color="primary"
type="submit"
:disable="!registrationForm.selectedWallet || !registrationForm.dca_mode"
class="full-width"
size="lg"
>
<q-icon name="rocket_launch" class="q-mr-sm" />
Start My DCA Journey
</q-btn>
</div>
</q-form>
</q-card>
</q-dialog>
</div> </div>
{% endblock %} {% endblock %}

View file

@ -199,7 +199,7 @@ async def api_export_transactions(
writer.writerow([ writer.writerow([
tx.created_at.isoformat(), tx.created_at.isoformat(),
tx.amount_sats, tx.amount_sats,
tx.amount_fiat, # Amount already in GTQ tx.amount_fiat, # Values are already in full currency units
tx.exchange_rate, tx.exchange_rate,
tx.transaction_type, tx.transaction_type,
tx.status tx.status
@ -214,7 +214,3 @@ async def api_export_transactions(
) )
else: else:
return {"transactions": transactions} return {"transactions": transactions}
# Removed local client-limits endpoint
# Client should call admin extension's public endpoint directly