From 7a9d7703ca4d1913949e111a29382d46c20c6739 Mon Sep 17 00:00:00 2001 From: Yves Gugger Date: Mon, 15 Dec 2025 22:46:29 +0100 Subject: [PATCH] feat: optimize drops to 24h only, award-winning analyze panel --- backend/app/api/drops.py | 4 +- backend/app/scheduler.py | 29 + backend/app/services/zone_file.py | 59 +- .../src/components/analyze/AnalyzePanel.tsx | 511 +++++++++------ .../src/components/hunt/BrandableForgeTab.tsx | 49 -- frontend/src/components/hunt/DropsTab.tsx | 585 +++++++----------- frontend/src/components/hunt/SearchTab.tsx | 49 -- frontend/src/lib/api.ts | 6 +- 8 files changed, 639 insertions(+), 653 deletions(-) diff --git a/backend/app/api/drops.py b/backend/app/api/drops.py index b932be3..b76a450 100644 --- a/backend/app/api/drops.py +++ b/backend/app/api/drops.py @@ -55,7 +55,7 @@ async def api_get_zone_stats( @router.get("") async def api_get_drops( tld: Optional[str] = Query(None, description="Filter by TLD"), - days: int = Query(7, ge=1, le=30, description="Days to look back"), + hours: int = Query(24, ge=1, le=48, description="Hours to look back (max 48h, we only store 48h)"), min_length: Optional[int] = Query(None, ge=1, le=63, description="Minimum domain length"), max_length: Optional[int] = Query(None, ge=1, le=63, description="Maximum domain length"), exclude_numeric: bool = Query(False, description="Exclude numeric-only domains"), @@ -86,7 +86,7 @@ async def api_get_drops( result = await get_dropped_domains( db=db, tld=tld, - days=days, + hours=hours, min_length=min_length, max_length=max_length, exclude_numeric=exclude_numeric, diff --git a/backend/app/scheduler.py b/backend/app/scheduler.py index 3874939..3f366c8 100644 --- a/backend/app/scheduler.py +++ b/backend/app/scheduler.py @@ -675,6 +675,15 @@ def setup_scheduler(): replace_existing=True, ) + # Zone data cleanup (hourly - delete drops older than 48h) + scheduler.add_job( + cleanup_zone_data, + CronTrigger(minute=45), # Every hour at :45 + id="zone_cleanup", + name="Zone Data Cleanup (hourly)", + replace_existing=True, + ) + logger.info( f"Scheduler configured:" f"\n - Scout domain check at {settings.check_hour:02d}:{settings.check_minute:02d} (daily)" @@ -850,6 +859,26 @@ async def scrape_auctions(): logger.exception(f"Auction scrape failed: {e}") +async def cleanup_zone_data(): + """Clean up old zone file data to save storage.""" + logger.info("Starting zone data cleanup...") + + try: + from app.services.zone_file import cleanup_old_drops, cleanup_old_snapshots + + async with AsyncSessionLocal() as db: + # Delete drops older than 48h + drops_deleted = await cleanup_old_drops(db, hours=48) + + # Delete snapshots older than 7 days + snapshots_deleted = await cleanup_old_snapshots(db, keep_days=7) + + logger.info(f"Zone cleanup: {drops_deleted} drops, {snapshots_deleted} snapshots deleted") + + except Exception as e: + logger.exception(f"Zone data cleanup 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/zone_file.py b/backend/app/services/zone_file.py index 1618cd9..e2d6f5f 100644 --- a/backend/app/services/zone_file.py +++ b/backend/app/services/zone_file.py @@ -256,7 +256,7 @@ class ZoneFileService: async def get_dropped_domains( db: AsyncSession, tld: Optional[str] = None, - days: int = 7, + hours: int = 24, min_length: Optional[int] = None, max_length: Optional[int] = None, exclude_numeric: bool = False, @@ -267,8 +267,9 @@ async def get_dropped_domains( ) -> dict: """ Get recently dropped domains with filters. + Only returns drops from last 24-48h (we don't store older data). """ - cutoff = datetime.utcnow() - timedelta(days=days) + cutoff = datetime.utcnow() - timedelta(hours=hours) query = select(DroppedDomain).where(DroppedDomain.dropped_date >= cutoff) count_query = select(func.count(DroppedDomain.id)).where(DroppedDomain.dropped_date >= cutoff) @@ -336,13 +337,12 @@ async def get_zone_stats(db: AsyncSession) -> dict: ch_snapshot = ch_result.scalar_one_or_none() li_snapshot = li_result.scalar_one_or_none() - # Count recent drops - today = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) - week_ago = today - timedelta(days=7) + # Count drops from last 24h only + cutoff_24h = datetime.utcnow() - timedelta(hours=24) - drops_query = select(func.count(DroppedDomain.id)).where(DroppedDomain.dropped_date >= week_ago) + drops_query = select(func.count(DroppedDomain.id)).where(DroppedDomain.dropped_date >= cutoff_24h) drops_result = await db.execute(drops_query) - weekly_drops = drops_result.scalar() or 0 + daily_drops = drops_result.scalar() or 0 return { "ch": { @@ -353,5 +353,48 @@ async def get_zone_stats(db: AsyncSession) -> dict: "domain_count": li_snapshot.domain_count if li_snapshot else 0, "last_sync": li_snapshot.snapshot_date.isoformat() if li_snapshot else None }, - "weekly_drops": weekly_drops + "daily_drops": daily_drops } + + +async def cleanup_old_drops(db: AsyncSession, hours: int = 48) -> int: + """ + Delete dropped domains older than specified hours. + Default: Keep only last 48h for safety margin (24h display + 24h buffer). + Returns number of deleted records. + """ + from sqlalchemy import delete + + cutoff = datetime.utcnow() - timedelta(hours=hours) + + # Delete old drops + stmt = delete(DroppedDomain).where(DroppedDomain.dropped_date < cutoff) + result = await db.execute(stmt) + await db.commit() + + deleted = result.rowcount + if deleted > 0: + logger.info(f"Cleaned up {deleted} old dropped domains (older than {hours}h)") + + return deleted + + +async def cleanup_old_snapshots(db: AsyncSession, keep_days: int = 7) -> int: + """ + Delete zone snapshots older than specified days. + Keep at least 7 days of metadata for debugging. + Returns number of deleted records. + """ + from sqlalchemy import delete + + cutoff = datetime.utcnow() - timedelta(days=keep_days) + + stmt = delete(ZoneSnapshot).where(ZoneSnapshot.snapshot_date < cutoff) + result = await db.execute(stmt) + await db.commit() + + deleted = result.rowcount + if deleted > 0: + logger.info(f"Cleaned up {deleted} old zone snapshots (older than {keep_days}d)") + + return deleted diff --git a/frontend/src/components/analyze/AnalyzePanel.tsx b/frontend/src/components/analyze/AnalyzePanel.tsx index 8756402..63ffc57 100644 --- a/frontend/src/components/analyze/AnalyzePanel.tsx +++ b/frontend/src/components/analyze/AnalyzePanel.tsx @@ -2,23 +2,73 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import clsx from 'clsx' -import { X, RefreshCw, Search, Shield, Zap, Copy, ExternalLink } from 'lucide-react' +import { + X, + RefreshCw, + Copy, + ExternalLink, + Shield, + TrendingUp, + AlertTriangle, + DollarSign, + Check, + Zap, + Globe, + Calendar, + Link2, + Radio, + Eye, + ChevronDown, + ChevronUp, +} from 'lucide-react' import { api } from '@/lib/api' import { useAnalyzePanelStore } from '@/lib/analyze-store' import type { AnalyzeResponse, AnalyzeSection, AnalyzeItem } from '@/components/analyze/types' -function statusPill(status: string) { +// ============================================================================ +// HELPERS +// ============================================================================ + +function getStatusColor(status: string) { switch (status) { case 'pass': - return 'bg-accent/10 text-accent border-accent/20' + return { bg: 'bg-accent/10', text: 'text-accent', border: 'border-accent/30', icon: Check } case 'warn': - return 'bg-amber-400/10 text-amber-300 border-amber-400/20' + return { bg: 'bg-amber-400/10', text: 'text-amber-300', border: 'border-amber-400/30', icon: AlertTriangle } case 'fail': - return 'bg-red-500/10 text-red-300 border-red-500/20' - case 'na': - return 'bg-white/5 text-white/30 border-white/10' + return { bg: 'bg-red-500/10', text: 'text-red-400', border: 'border-red-500/30', icon: X } default: - return 'bg-white/5 text-white/50 border-white/10' + return { bg: 'bg-white/5', text: 'text-white/40', border: 'border-white/10', icon: null } + } +} + +function getSectionIcon(key: string) { + switch (key) { + case 'authority': + return Shield + case 'market': + return TrendingUp + case 'risk': + return AlertTriangle + case 'value': + return DollarSign + default: + return Globe + } +} + +function getSectionColor(key: string) { + switch (key) { + case 'authority': + return 'text-blue-400' + case 'market': + return 'text-emerald-400' + case 'risk': + return 'text-amber-400' + case 'value': + return 'text-violet-400' + default: + return 'text-white/60' } } @@ -32,7 +82,7 @@ async function copyToClipboard(text: string) { } function formatValue(value: unknown): string { - if (value === null || value === undefined) return 'N/A' + if (value === null || value === undefined) return 'โ€”' if (typeof value === 'string') return value if (typeof value === 'number') return String(value) if (typeof value === 'boolean') return value ? 'Yes' : 'No' @@ -40,28 +90,35 @@ function formatValue(value: unknown): string { return 'Details' } -function filterSection(section: AnalyzeSection, filterText: string): AnalyzeSection { - const f = filterText.trim().toLowerCase() - if (!f) return section - const items = section.items.filter((it) => { - const base = `${it.label} ${it.key} ${formatValue(it.value)}`.toLowerCase() - return base.includes(f) - }) - return { ...section, items } -} - function isMatrix(item: AnalyzeItem) { return item.key === 'tld_matrix' && Array.isArray(item.value) } +// ============================================================================ +// COMPONENT +// ============================================================================ + export function AnalyzePanel() { - const { isOpen, domain, close, fastMode, setFastMode, filterText, setFilterText, sectionVisibility, setSectionVisibility } = - useAnalyzePanelStore() + const { + isOpen, + domain, + close, + fastMode, + setFastMode, + sectionVisibility, + setSectionVisibility + } = useAnalyzePanelStore() const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [data, setData] = useState(null) const [copied, setCopied] = useState(false) + const [expandedSections, setExpandedSections] = useState>({ + authority: true, + market: true, + risk: true, + value: true + }) const refresh = useCallback(async () => { if (!domain) return @@ -97,9 +154,7 @@ export function AnalyzePanel() { } } run() - return () => { - cancelled = true - } + return () => { cancelled = true } }, [isOpen, domain, fastMode]) // ESC to close @@ -112,210 +167,296 @@ export function AnalyzePanel() { return () => window.removeEventListener('keydown', onKey) }, [isOpen, close]) + const toggleSection = useCallback((key: string) => { + setExpandedSections(prev => ({ ...prev, [key]: !prev[key] })) + }, []) + const visibleSections = useMemo(() => { const sections = data?.sections || [] const order = ['authority', 'market', 'risk', 'value'] - const ordered = [...sections].sort((a, b) => order.indexOf(a.key) - order.indexOf(b.key)) - return ordered + return [...sections] + .sort((a, b) => order.indexOf(a.key) - order.indexOf(b.key)) .filter((s) => sectionVisibility[s.key] !== false) - .map((s) => filterSection(s, filterText)) - .filter((s) => s.items.length > 0 || !filterText.trim()) - }, [data, sectionVisibility, filterText]) + }, [data, sectionVisibility]) + + // Calculate overall score + const overallScore = useMemo(() => { + if (!data?.sections) return null + let pass = 0, warn = 0, fail = 0 + data.sections.forEach(s => { + s.items.forEach(item => { + if (item.status === 'pass') pass++ + else if (item.status === 'warn') warn++ + else if (item.status === 'fail') fail++ + }) + }) + const total = pass + warn + fail + if (total === 0) return null + const score = Math.round((pass * 100 + warn * 50) / total) + return { score, pass, warn, fail, total } + }, [data]) const headerDomain = data?.domain || domain || '' - const computedAt = data?.computed_at ? new Date(data.computed_at).toLocaleString() : null if (!isOpen) return null return (
-
+ {/* Backdrop */} +
-
+ {/* Panel */} +
+ {/* Header */} -
-
-
- -
ANALYZE
- {data?.cached ? ( - CACHED - ) : null} +
+ {/* Top Bar */} +
+
+
+ +
+
+
Analyze
+
+ {headerDomain} +
+
-
-
{headerDomain}
+ +
+ + + + + - {copied ? Copied : null}
- {computedAt ?
Computed: {computedAt}
: null}
-
- - - - - -
-
- - {/* Controls */} -
-
-
- - setFilterText(e.target.value)} - placeholder="Filter signalsโ€ฆ" - className="w-full bg-white/[0.02] border border-white/10 pl-9 pr-3 py-2 text-sm text-white placeholder:text-white/25 outline-none focus:border-accent/40 font-mono" - /> + {/* Score Bar */} + {overallScore && !loading && ( +
+
+
= 70 ? "text-accent" : overallScore.score >= 40 ? "text-amber-400" : "text-red-400" + )}> + {overallScore.score} +
+
+
+
+
+
+
+
+
+ {overallScore.pass} pass + {overallScore.warn} warn + {overallScore.fail} fail +
+
+ )} + + {/* Mode Toggle */} +
-
- -
- {(['authority', 'market', 'risk', 'value'] as const).map((key) => { - const on = sectionVisibility[key] !== false - return ( - - ) - })} + {data?.cached && ( + + Cached + + )}
{/* Body */}
{loading ? ( -
Loadingโ€ฆ
- ) : error ? ( -
-
Analyze failed
-
{error}
-
- ) : !data ? ( -
No data.
- ) : ( -
-
- {visibleSections.map((section) => ( -
-
-
{section.title}
-
{section.key}
-
-
- {section.items.map((it) => ( -
-
-
{it.label}
-
- {isMatrix(it) ? ( -
- {(it.value as any[]).slice(0, 14).map((row: any) => ( -
- {String(row.domain)} - - {String(row.status).toUpperCase()} - -
- ))} -
- ) : ( - formatValue(it.value) - )} -
- {it.details && Object.keys(it.details).length ? ( -
- - Details - -
-                                  {JSON.stringify(it.details, null, 2)}
-                                
-
- ) : null} -
-
- - {it.status} - - {it.source} -
-
- ))} -
-
- ))} +
+
+ +
Analyzing...
- )} -
+ ) : error ? ( +
+
+
Analysis Failed
+
{error}
+
+
+ ) : !data ? ( +
+
No data
+
+ ) : ( +
+ {visibleSections.map((section) => { + const SectionIcon = getSectionIcon(section.key) + const sectionColor = getSectionColor(section.key) + const isExpanded = expandedSections[section.key] !== false - {/* Footer */} -
-
- Open-data-first. Some signals (trademarks/search volume/Wayback) require explicit data sources; weโ€™ll only add them when we can do it without external APIs. -
+ return ( +
+ {/* Section Header */} + + + {/* Section Items */} + {isExpanded && ( +
+ {section.items.map((item) => { + const statusStyle = getStatusColor(item.status) + const StatusIcon = statusStyle.icon + + return ( +
+
+ {/* Status Indicator */} +
+ {StatusIcon && } +
+ + {/* Content */} +
+
+ + {item.label} + + + {item.source} + +
+ + {/* Value */} +
+ {isMatrix(item) ? ( +
+ {(item.value as any[]).slice(0, 12).map((row: any) => ( +
+ {String(row.domain)} + {row.status === 'available' && } +
+ ))} +
+ ) : ( +
+ {formatValue(item.value)} +
+ )} +
+ + {/* Details Toggle */} + {item.details && Object.keys(item.details).length > 0 && ( +
+ + View details + +
+                                        {JSON.stringify(item.details, null, 2)}
+                                      
+
+ )} +
+
+
+ ) + })} +
+ )} +
+ ) + })} +
+ )}
) } - diff --git a/frontend/src/components/hunt/BrandableForgeTab.tsx b/frontend/src/components/hunt/BrandableForgeTab.tsx index 14c926e..f224ed0 100644 --- a/frontend/src/components/hunt/BrandableForgeTab.tsx +++ b/frontend/src/components/hunt/BrandableForgeTab.tsx @@ -350,55 +350,6 @@ export function BrandableForgeTab({ showToast }: { showToast: (message: string,
)} - {/* Info Cards */} - {items.length === 0 && ( -
-
-
-
- -
-
-
AI-Powered
-
Smart generation
-
-
-

- Generate pronounceable, memorable names following proven patterns like CVCVC. -

-
- -
-
-
- -
-
-
Verified
-
Real availability
-
-
-

- Every domain is checked via DNS/RDAP. Only verified available domains are shown. -

-
- -
-
-
- -
-
-
Instant Register
-
One-click buy
-
-
-

- Found a perfect name? Register instantly via Namecheap with one click. -

-
-
- )}
) } diff --git a/frontend/src/components/hunt/DropsTab.tsx b/frontend/src/components/hunt/DropsTab.tsx index 6256a16..04fcca1 100644 --- a/frontend/src/components/hunt/DropsTab.tsx +++ b/frontend/src/components/hunt/DropsTab.tsx @@ -5,12 +5,10 @@ import { api } from '@/lib/api' import { useAnalyzePanelStore } from '@/lib/analyze-store' import { useStore } from '@/lib/store' import { - Download, Clock, Globe, Loader2, Search, - Filter, ChevronRight, ChevronLeft, ChevronUp, @@ -21,7 +19,7 @@ import { Shield, ExternalLink, Zap, - Calendar, + Filter, Ban, Hash, } from 'lucide-react' @@ -43,9 +41,23 @@ interface DroppedDomain { interface ZoneStats { ch: { domain_count: number; last_sync: string | null } li: { domain_count: number; last_sync: string | null } - weekly_drops: number + daily_drops: number } +// All supported TLDs +type SupportedTld = 'ch' | 'li' | 'xyz' | 'org' | 'online' | 'info' | 'dev' | 'app' + +const ALL_TLDS: { tld: SupportedTld; flag: string }[] = [ + { tld: 'ch', flag: '๐Ÿ‡จ๐Ÿ‡ญ' }, + { tld: 'li', flag: '๐Ÿ‡ฑ๐Ÿ‡ฎ' }, + { tld: 'xyz', flag: '๐ŸŒ' }, + { tld: 'org', flag: '๐Ÿ›๏ธ' }, + { tld: 'online', flag: '๐Ÿ’ป' }, + { tld: 'info', flag: 'โ„น๏ธ' }, + { tld: 'dev', flag: '๐Ÿ‘จโ€๐Ÿ’ป' }, + { tld: 'app', flag: '๐Ÿ“ฑ' }, +] + // ============================================================================ // COMPONENT // ============================================================================ @@ -65,14 +77,10 @@ export function DropsTab({ showToast }: DropsTabProps) { const [refreshing, setRefreshing] = useState(false) const [total, setTotal] = useState(0) - // All supported TLDs - type SupportedTld = 'ch' | 'li' | 'xyz' | 'org' | 'online' | 'info' | 'dev' | 'app' - - // Filter State + // Filter State const [selectedTld, setSelectedTld] = useState(null) const [searchQuery, setSearchQuery] = useState('') const [searchFocused, setSearchFocused] = useState(false) - const [days, setDays] = useState(7) const [minLength, setMinLength] = useState(undefined) const [maxLength, setMaxLength] = useState(undefined) const [excludeNumeric, setExcludeNumeric] = useState(true) @@ -84,8 +92,8 @@ export function DropsTab({ showToast }: DropsTabProps) { const ITEMS_PER_PAGE = 50 // Sorting - const [sortField, setSortField] = useState<'domain' | 'length' | 'date'>('date') - const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc') + const [sortField, setSortField] = useState<'domain' | 'length' | 'date'>('length') + const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc') // Tracking const [tracking, setTracking] = useState(null) @@ -100,7 +108,7 @@ export function DropsTab({ showToast }: DropsTabProps) { } }, []) - // Load Drops + // Load Drops (only last 24h) const loadDrops = useCallback(async (currentPage = 1, isRefresh = false) => { if (isRefresh) setRefreshing(true) else setLoading(true) @@ -108,7 +116,7 @@ export function DropsTab({ showToast }: DropsTabProps) { try { const result = await api.getDrops({ tld: selectedTld || undefined, - days, + hours: 24, // Only last 24h - fresh drops only! min_length: minLength, max_length: maxLength, exclude_numeric: excludeNumeric, @@ -121,7 +129,6 @@ export function DropsTab({ showToast }: DropsTabProps) { setTotal(result.total) } catch (error: any) { console.error('Failed to load drops:', error) - // If API returns error, show info message if (error.message?.includes('401') || error.message?.includes('auth')) { showToast('Login required to view drops', 'info') } @@ -131,7 +138,7 @@ export function DropsTab({ showToast }: DropsTabProps) { setLoading(false) setRefreshing(false) } - }, [selectedTld, days, minLength, maxLength, excludeNumeric, excludeHyphen, searchQuery, showToast]) + }, [selectedTld, minLength, maxLength, excludeNumeric, excludeHyphen, searchQuery, showToast]) // Initial Load useEffect(() => { @@ -199,12 +206,15 @@ export function DropsTab({ showToast }: DropsTabProps) { maxLength !== undefined, excludeNumeric, excludeHyphen, - days !== 7 ].filter(Boolean).length - const formatDate = (iso: string) => { + const formatTime = (iso: string) => { const d = new Date(iso) - return d.toLocaleDateString('de-CH', { day: '2-digit', month: 'short' }) + const now = new Date() + const diffH = Math.floor((now.getTime() - d.getTime()) / (1000 * 60 * 60)) + if (diffH < 1) return 'Just now' + if (diffH === 1) return '1h ago' + return `${diffH}h ago` } if (loading && items.length === 0) { @@ -217,286 +227,189 @@ export function DropsTab({ showToast }: DropsTabProps) { return (
- {/* Stats Cards */} - {stats && ( -
-
-
- - Weekly Drops + {/* Header Stats */} +
+
+
+ +
+
+
+ {stats?.daily_drops?.toLocaleString() || total.toLocaleString()}
-
- {stats.weekly_drops.toLocaleString()} -
-
- Last 7 days +
Fresh drops (24h)
+
+
+ +
+ + {/* Search */} +
+
+ + setSearchQuery(e.target.value)} + onFocus={() => setSearchFocused(true)} + onBlur={() => setSearchFocused(false)} + placeholder="Search drops..." + className="flex-1 bg-transparent px-3 py-3 text-sm text-white placeholder:text-white/20 outline-none font-mono" + /> + {searchQuery && ( + + )} +
+
+ + {/* TLD Quick Filter */} +
+ + {ALL_TLDS.map(({ tld, flag }) => ( + + ))} +
+ + {/* Filter Toggle */} + + + {/* Filters Panel */} + {filtersOpen && ( +
+ {/* Length Filter */} +
+
Length
+
+ setMinLength(e.target.value ? Number(e.target.value) : undefined)} + placeholder="Min" + className="w-16 bg-white/[0.02] border border-white/10 px-2 py-1.5 text-xs text-white placeholder:text-white/25 outline-none font-mono" + min={1} + max={63} + /> + โ€“ + setMaxLength(e.target.value ? Number(e.target.value) : undefined)} + placeholder="Max" + className="w-16 bg-white/[0.02] border border-white/10 px-2 py-1.5 text-xs text-white placeholder:text-white/25 outline-none font-mono" + min={1} + max={63} + />
-
-
- ๐Ÿ‡จ๐Ÿ‡ญ๐Ÿ‡ฑ๐Ÿ‡ฎ - Switch.ch -
-
- {((stats.ch?.domain_count || 0) + (stats.li?.domain_count || 0)).toLocaleString()} -
-
- .ch + .li zones -
-
+ {/* Quality Filters */} +
+ -
-
- - ICANN CZDS -
-
6 TLDs
-
- .xyz .org .dev .app ... -
-
- -
-
- - Last Sync -
-
- {stats.ch?.last_sync - ? new Date(stats.ch.last_sync).toLocaleDateString() - : 'Pending' - } -
-
- Daily @ 05:00 UTC -
+
)} - {/* Search & Filters */} -
- {/* Search */} -
-
- - setSearchQuery(e.target.value)} - onFocus={() => setSearchFocused(true)} - onBlur={() => setSearchFocused(false)} - placeholder="Search dropped domains..." - className="flex-1 bg-transparent px-3 py-3 text-sm text-white placeholder:text-white/20 outline-none font-mono" - /> - {searchQuery && ( - - )} - -
-
- - {/* TLD Quick Filter */} -
- - {/* Switch.ch TLDs */} - {[ - { tld: 'ch', flag: '๐Ÿ‡จ๐Ÿ‡ญ' }, - { tld: 'li', flag: '๐Ÿ‡ฑ๐Ÿ‡ฎ' }, - ].map(({ tld, flag }) => ( - - ))} - {/* Separator */} -
- {/* CZDS TLDs */} - {[ - { tld: 'xyz', flag: '๐ŸŒ' }, - { tld: 'org', flag: '๐Ÿ›๏ธ' }, - { tld: 'online', flag: '๐Ÿ’ป' }, - { tld: 'info', flag: 'โ„น๏ธ' }, - { tld: 'dev', flag: '๐Ÿ‘จโ€๐Ÿ’ป' }, - { tld: 'app', flag: '๐Ÿ“ฑ' }, - ].map(({ tld, flag }) => ( - - ))} -
- - {/* Filter Toggle */} - - - {/* Filters Panel */} - {filtersOpen && ( -
- {/* Time Range */} -
-
Time Range
-
- {[ - { value: 1, label: '24h' }, - { value: 7, label: '7 days' }, - { value: 14, label: '14 days' }, - { value: 30, label: '30 days' }, - ].map((item) => ( - - ))} -
-
- - {/* Length Filter */} -
-
Domain Length
-
- setMinLength(e.target.value ? Number(e.target.value) : undefined)} - placeholder="Min" - className="w-20 bg-white/[0.02] border border-white/10 px-2 py-1.5 text-xs text-white placeholder:text-white/25 outline-none focus:border-accent/40 font-mono" - min={1} - max={63} - /> - โ€“ - setMaxLength(e.target.value ? Number(e.target.value) : undefined)} - placeholder="Max" - className="w-20 bg-white/[0.02] border border-white/10 px-2 py-1.5 text-xs text-white placeholder:text-white/25 outline-none focus:border-accent/40 font-mono" - min={1} - max={63} - /> - characters -
-
- - {/* Quality Filters */} -
- - - -
-
- )} -
- {/* Stats Bar */}
- {total.toLocaleString()} dropped domains found - Page {page}/{Math.max(1, totalPages)} + {total.toLocaleString()} fresh drops + {totalPages > 1 && Page {page}/{totalPages}}
{/* Results */} {sortedItems.length === 0 ? (
-

No dropped domains found

-

- {stats?.weekly_drops === 0 - ? 'Zone file sync may be pending' - : 'Try adjusting your filters'} -

+

No fresh drops

+

Check back after the next sync

) : ( <>
{/* Desktop Table Header */} -
+
Actions
@@ -506,21 +419,21 @@ export function DropsTab({ showToast }: DropsTabProps) {
{/* Mobile Row */}
-
-
-
- {item.tld === 'ch' ? '๐Ÿ‡จ๐Ÿ‡ญ' : '๐Ÿ‡ฑ๐Ÿ‡ฎ'} -
-
- -
- {item.length} chars - | - {formatDate(item.dropped_date)} -
-
+
+
+ {ALL_TLDS.find(t => t.tld === item.tld)?.flag || '๐ŸŒ'} + +
+
+ + {item.length} + + {formatTime(item.dropped_date)}
@@ -528,34 +441,29 @@ export function DropsTab({ showToast }: DropsTabProps) { - - - Register - + Get
{/* Desktop Row */} -
-
-
- {item.tld === 'ch' ? '๐Ÿ‡จ๐Ÿ‡ญ' : '๐Ÿ‡ฑ๐Ÿ‡ฎ'} -
+
+
+ {ALL_TLDS.find(t => t.tld === item.tld)?.flag || '๐ŸŒ'} - - Get -
@@ -610,60 +515,26 @@ export function DropsTab({ showToast }: DropsTabProps) { {/* Pagination */} {totalPages > 1 && ( -
-
- Page {page}/{totalPages} -
-
- - - - {page}/{totalPages} - - - -
+
+ + {page}/{totalPages} +
)} )} - - {/* Info Box */} -
-
-
- -
-
-

Zone File Analysis

-

- Domains are detected by comparing daily zone file snapshots. Data sources: -

-
-
- ๐Ÿ‡จ๐Ÿ‡ญ - .ch/.li via Switch.ch (AXFR) -
-
- ๐ŸŒ - gTLDs via ICANN CZDS -
-
-
-
-
) } diff --git a/frontend/src/components/hunt/SearchTab.tsx b/frontend/src/components/hunt/SearchTab.tsx index 74a8c93..c1f19d9 100644 --- a/frontend/src/components/hunt/SearchTab.tsx +++ b/frontend/src/components/hunt/SearchTab.tsx @@ -331,55 +331,6 @@ export function SearchTab({ showToast }: SearchTabProps) {
)} - {/* Quick Tips */} - {!searchResult && ( -
-
-
-
- -
-
-
Instant Check
-
RDAP/WHOIS
-
-
-

- Check any domain's availability in real-time using RDAP/WHOIS protocols. -

-
- -
-
-
- -
-
-
Monitor Drops
-
Watchlist
-
-
-

- Add taken domains to your watchlist. Get alerted when they become available. -

-
- -
-
-
- -
-
-
Deep Analysis
-
Full report
-
-
-

- Run full analysis: backlinks, SEO metrics, history, trademark checks. -

-
-
- )}
) } diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 9a4e371..08e167f 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1839,13 +1839,13 @@ class AdminApiClient extends ApiClient { return this.request<{ ch: { domain_count: number; last_sync: string | null } li: { domain_count: number; last_sync: string | null } - weekly_drops: number + daily_drops: number }>('/drops/stats') } async getDrops(params?: { tld?: 'ch' | 'li' | 'xyz' | 'org' | 'online' | 'info' | 'dev' | 'app' - days?: number + hours?: number min_length?: number max_length?: number exclude_numeric?: boolean @@ -1856,7 +1856,7 @@ class AdminApiClient extends ApiClient { }) { const query = new URLSearchParams() if (params?.tld) query.set('tld', params.tld) - if (params?.days) query.set('days', params.days.toString()) + if (params?.hours) query.set('hours', params.hours.toString()) if (params?.min_length) query.set('min_length', params.min_length.toString()) if (params?.max_length) query.set('max_length', params.max_length.toString()) if (params?.exclude_numeric) query.set('exclude_numeric', 'true')