diff --git a/backend/app/services/czds_client.py b/backend/app/services/czds_client.py index 5e3602d..4bf3311 100644 --- a/backend/app/services/czds_client.py +++ b/backend/app/services/czds_client.py @@ -261,50 +261,89 @@ class CZDSClient: previous: set[str], current: set[str] ) -> list[dict]: - """Find and store dropped domains.""" + """ + Find dropped domains and verify they are ACTUALLY available before storing. + + Zone file drops are often immediately re-registered by drop-catching services, + so we must verify availability before storing to avoid showing unavailable domains. + """ + from app.services.domain_checker import domain_checker + dropped = previous - current if not dropped: logger.info(f"No dropped domains found for .{tld}") return [] - logger.info(f"Found {len(dropped):,} dropped domains for .{tld}") + logger.info(f"Found {len(dropped):,} potential drops for .{tld}, verifying availability...") today = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) - # Batch insert for performance + # Filter to valuable domains first (short, no numbers, no hyphens) + valuable_drops = [ + name for name in dropped + if len(name) <= 10 and not name.isdigit() and '-' not in name + ] + + # Also include some longer domains (up to 500 total) + other_drops = [ + name for name in dropped + if name not in valuable_drops and len(name) <= 15 + ][:max(0, 500 - len(valuable_drops))] + + candidates = valuable_drops + other_drops + logger.info(f"Checking availability of {len(candidates)} candidates (of {len(dropped):,} total drops)") + + # Verify availability and only store truly available domains dropped_records = [] - batch_size = 1000 - batch = [] + available_count = 0 + checked_count = 0 - for name in dropped: - record = DroppedDomain( - domain=f"{name}.{tld}", - tld=tld, - dropped_date=today, - length=len(name), - is_numeric=name.isdigit(), - has_hyphen='-' in name - ) - batch.append(record) - dropped_records.append({ - "domain": f"{name}.{tld}", - "length": len(name), - "is_numeric": name.isdigit(), - "has_hyphen": '-' in name - }) + for i, name in enumerate(candidates): + full_domain = f"{name}.{tld}" - if len(batch) >= batch_size: - db.add_all(batch) - await db.flush() - batch = [] - - # Add remaining - if batch: - db.add_all(batch) + try: + # Quick DNS check + result = await domain_checker.check_domain(full_domain) + checked_count += 1 + + if result.is_available: + available_count += 1 + record = DroppedDomain( + domain=full_domain, + tld=tld, + dropped_date=today, + length=len(name), + is_numeric=name.isdigit(), + has_hyphen='-' in name + ) + db.add(record) + dropped_records.append({ + "domain": full_domain, + "length": len(name), + "is_numeric": name.isdigit(), + "has_hyphen": '-' in name + }) + + # Progress log every 100 domains + if (i + 1) % 100 == 0: + logger.info(f"Verified {i + 1}/{len(candidates)}: {available_count} available so far") + + # Small delay to avoid rate limiting + if i % 20 == 0: + await asyncio.sleep(0.1) + + except Exception as e: + logger.warning(f"Error checking {full_domain}: {e}") await db.commit() + logger.info( + f"CZDS drops for .{tld}: " + f"{checked_count} verified, {available_count} actually available, " + f"{len(dropped_records)} stored" + ) + return dropped_records async def sync_zone( diff --git a/backend/app/services/zone_file.py b/backend/app/services/zone_file.py index 32ba051..78e3fe9 100644 --- a/backend/app/services/zone_file.py +++ b/backend/app/services/zone_file.py @@ -178,38 +178,90 @@ class ZoneFileService: previous: set[str], current: set[str] ) -> list[dict]: - """Find and store dropped domains""" + """ + Find dropped domains and verify they are ACTUALLY available before storing. + + Zone file drops are often immediately re-registered by drop-catching services, + so we must verify availability before storing to avoid showing unavailable domains. + """ + from app.services.domain_checker import domain_checker + dropped = previous - current if not dropped: logger.info(f"No dropped domains found for .{tld}") return [] - logger.info(f"Found {len(dropped)} dropped domains for .{tld}") + logger.info(f"Found {len(dropped)} potential drops for .{tld}, verifying availability...") today = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) - # Store dropped domains + # Filter to valuable domains first (short, no numbers, no hyphens) + # This reduces the number of availability checks needed + valuable_drops = [ + name for name in dropped + if len(name) <= 10 and not name.isdigit() and '-' not in name + ] + + # Also include some longer domains (up to 500 total) + other_drops = [ + name for name in dropped + if name not in valuable_drops and len(name) <= 15 + ][:max(0, 500 - len(valuable_drops))] + + candidates = valuable_drops + other_drops + logger.info(f"Checking availability of {len(candidates)} candidates (of {len(dropped)} total drops)") + + # Verify availability and only store truly available domains dropped_records = [] - for name in dropped: - record = DroppedDomain( - domain=f"{name}.{tld}", - tld=tld, - dropped_date=today, - length=len(name), - is_numeric=name.isdigit(), - has_hyphen='-' in name - ) - db.add(record) - dropped_records.append({ - "domain": f"{name}.{tld}", - "length": len(name), - "is_numeric": name.isdigit(), - "has_hyphen": '-' in name - }) + available_count = 0 + checked_count = 0 + + for i, name in enumerate(candidates): + full_domain = f"{name}.{tld}" + + try: + # Quick DNS check + result = await domain_checker.check_domain(full_domain) + checked_count += 1 + + if result.is_available: + available_count += 1 + record = DroppedDomain( + domain=full_domain, + tld=tld, + dropped_date=today, + length=len(name), + is_numeric=name.isdigit(), + has_hyphen='-' in name + ) + db.add(record) + dropped_records.append({ + "domain": full_domain, + "length": len(name), + "is_numeric": name.isdigit(), + "has_hyphen": '-' in name + }) + + # Progress log every 100 domains + if (i + 1) % 100 == 0: + logger.info(f"Verified {i + 1}/{len(candidates)}: {available_count} available so far") + + # Small delay to avoid rate limiting + if i % 20 == 0: + await asyncio.sleep(0.1) + + except Exception as e: + logger.warning(f"Error checking {full_domain}: {e}") await db.commit() + logger.info( + f"Zone file drops for .{tld}: " + f"{checked_count} verified, {available_count} actually available, " + f"{len(dropped_records)} stored" + ) + return dropped_records async def run_daily_sync(self, db: AsyncSession, tld: str) -> dict: diff --git a/frontend/src/app/terminal/portfolio/page.tsx b/frontend/src/app/terminal/portfolio/page.tsx index 849d195..9d7473d 100755 --- a/frontend/src/app/terminal/portfolio/page.tsx +++ b/frontend/src/app/terminal/portfolio/page.tsx @@ -1523,17 +1523,17 @@ export default function PortfolioPage() { {domain.registrar} )} {renderStatusBadges(domain)} + + - -