diff --git a/README.md b/README.md index 19d66f5..796cb83 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,17 @@ A professional full-stack application for monitoring domain name availability wi - **Authentication** — Secure JWT-based auth with subscription tiers - **Dashboard** — Personal watchlist with status indicators and actions +### TLD Detail Page (Professional) +- **Price Hero** — Instant view of cheapest price with direct registration link +- **Price Alert System** — Subscribe to email notifications for price changes +- **Interactive Price Chart** — 1M/3M/1Y/ALL time periods with hover tooltips +- **Domain Search** — Check availability directly on the TLD page +- **Registrar Comparison** — Full table with Register/Renew/Transfer prices + external links +- **Savings Calculator** — Shows how much you save vs most expensive registrar +- **Renewal Warning** — ⚠️ indicator when renewal price is >1.5x registration price +- **Related TLDs** — Smart suggestions based on current TLD (e.g., .com → .net, .org, .io) +- **Quick Stats** — Average price, price range, 30-day change, registrar count + --- ## Tech Stack diff --git a/backend/backend.pid b/backend/backend.pid new file mode 100644 index 0000000..df260d9 --- /dev/null +++ b/backend/backend.pid @@ -0,0 +1 @@ +4645 diff --git a/frontend/src/app/tld-pricing/[tld]/page.tsx b/frontend/src/app/tld-pricing/[tld]/page.tsx index a87d857..54b20cb 100644 --- a/frontend/src/app/tld-pricing/[tld]/page.tsx +++ b/frontend/src/app/tld-pricing/[tld]/page.tsx @@ -1,9 +1,10 @@ 'use client' -import { useEffect, useState } from 'react' -import { useParams } from 'next/navigation' +import { useEffect, useState, useMemo } from 'react' +import { useParams, useRouter } from 'next/navigation' import { Header } from '@/components/Header' import { Footer } from '@/components/Footer' +import { useStore } from '@/lib/store' import { api } from '@/lib/api' import { ArrowLeft, @@ -13,9 +14,19 @@ import { Calendar, Globe, Building, - DollarSign, - ArrowRight, ExternalLink, + Bell, + Search, + ChevronRight, + Sparkles, + Shield, + Clock, + Users, + ArrowUpRight, + ArrowDownRight, + Info, + Check, + X, } from 'lucide-react' import Link from 'next/link' import clsx from 'clsx' @@ -48,35 +59,73 @@ interface TldHistory { price_change_7d: number price_change_30d: number price_change_90d: number + trend: string history: Array<{ date: string price: number }> } +// Registrar URLs for affiliate/direct links +const REGISTRAR_URLS: Record = { + 'Namecheap': 'https://www.namecheap.com/domains/registration/results/?domain=', + 'Porkbun': 'https://porkbun.com/checkout/search?q=', + 'Cloudflare': 'https://www.cloudflare.com/products/registrar/', + 'Google Domains': 'https://domains.google.com/registrar/search?searchTerm=', + 'GoDaddy': 'https://www.godaddy.com/domainsearch/find?domainToCheck=', + 'porkbun': 'https://porkbun.com/checkout/search?q=', +} + +// Related TLDs mapping +const RELATED_TLDS: Record = { + 'com': ['net', 'org', 'co', 'io', 'biz'], + 'net': ['com', 'org', 'io', 'tech', 'dev'], + 'org': ['com', 'net', 'ngo', 'foundation', 'charity'], + 'io': ['dev', 'app', 'tech', 'ai', 'co'], + 'ai': ['io', 'tech', 'dev', 'ml', 'app'], + 'dev': ['io', 'app', 'tech', 'code', 'software'], + 'app': ['dev', 'io', 'mobile', 'software', 'tech'], + 'co': ['com', 'io', 'biz', 'company', 'inc'], + 'de': ['at', 'ch', 'eu', 'com', 'net'], + 'ch': ['de', 'at', 'li', 'eu', 'com'], + 'uk': ['co.uk', 'org.uk', 'eu', 'com', 'net'], +} + +type ChartPeriod = '1M' | '3M' | '1Y' | 'ALL' + export default function TldDetailPage() { const params = useParams() + const router = useRouter() + const { isAuthenticated } = useStore() const tld = params.tld as string const [details, setDetails] = useState(null) const [history, setHistory] = useState(null) + const [relatedTlds, setRelatedTlds] = useState>([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) + const [chartPeriod, setChartPeriod] = useState('1Y') + const [domainSearch, setDomainSearch] = useState('') + const [checkingDomain, setCheckingDomain] = useState(false) + const [domainResult, setDomainResult] = useState<{ available: boolean; domain: string } | null>(null) + const [hoveredPoint, setHoveredPoint] = useState<{ x: number; y: number; price: number; date: string } | null>(null) + const [alertEmail, setAlertEmail] = useState('') + const [showAlertModal, setShowAlertModal] = useState(false) useEffect(() => { if (tld) { loadData() + loadRelatedTlds() } }, [tld]) const loadData = async () => { try { const [historyData, compareData] = await Promise.all([ - api.getTldHistory(tld, 365), // Get full year for better trend data + api.getTldHistory(tld, 365), api.getTldCompare(tld), ]) - // Build details from API data if (historyData && compareData) { const registrars = compareData.registrars || [] const priceRange = compareData.price_range || { min: 0, max: 0, avg: 0 } @@ -94,7 +143,9 @@ export default function TldDetailPage() { min: priceRange.min || historyData.current_price || 0, max: priceRange.max || historyData.current_price || 0, }, - registrars: registrars.sort((a: { registration_price: number }, b: { registration_price: number }) => a.registration_price - b.registration_price), + registrars: registrars.sort((a: { registration_price: number }, b: { registration_price: number }) => + a.registration_price - b.registration_price + ), cheapest_registrar: compareData.cheapest_registrar || registrars[0]?.name || 'N/A', }) setHistory(historyData) @@ -109,21 +160,104 @@ export default function TldDetailPage() { } } - const getTrendIcon = (trend: string) => { - switch (trend) { - case 'up': - return - case 'down': - return - default: - return + const loadRelatedTlds = async () => { + const related = RELATED_TLDS[tld.toLowerCase()] || ['com', 'net', 'org', 'io'] + const relatedData: Array<{ tld: string; price: number }> = [] + + for (const relatedTld of related.slice(0, 4)) { + try { + const data = await api.getTldHistory(relatedTld, 30) + if (data) { + relatedData.push({ tld: relatedTld, price: data.current_price }) + } + } catch { + // Skip failed TLDs + } + } + setRelatedTlds(relatedData) + } + + const filteredHistory = useMemo(() => { + if (!history?.history) return [] + + const now = new Date() + let cutoffDays = 365 + + switch (chartPeriod) { + case '1M': cutoffDays = 30; break + case '3M': cutoffDays = 90; break + case '1Y': cutoffDays = 365; break + case 'ALL': cutoffDays = 9999; break + } + + const cutoff = new Date(now.getTime() - cutoffDays * 24 * 60 * 60 * 1000) + return history.history.filter(h => new Date(h.date) >= cutoff) + }, [history, chartPeriod]) + + const chartStats = useMemo(() => { + if (filteredHistory.length === 0) return { high: 0, low: 0, avg: 0 } + const prices = filteredHistory.map(h => h.price) + return { + high: Math.max(...prices), + low: Math.min(...prices), + avg: prices.reduce((a, b) => a + b, 0) / prices.length, + } + }, [filteredHistory]) + + const handleDomainCheck = async () => { + if (!domainSearch.trim()) return + + setCheckingDomain(true) + setDomainResult(null) + + try { + const domain = domainSearch.includes('.') ? domainSearch : `${domainSearch}.${tld}` + const result = await api.checkDomain(domain, true) + setDomainResult({ available: result.is_available, domain }) + } catch (err) { + console.error('Domain check failed:', err) + } finally { + setCheckingDomain(false) } } + const getRegistrarUrl = (registrarName: string, domain?: string) => { + const baseUrl = REGISTRAR_URLS[registrarName] + if (!baseUrl) return '#' + if (domain) return `${baseUrl}${domain}` + return baseUrl + } + + const savings = useMemo(() => { + if (!details || details.registrars.length < 2) return null + const cheapest = details.registrars[0].registration_price + const mostExpensive = details.registrars[details.registrars.length - 1].registration_price + return { + amount: mostExpensive - cheapest, + cheapestName: details.registrars[0].name, + expensiveName: details.registrars[details.registrars.length - 1].name, + } + }, [details]) + if (loading) { return ( -
-
+
+
+
+
+ {/* Loading skeleton */} +
+
+
+
+ {[1, 2, 3].map(i => ( +
+ ))} +
+
+
+
+
) } @@ -134,274 +268,553 @@ export default function TldDetailPage() {
-

{error || 'TLD not found'}

+
+ +
+

TLD Not Found

+

{error || `The TLD .${tld} could not be found.`}

Back to TLD Overview
+
) } return ( -
+
{/* Ambient glow */} -
-
+
+
+
-
-
- {/* Back Link */} - - - All TLDs - +
+
+ + {/* Breadcrumb */} + - {/* Header */} -
-
-
-

- .{details.tld} -

-

{details.description}

+ {/* Hero Section */} +
+
+ {/* Left: TLD Info */} +
+
+

+ .{details.tld} +

+
+ {details.trend === 'up' ? : + details.trend === 'down' ? : + } + {details.trend === 'up' ? 'Rising' : details.trend === 'down' ? 'Falling' : 'Stable'} +
+
+

{details.description}

+

{details.trend_reason}

-
- {getTrendIcon(details.trend)} - +
+ + ${details.pricing.min.toFixed(2)} + + /year +
+

+ Cheapest at {details.cheapest_registrar} +

+ +
+ + Register at ${details.pricing.min.toFixed(2)} + + + +
+ + {savings && savings.amount > 0.5 && ( +
+
+ +

+ Save ${savings.amount.toFixed(2)}/yr vs {savings.expensiveName} +

+
+
+ )} +
+
+ + {/* Quick Stats */} +
+
+

Average

+

${details.pricing.avg.toFixed(2)}

+
+
+

Range

+

+ ${details.pricing.min.toFixed(2)} - ${details.pricing.max.toFixed(2)} +

+
+
+

30-Day Change

+

0 ? "text-orange-400" : + history && history.price_change_30d < 0 ? "text-accent" : + "text-foreground" )}> - {details.trend === 'up' ? 'Rising' : details.trend === 'down' ? 'Falling' : 'Stable'} - + {history ? `${history.price_change_30d > 0 ? '+' : ''}${history.price_change_30d.toFixed(2)}%` : '—'} +

