diff --git a/backend/app/api/domains.py b/backend/app/api/domains.py index 1b48dcf..0c0a229 100644 --- a/backend/app/api/domains.py +++ b/backend/app/api/domains.py @@ -240,7 +240,7 @@ async def refresh_domain( current_user: CurrentUser, db: Database, ): - """Manually refresh domain availability status.""" + """Manually refresh domain availability status with a live check.""" result = await db.execute( select(Domain).where( Domain.id == domain_id, @@ -255,7 +255,10 @@ async def refresh_domain( detail="Domain not found", ) - # Check domain + # Track previous state for logging + was_available = domain.is_available + + # Check domain - always uses live data, no cache check_result = await domain_checker.check_domain(domain.name) # Update domain @@ -278,9 +281,97 @@ async def refresh_domain( await db.commit() await db.refresh(domain) + # Log status changes + if was_available != domain.is_available: + import logging + logger = logging.getLogger(__name__) + if was_available and not domain.is_available: + logger.info(f"Manual refresh: {domain.name} changed from AVAILABLE to TAKEN (registrar: {domain.registrar})") + else: + logger.info(f"Manual refresh: {domain.name} changed from TAKEN to AVAILABLE") + return domain +@router.post("/refresh-all") +async def refresh_all_domains( + current_user: CurrentUser, + db: Database, +): + """ + Refresh all domains in user's watchlist with live checks. + + This is useful for bulk updates and to ensure all data is current. + Returns summary of changes detected. + """ + import logging + logger = logging.getLogger(__name__) + + result = await db.execute( + select(Domain).where(Domain.user_id == current_user.id) + ) + domains = result.scalars().all() + + if not domains: + return {"message": "No domains to refresh", "checked": 0, "changes": []} + + checked = 0 + errors = 0 + changes = [] + + for domain in domains: + try: + was_available = domain.is_available + was_registrar = domain.registrar + + # Live check + check_result = await domain_checker.check_domain(domain.name) + + # Track changes + if was_available != check_result.is_available: + change_type = "became_available" if check_result.is_available else "became_taken" + changes.append({ + "domain": domain.name, + "change": change_type, + "old_registrar": was_registrar, + "new_registrar": check_result.registrar, + }) + logger.info(f"Bulk refresh: {domain.name} {change_type}") + + # Update domain + domain.status = check_result.status + domain.is_available = check_result.is_available + domain.registrar = check_result.registrar + domain.expiration_date = check_result.expiration_date + domain.last_checked = datetime.utcnow() + + # Create check record + check = DomainCheck( + domain_id=domain.id, + status=check_result.status, + is_available=check_result.is_available, + response_data=str(check_result.to_dict()), + checked_at=datetime.utcnow(), + ) + db.add(check) + + checked += 1 + + except Exception as e: + logger.error(f"Error refreshing {domain.name}: {e}") + errors += 1 + + await db.commit() + + return { + "message": f"Refreshed {checked} domains", + "checked": checked, + "errors": errors, + "changes": changes, + "total_domains": len(domains), + } + + class NotifyUpdate(BaseModel): """Schema for updating notification settings.""" notify: bool diff --git a/backend/app/scheduler.py b/backend/app/scheduler.py index 68d7b38..303df35 100644 --- a/backend/app/scheduler.py +++ b/backend/app/scheduler.py @@ -59,6 +59,10 @@ async def check_domains_by_frequency(frequency: str): Args: frequency: One of 'daily', 'hourly', 'realtime' (10-min) + + This function now detects BOTH transitions: + - taken -> available: Domain dropped, notify user to register + - available -> taken: Domain was registered, notify user they missed it """ logger.info(f"Starting {frequency} domain check...") start_time = datetime.utcnow() @@ -101,27 +105,51 @@ async def check_domains_by_frequency(frequency: str): checked = 0 errors = 0 newly_available = [] + newly_taken = [] # Track domains that became taken + status_changes = [] # All status changes for logging for domain in domains: try: # Check domain availability check_result = await domain_checker.check_domain(domain.name) - # Track if domain became available - was_taken = not domain.is_available + # Track status transitions + was_available = domain.is_available is_now_available = check_result.is_available - if was_taken and is_now_available and domain.notify_on_available: - newly_available.append(domain) + # Detect transition: taken -> available (domain dropped!) + if not was_available and is_now_available: + status_changes.append({ + 'domain': domain.name, + 'change': 'became_available', + 'old_registrar': domain.registrar, + }) + if domain.notify_on_available: + newly_available.append(domain) + logger.info(f"🎯 Domain AVAILABLE: {domain.name} (was registered by {domain.registrar})") - # Update domain + # Detect transition: available -> taken (someone registered it!) + elif was_available and not is_now_available: + status_changes.append({ + 'domain': domain.name, + 'change': 'became_taken', + 'new_registrar': check_result.registrar, + }) + if domain.notify_on_available: # Notify if alerts are on + newly_taken.append({ + 'domain': domain, + 'registrar': check_result.registrar, + }) + logger.info(f"⚠️ Domain TAKEN: {domain.name} (now registered by {check_result.registrar})") + + # Update domain with fresh data domain.status = check_result.status domain.is_available = check_result.is_available domain.registrar = check_result.registrar domain.expiration_date = check_result.expiration_date domain.last_checked = datetime.utcnow() - # Create check record + # Create check record for history check = DomainCheck( domain_id=domain.id, status=check_result.status, @@ -145,13 +173,19 @@ async def check_domains_by_frequency(frequency: str): elapsed = (datetime.utcnow() - start_time).total_seconds() logger.info( f"Domain check complete. Checked: {checked}, Errors: {errors}, " - f"Newly available: {len(newly_available)}, Time: {elapsed:.2f}s" + f"Newly available: {len(newly_available)}, Newly taken: {len(newly_taken)}, " + f"Total changes: {len(status_changes)}, Time: {elapsed:.2f}s" ) # Send notifications for newly available domains if newly_available: logger.info(f"Domains that became available: {[d.name for d in newly_available]}") await send_domain_availability_alerts(db, newly_available) + + # Send notifications for domains that got taken (user missed them!) + if newly_taken: + logger.info(f"Domains that were taken: {[d['domain'].name for d in newly_taken]}") + await send_domain_taken_alerts(db, newly_taken) async def check_all_domains(): @@ -766,6 +800,77 @@ async def send_domain_availability_alerts(db, domains: list[Domain]): logger.info(f"Sent {alerts_sent} domain availability alerts") +async def send_domain_taken_alerts(db, taken_domains: list[dict]): + """ + Send email alerts when watched available domains get registered by someone. + + This notifies users that a domain they were watching (and was available) + has now been taken - either by them or someone else. + + Args: + db: Database session + taken_domains: List of dicts with 'domain' (Domain object) and 'registrar' (str) + """ + if not email_service.is_configured(): + logger.info("Email service not configured, skipping domain taken alerts") + return + + alerts_sent = 0 + + for item in taken_domains: + domain = item['domain'] + registrar = item.get('registrar') or 'Unknown registrar' + + try: + # Get domain owner + result = await db.execute( + select(User).where(User.id == domain.user_id) + ) + user = result.scalar_one_or_none() + + if user and user.email: + # Send notification that the domain is no longer available + success = await email_service.send_email( + to_email=user.email, + subject=f"⚡ Domain Update: {domain.name} was registered", + html_content=f""" +
+ A domain on your watchlist has been registered: +
++ {domain.name} +
++ Registrar: {registrar} +
+
+ If you registered this domain yourself, congratulations! 🎉
+ If not, the domain might become available again in the future.
+