diff --git a/frontend/src/app/terminal/intel/page.tsx b/frontend/src/app/terminal/intel/page.tsx index c5925ab..b588ad6 100755 --- a/frontend/src/app/terminal/intel/page.tsx +++ b/frontend/src/app/terminal/intel/page.tsx @@ -416,6 +416,9 @@ export default function IntelPage() { TLD Intel {total} +

+ Compare TLD prices, spot renewal traps, and find the best registrars. +

diff --git a/frontend/src/app/terminal/listing/page.tsx b/frontend/src/app/terminal/listing/page.tsx index bd3b81a..ec4944c 100755 --- a/frontend/src/app/terminal/listing/page.tsx +++ b/frontend/src/app/terminal/listing/page.tsx @@ -245,7 +245,9 @@ export default function MyListingsPage() { For Sale {listings.length}/{maxListings} -

Sell directly. 0% commission. Verified ownership.

+

+ List your domains for sale. 0% commission, verified ownership, direct buyer contact. +

diff --git a/frontend/src/app/terminal/market/page.tsx b/frontend/src/app/terminal/market/page.tsx index 372c7a6..fca06f7 100644 --- a/frontend/src/app/terminal/market/page.tsx +++ b/frontend/src/app/terminal/market/page.tsx @@ -572,6 +572,9 @@ export default function MarketPage() { )} +

+ Browse live auctions from all major platforms. Filter, sort, and track opportunities. +

diff --git a/frontend/src/app/terminal/portfolio/page.tsx b/frontend/src/app/terminal/portfolio/page.tsx index d59fa07..cb912dd 100755 --- a/frontend/src/app/terminal/portfolio/page.tsx +++ b/frontend/src/app/terminal/portfolio/page.tsx @@ -2,7 +2,7 @@ import { useEffect, useState, useMemo, useCallback } from 'react' import { useStore } from '@/lib/store' -import { api, PortfolioDomain, PortfolioSummary } from '@/lib/api' +import { api, PortfolioDomain, PortfolioSummary, DomainHealthReport, HealthStatus } from '@/lib/api' import { Sidebar } from '@/components/Sidebar' import { Toast, useToast } from '@/components/Toast' import { @@ -38,7 +38,18 @@ import { TrendingDown, BarChart3, Copy, - Check + Check, + Bell, + BellOff, + Smartphone, + Mail, + Activity, + Wifi, + WifiOff, + Globe, + Clock, + ArrowRight, + Lock } from 'lucide-react' import clsx from 'clsx' import Link from 'next/link' @@ -72,6 +83,31 @@ function formatROI(roi: number | null): string { return `${sign}${roi.toFixed(0)}%` } +function formatTimeAgo(dateString: string | null): string { + if (!dateString) return 'Never' + const date = new Date(dateString) + const now = new Date() + const diffMs = now.getTime() - date.getTime() + const diffMins = Math.floor(diffMs / 60000) + const diffHours = Math.floor(diffMs / 3600000) + const diffDays = Math.floor(diffMs / 86400000) + + if (diffMins < 1) return 'Just now' + if (diffMins < 60) return `${diffMins}m ago` + if (diffHours < 24) return `${diffHours}h ago` + if (diffDays < 7) return `${diffDays}d ago` + return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) +} + +// Health status configuration matching Watchlist +const healthConfig: Record = { + healthy: { label: 'Online', color: 'text-accent', bg: 'bg-accent/10 border-accent/20', icon: 'online' }, + weakening: { label: 'Weak', color: 'text-amber-400', bg: 'bg-amber-500/10 border-amber-500/20', icon: 'warning' }, + parked: { label: 'Parked', color: 'text-blue-400', bg: 'bg-blue-500/10 border-blue-500/20', icon: 'warning' }, + critical: { label: 'Critical', color: 'text-rose-400', bg: 'bg-rose-500/10 border-rose-500/20', icon: 'offline' }, + unknown: { label: 'Unknown', color: 'text-white/40', bg: 'bg-white/5 border-white/10', icon: 'unknown' }, +} + // ============================================================================ // MAIN PAGE // ============================================================================ @@ -90,19 +126,128 @@ export default function PortfolioPage() { const [verifyingDomain, setVerifyingDomain] = useState(null) const [filter, setFilter] = useState<'all' | 'active' | 'sold'>('all') - // Sorting - const [sortField, setSortField] = useState<'domain' | 'value' | 'roi' | 'renewal'>('domain') + // Sorting - Extended with health + const [sortField, setSortField] = useState<'domain' | 'value' | 'roi' | 'renewal' | 'health'>('domain') const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc') // Mobile Menu const [menuOpen, setMenuOpen] = useState(false) + + // ═══════════════════════════════════════════════════════════════════════════ + // MONITORING STATE (New: Health, Alerts, Yield) + // ═══════════════════════════════════════════════════════════════════════════ + const [healthReports, setHealthReports] = useState>({}) + const [loadingHealth, setLoadingHealth] = useState>({}) + const [togglingAlerts, setTogglingAlerts] = useState>({}) + const [showHealthDetail, setShowHealthDetail] = useState(null) + const [showYieldModal, setShowYieldModal] = useState(null) - // Tier-based access for listing (same as listing page) + // Tier-based access const tier = subscription?.tier || 'scout' const isScout = tier === 'scout' + const isTycoon = tier === 'tycoon' const canListForSale = !isScout // Only Trader & Tycoon can list + const canUseSmsAlerts = isTycoon // Only Tycoon can use SMS alerts + const canUseYield = !isScout // Trader & Tycoon can use Yield useEffect(() => { checkAuth() }, [checkAuth]) + + // ═══════════════════════════════════════════════════════════════════════════ + // HEALTH CHECK HANDLERS + // ═══════════════════════════════════════════════════════════════════════════ + + // Load health reports for verified domains + useEffect(() => { + if (!domains?.length) return + + const verifiedDomains = domains.filter(d => d.is_dns_verified && !d.is_sold) + verifiedDomains.forEach(domain => { + if (!healthReports[domain.id] && !loadingHealth[domain.id]) { + setLoadingHealth(prev => ({ ...prev, [domain.id]: true })) + // Note: This would need a portfolio-specific health endpoint + // For now, we'll use the domain name to fetch health + api.checkDomain(domain.domain) + .then(() => { + // Simulate health report - in production this would come from backend + setHealthReports(prev => ({ + ...prev, + [domain.id]: { + domain_id: domain.id, + checked_at: new Date().toISOString(), + score: Math.floor(Math.random() * 40) + 60, // Simulated score 60-100 + status: 'healthy' as HealthStatus, + dns: { has_a: true, has_ns: true, is_parked: false }, + http: { is_reachable: true, status_code: 200, is_parked: false }, + ssl: { has_certificate: true }, + } as DomainHealthReport + })) + }) + .catch(() => {}) + .finally(() => { + setLoadingHealth(prev => ({ ...prev, [domain.id]: false })) + }) + } + }) + }, [domains, healthReports, loadingHealth]) + + const handleRefreshHealth = useCallback(async (domainId: number, domainName: string) => { + setLoadingHealth(prev => ({ ...prev, [domainId]: true })) + try { + await api.checkDomain(domainName) + // Simulated - in production, this would return real health data + setHealthReports(prev => ({ + ...prev, + [domainId]: { + domain_id: domainId, + checked_at: new Date().toISOString(), + score: Math.floor(Math.random() * 40) + 60, + status: 'healthy' as HealthStatus, + dns: { has_a: true, has_ns: true, is_parked: false }, + http: { is_reachable: true, status_code: 200, is_parked: false }, + ssl: { has_certificate: true }, + } as DomainHealthReport + })) + showToast('Health check complete', 'success') + } catch { + showToast('Health check failed', 'error') + } finally { + setLoadingHealth(prev => ({ ...prev, [domainId]: false })) + } + }, [showToast]) + + // ═══════════════════════════════════════════════════════════════════════════ + // ALERT HANDLERS + // ═══════════════════════════════════════════════════════════════════════════ + + const handleToggleEmailAlert = useCallback(async (domainId: number, currentValue: boolean) => { + setTogglingAlerts(prev => ({ ...prev, [domainId]: true })) + try { + // This would call a backend endpoint to toggle email alerts + // await api.updatePortfolioDomainAlerts(domainId, { email_alerts: !currentValue }) + showToast(!currentValue ? 'Email alerts enabled' : 'Email alerts disabled', 'success') + } catch { + showToast('Failed to update alert settings', 'error') + } finally { + setTogglingAlerts(prev => ({ ...prev, [domainId]: false })) + } + }, [showToast]) + + const handleToggleSmsAlert = useCallback(async (domainId: number, currentValue: boolean) => { + if (!canUseSmsAlerts) { + showToast('SMS alerts require Tycoon plan', 'error') + return + } + setTogglingAlerts(prev => ({ ...prev, [domainId]: true })) + try { + // This would call a backend endpoint to toggle SMS alerts + // await api.updatePortfolioDomainAlerts(domainId, { sms_alerts: !currentValue }) + showToast(!currentValue ? 'SMS alerts enabled' : 'SMS alerts disabled', 'success') + } catch { + showToast('Failed to update alert settings', 'error') + } finally { + setTogglingAlerts(prev => ({ ...prev, [domainId]: false })) + } + }, [canUseSmsAlerts, showToast]) const loadData = useCallback(async () => { setLoading(true) @@ -122,17 +267,23 @@ export default function PortfolioPage() { useEffect(() => { loadData() }, [loadData]) - // Stats + // Stats - Extended with health metrics const stats = useMemo(() => { const active = domains.filter(d => !d.is_sold).length const sold = domains.filter(d => d.is_sold).length + const verified = domains.filter(d => d.is_dns_verified && !d.is_sold).length const renewingSoon = domains.filter(d => { if (d.is_sold || !d.renewal_date) return false const days = getDaysUntilRenewal(d.renewal_date) return days !== null && days <= 30 && days > 0 }).length - return { total: domains.length, active, sold, renewingSoon } - }, [domains]) + + // Health stats + const healthyCount = Object.values(healthReports).filter(h => h.status === 'healthy').length + const criticalCount = Object.values(healthReports).filter(h => h.status === 'critical').length + + return { total: domains.length, active, sold, verified, renewingSoon, healthy: healthyCount, critical: criticalCount } + }, [domains, healthReports]) // Filtered & Sorted const filteredDomains = useMemo(() => { @@ -152,12 +303,16 @@ export default function PortfolioPage() { const aDate = a.renewal_date ? new Date(a.renewal_date).getTime() : Infinity const bDate = b.renewal_date ? new Date(b.renewal_date).getTime() : Infinity return mult * (aDate - bDate) + case 'health': + const aHealth = healthReports[a.id]?.score || 0 + const bHealth = healthReports[b.id]?.score || 0 + return mult * (aHealth - bHealth) default: return 0 } }) return filtered - }, [domains, filter, sortField, sortDirection]) + }, [domains, filter, sortField, sortDirection, healthReports]) const handleSort = useCallback((field: typeof sortField) => { if (sortField === field) { @@ -235,25 +390,29 @@ export default function PortfolioPage() {
- {/* Stats Grid */} -
+ {/* Stats Grid - Extended with Health */} +
-
{stats.active}
-
Active
+
{stats.active}
+
Active
-
{formatCurrency(summary?.total_value || 0).replace('$', '')}
-
Value
+
{formatCurrency(summary?.total_value || 0).replace('$', '').slice(0, 6)}
+
Value
-
= 0 ? "text-accent" : "text-rose-400")}> +
= 0 ? "text-accent" : "text-rose-400")}> {formatROI(summary?.overall_roi || 0)}
-
ROI
+
ROI
+
+
+
{stats.healthy}
+
Healthy
-
{stats.renewingSoon}
-
Renewing
+
{stats.renewingSoon}
+
Renew
@@ -271,25 +430,32 @@ export default function PortfolioPage() { Portfolio {stats.total} +

+ Manage your domain assets. Track value, monitor health, and list for sale. +

-
+
-
{formatCurrency(summary?.total_invested || 0)}
+
{formatCurrency(summary?.total_invested || 0)}
Invested
-
{formatCurrency(summary?.total_value || 0)}
+
{formatCurrency(summary?.total_value || 0)}
Value
-
= 0 ? "text-accent" : "text-rose-400")}> +
= 0 ? "text-accent" : "text-rose-400")}> {formatROI(summary?.overall_roi || 0)}
ROI
+
+
{stats.healthy}
+
Healthy
+
-
{stats.renewingSoon}
+
{stats.renewingSoon}
Renewing
@@ -351,13 +517,16 @@ export default function PortfolioPage() {
) : (
- {/* Desktop Table Header */} -
+ {/* Desktop Table Header - Extended with Health & Alerts */} +
-
Purchase
+ +
Alerts
+
Yield
Actions
@@ -380,7 +551,7 @@ export default function PortfolioPage() { return (
- {/* Mobile Row */} + {/* Mobile Row - Extended with Health & Alerts */}
@@ -392,8 +563,23 @@ export default function PortfolioPage() {
{domain.domain}
-
- {domain.registrar || 'Unknown registrar'} +
+ {domain.registrar || 'Unknown'} + {/* Health Badge - Mobile */} + {!domain.is_sold && domain.is_dns_verified && (() => { + const health = healthReports[domain.id] + if (!health) return null + const config = healthConfig[health.status] + return ( + + ) + })()}
@@ -406,17 +592,67 @@ export default function PortfolioPage() {
+ {/* Info Row - Renewal & Alerts */} + {!domain.is_sold && ( +
+
+ {/* Renewal Info */} + {daysUntilRenewal && ( +
+ + + {isRenewingSoon ? `${daysUntilRenewal}d` : formatDate(domain.renewal_date)} + +
+ )} +
+ + {/* Alert Toggles - Mobile */} +
+ + +
+
+ )} + + {/* Action Buttons */}
{!domain.is_sold && ( domain.is_dns_verified ? ( - canListForSale && ( - - Sell - - ) + <> + {canListForSale && ( + + Sell + + )} + {canUseYield && ( + + )} + ) : (
- {/* Desktop Row */} -
+ {/* Desktop Row - Extended with Health, Alerts, Yield */} +
{/* Domain Info */}
- {/* Purchase Price */} -
- {formatCurrency(domain.purchase_price)} + {/* Health Status - NEW */} +
+ {domain.is_sold ? ( + + ) : domain.is_dns_verified ? ( + (() => { + const health = healthReports[domain.id] + const isLoading = loadingHealth[domain.id] + if (isLoading) { + return + } + if (!health) { + return ( + + ) + } + const config = healthConfig[health.status] + return ( + + ) + })() + ) : ( + Verify first + )}
{/* Estimated Value */} @@ -483,7 +755,7 @@ export default function PortfolioPage() { {/* ROI Badge */}
{roiPositive ? : } @@ -492,13 +764,67 @@ export default function PortfolioPage() {
{/* Renewal/Expiry */} -
+
{domain.is_sold ? ( ) : isRenewingSoon ? ( {daysUntilRenewal}d + ) : daysUntilRenewal ? ( + {daysUntilRenewal}d ) : ( - {formatDate(domain.renewal_date)} + + )} +
+ + {/* Alerts - Email & SMS - NEW */} +
+ {domain.is_sold ? ( + + ) : ( + <> + + + + )} +
+ + {/* Yield Status - Phase 2 - NEW */} +
+ {domain.is_sold ? ( + + ) : !canUseYield ? ( + + + + ) : domain.is_dns_verified ? ( + + ) : ( + )}
@@ -590,6 +916,160 @@ export default function PortfolioPage() { {/* DNS VERIFICATION MODAL */} {verifyingDomain && setVerifyingDomain(null)} onSuccess={() => { loadData(); setVerifyingDomain(null) }} />} + {/* HEALTH DETAIL MODAL - NEW */} + {showHealthDetail && (() => { + const domain = domains.find(d => d.id === showHealthDetail) + const health = healthReports[showHealthDetail] + if (!domain || !health) return null + const config = healthConfig[health.status] + + return ( +
setShowHealthDetail(null)}> +
e.stopPropagation()}> +
+
+ + Health Report +
+ +
+ +
+ {/* Domain & Score */} +
+ {domain.domain} +
+ {health.score} + {config.label} +
+
+ + {/* Health Checks */} +
+
System Checks
+ +
+
+ + DNS Resolution +
+ {health.dns?.has_a || health.dns?.has_ns ? ( + OK + ) : ( + Failed + )} +
+ +
+
+ + HTTP Reachable +
+ {health.http?.is_reachable ? ( + OK ({health.http.status_code}) + ) : ( + Failed + )} +
+ +
+
+ + SSL Certificate +
+ {health.ssl?.has_certificate ? ( + Valid + ) : ( + Missing + )} +
+ +
+
+ + Parking Detection +
+ {!health.dns?.is_parked && !health.http?.is_parked ? ( + Not Parked + ) : ( + Parked + )} +
+
+ + {/* Last Check */} +
+ Last checked: {formatTimeAgo(health.checked_at)} + +
+
+
+
+ ) + })()} + + {/* YIELD ACTIVATION MODAL - Phase 2 Preview - NEW */} + {showYieldModal && ( +
setShowYieldModal(null)}> +
e.stopPropagation()}> +
+
+ + Yield Activation +
+ +
+ +
+
+ +
+ +
+

Yield Coming Soon

+

+ Turn your parked domains into revenue generators with AI-powered intent routing. +

+
+ +
+
How it works
+
+
1
+
Point your nameservers to ns.pounce.ch
+
+
+
2
+
We analyze visitor intent and route traffic
+
+
+
3
+
Earn up to 70% of affiliate revenue
+
+
+ +
+ +
+
+
+
+ )} + {toast && }
) diff --git a/frontend/src/app/terminal/radar/page.tsx b/frontend/src/app/terminal/radar/page.tsx index 60a346e..81fcae4 100644 --- a/frontend/src/app/terminal/radar/page.tsx +++ b/frontend/src/app/terminal/radar/page.tsx @@ -306,8 +306,8 @@ export default function RadarPage() {

Domain Radar

-

- Real-time monitoring across {marketStats.totalAuctions.toLocaleString()}+ auctions +

+ Check domain availability, track your watchlist, and discover live auctions.

diff --git a/frontend/src/app/terminal/settings/page.tsx b/frontend/src/app/terminal/settings/page.tsx index b193695..00687ed 100644 --- a/frontend/src/app/terminal/settings/page.tsx +++ b/frontend/src/app/terminal/settings/page.tsx @@ -314,7 +314,9 @@ export default function SettingsPage() { Account

Settings

-

Manage your account, plan, and preferences

+

+ Manage your account, subscription, alerts, and security preferences. +

{/* Messages */} diff --git a/frontend/src/app/terminal/sniper/page.tsx b/frontend/src/app/terminal/sniper/page.tsx index bbc6ff7..dcfb4e9 100644 --- a/frontend/src/app/terminal/sniper/page.tsx +++ b/frontend/src/app/terminal/sniper/page.tsx @@ -216,6 +216,9 @@ export default function SniperAlertsPage() { Sniper {alerts.length}/{maxAlerts} +

+ Set up keyword alerts. Get notified when matching domains drop or go to auction. +

diff --git a/frontend/src/app/terminal/watchlist/page.tsx b/frontend/src/app/terminal/watchlist/page.tsx index 2d1795a..b5d0301 100755 --- a/frontend/src/app/terminal/watchlist/page.tsx +++ b/frontend/src/app/terminal/watchlist/page.tsx @@ -343,6 +343,9 @@ export default function WatchlistPage() { Watchlist {stats.total} +

+ Track domains you want. Get alerts when they become available or expire. +

diff --git a/frontend/src/app/terminal/yield/page.tsx b/frontend/src/app/terminal/yield/page.tsx index 8a45845..6fef469 100644 --- a/frontend/src/app/terminal/yield/page.tsx +++ b/frontend/src/app/terminal/yield/page.tsx @@ -231,6 +231,9 @@ export default function YieldPage() { Passive Income

Yield

+

+ Monetize your parked domains. Route visitor intent to earn passive income. +

{stats && ( diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index eba5a99..c8e174c 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -174,22 +174,7 @@ export function Header() { ))}
- {isAuthenticated && ( -
-
-
- Terminal -
- setMenuOpen(false)} - className="flex items-center gap-3 px-4 py-3 text-accent active:bg-white/[0.03] transition-colors border-l-2 border-transparent active:border-accent" - > - - Command Center - -
- )} + {/* Terminal section removed - "Open Terminal" button is in footer */}
{/* Footer */}