feat(hub,site): enhance domain management and monitor UI
Build Docker images / Hub (push) Failing after 54s

Implement manual domain expiry overrides, improve subdomain discovery via CT logs, and enhance the monitoring dashboard with favicons and configurable display options.

hub:
- allow manual expiry and creation date overrides in domain API when WHOIS lookup fails
- implement JSON parsing for crt.sh certificate transparency log searches in subdomain discovery
- update monitor API routes to use curly brace syntax for path parameters

site:
- add manual registration date and period inputs to domain dialog
- implement monitor favicon support using Google's favicon service
- add configurable display options (uptime pills, heartbeat dots) to monitors table
- update localization files to include new UI elements
This commit is contained in:
Tomas Dvorak
2026-05-10 10:24:28 +02:00
parent b6f40af67f
commit 0dd7db8a82
39 changed files with 641 additions and 218 deletions
@@ -36,7 +36,7 @@ import {
type UpdateDomainRequest,
type DomainLookupResult,
} from "@/lib/domains"
import { Loader2, Search } from "lucide-react"
import { Loader2, Search, AlertTriangle, Calendar } from "lucide-react"
const formSchema = z.object({
domain_name: z.string().min(1, "Domain name is required"),
@@ -79,6 +79,13 @@ export function DomainDialog({ open, onOpenChange, domain, isEdit = false }: Dom
const [activeTab, setActiveTab] = useState("basic")
const [lookupData, setLookupData] = useState<DomainLookupResult | null>(null)
const [isLookingUp, setIsLookingUp] = useState(false)
// Manual expiry inputs when WHOIS fails
const [manualRegDate, setManualRegDate] = useState(() => {
const today = new Date()
return today.toISOString().split("T")[0]
})
const [manualRegPeriod, setManualRegPeriod] = useState<number>(1)
const [manualPurchasePrice, setManualPurchasePrice] = useState<number>(0)
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
@@ -163,6 +170,10 @@ export function DomainDialog({ open, onOpenChange, domain, isEdit = false }: Dom
quiet_hours_end: "08:00",
})
setLookupData(null)
const today = new Date().toISOString().split("T")[0]
setManualRegDate(today)
setManualRegPeriod(1)
setManualPurchasePrice(0)
}
}, [open, isEdit, domain, form])
@@ -207,6 +218,11 @@ export function DomainDialog({ open, onOpenChange, domain, isEdit = false }: Dom
try {
const data = await lookupDomain(domainName)
setLookupData(data)
// Reset manual inputs on new lookup
const today = new Date().toISOString().split("T")[0]
setManualRegDate(today)
setManualRegPeriod(1)
setManualPurchasePrice(0)
toast({ title: "Domain info retrieved successfully" })
} catch (error) {
toast({
@@ -219,6 +235,17 @@ export function DomainDialog({ open, onOpenChange, domain, isEdit = false }: Dom
}
}
// Calculate expiry date from registration date + period in years
const calculateExpiryDate = (regDateStr: string, years: number): string | null => {
const regDate = new Date(regDateStr)
if (isNaN(regDate.getTime())) return null
const expiry = new Date(regDate)
expiry.setFullYear(expiry.getFullYear() + years)
// Subtract 1 day (expiry is typically the day before the anniversary)
expiry.setDate(expiry.getDate() - 1)
return expiry.toISOString().split("T")[0]
}
const onSubmit = (data: FormData) => {
const payload: CreateDomainRequest = {
domain_name: cleanDomain(data.domain_name),
@@ -247,6 +274,19 @@ export function DomainDialog({ open, onOpenChange, domain, isEdit = false }: Dom
quiet_hours_end: data.quiet_hours_enabled ? data.quiet_hours_end : undefined,
}
// If lookup returned no expiry, attach manual dates
if (!isEdit && lookupData && !lookupData.expiry_date) {
const calculatedExpiry = calculateExpiryDate(manualRegDate, manualRegPeriod)
if (calculatedExpiry) {
payload.expiry_date = calculatedExpiry
payload.creation_date = manualRegDate
}
// Use the manual purchase price if set (overrides form value when WHOIS fails)
if (manualPurchasePrice > 0) {
payload.purchase_price = manualPurchasePrice
}
}
if (isEdit && domain) {
updateMutation.mutate({
id: domain.id,
@@ -384,19 +424,91 @@ export function DomainDialog({ open, onOpenChange, domain, isEdit = false }: Dom
/>
{lookupData && !isEdit && (
<div className="rounded-lg border p-4 space-y-2">
<h4 className="font-medium">Lookup Results</h4>
{lookupData.registrar_name && (
<p className="text-sm">Registrar: {lookupData.registrar_name}</p>
)}
{lookupData.expiry_date && (
<p className="text-sm">Expires: {lookupData.expiry_date}</p>
)}
{lookupData.ssl_valid_to && (
<p className="text-sm">SSL Expires: {lookupData.ssl_valid_to}</p>
)}
{lookupData.host_country && (
<p className="text-sm">Location: {lookupData.host_country}</p>
<div className="space-y-3">
{/* Lookup Results */}
<div className="rounded-lg border p-4 space-y-2">
<h4 className="font-medium">Lookup Results</h4>
{lookupData.registrar_name && (
<p className="text-sm">Registrar: {lookupData.registrar_name}</p>
)}
{lookupData.expiry_date && (
<p className="text-sm">Expires: {lookupData.expiry_date}</p>
)}
{lookupData.ssl_valid_to && (
<p className="text-sm">SSL Expires: {lookupData.ssl_valid_to}</p>
)}
{lookupData.host_country && (
<p className="text-sm">Location: {lookupData.host_country}</p>
)}
</div>
{/* Manual expiry fallback when WHOIS doesn't return expiry */}
{!lookupData.expiry_date && (
<div className="rounded-lg border border-yellow-500/30 bg-yellow-500/5 p-4 space-y-3">
<div className="flex items-center gap-2 text-yellow-700">
<AlertTriangle className="h-4 w-4" />
<h4 className="font-medium text-sm">Expiry date not found in WHOIS</h4>
</div>
<p className="text-xs text-muted-foreground">
Enter your registration details below and we&apos;ll calculate the expiry date.
</p>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1">
<label className="text-xs font-medium">Registration Date</label>
<div className="flex items-center gap-2">
<Calendar className="h-3.5 w-3.5 text-muted-foreground" />
<Input
type="date"
value={manualRegDate}
onChange={(e) => setManualRegDate(e.target.value)}
className="h-8 text-sm"
/>
</div>
</div>
<div className="space-y-1">
<label className="text-xs font-medium">Registration Period</label>
<select
value={manualRegPeriod}
onChange={(e) => setManualRegPeriod(Number(e.target.value))}
className="w-full h-8 px-2 rounded-md border border-input bg-background text-sm"
>
<option value={1}>1 year</option>
<option value={2}>2 years</option>
<option value={3}>3 years</option>
<option value={5}>5 years</option>
<option value={10}>10 years</option>
</select>
</div>
</div>
<div className="space-y-1">
<label className="text-xs font-medium">Purchase Price (total for selected period)</label>
<Input
type="number"
min={0}
step="0.01"
value={manualPurchasePrice || ""}
placeholder="e.g. 29.99"
onChange={(e) => setManualPurchasePrice(Number(e.target.value))}
className="h-8 text-sm"
/>
{manualPurchasePrice > 0 && manualRegPeriod > 1 && (
<p className="text-[10px] text-muted-foreground">
~{(manualPurchasePrice / manualRegPeriod).toFixed(2)} per year
</p>
)}
</div>
{manualRegDate && manualRegPeriod > 0 && (
<div className="rounded-md bg-muted p-2 text-xs">
<p className="font-medium">Calculated Expiry:</p>
<p className="text-muted-foreground">
{calculateExpiryDate(manualRegDate, manualRegPeriod)}
</p>
</div>
)}
</div>
)}
</div>
)}