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