🚀 Implement tier-based scan intervals & improve Settings page
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
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
Backend (Scheduler): - Add check_domains_by_frequency() for tier-based scanning - Scout: Daily checks (at configured hour) - Trader: Hourly checks (every :00) - Tycoon: 10-minute real-time checks - Smart tier filtering to avoid duplicate checks Frontend (Pricing): - All feature text now white (text-foreground) Frontend (Settings/Billing): - Show current plan with visual stats (domains, interval, portfolio) - Display check frequency in human-readable format - Full plan comparison table - Green checkmarks for active features - Upgrade CTA for free users
This commit is contained in:
@ -11,6 +11,7 @@ from app.config import get_settings
|
||||
from app.database import AsyncSessionLocal
|
||||
from app.models.domain import Domain, DomainCheck
|
||||
from app.models.user import User
|
||||
from app.models.subscription import Subscription, SubscriptionTier, TIER_CONFIG
|
||||
from app.services.domain_checker import domain_checker
|
||||
from app.services.email_service import email_service
|
||||
from app.services.price_tracker import price_tracker
|
||||
@ -44,14 +45,38 @@ async def scrape_tld_prices():
|
||||
logger.exception(f"TLD price scrape failed: {e}")
|
||||
|
||||
|
||||
async def check_all_domains():
|
||||
"""Check availability of all monitored domains."""
|
||||
logger.info("Starting daily domain check...")
|
||||
async def check_domains_by_frequency(frequency: str):
|
||||
"""Check availability of domains based on their subscription frequency.
|
||||
|
||||
Args:
|
||||
frequency: One of 'daily', 'hourly', 'realtime' (10-min)
|
||||
"""
|
||||
logger.info(f"Starting {frequency} domain check...")
|
||||
start_time = datetime.utcnow()
|
||||
|
||||
async with AsyncSessionLocal() as db:
|
||||
# Get all domains
|
||||
result = await db.execute(select(Domain))
|
||||
# Get users with matching check frequency
|
||||
tiers_for_frequency = []
|
||||
for tier, config in TIER_CONFIG.items():
|
||||
if config['check_frequency'] == frequency:
|
||||
tiers_for_frequency.append(tier)
|
||||
# Realtime includes hourly and daily too (more frequent = superset)
|
||||
elif frequency == 'realtime':
|
||||
tiers_for_frequency.append(tier)
|
||||
elif frequency == 'hourly' and config['check_frequency'] in ['hourly', 'realtime']:
|
||||
tiers_for_frequency.append(tier)
|
||||
|
||||
# Get domains from users with matching subscription tier
|
||||
from sqlalchemy.orm import joinedload
|
||||
result = await db.execute(
|
||||
select(Domain)
|
||||
.join(User, Domain.user_id == User.id)
|
||||
.outerjoin(Subscription, Subscription.user_id == User.id)
|
||||
.where(
|
||||
(Subscription.tier.in_(tiers_for_frequency)) |
|
||||
(Subscription.id.is_(None) & (frequency == 'daily')) # Scout users (no subscription) = daily
|
||||
)
|
||||
)
|
||||
domains = result.scalars().all()
|
||||
|
||||
logger.info(f"Checking {len(domains)} domains...")
|
||||
@ -112,14 +137,47 @@ async def check_all_domains():
|
||||
await send_domain_availability_alerts(db, newly_available)
|
||||
|
||||
|
||||
async def check_all_domains():
|
||||
"""Legacy function - checks all domains (daily)."""
|
||||
await check_domains_by_frequency('daily')
|
||||
|
||||
|
||||
async def check_hourly_domains():
|
||||
"""Check domains for Trader users (hourly)."""
|
||||
await check_domains_by_frequency('hourly')
|
||||
|
||||
|
||||
async def check_realtime_domains():
|
||||
"""Check domains for Tycoon users (every 10 minutes)."""
|
||||
await check_domains_by_frequency('realtime')
|
||||
|
||||
|
||||
def setup_scheduler():
|
||||
"""Configure and start the scheduler."""
|
||||
# Daily domain check at configured hour
|
||||
# Daily domain check for Scout users at configured hour
|
||||
scheduler.add_job(
|
||||
check_all_domains,
|
||||
CronTrigger(hour=settings.check_hour, minute=settings.check_minute),
|
||||
id="daily_domain_check",
|
||||
name="Daily Domain Availability Check",
|
||||
name="Daily Domain Check (Scout)",
|
||||
replace_existing=True,
|
||||
)
|
||||
|
||||
# Hourly domain check for Trader users
|
||||
scheduler.add_job(
|
||||
check_hourly_domains,
|
||||
CronTrigger(minute=0), # Every hour at :00
|
||||
id="hourly_domain_check",
|
||||
name="Hourly Domain Check (Trader)",
|
||||
replace_existing=True,
|
||||
)
|
||||
|
||||
# 10-minute domain check for Tycoon users
|
||||
scheduler.add_job(
|
||||
check_realtime_domains,
|
||||
CronTrigger(minute='*/10'), # Every 10 minutes
|
||||
id="realtime_domain_check",
|
||||
name="10-Minute Domain Check (Tycoon)",
|
||||
replace_existing=True,
|
||||
)
|
||||
|
||||
@ -152,7 +210,9 @@ def setup_scheduler():
|
||||
|
||||
logger.info(
|
||||
f"Scheduler configured:"
|
||||
f"\n - Domain check at {settings.check_hour:02d}:{settings.check_minute:02d}"
|
||||
f"\n - Scout domain check at {settings.check_hour:02d}:{settings.check_minute:02d} (daily)"
|
||||
f"\n - Trader domain check every hour at :00"
|
||||
f"\n - Tycoon domain check every 10 minutes"
|
||||
f"\n - TLD price scrape at 03:00 UTC"
|
||||
f"\n - Price change alerts at 04:00 UTC"
|
||||
f"\n - Auction scrape every hour at :30"
|
||||
|
||||
@ -231,10 +231,7 @@ export default function PricingPage() {
|
||||
{tier.features.map((feature) => (
|
||||
<li key={feature.text} className="flex items-start gap-3">
|
||||
<Check className="w-4 h-4 mt-0.5 shrink-0 text-accent" strokeWidth={2.5} />
|
||||
<span className={clsx(
|
||||
"text-body-sm",
|
||||
feature.highlight ? "text-foreground" : "text-foreground-muted"
|
||||
)}>
|
||||
<span className="text-body-sm text-foreground">
|
||||
{feature.text}
|
||||
</span>
|
||||
</li>
|
||||
|
||||
@ -8,7 +8,6 @@ import { useStore } from '@/lib/store'
|
||||
import { api, PriceAlert } from '@/lib/api'
|
||||
import {
|
||||
User,
|
||||
Mail,
|
||||
Bell,
|
||||
CreditCard,
|
||||
Shield,
|
||||
@ -20,8 +19,8 @@ import {
|
||||
ExternalLink,
|
||||
Crown,
|
||||
Zap,
|
||||
Settings,
|
||||
Key,
|
||||
TrendingUp,
|
||||
} from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import clsx from 'clsx'
|
||||
@ -466,98 +465,214 @@ export default function SettingsPage() {
|
||||
|
||||
{/* Billing Tab */}
|
||||
{activeTab === 'billing' && (
|
||||
<div className="p-6 sm:p-8 bg-background-secondary/30 border border-border rounded-2xl">
|
||||
<h2 className="text-body-lg font-medium text-foreground mb-6">Subscription & Billing</h2>
|
||||
|
||||
<div className="p-5 bg-accent/5 border border-accent/20 rounded-xl mb-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<p className="text-body font-medium text-foreground">{tierName} Plan</p>
|
||||
<p className="text-body-sm text-foreground-muted">
|
||||
{subscription?.check_frequency || 'Daily'} checks · {subscription?.domain_limit || 5} domains
|
||||
</p>
|
||||
</div>
|
||||
<span className={clsx(
|
||||
"px-3 py-1.5 text-ui-xs font-medium rounded-full",
|
||||
isProOrHigher ? "bg-accent/10 text-accent" : "bg-foreground/5 text-foreground-muted"
|
||||
)}>
|
||||
{isProOrHigher ? 'Active' : 'Free'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
{/* Current Plan */}
|
||||
<div className="p-6 sm:p-8 bg-background-secondary/30 border border-border rounded-2xl">
|
||||
<h2 className="text-body-lg font-medium text-foreground mb-6">Your Current Plan</h2>
|
||||
|
||||
{isProOrHigher ? (
|
||||
<button
|
||||
onClick={handleOpenBillingPortal}
|
||||
className="w-full py-3 bg-background text-foreground text-ui font-medium rounded-xl border border-border
|
||||
hover:border-foreground/20 transition-all flex items-center justify-center gap-2"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
Manage Subscription
|
||||
</button>
|
||||
) : (
|
||||
<Link
|
||||
href="/pricing"
|
||||
className="w-full py-3 bg-accent text-background text-ui font-medium rounded-xl
|
||||
hover:bg-accent-hover transition-all flex items-center justify-center gap-2 shadow-lg shadow-accent/20"
|
||||
>
|
||||
<Zap className="w-4 h-4" />
|
||||
Upgrade Plan
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-5 bg-accent/5 border border-accent/20 rounded-xl mb-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{tierName === 'Tycoon' ? (
|
||||
<Crown className="w-6 h-6 text-accent" />
|
||||
) : tierName === 'Trader' ? (
|
||||
<TrendingUp className="w-6 h-6 text-accent" />
|
||||
) : (
|
||||
<Zap className="w-6 h-6 text-accent" />
|
||||
)}
|
||||
<div>
|
||||
<p className="text-xl font-semibold text-foreground">{tierName}</p>
|
||||
<p className="text-body-sm text-foreground-muted">
|
||||
{tierName === 'Scout' ? 'Free forever' : tierName === 'Trader' ? '$19/month' : '$49/month'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className={clsx(
|
||||
"px-3 py-1.5 text-ui-xs font-medium rounded-full",
|
||||
isProOrHigher ? "bg-accent/10 text-accent" : "bg-foreground/5 text-foreground-muted"
|
||||
)}>
|
||||
{isProOrHigher ? 'Active' : 'Free'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Plan Stats */}
|
||||
<div className="grid grid-cols-3 gap-4 p-4 bg-background/50 rounded-xl mb-4">
|
||||
<div className="text-center">
|
||||
<p className="text-2xl font-semibold text-foreground">{subscription?.domain_limit || 5}</p>
|
||||
<p className="text-xs text-foreground-muted">Domains</p>
|
||||
</div>
|
||||
<div className="text-center border-x border-border/50">
|
||||
<p className="text-2xl font-semibold text-foreground">
|
||||
{subscription?.check_frequency === 'realtime' ? '10m' :
|
||||
subscription?.check_frequency === 'hourly' ? '1h' : '24h'}
|
||||
</p>
|
||||
<p className="text-xs text-foreground-muted">Check Interval</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-2xl font-semibold text-foreground">
|
||||
{subscription?.portfolio_limit === -1 ? '∞' : subscription?.portfolio_limit || 0}
|
||||
</p>
|
||||
<p className="text-xs text-foreground-muted">Portfolio</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isProOrHigher ? (
|
||||
<button
|
||||
onClick={handleOpenBillingPortal}
|
||||
className="w-full py-3 bg-background text-foreground text-ui font-medium rounded-xl border border-border
|
||||
hover:border-foreground/20 transition-all flex items-center justify-center gap-2"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
Manage Subscription
|
||||
</button>
|
||||
) : (
|
||||
<Link
|
||||
href="/pricing"
|
||||
className="w-full py-3 bg-accent text-background text-ui font-medium rounded-xl
|
||||
hover:bg-accent-hover transition-all flex items-center justify-center gap-2 shadow-lg shadow-accent/20"
|
||||
>
|
||||
<Zap className="w-4 h-4" />
|
||||
Upgrade Plan
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-body-sm font-medium text-foreground">Plan Features</h3>
|
||||
<ul className="space-y-2">
|
||||
{subscription?.features && Object.entries(subscription.features)
|
||||
.filter(([key]) => !['sms_alerts', 'api_access', 'webhooks', 'bulk_tools', 'seo_metrics'].includes(key))
|
||||
.map(([key, value]) => {
|
||||
const featureNames: Record<string, string> = {
|
||||
email_alerts: 'Email Alerts',
|
||||
priority_alerts: 'Priority Alerts',
|
||||
full_whois: 'Full WHOIS Data',
|
||||
expiration_tracking: 'Expiry Tracking',
|
||||
domain_valuation: 'Domain Valuation',
|
||||
market_insights: 'Market Insights',
|
||||
}
|
||||
return (
|
||||
<li key={key} className="flex items-center gap-2 text-body-sm">
|
||||
{value ? (
|
||||
<Check className="w-4 h-4 text-accent" />
|
||||
) : (
|
||||
<span className="w-4 h-4 text-foreground-subtle">—</span>
|
||||
)}
|
||||
<span className={value ? 'text-foreground' : 'text-foreground-muted'}>
|
||||
{featureNames[key] || key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
|
||||
</span>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
{/* Show additional plan info */}
|
||||
{/* Plan Features */}
|
||||
<h3 className="text-body-sm font-medium text-foreground mb-3">Your Plan Includes</h3>
|
||||
<ul className="grid grid-cols-2 gap-2">
|
||||
<li className="flex items-center gap-2 text-body-sm">
|
||||
<Check className="w-4 h-4 text-accent" />
|
||||
<span className="text-foreground">{subscription?.domain_limit || 5} Watchlist Domains</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2 text-body-sm">
|
||||
<Check className="w-4 h-4 text-accent" />
|
||||
<span className="text-foreground">
|
||||
{subscription?.domain_limit} Watchlist Domains
|
||||
{subscription?.check_frequency === 'realtime' ? '10-minute' :
|
||||
subscription?.check_frequency === 'hourly' ? 'Hourly' : 'Daily'} Scans
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2 text-body-sm">
|
||||
<Check className="w-4 h-4 text-accent" />
|
||||
<span className="text-foreground">Email Alerts</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2 text-body-sm">
|
||||
<Check className="w-4 h-4 text-accent" />
|
||||
<span className="text-foreground">TLD Price Data</span>
|
||||
</li>
|
||||
{subscription?.features?.domain_valuation && (
|
||||
<li className="flex items-center gap-2 text-body-sm">
|
||||
<Check className="w-4 h-4 text-accent" />
|
||||
<span className="text-foreground">Domain Valuation</span>
|
||||
</li>
|
||||
)}
|
||||
{(subscription?.portfolio_limit ?? 0) !== 0 && (
|
||||
<li className="flex items-center gap-2 text-body-sm">
|
||||
<Check className="w-4 h-4 text-accent" />
|
||||
<span className="text-foreground">
|
||||
{subscription?.portfolio_limit === -1 ? 'Unlimited' : subscription?.portfolio_limit} Portfolio Domains
|
||||
{subscription?.portfolio_limit === -1 ? 'Unlimited' : subscription?.portfolio_limit} Portfolio
|
||||
</span>
|
||||
</li>
|
||||
)}
|
||||
{subscription?.features?.expiration_tracking && (
|
||||
<li className="flex items-center gap-2 text-body-sm">
|
||||
<Check className="w-4 h-4 text-accent" />
|
||||
<span className="text-foreground">Expiry Tracking</span>
|
||||
</li>
|
||||
)}
|
||||
{(subscription?.history_days ?? 0) !== 0 && (
|
||||
<li className="flex items-center gap-2 text-body-sm">
|
||||
<Check className="w-4 h-4 text-accent" />
|
||||
<span className="text-foreground">
|
||||
{subscription?.history_days === -1 ? 'Full' : `${subscription?.history_days}-day`} Price History
|
||||
{subscription?.history_days === -1 ? 'Full' : `${subscription?.history_days}-day`} History
|
||||
</span>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Compare All Plans */}
|
||||
<div className="p-6 sm:p-8 bg-background-secondary/30 border border-border rounded-2xl">
|
||||
<h2 className="text-body-lg font-medium text-foreground mb-6">Compare All Plans</h2>
|
||||
|
||||
<div className="overflow-x-auto -mx-2">
|
||||
<table className="w-full min-w-[500px]">
|
||||
<thead>
|
||||
<tr className="border-b border-border">
|
||||
<th className="text-left py-3 px-3 text-body-sm font-medium text-foreground-muted">Feature</th>
|
||||
<th className={clsx(
|
||||
"text-center py-3 px-3 text-body-sm font-medium",
|
||||
tierName === 'Scout' ? "text-accent" : "text-foreground-muted"
|
||||
)}>Scout</th>
|
||||
<th className={clsx(
|
||||
"text-center py-3 px-3 text-body-sm font-medium",
|
||||
tierName === 'Trader' ? "text-accent" : "text-foreground-muted"
|
||||
)}>Trader</th>
|
||||
<th className={clsx(
|
||||
"text-center py-3 px-3 text-body-sm font-medium",
|
||||
tierName === 'Tycoon' ? "text-accent" : "text-foreground-muted"
|
||||
)}>Tycoon</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr className="border-b border-border/50">
|
||||
<td className="py-3 px-3 text-body-sm text-foreground">Price</td>
|
||||
<td className="py-3 px-3 text-center text-body-sm text-foreground">Free</td>
|
||||
<td className="py-3 px-3 text-center text-body-sm text-foreground">$19/mo</td>
|
||||
<td className="py-3 px-3 text-center text-body-sm text-foreground">$49/mo</td>
|
||||
</tr>
|
||||
<tr className="border-b border-border/50">
|
||||
<td className="py-3 px-3 text-body-sm text-foreground">Watchlist Domains</td>
|
||||
<td className="py-3 px-3 text-center text-body-sm text-foreground">5</td>
|
||||
<td className="py-3 px-3 text-center text-body-sm text-foreground">50</td>
|
||||
<td className="py-3 px-3 text-center text-body-sm text-foreground">500</td>
|
||||
</tr>
|
||||
<tr className="border-b border-border/50">
|
||||
<td className="py-3 px-3 text-body-sm text-foreground">Scan Frequency</td>
|
||||
<td className="py-3 px-3 text-center text-body-sm text-foreground">Daily</td>
|
||||
<td className="py-3 px-3 text-center text-body-sm text-foreground">Hourly</td>
|
||||
<td className="py-3 px-3 text-center text-body-sm text-accent font-medium">10 min</td>
|
||||
</tr>
|
||||
<tr className="border-b border-border/50">
|
||||
<td className="py-3 px-3 text-body-sm text-foreground">Portfolio</td>
|
||||
<td className="py-3 px-3 text-center text-body-sm text-foreground-muted">—</td>
|
||||
<td className="py-3 px-3 text-center text-body-sm text-foreground">25</td>
|
||||
<td className="py-3 px-3 text-center text-body-sm text-foreground">Unlimited</td>
|
||||
</tr>
|
||||
<tr className="border-b border-border/50">
|
||||
<td className="py-3 px-3 text-body-sm text-foreground">Domain Valuation</td>
|
||||
<td className="py-3 px-3 text-center text-body-sm text-foreground-muted">—</td>
|
||||
<td className="py-3 px-3 text-center"><Check className="w-5 h-5 text-accent mx-auto" /></td>
|
||||
<td className="py-3 px-3 text-center"><Check className="w-5 h-5 text-accent mx-auto" /></td>
|
||||
</tr>
|
||||
<tr className="border-b border-border/50">
|
||||
<td className="py-3 px-3 text-body-sm text-foreground">Price History</td>
|
||||
<td className="py-3 px-3 text-center text-body-sm text-foreground-muted">—</td>
|
||||
<td className="py-3 px-3 text-center text-body-sm text-foreground">90 days</td>
|
||||
<td className="py-3 px-3 text-center text-body-sm text-foreground">Unlimited</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-3 px-3 text-body-sm text-foreground">Expiry Tracking</td>
|
||||
<td className="py-3 px-3 text-center text-body-sm text-foreground-muted">—</td>
|
||||
<td className="py-3 px-3 text-center"><Check className="w-5 h-5 text-accent mx-auto" /></td>
|
||||
<td className="py-3 px-3 text-center"><Check className="w-5 h-5 text-accent mx-auto" /></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{!isProOrHigher && (
|
||||
<div className="mt-6 text-center">
|
||||
<Link
|
||||
href="/pricing"
|
||||
className="inline-flex items-center gap-2 px-6 py-3 bg-accent text-background text-ui font-medium rounded-xl
|
||||
hover:bg-accent-hover transition-all shadow-lg shadow-accent/20"
|
||||
>
|
||||
<Zap className="w-4 h-4" />
|
||||
Upgrade Now
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user