From 2c6d62afcae7fa9907efbde093cb0856d5a6871b Mon Sep 17 00:00:00 2001 From: "yves.gugger" Date: Wed, 10 Dec 2025 15:10:11 +0100 Subject: [PATCH] feat: Add Health Monitoring to Portfolio page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PORTFOLIO HEALTH MONITORING: - Added 'Health' column to portfolio table - Health check button with SSL, DNS, HTTP analysis - Full Health Report Modal with: - DNS Infrastructure (NS, A, MX records) - Website Status (reachable, HTTP status) - SSL Certificate (valid, expiry days) - Signals & Recommendations - Uses quickHealthCheck API for any domain - Status badges: Healthy, Weak, Parked, Critical FEATURE STATUS CHECK: ✅ My Listings (/command/listings): - Create listings with domain verification - DNS-based verification - Public listing pages - Inquiry management ✅ Sniper Alerts (/command/alerts): - Custom filters (TLD, length, price, keywords) - No numbers/hyphens options - Email notification toggle - Test alert functionality - Match history ✅ SEO Juice (/command/seo): - Domain SEO analysis - Domain Authority, Backlinks - Notable links (Wikipedia, Gov, Edu) - Top backlinks list - Tycoon-only feature --- frontend/src/app/command/portfolio/page.tsx | 290 +++++++++++++++++++- 1 file changed, 289 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/command/portfolio/page.tsx b/frontend/src/app/command/portfolio/page.tsx index cf18beb..f0b1c6b 100644 --- a/frontend/src/app/command/portfolio/page.tsx +++ b/frontend/src/app/command/portfolio/page.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react' import { useStore } from '@/lib/store' -import { api, PortfolioDomain, PortfolioSummary, DomainValuation } from '@/lib/api' +import { api, PortfolioDomain, PortfolioSummary, DomainValuation, DomainHealthReport, HealthStatus } from '@/lib/api' import { CommandCenterLayout } from '@/components/CommandCenterLayout' import { PremiumTable, StatCard, PageContainer, TableActionButton } from '@/components/PremiumTable' import { Toast, useToast } from '@/components/Toast' @@ -22,7 +22,24 @@ import { Briefcase, PiggyBank, ShoppingCart, + Activity, + Shield, + AlertTriangle, } from 'lucide-react' + +// Health status configuration +const healthStatusConfig: Record = { + healthy: { label: 'Healthy', color: 'text-accent', bgColor: 'bg-accent/10', icon: Activity }, + weakening: { label: 'Weak', color: 'text-amber-400', bgColor: 'bg-amber-400/10', icon: AlertTriangle }, + parked: { label: 'Parked', color: 'text-orange-400', bgColor: 'bg-orange-400/10', icon: ShoppingCart }, + critical: { label: 'Critical', color: 'text-red-400', bgColor: 'bg-red-400/10', icon: AlertTriangle }, + unknown: { label: 'Unknown', color: 'text-foreground-muted', bgColor: 'bg-foreground/5', icon: Activity }, +} import clsx from 'clsx' import Link from 'next/link' @@ -44,6 +61,11 @@ export default function PortfolioPage() { const [savingEdit, setSavingEdit] = useState(false) const [processingSale, setProcessingSale] = useState(false) const [refreshingId, setRefreshingId] = useState(null) + + // Health monitoring state + const [healthReports, setHealthReports] = useState>({}) + const [loadingHealth, setLoadingHealth] = useState>({}) + const [selectedHealthDomain, setSelectedHealthDomain] = useState(null) const [addForm, setAddForm] = useState({ domain: '', @@ -183,6 +205,21 @@ export default function PortfolioPage() { } } + const handleHealthCheck = async (domainName: string) => { + if (loadingHealth[domainName]) return + + setLoadingHealth(prev => ({ ...prev, [domainName]: true })) + try { + const report = await api.quickHealthCheck(domainName) + setHealthReports(prev => ({ ...prev, [domainName]: report })) + setSelectedHealthDomain(domainName) + } catch (err: any) { + showToast(err.message || 'Health check failed', 'error') + } finally { + setLoadingHealth(prev => ({ ...prev, [domainName]: false })) + } + } + const handleDelete = async (domain: PortfolioDomain) => { if (!confirm(`Remove ${domain.domain} from your portfolio?`)) return @@ -345,12 +382,55 @@ export default function PortfolioPage() { ) ), }, + { + key: 'health', + header: 'Health', + hideOnMobile: true, + render: (domain) => { + const report = healthReports[domain.domain] + if (loadingHealth[domain.domain]) { + return + } + if (report) { + const config = healthStatusConfig[report.status] + const Icon = config.icon + return ( + + ) + } + return ( + + ) + }, + }, { key: 'actions', header: '', align: 'right', render: (domain) => (
+ handleHealthCheck(domain.domain)} + loading={loadingHealth[domain.domain]} + title="Health check (SSL, DNS, HTTP)" + variant={healthReports[domain.domain] ? 'accent' : 'default'} + /> handleValuate(domain)} @@ -583,10 +663,218 @@ export default function PortfolioPage() { ) : null} )} + + {/* Health Report Modal */} + {selectedHealthDomain && healthReports[selectedHealthDomain] && ( + setSelectedHealthDomain(null)} + /> + )} ) } +// Health Report Modal Component +function HealthReportModal({ report, onClose }: { report: DomainHealthReport; onClose: () => void }) { + const config = healthStatusConfig[report.status] + const Icon = config.icon + + return ( +
+
e.stopPropagation()} + > + {/* Header */} +
+
+
+ +
+
+

