fix: Radar mobile auctions layout + import 886 TLDs
Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled

This commit is contained in:
2025-12-13 17:21:49 +01:00
parent 8996929174
commit ce961aa03d
3 changed files with 190 additions and 99 deletions

View File

@ -533,7 +533,13 @@ async def create_listing(
listing_count = user_listings.scalar() or 0 listing_count = user_listings.scalar() or 0
# Listing limits by tier (from pounce_pricing.md) # 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} limits = {"scout": 0, "trader": 5, "tycoon": 50}
max_listings = limits.get(tier, 0) max_listings = limits.get(tier, 0)

View File

@ -9,8 +9,8 @@ from decimal import Decimal
from typing import Optional from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, status, Query from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy import func, and_, or_ from sqlalchemy import func, and_, or_, Integer, case, select
from sqlalchemy.orm import Session from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_db, get_current_user from app.api.deps import get_db, get_current_user
from app.models.user import User from app.models.user import User
@ -97,31 +97,47 @@ async def analyze_domain_intent(
@router.get("/dashboard", response_model=YieldDashboardResponse) @router.get("/dashboard", response_model=YieldDashboardResponse)
async def get_yield_dashboard( async def get_yield_dashboard(
db: Session = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
""" """
Get yield dashboard with stats, domains, and recent transactions. Get yield dashboard with stats, domains, and recent transactions.
""" """
# Get user's yield domains # Get user's yield domains
domains = db.query(YieldDomain).filter( result = await db.execute(
YieldDomain.user_id == current_user.id select(YieldDomain)
).order_by(YieldDomain.total_revenue.desc()).all() .where(YieldDomain.user_id == current_user.id)
.order_by(YieldDomain.total_revenue.desc())
)
domains = list(result.scalars().all())
# Calculate stats # Calculate stats
now = datetime.utcnow() now = datetime.utcnow()
month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
# Monthly stats from transactions # Monthly stats from transactions (simplified for async)
monthly_stats = db.query( 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.count(YieldTransaction.id).label("count"),
func.sum(YieldTransaction.net_amount).label("revenue"), func.coalesce(func.sum(YieldTransaction.net_amount), 0).label("revenue"),
func.sum(func.cast(YieldTransaction.event_type == "click", Integer)).label("clicks"), func.sum(case((YieldTransaction.event_type == "click", 1), else_=0)).label("clicks"),
func.sum(func.cast(YieldTransaction.event_type.in_(["lead", "sale"]), Integer)).label("conversions"), func.sum(case((YieldTransaction.event_type.in_(["lead", "sale"]), 1), else_=0)).label("conversions"),
).join(YieldDomain).filter( ).where(
YieldDomain.user_id == current_user.id, YieldTransaction.yield_domain_id.in_(domain_ids),
YieldTransaction.created_at >= month_start, YieldTransaction.created_at >= month_start,
).first() )
)
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 # Aggregate domain stats
total_active = sum(1 for d in domains if d.status == "active") 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) lifetime_conversions = sum(d.total_conversions for d in domains)
# Pending payout # Pending payout
pending_payout = db.query(func.sum(YieldTransaction.net_amount)).filter( pending_payout = Decimal("0")
YieldTransaction.yield_domain_id.in_([d.id for d in domains]), 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.status == "confirmed",
YieldTransaction.paid_at.is_(None), YieldTransaction.paid_at.is_(None),
).scalar() or Decimal("0") )
)
pending_payout = pending_result.scalar() or Decimal("0")
# Get recent transactions # Get recent transactions
recent_txs = db.query(YieldTransaction).join(YieldDomain).filter( recent_txs = []
YieldDomain.user_id == current_user.id, if domains:
).order_by(YieldTransaction.created_at.desc()).limit(10).all() 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 performing domains
top_domains = sorted(domains, key=lambda d: d.total_revenue, reverse=True)[:5] 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), total_domains=len(domains),
active_domains=total_active, active_domains=total_active,
pending_domains=total_pending, pending_domains=total_pending,
monthly_revenue=monthly_stats.revenue or Decimal("0"), monthly_revenue=monthly_revenue,
monthly_clicks=monthly_stats.clicks or 0, monthly_clicks=monthly_clicks,
monthly_conversions=monthly_stats.conversions or 0, monthly_conversions=monthly_conversions,
lifetime_revenue=lifetime_revenue, lifetime_revenue=lifetime_revenue,
lifetime_clicks=lifetime_clicks, lifetime_clicks=lifetime_clicks,
lifetime_conversions=lifetime_conversions, lifetime_conversions=lifetime_conversions,
@ -177,22 +206,34 @@ async def list_yield_domains(
status: Optional[str] = Query(None, description="Filter by status"), status: Optional[str] = Query(None, description="Filter by status"),
limit: int = Query(50, le=100), limit: int = Query(50, le=100),
offset: int = Query(0, ge=0), offset: int = Query(0, ge=0),
db: Session = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
""" """
List user's yield domains. 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: if status:
query = query.filter(YieldDomain.status == status) query = query.where(YieldDomain.status == status)
total = query.count() # Get total count
domains = query.order_by(YieldDomain.created_at.desc()).offset(offset).limit(limit).all() count_result = await db.execute(
select(func.count(YieldDomain.id)).where(YieldDomain.user_id == current_user.id)
)
total = count_result.scalar() or 0
# Aggregates # Get domains
all_domains = db.query(YieldDomain).filter(YieldDomain.user_id == current_user.id).all() 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_active = sum(1 for d in all_domains if d.status == "active")
total_revenue = sum(d.total_revenue for d in all_domains) total_revenue = sum(d.total_revenue for d in all_domains)
total_clicks = sum(d.total_clicks 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) @router.get("/domains/{domain_id}", response_model=YieldDomainResponse)
async def get_yield_domain( async def get_yield_domain(
domain_id: int, domain_id: int,
db: Session = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
""" """
Get details of a specific yield domain. Get details of a specific yield domain.
""" """
domain = db.query(YieldDomain).filter( result = await db.execute(
select(YieldDomain).where(
YieldDomain.id == domain_id, YieldDomain.id == domain_id,
YieldDomain.user_id == current_user.id, YieldDomain.user_id == current_user.id,
).first() )
)
domain = result.scalar_one_or_none()
if not domain: if not domain:
raise HTTPException(status_code=404, detail="Yield domain not found") raise HTTPException(status_code=404, detail="Yield domain not found")

View File

@ -547,9 +547,49 @@ export default function RadarPage() {
<Loader2 className="w-6 h-6 text-accent animate-spin" /> <Loader2 className="w-6 h-6 text-accent animate-spin" />
</div> </div>
) : sortedAuctions.length > 0 ? ( ) : sortedAuctions.length > 0 ? (
<div className="space-y-px bg-white/[0.04] border border-white/[0.08]"> <>
{/* MOBILE Auction List */}
<div className="lg:hidden space-y-2">
{sortedAuctions.map((auction, i) => (
<a
key={i}
href={auction.affiliate_url || '#'}
target="_blank"
rel="noopener noreferrer"
className="block p-3 bg-[#0A0A0A] border border-white/[0.08] active:bg-white/[0.03] transition-all"
>
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-3 min-w-0 flex-1">
<div className="w-8 h-8 bg-white/[0.02] border border-white/[0.06] flex items-center justify-center shrink-0">
<Gavel className="w-4 h-4 text-white/40" />
</div>
<div className="min-w-0 flex-1">
<div className="text-sm font-bold text-white font-mono truncate">
{auction.domain}
</div>
<div className="text-[10px] font-mono text-white/30 uppercase">
{auction.platform}
</div>
</div>
</div>
<div className="text-right shrink-0">
<div className="text-sm font-bold text-accent font-mono">
${auction.current_bid.toLocaleString()}
</div>
<div className="text-[10px] font-mono text-white/40 flex items-center justify-end gap-1">
<Clock className="w-3 h-3" />
{auction.time_remaining}
</div>
</div>
</div>
</a>
))}
</div>
{/* DESKTOP Table */}
<div className="hidden lg:block space-y-px bg-white/[0.04] border border-white/[0.08]">
{/* Desktop Table Header */} {/* Desktop Table Header */}
<div className="hidden lg:grid grid-cols-[1fr_100px_80px_100px_40px] gap-4 px-3 py-2 text-[10px] font-mono text-white/40 uppercase tracking-wider border-b border-white/[0.08]"> <div className="grid grid-cols-[1fr_100px_80px_100px_40px] gap-4 px-3 py-2 text-[10px] font-mono text-white/40 uppercase tracking-wider border-b border-white/[0.08]">
<button onClick={() => handleAuctionSort('domain')} className="flex items-center gap-1 hover:text-white/60 text-left"> <button onClick={() => handleAuctionSort('domain')} className="flex items-center gap-1 hover:text-white/60 text-left">
Domain Domain
{auctionSort === 'domain' && (auctionSortDir === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)} {auctionSort === 'domain' && (auctionSortDir === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
@ -611,6 +651,7 @@ export default function RadarPage() {
</a> </a>
))} ))}
</div> </div>
</>
) : ( ) : (
<div className="text-center py-12 border border-dashed border-white/[0.08]"> <div className="text-center py-12 border border-dashed border-white/[0.08]">
<Gavel className="w-8 h-8 text-white/10 mx-auto mb-3" /> <Gavel className="w-8 h-8 text-white/10 mx-auto mb-3" />