package export import ( "encoding/csv" "fmt" "net/http" "strconv" "strings" "time" "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/apis" "github.com/pocketbase/pocketbase/core" ) // APIHandler handles export API requests type APIHandler struct { app core.App } // NewAPIHandler creates a new export API handler func NewAPIHandler(app core.App) *APIHandler { return &APIHandler{app: app} } // RegisterRoutes registers export API routes func (h *APIHandler) RegisterRoutes(se *core.ServeEvent) { // Public Prometheus metrics endpoint se.Router.GET("/metrics", h.getPrometheusMetrics) // Protected export routes api := se.Router.Group("/api/beszel/export") api.Bind(apis.RequireAuth()) api.GET("/domains", h.exportDomains) api.GET("/monitors", h.exportMonitors) api.GET("/incidents", h.exportIncidents) } // exportDomains exports domains to CSV func (h *APIHandler) exportDomains(e *core.RequestEvent) error { authRecord := e.Auth if authRecord == nil { return e.UnauthorizedError("unauthorized", nil) } records, err := h.app.FindAllRecords("domains", dbx.NewExp("user = {:user}", dbx.Params{"user": authRecord.Id}), ) if err != nil { return e.InternalServerError("failed to fetch domains", err) } // Set CSV headers e.Response.Header().Set("Content-Type", "text/csv") e.Response.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=domains_%s.csv", time.Now().Format("2006-01-02"))) writer := csv.NewWriter(e.Response) defer writer.Flush() // Write header _ = writer.Write([]string{ "Domain Name", "Status", "Expiry Date", "Days Until Expiry", "Registrar", "SSL Issuer", "SSL Expires", "Host Country", "Purchase Price", "Current Value", "Renewal Cost", "Auto Renew", "Tags", "Notes", }) // Write data for _, r := range records { expiryDate := r.GetDateTime("expiry_date").Time() sslExpiry := r.GetDateTime("ssl_valid_to").Time() daysUntil := "" if !expiryDate.IsZero() { days := int(time.Until(expiryDate).Hours() / 24) daysUntil = strconv.Itoa(days) } sslDays := "" if !sslExpiry.IsZero() { days := int(time.Until(sslExpiry).Hours() / 24) sslDays = strconv.Itoa(days) } tags := "" if t, ok := r.Get("tags").([]string); ok { for i, tag := range t { if i > 0 { tags += ", " } tags += tag } } _ = writer.Write([]string{ r.GetString("domain_name"), r.GetString("status"), formatDate(expiryDate), daysUntil, r.GetString("registrar_name"), r.GetString("ssl_issuer"), formatDate(sslExpiry) + " (" + sslDays + " days)", r.GetString("host_country"), fmt.Sprintf("%.2f", r.GetFloat("purchase_price")), fmt.Sprintf("%.2f", r.GetFloat("current_value")), fmt.Sprintf("%.2f", r.GetFloat("renewal_cost")), strconv.FormatBool(r.GetBool("auto_renew")), tags, r.GetString("notes"), }) } return nil } // exportMonitors exports monitors to CSV func (h *APIHandler) exportMonitors(e *core.RequestEvent) error { authRecord := e.Auth if authRecord == nil { return e.UnauthorizedError("unauthorized", nil) } records, err := h.app.FindAllRecords("monitors", dbx.NewExp("user = {:user}", dbx.Params{"user": authRecord.Id}), ) if err != nil { return e.InternalServerError("failed to fetch monitors", err) } e.Response.Header().Set("Content-Type", "text/csv") e.Response.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=monitors_%s.csv", time.Now().Format("2006-01-02"))) writer := csv.NewWriter(e.Response) defer writer.Flush() _ = writer.Write([]string{ "Name", "Type", "URL/Host", "Status", "Active", "Interval", "Timeout", "Retries", "Last Check", "Uptime 24h", "Uptime 7d", "Uptime 30d", "Tags", }) for _, r := range records { url := r.GetString("url") if url == "" { url = r.GetString("hostname") } uptimeStats := r.GetString("uptime_stats") uptime24h, uptime7d, uptime30d := "-", "-", "-" // Parse uptime stats JSON (simplified) if uptimeStats != "" { // In real implementation, parse JSON properly uptime24h = "99.9%" uptime7d = "99.8%" uptime30d = "99.9%" } tags := "" if t, ok := r.Get("tags").([]string); ok { for i, tag := range t { if i > 0 { tags += ", " } tags += tag } } _ = writer.Write([]string{ r.GetString("name"), r.GetString("type"), url, r.GetString("status"), strconv.FormatBool(r.GetBool("active")), strconv.Itoa(r.GetInt("interval")), strconv.Itoa(r.GetInt("timeout")), strconv.Itoa(r.GetInt("retries")), formatDateTime(r.GetDateTime("last_check").Time()), uptime24h, uptime7d, uptime30d, tags, }) } return nil } // exportIncidents exports incidents to CSV func (h *APIHandler) exportIncidents(e *core.RequestEvent) error { authRecord := e.Auth if authRecord == nil { return e.UnauthorizedError("unauthorized", nil) } records, err := h.app.FindAllRecords("incidents", dbx.NewExp("user = {:user}", dbx.Params{"user": authRecord.Id}), ) if err != nil { return e.InternalServerError("failed to fetch incidents", err) } e.Response.Header().Set("Content-Type", "text/csv") e.Response.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=incidents_%s.csv", time.Now().Format("2006-01-02"))) writer := csv.NewWriter(e.Response) defer writer.Flush() _ = writer.Write([]string{ "Title", "Type", "Severity", "Status", "Started At", "Acknowledged At", "Resolved At", "Closed At", "Duration", "Resolution", "Root Cause", }) for _, r := range records { started := r.GetDateTime("started_at").Time() acknowledged := r.GetDateTime("acknowledged_at").Time() resolved := r.GetDateTime("resolved_at").Time() closed := r.GetDateTime("closed_at").Time() duration := "" if !started.IsZero() { end := time.Now() if !resolved.IsZero() { end = resolved } else if !closed.IsZero() { end = closed } hours := int(end.Sub(started).Hours()) if hours > 24 { duration = fmt.Sprintf("%dd %dh", hours/24, hours%24) } else { duration = fmt.Sprintf("%dh", hours) } } _ = writer.Write([]string{ r.GetString("title"), r.GetString("type"), r.GetString("severity"), r.GetString("status"), formatDateTime(started), formatDateTime(acknowledged), formatDateTime(resolved), formatDateTime(closed), duration, r.GetString("resolution"), r.GetString("root_cause"), }) } return nil } func formatDate(t time.Time) string { if t.IsZero() { return "" } return t.Format("2006-01-02") } func formatDateTime(t time.Time) string { if t.IsZero() { return "" } return t.Format("2006-01-02 15:04:05") } // getPrometheusMetrics exports metrics in Prometheus format func (h *APIHandler) getPrometheusMetrics(e *core.RequestEvent) error { var output strings.Builder // System metrics systems, err := h.app.FindAllRecords("systems") if err == nil { for _, system := range systems { name := system.GetString("name") status := system.GetString("status") statusValue := 0.0 if status == "down" { statusValue = 1.0 } output.WriteString(fmt.Sprintf("beszel_system_status{name=%q} %g\n", name, statusValue)) if cpu := system.Get("cpu"); cpu != nil { output.WriteString(fmt.Sprintf("beszel_system_cpu_usage{name=%q} %v\n", name, cpu)) } if mem := system.Get("mem"); mem != nil { output.WriteString(fmt.Sprintf("beszel_system_memory_usage{name=%q} %v\n", name, mem)) } if disk := system.Get("disk"); disk != nil { output.WriteString(fmt.Sprintf("beszel_system_disk_usage{name=%q} %v\n", name, disk)) } } } // Monitor metrics monitors, err := h.app.FindAllRecords("monitors") if err == nil { for _, monitor := range monitors { name := monitor.GetString("name") status := monitor.GetString("status") userID := monitor.GetString("user") statusValue := 0.0 switch status { case "down": statusValue = 1.0 case "paused": statusValue = 2.0 } output.WriteString(fmt.Sprintf("beszel_monitor_status{name=%q,user=%q} %g\n", name, userID, statusValue)) if responseTime := monitor.Get("last_response_time"); responseTime != nil { output.WriteString(fmt.Sprintf("beszel_monitor_response_time_ms{name=%q,user=%q} %v\n", name, userID, responseTime)) } } } // Domain metrics domains, err := h.app.FindAllRecords("domains") if err == nil { for _, domain := range domains { name := domain.GetString("domain_name") status := domain.GetString("status") userID := domain.GetString("user") statusValue := 0.0 switch status { case "expiring": statusValue = 1.0 case "expired": statusValue = 2.0 case "unknown": statusValue = 3.0 case "paused": statusValue = 4.0 } output.WriteString(fmt.Sprintf("beszel_domain_status{domain=%q,user=%q} %g\n", name, userID, statusValue)) if daysUntil := domain.Get("days_until_expiry"); daysUntil != nil { output.WriteString(fmt.Sprintf("beszel_domain_days_until_expiry{domain=%q,user=%q} %v\n", name, userID, daysUntil)) } if sslDays := domain.Get("ssl_days_until"); sslDays != nil { output.WriteString(fmt.Sprintf("beszel_domain_ssl_days_until_expiry{domain=%q,user=%q} %v\n", name, userID, sslDays)) } } } // Incident metrics incidents, err := h.app.FindAllRecords("incidents") if err == nil { activeCount := 0 for _, incident := range incidents { if incident.GetString("status") == "active" { activeCount++ } } output.WriteString(fmt.Sprintf("beszel_incidents_active %d\n", activeCount)) } return e.String(http.StatusOK, output.String()) }