Deploy: 2025-12-19 12:34
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

This commit is contained in:
2025-12-19 12:34:31 +01:00
parent 06976674d3
commit c35548c9d4
2 changed files with 96 additions and 33 deletions

View File

@ -735,6 +735,15 @@ def setup_scheduler():
replace_existing=True, replace_existing=True,
) )
# Drops RDAP status update (every 15 minutes - check real status)
scheduler.add_job(
update_drops_status,
CronTrigger(minute='*/15'), # Every 15 minutes
id="drops_status_update",
name="Drops Status Update (15-min)",
replace_existing=True,
)
logger.info( logger.info(
f"Scheduler configured:" f"Scheduler configured:"
f"\n - Scout domain check at {settings.check_hour:02d}:{settings.check_minute:02d} (daily)" f"\n - Scout domain check at {settings.check_hour:02d}:{settings.check_minute:02d} (daily)"
@ -1033,6 +1042,91 @@ async def verify_drops():
logger.exception(f"Drops verification failed: {e}") logger.exception(f"Drops verification failed: {e}")
async def update_drops_status():
"""
Update RDAP status for dropped domains.
This job runs every 30 minutes to check the real status of drops
(available, dropping_soon, taken) and store it in the database.
This way users see the status instantly without needing to check manually.
"""
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 30 minutes
# Or have never been checked
thirty_min_ago = datetime.utcnow() - timedelta(minutes=30)
query = (
select(DroppedDomain)
.where(
(DroppedDomain.availability_status == 'unknown') |
(DroppedDomain.last_status_check == None) |
(DroppedDomain.last_status_check < thirty_min_ago)
)
.order_by(DroppedDomain.length.asc()) # Short domains first
.limit(200) # Process 200 per run
)
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...")
# Prepare domain list
domains_to_check = [(d.id, f"{d.domain}.{d.tld}") for d in drops]
# Batch check with rate limiting
results = await batch_check_drops(domains_to_check)
# Update database
available_count = 0
dropping_soon_count = 0
taken_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
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"
)
except Exception as e:
logger.exception(f"Drops status update failed: {e}")
async def sync_zone_files(): async def sync_zone_files():
"""Sync zone files from Switch.ch (.ch, .li) and ICANN CZDS (gTLDs).""" """Sync zone files from Switch.ch (.ch, .li) and ICANN CZDS (gTLDs)."""
logger.info("Starting zone file sync...") logger.info("Starting zone file sync...")

View File

@ -156,29 +156,6 @@ export function DropsTab({ showToast }: DropsTabProps) {
loadDrops(1) loadDrops(1)
}, [loadDrops]) }, [loadDrops])
// Auto batch-check status when drops are loaded
const [batchChecking, setBatchChecking] = useState(false)
useEffect(() => {
// Check if we have unknown status drops
const unknownCount = items.filter(i => i.availability_status === 'unknown').length
if (unknownCount > 0 && !batchChecking && items.length > 0) {
setBatchChecking(true)
// Trigger batch check in background
api.batchCheckDrops(50).then(() => {
// Reload drops after a short delay to get updated status
setTimeout(() => {
loadDrops(page, false)
setBatchChecking(false)
}, 3000)
}).catch(() => {
setBatchChecking(false)
})
}
}, [items, batchChecking, loadDrops, page])
const handlePageChange = useCallback((newPage: number) => { const handlePageChange = useCallback((newPage: number) => {
setPage(newPage) setPage(newPage)
loadDrops(newPage) loadDrops(newPage)
@ -187,8 +164,6 @@ export function DropsTab({ showToast }: DropsTabProps) {
const handleRefresh = useCallback(async () => { const handleRefresh = useCallback(async () => {
await loadDrops(page, true) await loadDrops(page, true)
await loadStats() await loadStats()
// Also trigger batch check
api.batchCheckDrops(50).catch(() => {})
}, [loadDrops, loadStats, page]) }, [loadDrops, loadStats, page])
// Check real-time status of a drop // Check real-time status of a drop
@ -523,12 +498,6 @@ export function DropsTab({ showToast }: DropsTabProps) {
<span>{availableCount} available now</span> <span>{availableCount} available now</span>
</div> </div>
)} )}
{batchChecking && (
<div className="flex items-center gap-2 text-amber-400">
<Loader2 className="w-3 h-3 animate-spin" />
<span>Checking status...</span>
</div>
)}
</div> </div>
{totalPages > 1 && !showOnlyAvailable && ( {totalPages > 1 && !showOnlyAvailable && (
<span className="text-[11px] font-mono text-white/30 uppercase tracking-widest"> <span className="text-[11px] font-mono text-white/30 uppercase tracking-widest">
@ -590,9 +559,9 @@ export function DropsTab({ showToast }: DropsTabProps) {
// Simplified status display config // Simplified status display config
const statusConfig = { const statusConfig = {
available: { label: 'Available', color: 'text-accent', bg: 'bg-accent/10', border: 'border-accent/30', icon: CheckCircle2 }, available: { label: 'Available', color: 'text-accent', bg: 'bg-accent/10', border: 'border-accent/30', icon: CheckCircle2 },
dropping_soon: { label: countdown || 'Soon', color: 'text-amber-400', bg: 'bg-amber-400/10', border: 'border-amber-400/30', icon: Clock }, dropping_soon: { label: countdown || 'Dropping', 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 }, taken: { label: 'Taken', color: 'text-rose-400', bg: 'bg-rose-400/10', border: 'border-rose-400/30', icon: Ban },
unknown: { label: batchChecking ? '...' : 'Check', color: 'text-white/50', bg: 'bg-white/5', border: 'border-white/20', icon: batchChecking ? Loader2 : Search }, unknown: { label: 'Pending', color: 'text-white/50', bg: 'bg-white/5', border: 'border-white/20', icon: Clock },
}[status] }[status]
const StatusIcon = statusConfig.icon const StatusIcon = statusConfig.icon