From b58b45f41224d5ef27f40836c1108dc0fbb8ee5c Mon Sep 17 00:00:00 2001 From: Yves Gugger Date: Fri, 19 Dec 2025 13:35:06 +0100 Subject: [PATCH] Deploy: 2025-12-19 13:35 --- UX_TERMINAL_UX_REPORT.md | 2 + backend/app/api/drops.py | 75 ++--------- backend/app/models/zone_file.py | 2 +- backend/app/scheduler.py | 105 --------------- backend/app/services/drop_status_checker.py | 134 ++++++++------------ backend/app/services/zone_file.py | 103 ++++++++------- frontend/src/components/hunt/DropsTab.tsx | 59 +++++---- frontend/src/lib/api.ts | 11 +- 8 files changed, 157 insertions(+), 334 deletions(-) diff --git a/UX_TERMINAL_UX_REPORT.md b/UX_TERMINAL_UX_REPORT.md index 2972bfa..0153f55 100644 --- a/UX_TERMINAL_UX_REPORT.md +++ b/UX_TERMINAL_UX_REPORT.md @@ -316,3 +316,5 @@ Empfehlungen: - Accessibility sweep: Kontrast, Focus states, ARIA, reduced motion. + + diff --git a/backend/app/api/drops.py b/backend/app/api/drops.py index bed91ce..05f1e02 100644 --- a/backend/app/api/drops.py +++ b/backend/app/api/drops.py @@ -190,9 +190,16 @@ async def api_check_drop_status( ): """ Check the real-time availability status of a dropped domain. + + Returns: + - available: Domain can be registered NOW + - dropping_soon: Domain is in deletion phase (track it!) + - taken: Domain was re-registered + - unknown: Could not determine status """ from app.services.drop_status_checker import check_drop_status + # Get the drop from DB result = await db.execute( select(DroppedDomain).where(DroppedDomain.id == drop_id) ) @@ -204,6 +211,7 @@ async def api_check_drop_status( full_domain = f"{drop.domain}.{drop.tld}" try: + # Check with dedicated drop status checker status_result = await check_drop_status(full_domain) # Update the drop in DB @@ -213,8 +221,7 @@ async def api_check_drop_status( .values( availability_status=status_result.status, rdap_status=str(status_result.rdap_status) if status_result.rdap_status else None, - last_status_check=datetime.utcnow(), - deletion_date=status_result.deletion_date, + last_status_check=datetime.utcnow() ) ) await db.commit() @@ -223,10 +230,11 @@ async def api_check_drop_status( "id": drop_id, "domain": full_domain, "status": status_result.status, - "deletion_date": status_result.deletion_date, + "rdap_status": status_result.rdap_status, "can_register_now": status_result.can_register_now, "should_track": status_result.should_monitor, "message": status_result.message, + "deletion_date": status_result.deletion_date.isoformat() if status_result.deletion_date else None, } except Exception as e: @@ -234,67 +242,6 @@ async def api_check_drop_status( raise HTTPException(status_code=500, detail=str(e)) -@router.post("/batch-check") -async def api_batch_check_status( - background_tasks: BackgroundTasks, - limit: int = Query(50, ge=1, le=100, description="Number of drops to check"), - db: AsyncSession = Depends(get_db), - current_user = Depends(get_current_user) -): - """ - Batch check status for drops that haven't been checked yet. - Returns immediately, updates happen in background. - """ - from app.services.drop_status_checker import batch_check_drops - - # Get drops that haven't been checked or were checked > 1 hour ago - one_hour_ago = datetime.utcnow().replace(hour=datetime.utcnow().hour - 1) - - result = await db.execute( - select(DroppedDomain) - .where( - (DroppedDomain.availability_status == 'unknown') | - (DroppedDomain.last_status_check == None) | - (DroppedDomain.last_status_check < one_hour_ago) - ) - .order_by(DroppedDomain.length) # Prioritize short domains - .limit(limit) - ) - drops = result.scalars().all() - - if not drops: - return {"message": "All drops already checked", "checked": 0} - - # Prepare domain list - domains_to_check = [(d.id, f"{d.domain}.{d.tld}") for d in drops] - - async def run_batch_check(): - from app.database import AsyncSessionLocal - - results = await batch_check_drops(domains_to_check) - - async with AsyncSessionLocal() as session: - for drop_id, status in results: - await session.execute( - update(DroppedDomain) - .where(DroppedDomain.id == drop_id) - .values( - availability_status=status.status, - rdap_status=str(status.rdap_status) if status.rdap_status else None, - last_status_check=datetime.utcnow(), - deletion_date=status.deletion_date, - ) - ) - await session.commit() - - background_tasks.add_task(run_batch_check) - - return { - "message": f"Checking {len(drops)} drops in background", - "checking": len(drops), - } - - @router.post("/track/{drop_id}") async def api_track_drop( drop_id: int, diff --git a/backend/app/models/zone_file.py b/backend/app/models/zone_file.py index 55a6fa1..35ecb94 100644 --- a/backend/app/models/zone_file.py +++ b/backend/app/models/zone_file.py @@ -42,7 +42,7 @@ class DroppedDomain(Base): availability_status = Column(String(20), default='unknown', index=True) rdap_status = Column(String(255), nullable=True) # Raw RDAP status string last_status_check = Column(DateTime, nullable=True) - deletion_date = Column(String(10), nullable=True) # YYYY-MM-DD when domain will be purged + deletion_date = Column(DateTime, nullable=True) # When domain will be fully deleted __table_args__ = ( Index('ix_dropped_domains_tld_date', 'tld', 'dropped_date'), diff --git a/backend/app/scheduler.py b/backend/app/scheduler.py index 08aa431..ba84f47 100644 --- a/backend/app/scheduler.py +++ b/backend/app/scheduler.py @@ -735,15 +735,6 @@ def setup_scheduler(): replace_existing=True, ) - # Drops RDAP status update (hourly - check real status with rate limiting) - scheduler.add_job( - update_drops_status, - CronTrigger(minute=20), # Every hour at :20 - id="drops_status_update", - name="Drops Status Update (hourly)", - replace_existing=True, - ) - logger.info( f"Scheduler configured:" f"\n - Scout domain check at {settings.check_hour:02d}:{settings.check_minute:02d} (daily)" @@ -1042,102 +1033,6 @@ async def verify_drops(): logger.exception(f"Drops verification failed: {e}") -async def update_drops_status(): - """ - Update RDAP status for dropped domains. - - This job runs every hour to check the real status of drops - (available, dropping_soon, taken) and store it in the database. - Uses rate limiting (0.5s delay) to avoid 429 errors from RDAP servers. - - With 0.5s delay, 50 domains takes ~25 seconds. - """ - logger.info("Starting drops status update...") - - try: - from app.services.drop_status_checker import batch_check_drops - from app.models.zone_file import DroppedDomain - from sqlalchemy import select, update - from datetime import datetime, timedelta - - async with AsyncSessionLocal() as db: - # Get drops that haven't been status-checked in the last hour - # Or have never been checked - one_hour_ago = datetime.utcnow() - timedelta(hours=1) - - # Prioritize .ch and .li (our main focus), then short domains - query = ( - select(DroppedDomain) - .where( - (DroppedDomain.availability_status == 'unknown') | - (DroppedDomain.last_status_check == None) | - (DroppedDomain.last_status_check < one_hour_ago) - ) - .order_by( - # Prioritize .ch and .li - DroppedDomain.tld.desc(), - DroppedDomain.length.asc() - ) - .limit(50) # Only 50 per run to avoid rate limiting - ) - - result = await db.execute(query) - drops = result.scalars().all() - - if not drops: - logger.info("All drops have been status-checked recently") - return - - logger.info(f"Checking status for {len(drops)} drops (with rate limiting)...") - - # Prepare domain list - domains_to_check = [(d.id, f"{d.domain}.{d.tld}") for d in drops] - - # Batch check with 0.5s delay between requests - results = await batch_check_drops(domains_to_check, delay=0.5) - - # Update database - available_count = 0 - dropping_soon_count = 0 - taken_count = 0 - unknown_count = 0 - - for drop_id, status in results: - await db.execute( - update(DroppedDomain) - .where(DroppedDomain.id == drop_id) - .values( - availability_status=status.status, - rdap_status=str(status.rdap_status) if status.rdap_status else None, - last_status_check=datetime.utcnow(), - deletion_date=status.deletion_date, - ) - ) - - if status.status == 'available': - available_count += 1 - elif status.status == 'dropping_soon': - dropping_soon_count += 1 - elif status.status == 'taken': - taken_count += 1 - else: - unknown_count += 1 - - await db.commit() - - logger.info( - f"Drops status update complete: " - f"{len(results)} checked, " - f"{available_count} available, " - f"{dropping_soon_count} dropping soon, " - f"{taken_count} taken, " - f"{unknown_count} unknown" - ) - - except Exception as e: - logger.exception(f"Drops status update failed: {e}") - - async def sync_zone_files(): """Sync zone files from Switch.ch (.ch, .li) and ICANN CZDS (gTLDs).""" logger.info("Starting zone file sync...") diff --git a/backend/app/services/drop_status_checker.py b/backend/app/services/drop_status_checker.py index ba091e4..8a76298 100644 --- a/backend/app/services/drop_status_checker.py +++ b/backend/app/services/drop_status_checker.py @@ -3,9 +3,10 @@ Drop Status Checker ==================== Dedicated RDAP checker for dropped domains. Correctly identifies pending_delete, redemption, and available status. -Also extracts deletion date for countdown display. +Extracts deletion date for countdown display. """ +import asyncio import httpx import logging from dataclasses import dataclass @@ -33,22 +34,6 @@ RDAP_ENDPOINTS = { 'app': 'https://rdap.nic.google/domain/', } -# Typical deletion periods after "pending delete" status (in days) -TLD_DELETE_PERIODS = { - 'ch': 40, # .ch has ~40 day redemption - 'li': 40, - 'com': 35, # 30 redemption + 5 pending delete - 'net': 35, - 'org': 35, - 'info': 35, - 'biz': 35, - 'online': 35, - 'xyz': 35, - 'club': 35, - 'dev': 35, - 'app': 35, -} - @dataclass class DropStatus: @@ -59,24 +44,7 @@ class DropStatus: can_register_now: bool should_monitor: bool message: str - deletion_date: Optional[str] = None # ISO format date when domain will be deleted - estimated_available: Optional[str] = None # Estimated date when registrable - - -def _parse_date(date_str: str) -> Optional[datetime]: - """Parse ISO date string to datetime.""" - if not date_str: - return None - try: - # Handle various formats - for fmt in ['%Y-%m-%dT%H:%M:%SZ', '%Y-%m-%dT%H:%M:%S', '%Y-%m-%d']: - try: - return datetime.strptime(date_str[:19].replace('Z', ''), fmt.replace('Z', '')) - except ValueError: - continue - return None - except Exception: - return None + deletion_date: Optional[datetime] = None # When domain will be fully deleted async def check_drop_status(domain: str) -> DropStatus: @@ -94,6 +62,7 @@ async def check_drop_status(domain: str) -> DropStatus: endpoint = RDAP_ENDPOINTS.get(tld) if not endpoint: + # Try generic lookup logger.warning(f"No RDAP endpoint for .{tld}, returning unknown") return DropStatus( domain=domain, @@ -107,11 +76,7 @@ async def check_drop_status(domain: str) -> DropStatus: url = f"{endpoint}{domain}" try: - headers = { - 'User-Agent': 'Mozilla/5.0 (compatible; PounceBot/1.0; +https://pounce.ch)', - 'Accept': 'application/rdap+json, application/json', - } - async with httpx.AsyncClient(timeout=10, headers=headers) as client: + async with httpx.AsyncClient(timeout=10) as client: resp = await client.get(url) # 404 = Domain not found = AVAILABLE! @@ -122,7 +87,7 @@ async def check_drop_status(domain: str) -> DropStatus: rdap_status=[], can_register_now=True, should_monitor=False, - message="Available now!" + message="Domain is available for registration!" ) # 200 = Domain exists in registry @@ -133,16 +98,16 @@ async def check_drop_status(domain: str) -> DropStatus: # Extract deletion date from events deletion_date = None - expiration_date = None events = data.get('events', []) for event in events: action = event.get('eventAction', '').lower() date_str = event.get('eventDate', '') - - if 'deletion' in action: - deletion_date = date_str[:10] if date_str else None - elif 'expiration' in action: - expiration_date = date_str[:10] if date_str else None + if action in ('deletion', 'expiration') and date_str: + try: + # Parse ISO date + deletion_date = datetime.fromisoformat(date_str.replace('Z', '+00:00')) + except (ValueError, TypeError): + pass # Check for pending delete / redemption status is_pending = any(x in status_lower for x in [ @@ -153,25 +118,14 @@ async def check_drop_status(domain: str) -> DropStatus: ]) if is_pending: - # Calculate estimated availability - estimated = None - if deletion_date: - try: - del_dt = datetime.strptime(deletion_date, '%Y-%m-%d') - # For most TLDs, domain is available shortly after deletion date - estimated = del_dt.strftime('%Y-%m-%d') - except Exception: - pass - return DropStatus( domain=domain, status='dropping_soon', rdap_status=rdap_status, can_register_now=False, should_monitor=True, - message="Dropping soon - track to get notified!", + message="Domain is being deleted. Track it to get notified when available!", deletion_date=deletion_date, - estimated_available=estimated, ) # Domain is actively registered @@ -181,7 +135,8 @@ async def check_drop_status(domain: str) -> DropStatus: rdap_status=rdap_status, can_register_now=False, should_monitor=False, - message="Re-registered" + message="Domain was re-registered", + deletion_date=None, ) # Other status code @@ -192,17 +147,18 @@ async def check_drop_status(domain: str) -> DropStatus: rdap_status=[], can_register_now=False, should_monitor=False, - message=f"RDAP {resp.status_code}" + message=f"RDAP returned HTTP {resp.status_code}" ) except httpx.TimeoutException: + logger.warning(f"RDAP timeout for {domain}") return DropStatus( domain=domain, status='unknown', rdap_status=[], can_register_now=False, should_monitor=False, - message="Timeout" + message="RDAP timeout" ) except Exception as e: logger.warning(f"RDAP error for {domain}: {e}") @@ -216,32 +172,46 @@ async def check_drop_status(domain: str) -> DropStatus: ) -async def batch_check_drops(domains: list[tuple[int, str]], delay: float = 0.5) -> list[tuple[int, DropStatus]]: +# Rate limiting: max requests per second per TLD +RATE_LIMITS = { + 'default': 5, # 5 requests per second + 'ch': 10, # Swiss registry is faster + 'li': 10, +} + + +async def check_drops_batch( + domains: list[tuple[int, str]], # List of (id, full_domain) + delay_between_requests: float = 0.2, # 200ms = 5 req/s +) -> list[tuple[int, DropStatus]]: """ - Check status for multiple domains with rate limiting. + Check multiple drops with rate limiting. Args: - domains: List of (id, domain_name) tuples - delay: Delay between checks in seconds (to avoid rate limiting) - - Returns: - List of (id, DropStatus) tuples - """ - import asyncio + domains: List of (drop_id, full_domain) tuples + delay_between_requests: Seconds to wait between requests (default 200ms) + Returns: + List of (drop_id, DropStatus) tuples + """ results = [] - # Process sequentially with delay to avoid rate limiting (429) - for i, (drop_id, domain) in enumerate(domains): - status = await check_drop_status(domain) - results.append((drop_id, status)) + for drop_id, domain in domains: + try: + status = await check_drop_status(domain) + results.append((drop_id, status)) + except Exception as e: + logger.error(f"Batch check failed for {domain}: {e}") + results.append((drop_id, DropStatus( + domain=domain, + status='unknown', + rdap_status=[], + can_register_now=False, + should_monitor=False, + message=str(e), + ))) - # Add delay between requests to avoid rate limiting - if i < len(domains) - 1: - await asyncio.sleep(delay) - - # Log progress every 50 domains - if (i + 1) % 50 == 0: - logger.info(f"Checked {i + 1}/{len(domains)} drops...") + # Rate limit + await asyncio.sleep(delay_between_requests) return results diff --git a/backend/app/services/zone_file.py b/backend/app/services/zone_file.py index 1e0846a..509d6e1 100644 --- a/backend/app/services/zone_file.py +++ b/backend/app/services/zone_file.py @@ -374,7 +374,7 @@ async def get_dropped_domains( "has_hyphen": item.has_hyphen, "availability_status": getattr(item, 'availability_status', 'unknown') or 'unknown', "last_status_check": item.last_status_check.isoformat() if getattr(item, 'last_status_check', None) else None, - "deletion_date": getattr(item, 'deletion_date', None), + "deletion_date": item.deletion_date.isoformat() if getattr(item, 'deletion_date', None) else None, } for item in items ] @@ -458,15 +458,16 @@ async def cleanup_old_snapshots(db: AsyncSession, keep_days: int = 7) -> int: async def verify_drops_availability( db: AsyncSession, - batch_size: int = 100, - max_checks: int = 500 + batch_size: int = 50, + max_checks: int = 200 ) -> dict: """ - Verify availability of dropped domains and remove those that are no longer available. + Verify availability of dropped domains and update their status. - This runs periodically to clean up the drops list by checking if domains - have been re-registered. If a domain is no longer available (taken), - it's removed from the drops list. + This runs periodically to check the real RDAP status of drops. + Updates availability_status and deletion_date fields. + + Rate limited: ~200ms between requests = ~5 req/sec Args: db: Database session @@ -474,20 +475,28 @@ async def verify_drops_availability( max_checks: Maximum domains to check per run (to avoid overload) Returns: - dict with stats: checked, removed, errors + dict with stats: checked, available, dropping_soon, taken, errors """ - from sqlalchemy import delete - from app.services.domain_checker import domain_checker + from sqlalchemy import update + from app.services.drop_status_checker import check_drop_status - logger.info(f"Starting drops availability verification (max {max_checks} checks)...") + logger.info(f"Starting drops status update (max {max_checks} checks)...") - # Get drops from last 24h that haven't been verified recently + # Get drops that haven't been checked recently (prioritize unchecked and short domains) cutoff = datetime.utcnow() - timedelta(hours=24) + check_cutoff = datetime.utcnow() - timedelta(hours=2) # Re-check every 2 hours query = ( select(DroppedDomain) .where(DroppedDomain.dropped_date >= cutoff) - .order_by(DroppedDomain.length.asc()) # Check short domains first (more valuable) + .where( + (DroppedDomain.last_status_check == None) | # Never checked + (DroppedDomain.last_status_check < check_cutoff) # Not checked recently + ) + .order_by( + DroppedDomain.availability_status.desc(), # Unknown first + DroppedDomain.length.asc() # Then short domains + ) .limit(max_checks) ) @@ -495,59 +504,59 @@ async def verify_drops_availability( drops = result.scalars().all() if not drops: - logger.info("No drops to verify") - return {"checked": 0, "removed": 0, "errors": 0, "available": 0} + logger.info("No drops need status update") + return {"checked": 0, "available": 0, "dropping_soon": 0, "taken": 0, "errors": 0} checked = 0 - removed = 0 + stats = {"available": 0, "dropping_soon": 0, "taken": 0, "unknown": 0} errors = 0 - available = 0 - domains_to_remove = [] - logger.info(f"Verifying {len(drops)} dropped domains...") + logger.info(f"Checking {len(drops)} dropped domains...") for i, drop in enumerate(drops): + full_domain = f"{drop.domain}.{drop.tld}" try: - # Quick DNS-only check for speed - result = await domain_checker.check_domain(drop.domain) + status_result = await check_drop_status(full_domain) checked += 1 + stats[status_result.status] = stats.get(status_result.status, 0) + 1 - if result.is_available: - available += 1 - else: - # Domain is taken - mark for removal - domains_to_remove.append(drop.id) - logger.debug(f"Domain {drop.domain} is now taken, marking for removal") + # Update in DB + await db.execute( + update(DroppedDomain) + .where(DroppedDomain.id == drop.id) + .values( + availability_status=status_result.status, + rdap_status=str(status_result.rdap_status)[:255] if status_result.rdap_status else None, + last_status_check=datetime.utcnow(), + deletion_date=status_result.deletion_date, + ) + ) - # Log progress every 50 domains - if (i + 1) % 50 == 0: - logger.info(f"Verified {i + 1}/{len(drops)} domains, {len(domains_to_remove)} taken so far") + # Log progress every 25 domains + if (i + 1) % 25 == 0: + logger.info(f"Checked {i + 1}/{len(drops)}: {stats}") + await db.commit() # Commit in batches - # Small delay to avoid hammering DNS - if i % 10 == 0: - await asyncio.sleep(0.1) + # Rate limit: 200ms between requests (5 req/sec) + await asyncio.sleep(0.2) except Exception as e: errors += 1 - logger.warning(f"Error checking {drop.domain}: {e}") + logger.warning(f"Error checking {full_domain}: {e}") - # Remove taken domains in batch - if domains_to_remove: - stmt = delete(DroppedDomain).where(DroppedDomain.id.in_(domains_to_remove)) - await db.execute(stmt) - await db.commit() - removed = len(domains_to_remove) - logger.info(f"Removed {removed} taken domains from drops list") + # Final commit + await db.commit() logger.info( - f"Drops verification complete: " - f"{checked} checked, {available} still available, " - f"{removed} removed (taken), {errors} errors" + f"Drops status update complete: " + f"{checked} checked, {stats['available']} available, " + f"{stats['dropping_soon']} dropping_soon, {stats['taken']} taken, {errors} errors" ) return { "checked": checked, - "removed": removed, - "errors": errors, - "available": available + "available": stats['available'], + "dropping_soon": stats['dropping_soon'], + "taken": stats['taken'], + "errors": errors } diff --git a/frontend/src/components/hunt/DropsTab.tsx b/frontend/src/components/hunt/DropsTab.tsx index 37e8168..94faf12 100644 --- a/frontend/src/components/hunt/DropsTab.tsx +++ b/frontend/src/components/hunt/DropsTab.tsx @@ -175,7 +175,12 @@ export function DropsTab({ showToast }: DropsTabProps) { // Update the item in our list setItems(prev => prev.map(item => item.id === dropId - ? { ...item, availability_status: result.status, last_status_check: new Date().toISOString() } + ? { + ...item, + availability_status: result.status, + last_status_check: new Date().toISOString(), + deletion_date: result.deletion_date, + } : item )) @@ -186,6 +191,25 @@ export function DropsTab({ showToast }: DropsTabProps) { setCheckingStatus(null) } }, [checkingStatus, showToast]) + + // Format countdown from deletion date + const formatCountdown = useCallback((deletionDate: string | null): string | null => { + if (!deletionDate) return null + + const del = new Date(deletionDate) + const now = new Date() + const diff = del.getTime() - now.getTime() + + if (diff <= 0) return 'Now' + + const days = Math.floor(diff / (1000 * 60 * 60 * 24)) + const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)) + const mins = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)) + + if (days > 0) return `${days}d ${hours}h` + if (hours > 0) return `${hours}h ${mins}m` + return `${mins}m` + }, []) // Track a drop (add to watchlist) const trackDrop = useCallback(async (dropId: number, domain: string) => { @@ -253,24 +277,6 @@ export function DropsTab({ showToast }: DropsTabProps) { showOnlyAvailable, ].filter(Boolean).length - // Format countdown to deletion date - const formatCountdown = (deletionDate: string | null) => { - if (!deletionDate) return null - - const now = new Date() - const target = new Date(deletionDate + 'T23:59:59') - const diffMs = target.getTime() - now.getTime() - - if (diffMs <= 0) return 'Now' - - const days = Math.floor(diffMs / (1000 * 60 * 60 * 24)) - const hours = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)) - - if (days > 0) return `${days}d ${hours}h` - if (hours > 0) return `${hours}h` - return '<1h' - } - const formatTime = (iso: string) => { const d = new Date(iso) const now = new Date() @@ -553,15 +559,12 @@ export function DropsTab({ showToast }: DropsTabProps) { const isTrackingThis = trackingDrop === item.id const status = item.availability_status || 'unknown' - // Countdown for dropping_soon domains - const countdown = formatCountdown(item.deletion_date) - // Simplified status display config const statusConfig = { available: { label: 'Available', color: 'text-accent', bg: 'bg-accent/10', border: 'border-accent/30', icon: CheckCircle2 }, - dropping_soon: { label: countdown || 'Dropping', color: 'text-amber-400', bg: 'bg-amber-400/10', border: 'border-amber-400/30', icon: Clock }, + dropping_soon: { label: 'Dropping Soon', color: 'text-amber-400', bg: 'bg-amber-400/10', border: 'border-amber-400/30', icon: Clock }, taken: { label: 'Taken', color: 'text-rose-400', bg: 'bg-rose-400/10', border: 'border-rose-400/30', icon: Ban }, - unknown: { label: 'Pending', color: 'text-white/50', bg: 'bg-white/5', border: 'border-white/20', icon: Clock }, + unknown: { label: 'Check', color: 'text-white/50', bg: 'bg-white/5', border: 'border-white/20', icon: Search }, }[status] const StatusIcon = statusConfig.icon @@ -596,7 +599,9 @@ export function DropsTab({ showToast }: DropsTabProps) { )} > {isChecking ? : } - {statusConfig.label} + {status === 'dropping_soon' && item.deletion_date + ? formatCountdown(item.deletion_date) + : statusConfig.label} @@ -689,7 +694,9 @@ export function DropsTab({ showToast }: DropsTabProps) { title="Click to check real-time status" > {isChecking ? : } - {statusConfig.label} + {status === 'dropping_soon' && item.deletion_date + ? formatCountdown(item.deletion_date) + : statusConfig.label} diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 7cd2594..7bf7ab6 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -2073,10 +2073,11 @@ class AdminApiClient extends ApiClient { id: number domain: string status: 'available' | 'dropping_soon' | 'taken' | 'unknown' - deletion_date: string | null + rdap_status: string[] can_register_now: boolean should_track: boolean message: string + deletion_date: string | null }>(`/drops/check-status/${dropId}`, { method: 'POST' }) } @@ -2087,14 +2088,6 @@ class AdminApiClient extends ApiClient { message: string }>(`/drops/track/${dropId}`, { method: 'POST' }) } - - async batchCheckDrops(limit: number = 50) { - return this.request<{ - message: string - checking?: number - checked?: number - }>(`/drops/batch-check?limit=${limit}`, { method: 'POST' }) - } } // Yield Types