package badges import ( "fmt" "net/http" "strconv" "strings" "time" "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/core" ) // APIHandler handles badge generation requests type APIHandler struct { app core.App } // NewAPIHandler creates a new badges API handler func NewAPIHandler(app core.App) *APIHandler { return &APIHandler{app: app} } // RegisterRoutes registers badge API routes func (h *APIHandler) RegisterRoutes(se *core.ServeEvent) { // Public badge endpoints (no auth required) se.Router.GET("/badge/:type/:id", h.generateBadge) se.Router.GET("/badge/:type/:id.svg", h.generateBadge) // Protected badge management api := se.Router.Group("/api/beszel/badges") api.GET("/", h.listBadges) api.POST("/", h.createBadge) api.DELETE("/{id}", h.deleteBadge) } // BadgeRequest for creating a badge type BadgeRequest struct { Name string `json:"name"` MonitorID string `json:"monitor_id,omitempty"` DomainID string `json:"domain_id,omitempty"` SystemID string `json:"system_id,omitempty"` StatusPageID string `json:"status_page_id,omitempty"` Type string `json:"type"` // uptime, status, response, cert Label string `json:"label,omitempty"` Color string `json:"color,omitempty"` Style string `json:"style,omitempty"` // flat, flat-square, plastic, for-the-badge } // listBadges lists all badges for the authenticated user func (h *APIHandler) listBadges(e *core.RequestEvent) error { authRecord := e.Auth if authRecord == nil { return e.UnauthorizedError("unauthorized", nil) } records, err := h.app.FindAllRecords("badges", dbx.NewExp("user = {:user}", dbx.Params{"user": authRecord.Id}), ) if err != nil { return e.InternalServerError("failed to fetch badges", err) } badges := make([]map[string]interface{}, 0, len(records)) for _, record := range records { badges = append(badges, map[string]interface{}{ "id": record.Id, "name": record.GetString("name"), "type": record.GetString("type"), "monitor_id": record.GetString("monitor"), "domain_id": record.GetString("domain"), "system_id": record.GetString("system"), "status_page_id": record.GetString("status_page"), "label": record.GetString("label"), "color": record.GetString("color"), "style": record.GetString("style"), "created": record.GetDateTime("created").Time(), }) } return e.JSON(http.StatusOK, badges) } // createBadge creates a new badge configuration func (h *APIHandler) createBadge(e *core.RequestEvent) error { authRecord := e.Auth if authRecord == nil { return e.UnauthorizedError("unauthorized", nil) } var req BadgeRequest if err := e.BindBody(&req); err != nil { return e.BadRequestError("invalid request body", err) } if req.Name == "" || req.Type == "" { return e.BadRequestError("name and type are required", nil) } collection, err := h.app.FindCollectionByNameOrId("badges") if err != nil { return e.InternalServerError("failed to find collection", err) } record := core.NewRecord(collection) record.Set("name", req.Name) record.Set("type", req.Type) record.Set("monitor", req.MonitorID) record.Set("domain", req.DomainID) record.Set("system", req.SystemID) record.Set("status_page", req.StatusPageID) record.Set("label", req.Label) record.Set("color", req.Color) record.Set("style", req.Style) record.Set("user", authRecord.Id) if err := h.app.Save(record); err != nil { return e.InternalServerError("failed to create badge", err) } // Generate badge URL badgeURL := fmt.Sprintf("/badge/%s/%s.svg", req.Type, record.Id) return e.JSON(http.StatusCreated, map[string]interface{}{ "id": record.Id, "name": req.Name, "type": req.Type, "url": badgeURL, "embed_code": fmt.Sprintf(`status`, badgeURL), "markdown": fmt.Sprintf(`![Status](%s)`, badgeURL), }) } // deleteBadge deletes a badge func (h *APIHandler) deleteBadge(e *core.RequestEvent) error { authRecord := e.Auth if authRecord == nil { return e.UnauthorizedError("unauthorized", nil) } id := e.Request.PathValue("id") record, err := h.app.FindRecordById("badges", id) if err != nil { return e.NotFoundError("badge not found", err) } if record.GetString("user") != authRecord.Id { return e.ForbiddenError("not authorized", nil) } if err := h.app.Delete(record); err != nil { return e.InternalServerError("failed to delete badge", err) } return e.NoContent(http.StatusNoContent) } // generateBadge generates an SVG badge func (h *APIHandler) generateBadge(e *core.RequestEvent) error { badgeType := e.Request.PathValue("type") id := e.Request.PathValue("id") // Get query parameters for customization label := e.Request.URL.Query().Get("label") color := e.Request.URL.Query().Get("color") style := e.Request.URL.Query().Get("style") if style == "" { style = "flat" } // Find the resource and get status/uptime var status, message, badgeColor string switch badgeType { case "monitor", "status": record, err := h.app.FindRecordById("monitors", id) if err != nil { return e.NotFoundError("monitor not found", err) } status = record.GetString("status") if label == "" { label = record.GetString("name") } message = status if status == "up" { badgeColor = "brightgreen" } else if status == "down" { badgeColor = "red" } else { badgeColor = "yellow" } case "uptime": record, err := h.app.FindRecordById("monitors", id) if err != nil { return e.NotFoundError("monitor not found", err) } if label == "" { label = "uptime" } // Get uptime from stats uptimeStats := record.GetString("uptime_stats") if uptimeStats != "" { // Parse simple uptime value if available message = uptimeStats + "%" } else { message = "unknown" } badgeColor = "blue" case "domain": record, err := h.app.FindRecordById("domains", id) if err != nil { return e.NotFoundError("domain not found", err) } status = record.GetString("status") if label == "" { label = record.GetString("domain_name") } message = status if status == "active" { badgeColor = "brightgreen" } else if status == "expiring" { badgeColor = "yellow" } else if status == "expired" { badgeColor = "red" } else { badgeColor = "lightgrey" } case "system": record, err := h.app.FindRecordById("systems", id) if err != nil { return e.NotFoundError("system not found", err) } status = record.GetString("status") if label == "" { label = record.GetString("name") } message = status if status == "up" { badgeColor = "brightgreen" } else { badgeColor = "red" } case "response": record, err := h.app.FindRecordById("monitors", id) if err != nil { return e.NotFoundError("monitor not found", err) } if label == "" { label = "response" } responseTime := record.GetInt("last_response_time") message = fmt.Sprintf("%dms", responseTime) if responseTime < 200 { badgeColor = "brightgreen" } else if responseTime < 500 { badgeColor = "yellow" } else { badgeColor = "red" } default: return e.BadRequestError("invalid badge type", nil) } // Override color if provided if color != "" { badgeColor = color } // Generate SVG badge svg := generateSVGBadge(label, message, badgeColor, style) e.Response.Header().Set("Content-Type", "image/svg+xml") e.Response.Header().Set("Cache-Control", "no-cache") return e.String(http.StatusOK, svg) } // generateSVGBadge creates an SVG badge func generateSVGBadge(label, message, color, style string) string { labelWidth := len(label) * 6 + 10 messageWidth := len(message) * 6 + 10 totalWidth := labelWidth + messageWidth // Colors labelColor := "#555" if style == "flat-square" || style == "for-the-badge" { labelColor = "#555" } // SVG template svg := fmt.Sprintf(` %s: %s %s %s %s %s `, totalWidth, label, message, label, message, totalWidth, labelWidth, labelColor, labelWidth, messageWidth, color, totalWidth, labelWidth/2, label, labelWidth/2, label, labelWidth+messageWidth/2, message, labelWidth+messageWidth/2, message, ) return svg } // getOverallStatusPageStatus calculates overall status for a status page func (h *APIHandler) getOverallStatusPageStatus(statusPageID string) (string, float64) { // Get all monitors linked to this status page links, err := h.app.FindAllRecords("status_page_monitors", dbx.NewExp("status_page = {:statusPage}", dbx.Params{"statusPage": statusPageID}), ) if err != nil { return "unknown", 0 } upCount := 0 downCount := 0 totalCount := len(links) for _, link := range links { monitorID := link.GetString("monitor") monitor, err := h.app.FindRecordById("monitors", monitorID) if err != nil { continue } status := monitor.GetString("status") if status == "up" { upCount++ } else if status == "down" { downCount++ } } if totalCount == 0 { return "unknown", 0 } uptime := float64(upCount) / float64(totalCount) * 100 if downCount > 0 { return "down", uptime } else if upCount == totalCount { return "up", uptime } return "degraded", uptime } // GetEmbedCode generates embed code for a badge func GetEmbedCode(badgeURL, format string) string { switch format { case "html": return fmt.Sprintf(`status badge`, badgeURL) case "markdown": return fmt.Sprintf(`![Status](%s)`, badgeURL) case "rst": return fmt.Sprintf(`.. image:: %s :alt: status badge`, badgeURL) case "asciidoc": return fmt.Sprintf(`image:%s[Status]`, badgeURL) default: return fmt.Sprintf(`status badge`, badgeURL) } } // FormatDuration formats a duration for display func FormatDuration(d time.Duration) string { if d < time.Minute { return fmt.Sprintf("%ds", int(d.Seconds())) } else if d < time.Hour { return fmt.Sprintf("%dm", int(d.Minutes())) } else if d < 24*time.Hour { return fmt.Sprintf("%dh", int(d.Hours())) } else { days := int(d.Hours()) / 24 return fmt.Sprintf("%dd", days) } } // ParseDuration parses a duration string (e.g., "24h", "7d") func ParseDuration(s string) (time.Duration, error) { if s == "" { return 0, nil } // Try to parse as number of hours if hours, err := strconv.Atoi(s); err == nil { return time.Duration(hours) * time.Hour, nil } // Parse with suffix if strings.HasSuffix(s, "d") { days, err := strconv.Atoi(s[:len(s)-1]) if err != nil { return 0, err } return time.Duration(days) * 24 * time.Hour, nil } if strings.HasSuffix(s, "h") { hours, err := strconv.Atoi(s[:len(s)-1]) if err != nil { return 0, err } return time.Duration(hours) * time.Hour, nil } if strings.HasSuffix(s, "m") { minutes, err := strconv.Atoi(s[:len(s)-1]) if err != nil { return 0, err } return time.Duration(minutes) * time.Minute, nil } return time.ParseDuration(s) }