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
## Watchlist & Monitoring - ✅ Automatic domain monitoring based on subscription tier - ✅ Email alerts when domains become available - ✅ Health checks (DNS/HTTP/SSL) with caching - ✅ Expiry warnings for domains <30 days - ✅ Weekly digest emails - ✅ Instant alert toggle (optimistic UI updates) - ✅ Redesigned health check overlays with full details - 🔒 'Not public' display for .ch/.de domains without public expiry ## Portfolio Management (NEW) - ✅ Track owned domains with purchase price & date - ✅ ROI calculation (unrealized & realized) - ✅ Domain valuation with auto-refresh - ✅ Renewal date tracking - ✅ Sale recording with profit calculation - ✅ List domains for sale directly from portfolio - ✅ Full portfolio summary dashboard ## Listings / For Sale - ✅ Renamed from 'Portfolio' to 'For Sale' - ✅ Fixed listing limits: Scout=0, Trader=5, Tycoon=50 - ✅ Featured badge for Tycoon listings - ✅ Inquiries modal for sellers - ✅ Email notifications when buyer inquires - ✅ Inquiries column in listings table ## Scrapers & Data - ✅ Added 4 new registrar scrapers (Namecheap, Cloudflare, GoDaddy, Dynadot) - ✅ Increased scraping frequency to 2x daily (03:00 & 15:00 UTC) - ✅ Real historical data from database - ✅ Fixed RDAP/WHOIS for .ch/.de domains - ✅ Enhanced SSL certificate parsing ## Scheduler Jobs - ✅ Tiered domain checks (Scout=daily, Trader=hourly, Tycoon=10min) - ✅ Daily health checks (06:00 UTC) - ✅ Weekly expiry warnings (Mon 08:00 UTC) - ✅ Weekly digest emails (Sun 10:00 UTC) - ✅ Auction cleanup every 15 minutes ## UI/UX Improvements - ✅ Removed 'Back' buttons from Intel pages - ✅ Redesigned Radar page to match Market/Intel design - ✅ Less prominent check frequency footer - ✅ Consistent StatCard components across all pages - ✅ Ambient background glows - ✅ Better error handling ## Documentation - ✅ Updated README with monitoring section - ✅ Added env.example with all required variables - ✅ Updated Memory Bank (activeContext.md) - ✅ SMTP configuration requirements documented
125 lines
4.1 KiB
Python
125 lines
4.1 KiB
Python
"""Domain models."""
|
|
from datetime import datetime
|
|
from enum import Enum
|
|
from sqlalchemy import String, Boolean, DateTime, ForeignKey, Text, Enum as SQLEnum
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
|
from app.database import Base
|
|
|
|
|
|
class DomainStatus(str, Enum):
|
|
"""Domain availability status."""
|
|
AVAILABLE = "available"
|
|
TAKEN = "taken"
|
|
ERROR = "error"
|
|
UNKNOWN = "unknown"
|
|
|
|
|
|
class Domain(Base):
|
|
"""Domain model for tracking domain names."""
|
|
|
|
__tablename__ = "domains"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True, index=True)
|
|
name: Mapped[str] = mapped_column(String(255), index=True, nullable=False)
|
|
|
|
# Current status
|
|
status: Mapped[DomainStatus] = mapped_column(
|
|
SQLEnum(DomainStatus), default=DomainStatus.UNKNOWN
|
|
)
|
|
is_available: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
|
|
# WHOIS data (optional)
|
|
registrar: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
expiration_date: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
|
|
|
# User relationship
|
|
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False)
|
|
user: Mapped["User"] = relationship("User", back_populates="domains")
|
|
|
|
# Timestamps
|
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
|
last_checked: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
|
|
|
# Check history relationship
|
|
checks: Mapped[list["DomainCheck"]] = relationship(
|
|
"DomainCheck", back_populates="domain", cascade="all, delete-orphan"
|
|
)
|
|
|
|
# Notification settings
|
|
notify_on_available: Mapped[bool] = mapped_column(Boolean, default=True)
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<Domain {self.name} ({self.status})>"
|
|
|
|
|
|
class DomainCheck(Base):
|
|
"""History of domain availability checks."""
|
|
|
|
__tablename__ = "domain_checks"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True, index=True)
|
|
domain_id: Mapped[int] = mapped_column(ForeignKey("domains.id"), nullable=False)
|
|
|
|
# Check results
|
|
status: Mapped[DomainStatus] = mapped_column(SQLEnum(DomainStatus))
|
|
is_available: Mapped[bool] = mapped_column(Boolean)
|
|
|
|
# Details
|
|
response_data: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
|
|
# Timestamp
|
|
checked_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
|
|
|
# Relationship
|
|
domain: Mapped["Domain"] = relationship("Domain", back_populates="checks")
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<DomainCheck {self.domain_id} at {self.checked_at}>"
|
|
|
|
|
|
class HealthStatus(str, Enum):
|
|
"""Domain health status levels."""
|
|
HEALTHY = "healthy"
|
|
WEAKENING = "weakening"
|
|
PARKED = "parked"
|
|
CRITICAL = "critical"
|
|
UNKNOWN = "unknown"
|
|
|
|
|
|
class DomainHealthCache(Base):
|
|
"""
|
|
Cached health check results for domains.
|
|
|
|
Updated daily by the scheduler to provide instant health status
|
|
without needing manual checks.
|
|
"""
|
|
|
|
__tablename__ = "domain_health_cache"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True, index=True)
|
|
domain_id: Mapped[int] = mapped_column(ForeignKey("domains.id"), unique=True, nullable=False)
|
|
|
|
# Health status
|
|
status: Mapped[str] = mapped_column(String(20), default="unknown")
|
|
score: Mapped[int] = mapped_column(default=0)
|
|
|
|
# Signals (JSON array as text)
|
|
signals: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
|
|
# Layer data (JSON as text for flexibility)
|
|
dns_data: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
http_data: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
ssl_data: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
|
|
# Timestamp
|
|
checked_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
|
|
|
# Relationship
|
|
domain: Mapped["Domain"] = relationship("Domain", backref="health_cache")
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<DomainHealthCache {self.domain_id} status={self.status}>"
|
|
|