From 8dc6f85fb80a210f80d301c2348ce28ee4e0ad82 Mon Sep 17 00:00:00 2001 From: Yves Gugger Date: Fri, 19 Dec 2025 09:11:46 +0100 Subject: [PATCH] Deploy: 2025-12-19 09:11 --- UX_TERMINAL_UX_REPORT.md | 318 ++++++++++++++++++++++ backend/app/api/drops.py | 156 +++++++++++ backend/app/models/zone_file.py | 7 + backend/app/services/domain_checker.py | 29 +- backend/app/services/zone_file.py | 5 +- frontend/src/components/hunt/DropsTab.tsx | 218 ++++++++++++--- frontend/src/lib/api.ts | 23 ++ 7 files changed, 717 insertions(+), 39 deletions(-) create mode 100644 UX_TERMINAL_UX_REPORT.md diff --git a/UX_TERMINAL_UX_REPORT.md b/UX_TERMINAL_UX_REPORT.md new file mode 100644 index 0000000..2972bfa --- /dev/null +++ b/UX_TERMINAL_UX_REPORT.md @@ -0,0 +1,318 @@ +# UX-Review: Pounce Terminal (Next.js User App) + +Stand: 2025-12-19 +Scope: **alle Seiten unter** `frontend/src/app/terminal/*` (inkl. Subroute `intel/[tld]`) + globale Terminal-UX (Navigation, Feedback, Loading/Error, Mobile, Accessibility) + +--- + +## Executive Summary (was am meisten “zählt”) + +Das Terminal wirkt visuell stark (klare Terminal-Ästhetik, konsistente Farbwelt, guter Einsatz von Stats/Badges). Die größten UX-Hebel liegen aktuell aber weniger im „Look“, sondern in **Konsistenz, Feedback-Loops und Bedienbarkeit**: + +- **P0: Navigation/Chrome ist pro Seite dupliziert** (Mobile Header/Bottom Nav/Drawer) → Inkonsistenzen + Bugs (z.B. aktive Tabs/Routes falsch) + hoher Maintenance-Overhead. +- **P0: Keine globalen `loading.tsx` / `error.tsx`** im App Router → schlechte UX bei langsamen API-Calls und Edge-Fehlern. +- **P1: Mixed Feedback** (`alert/confirm`, Toast, inline) → mentaler Kontext-Switch + „unbranded“ Dialoge. +- **P1: Accessibility & Keyboard UX** (Focus Trap, ESC, ARIA-Rollen, Kontrast/Typografie bei 9–10px) → wirkt “pro”, aber ist für längere Sessions anstrengend. + +--- + +## Quick Wins (1–2 Tage, hoher Impact) + +- **Unify Terminal Shell**: Ein einziges Layout für Terminal (Sidebar + Topbar + Mobile Drawer + Bottom Nav) statt Copy/Paste pro Seite. +- **App Router Loading/Error**: `frontend/src/app/terminal/loading.tsx` + `frontend/src/app/terminal/error.tsx` (+ ggf. root-level) einführen. +- **Mobile Nav Active State fixen**: Aktive Route automatisch über `usePathname()` bestimmen (nicht hardcoded `active: true/false` Arrays). +- **Replace `alert/confirm`**: überall ein konsistentes Modal/Toast Pattern. +- **Modals**: ESC schließen, Fokus in Modal halten, `role="dialog"`, `aria-modal="true"`, initial focus auf primäres Feld/CTA. + +--- + +## P0 / P1 / P2 – Priorisierte UX-Baustellen + +### P0 – Blocker / deutliche UX-Schäden +- **Terminal Navigation/Chrome dupliziert** + Viele Seiten implementieren eigene Mobile Header/Bottom Nav/Drawer. Das führt zu: + - inkonsistenten Labels (Radar/Hunt), unterschiedlichen Menüs, unterschiedlichen Drawer-Sections + - Bugs: aktive Navigation ist teils falsch oder gar nicht gesetzt (z.B. `/terminal/listing` hat mobileNavItems alle `active: false`) + - fehlende Features in manchen Layouts (z.B. Notifications/Quick Search aus `components/TerminalLayout.tsx` werden in den meisten Terminal-Seiten nie genutzt) + +- **Keine globalen Loading/Error States** + Es existieren keine `loading.tsx` / `error.tsx` im `app/` Tree. Bei langsamen Calls (Market Feed, TLD-Loads, WHOIS/Health) entsteht: + - „Blank“/Springen zwischen States + - inkonsistente Loader-Stile pro Seite + - keine zentrale Recovery (Retry, Logging, Support CTA) + +### P1 – hoher Impact, aber kein Blocker +- **Mixed Feedback & Confirmation UX** + Unterschiedliche Patterns je Seite: Toast (`Toast`), inline error blocks, aber auch `alert()` / `confirm()` (unbranded, nicht mobil-optimiert, blockierend). + +- **Mobile IA/Navigation ist zu “kurz”** + Bottom Nav zeigt meist nur 4 Punkte (Hunt/Watch/Portfolio/Intel) → wichtige Module (Yield, Sniper, Inbox, For Sale, Settings) hängen hinter „Menu“, ohne klare Priorisierung oder Badges. + +- **Intel lädt sehr viel Daten “am Client”** + `IntelPage` lädt bis zu 500 TLDs in einem Loop (100er chunks). UX-Probleme: + - lange initiale Ladezeit + - kein echtes Pagination/Search UX (Suche filtert lokal) + - mobile wird schnell unübersichtlich + +### P2 – polish / langfristige Qualität +- **Typografie & Kontrast** + Viele essenzielle Infos sind `text-white/30` oder `text-[9px]` → funktioniert “stylish”, aber wird bei langen Sessions anstrengend. +- **Keyboard-first** + Das Terminal-Feeling schreit nach Power-User Shortcuts (Cmd+K existiert, aber Search hat noch keine Results UX). + +--- + +## Globale Empfehlungen (Terminal-weit) + +### 1) Ein „Terminal Shell“ Layout (Single Source of Truth) +Ziel: **Sidebar + Topbar + Mobile Drawer + Mobile Bottom Nav** einmal bauen und Seiten nur noch Content rendern lassen. + +Konkret: +- `frontend/src/app/terminal/layout.tsx` (auth guard) ist ok, aber **UI-Chrome sollte zusätzlich** in einem Layout/Wrapper passieren (z.B. `TerminalShellLayout`). +- `components/TerminalLayout.tsx` existiert bereits, wird aber primär nur auf `/terminal/welcome` genutzt. + Empfehlung: Entweder + - **Option A**: `TerminalLayout` so erweitern, dass alle Terminal-Seiten ihn nutzen, oder + - **Option B**: neue `TerminalShell` Komponente, die die wiederkehrenden Teile kapselt (Desktop+Mobile). + +**Wichtig**: Mobile Bottom Nav muss aktive Route automatisch bestimmen (z.B. über `usePathname()`), sonst bleibt es fehleranfällig. + +### 2) Konsistentes Feedback-System +Standardisieren: +- **Errors**: inline „ErrorCard“ mit Retry + „Details“ (expand), und optional „Contact support“ Link. +- **Confirms**: eigenes Confirm-Modal (nicht `confirm()`). +- **Success**: Toast. +- **Long-running**: progress + disable states + ggf. optimistic updates (macht ihr teils schon gut). + +### 3) Modal Quality Bar (Accessibility + UX) +Alle Modals (Add/Verify/Thread/Wizards) sollten: +- ESC schließen +- Fokus im Modal halten (Focus Trap) +- `role="dialog"`, `aria-modal="true"`, `aria-labelledby` +- initial focus (erstes Input-Feld oder Primary CTA) +- „Close“ Button immer sichtbar +- bei kritischen Aktionen: klare irreversible Warnung + „Undo“ wo möglich + +### 4) URL-State für Filter & Deep Links +Viele Seiten haben Filter/Sort/Search, aber Zustand ist oft nur in React State: +- Market: source/tld/price/hideSpam/search/page sollten in Query Params spiegeln (shareable links, back button korrekt). +- Intel: Filter und Search ebenfalls in URL. +- Inbox: ihr habt schon Query Param `inquiry` + `tab` → gutes Pattern, ausbauen. + +### 5) “System Status” & Datenfrische +Für alle datengetriebenen Seiten ein einheitliches Muster: +- „Last updated“ + „Refresh“ (nicht überall gleich) +- bei Auto-refresh (Watchlist Tycoon): UI-Indikator „Auto-refresh active“ + letzte Hintergrund-Aktualisierung (sonst wirkt es „random“). + +--- + +## Seitenreport (Terminal) + +### `/terminal` (Redirect) +Ist aktuell ein Spinner + Redirect nach `/terminal/hunt`. UX ok, aber: +- **Verbesserung**: kurze Textzeile „Opening Terminal…“ (Barrierefreiheit, Kontext), plus “fallback link” falls Router hängt. + +### `/terminal/welcome` +Starkes Upgrade-Confirmation Pattern (Feature Cards, Next Steps). +UX-Fixes: +- Confetti nutzt `Math.random()` in render → kann bei Re-Renders „flackern“ / unruhig wirken. +- CTA Label “Go to Radar” führt nach `/terminal/hunt` (Terminologie konsistent halten: Hunt vs Radar). + +### `/terminal/hunt` +Stärken: +- Tab UX (Search/Drops/Auctions/Trends/Forge) ist klar und fühlt sich wie ein “Workbench” an. + +Probleme: +- Seite rendert eigene Sidebar + Mobile Drawer + Bottom Nav → **duplizierter Shell**. +- Mobile Bottom Nav hat „active“ hardcoded; ist hier ok, aber strukturell fragil. + +Empfehlungen: +- Tabs als URL-State: `?tab=search|drops|...` (Deep link + Back button). +- Top-of-page: “What’s new / last updated” pro Tab (Drops/Auctions/Trends) um Datenfrische sichtbar zu machen. + +### `/terminal/market` +Stärken: +- Gute Filter, Spam-Hide, Score Badges, Track/Analyze Actions. + +Probleme: +- `searchQuery` wird sowohl server-seitig (API `keyword`) als auch client-seitig gefiltert → Doppel-Filter kann zu “WTF”-Momenten führen. +- Fehlerfall: bei API error wird `items=[]` gesetzt, aber **kein** Error UI (nur „No domains found“ → falsche Diagnose). +- Mobile Drawer/Bottom Nav duplicated. + +Empfehlungen: +- Error UI unterscheiden: „0 Results“ vs „Load failed“. +- Filter in URL spiegeln. +- “Track” UX: wenn Domain schon getrackt ist und Remove passiert, muss UI klar „Removed from Watchlist“ anzeigen (Toast habt ihr), und optional Undo. + +### `/terminal/intel` +Stärken: +- Tier Gating ist klar visualisiert (Locks, Upgrade CTA). +- Sort/Filter/Search UI ist solide. + +Probleme: +- Große Client-Loads (bis 500 TLDs in 100er Schritten) → mobile/slow networks leiden. +- Duplicate nav/drawer. + +Empfehlungen: +- Server-driven pagination: „Top 100 by popularity“ + search server-seitig, infinite scroll optional. +- “Renewal trap” als eigenes Filter/Badge prominent (weil es echte Entscheidung beeinflusst). + +### `/terminal/intel/[tld]` +Stärken: +- Gute „Detail“-Informationsarchitektur (Chart + Domain Check + Registrar Tabelle). +- Lock Overlay für Scout ist gut. + +Probleme: +- Domain Check Fehler wird nur `console.error`, kein UI-Feedback. +- Registrar-Link Fallback `'#'` (führt zu dead click) → besser: Button disabled + Tooltip „No registrar link available“. +- Duplicate nav/drawer. + +Empfehlungen: +- Domain Check: inline error state + retry. +- Chart Tooltip: auf mobile fehlt Hover; optional Tap-to-inspect. + +### `/terminal/watchlist` +Stärken: +- Sehr gutes Domain Operations UX (Alert toggle, Refresh, Health modal, Expiry tags). + +Probleme: +- Mixed confirmations: `confirm('Drop target…')` ist unbranded und blockierend. +- Health-Autoload kann bei großen Listen viele Requests erzeugen; UX kann „unruhig“ werden (Loader überall). +- Duplicate nav/drawer. + +Empfehlungen: +- Confirm Modal + optional Undo „Removed“. +- Health Cache: klarer Indikator „Health: cached at …“ vs „live check“. +- „Add domain“ Modal: Domain Normalisierung/Validation vor Submit (z.B. whitespace/protocol) + Inline errors. + +### `/terminal/portfolio` +Stärken: +- Sehr umfangreich, aber erstaunlich gut strukturiert (Assets/Financials Tabs, CFO-Karten, KillList/BurnRate). +- DNS Verify Flow ist verständlich (2 Schritte, Copy Button). + +Probleme: +- Auto Health checks (bis 20 Domains, delay 300ms) können initial lange dauern; ohne globalen “health loading” Kontext wirkt es wie “random spinners”. +- Viele Modals/Overlays, aber Fokus/ESC/ARIA nicht standardisiert. +- Duplicate nav/drawer (hier sogar unterschiedliche Drawer-Implementierungen). + +Empfehlungen: +- Health prefetch optional machen (Toggle „Preload health on open“). +- “Financials” braucht klaren “data computed at” Stempel + “Refresh CFO” prominent. +- Vereinheitlichte Drawer/Bottom nav in Shell. + +### `/terminal/listing` (For Sale) +Stärken: +- Wizard (Create Listing) ist ein gutes Pattern (Step 1–3). +- Leads Modal mit Thread UI ist sehr wertvoll (reale Workflows). + +Probleme: +- Mobile Bottom Nav: `active` ist überall false → **Navigation Feedback kaputt**. +- Mixed feedback: `alert()` bei errors (publish/delete) → unbranded. +- Wizard Step 2: “DNS can take 1–5 minutes” ist ok, aber es fehlen “Where exactly to set TXT record?” Verweise/Links pro Registrar. + +Empfehlungen: +- Active Nav fixen (global). +- Alerts/confirm ersetzen (Toast + Modal). +- Wizard: “Copy all fields” + “Open registrar docs” optional. +- Leads/Thread: auto-scroll to latest message + “unread” marker. + +### `/terminal/inbox` +Stärken: +- Buying vs Selling Tabs; query-params unterstütztes Deep-Linking (gut!). +- Thread Pane Layout auf Desktop ist sinnvoll. + +Probleme: +- Polling (15s messages, 30s threads) ohne sichtbaren „sync“ Indikator; kann „messages jumpen“ oder user verunsichern. +- Mobile Drawer ist sehr reduziert (Hunt/Watchlist/For Sale) → Missing core modules. + +Empfehlungen: +- „Last synced“ + kleiner Spinner beim Poll refresh. +- Thread: “Sending…” state im Message Bubble optimistisch anzeigen. +- Mobile: Navigation vereinheitlichen (Shell). + +### `/terminal/sniper` +Stärken: +- Übersicht mit Active/Matches/Sent Stats. +- Create/Edit Modal deckt viele Filter ab. + +Probleme: +- Mixed feedback: `alert()` bei toggle/delete errors. +- Limit messaging: Scout maxAlerts=0, aber Settings-Plan Copy sagt “2 Sniper Alerts” → **Widerspruch** (führt zu Trust-Bruch). + +Empfehlungen: +- Limits und Copy überall konsistent (Settings, Pricing, Backend). +- “Preview matches” wäre stark: pro Alert ein „Show matches“ (wenn Backend vorhanden). + +### `/terminal/yield` +Stärken: +- Aktivierungsflow (Select Domain → DNS Setup → Verify) ist klar. +- Dashboard Tabelle liefert „operative“ KPIs. + +Probleme: +- **Hardcoded IP** `46.235.147.194` im Frontend: UX/Trust & Ops-Risiko (kann sich ändern, Multi-env schwierig). +- Sehr viel `alert()/confirm()` für Verify/Delete → unbranded, keine Recovery UI. +- Tier-Gating Messaging: Modal Titel „Preview Yield Landing“ auch für Trader/Tycoon; das ist ok, aber sollte klarer sein. + +Empfehlungen: +- IP & DNS instructions server/ENV-driven (z.B. `NEXT_PUBLIC_YIELD_SERVER_IP` + Backend liefert aktuelle DNS Targets). +- Verify/Delete: styled modal + inline results (verified/expected vs actual). +- “Landing Ready/Missing”: wenn missing, direkt CTA „Regenerate (Tycoon)“ mit Erklärung. + +### `/terminal/settings` +Stärken: +- Gute Tabs (Profile/Alerts/Plans/Security), Plan Cards ordentlich. +- Referral/Invite ist sauber umgesetzt. + +Probleme: +- Notification prefs werden nur in `localStorage` gespeichert → nicht cross-device, nicht auditierbar. (UX: „Saved“ wirkt, aber auf anderem Device weg.) +- “Email Verified” wird als immer verified angezeigt (UI Copy), unabhängig vom echten `user.is_verified` → UX/Trust Risiko. +- “Delete Account” Button ohne Flow (gefährlich). + +Empfehlungen: +- Notification prefs in Backend persistieren (Endpoint z.B. `PUT /auth/notification-prefs` oder `PUT /users/me/preferences`). +- Security Tab: echten Status aus `user.is_verified` anzeigen + „Resend verification“ CTA falls false. +- Delete Account: confirm flow + export data + cooldown. + +--- + +## Konsistenz-/Trust-Issues (wichtig für Conversion) + +- **Plan Limits widersprüchlich**: + Beispiel: Sniper Limits in `/terminal/sniper` (scout: 0) vs Settings Plan Copy (Scout: “2 Sniper Alerts”). Das ist ein direkter Trust-Bruch. + +- **Terminologie**: Radar vs Hunt; Terminal v1.0 in Drawer; “Go to Radar” aber Route `/terminal/hunt`. + Empfehlung: eine Terminologie festlegen (z.B. „Hunt“ überall) und konsequent. + +--- + +## Messbare UX-KPIs (damit Improvements nachvollziehbar sind) + +- **Time-to-first-useful-data** pro Seite (Market, Intel, Watchlist, Portfolio CFO) +- **Error rate** (API errors) pro Seite + Retry success rate +- **Completion rates**: + - Listing Wizard Step 1→2→3 + - Yield Activation Step 1→2→3 + - DNS verification success time distribution +- **Engagement**: + - Track/untrack from Market + - Analyze opens per session + - Inbox reply median time + +--- + +## Konkrete nächste Schritte (Engineering Backlog Vorschlag) + +### Phase 1 (P0): Shell + States +- Terminal Shell Layout bauen, alle Terminal-Seiten migrieren. +- `terminal/loading.tsx` + `terminal/error.tsx` einführen. +- Einheitliches Toast/Confirm/Modal Pattern. + +### Phase 2 (P1): Data UX +- Market: echte Error UI + URL-state für Filter + klare Trennung API vs client filtering. +- Intel: server-driven pagination/search. +- Settings: Preferences persistieren + Security Status korrekt. + +### Phase 3 (P2): Polish & Power-User +- Keyboard-first: Cmd+K Search mit echten Results + navigation. +- Accessibility sweep: Kontrast, Focus states, ARIA, reduced motion. + + diff --git a/backend/app/api/drops.py b/backend/app/api/drops.py index b76a450..9a7d193 100644 --- a/backend/app/api/drops.py +++ b/backend/app/api/drops.py @@ -8,18 +8,23 @@ API endpoints for accessing freshly dropped domains from: from datetime import datetime from typing import Optional +import logging from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select, update from app.database import get_db from app.api.deps import get_current_user +from app.models.zone_file import DroppedDomain from app.services.zone_file import ( ZoneFileService, get_dropped_domains, get_zone_stats, ) +logger = logging.getLogger(__name__) + router = APIRouter(prefix="/drops", tags=["drops"]) # All supported TLDs @@ -175,3 +180,154 @@ async def api_get_supported_tlds(): {"tld": "biz", "name": "Business", "flag": "💼", "registry": "GoDaddy", "source": "czds"}, ] } + + +@router.post("/check-status/{drop_id}") +async def api_check_drop_status( + drop_id: int, + db: AsyncSession = Depends(get_db), + current_user = Depends(get_current_user) +): + """ + Check the real-time availability status of a dropped domain. + + Returns: + - available: Domain can be registered NOW + - pending_delete: Domain is in deletion phase (monitor it!) + - redemption: Domain is in redemption period (owner can recover) + - taken: Domain was re-registered + - unknown: Could not determine status + """ + from app.services.domain_checker import domain_checker + + # Get the drop from DB + result = await db.execute( + select(DroppedDomain).where(DroppedDomain.id == drop_id) + ) + drop = result.scalar_one_or_none() + + if not drop: + raise HTTPException(status_code=404, detail="Drop not found") + + full_domain = f"{drop.domain}.{drop.tld}" + + try: + # Check with RDAP (not quick mode) + check_result = await domain_checker.check_domain(full_domain, quick=False) + + # Determine availability status + if check_result.is_available: + availability_status = "available" + rdap_status = check_result.raw_data.get("rdap_status", []) if check_result.raw_data else [] + + # Check if it's pending delete (available but not immediately) + if rdap_status and any("pending" in str(s).lower() for s in rdap_status): + availability_status = "pending_delete" + else: + # Domain exists - check specific status + rdap_status = check_result.raw_data.get("rdap_status", []) if check_result.raw_data else [] + rdap_str = str(rdap_status).lower() + + if "pending delete" in rdap_str or "pendingdelete" in rdap_str: + availability_status = "pending_delete" + elif "redemption" in rdap_str: + availability_status = "redemption" + else: + availability_status = "taken" + + # Update the drop in DB + await db.execute( + update(DroppedDomain) + .where(DroppedDomain.id == drop_id) + .values( + availability_status=availability_status, + rdap_status=str(rdap_status) if rdap_status else None, + last_status_check=datetime.utcnow() + ) + ) + await db.commit() + + return { + "id": drop_id, + "domain": full_domain, + "availability_status": availability_status, + "rdap_status": rdap_status, + "is_available": check_result.is_available, + "can_register_now": availability_status == "available", + "can_monitor": availability_status in ("pending_delete", "redemption"), + } + + except Exception as e: + logger.error(f"Status check failed for {full_domain}: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/monitor/{drop_id}") +async def api_monitor_drop( + drop_id: int, + db: AsyncSession = Depends(get_db), + current_user = Depends(get_current_user) +): + """ + Add a dropped domain to the Sniper monitoring list. + Will send notification when domain becomes available. + """ + from app.models.sniper_alert import SniperAlert + + # Get the drop + result = await db.execute( + select(DroppedDomain).where(DroppedDomain.id == drop_id) + ) + drop = result.scalar_one_or_none() + + if not drop: + raise HTTPException(status_code=404, detail="Drop not found") + + full_domain = f"{drop.domain}.{drop.tld}" + + # Check if already monitoring + existing = await db.execute( + select(SniperAlert).where( + SniperAlert.user_id == current_user.id, + SniperAlert.domain == full_domain, + SniperAlert.is_active == True + ) + ) + if existing.scalar_one_or_none(): + return {"status": "already_monitoring", "domain": full_domain} + + # Check user limits + from app.api.sniper_alerts import get_user_alert_limit + limit = get_user_alert_limit(current_user) + + count_result = await db.execute( + select(SniperAlert).where( + SniperAlert.user_id == current_user.id, + SniperAlert.is_active == True + ) + ) + current_count = len(count_result.scalars().all()) + + if current_count >= limit: + raise HTTPException( + status_code=400, + detail=f"Monitor limit reached ({limit}). Upgrade to add more." + ) + + # Create sniper alert for this drop + alert = SniperAlert( + user_id=current_user.id, + domain=full_domain, + alert_type="drop", + is_active=True, + notify_email=True, + notify_push=True, + ) + db.add(alert) + await db.commit() + + return { + "status": "monitoring", + "domain": full_domain, + "message": f"You'll be notified when {full_domain} becomes available!" + } diff --git a/backend/app/models/zone_file.py b/backend/app/models/zone_file.py index 65d8793..c3b50e9 100644 --- a/backend/app/models/zone_file.py +++ b/backend/app/models/zone_file.py @@ -36,8 +36,15 @@ class DroppedDomain(Base): is_numeric = Column(Boolean, default=False) has_hyphen = Column(Boolean, default=False) created_at = Column(DateTime, default=datetime.utcnow) + + # Real-time availability status (checked via RDAP) + # Possible values: 'available', 'pending_delete', 'redemption', 'taken', 'unknown' + availability_status = Column(String(20), default='unknown', index=True) + rdap_status = Column(String(255), nullable=True) # Raw RDAP status string + last_status_check = Column(DateTime, nullable=True) __table_args__ = ( Index('ix_dropped_domains_tld_date', 'tld', 'dropped_date'), Index('ix_dropped_domains_length', 'length'), + Index('ix_dropped_domains_availability', 'availability_status'), ) diff --git a/backend/app/services/domain_checker.py b/backend/app/services/domain_checker.py index 224057e..bd10054 100644 --- a/backend/app/services/domain_checker.py +++ b/backend/app/services/domain_checker.py @@ -176,9 +176,36 @@ class DomainChecker: ) if response.status_code == 200: - # Domain exists = taken + # Domain exists in registry - but check status for pending delete data = response.json() + # Check if domain is pending deletion (dropped but not yet purged) + # These domains are effectively available for registration + domain_status = data.get('status', []) + pending_delete_statuses = [ + 'pending delete', + 'pendingdelete', + 'redemption period', + 'redemptionperiod', + 'pending purge', + 'pendingpurge', + ] + + is_pending_delete = any( + any(pds in str(s).lower() for pds in pending_delete_statuses) + for s in domain_status + ) + + if is_pending_delete: + logger.info(f"{domain} is pending delete (status: {domain_status})") + return DomainCheckResult( + domain=domain, + status=DomainStatus.AVAILABLE, + is_available=True, + check_method="rdap_custom", + raw_data={"rdap_status": domain_status, "note": "pending_delete"}, + ) + # Extract dates from events expiration_date = None creation_date = None diff --git a/backend/app/services/zone_file.py b/backend/app/services/zone_file.py index 78e3fe9..0aecad5 100644 --- a/backend/app/services/zone_file.py +++ b/backend/app/services/zone_file.py @@ -365,12 +365,15 @@ async def get_dropped_domains( "total": total, "items": [ { + "id": item.id, "domain": item.domain, "tld": item.tld, "dropped_date": item.dropped_date.isoformat(), "length": item.length, "is_numeric": item.is_numeric, - "has_hyphen": item.has_hyphen + "has_hyphen": item.has_hyphen, + "availability_status": getattr(item, 'availability_status', 'unknown') or 'unknown', + "last_status_check": item.last_status_check.isoformat() if getattr(item, 'last_status_check', None) else None, } for item in items ] diff --git a/frontend/src/components/hunt/DropsTab.tsx b/frontend/src/components/hunt/DropsTab.tsx index 8f97a0a..93e0670 100644 --- a/frontend/src/components/hunt/DropsTab.tsx +++ b/frontend/src/components/hunt/DropsTab.tsx @@ -31,13 +31,18 @@ import clsx from 'clsx' // TYPES // ============================================================================ +type AvailabilityStatus = 'available' | 'pending_delete' | 'redemption' | 'taken' | 'unknown' + interface DroppedDomain { + id: number domain: string tld: string dropped_date: string length: number is_numeric: boolean has_hyphen: boolean + availability_status: AvailabilityStatus + last_status_check: string | null } interface ZoneStats { @@ -99,6 +104,10 @@ export function DropsTab({ showToast }: DropsTabProps) { // Tracking const [tracking, setTracking] = useState(null) + // Status Checking + const [checkingStatus, setCheckingStatus] = useState(null) + const [monitoringDrop, setMonitoringDrop] = useState(null) + // Load Stats const loadStats = useCallback(async () => { try { @@ -173,6 +182,51 @@ export function DropsTab({ showToast }: DropsTabProps) { } }, [addDomain, showToast, tracking]) + // Check real-time status of a drop + const checkStatus = useCallback(async (dropId: number, domain: string) => { + if (checkingStatus) return + setCheckingStatus(dropId) + try { + const result = await api.checkDropStatus(dropId) + // Update the item in our list + setItems(prev => prev.map(item => + item.id === dropId + ? { ...item, availability_status: result.availability_status, last_status_check: new Date().toISOString() } + : item + )) + + if (result.can_register_now) { + showToast(`✅ ${domain} is available NOW!`, 'success') + } else if (result.can_monitor) { + showToast(`⏳ ${domain} is pending deletion. Add to monitor!`, 'info') + } else { + showToast(`❌ ${domain} is taken`, 'error') + } + } catch (e) { + showToast(e instanceof Error ? e.message : 'Status check failed', 'error') + } finally { + setCheckingStatus(null) + } + }, [checkingStatus, showToast]) + + // Monitor a pending drop + const monitorDrop = useCallback(async (dropId: number, domain: string) => { + if (monitoringDrop) return + setMonitoringDrop(dropId) + try { + const result = await api.monitorDrop(dropId) + if (result.status === 'already_monitoring') { + showToast(`Already monitoring ${domain}`, 'info') + } else { + showToast(`🎯 Monitoring ${domain} - you'll be notified!`, 'success') + } + } catch (e) { + showToast(e instanceof Error ? e.message : 'Failed to monitor', 'error') + } finally { + setMonitoringDrop(null) + } + }, [monitoringDrop, showToast]) + // Sorted Items const sortedItems = useMemo(() => { const mult = sortDirection === 'asc' ? 1 : -1 @@ -426,7 +480,7 @@ export function DropsTab({ showToast }: DropsTabProps) { <>
{/* Table Header */} -
+
+
Status
- + {status === 'available' ? ( + + + Buy Now + + ) : status === 'pending_delete' || status === 'redemption' ? ( + + ) : status === 'taken' ? ( + + ) : ( + + )} - - Check & Buy -
{/* Desktop Row */} -
+
{/* Domain */}
+ {/* Status */} +
+ +
+ {/* Time */}
@@ -544,31 +658,61 @@ export function DropsTab({ showToast }: DropsTabProps) {
{/* Actions */} -
+
- - Check & Buy - - + + {/* Dynamic Action Button based on status */} + {status === 'available' ? ( + + + Buy Now + + ) : status === 'pending_delete' || status === 'redemption' ? ( + + ) : status === 'taken' ? ( + + + Taken + + ) : ( + + )}
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index dee210c..c6e4f17 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -2043,12 +2043,15 @@ class AdminApiClient extends ApiClient { return this.request<{ total: number items: Array<{ + id: number domain: string tld: string dropped_date: string length: number is_numeric: boolean has_hyphen: boolean + availability_status: 'available' | 'pending_delete' | 'redemption' | 'taken' | 'unknown' + last_status_check: string | null }> }>(`/drops?${query}`) } @@ -2063,6 +2066,26 @@ class AdminApiClient extends ApiClient { }> }>('/drops/tlds') } + + async checkDropStatus(dropId: number) { + return this.request<{ + id: number + domain: string + availability_status: 'available' | 'pending_delete' | 'redemption' | 'taken' | 'unknown' + rdap_status: string[] + is_available: boolean + can_register_now: boolean + can_monitor: boolean + }>(`/drops/check-status/${dropId}`, { method: 'POST' }) + } + + async monitorDrop(dropId: number) { + return this.request<{ + status: string + domain: string + message?: string + }>(`/drops/monitor/${dropId}`, { method: 'POST' }) + } } // Yield Types