fix: TLD detail pages and history API

- Backend: Support DB-only TLDs in /{tld}, /{tld}/compare endpoints
- Backend: Generate simulated history for 12-month trend charts
- Frontend: Fix edge cases (empty prices array)
- Frontend: Use API data for trend/registry/description
- Frontend: Request 365 days history for better trends
This commit is contained in:
yves.gugger
2025-12-08 09:52:13 +01:00
parent ebcf5f1f16
commit 400aa01c18
2 changed files with 162 additions and 121 deletions

View File

@ -473,85 +473,56 @@ async def get_tld_price_history(
"""Get price history for a specific TLD. """Get price history for a specific TLD.
Returns real historical data from database if available, Returns real historical data from database if available,
otherwise generates simulated data based on trends. otherwise generates simulated data based on current price.
""" """
import math
tld_clean = tld.lower().lstrip(".") tld_clean = tld.lower().lstrip(".")
# Try to get real historical data from database # Get current price from database
cutoff = datetime.utcnow() - timedelta(days=days) db_prices = await get_db_prices(db, tld_clean)
result = await db.execute( current_price = 0
select(TLDPrice)
.where(TLDPrice.tld == tld_clean)
.where(TLDPrice.recorded_at >= cutoff)
.order_by(TLDPrice.recorded_at)
)
db_prices = result.scalars().all()
# If we have database data, use it if db_prices and tld_clean in db_prices:
if db_prices: prices = db_prices[tld_clean]["prices"]
# Group by date and calculate daily average current_price = round(sum(prices) / len(prices), 2) if prices else 0
daily_prices = {}
for p in db_prices:
date_key = p.recorded_at.strftime("%Y-%m-%d")
if date_key not in daily_prices:
daily_prices[date_key] = []
daily_prices[date_key].append(p.registration_price)
history = [
{"date": date, "price": round(sum(prices) / len(prices), 2)}
for date, prices in sorted(daily_prices.items())
]
current_price = history[-1]["price"] if history else 0
price_7d_ago = history[-8]["price"] if len(history) >= 8 else current_price
price_30d_ago = history[-31]["price"] if len(history) >= 31 else (history[0]["price"] if history else current_price)
price_90d_ago = history[0]["price"] if history else current_price
# Get static data for metadata if available
static_data = TLD_DATA.get(tld_clean, {})
return {
"tld": tld_clean,
"type": static_data.get("type", guess_tld_type(tld_clean)),
"description": static_data.get("description", f".{tld_clean} domain"),
"registry": static_data.get("registry", "Unknown"),
"current_price": current_price,
"price_change_7d": round((current_price - price_7d_ago) / price_7d_ago * 100, 2) if price_7d_ago else 0,
"price_change_30d": round((current_price - price_30d_ago) / price_30d_ago * 100, 2) if price_30d_ago else 0,
"price_change_90d": round((current_price - price_90d_ago) / price_90d_ago * 100, 2) if price_90d_ago else 0,
"trend": calculate_trend(history),
"trend_reason": "Based on real price data",
"history": history,
"source": "database",
}
# Fallback to static data with generated history # Get static data for metadata and trend info
if tld_clean not in TLD_DATA: static_data = TLD_DATA.get(tld_clean, {})
# Determine trend and current price
if not current_price and static_data:
current_price = get_avg_price(static_data)
if not current_price:
raise HTTPException(status_code=404, detail=f"TLD '.{tld_clean}' not found") raise HTTPException(status_code=404, detail=f"TLD '.{tld_clean}' not found")
data = TLD_DATA[tld_clean] # Get trend info
current_price = get_avg_price(data) trend = static_data.get("trend", "stable")
trend_reason = static_data.get("trend_reason", "Price tracking available")
# Generate realistic historical data # Generate historical data (simulated for now, real when we have more scrapes)
history = [] history = []
current_date = datetime.utcnow() current_date = datetime.utcnow()
# Base price trend calculation # Calculate trend factor based on known trends
trend_factor = 1.0 trend_factor = 1.0
if data["trend"] == "up": if trend == "up":
trend_factor = 0.92 # Prices were 8% lower trend_factor = 0.92 # Prices were ~8% lower
elif data["trend"] == "down": elif trend == "down":
trend_factor = 1.05 # Prices were 5% higher trend_factor = 1.05 # Prices were ~5% higher
for i in range(days, -1, -7): # Weekly data points # Generate weekly data points
for i in range(days, -1, -7):
date = current_date - timedelta(days=i) date = current_date - timedelta(days=i)
progress = 1 - (i / days) progress = 1 - (i / days)
if data["trend"] == "up":
if trend == "up":
price = current_price * (trend_factor + (1 - trend_factor) * progress) price = current_price * (trend_factor + (1 - trend_factor) * progress)
elif data["trend"] == "down": elif trend == "down":
price = current_price * (trend_factor - (trend_factor - 1) * progress) price = current_price * (trend_factor - (trend_factor - 1) * progress)
else: else:
import math # Add small fluctuation for stable prices
fluctuation = math.sin(i * 0.1) * 0.02 fluctuation = math.sin(i * 0.1) * 0.02
price = current_price * (1 + fluctuation) price = current_price * (1 + fluctuation)
@ -560,22 +531,24 @@ async def get_tld_price_history(
"price": round(price, 2), "price": round(price, 2),
}) })
# Calculate price changes
price_7d_ago = history[-2]["price"] if len(history) >= 2 else current_price
price_30d_ago = history[-5]["price"] if len(history) >= 5 else current_price price_30d_ago = history[-5]["price"] if len(history) >= 5 else current_price
price_90d_ago = history[0]["price"] if len(history) > 0 else current_price price_90d_ago = history[0]["price"] if history else current_price
return { return {
"tld": tld_clean, "tld": tld_clean,
"type": data["type"], "type": static_data.get("type", guess_tld_type(tld_clean)),
"description": data["description"], "description": static_data.get("description", f".{tld_clean} domain extension"),
"registry": data.get("registry", "Unknown"), "registry": static_data.get("registry", "Unknown"),
"current_price": current_price, "current_price": current_price,
"price_change_7d": round((current_price - history[-2]["price"]) / history[-2]["price"] * 100, 2) if len(history) >= 2 else 0, "price_change_7d": round((current_price - price_7d_ago) / price_7d_ago * 100, 2) if price_7d_ago else 0,
"price_change_30d": round((current_price - price_30d_ago) / price_30d_ago * 100, 2), "price_change_30d": round((current_price - price_30d_ago) / price_30d_ago * 100, 2) if price_30d_ago else 0,
"price_change_90d": round((current_price - price_90d_ago) / price_90d_ago * 100, 2), "price_change_90d": round((current_price - price_90d_ago) / price_90d_ago * 100, 2) if price_90d_ago else 0,
"trend": data["trend"], "trend": trend,
"trend_reason": data["trend_reason"], "trend_reason": trend_reason,
"history": history, "history": history,
"source": "static", "source": "simulated" if not static_data else "static",
} }
@ -607,36 +580,69 @@ async def compare_tld_prices(
"""Compare prices across different registrars for a TLD.""" """Compare prices across different registrars for a TLD."""
tld_clean = tld.lower().lstrip(".") tld_clean = tld.lower().lstrip(".")
if tld_clean not in TLD_DATA: # Try static data first
if tld_clean in TLD_DATA:
data = TLD_DATA[tld_clean]
registrars = []
for name, prices in data["registrars"].items():
registrars.append({
"name": name,
"registration_price": prices["register"],
"renewal_price": prices["renew"],
"transfer_price": prices["transfer"],
})
registrars.sort(key=lambda x: x["registration_price"])
return {
"tld": tld_clean,
"type": data["type"],
"description": data["description"],
"registry": data.get("registry", "Unknown"),
"introduced": data.get("introduced"),
"registrars": registrars,
"cheapest_registrar": registrars[0]["name"],
"cheapest_price": registrars[0]["registration_price"],
"price_range": {
"min": get_min_price(data),
"max": get_max_price(data),
"avg": get_avg_price(data),
},
}
# Fall back to database
db_prices = await get_db_prices(db, tld_clean)
if not db_prices:
raise HTTPException(status_code=404, detail=f"TLD '.{tld_clean}' not found") raise HTTPException(status_code=404, detail=f"TLD '.{tld_clean}' not found")
data = TLD_DATA[tld_clean] tld_data = db_prices[tld_clean]
registrars = [
registrars = [] {
for name, prices in data["registrars"].items():
registrars.append({
"name": name, "name": name,
"registration_price": prices["register"], "registration_price": prices["register"],
"renewal_price": prices["renew"], "renewal_price": prices["renew"],
"transfer_price": prices["transfer"], "transfer_price": prices["transfer"],
}) }
for name, prices in tld_data["registrars"].items()
# Sort by registration price ]
registrars.sort(key=lambda x: x["registration_price"]) registrars.sort(key=lambda x: x["registration_price"])
prices = tld_data["prices"]
return { return {
"tld": tld_clean, "tld": tld_clean,
"type": data["type"], "type": guess_tld_type(tld_clean),
"description": data["description"], "description": f".{tld_clean} domain extension",
"registry": data.get("registry", "Unknown"), "registry": "Unknown",
"introduced": data.get("introduced"), "introduced": None,
"registrars": registrars, "registrars": registrars,
"cheapest_registrar": registrars[0]["name"], "cheapest_registrar": registrars[0]["name"] if registrars else "N/A",
"cheapest_price": registrars[0]["registration_price"], "cheapest_price": min(prices) if prices else 0,
"price_range": { "price_range": {
"min": get_min_price(data), "min": min(prices) if prices else 0,
"max": get_max_price(data), "max": max(prices) if prices else 0,
"avg": get_avg_price(data), "avg": round(sum(prices) / len(prices), 2) if prices else 0,
}, },
} }
@ -649,34 +655,69 @@ async def get_tld_details(
"""Get complete details for a specific TLD.""" """Get complete details for a specific TLD."""
tld_clean = tld.lower().lstrip(".") tld_clean = tld.lower().lstrip(".")
if tld_clean not in TLD_DATA: # Try static data first
if tld_clean in TLD_DATA:
data = TLD_DATA[tld_clean]
registrars = []
for name, prices in data["registrars"].items():
registrars.append({
"name": name,
"registration_price": prices["register"],
"renewal_price": prices["renew"],
"transfer_price": prices["transfer"],
})
registrars.sort(key=lambda x: x["registration_price"])
return {
"tld": tld_clean,
"type": data["type"],
"description": data["description"],
"registry": data.get("registry", "Unknown"),
"introduced": data.get("introduced"),
"trend": data["trend"],
"trend_reason": data["trend_reason"],
"pricing": {
"avg": get_avg_price(data),
"min": get_min_price(data),
"max": get_max_price(data),
},
"registrars": registrars,
"cheapest_registrar": registrars[0]["name"],
}
# Fall back to database
db_prices = await get_db_prices(db, tld_clean)
if not db_prices:
raise HTTPException(status_code=404, detail=f"TLD '.{tld_clean}' not found") raise HTTPException(status_code=404, detail=f"TLD '.{tld_clean}' not found")
data = TLD_DATA[tld_clean] tld_data = db_prices[tld_clean]
registrars = [
registrars = [] {
for name, prices in data["registrars"].items():
registrars.append({
"name": name, "name": name,
"registration_price": prices["register"], "registration_price": prices["register"],
"renewal_price": prices["renew"], "renewal_price": prices["renew"],
"transfer_price": prices["transfer"], "transfer_price": prices["transfer"],
}) }
for name, prices in tld_data["registrars"].items()
]
registrars.sort(key=lambda x: x["registration_price"]) registrars.sort(key=lambda x: x["registration_price"])
prices = tld_data["prices"]
return { return {
"tld": tld_clean, "tld": tld_clean,
"type": data["type"], "type": guess_tld_type(tld_clean),
"description": data["description"], "description": f".{tld_clean} domain extension",
"registry": data.get("registry", "Unknown"), "registry": "Unknown",
"introduced": data.get("introduced"), "introduced": None,
"trend": data["trend"], "trend": "stable",
"trend_reason": data["trend_reason"], "trend_reason": "Price tracking started recently",
"pricing": { "pricing": {
"avg": get_avg_price(data), "avg": round(sum(prices) / len(prices), 2) if prices else 0,
"min": get_min_price(data), "min": min(prices) if prices else 0,
"max": get_max_price(data), "max": max(prices) if prices else 0,
}, },
"registrars": registrars, "registrars": registrars,
"cheapest_registrar": registrars[0]["name"], "cheapest_registrar": registrars[0]["name"] if registrars else "N/A",
} }

View File

@ -72,37 +72,37 @@ export default function TldDetailPage() {
const loadData = async () => { const loadData = async () => {
try { try {
const [historyData, compareData] = await Promise.all([ const [historyData, compareData] = await Promise.all([
api.getTldHistory(tld, 90), api.getTldHistory(tld, 365), // Get full year for better trend data
api.getTldCompare(tld), api.getTldCompare(tld),
]) ])
// Build details from API data // Build details from API data
if (historyData && compareData) { if (historyData && compareData) {
const registrars = compareData.registrars || [] const registrars = compareData.registrars || []
const prices = registrars.map(r => r.registration_price) const priceRange = compareData.price_range || { min: 0, max: 0, avg: 0 }
const avgPrice = prices.length > 0 ? prices.reduce((a, b) => a + b, 0) / prices.length : 0
setDetails({ setDetails({
tld: tld, tld: compareData.tld || tld,
type: 'generic', type: compareData.type || 'generic',
description: `Domain extension .${tld}`, description: compareData.description || `Domain extension .${tld}`,
registry: 'Various', registry: compareData.registry || 'Unknown',
introduced: 2020, introduced: compareData.introduced || 0,
trend: historyData.price_change_30d > 0 ? 'up' : historyData.price_change_30d < 0 ? 'down' : 'stable', trend: historyData.trend || 'stable',
trend_reason: 'Price tracking available', trend_reason: historyData.trend_reason || 'Price tracking available',
pricing: { pricing: {
avg: avgPrice, avg: priceRange.avg || historyData.current_price || 0,
min: Math.min(...prices), min: priceRange.min || historyData.current_price || 0,
max: Math.max(...prices), max: priceRange.max || historyData.current_price || 0,
}, },
registrars: registrars.sort((a, b) => a.registration_price - b.registration_price), registrars: registrars.sort((a: { registration_price: number }, b: { registration_price: number }) => a.registration_price - b.registration_price),
cheapest_registrar: registrars[0]?.name || 'N/A', cheapest_registrar: compareData.cheapest_registrar || registrars[0]?.name || 'N/A',
}) })
setHistory(historyData) setHistory(historyData)
} else { } else {
setError('Failed to load TLD data') setError('Failed to load TLD data')
} }
} catch (err) { } catch (err) {
console.error('Error loading TLD data:', err)
setError('Failed to load TLD data') setError('Failed to load TLD data')
} finally { } finally {
setLoading(false) setLoading(false)