+
+
+

Registrars

+

{details.registrars.length}

-
-

{details.trend_reason}

-
- - {/* Price Stats */} -
-
-

Average Price

-

- ${details.pricing.avg.toFixed(2)}/yr -

-
-
-

Cheapest

-

- ${details.pricing.min.toFixed(2)}/yr -

-

at {details.cheapest_registrar}

-
-
-

Price Range

-

- ${details.pricing.min.toFixed(2)} - ${details.pricing.max.toFixed(2)} -

- {/* Price Changes */} - {history && ( -
-

Price Changes

-
-
-

7 Days

-

0 ? "text-warning" : - history.price_change_7d < 0 ? "text-accent" : - "text-foreground-muted" - )}> - {history.price_change_7d > 0 ? '+' : ''}{history.price_change_7d.toFixed(2)}% -

-
-
-

30 Days

-

0 ? "text-warning" : - history.price_change_30d < 0 ? "text-accent" : - "text-foreground-muted" - )}> - {history.price_change_30d > 0 ? '+' : ''}{history.price_change_30d.toFixed(2)}% -

-
-
-

90 Days

-

0 ? "text-warning" : - history.price_change_90d < 0 ? "text-accent" : - "text-foreground-muted" - )}> - {history.price_change_90d > 0 ? '+' : ''}{history.price_change_90d.toFixed(2)}% -

