Add public monitoring features and CI updates

- Add status pages, incidents, badges, maintenance, bulk ops, and metrics
- Add Docker packaging, env example, and frontend routes
- Refresh GitHub workflows and project metadata
This commit is contained in:
Tomas Dvorak
2026-04-27 11:10:18 +02:00
parent 363d708e91
commit 8011d487f1
101 changed files with 16126 additions and 2028 deletions
@@ -46,8 +46,22 @@ const formSchema = z.object({
current_value: z.coerce.number().min(0).optional(),
renewal_cost: z.coerce.number().min(0).optional(),
auto_renew: z.boolean(),
monitor_type: z.enum(["expiry", "watchlist", "portfolio"]),
// Expiry alerts
alert_days_before: z.coerce.number().min(1).max(365),
ssl_alert_enabled: z.boolean(),
ssl_alert_days: z.coerce.number().min(1).max(90),
// Notification settings
notify_on_expiry: z.boolean(),
notify_on_ssl_expiry: z.boolean(),
notify_on_dns_change: z.boolean(),
notify_on_registrar_change: z.boolean(),
notify_on_value_change: z.boolean(),
value_change_threshold: z.coerce.number().min(1).optional(),
// Quiet hours
quiet_hours_enabled: z.boolean(),
quiet_hours_start: z.string(),
quiet_hours_end: z.string(),
})
type FormData = z.infer<typeof formSchema>
@@ -76,8 +90,22 @@ export function DomainDialog({ open, onOpenChange, domain, isEdit = false }: Dom
current_value: 0,
renewal_cost: 0,
auto_renew: false,
monitor_type: "expiry",
// Expiry alerts
alert_days_before: 30,
ssl_alert_enabled: true,
ssl_alert_days: 14,
// Notification settings
notify_on_expiry: true,
notify_on_ssl_expiry: true,
notify_on_dns_change: false,
notify_on_registrar_change: false,
notify_on_value_change: false,
value_change_threshold: 10,
// Quiet hours
quiet_hours_enabled: false,
quiet_hours_start: "22:00",
quiet_hours_end: "08:00",
},
})
@@ -91,8 +119,22 @@ export function DomainDialog({ open, onOpenChange, domain, isEdit = false }: Dom
current_value: domain.current_value || 0,
renewal_cost: domain.renewal_cost || 0,
auto_renew: domain.auto_renew || false,
monitor_type: domain.monitor_type || "expiry",
// Expiry alerts
alert_days_before: domain.alert_days_before || 30,
ssl_alert_enabled: domain.ssl_alert_enabled || true,
ssl_alert_days: domain.ssl_alert_days || 14,
// Notification settings
notify_on_expiry: domain.notify_on_expiry !== false,
notify_on_ssl_expiry: domain.notify_on_ssl_expiry !== false,
notify_on_dns_change: domain.notify_on_dns_change || false,
notify_on_registrar_change: domain.notify_on_registrar_change || false,
notify_on_value_change: domain.notify_on_value_change || false,
value_change_threshold: domain.value_change_threshold || 10,
// Quiet hours
quiet_hours_enabled: domain.quiet_hours_enabled || false,
quiet_hours_start: domain.quiet_hours_start || "22:00",
quiet_hours_end: domain.quiet_hours_end || "08:00",
})
} else if (open && !isEdit) {
form.reset({
@@ -103,8 +145,22 @@ export function DomainDialog({ open, onOpenChange, domain, isEdit = false }: Dom
current_value: 0,
renewal_cost: 0,
auto_renew: false,
monitor_type: "expiry",
// Expiry alerts
alert_days_before: 30,
ssl_alert_enabled: true,
ssl_alert_days: 14,
// Notification settings
notify_on_expiry: true,
notify_on_ssl_expiry: true,
notify_on_dns_change: false,
notify_on_registrar_change: false,
notify_on_value_change: false,
value_change_threshold: 10,
// Quiet hours
quiet_hours_enabled: false,
quiet_hours_start: "22:00",
quiet_hours_end: "08:00",
})
setLookupData(null)
}
@@ -173,8 +229,22 @@ export function DomainDialog({ open, onOpenChange, domain, isEdit = false }: Dom
current_value: data.current_value,
renewal_cost: data.renewal_cost,
auto_renew: data.auto_renew,
monitor_type: data.monitor_type,
// Expiry alerts
alert_days_before: data.alert_days_before,
ssl_alert_enabled: data.ssl_alert_enabled,
ssl_alert_days: data.ssl_alert_days,
// Notification settings
notify_on_expiry: data.notify_on_expiry,
notify_on_ssl_expiry: data.notify_on_ssl_expiry,
notify_on_dns_change: data.notify_on_dns_change,
notify_on_registrar_change: data.notify_on_registrar_change,
notify_on_value_change: data.notify_on_value_change,
value_change_threshold: data.notify_on_value_change ? data.value_change_threshold : undefined,
// Quiet hours
quiet_hours_enabled: data.quiet_hours_enabled,
quiet_hours_start: data.quiet_hours_enabled ? data.quiet_hours_start : undefined,
quiet_hours_end: data.quiet_hours_enabled ? data.quiet_hours_end : undefined,
}
if (isEdit && domain) {
@@ -187,8 +257,22 @@ export function DomainDialog({ open, onOpenChange, domain, isEdit = false }: Dom
current_value: payload.current_value,
renewal_cost: payload.renewal_cost,
auto_renew: payload.auto_renew,
monitor_type: payload.monitor_type,
// Expiry alerts
alert_days_before: payload.alert_days_before,
ssl_alert_enabled: payload.ssl_alert_enabled,
ssl_alert_days: payload.ssl_alert_days,
// Notification settings
notify_on_expiry: payload.notify_on_expiry,
notify_on_ssl_expiry: payload.notify_on_ssl_expiry,
notify_on_dns_change: payload.notify_on_dns_change,
notify_on_registrar_change: payload.notify_on_registrar_change,
notify_on_value_change: payload.notify_on_value_change,
value_change_threshold: payload.value_change_threshold,
// Quiet hours
quiet_hours_enabled: payload.quiet_hours_enabled,
quiet_hours_start: payload.quiet_hours_start,
quiet_hours_end: payload.quiet_hours_end,
},
})
} else {
@@ -231,7 +315,7 @@ export function DomainDialog({ open, onOpenChange, domain, isEdit = false }: Dom
<FormItem className="flex-1">
<FormLabel>Domain Name</FormLabel>
<FormControl>
<Input placeholder="example.com" {...field} />
<Input placeholder="example.com" autoFocus tabIndex={0} {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -381,37 +465,259 @@ export function DomainDialog({ open, onOpenChange, domain, isEdit = false }: Dom
</TabsContent>
<TabsContent value="alerts" className="space-y-4 mt-4">
{/* Monitor Type */}
<FormField
control={form.control}
name="alert_days_before"
name="monitor_type"
render={({ field }) => (
<FormItem>
<FormLabel>Alert Days Before Expiry</FormLabel>
<FormItem className="rounded-lg border p-4">
<FormLabel className="font-medium">Monitoring Purpose</FormLabel>
<FormControl>
<Input type="number" min={1} max={365} {...field} />
<div className="grid grid-cols-3 gap-2 mt-2">
<Button
type="button"
variant={field.value === "expiry" ? "default" : "outline"}
onClick={() => field.onChange("expiry")}
className="flex-col h-auto py-3"
>
<span className="text-xs">Track Expiry</span>
<span className="text-[10px] opacity-70">Monitor expiration</span>
</Button>
<Button
type="button"
variant={field.value === "watchlist" ? "default" : "outline"}
onClick={() => field.onChange("watchlist")}
className="flex-col h-auto py-3"
>
<span className="text-xs">Watch to Buy</span>
<span className="text-[10px] opacity-70">Track availability</span>
</Button>
<Button
type="button"
variant={field.value === "portfolio" ? "default" : "outline"}
onClick={() => field.onChange("portfolio")}
className="flex-col h-auto py-3"
>
<span className="text-xs">Portfolio</span>
<span className="text-[10px] opacity-70">Value tracking</span>
</Button>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="ssl_alert_enabled"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-3">
<div className="space-y-0.5">
<FormLabel>SSL Expiry Alerts</FormLabel>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
{/* Domain Expiry Alerts */}
<div className="space-y-4 rounded-lg border p-4">
<h4 className="font-medium text-sm">Domain Expiry Alerts</h4>
<FormField
control={form.control}
name="notify_on_expiry"
render={({ field }) => (
<FormItem className="flex items-center justify-between">
<div className="space-y-0.5">
<FormLabel>Notify before expiry</FormLabel>
<p className="text-xs text-muted-foreground">Alert when domain is about to expire</p>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
{form.watch("notify_on_expiry") && (
<FormField
control={form.control}
name="alert_days_before"
render={({ field }) => (
<FormItem className="pl-4 border-l-2">
<FormLabel>Days before expiry</FormLabel>
<FormControl>
<Input type="number" min={1} max={365} className="w-32" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
/>
</div>
{/* SSL Alerts */}
<div className="space-y-4 rounded-lg border p-4">
<h4 className="font-medium text-sm">SSL Certificate Alerts</h4>
<FormField
control={form.control}
name="notify_on_ssl_expiry"
render={({ field }) => (
<FormItem className="flex items-center justify-between">
<div className="space-y-0.5">
<FormLabel>Notify on SSL expiry</FormLabel>
<p className="text-xs text-muted-foreground">Alert when SSL certificate expires</p>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
{form.watch("notify_on_ssl_expiry") && (
<FormField
control={form.control}
name="ssl_alert_days"
render={({ field }) => (
<FormItem className="pl-4 border-l-2">
<FormLabel>Days before SSL expiry</FormLabel>
<FormControl>
<Input type="number" min={1} max={90} className="w-32" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
</div>
{/* Change Detection Alerts */}
<div className="space-y-4 rounded-lg border p-4">
<h4 className="font-medium text-sm">Change Detection</h4>
<FormField
control={form.control}
name="notify_on_dns_change"
render={({ field }) => (
<FormItem className="flex items-center justify-between">
<div className="space-y-0.5">
<FormLabel>DNS changes</FormLabel>
<p className="text-xs text-muted-foreground">Alert when DNS records change</p>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="notify_on_registrar_change"
render={({ field }) => (
<FormItem className="flex items-center justify-between">
<div className="space-y-0.5">
<FormLabel>Registrar changes</FormLabel>
<p className="text-xs text-muted-foreground">Alert when registrar information changes</p>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="notify_on_value_change"
render={({ field }) => (
<FormItem className="flex items-center justify-between">
<div className="space-y-0.5">
<FormLabel>Value changes</FormLabel>
<p className="text-xs text-muted-foreground">Alert when estimated value changes significantly</p>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
{form.watch("notify_on_value_change") && (
<FormField
control={form.control}
name="value_change_threshold"
render={({ field }) => (
<FormItem className="pl-4 border-l-2">
<FormLabel>Change threshold (%)</FormLabel>
<FormControl>
<Input type="number" min={1} max={100} className="w-32" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
</div>
{/* Quiet Hours */}
<div className="space-y-4 rounded-lg border p-4">
<h4 className="font-medium text-sm">Quiet Hours</h4>
<FormField
control={form.control}
name="quiet_hours_enabled"
render={({ field }) => (
<FormItem className="flex items-center justify-between">
<div className="space-y-0.5">
<FormLabel>Enable quiet hours</FormLabel>
<p className="text-xs text-muted-foreground">Suppress notifications during specific hours</p>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
{form.watch("quiet_hours_enabled") && (
<div className="grid grid-cols-2 gap-4 pl-4 border-l-2">
<FormField
control={form.control}
name="quiet_hours_start"
render={({ field }) => (
<FormItem>
<FormLabel>Start time</FormLabel>
<FormControl>
<Input type="time" {...field} />
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="quiet_hours_end"
render={({ field }) => (
<FormItem>
<FormLabel>End time</FormLabel>
<FormControl>
<Input type="time" {...field} />
</FormControl>
</FormItem>
)}
/>
</div>
)}
</div>
</TabsContent>
</Tabs>