From 400aa01c1870217ab005fc08b1ac7db08b6cca1b Mon Sep 17 00:00:00 2001 From: "yves.gugger" Date: Mon, 8 Dec 2025 09:52:13 +0100 Subject: [PATCH] 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 --- backend/app/api/tld_prices.py | 253 ++++++++++++-------- frontend/src/app/tld-pricing/[tld]/page.tsx | 30 +-- 2 files changed, 162 insertions(+), 121 deletions(-) diff --git a/backend/app/api/tld_prices.py b/backend/app/api/tld_prices.py index 76004de..3f38884 100644 --- a/backend/app/api/tld_prices.py +++ b/backend/app/api/tld_prices.py @@ -473,85 +473,56 @@ async def get_tld_price_history( """Get price history for a specific TLD. 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(".") - # Try to get real historical data from database - cutoff = datetime.utcnow() - timedelta(days=days) - result = await db.execute( - select(TLDPrice) - .where(TLDPrice.tld == tld_clean) - .where(TLDPrice.recorded_at >= cutoff) - .order_by(TLDPrice.recorded_at) - ) - db_prices = result.scalars().all() + # Get current price from database + db_prices = await get_db_prices(db, tld_clean) + current_price = 0 - # If we have database data, use it - if db_prices: - # Group by date and calculate daily average - 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", - } + if db_prices and tld_clean in db_prices: + prices = db_prices[tld_clean]["prices"] + current_price = round(sum(prices) / len(prices), 2) if prices else 0 - # Fallback to static data with generated history - if tld_clean not in TLD_DATA: + # Get static data for metadata and trend info + 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") - data = TLD_DATA[tld_clean] - current_price = get_avg_price(data) + # Get trend info + 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 = [] current_date = datetime.utcnow() - # Base price trend calculation + # Calculate trend factor based on known trends trend_factor = 1.0 - if data["trend"] == "up": - trend_factor = 0.92 # Prices were 8% lower - elif data["trend"] == "down": - trend_factor = 1.05 # Prices were 5% higher + if trend == "up": + trend_factor = 0.92 # Prices were ~8% lower + elif trend == "down": + 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) progress = 1 - (i / days) - if data["trend"] == "up": + + if trend == "up": 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) else: - import math + # Add small fluctuation for stable prices fluctuation = math.sin(i * 0.1) * 0.02 price = current_price * (1 + fluctuation) @@ -560,22 +531,24 @@ async def get_tld_price_history( "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_90d_ago = history[0]["price"] if len(history) > 0 else current_price + price_90d_ago = history[0]["price"] if history else current_price return { "tld": tld_clean, - "type": data["type"], - "description": data["description"], - "registry": data.get("registry", "Unknown"), + "type": static_data.get("type", guess_tld_type(tld_clean)), + "description": static_data.get("description", f".{tld_clean} domain extension"), + "registry": static_data.get("registry", "Unknown"), "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_30d": round((current_price - price_30d_ago) / price_30d_ago * 100, 2), - "price_change_90d": round((current_price - price_90d_ago) / price_90d_ago * 100, 2), - "trend": data["trend"], - "trend_reason": data["trend_reason"], + "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": trend, + "trend_reason": trend_reason, "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.""" 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") - data = TLD_DATA[tld_clean] - - registrars = [] - for name, prices in data["registrars"].items(): - registrars.append({ + tld_data = db_prices[tld_clean] + registrars = [ + { "name": name, "registration_price": prices["register"], "renewal_price": prices["renew"], "transfer_price": prices["transfer"], - }) - - # Sort by registration price + } + for name, prices in tld_data["registrars"].items() + ] registrars.sort(key=lambda x: x["registration_price"]) + prices = tld_data["prices"] + return { "tld": tld_clean, - "type": data["type"], - "description": data["description"], - "registry": data.get("registry", "Unknown"), - "introduced": data.get("introduced"), + "type": guess_tld_type(tld_clean), + "description": f".{tld_clean} domain extension", + "registry": "Unknown", + "introduced": None, "registrars": registrars, - "cheapest_registrar": registrars[0]["name"], - "cheapest_price": registrars[0]["registration_price"], + "cheapest_registrar": registrars[0]["name"] if registrars else "N/A", + "cheapest_price": min(prices) if prices else 0, "price_range": { - "min": get_min_price(data), - "max": get_max_price(data), - "avg": get_avg_price(data), + "min": min(prices) if prices else 0, + "max": max(prices) if prices else 0, + "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.""" 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") - data = TLD_DATA[tld_clean] - - registrars = [] - for name, prices in data["registrars"].items(): - registrars.append({ + tld_data = db_prices[tld_clean] + registrars = [ + { "name": name, "registration_price": prices["register"], "renewal_price": prices["renew"], "transfer_price": prices["transfer"], - }) + } + for name, prices in tld_data["registrars"].items() + ] registrars.sort(key=lambda x: x["registration_price"]) + prices = tld_data["prices"] + 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"], + "type": guess_tld_type(tld_clean), + "description": f".{tld_clean} domain extension", + "registry": "Unknown", + "introduced": None, + "trend": "stable", + "trend_reason": "Price tracking started recently", "pricing": { - "avg": get_avg_price(data), - "min": get_min_price(data), - "max": get_max_price(data), + "avg": round(sum(prices) / len(prices), 2) if prices else 0, + "min": min(prices) if prices else 0, + "max": max(prices) if prices else 0, }, "registrars": registrars, - "cheapest_registrar": registrars[0]["name"], + "cheapest_registrar": registrars[0]["name"] if registrars else "N/A", } diff --git a/frontend/src/app/tld-pricing/[tld]/page.tsx b/frontend/src/app/tld-pricing/[tld]/page.tsx index 21072c4..a87d857 100644 --- a/frontend/src/app/tld-pricing/[tld]/page.tsx +++ b/frontend/src/app/tld-pricing/[tld]/page.tsx @@ -72,37 +72,37 @@ export default function TldDetailPage() { const loadData = async () => { try { const [historyData, compareData] = await Promise.all([ - api.getTldHistory(tld, 90), + api.getTldHistory(tld, 365), // Get full year for better trend data api.getTldCompare(tld), ]) // Build details from API data if (historyData && compareData) { const registrars = compareData.registrars || [] - const prices = registrars.map(r => r.registration_price) - const avgPrice = prices.length > 0 ? prices.reduce((a, b) => a + b, 0) / prices.length : 0 + const priceRange = compareData.price_range || { min: 0, max: 0, avg: 0 } setDetails({ - tld: tld, - type: 'generic', - description: `Domain extension .${tld}`, - registry: 'Various', - introduced: 2020, - trend: historyData.price_change_30d > 0 ? 'up' : historyData.price_change_30d < 0 ? 'down' : 'stable', - trend_reason: 'Price tracking available', + tld: compareData.tld || tld, + type: compareData.type || 'generic', + description: compareData.description || `Domain extension .${tld}`, + registry: compareData.registry || 'Unknown', + introduced: compareData.introduced || 0, + trend: historyData.trend || 'stable', + trend_reason: historyData.trend_reason || 'Price tracking available', pricing: { - avg: avgPrice, - min: Math.min(...prices), - max: Math.max(...prices), + avg: priceRange.avg || historyData.current_price || 0, + min: priceRange.min || historyData.current_price || 0, + max: priceRange.max || historyData.current_price || 0, }, - registrars: registrars.sort((a, b) => a.registration_price - b.registration_price), - cheapest_registrar: registrars[0]?.name || 'N/A', + registrars: registrars.sort((a: { registration_price: number }, b: { registration_price: number }) => a.registration_price - b.registration_price), + cheapest_registrar: compareData.cheapest_registrar || registrars[0]?.name || 'N/A', }) setHistory(historyData) } else { setError('Failed to load TLD data') } } catch (err) { + console.error('Error loading TLD data:', err) setError('Failed to load TLD data') } finally { setLoading(false)