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
208 lines
4.7 KiB
TypeScript
208 lines
4.7 KiB
TypeScript
/**
|
|
* Global state management with Zustand
|
|
*/
|
|
import { create } from 'zustand'
|
|
import { api } from './api'
|
|
|
|
interface User {
|
|
id: number
|
|
email: string
|
|
name: string | null
|
|
is_admin?: boolean
|
|
is_verified?: boolean
|
|
}
|
|
|
|
interface Domain {
|
|
id: number
|
|
name: string
|
|
status: string
|
|
is_available: boolean
|
|
registrar: string | null
|
|
expiration_date: string | null
|
|
notify_on_available: boolean
|
|
created_at: string
|
|
last_checked: string | null
|
|
}
|
|
|
|
interface Subscription {
|
|
tier: string
|
|
tier_name?: string
|
|
domain_limit: number
|
|
domains_used: number
|
|
portfolio_limit?: number
|
|
check_frequency?: string
|
|
history_days?: number
|
|
features?: {
|
|
email_alerts: boolean
|
|
priority_alerts: boolean
|
|
full_whois: boolean
|
|
expiration_tracking: boolean
|
|
domain_valuation: boolean
|
|
market_insights: boolean
|
|
api_access: boolean
|
|
webhooks: boolean
|
|
bulk_tools: boolean
|
|
seo_metrics: boolean
|
|
}
|
|
}
|
|
|
|
interface AppState {
|
|
// Auth
|
|
user: User | null
|
|
isAuthenticated: boolean
|
|
isLoading: boolean
|
|
|
|
// Domains
|
|
domains: Domain[]
|
|
domainsTotal: number
|
|
domainsPage: number
|
|
|
|
// Subscription
|
|
subscription: Subscription | null
|
|
|
|
// Actions
|
|
login: (email: string, password: string) => Promise<void>
|
|
register: (email: string, password: string, name?: string) => Promise<void>
|
|
logout: () => void
|
|
checkAuth: () => Promise<void>
|
|
|
|
fetchDomains: (page?: number) => Promise<void>
|
|
addDomain: (name: string) => Promise<void>
|
|
deleteDomain: (id: number) => Promise<void>
|
|
refreshDomain: (id: number) => Promise<void>
|
|
updateDomain: (id: number, updates: Partial<Domain>) => void
|
|
|
|
fetchSubscription: () => Promise<void>
|
|
}
|
|
|
|
export const useStore = create<AppState>((set, get) => ({
|
|
// Initial state
|
|
user: null,
|
|
isAuthenticated: false,
|
|
isLoading: true,
|
|
domains: [],
|
|
domainsTotal: 0,
|
|
domainsPage: 1,
|
|
subscription: null,
|
|
|
|
// Auth actions
|
|
login: async (email, password) => {
|
|
await api.login(email, password)
|
|
const user = await api.getMe()
|
|
set({ user, isAuthenticated: true, isLoading: false })
|
|
|
|
// Fetch user data (only once after login)
|
|
await Promise.all([
|
|
get().fetchDomains(),
|
|
get().fetchSubscription()
|
|
])
|
|
},
|
|
|
|
register: async (email, password, name) => {
|
|
await api.register(email, password, name)
|
|
// Auto-login after registration
|
|
await get().login(email, password)
|
|
},
|
|
|
|
logout: () => {
|
|
api.logout()
|
|
set({
|
|
user: null,
|
|
isAuthenticated: false,
|
|
domains: [],
|
|
subscription: null,
|
|
})
|
|
},
|
|
|
|
checkAuth: async () => {
|
|
// Skip if already authenticated and have data (prevents redundant fetches)
|
|
const state = get()
|
|
if (state.isAuthenticated && state.user && state.subscription) {
|
|
set({ isLoading: false })
|
|
return
|
|
}
|
|
|
|
set({ isLoading: true })
|
|
try {
|
|
if (api.getToken()) {
|
|
const user = await api.getMe()
|
|
set({ user, isAuthenticated: true })
|
|
|
|
// Fetch in parallel for speed
|
|
await Promise.all([
|
|
get().fetchDomains(),
|
|
get().fetchSubscription()
|
|
])
|
|
}
|
|
} catch {
|
|
api.logout()
|
|
set({ user: null, isAuthenticated: false })
|
|
} finally {
|
|
set({ isLoading: false })
|
|
}
|
|
},
|
|
|
|
// Domain actions
|
|
fetchDomains: async (page = 1) => {
|
|
try {
|
|
const response = await api.getDomains(page)
|
|
set({
|
|
domains: response.domains,
|
|
domainsTotal: response.total,
|
|
domainsPage: response.page,
|
|
})
|
|
} catch (error) {
|
|
console.error('Failed to fetch domains:', error)
|
|
}
|
|
},
|
|
|
|
addDomain: async (name) => {
|
|
await api.addDomain(name)
|
|
await get().fetchDomains(get().domainsPage)
|
|
await get().fetchSubscription()
|
|
},
|
|
|
|
deleteDomain: async (id) => {
|
|
await api.deleteDomain(id)
|
|
await get().fetchDomains(get().domainsPage)
|
|
await get().fetchSubscription()
|
|
},
|
|
|
|
refreshDomain: async (id) => {
|
|
const updated = await api.refreshDomain(id)
|
|
const domains = get().domains.map((d) =>
|
|
d.id === id ? { ...d, ...updated } : d
|
|
)
|
|
set({ domains })
|
|
},
|
|
|
|
updateDomain: (id, updates) => {
|
|
const domains = get().domains.map((d) =>
|
|
d.id === id ? { ...d, ...updates } : d
|
|
)
|
|
set({ domains })
|
|
},
|
|
|
|
// Subscription actions
|
|
fetchSubscription: async () => {
|
|
try {
|
|
const sub = await api.getSubscription()
|
|
set({
|
|
subscription: {
|
|
tier: sub.tier,
|
|
tier_name: sub.tier_name,
|
|
domain_limit: sub.domain_limit,
|
|
domains_used: sub.domains_used,
|
|
portfolio_limit: sub.portfolio_limit,
|
|
check_frequency: sub.check_frequency,
|
|
history_days: sub.history_days,
|
|
features: sub.features,
|
|
},
|
|
})
|
|
} catch {
|
|
// User might not have subscription
|
|
}
|
|
},
|
|
}))
|
|
|