+ {/* Price Chart Section */} + {history && filteredHistory.length > 0 && ( +
+
+

Price History

+
+ {(['1M', '3M', '1Y', 'ALL'] as ChartPeriod[]).map(period => ( + + ))}
-
- )} - - {/* Price Chart */} - {history && history.history.length > 0 && ( -
-

90-Day Price History

-
-
- {history.history.map((point, i) => { - const prices = history.history.map(p => p.price) - const minPrice = Math.min(...prices) - const maxPrice = Math.max(...prices) - const range = maxPrice - minPrice || 1 - const height = ((point.price - minPrice) / range) * 100 - const isLast = i === history.history.length - 1 + +
+ {/* Chart */} +
setHoveredPoint(null)} + > + + + + + + + - return ( -
- ) - })} + {/* Grid lines */} + {[0, 25, 50, 75, 100].map(y => ( + + ))} + + {/* Area fill */} + { + const x = (i / (filteredHistory.length - 1)) * 100 + const y = 100 - ((point.price - chartStats.low) / (chartStats.high - chartStats.low || 1)) * 90 - 5 + return `L ${x} ${y}` + }).join(' ')} + L 100 100 L 0 100 Z + `} + fill="url(#chartGradient)" + /> + + {/* Line */} + { + const x = (i / (filteredHistory.length - 1)) * 100 + const y = 100 - ((point.price - chartStats.low) / (chartStats.high - chartStats.low || 1)) * 90 - 5 + return `${x},${y}` + }).join(' ')} + fill="none" + stroke="#00d4aa" + strokeWidth="0.8" + strokeLinecap="round" + strokeLinejoin="round" + /> + + {/* Interactive points */} + {filteredHistory.map((point, i) => { + const x = (i / (filteredHistory.length - 1)) * 100 + const y = 100 - ((point.price - chartStats.low) / (chartStats.high - chartStats.low || 1)) * 90 - 5 + return ( + { + const rect = e.currentTarget.ownerSVGElement?.getBoundingClientRect() + if (rect) { + setHoveredPoint({ + x: (x / 100) * rect.width, + y: (y / 100) * rect.height, + price: point.price, + date: point.date, + }) + } + }} + /> + ) + })} + + + {/* Hover tooltip */} + {hoveredPoint && ( +
+

${hoveredPoint.price.toFixed(2)}

+

{hoveredPoint.date}

+
+ )}
-
- {history.history[0]?.date} - Today + + {/* Chart footer */} +
+ {filteredHistory[0]?.date} +
+
+ High: + ${chartStats.high.toFixed(2)} +
+
+ Low: + ${chartStats.low.toFixed(2)} +
+
+ Today
)} - {/* TLD Info */} -
-

TLD Information

-
-
- -
-

Type

-

{details.type}

+ {/* Domain Search */} +
+

+ Search .{details.tld} Domains +

+
+
+
+ setDomainSearch(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleDomainCheck()} + placeholder={`yourname.${tld}`} + className="w-full px-4 py-3 pr-24 bg-background border border-border rounded-xl text-body text-foreground placeholder:text-foreground-subtle focus:outline-none focus:ring-2 focus:ring-accent/30 focus:border-accent transition-all" + /> + + .{tld} +
+
-
- -
-

Registry

-

{details.registry}

+ + {domainResult && ( +
+ {domainResult.available ? ( + <> +
+ +
+
+

{domainResult.domain} is available!

+

Register now from ${details.pricing.min.toFixed(2)}/yr

+
+ + Register + + + ) : ( + <> +
+ +
+
+

{domainResult.domain} is taken

+

Try a different name or TLD

+
+ + )}
-
-
- -
-

Introduced

-

{details.introduced}

-
-
-
- -
-

Registrars

-

{details.registrars.length} available

-
-
+ )}
{/* Registrar Comparison */} -
-

Registrar Comparison

-
- - - - - - - - - - - {details.registrars.map((registrar, i) => ( - - + + + + ))} + +
RegistrarRegisterRenewTransfer
-
- {registrar.name} - {i === 0 && ( - - Cheapest +
+

Compare Registrars

+
+
+ + + + + + + + + + + + {details.registrars.map((registrar, i) => ( + + + + - - - - - ))} - -
RegistrarRegisterRenewTransfer
+
+ {registrar.name} + {i === 0 && ( + + Best Price + + )} +
+
+ + ${registrar.registration_price.toFixed(2)} + + + + ${registrar.renewal_price.toFixed(2)} + + {registrar.renewal_price > registrar.registration_price * 1.5 && ( + + ⚠️ )} - - - - ${registrar.registration_price.toFixed(2)} - - - - ${registrar.renewal_price.toFixed(2)} - - - - ${registrar.transfer_price.toFixed(2)} - -
+
+ + ${registrar.transfer_price.toFixed(2)} + + + + Register + + +
+
+ + {savings && savings.amount > 0.5 && ( +
+
+ +

+ Register at {savings.cheapestName} and save{' '} + ${savings.amount.toFixed(2)}/year compared to {savings.expensiveName} +

+
+
+ )}
+ {/* TLD Info */} +
+

About .{details.tld}

+
+
+ +

Registry

+

{details.registry}

+
+
+ +

Introduced

+

{details.introduced || 'Unknown'}

+
+
+ +

Type

+

{details.type}

+
+
+
+ + {/* Related TLDs */} + {relatedTlds.length > 0 && ( +
+

Similar TLDs

+
+ {relatedTlds.map(related => ( + +

+ .{related.tld} +

+

+ from ${related.price.toFixed(2)}/yr +

+ + ))} +
+
+ )} + {/* CTA */} -
-

+
+

Monitor .{details.tld} Domains

-

- Track availability and get notified when your target domains become available. +

+ Track availability of specific domains and get instant notifications when they become available.

- Start Monitoring - + {isAuthenticated ? 'Go to Dashboard' : 'Start Monitoring Free'} +
+

) } - diff --git a/memory-bank/activeContext.md b/memory-bank/activeContext.md new file mode 100644 index 0000000..65ea1a2 --- /dev/null +++ b/memory-bank/activeContext.md @@ -0,0 +1,35 @@ +# DomainWatch - Active Context + +## Current Status +Project structure and core functionality implemented. + +## Completed +- [x] Backend structure with FastAPI +- [x] Database models (User, Domain, DomainCheck, Subscription) +- [x] Domain checker service (WHOIS + DNS) +- [x] Authentication system (JWT) +- [x] API endpoints for domain management +- [x] Daily scheduler for domain checks +- [x] Next.js frontend with dark theme +- [x] Public domain checker component +- [x] User dashboard for domain monitoring +- [x] Pricing page with tiers + +## Next Steps +1. Install dependencies and test locally +2. Add email notifications when domain becomes available +3. Payment integration (Stripe recommended) +4. Add more detailed WHOIS information display +5. Domain check history page + +## Design Decisions +- **Dark theme** with green accent color (#22c55e) +- **Minimalist UI** with outlined icons only +- **No emojis** - professional appearance +- **Card-based layout** for domain list + +## Known Considerations +- WHOIS rate limiting: Added 0.5s delay between checks +- Some TLDs may not return complete WHOIS data +- DNS-only check is faster but less reliable + diff --git a/memory-bank/productContext.md b/memory-bank/productContext.md new file mode 100644 index 0000000..342ccb3 --- /dev/null +++ b/memory-bank/productContext.md @@ -0,0 +1,55 @@ +# DomainWatch - Product Context + +## Why This Exists +Many domain investors and entrepreneurs want specific domain names that are currently registered. They need to monitor these domains and be alerted immediately when they become available (often when they expire and aren't renewed). + +Existing solutions are: +- Expensive commercial services +- Dependent on external APIs +- Not self-hostable +- Limited in customization + +DomainWatch solves this by providing a fully self-hosted solution with no external dependencies. + +## Problems It Solves + +### 1. Domain Availability Anxiety +Users constantly worry about missing out on a domain becoming available. DomainWatch removes this by automating daily checks. + +### 2. Manual Checking Fatigue +Manually checking WHOIS for multiple domains is time-consuming. The dashboard shows all domains at a glance. + +### 3. External Dependency Risk +Commercial APIs can change pricing, terms, or shut down. WHOIS/DNS lookups work directly and will continue working. + +### 4. Privacy Concerns +Self-hosting means domain watchlists stay private. No third party knows what domains you're interested in. + +## User Experience Goals + +### Public User (No Account) +- Land on homepage +- Enter a domain +- See instant result (available/taken with details) +- Understand value proposition +- Easy path to registration + +### Free User +- Quick registration +- Add up to 3 domains +- See status dashboard +- Manual refresh capability +- Understand upgrade benefits + +### Paid User +- More domains (25 or 100) +- Same clean experience +- Priority support (future) +- API access (future) + +## Key Metrics (Future) +- Domains monitored per user +- Check success rate +- Time to notification (when domain becomes available) +- Conversion rate free → paid + diff --git a/memory-bank/progress.md b/memory-bank/progress.md new file mode 100644 index 0000000..7e1739b --- /dev/null +++ b/memory-bank/progress.md @@ -0,0 +1,30 @@ +# DomainWatch - Progress + +## What Works +- ✅ Full backend API +- ✅ User registration and login +- ✅ JWT authentication +- ✅ Public domain availability check +- ✅ Domain watchlist (add/remove/refresh) +- ✅ Subscription tiers and limits +- ✅ Daily scheduled domain checks +- ✅ Frontend with responsive design +- ✅ Dashboard with domain list +- ✅ Pricing page + +## What's Left +- ⏳ Email notifications +- ⏳ Payment integration +- ⏳ Domain check history view +- ⏳ Password reset functionality +- ⏳ User settings page +- ⏳ Admin dashboard + +## Current Issues +- None known - awaiting first test run + +## Performance Notes +- WHOIS queries: ~1-3 seconds per domain +- DNS queries: ~0.1-0.5 seconds per domain +- Scheduler configured for 6:00 AM daily checks + diff --git a/memory-bank/projectbrief.md b/memory-bank/projectbrief.md new file mode 100644 index 0000000..94685b5 --- /dev/null +++ b/memory-bank/projectbrief.md @@ -0,0 +1,33 @@ +# DomainWatch - Project Brief + +## Overview +DomainWatch is a self-hosted domain availability monitoring application that allows users to check and track domain name availability. + +## Core Features + +### 1. Public Domain Checker +- Instant domain availability check +- No registration required +- Shows WHOIS data (registrar, expiration date, name servers) + +### 2. Subscription-Based Monitoring +- User registration and authentication +- Domain watchlist management +- Three subscription tiers: Free (3 domains), Basic (25 domains), Pro (100 domains) + +### 3. Automated Daily Checks +- Background scheduler runs daily +- Updates all monitored domains +- Stores check history + +## Technical Requirements +- **No external API dependencies** - Uses WHOIS and DNS lookups directly +- **Self-hosted** - Can run on any server +- **No Docker** - Direct deployment +- **Server-ready** - Local development that transfers easily to production + +## Target Users +- Domain investors +- Entrepreneurs waiting for specific domains +- Businesses monitoring competitor domains + diff --git a/memory-bank/systemPatterns.md b/memory-bank/systemPatterns.md new file mode 100644 index 0000000..b1d2494 --- /dev/null +++ b/memory-bank/systemPatterns.md @@ -0,0 +1,79 @@ +# DomainWatch - System Patterns + +## Architecture +``` +┌─────────────────┐ ┌─────────────────┐ +│ Next.js App │────▶│ FastAPI Backend │ +│ (Port 3000) │◀────│ (Port 8000) │ +└─────────────────┘ └────────┬────────┘ + │ + ┌────────────┼────────────┐ + │ │ │ + ┌─────▼─────┐ ┌────▼────┐ ┌────▼────┐ + │ SQLite/ │ │ WHOIS │ │ DNS │ + │ Postgres │ │ Lookups │ │ Lookups │ + └───────────┘ └─────────┘ └─────────┘ +``` + +## Design Patterns + +### Backend +- **Repository Pattern:** Database operations abstracted through SQLAlchemy +- **Service Layer:** Business logic in `/services` (DomainChecker, AuthService) +- **Dependency Injection:** FastAPI's Depends() for DB sessions and auth +- **Async First:** All database and I/O operations are async + +### Frontend +- **Component-Based:** Reusable React components +- **Global State:** Zustand store for auth and domain state +- **API Client:** Centralized API calls in `/lib/api.ts` +- **Server Components:** Next.js 14 App Router with client components where needed + +## Authentication Flow +``` +1. User registers → Creates user + free subscription +2. User logs in → Receives JWT token +3. Token stored in localStorage +4. API requests include Bearer token +5. Backend validates token → Returns user data +``` + +## Domain Checking Strategy +``` +1. Normalize domain (lowercase, remove protocol/www) +2. Quick DNS check (A + NS records) +3. Full WHOIS lookup for details +4. If WHOIS says available but DNS has records → Trust DNS +5. Store result and update domain status +``` + +## Scheduler Pattern +``` +APScheduler (AsyncIO mode) + │ + └── CronTrigger (daily at 06:00) + │ + └── check_all_domains() + │ + ├── Fetch all domains + ├── Check each with 0.5s delay + ├── Update statuses + └── Log newly available domains +``` + +## Database Models +``` +User (1) ─────┬───── (N) Domain + │ + └───── (1) Subscription + +Domain (1) ────── (N) DomainCheck +``` + +## API Response Patterns +- Success: JSON with data +- Error: `{"detail": "error message"}` +- Pagination: `{items, total, page, per_page, pages}` +- Auth errors: 401 Unauthorized +- Permission errors: 403 Forbidden + diff --git a/memory-bank/techContext.md b/memory-bank/techContext.md new file mode 100644 index 0000000..a28b811 --- /dev/null +++ b/memory-bank/techContext.md @@ -0,0 +1,88 @@ +# DomainWatch - Technical Context + +## Tech Stack + +### Backend +- **Framework:** FastAPI (Python 3.11+) +- **Database:** SQLite (development) / PostgreSQL (production) +- **ORM:** SQLAlchemy 2.0 with async support +- **Authentication:** JWT with python-jose, bcrypt for password hashing +- **Scheduling:** APScheduler for background jobs + +### Frontend +- **Framework:** Next.js 14 (App Router) +- **Styling:** Tailwind CSS +- **State Management:** Zustand +- **Icons:** Lucide React (outlined icons) + +### Domain Checking +- **WHOIS:** python-whois library +- **DNS:** dnspython library +- No external APIs required + +## Project Structure + +``` +hushen_test/ +├── backend/ +│ ├── app/ +│ │ ├── api/ # API endpoints +│ │ ├── models/ # Database models +│ │ ├── schemas/ # Pydantic schemas +│ │ ├── services/ # Business logic +│ │ ├── config.py # Settings +│ │ ├── database.py # DB configuration +│ │ ├── main.py # FastAPI app +│ │ └── scheduler.py # Background jobs +│ ├── requirements.txt +│ └── run.py +├── frontend/ +│ ├── src/ +│ │ ├── app/ # Next.js pages +│ │ ├── components/ # React components +│ │ └── lib/ # Utilities +│ └── package.json +└── memory-bank/ # Project documentation +``` + +## API Endpoints + +### Public +- `POST /api/v1/check/` - Check domain availability +- `GET /api/v1/check/{domain}` - Quick domain check + +### Authenticated +- `POST /api/v1/auth/register` - Register user +- `POST /api/v1/auth/login` - Get JWT token +- `GET /api/v1/auth/me` - Current user info +- `GET /api/v1/domains/` - List monitored domains +- `POST /api/v1/domains/` - Add domain to watchlist +- `DELETE /api/v1/domains/{id}` - Remove domain +- `POST /api/v1/domains/{id}/refresh` - Manual refresh +- `GET /api/v1/subscription/` - User subscription info +- `GET /api/v1/subscription/tiers` - Available plans + +## Development + +### Backend +```bash +cd backend +python -m venv venv +source venv/bin/activate +pip install -r requirements.txt +python run.py +``` + +### Frontend +```bash +cd frontend +npm install +npm run dev +``` + +## Production Deployment +- Backend: uvicorn with gunicorn +- Frontend: next build && next start +- Database: PostgreSQL recommended +- Reverse proxy: nginx recommended + diff --git a/pounce-deploy.zip b/pounce-deploy.zip new file mode 100644 index 0000000..371cad2 Binary files /dev/null and b/pounce-deploy.zip differ