{report.domain}

+

{config.label}

+
+
+ +
+ + {/* Score */} +
+
+ Health Score +
+
+
= 70 ? "bg-accent" : + report.score >= 40 ? "bg-amber-400" : "bg-red-400" + )} + style={{ width: `${report.score}%` }} + /> +
+ = 70 ? "text-accent" : + report.score >= 40 ? "text-amber-400" : "text-red-400" + )}> + {report.score}/100 + +
+
+
+ + {/* Check Results */} +
+ {/* DNS */} + {report.dns && ( +
+

+ + DNS Infrastructure +

+
+
+ + {report.dns.has_ns ? '✓' : '✗'} + + Nameservers +
+
+ + {report.dns.has_a ? '✓' : '✗'} + + A Record +
+
+ + {report.dns.has_mx ? '✓' : '—'} + + MX Record +
+
+ {report.dns.is_parked && ( +

⚠ Parked at {report.dns.parking_provider || 'unknown provider'}

+ )} +
+ )} + + {/* HTTP */} + {report.http && ( +
+

+ + Website Status +

+
+ + {report.http.is_reachable ? 'Reachable' : 'Unreachable'} + + {report.http.status_code && ( + + HTTP {report.http.status_code} + + )} +
+ {report.http.is_parked && ( +

⚠ Parking page detected

+ )} +
+ )} + + {/* SSL */} + {report.ssl && ( +
+

+ + SSL Certificate +

+
+ {report.ssl.has_certificate ? ( +
+

+ {report.ssl.is_valid ? '✓ Valid certificate' : '✗ Certificate invalid/expired'} +

+ {report.ssl.days_until_expiry !== undefined && ( +

30 ? "text-foreground-muted" : + report.ssl.days_until_expiry > 7 ? "text-amber-400" : "text-red-400" + )}> + Expires in {report.ssl.days_until_expiry} days +

+ )} +
+ ) : ( +

No SSL certificate

+ )} +
+
+ )} + + {/* Signals & Recommendations */} + {((report.signals?.length || 0) > 0 || (report.recommendations?.length || 0) > 0) && ( +
+ {(report.signals?.length || 0) > 0 && ( +
+

Signals

+
    + {report.signals?.map((signal, i) => ( +
  • + + {signal} +
  • + ))} +
+
+ )} + {(report.recommendations?.length || 0) > 0 && ( +
+

Recommendations

+
    + {report.recommendations?.map((rec, i) => ( +
  • + + {rec} +
  • + ))} +
+
+ )} +
+ )} +
+ + {/* Footer */} +
+

+ Checked at {new Date(report.checked_at).toLocaleString()} +

+
+
+
+ ) +} + // Modal Component function Modal({ title, children, onClose }: { title: string; children: React.ReactNode; onClose: () => void }) { return (