feat: Consistent domain status across all pages

Backend:
- Added DROPPING_SOON status to DomainStatus enum
- Added deletion_date field to Domain model
- domain_checker now returns DROPPING_SOON for pending delete
- Track endpoint copies status and deletion_date from drop

Frontend:
- Watchlist shows "TRANSITION" status for dropping_soon domains
- AnalyzePanel shows consistent status from Watchlist
- Status display unified between Drops, Watchlist, and Panel
This commit is contained in:
2025-12-21 12:32:53 +01:00
parent 5df7d5cb96
commit 3bdb005efb
5 changed files with 43 additions and 15 deletions

View File

@ -326,3 +326,5 @@ Empfehlungen:

View File

@ -284,12 +284,22 @@ async def api_track_drop(
}
try:
# Map drop status to Domain status
status_map = {
'available': DomainStatus.AVAILABLE,
'dropping_soon': DomainStatus.DROPPING_SOON,
'taken': DomainStatus.TAKEN,
'unknown': DomainStatus.UNKNOWN,
}
domain_status = status_map.get(drop.availability_status, DomainStatus.UNKNOWN)
# Add to watchlist with notification enabled
domain = Domain(
user_id=current_user.id,
name=full_domain,
status=DomainStatus.AVAILABLE if drop.availability_status == 'available' else DomainStatus.UNKNOWN,
status=domain_status,
is_available=drop.availability_status == 'available',
deletion_date=drop.deletion_date, # Copy deletion date for countdown
notify_on_available=True, # Enable notification!
)
db.add(domain)

View File

@ -11,6 +11,7 @@ class DomainStatus(str, Enum):
"""Domain availability status."""
AVAILABLE = "available"
TAKEN = "taken"
DROPPING_SOON = "dropping_soon" # In transition/pending delete
ERROR = "error"
UNKNOWN = "unknown"
@ -32,6 +33,7 @@ class Domain(Base):
# WHOIS data (optional)
registrar: Mapped[str | None] = mapped_column(String(255), nullable=True)
expiration_date: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
deletion_date: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) # When domain will be fully deleted
# User relationship
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False)

View File

@ -197,11 +197,11 @@ class DomainChecker:
)
if is_pending_delete:
logger.info(f"{domain} is pending delete (status: {domain_status})")
logger.info(f"{domain} is in transition/pending delete (status: {domain_status})")
return DomainCheckResult(
domain=domain,
status=DomainStatus.AVAILABLE,
is_available=True,
status=DomainStatus.DROPPING_SOON, # In transition, not yet available
is_available=False, # Not yet registrable
check_method="rdap_custom",
raw_data={"rdap_status": domain_status, "note": "pending_delete"},
)

View File

@ -150,10 +150,18 @@ export default function WatchlistPage() {
const openAnalyzePanel = useAnalyzePanelStore((s) => s.open)
// Wrapper to open analyze panel with domain status
const openAnalyze = useCallback((domainData: { name: string; is_available: boolean; expiration_date: string | null }) => {
const openAnalyze = useCallback((domainData: { name: string; status: string; is_available: boolean; expiration_date: string | null; deletion_date?: string | null }) => {
// Map domain status to drop status format
const statusMap: Record<string, 'available' | 'dropping_soon' | 'taken' | 'unknown'> = {
'available': 'available',
'dropping_soon': 'dropping_soon',
'taken': 'taken',
'error': 'unknown',
'unknown': 'unknown',
}
openAnalyzePanel(domainData.name, {
status: domainData.is_available ? 'available' : 'taken',
deletion_date: domainData.expiration_date,
status: statusMap[domainData.status] || (domainData.is_available ? 'available' : 'taken'),
deletion_date: domainData.deletion_date || domainData.expiration_date,
is_drop: false,
})
}, [openAnalyzePanel])
@ -607,6 +615,16 @@ export default function WatchlistPage() {
const config = healthConfig[healthStatus]
const days = getDaysUntilExpiry(domain.expiration_date)
// Domain status display config (consistent with DropsTab)
const domainStatus = domain.status || (domain.is_available ? 'available' : 'taken')
const statusConfig = {
available: { label: 'AVAIL', color: 'text-accent', bg: 'bg-accent/5 border-accent/20' },
dropping_soon: { label: 'TRANSITION', color: 'text-amber-400', bg: 'bg-amber-400/5 border-amber-400/20' },
taken: { label: 'TAKEN', color: 'text-white/40', bg: 'bg-white/5 border-white/10' },
error: { label: 'ERROR', color: 'text-rose-400', bg: 'bg-rose-400/5 border-rose-400/20' },
unknown: { label: 'CHECK', color: 'text-white/30', bg: 'bg-white/5 border-white/5' },
}[domainStatus] || { label: 'UNKNOWN', color: 'text-white/30', bg: 'bg-white/5 border-white/5' }
return (
<div
key={domain.id}
@ -633,11 +651,9 @@ export default function WatchlistPage() {
<div className="text-right shrink-0">
<div className={clsx(
"text-[10px] font-mono px-2 py-0.5 mt-1 inline-block border",
domain.is_available
? "text-accent bg-accent/5 border-accent/20"
: "text-white/30 bg-white/5 border-white/5"
statusConfig.color, statusConfig.bg
)}>
{domain.is_available ? 'AVAIL' : 'TAKEN'}
{statusConfig.label}
</div>
</div>
</div>
@ -719,11 +735,9 @@ export default function WatchlistPage() {
<div className="flex justify-center">
<span className={clsx(
"text-[10px] font-mono font-bold uppercase px-2.5 py-1.5 border",
domain.is_available
? "text-accent bg-accent/10 border-accent/30"
: "text-white/40 bg-white/5 border-white/10"
statusConfig.color, statusConfig.bg
)}>
{domain.is_available ? 'AVAIL' : 'TAKEN'}
{statusConfig.label}
</span>
</div>