mirror of
https://github.com/Dvorinka/beszel.git
synced 2026-06-03 21:02:56 +00:00
feat(site): enhance monitoring dashboard and public status pages
Implement incident tracking for public status pages, improve the monitoring dashboard UI with better grouping and loading states, and refine domain resolution logic. - feat(hub): add incident support to public status pages - feat(hub): implement immediate monitor checks on creation and resume - feat(hub): improve domain status detection using DNS fallback when WHOIS fails - feat(site): redesign monitoring dashboard with categorized cards - feat(site): add incident detail view and management in the dashboard - feat(site): add active incidents section to public status pages - feat(site): add "Add System" functionality to systems table - refactor(site): improve calendar view responsiveness and loading states - style(site): add skeleton components for better UX during data fetching
This commit is contained in:
@@ -115,8 +115,26 @@ func (s *Scheduler) checkDomain(record *core.Record) error {
|
||||
|
||||
newData, err := s.whois.LookupDomain(ctx, domainName)
|
||||
if err != nil {
|
||||
log.Printf("[domain-scheduler] Failed to lookup %s: %v", domainName, err)
|
||||
return err
|
||||
log.Printf("[domain-scheduler] WHOIS lookup failed for %s: %v", domainName, err)
|
||||
// Don't return early - try DNS resolution independently to verify domain is alive
|
||||
newData = &domain.Domain{DomainName: domainName}
|
||||
}
|
||||
|
||||
// Independent DNS resolution check: if WHOIS failed, try to resolve the domain directly
|
||||
if err != nil {
|
||||
ips, lookupErr := net.LookupHost(domainName)
|
||||
if lookupErr == nil && len(ips) > 0 {
|
||||
newData.IPv4Addresses = []string{}
|
||||
newData.IPv6Addresses = []string{}
|
||||
for _, ip := range ips {
|
||||
if strings.Contains(ip, ":") {
|
||||
newData.IPv6Addresses = append(newData.IPv6Addresses, ip)
|
||||
} else {
|
||||
newData.IPv4Addresses = append(newData.IPv4Addresses, ip)
|
||||
}
|
||||
}
|
||||
log.Printf("[domain-scheduler] DNS resolution succeeded for %s despite WHOIS failure", domainName)
|
||||
}
|
||||
}
|
||||
|
||||
oldRecord := record.Fresh()
|
||||
@@ -263,7 +281,20 @@ func (s *Scheduler) checkDomain(record *core.Record) error {
|
||||
status = domain.DomainStatusActive
|
||||
}
|
||||
} else {
|
||||
status = domain.DomainStatusUnknown
|
||||
// No expiry date from WHOIS - determine status from DNS resolution.
|
||||
hasDNS := len(newData.IPv4Addresses) > 0 || len(newData.IPv6Addresses) > 0 || len(newData.NameServers) > 0
|
||||
if hasDNS {
|
||||
// DNS resolves means the domain is active and functioning.
|
||||
// If we previously had a valid status (active/expiring), keep it.
|
||||
// If status was unknown or empty, upgrade to active since DNS proves the domain exists.
|
||||
if status == domain.DomainStatusUnknown || status == "" {
|
||||
status = domain.DomainStatusActive
|
||||
}
|
||||
// Otherwise keep the existing valid status (active/expiring)
|
||||
} else {
|
||||
// No DNS resolution and no expiry date - we can't determine the domain's state
|
||||
status = domain.DomainStatusUnknown
|
||||
}
|
||||
}
|
||||
record.Set("status", status)
|
||||
|
||||
|
||||
@@ -177,7 +177,10 @@ func (s *LookupService) tryRDAP(ctx context.Context, domainName string) (*domain
|
||||
// Parse events
|
||||
var creationDate, expiryDate, updatedDate *time.Time
|
||||
for _, event := range rdapResp.Events {
|
||||
t, _ := time.Parse(time.RFC3339, event.EventDate)
|
||||
t, err := time.Parse(time.RFC3339, event.EventDate)
|
||||
if err != nil || t.IsZero() {
|
||||
continue
|
||||
}
|
||||
switch event.EventAction {
|
||||
case "registration":
|
||||
creationDate = &t
|
||||
@@ -831,8 +834,11 @@ func hasValidData(data *domain.WHOISData) bool {
|
||||
if data == nil {
|
||||
return false
|
||||
}
|
||||
// Accept if we got any meaningful data
|
||||
if data.Dates.ExpiryDate != nil || data.Dates.CreationDate != nil {
|
||||
// Accept if we got any meaningful date (non-nil and not zero)
|
||||
if data.Dates.ExpiryDate != nil && !data.Dates.ExpiryDate.IsZero() {
|
||||
return true
|
||||
}
|
||||
if data.Dates.CreationDate != nil && !data.Dates.CreationDate.IsZero() {
|
||||
return true
|
||||
}
|
||||
if data.Registrar.Name != "" && data.Registrar.Name != "Unknown" {
|
||||
|
||||
@@ -2,6 +2,7 @@ package monitors
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -251,6 +252,13 @@ func (h *APIHandler) createMonitor(e *core.RequestEvent) error {
|
||||
// Add to scheduler
|
||||
h.scheduler.AddMonitor(record)
|
||||
|
||||
// Run initial check immediately so the monitor shows real status instead of pending
|
||||
go func() {
|
||||
if _, err := h.scheduler.RunManualCheck(record.Id); err != nil {
|
||||
log.Printf("[monitor-api] Initial check failed for %s: %v", record.Id, err)
|
||||
}
|
||||
}()
|
||||
|
||||
return e.JSON(http.StatusCreated, recordToResponse(record))
|
||||
}
|
||||
|
||||
@@ -485,6 +493,13 @@ func (h *APIHandler) resumeMonitor(e *core.RequestEvent) error {
|
||||
|
||||
h.scheduler.UpdateMonitor(record)
|
||||
|
||||
// Run an immediate check so the monitor shows real status instead of pending
|
||||
go func() {
|
||||
if _, err := h.scheduler.RunManualCheck(record.Id); err != nil {
|
||||
log.Printf("[monitor-api] Resume check failed for %s: %v", record.Id, err)
|
||||
}
|
||||
}()
|
||||
|
||||
return e.JSON(http.StatusOK, recordToResponse(record))
|
||||
}
|
||||
|
||||
|
||||
@@ -390,9 +390,15 @@ func (s *Scheduler) AddMonitor(record *core.Record) {
|
||||
func (s *Scheduler) UpdateMonitor(record *core.Record) {
|
||||
m := recordToMonitor(record)
|
||||
|
||||
// Get existing scheduled monitor to preserve next check time if appropriate
|
||||
// Get existing scheduled monitor
|
||||
if sm, ok := s.monitors.GetOk(m.ID); ok {
|
||||
sm.mu.Lock()
|
||||
wasPaused := sm.Monitor.Status == monitor.StatusPaused || !sm.Monitor.Active
|
||||
nowActive := m.Active && m.Status != monitor.StatusPaused
|
||||
// If monitor just became active (resumed), reset NextCheck so it's checked immediately
|
||||
if wasPaused && nowActive {
|
||||
sm.NextCheck = time.Now()
|
||||
}
|
||||
sm.Monitor = m
|
||||
sm.mu.Unlock()
|
||||
} else {
|
||||
|
||||
@@ -392,6 +392,23 @@ func (h *APIHandler) buildPublicStatusPage(record *core.Record) *statuspage.Publ
|
||||
})
|
||||
}
|
||||
|
||||
// Get active incidents for this user (not closed)
|
||||
var publicIncidents []statuspage.PublicIncident
|
||||
incidentRecords, _ := h.app.FindAllRecords("incidents",
|
||||
dbx.NewExp("user = {:user} && status != {:status}",
|
||||
dbx.Params{"user": record.GetString("user"), "status": "closed"}),
|
||||
)
|
||||
for _, incident := range incidentRecords {
|
||||
publicIncidents = append(publicIncidents, statuspage.PublicIncident{
|
||||
ID: incident.Id,
|
||||
Title: incident.GetString("title"),
|
||||
Description: incident.GetString("description"),
|
||||
Status: incident.GetString("status"),
|
||||
Severity: incident.GetString("severity"),
|
||||
StartedAt: incident.GetDateTime("started_at").Time(),
|
||||
})
|
||||
}
|
||||
|
||||
return &statuspage.PublicStatusPage{
|
||||
ID: record.Id,
|
||||
Name: record.GetString("name"),
|
||||
@@ -402,6 +419,7 @@ func (h *APIHandler) buildPublicStatusPage(record *core.Record) *statuspage.Publ
|
||||
Theme: record.GetString("theme"),
|
||||
CustomCSS: record.GetString("custom_css"),
|
||||
Monitors: publicMonitors,
|
||||
Incidents: publicIncidents,
|
||||
OverallStatus: overallStatus,
|
||||
UpdatedAt: record.GetDateTime("updated").Time(),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user