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")
func.count(YieldTransaction.id).label("count"), monthly_clicks = 0
func.sum(YieldTransaction.net_amount).label("revenue"), monthly_conversions = 0
func.sum(func.cast(YieldTransaction.event_type == "click", Integer)).label("clicks"),
func.sum(func.cast(YieldTransaction.event_type.in_(["lead", "sale"]), Integer)).label("conversions"), if domains:
).join(YieldDomain).filter( domain_ids = [d.id for d in domains]
YieldDomain.user_id == current_user.id, monthly_result = await db.execute(
YieldTransaction.created_at >= month_start, select(
).first() 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 # 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:
YieldTransaction.status == "confirmed", domain_ids = [d.id for d in domains]
YieldTransaction.paid_at.is_(None), pending_result = await db.execute(
).scalar() or Decimal("0") 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 # 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(
YieldDomain.id == domain_id, select(YieldDomain).where(
YieldDomain.user_id == current_user.id, YieldDomain.id == domain_id,
).first() YieldDomain.user_id == current_user.id,
)
)
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,70 +547,111 @@ 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]"> <>
{/* Desktop Table Header */} {/* MOBILE Auction List */}
<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="lg:hidden space-y-2">
<button onClick={() => handleAuctionSort('domain')} className="flex items-center gap-1 hover:text-white/60 text-left"> {sortedAuctions.map((auction, i) => (
Domain <a
{auctionSort === 'domain' && (auctionSortDir === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)} key={i}
</button> href={auction.affiliate_url || '#'}
<div className="text-center">Platform</div> target="_blank"
<button onClick={() => handleAuctionSort('time')} className="flex items-center gap-1 justify-center hover:text-white/60"> rel="noopener noreferrer"
Time className="block p-3 bg-[#0A0A0A] border border-white/[0.08] active:bg-white/[0.03] transition-all"
{auctionSort === 'time' && (auctionSortDir === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)} >
</button> <div className="flex items-start justify-between gap-3">
<button onClick={() => handleAuctionSort('bid')} className="flex items-center gap-1 justify-end hover:text-white/60"> <div className="flex items-center gap-3 min-w-0 flex-1">
Bid <div className="w-8 h-8 bg-white/[0.02] border border-white/[0.06] flex items-center justify-center shrink-0">
{auctionSort === 'bid' && (auctionSortDir === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)} <Gavel className="w-4 h-4 text-white/40" />
</button> </div>
<div></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> </div>
{sortedAuctions.map((auction, i) => ( {/* DESKTOP Table */}
<a <div className="hidden lg:block space-y-px bg-white/[0.04] border border-white/[0.08]">
key={i} {/* Desktop Table Header */}
href={auction.affiliate_url || '#'} <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]">
target="_blank" <button onClick={() => handleAuctionSort('domain')} className="flex items-center gap-1 hover:text-white/60 text-left">
rel="noopener noreferrer" Domain
className="grid grid-cols-[1fr_100px_80px_100px_40px] gap-4 items-center p-3 bg-[#020202] hover:bg-white/[0.02] active:bg-white/[0.03] transition-all group" {auctionSort === 'domain' && (auctionSortDir === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
> </button>
{/* Domain */} <div className="text-center">Platform</div>
<div className="flex items-center gap-3 min-w-0"> <button onClick={() => handleAuctionSort('time')} className="flex items-center gap-1 justify-center hover:text-white/60">
<div className="w-8 h-8 bg-white/[0.02] border border-white/[0.06] flex items-center justify-center shrink-0"> Time
<Gavel className="w-4 h-4 text-white/40 group-hover:text-accent transition-colors" /> {auctionSort === 'time' && (auctionSortDir === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button>
<button onClick={() => handleAuctionSort('bid')} className="flex items-center gap-1 justify-end hover:text-white/60">
Bid
{auctionSort === 'bid' && (auctionSortDir === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button>
<div></div>
</div>
{sortedAuctions.map((auction, i) => (
<a
key={i}
href={auction.affiliate_url || '#'}
target="_blank"
rel="noopener noreferrer"
className="grid grid-cols-[1fr_100px_80px_100px_40px] gap-4 items-center p-3 bg-[#020202] hover:bg-white/[0.02] active:bg-white/[0.03] transition-all group"
>
{/* Domain */}
<div className="flex items-center gap-3 min-w-0">
<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 group-hover:text-accent transition-colors" />
</div>
<div className="text-sm font-bold text-white font-mono truncate group-hover:text-accent transition-colors">
{auction.domain}
</div>
</div> </div>
<div className="text-sm font-bold text-white font-mono truncate group-hover:text-accent transition-colors">
{auction.domain} {/* Platform */}
<div className="text-center">
<span className="text-[10px] font-mono text-white/40 uppercase">{auction.platform}</span>
</div> </div>
</div>
{/* Time */}
{/* Platform */} <div className="text-center">
<div className="text-center"> <span className="text-xs font-mono text-white/50 flex items-center justify-center gap-1">
<span className="text-[10px] font-mono text-white/40 uppercase">{auction.platform}</span> <Clock className="w-3 h-3" />
</div> {auction.time_remaining}
</span>
{/* Time */}
<div className="text-center">
<span className="text-xs font-mono text-white/50 flex items-center justify-center gap-1">
<Clock className="w-3 h-3" />
{auction.time_remaining}
</span>
</div>
{/* Bid */}
<div className="text-right">
<div className="text-sm font-bold text-accent font-mono">
${auction.current_bid.toLocaleString()}
</div> </div>
</div>
{/* Bid */}
{/* Link */} <div className="text-right">
<div className="flex justify-end"> <div className="text-sm font-bold text-accent font-mono">
<ExternalLink className="w-4 h-4 text-white/20 group-hover:text-accent transition-colors" /> ${auction.current_bid.toLocaleString()}
</div> </div>
</a> </div>
))}
</div> {/* Link */}
<div className="flex justify-end">
<ExternalLink className="w-4 h-4 text-white/20 group-hover:text-accent transition-colors" />
</div>
</a>
))}
</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" />