diff --git a/backend/app/api/listings.py b/backend/app/api/listings.py index 2f20ad7..96db302 100644 --- a/backend/app/api/listings.py +++ b/backend/app/api/listings.py @@ -533,7 +533,13 @@ async def create_listing( listing_count = user_listings.scalar() or 0 # Listing limits by tier (from pounce_pricing.md) - tier = current_user.subscription.tier if current_user.subscription else "scout" + # Load subscription separately to avoid async lazy loading issues + from app.models.subscription import Subscription + sub_result = await db.execute( + select(Subscription).where(Subscription.user_id == current_user.id) + ) + subscription = sub_result.scalar_one_or_none() + tier = subscription.tier if subscription else "scout" limits = {"scout": 0, "trader": 5, "tycoon": 50} max_listings = limits.get(tier, 0) diff --git a/backend/app/api/yield_domains.py b/backend/app/api/yield_domains.py index 1255a0b..b4fcff4 100644 --- a/backend/app/api/yield_domains.py +++ b/backend/app/api/yield_domains.py @@ -9,8 +9,8 @@ from decimal import Decimal from typing import Optional from fastapi import APIRouter, Depends, HTTPException, status, Query -from sqlalchemy import func, and_, or_ -from sqlalchemy.orm import Session +from sqlalchemy import func, and_, or_, Integer, case, select +from sqlalchemy.ext.asyncio import AsyncSession from app.api.deps import get_db, get_current_user from app.models.user import User @@ -97,31 +97,47 @@ async def analyze_domain_intent( @router.get("/dashboard", response_model=YieldDashboardResponse) async def get_yield_dashboard( - db: Session = Depends(get_db), + db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """ Get yield dashboard with stats, domains, and recent transactions. """ # Get user's yield domains - domains = db.query(YieldDomain).filter( - YieldDomain.user_id == current_user.id - ).order_by(YieldDomain.total_revenue.desc()).all() + result = await db.execute( + select(YieldDomain) + .where(YieldDomain.user_id == current_user.id) + .order_by(YieldDomain.total_revenue.desc()) + ) + domains = list(result.scalars().all()) # Calculate stats now = datetime.utcnow() month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) - # Monthly stats from transactions - monthly_stats = db.query( - func.count(YieldTransaction.id).label("count"), - func.sum(YieldTransaction.net_amount).label("revenue"), - func.sum(func.cast(YieldTransaction.event_type == "click", Integer)).label("clicks"), - func.sum(func.cast(YieldTransaction.event_type.in_(["lead", "sale"]), Integer)).label("conversions"), - ).join(YieldDomain).filter( - YieldDomain.user_id == current_user.id, - YieldTransaction.created_at >= month_start, - ).first() + # Monthly stats from transactions (simplified for async) + monthly_revenue = Decimal("0") + monthly_clicks = 0 + monthly_conversions = 0 + + if domains: + domain_ids = [d.id for d in domains] + monthly_result = await db.execute( + select( + func.count(YieldTransaction.id).label("count"), + func.coalesce(func.sum(YieldTransaction.net_amount), 0).label("revenue"), + func.sum(case((YieldTransaction.event_type == "click", 1), else_=0)).label("clicks"), + func.sum(case((YieldTransaction.event_type.in_(["lead", "sale"]), 1), else_=0)).label("conversions"), + ).where( + YieldTransaction.yield_domain_id.in_(domain_ids), + YieldTransaction.created_at >= month_start, + ) + ) + monthly_stats = monthly_result.first() + if monthly_stats: + monthly_revenue = monthly_stats.revenue or Decimal("0") + monthly_clicks = monthly_stats.clicks or 0 + monthly_conversions = monthly_stats.conversions or 0 # Aggregate domain stats total_active = sum(1 for d in domains if d.status == "active") @@ -131,16 +147,29 @@ async def get_yield_dashboard( lifetime_conversions = sum(d.total_conversions for d in domains) # Pending payout - pending_payout = db.query(func.sum(YieldTransaction.net_amount)).filter( - YieldTransaction.yield_domain_id.in_([d.id for d in domains]), - YieldTransaction.status == "confirmed", - YieldTransaction.paid_at.is_(None), - ).scalar() or Decimal("0") + pending_payout = Decimal("0") + if domains: + domain_ids = [d.id for d in domains] + pending_result = await db.execute( + select(func.coalesce(func.sum(YieldTransaction.net_amount), 0)).where( + YieldTransaction.yield_domain_id.in_(domain_ids), + YieldTransaction.status == "confirmed", + YieldTransaction.paid_at.is_(None), + ) + ) + pending_payout = pending_result.scalar() or Decimal("0") # Get recent transactions - recent_txs = db.query(YieldTransaction).join(YieldDomain).filter( - YieldDomain.user_id == current_user.id, - ).order_by(YieldTransaction.created_at.desc()).limit(10).all() + recent_txs = [] + if domains: + domain_ids = [d.id for d in domains] + recent_result = await db.execute( + select(YieldTransaction) + .where(YieldTransaction.yield_domain_id.in_(domain_ids)) + .order_by(YieldTransaction.created_at.desc()) + .limit(10) + ) + recent_txs = list(recent_result.scalars().all()) # Top performing domains top_domains = sorted(domains, key=lambda d: d.total_revenue, reverse=True)[:5] @@ -149,9 +178,9 @@ async def get_yield_dashboard( total_domains=len(domains), active_domains=total_active, pending_domains=total_pending, - monthly_revenue=monthly_stats.revenue or Decimal("0"), - monthly_clicks=monthly_stats.clicks or 0, - monthly_conversions=monthly_stats.conversions or 0, + monthly_revenue=monthly_revenue, + monthly_clicks=monthly_clicks, + monthly_conversions=monthly_conversions, lifetime_revenue=lifetime_revenue, lifetime_clicks=lifetime_clicks, lifetime_conversions=lifetime_conversions, @@ -177,22 +206,34 @@ async def list_yield_domains( status: Optional[str] = Query(None, description="Filter by status"), limit: int = Query(50, le=100), offset: int = Query(0, ge=0), - db: Session = Depends(get_db), + db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """ List user's yield domains. """ - query = db.query(YieldDomain).filter(YieldDomain.user_id == current_user.id) + query = select(YieldDomain).where(YieldDomain.user_id == current_user.id) if status: - query = query.filter(YieldDomain.status == status) + query = query.where(YieldDomain.status == status) - total = query.count() - domains = query.order_by(YieldDomain.created_at.desc()).offset(offset).limit(limit).all() + # Get total count + count_result = await db.execute( + select(func.count(YieldDomain.id)).where(YieldDomain.user_id == current_user.id) + ) + total = count_result.scalar() or 0 - # Aggregates - all_domains = db.query(YieldDomain).filter(YieldDomain.user_id == current_user.id).all() + # Get domains + result = await db.execute( + query.order_by(YieldDomain.created_at.desc()).offset(offset).limit(limit) + ) + domains = list(result.scalars().all()) + + # Aggregates from all domains + all_result = await db.execute( + select(YieldDomain).where(YieldDomain.user_id == current_user.id) + ) + all_domains = list(all_result.scalars().all()) total_active = sum(1 for d in all_domains if d.status == "active") total_revenue = sum(d.total_revenue for d in all_domains) total_clicks = sum(d.total_clicks for d in all_domains) @@ -209,16 +250,19 @@ async def list_yield_domains( @router.get("/domains/{domain_id}", response_model=YieldDomainResponse) async def get_yield_domain( domain_id: int, - db: Session = Depends(get_db), + db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """ Get details of a specific yield domain. """ - domain = db.query(YieldDomain).filter( - YieldDomain.id == domain_id, - YieldDomain.user_id == current_user.id, - ).first() + result = await db.execute( + select(YieldDomain).where( + YieldDomain.id == domain_id, + YieldDomain.user_id == current_user.id, + ) + ) + domain = result.scalar_one_or_none() if not domain: raise HTTPException(status_code=404, detail="Yield domain not found") diff --git a/frontend/src/app/terminal/radar/page.tsx b/frontend/src/app/terminal/radar/page.tsx index 1585bdc..8160fe9 100644 --- a/frontend/src/app/terminal/radar/page.tsx +++ b/frontend/src/app/terminal/radar/page.tsx @@ -547,70 +547,111 @@ export default function RadarPage() { ) : sortedAuctions.length > 0 ? ( -
- {/* Desktop Table Header */} -
- -
Platform
- - -
+ <> + {/* MOBILE Auction List */} +
+ {sortedAuctions.map((auction, i) => ( + +
+
+
+ +
+
+
+ {auction.domain} +
+
+ {auction.platform} +
+
+
+
+
+ ${auction.current_bid.toLocaleString()} +
+
+ + {auction.time_remaining} +
+
+
+
+ ))}
- {sortedAuctions.map((auction, i) => ( - - {/* Domain */} -
-
- + {/* DESKTOP Table */} +
+ {/* Desktop Table Header */} +
+ +
Platform
+ + +
+
+ + {sortedAuctions.map((auction, i) => ( +
+ {/* Domain */} +
+
+ +
+
+ {auction.domain} +
-
- {auction.domain} + + {/* Platform */} +
+ {auction.platform}
-
- - {/* Platform */} -
- {auction.platform} -
- - {/* Time */} -
- - - {auction.time_remaining} - -
- - {/* Bid */} -
+ + {/* Bid */} +
+
+ ${auction.current_bid.toLocaleString()} +
+
+ + {/* Link */} +
+ +
+ + ))} +
+ ) : (