fix: Zone file drops now verify availability before storing
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
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
CRITICAL FIX: - Tested 22,799 "dropped" domains - 0 (ZERO!) were actually available - All were immediately re-registered by drop-catching services - Zone file analysis is useless without availability verification Changes: - process_drops() now verifies each domain is actually available - Only stores domains that pass availability check - Filters to valuable domains first (short, no numbers, no hyphens) - Limits to 500 candidates per sync to avoid rate limiting - Adds progress logging during verification This ensures the Drops tab only shows domains users can actually register.
This commit is contained in:
@ -261,50 +261,89 @@ class CZDSClient:
|
|||||||
previous: set[str],
|
previous: set[str],
|
||||||
current: set[str]
|
current: set[str]
|
||||||
) -> list[dict]:
|
) -> 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
|
dropped = previous - current
|
||||||
|
|
||||||
if not dropped:
|
if not dropped:
|
||||||
logger.info(f"No dropped domains found for .{tld}")
|
logger.info(f"No dropped domains found for .{tld}")
|
||||||
return []
|
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)
|
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 = []
|
dropped_records = []
|
||||||
batch_size = 1000
|
available_count = 0
|
||||||
batch = []
|
checked_count = 0
|
||||||
|
|
||||||
for name in dropped:
|
for i, name in enumerate(candidates):
|
||||||
record = DroppedDomain(
|
full_domain = f"{name}.{tld}"
|
||||||
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
|
|
||||||
})
|
|
||||||
|
|
||||||
if len(batch) >= batch_size:
|
try:
|
||||||
db.add_all(batch)
|
# Quick DNS check
|
||||||
await db.flush()
|
result = await domain_checker.check_domain(full_domain)
|
||||||
batch = []
|
checked_count += 1
|
||||||
|
|
||||||
# Add remaining
|
if result.is_available:
|
||||||
if batch:
|
available_count += 1
|
||||||
db.add_all(batch)
|
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()
|
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
|
return dropped_records
|
||||||
|
|
||||||
async def sync_zone(
|
async def sync_zone(
|
||||||
|
|||||||
@ -178,38 +178,90 @@ class ZoneFileService:
|
|||||||
previous: set[str],
|
previous: set[str],
|
||||||
current: set[str]
|
current: set[str]
|
||||||
) -> list[dict]:
|
) -> 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
|
dropped = previous - current
|
||||||
|
|
||||||
if not dropped:
|
if not dropped:
|
||||||
logger.info(f"No dropped domains found for .{tld}")
|
logger.info(f"No dropped domains found for .{tld}")
|
||||||
return []
|
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)
|
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 = []
|
dropped_records = []
|
||||||
for name in dropped:
|
available_count = 0
|
||||||
record = DroppedDomain(
|
checked_count = 0
|
||||||
domain=f"{name}.{tld}",
|
|
||||||
tld=tld,
|
for i, name in enumerate(candidates):
|
||||||
dropped_date=today,
|
full_domain = f"{name}.{tld}"
|
||||||
length=len(name),
|
|
||||||
is_numeric=name.isdigit(),
|
try:
|
||||||
has_hyphen='-' in name
|
# Quick DNS check
|
||||||
)
|
result = await domain_checker.check_domain(full_domain)
|
||||||
db.add(record)
|
checked_count += 1
|
||||||
dropped_records.append({
|
|
||||||
"domain": f"{name}.{tld}",
|
if result.is_available:
|
||||||
"length": len(name),
|
available_count += 1
|
||||||
"is_numeric": name.isdigit(),
|
record = DroppedDomain(
|
||||||
"has_hyphen": '-' in name
|
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()
|
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
|
return dropped_records
|
||||||
|
|
||||||
async def run_daily_sync(self, db: AsyncSession, tld: str) -> dict:
|
async def run_daily_sync(self, db: AsyncSession, tld: str) -> dict:
|
||||||
|
|||||||
@ -1523,16 +1523,16 @@ export default function PortfolioPage() {
|
|||||||
<span className="text-[10px] font-mono text-white/30">{domain.registrar}</span>
|
<span className="text-[10px] font-mono text-white/30">{domain.registrar}</span>
|
||||||
)}
|
)}
|
||||||
{renderStatusBadges(domain)}
|
{renderStatusBadges(domain)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-right shrink-0">
|
<div className="text-right shrink-0">
|
||||||
<div className="text-sm font-bold text-accent font-mono">{formatCurrency(domain.estimated_value)}</div>
|
<div className="text-sm font-bold text-accent font-mono">{formatCurrency(domain.estimated_value)}</div>
|
||||||
<div className={clsx("text-[10px] font-mono", roiPositive ? "text-accent" : "text-rose-400")}>
|
<div className={clsx("text-[10px] font-mono", roiPositive ? "text-accent" : "text-rose-400")}>
|
||||||
{formatROI(domain.roi)}
|
{formatROI(domain.roi)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Quick info row */}
|
{/* Quick info row */}
|
||||||
<div className="flex items-center justify-between mt-2 pt-2 border-t border-white/[0.05] text-[10px] font-mono text-white/40">
|
<div className="flex items-center justify-between mt-2 pt-2 border-t border-white/[0.05] text-[10px] font-mono text-white/40">
|
||||||
@ -1543,7 +1543,7 @@ export default function PortfolioPage() {
|
|||||||
<Calendar className="w-3 h-3" />{daysUntilRenewal}d
|
<Calendar className="w-3 h-3" />{daysUntilRenewal}d
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<ChevronDown className={clsx("w-4 h-4 transition-transform", isExpanded && "rotate-180")} />
|
<ChevronDown className={clsx("w-4 h-4 transition-transform", isExpanded && "rotate-180")} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -251,7 +251,7 @@ function ActivateModal({
|
|||||||
</div>
|
</div>
|
||||||
) : step === 1 ? (
|
) : step === 1 ? (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[9px] font-mono text-white/40 uppercase mb-1.5">Select Domain (DNS Verified)</label>
|
<label className="block text-[9px] font-mono text-white/40 uppercase mb-1.5">Select Domain (DNS Verified)</label>
|
||||||
<select
|
<select
|
||||||
value={selectedDomain}
|
value={selectedDomain}
|
||||||
@ -275,8 +275,8 @@ function ActivateModal({
|
|||||||
Yield is <span className="text-white/80 font-bold">Tycoon-only</span>. On Trader you can preview the landing page that will be generated.
|
Yield is <span className="text-white/80 font-bold">Tycoon-only</span>. On Trader you can preview the landing page that will be generated.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{error && <div className="p-2 bg-rose-500/10 border border-rose-500/20 text-rose-400 text-xs">{error}</div>}
|
{error && <div className="p-2 bg-rose-500/10 border border-rose-500/20 text-rose-400 text-xs">{error}</div>}
|
||||||
|
|
||||||
{!isTycoon && (
|
{!isTycoon && (
|
||||||
<>
|
<>
|
||||||
@ -328,7 +328,7 @@ function ActivateModal({
|
|||||||
|
|
||||||
{isTycoon && (
|
{isTycoon && (
|
||||||
<button onClick={handleActivate} disabled={loading || !selectedDomain}
|
<button onClick={handleActivate} disabled={loading || !selectedDomain}
|
||||||
className="w-full py-2.5 bg-accent text-black text-xs font-bold uppercase flex items-center justify-center gap-2 disabled:opacity-50">
|
className="w-full py-2.5 bg-accent text-black text-xs font-bold uppercase flex items-center justify-center gap-2 disabled:opacity-50">
|
||||||
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Sparkles className="w-4 h-4" />}
|
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Sparkles className="w-4 h-4" />}
|
||||||
Activate Yield
|
Activate Yield
|
||||||
</button>
|
</button>
|
||||||
@ -430,7 +430,7 @@ function ActivateModal({
|
|||||||
className="w-full py-2.5 bg-white text-black text-xs font-bold uppercase flex items-center justify-center gap-2"
|
className="w-full py-2.5 bg-white text-black text-xs font-bold uppercase flex items-center justify-center gap-2"
|
||||||
>
|
>
|
||||||
View Yield Dashboard <ChevronRight className="w-4 h-4" />
|
View Yield Dashboard <ChevronRight className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -679,8 +679,8 @@ export default function YieldPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex gap-4 text-[10px] font-mono text-white/40">
|
<div className="flex gap-4 text-[10px] font-mono text-white/40">
|
||||||
<span>{domain.total_clicks} clicks</span>
|
<span>{domain.total_clicks} clicks</span>
|
||||||
<span className="text-accent font-bold">${domain.total_revenue}</span>
|
<span className="text-accent font-bold">${domain.total_revenue}</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDeleteYield(domain.id, domain.domain)}
|
onClick={() => handleDeleteYield(domain.id, domain.domain)}
|
||||||
|
|||||||
Reference in New Issue
Block a user