update and fix
This commit is contained in:
parent
c3adc37d84
commit
234daebef7
3 changed files with 473 additions and 115 deletions
324
crud.py
324
crud.py
|
|
@ -197,114 +197,228 @@ async def get_client_transactions(
|
||||||
async def get_client_analytics(user_id: str, time_range: str = "30d") -> Optional[ClientAnalytics]:
|
async def get_client_analytics(user_id: str, time_range: str = "30d") -> Optional[ClientAnalytics]:
|
||||||
"""Get client performance analytics"""
|
"""Get client performance analytics"""
|
||||||
|
|
||||||
# Get client ID
|
try:
|
||||||
client = await db.fetchone(
|
from datetime import datetime
|
||||||
"SELECT id FROM satmachineadmin.dca_clients WHERE user_id = :user_id",
|
|
||||||
{"user_id": user_id}
|
# Get client ID
|
||||||
)
|
client = await db.fetchone(
|
||||||
|
"SELECT id FROM satmachineadmin.dca_clients WHERE user_id = :user_id",
|
||||||
|
{"user_id": user_id}
|
||||||
|
)
|
||||||
|
|
||||||
|
if not client:
|
||||||
|
print(f"No client found for user_id: {user_id}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
print(f"Found client {client['id']} for user {user_id}, loading analytics for time_range: {time_range}")
|
||||||
|
|
||||||
if not client:
|
# Calculate date range
|
||||||
return None
|
if time_range == "7d":
|
||||||
|
start_date = datetime.now() - timedelta(days=7)
|
||||||
# Calculate date range
|
elif time_range == "30d":
|
||||||
if time_range == "7d":
|
start_date = datetime.now() - timedelta(days=30)
|
||||||
start_date = datetime.now() - timedelta(days=7)
|
elif time_range == "90d":
|
||||||
elif time_range == "30d":
|
start_date = datetime.now() - timedelta(days=90)
|
||||||
start_date = datetime.now() - timedelta(days=30)
|
elif time_range == "1y":
|
||||||
elif time_range == "90d":
|
start_date = datetime.now() - timedelta(days=365)
|
||||||
start_date = datetime.now() - timedelta(days=90)
|
else: # "all"
|
||||||
elif time_range == "1y":
|
start_date = datetime(2020, 1, 1) # Arbitrary early date
|
||||||
start_date = datetime.now() - timedelta(days=365)
|
|
||||||
else: # "all"
|
# Get cost basis history (running average)
|
||||||
start_date = datetime(2020, 1, 1) # Arbitrary early date
|
cost_basis_data = await db.fetchall(
|
||||||
|
"""
|
||||||
# Get cost basis history (running average)
|
SELECT
|
||||||
cost_basis_data = await db.fetchall(
|
COALESCE(transaction_time, created_at) as transaction_date,
|
||||||
"""
|
amount_sats,
|
||||||
SELECT
|
amount_fiat,
|
||||||
created_at,
|
exchange_rate,
|
||||||
amount_sats,
|
SUM(amount_sats) OVER (ORDER BY COALESCE(transaction_time, created_at)) as cumulative_sats,
|
||||||
amount_fiat,
|
SUM(amount_fiat) OVER (ORDER BY COALESCE(transaction_time, created_at)) as cumulative_fiat
|
||||||
exchange_rate,
|
FROM satmachineadmin.dca_payments
|
||||||
SUM(amount_sats) OVER (ORDER BY created_at) as cumulative_sats,
|
WHERE client_id = :client_id
|
||||||
SUM(amount_fiat) OVER (ORDER BY created_at) as cumulative_fiat
|
AND status = 'confirmed'
|
||||||
FROM satmachineadmin.dca_payments
|
AND COALESCE(transaction_time, created_at) IS NOT NULL
|
||||||
WHERE client_id = :client_id
|
AND COALESCE(transaction_time, created_at) >= :start_date
|
||||||
AND status = 'confirmed'
|
ORDER BY COALESCE(transaction_time, created_at)
|
||||||
AND created_at >= :start_date
|
""",
|
||||||
ORDER BY created_at
|
{"client_id": client["id"], "start_date": start_date}
|
||||||
""",
|
)
|
||||||
{"client_id": client["id"], "start_date": start_date}
|
|
||||||
)
|
# Build cost basis history
|
||||||
|
cost_basis_history = []
|
||||||
# Build cost basis history
|
for record in cost_basis_data:
|
||||||
cost_basis_history = []
|
avg_cost_basis = record["cumulative_sats"] / record["cumulative_fiat"] if record["cumulative_fiat"] > 0 else 0
|
||||||
for record in cost_basis_data:
|
# Use transaction_date (which is COALESCE(transaction_time, created_at))
|
||||||
avg_cost_basis = record["cumulative_sats"] / record["cumulative_fiat"] if record["cumulative_fiat"] > 0 else 0
|
date_to_use = record["transaction_date"]
|
||||||
cost_basis_history.append({
|
if date_to_use is None:
|
||||||
"date": record["created_at"].isoformat(),
|
print(f"Warning: Null date in cost basis data, skipping record")
|
||||||
"average_cost_basis": avg_cost_basis,
|
continue
|
||||||
"cumulative_sats": record["cumulative_sats"],
|
elif hasattr(date_to_use, 'isoformat'):
|
||||||
"cumulative_fiat": record["cumulative_fiat"]
|
# This is a datetime object
|
||||||
})
|
date_str = date_to_use.isoformat()
|
||||||
|
elif hasattr(date_to_use, 'strftime'):
|
||||||
# Get accumulation timeline (daily/weekly aggregation)
|
# This is a date object
|
||||||
accumulation_data = await db.fetchall(
|
date_str = date_to_use.strftime('%Y-%m-%d')
|
||||||
"""
|
elif isinstance(date_to_use, (int, float)):
|
||||||
SELECT
|
# This might be a Unix timestamp - check if it's in a reasonable range
|
||||||
DATE(created_at) as date,
|
timestamp = float(date_to_use)
|
||||||
SUM(amount_sats) as daily_sats,
|
# Check if this looks like a timestamp (between 1970 and 2100)
|
||||||
SUM(amount_fiat) as daily_fiat,
|
if 0 < timestamp < 4102444800: # Jan 1, 2100
|
||||||
COUNT(*) as daily_transactions
|
# Could be seconds or milliseconds
|
||||||
FROM satmachineadmin.dca_payments
|
if timestamp > 1000000000000: # Likely milliseconds
|
||||||
WHERE client_id = :client_id
|
timestamp = timestamp / 1000
|
||||||
AND status = 'confirmed'
|
date_str = datetime.fromtimestamp(timestamp).isoformat()
|
||||||
AND created_at >= :start_date
|
else:
|
||||||
GROUP BY DATE(created_at)
|
# Not a timestamp, treat as string
|
||||||
ORDER BY date
|
date_str = str(date_to_use)
|
||||||
""",
|
print(f"Warning: Numeric date value out of timestamp range: {date_to_use}")
|
||||||
{"client_id": client["id"], "start_date": start_date}
|
elif isinstance(date_to_use, str) and date_to_use.isdigit():
|
||||||
)
|
# This is a numeric string - might be a timestamp
|
||||||
|
timestamp = float(date_to_use)
|
||||||
accumulation_timeline = [
|
# Check if this looks like a timestamp
|
||||||
{
|
if 0 < timestamp < 4102444800: # Jan 1, 2100
|
||||||
"date": record["date"],
|
# Could be seconds or milliseconds
|
||||||
"sats": record["daily_sats"],
|
if timestamp > 1000000000000: # Likely milliseconds
|
||||||
"fiat": record["daily_fiat"],
|
timestamp = timestamp / 1000
|
||||||
"transactions": record["daily_transactions"]
|
date_str = datetime.fromtimestamp(timestamp).isoformat()
|
||||||
|
else:
|
||||||
|
# Not a timestamp, treat as string
|
||||||
|
date_str = str(date_to_use)
|
||||||
|
print(f"Warning: Numeric date string out of timestamp range: {date_to_use}")
|
||||||
|
else:
|
||||||
|
# Convert string representation to proper format
|
||||||
|
date_str = str(date_to_use)
|
||||||
|
print(f"Warning: Unexpected date format: {date_to_use} (type: {type(date_to_use)})")
|
||||||
|
|
||||||
|
cost_basis_history.append({
|
||||||
|
"date": date_str,
|
||||||
|
"average_cost_basis": avg_cost_basis,
|
||||||
|
"cumulative_sats": record["cumulative_sats"],
|
||||||
|
"cumulative_fiat": record["cumulative_fiat"]
|
||||||
|
})
|
||||||
|
|
||||||
|
# Get accumulation timeline (daily/weekly aggregation)
|
||||||
|
accumulation_data = await db.fetchall(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
DATE(COALESCE(transaction_time, created_at)) as date,
|
||||||
|
SUM(amount_sats) as daily_sats,
|
||||||
|
SUM(amount_fiat) as daily_fiat,
|
||||||
|
COUNT(*) as daily_transactions
|
||||||
|
FROM satmachineadmin.dca_payments
|
||||||
|
WHERE client_id = :client_id
|
||||||
|
AND status = 'confirmed'
|
||||||
|
AND COALESCE(transaction_time, created_at) IS NOT NULL
|
||||||
|
AND COALESCE(transaction_time, created_at) >= :start_date
|
||||||
|
GROUP BY DATE(COALESCE(transaction_time, created_at))
|
||||||
|
ORDER BY date
|
||||||
|
""",
|
||||||
|
{"client_id": client["id"], "start_date": start_date}
|
||||||
|
)
|
||||||
|
|
||||||
|
accumulation_timeline = []
|
||||||
|
for record in accumulation_data:
|
||||||
|
# Handle date conversion safely
|
||||||
|
date_value = record["date"]
|
||||||
|
if date_value is None:
|
||||||
|
print(f"Warning: Null date in accumulation data, skipping record")
|
||||||
|
continue
|
||||||
|
elif hasattr(date_value, 'isoformat'):
|
||||||
|
# This is a datetime object
|
||||||
|
date_str = date_value.isoformat()
|
||||||
|
elif hasattr(date_value, 'strftime'):
|
||||||
|
# This is a date object (from DATE() function)
|
||||||
|
date_str = date_value.strftime('%Y-%m-%d')
|
||||||
|
elif isinstance(date_value, (int, float)):
|
||||||
|
# This might be a Unix timestamp - check if it's in a reasonable range
|
||||||
|
timestamp = float(date_value)
|
||||||
|
# Check if this looks like a timestamp (between 1970 and 2100)
|
||||||
|
if 0 < timestamp < 4102444800: # Jan 1, 2100
|
||||||
|
# Could be seconds or milliseconds
|
||||||
|
if timestamp > 1000000000000: # Likely milliseconds
|
||||||
|
timestamp = timestamp / 1000
|
||||||
|
date_str = datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d')
|
||||||
|
else:
|
||||||
|
# Not a timestamp, treat as string
|
||||||
|
date_str = str(date_value)
|
||||||
|
print(f"Warning: Numeric accumulation date out of timestamp range: {date_value}")
|
||||||
|
elif isinstance(date_value, str) and date_value.isdigit():
|
||||||
|
# This is a numeric string - might be a timestamp
|
||||||
|
timestamp = float(date_value)
|
||||||
|
# Check if this looks like a timestamp
|
||||||
|
if 0 < timestamp < 4102444800: # Jan 1, 2100
|
||||||
|
# Could be seconds or milliseconds
|
||||||
|
if timestamp > 1000000000000: # Likely milliseconds
|
||||||
|
timestamp = timestamp / 1000
|
||||||
|
date_str = datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d')
|
||||||
|
else:
|
||||||
|
# Not a timestamp, treat as string
|
||||||
|
date_str = str(date_value)
|
||||||
|
print(f"Warning: Numeric accumulation date string out of timestamp range: {date_value}")
|
||||||
|
else:
|
||||||
|
# Convert string representation to proper format
|
||||||
|
date_str = str(date_value)
|
||||||
|
print(f"Warning: Unexpected accumulation date format: {date_value} (type: {type(date_value)})")
|
||||||
|
|
||||||
|
accumulation_timeline.append({
|
||||||
|
"date": date_str,
|
||||||
|
"sats": record["daily_sats"],
|
||||||
|
"fiat": record["daily_fiat"],
|
||||||
|
"transactions": record["daily_transactions"]
|
||||||
|
})
|
||||||
|
|
||||||
|
# Get transaction frequency metrics
|
||||||
|
frequency_stats = await db.fetchone(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as total_transactions,
|
||||||
|
AVG(amount_sats) as avg_sats_per_tx,
|
||||||
|
AVG(amount_fiat) as avg_fiat_per_tx,
|
||||||
|
MIN(COALESCE(transaction_time, created_at)) as first_tx,
|
||||||
|
MAX(COALESCE(transaction_time, created_at)) as last_tx
|
||||||
|
FROM satmachineadmin.dca_payments
|
||||||
|
WHERE client_id = :client_id AND status = 'confirmed'
|
||||||
|
""",
|
||||||
|
{"client_id": client["id"]}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build transaction frequency with safe date handling
|
||||||
|
transaction_frequency = {
|
||||||
|
"total_transactions": frequency_stats["total_transactions"] if frequency_stats else 0,
|
||||||
|
"avg_sats_per_transaction": frequency_stats["avg_sats_per_tx"] if frequency_stats else 0,
|
||||||
|
"avg_fiat_per_transaction": frequency_stats["avg_fiat_per_tx"] if frequency_stats else 0,
|
||||||
|
"first_transaction": None,
|
||||||
|
"last_transaction": None
|
||||||
}
|
}
|
||||||
for record in accumulation_data
|
|
||||||
]
|
# Handle first_tx date safely
|
||||||
|
if frequency_stats and frequency_stats["first_tx"]:
|
||||||
|
first_tx = frequency_stats["first_tx"]
|
||||||
|
if hasattr(first_tx, 'isoformat'):
|
||||||
|
transaction_frequency["first_transaction"] = first_tx.isoformat()
|
||||||
|
else:
|
||||||
|
transaction_frequency["first_transaction"] = str(first_tx)
|
||||||
|
|
||||||
|
# Handle last_tx date safely
|
||||||
|
if frequency_stats and frequency_stats["last_tx"]:
|
||||||
|
last_tx = frequency_stats["last_tx"]
|
||||||
|
if hasattr(last_tx, 'isoformat'):
|
||||||
|
transaction_frequency["last_transaction"] = last_tx.isoformat()
|
||||||
|
else:
|
||||||
|
transaction_frequency["last_transaction"] = str(last_tx)
|
||||||
|
|
||||||
# Get transaction frequency metrics
|
return ClientAnalytics(
|
||||||
frequency_stats = await db.fetchone(
|
user_id=user_id,
|
||||||
"""
|
cost_basis_history=cost_basis_history,
|
||||||
SELECT
|
accumulation_timeline=accumulation_timeline,
|
||||||
COUNT(*) as total_transactions,
|
transaction_frequency=transaction_frequency
|
||||||
AVG(amount_sats) as avg_sats_per_tx,
|
)
|
||||||
AVG(amount_fiat) as avg_fiat_per_tx,
|
|
||||||
MIN(created_at) as first_tx,
|
except Exception as e:
|
||||||
MAX(created_at) as last_tx
|
print(f"Error in get_client_analytics for user {user_id}: {str(e)}")
|
||||||
FROM satmachineadmin.dca_payments
|
import traceback
|
||||||
WHERE client_id = :client_id AND status = 'confirmed'
|
traceback.print_exc()
|
||||||
""",
|
return None
|
||||||
{"client_id": client["id"]}
|
|
||||||
)
|
|
||||||
|
|
||||||
transaction_frequency = {
|
|
||||||
"total_transactions": frequency_stats["total_transactions"] if frequency_stats else 0,
|
|
||||||
"avg_sats_per_transaction": frequency_stats["avg_sats_per_tx"] if frequency_stats else 0,
|
|
||||||
"avg_fiat_per_transaction": frequency_stats["avg_fiat_per_tx"] if frequency_stats else 0,
|
|
||||||
"first_transaction": frequency_stats["first_tx"].isoformat() if frequency_stats and frequency_stats["first_tx"] else None,
|
|
||||||
"last_transaction": frequency_stats["last_tx"].isoformat() if frequency_stats and frequency_stats["last_tx"] else None
|
|
||||||
}
|
|
||||||
|
|
||||||
return ClientAnalytics(
|
|
||||||
user_id=user_id,
|
|
||||||
cost_basis_history=cost_basis_history,
|
|
||||||
accumulation_timeline=accumulation_timeline,
|
|
||||||
transaction_frequency=transaction_frequency
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_client_by_user_id(user_id: str):
|
async def get_client_by_user_id(user_id: str):
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,10 @@ window.app = Vue.createApp({
|
||||||
descending: true,
|
descending: true,
|
||||||
page: 1,
|
page: 1,
|
||||||
rowsPerPage: 10
|
rowsPerPage: 10
|
||||||
}
|
},
|
||||||
|
chartTimeRange: '30d',
|
||||||
|
dcaChart: null,
|
||||||
|
analyticsData: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -81,14 +84,24 @@ window.app = Vue.createApp({
|
||||||
|
|
||||||
formatDate(dateString) {
|
formatDate(dateString) {
|
||||||
if (!dateString) return ''
|
if (!dateString) return ''
|
||||||
return new Date(dateString).toLocaleDateString()
|
const date = new Date(dateString)
|
||||||
|
if (isNaN(date.getTime())) {
|
||||||
|
console.warn('Invalid date string:', dateString)
|
||||||
|
return 'Invalid Date'
|
||||||
|
}
|
||||||
|
return date.toLocaleDateString()
|
||||||
},
|
},
|
||||||
|
|
||||||
formatTime(dateString) {
|
formatTime(dateString) {
|
||||||
if (!dateString) return ''
|
if (!dateString) return ''
|
||||||
return new Date(dateString).toLocaleTimeString('en-US', {
|
const date = new Date(dateString)
|
||||||
hour: '2-digit',
|
if (isNaN(date.getTime())) {
|
||||||
minute: '2-digit'
|
console.warn('Invalid time string:', dateString)
|
||||||
|
return 'Invalid Time'
|
||||||
|
}
|
||||||
|
return date.toLocaleTimeString('en-US', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -104,7 +117,7 @@ window.app = Vue.createApp({
|
||||||
|
|
||||||
async loadDashboardData() {
|
async loadDashboardData() {
|
||||||
try {
|
try {
|
||||||
const {data} = await LNbits.api.request(
|
const { data } = await LNbits.api.request(
|
||||||
'GET',
|
'GET',
|
||||||
'/satmachineclient/api/v1/dashboard/summary',
|
'/satmachineclient/api/v1/dashboard/summary',
|
||||||
this.g.user.wallets[0].inkey
|
this.g.user.wallets[0].inkey
|
||||||
|
|
@ -118,11 +131,19 @@ window.app = Vue.createApp({
|
||||||
|
|
||||||
async loadTransactions() {
|
async loadTransactions() {
|
||||||
try {
|
try {
|
||||||
const {data} = await LNbits.api.request(
|
const { data } = await LNbits.api.request(
|
||||||
'GET',
|
'GET',
|
||||||
'/satmachineclient/api/v1/dashboard/transactions?limit=50',
|
'/satmachineclient/api/v1/dashboard/transactions?limit=50',
|
||||||
this.g.user.wallets[0].inkey
|
this.g.user.wallets[0].inkey
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Debug: Log the first transaction to see date format
|
||||||
|
if (data.length > 0) {
|
||||||
|
console.log('Sample transaction data:', data[0])
|
||||||
|
console.log('transaction_time:', data[0].transaction_time)
|
||||||
|
console.log('created_at:', data[0].created_at)
|
||||||
|
}
|
||||||
|
|
||||||
// Sort by most recent first and store
|
// Sort by most recent first and store
|
||||||
this.transactions = data.sort((a, b) => {
|
this.transactions = data.sort((a, b) => {
|
||||||
const dateA = new Date(a.transaction_time || a.created_at)
|
const dateA = new Date(a.transaction_time || a.created_at)
|
||||||
|
|
@ -167,7 +188,7 @@ window.app = Vue.createApp({
|
||||||
getNextMilestone() {
|
getNextMilestone() {
|
||||||
if (!this.dashboardData) return { target: 100000, name: '100k sats' }
|
if (!this.dashboardData) return { target: 100000, name: '100k sats' }
|
||||||
const sats = this.dashboardData.total_sats_accumulated
|
const sats = this.dashboardData.total_sats_accumulated
|
||||||
|
|
||||||
if (sats < 10000) return { target: 10000, name: '10k sats' }
|
if (sats < 10000) return { target: 10000, name: '10k 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' }
|
||||||
|
|
@ -180,10 +201,195 @@ window.app = Vue.createApp({
|
||||||
if (!this.dashboardData) return 0
|
if (!this.dashboardData) return 0
|
||||||
const sats = this.dashboardData.total_sats_accumulated
|
const sats = this.dashboardData.total_sats_accumulated
|
||||||
const milestone = this.getNextMilestone()
|
const milestone = this.getNextMilestone()
|
||||||
|
|
||||||
// Show total progress toward the next milestone (from 0)
|
// Show total progress toward the next milestone (from 0)
|
||||||
const progress = (sats / milestone.target) * 100
|
const progress = (sats / milestone.target) * 100
|
||||||
return Math.min(Math.max(progress, 0), 100)
|
return Math.min(Math.max(progress, 0), 100)
|
||||||
|
},
|
||||||
|
async loadChartData() {
|
||||||
|
try {
|
||||||
|
const { data } = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
`/satmachineclient/api/v1/dashboard/analytics?time_range=${this.chartTimeRange}`,
|
||||||
|
this.g.user.wallets[0].inkey
|
||||||
|
)
|
||||||
|
|
||||||
|
// Debug: Log analytics data
|
||||||
|
console.log('Analytics data received:', data)
|
||||||
|
if (data && data.cost_basis_history && data.cost_basis_history.length > 0) {
|
||||||
|
console.log('Sample cost basis point:', data.cost_basis_history[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
this.analyticsData = data
|
||||||
|
// Use nextTick to ensure DOM is ready
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.initDCAChart()
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading chart data:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
initDCAChart() {
|
||||||
|
console.log('initDCAChart called')
|
||||||
|
console.log('analyticsData:', this.analyticsData)
|
||||||
|
console.log('dcaChart ref:', this.$refs.dcaChart)
|
||||||
|
|
||||||
|
if (!this.analyticsData) {
|
||||||
|
console.log('No analytics data available')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.$refs.dcaChart) {
|
||||||
|
console.log('No chart ref available')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if Chart.js is loaded
|
||||||
|
if (typeof Chart === 'undefined') {
|
||||||
|
console.error('Chart.js is not loaded')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy existing chart
|
||||||
|
if (this.dcaChart) {
|
||||||
|
this.dcaChart.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx = this.$refs.dcaChart.getContext('2d')
|
||||||
|
|
||||||
|
// Use accumulation_timeline data which is already aggregated by day
|
||||||
|
const timelineData = this.analyticsData.accumulation_timeline || []
|
||||||
|
|
||||||
|
console.log('Timeline data:', timelineData)
|
||||||
|
console.log('Timeline data length:', timelineData.length)
|
||||||
|
|
||||||
|
if (timelineData.length === 0) {
|
||||||
|
console.log('No timeline data available, falling back to cost basis data')
|
||||||
|
// Fallback to cost_basis_history if no timeline data
|
||||||
|
const costBasisData = this.analyticsData.cost_basis_history || []
|
||||||
|
if (costBasisData.length === 0) {
|
||||||
|
console.log('No chart data available')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group cost basis data by date to avoid duplicates
|
||||||
|
const groupedData = new Map()
|
||||||
|
costBasisData.forEach(point => {
|
||||||
|
const dateStr = new Date(point.date).toDateString()
|
||||||
|
if (!groupedData.has(dateStr)) {
|
||||||
|
groupedData.set(dateStr, point)
|
||||||
|
} else {
|
||||||
|
// Use the latest cumulative values for the same date
|
||||||
|
const existing = groupedData.get(dateStr)
|
||||||
|
if (point.cumulative_sats > existing.cumulative_sats) {
|
||||||
|
groupedData.set(dateStr, point)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const chartData = Array.from(groupedData.values()).sort((a, b) =>
|
||||||
|
new Date(a.date).getTime() - new Date(b.date).getTime()
|
||||||
|
)
|
||||||
|
|
||||||
|
const labels = chartData.map(point => {
|
||||||
|
const date = new Date(point.date)
|
||||||
|
return date.toLocaleDateString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const cumulativeSats = chartData.map(point => point.cumulative_sats)
|
||||||
|
|
||||||
|
this.createChart(labels, cumulativeSats)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate running totals for timeline data
|
||||||
|
let runningSats = 0
|
||||||
|
const labels = []
|
||||||
|
const cumulativeSats = []
|
||||||
|
|
||||||
|
timelineData.forEach(point => {
|
||||||
|
runningSats += point.sats
|
||||||
|
|
||||||
|
const date = new Date(point.date)
|
||||||
|
if (!isNaN(date.getTime())) {
|
||||||
|
labels.push(date.toLocaleDateString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric'
|
||||||
|
}))
|
||||||
|
cumulativeSats.push(runningSats)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.createChart(labels, cumulativeSats)
|
||||||
|
},
|
||||||
|
|
||||||
|
createChart(labels, cumulativeSats) {
|
||||||
|
const ctx = this.$refs.dcaChart.getContext('2d')
|
||||||
|
|
||||||
|
this.dcaChart = new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [{
|
||||||
|
label: 'Total Sats Accumulated',
|
||||||
|
data: cumulativeSats,
|
||||||
|
borderColor: '#FF9500', // Bitcoin orange
|
||||||
|
backgroundColor: 'rgba(255, 149, 0, 0.1)',
|
||||||
|
borderWidth: 3,
|
||||||
|
fill: true,
|
||||||
|
tension: 0.4,
|
||||||
|
pointBackgroundColor: '#FF9500',
|
||||||
|
pointBorderColor: '#FF9500',
|
||||||
|
pointRadius: 4,
|
||||||
|
pointHoverRadius: 6
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false // Hide legend to keep it clean
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
callbacks: {
|
||||||
|
label: function (context) {
|
||||||
|
return `${context.parsed.y.toLocaleString()} sats`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
display: true,
|
||||||
|
grid: {
|
||||||
|
display: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
display: true,
|
||||||
|
grid: {
|
||||||
|
color: 'rgba(0,0,0,0.1)'
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
callback: function (value) {
|
||||||
|
return value.toLocaleString() + ' sats'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
interaction: {
|
||||||
|
mode: 'nearest',
|
||||||
|
axis: 'x',
|
||||||
|
intersect: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -192,7 +398,8 @@ window.app = Vue.createApp({
|
||||||
this.loading = true
|
this.loading = true
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.loadDashboardData(),
|
this.loadDashboardData(),
|
||||||
this.loadTransactions()
|
this.loadTransactions(),
|
||||||
|
this.loadChartData()
|
||||||
])
|
])
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error initializing dashboard:', error)
|
console.error('Error initializing dashboard:', error)
|
||||||
|
|
@ -202,6 +409,15 @@ window.app = Vue.createApp({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
// Initialize chart after DOM is ready
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.analyticsData) {
|
||||||
|
this.initDCAChart()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
hasData() {
|
hasData() {
|
||||||
return this.dashboardData && !this.loading
|
return this.dashboardData && !this.loading
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||||
%} {% block scripts %} {{ window_vars(user) }}
|
%} {% block scripts %} {{ window_vars(user) }}
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.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="dcaClient">
|
<div class="row q-col-gutter-md" id="dcaClient">
|
||||||
|
|
@ -213,6 +214,33 @@
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
||||||
|
<!-- DCA Performance Chart -->
|
||||||
|
<q-card class="q-mb-md">
|
||||||
|
<q-card-section>
|
||||||
|
<h6 class="text-subtitle2 q-my-none q-mb-md">Bitcoin Accumulation Progress</h6>
|
||||||
|
<div class="chart-container" style="position: relative; height: 300px;">
|
||||||
|
<canvas ref="dcaChart" style="max-height: 300px;"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="row q-mt-sm">
|
||||||
|
<div class="col">
|
||||||
|
<q-btn-toggle
|
||||||
|
v-model="chartTimeRange"
|
||||||
|
@update:model-value="loadChartData"
|
||||||
|
toggle-color="orange"
|
||||||
|
:options="[
|
||||||
|
{label: '7D', value: '7d'},
|
||||||
|
{label: '30D', value: '30d'},
|
||||||
|
{label: '90D', value: '90d'},
|
||||||
|
{label: 'ALL', value: 'all'}
|
||||||
|
]"
|
||||||
|
size="sm"
|
||||||
|
flat
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
|
||||||
<!-- Transaction History -->
|
<!-- Transaction History -->
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue