fix: Consistent domain status across pages + refresh-all timezone fix

Backend:
- Fixed datetime timezone error in refresh-all endpoint
- Added _to_naive_utc() helper for PostgreSQL compatibility

Frontend:
- Watchlist now passes domain status to AnalyzePanel
- Status is consistent between Drops, Watchlist, and Sidepanel
- Shows "Available" or "Taken" status in AnalyzePanel from Watchlist
This commit is contained in:
2025-12-20 23:44:53 +01:00
parent 4995101dd1
commit 5df7d5cb96
2 changed files with 27 additions and 8 deletions

View File

@ -1,6 +1,6 @@
"""Domain management API (requires authentication).""" """Domain management API (requires authentication)."""
import json import json
from datetime import datetime from datetime import datetime, timezone
from math import ceil from math import ceil
from fastapi import APIRouter, HTTPException, status, Query from fastapi import APIRouter, HTTPException, status, Query
@ -16,6 +16,16 @@ from app.services.domain_health import get_health_checker, HealthStatus
router = APIRouter() router = APIRouter()
def _to_naive_utc(dt: datetime | None) -> datetime | None:
"""Convert timezone-aware datetime to naive UTC datetime for PostgreSQL."""
if dt is None:
return None
if dt.tzinfo is not None:
# Convert to UTC and remove timezone info
return dt.astimezone(timezone.utc).replace(tzinfo=None)
return dt
def _safe_json_loads(value: str | None, default): def _safe_json_loads(value: str | None, default):
if not value: if not value:
return default return default
@ -265,7 +275,7 @@ async def refresh_domain(
domain.status = check_result.status domain.status = check_result.status
domain.is_available = check_result.is_available domain.is_available = check_result.is_available
domain.registrar = check_result.registrar domain.registrar = check_result.registrar
domain.expiration_date = check_result.expiration_date domain.expiration_date = _to_naive_utc(check_result.expiration_date)
domain.last_checked = datetime.utcnow() domain.last_checked = datetime.utcnow()
# Create check record # Create check record
@ -342,7 +352,7 @@ async def refresh_all_domains(
domain.status = check_result.status domain.status = check_result.status
domain.is_available = check_result.is_available domain.is_available = check_result.is_available
domain.registrar = check_result.registrar domain.registrar = check_result.registrar
domain.expiration_date = check_result.expiration_date domain.expiration_date = _to_naive_utc(check_result.expiration_date)
domain.last_checked = datetime.utcnow() domain.last_checked = datetime.utcnow()
# Create check record # Create check record

View File

@ -147,7 +147,16 @@ const healthConfig: Record<HealthStatus, { label: string; color: string; bg: str
export default function WatchlistPage() { export default function WatchlistPage() {
const { domains, addDomain, deleteDomain, refreshDomain, updateDomain, subscription, user, logout, checkAuth } = useStore() const { domains, addDomain, deleteDomain, refreshDomain, updateDomain, subscription, user, logout, checkAuth } = useStore()
const { toast, showToast, hideToast } = useToast() const { toast, showToast, hideToast } = useToast()
const openAnalyze = useAnalyzePanelStore((s) => s.open) 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 }) => {
openAnalyzePanel(domainData.name, {
status: domainData.is_available ? 'available' : 'taken',
deletion_date: domainData.expiration_date,
is_drop: false,
})
}, [openAnalyzePanel])
// Modal state // Modal state
const [showAddModal, setShowAddModal] = useState(false) const [showAddModal, setShowAddModal] = useState(false)
@ -608,7 +617,7 @@ export default function WatchlistPage() {
<div className="flex items-start justify-between gap-4 mb-4"> <div className="flex items-start justify-between gap-4 mb-4">
<div className="min-w-0"> <div className="min-w-0">
<button <button
onClick={() => openAnalyze(domain.name)} onClick={() => openAnalyze(domain)}
className="text-lg font-bold text-white font-mono truncate block text-left hover:text-accent transition-colors" className="text-lg font-bold text-white font-mono truncate block text-left hover:text-accent transition-colors"
> >
{domain.name} {domain.name}
@ -667,7 +676,7 @@ export default function WatchlistPage() {
)} )}
<button <button
onClick={() => openAnalyze(domain.name)} onClick={() => openAnalyze(domain)}
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" 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" /> <Shield className="w-5 h-5" />
@ -695,7 +704,7 @@ export default function WatchlistPage() {
{/* Domain */} {/* Domain */}
<div className="flex items-center gap-3 min-w-0"> <div className="flex items-center gap-3 min-w-0">
<button <button
onClick={() => openAnalyze(domain.name)} onClick={() => openAnalyze(domain)}
className="text-sm font-bold text-white font-mono truncate group-hover:text-accent transition-colors text-left" className="text-sm font-bold text-white font-mono truncate group-hover:text-accent transition-colors text-left"
title="Analyze" title="Analyze"
> >
@ -792,7 +801,7 @@ export default function WatchlistPage() {
<RefreshCw className={clsx("w-4 h-4", refreshingId === domain.id && "animate-spin")} /> <RefreshCw className={clsx("w-4 h-4", refreshingId === domain.id && "animate-spin")} />
</button> </button>
<button <button
onClick={() => openAnalyze(domain.name)} onClick={() => openAnalyze(domain)}
title="Analyze" title="Analyze"
className="w-10 h-10 flex items-center justify-center text-white/40 hover:text-accent border border-white/10 hover:bg-accent/10 hover:border-accent/20 transition-all" className="w-10 h-10 flex items-center justify-center text-white/40 hover:text-accent border border-white/10 hover:bg-accent/10 hover:border-accent/20 transition-all"
> >