Deploy: 2025-12-19 09:11
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 09:11:46 +01:00
parent 108b0ae775
commit 8dc6f85fb8
7 changed files with 717 additions and 39 deletions

318
UX_TERMINAL_UX_REPORT.md Normal file
View File

@ -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 910px) → wirkt “pro”, aber ist für längere Sessions anstrengend.
---
## Quick Wins (12 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: “Whats 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 13).
- 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 15 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.

View File

@ -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!"
}

View File

@ -37,7 +37,14 @@ class DroppedDomain(Base):
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'),
)

View File

@ -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

View File

@ -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
]

View File

@ -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<string | null>(null)
// Status Checking
const [checkingStatus, setCheckingStatus] = useState<number | null>(null)
const [monitoringDrop, setMonitoringDrop] = useState<number | null>(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) {
<>
<div className="border border-white/[0.08] bg-[#020202] overflow-hidden">
{/* Table Header */}
<div className="hidden lg:grid grid-cols-[1fr_100px_120px_180px] gap-6 px-6 py-4 text-[10px] font-mono text-white/40 uppercase tracking-[0.15em] border-b border-white/[0.08] bg-white/[0.02]">
<div className="hidden lg:grid grid-cols-[1fr_80px_110px_100px_200px] gap-4 px-6 py-4 text-[10px] font-mono text-white/40 uppercase tracking-[0.15em] border-b border-white/[0.08] bg-white/[0.02]">
<button
onClick={() => handleSort('domain')}
className="flex items-center gap-2 hover:text-white transition-colors text-left"
@ -441,6 +495,7 @@ export function DropsTab({ showToast }: DropsTabProps) {
<span className={clsx(sortField === 'length' && "text-accent font-bold")}>Length</span>
{sortField === 'length' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3 text-accent" /> : <ChevronDown className="w-3 h-3 text-accent" />)}
</button>
<div className="text-center">Status</div>
<button
onClick={() => handleSort('date')}
className="flex items-center gap-2 justify-center hover:text-white transition-colors"
@ -456,9 +511,23 @@ export function DropsTab({ showToast }: DropsTabProps) {
{sortedItems.map((item) => {
const fullDomain = `${item.domain}.${item.tld}`
const isTracking = tracking === fullDomain
const isChecking = checkingStatus === item.id
const isMonitoring = monitoringDrop === item.id
const status = item.availability_status || 'unknown'
// Status display config
const statusConfig = {
available: { label: 'Available', color: 'text-accent', bg: 'bg-accent/10', border: 'border-accent/30', icon: CheckCircle2 },
pending_delete: { label: 'Pending', color: 'text-amber-400', bg: 'bg-amber-400/10', border: 'border-amber-400/30', icon: Clock },
redemption: { label: 'Redemption', color: 'text-orange-400', bg: 'bg-orange-400/10', border: 'border-orange-400/30', icon: AlertCircle },
taken: { label: 'Taken', color: 'text-rose-400', bg: 'bg-rose-400/10', border: 'border-rose-400/30', icon: Ban },
unknown: { label: 'Check', color: 'text-white/40', bg: 'bg-white/5', border: 'border-white/10', icon: Search },
}[status]
const StatusIcon = statusConfig.icon
return (
<div key={fullDomain} className="group hover:bg-white/[0.02] transition-all">
<div key={`${item.id}-${fullDomain}`} className="group hover:bg-white/[0.02] transition-all">
{/* Mobile Row */}
<div className="lg:hidden p-5">
<div className="flex items-start justify-between gap-4 mb-4">
@ -478,42 +547,71 @@ export function DropsTab({ showToast }: DropsTabProps) {
)}>
{item.length} chars
</span>
<span className="text-[10px] font-mono text-white/30 uppercase flex items-center gap-1.5">
<Clock className="w-3 h-3" />
{formatTime(item.dropped_date)}
</span>
<button
onClick={() => checkStatus(item.id, fullDomain)}
disabled={isChecking}
className={clsx(
"text-[10px] font-mono font-bold px-2.5 py-1 border flex items-center gap-1",
statusConfig.color, statusConfig.bg, statusConfig.border
)}
>
{isChecking ? <Loader2 className="w-3 h-3 animate-spin" /> : <StatusIcon className="w-3 h-3" />}
{statusConfig.label}
</button>
</div>
</div>
</div>
<div className="flex gap-2">
<button
onClick={() => track(fullDomain)}
disabled={isTracking}
className="flex-1 h-12 text-xs font-bold uppercase tracking-widest border border-white/10 text-white/50 flex items-center justify-center gap-2 hover:bg-white/5 active:scale-[0.98] transition-all"
>
{isTracking ? <Loader2 className="w-4 h-4 animate-spin" /> : <Eye className="w-4 h-4" />}
Track
</button>
<button
onClick={() => openAnalyze(fullDomain)}
className="w-14 h-12 border border-white/10 text-white/50 flex items-center justify-center hover:text-accent hover:border-accent/30 hover:bg-accent/5 transition-all"
>
<Shield className="w-5 h-5" />
</button>
{status === 'available' ? (
<a
href={`https://www.namecheap.com/domains/registration/results/?domain=${fullDomain}`}
target="_blank"
rel="noopener noreferrer"
className="flex-1 h-12 bg-accent text-black text-xs font-black uppercase tracking-widest flex items-center justify-center gap-2 hover:bg-white active:scale-[0.98] transition-all"
>
Check & Buy
<Zap className="w-4 h-4" />
Buy Now
</a>
) : status === 'pending_delete' || status === 'redemption' ? (
<button
onClick={() => monitorDrop(item.id, fullDomain)}
disabled={isMonitoring}
className="flex-1 h-12 bg-amber-500 text-black text-xs font-black uppercase tracking-widest flex items-center justify-center gap-2 hover:bg-amber-400 active:scale-[0.98] transition-all"
>
{isMonitoring ? <Loader2 className="w-4 h-4 animate-spin" /> : <Eye className="w-4 h-4" />}
Monitor
</button>
) : status === 'taken' ? (
<button
onClick={() => checkStatus(item.id, fullDomain)}
disabled={isChecking}
className="flex-1 h-12 border border-white/10 text-white/40 text-xs font-bold uppercase tracking-widest flex items-center justify-center gap-2"
>
{isChecking ? <Loader2 className="w-4 h-4 animate-spin" /> : <RefreshCw className="w-4 h-4" />}
Recheck
</button>
) : (
<button
onClick={() => checkStatus(item.id, fullDomain)}
disabled={isChecking}
className="flex-1 h-12 border border-accent/30 text-accent text-xs font-bold uppercase tracking-widest flex items-center justify-center gap-2 hover:bg-accent/10 active:scale-[0.98] transition-all"
>
{isChecking ? <Loader2 className="w-4 h-4 animate-spin" /> : <Search className="w-4 h-4" />}
Check Status
</button>
)}
<button
onClick={() => openAnalyze(fullDomain)}
className="w-14 h-12 border border-white/10 text-white/50 flex items-center justify-center hover:text-accent hover:border-accent/30 hover:bg-accent/5 transition-all"
>
<Shield className="w-5 h-5" />
</button>
</div>
</div>
{/* Desktop Row */}
<div className="hidden lg:grid grid-cols-[1fr_100px_120px_180px] gap-6 items-center px-6 py-4">
<div className="hidden lg:grid grid-cols-[1fr_80px_110px_100px_200px] gap-4 items-center px-6 py-3">
{/* Domain */}
<div className="min-w-0">
<button
@ -536,6 +634,22 @@ export function DropsTab({ showToast }: DropsTabProps) {
</span>
</div>
{/* Status */}
<div className="text-center">
<button
onClick={() => checkStatus(item.id, fullDomain)}
disabled={isChecking}
className={clsx(
"text-[10px] font-mono font-bold px-2.5 py-1 border inline-flex items-center gap-1.5 transition-all hover:opacity-80",
statusConfig.color, statusConfig.bg, statusConfig.border
)}
title="Click to check real-time status"
>
{isChecking ? <Loader2 className="w-3 h-3 animate-spin" /> : <StatusIcon className="w-3 h-3" />}
{statusConfig.label}
</button>
</div>
{/* Time */}
<div className="text-center">
<span className="text-xs font-mono text-white/40 uppercase tracking-wider">
@ -544,31 +658,61 @@ export function DropsTab({ showToast }: DropsTabProps) {
</div>
{/* Actions */}
<div className="flex items-center justify-end gap-2 opacity-40 group-hover:opacity-100 transition-all">
<div className="flex items-center justify-end gap-2 opacity-60 group-hover:opacity-100 transition-all">
<button
onClick={() => track(fullDomain)}
disabled={isTracking}
className="w-10 h-10 flex items-center justify-center border border-white/10 text-white/50 hover:text-white hover:bg-white/5 transition-all"
className="w-9 h-9 flex items-center justify-center border border-white/10 text-white/50 hover:text-white hover:bg-white/5 transition-all"
title="Add to Watchlist"
>
{isTracking ? <Loader2 className="w-4 h-4 animate-spin" /> : <Eye className="w-4 h-4" />}
{isTracking ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Eye className="w-3.5 h-3.5" />}
</button>
<button
onClick={() => openAnalyze(fullDomain)}
className="w-10 h-10 flex items-center justify-center border border-white/10 text-white/50 hover:text-accent hover:border-accent/30 hover:bg-accent/5 transition-all"
className="w-9 h-9 flex items-center justify-center border border-white/10 text-white/50 hover:text-accent hover:border-accent/30 hover:bg-accent/5 transition-all"
title="Analyze Domain"
>
<Shield className="w-4 h-4" />
<Shield className="w-3.5 h-3.5" />
</button>
{/* Dynamic Action Button based on status */}
{status === 'available' ? (
<a
href={`https://www.namecheap.com/domains/registration/results/?domain=${fullDomain}`}
target="_blank"
rel="noopener noreferrer"
className="h-10 px-5 bg-accent text-black text-[10px] font-black uppercase tracking-widest flex items-center gap-2 hover:bg-white transition-all"
className="h-9 px-4 bg-accent text-black text-[10px] font-black uppercase tracking-widest flex items-center gap-1.5 hover:bg-white transition-all"
title="Register this domain now!"
>
Check & Buy
<ExternalLink className="w-3.5 h-3.5" />
<Zap className="w-3 h-3" />
Buy Now
</a>
) : status === 'pending_delete' || status === 'redemption' ? (
<button
onClick={() => monitorDrop(item.id, fullDomain)}
disabled={isMonitoring}
className="h-9 px-4 bg-amber-500 text-black text-[10px] font-black uppercase tracking-widest flex items-center gap-1.5 hover:bg-amber-400 transition-all"
title="Get notified when available!"
>
{isMonitoring ? <Loader2 className="w-3 h-3 animate-spin" /> : <Eye className="w-3 h-3" />}
Monitor
</button>
) : status === 'taken' ? (
<span className="h-9 px-4 text-rose-400/60 text-[10px] font-bold uppercase tracking-widest flex items-center gap-1.5 border border-rose-400/20 bg-rose-400/5">
<Ban className="w-3 h-3" />
Taken
</span>
) : (
<button
onClick={() => checkStatus(item.id, fullDomain)}
disabled={isChecking}
className="h-9 px-4 border border-accent/40 text-accent text-[10px] font-bold uppercase tracking-widest flex items-center gap-1.5 hover:bg-accent/10 transition-all"
title="Check availability status"
>
{isChecking ? <Loader2 className="w-3 h-3 animate-spin" /> : <Search className="w-3 h-3" />}
Check
</button>
)}
</div>
</div>
</div>

View File

@ -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