mirror of
https://github.com/Dvorinka/beszel.git
synced 2026-06-03 21:02:56 +00:00
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:
@@ -0,0 +1,295 @@
|
||||
package maintenance
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/apis"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
|
||||
// APIHandler handles maintenance window API requests
|
||||
type APIHandler struct {
|
||||
app core.App
|
||||
}
|
||||
|
||||
// NewAPIHandler creates a new maintenance API handler
|
||||
func NewAPIHandler(app core.App) *APIHandler {
|
||||
return &APIHandler{app: app}
|
||||
}
|
||||
|
||||
// RegisterRoutes registers maintenance API routes
|
||||
func (h *APIHandler) RegisterRoutes(se *core.ServeEvent) {
|
||||
api := se.Router.Group("/api/beszel/maintenance")
|
||||
api.Bind(apis.RequireAuth())
|
||||
|
||||
api.GET("/", h.listMaintenanceWindows)
|
||||
api.POST("/", h.createMaintenanceWindow)
|
||||
api.GET("/{id}", h.getMaintenanceWindow)
|
||||
api.PATCH("/{id}", h.updateMaintenanceWindow)
|
||||
api.DELETE("/{id}", h.deleteMaintenanceWindow)
|
||||
api.POST("/{id}/cancel", h.cancelMaintenanceWindow)
|
||||
}
|
||||
|
||||
// MaintenanceWindowRequest for create/update
|
||||
type MaintenanceWindowRequest struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
MonitorID string `json:"monitor_id"`
|
||||
DomainID string `json:"domain_id"`
|
||||
StartTime time.Time `json:"start_time"`
|
||||
EndTime time.Time `json:"end_time"`
|
||||
Recurring bool `json:"recurring"`
|
||||
RecurrencePattern string `json:"recurrence_pattern"`
|
||||
SuppressAlerts bool `json:"suppress_alerts"`
|
||||
}
|
||||
|
||||
// listMaintenanceWindows lists all maintenance windows for the authenticated user
|
||||
func (h *APIHandler) listMaintenanceWindows(e *core.RequestEvent) error {
|
||||
authRecord := e.Auth
|
||||
if authRecord == nil {
|
||||
return e.UnauthorizedError("unauthorized", nil)
|
||||
}
|
||||
|
||||
records, err := h.app.FindAllRecords("maintenance_windows",
|
||||
dbx.NewExp("user = {:user}", dbx.Params{"user": authRecord.Id}),
|
||||
)
|
||||
if err != nil {
|
||||
return e.InternalServerError("failed to fetch maintenance windows", err)
|
||||
}
|
||||
|
||||
windows := make([]map[string]interface{}, 0, len(records))
|
||||
for _, record := range records {
|
||||
windows = append(windows, h.recordToResponse(record))
|
||||
}
|
||||
|
||||
return e.JSON(http.StatusOK, windows)
|
||||
}
|
||||
|
||||
// createMaintenanceWindow creates a new maintenance window
|
||||
func (h *APIHandler) createMaintenanceWindow(e *core.RequestEvent) error {
|
||||
authRecord := e.Auth
|
||||
if authRecord == nil {
|
||||
return e.UnauthorizedError("unauthorized", nil)
|
||||
}
|
||||
|
||||
var req MaintenanceWindowRequest
|
||||
if err := json.NewDecoder(e.Request.Body).Decode(&req); err != nil {
|
||||
return e.BadRequestError("invalid request body", err)
|
||||
}
|
||||
|
||||
if req.Name == "" || req.StartTime.IsZero() || req.EndTime.IsZero() {
|
||||
return e.BadRequestError("name, start_time, and end_time are required", nil)
|
||||
}
|
||||
|
||||
// Verify monitor/domain belongs to user if specified
|
||||
if req.MonitorID != "" {
|
||||
monitor, err := h.app.FindRecordById("monitors", req.MonitorID)
|
||||
if err != nil || monitor.GetString("user") != authRecord.Id {
|
||||
return e.BadRequestError("invalid monitor_id", nil)
|
||||
}
|
||||
}
|
||||
if req.DomainID != "" {
|
||||
domain, err := h.app.FindRecordById("domains", req.DomainID)
|
||||
if err != nil || domain.GetString("user") != authRecord.Id {
|
||||
return e.BadRequestError("invalid domain_id", nil)
|
||||
}
|
||||
}
|
||||
|
||||
collection, err := h.app.FindCollectionByNameOrId("maintenance_windows")
|
||||
if err != nil {
|
||||
return e.InternalServerError("failed to find collection", err)
|
||||
}
|
||||
|
||||
record := core.NewRecord(collection)
|
||||
record.Set("name", req.Name)
|
||||
record.Set("description", req.Description)
|
||||
record.Set("monitor", req.MonitorID)
|
||||
record.Set("domain", req.DomainID)
|
||||
record.Set("start_time", req.StartTime)
|
||||
record.Set("end_time", req.EndTime)
|
||||
record.Set("recurring", req.Recurring)
|
||||
record.Set("recurrence_pattern", req.RecurrencePattern)
|
||||
record.Set("suppress_alerts", req.SuppressAlerts)
|
||||
record.Set("status", "scheduled")
|
||||
record.Set("user", authRecord.Id)
|
||||
|
||||
if err := h.app.Save(record); err != nil {
|
||||
return e.InternalServerError("failed to create maintenance window", err)
|
||||
}
|
||||
|
||||
return e.JSON(http.StatusCreated, h.recordToResponse(record))
|
||||
}
|
||||
|
||||
// getMaintenanceWindow gets a single maintenance window
|
||||
func (h *APIHandler) getMaintenanceWindow(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("maintenance_windows", id)
|
||||
if err != nil {
|
||||
return e.NotFoundError("maintenance window not found", err)
|
||||
}
|
||||
|
||||
if record.GetString("user") != authRecord.Id {
|
||||
return e.ForbiddenError("not authorized", nil)
|
||||
}
|
||||
|
||||
return e.JSON(http.StatusOK, h.recordToResponse(record))
|
||||
}
|
||||
|
||||
// updateMaintenanceWindow updates a maintenance window
|
||||
func (h *APIHandler) updateMaintenanceWindow(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("maintenance_windows", id)
|
||||
if err != nil {
|
||||
return e.NotFoundError("maintenance window not found", err)
|
||||
}
|
||||
|
||||
if record.GetString("user") != authRecord.Id {
|
||||
return e.ForbiddenError("not authorized", nil)
|
||||
}
|
||||
|
||||
// Only allow updates if not already completed or cancelled
|
||||
status := record.GetString("status")
|
||||
if status == "completed" || status == "cancelled" {
|
||||
return e.BadRequestError("cannot update completed or cancelled maintenance window", nil)
|
||||
}
|
||||
|
||||
var req MaintenanceWindowRequest
|
||||
if err := json.NewDecoder(e.Request.Body).Decode(&req); err != nil {
|
||||
return e.BadRequestError("invalid request body", err)
|
||||
}
|
||||
|
||||
if req.Name != "" {
|
||||
record.Set("name", req.Name)
|
||||
}
|
||||
if req.Description != "" {
|
||||
record.Set("description", req.Description)
|
||||
}
|
||||
if !req.StartTime.IsZero() {
|
||||
record.Set("start_time", req.StartTime)
|
||||
}
|
||||
if !req.EndTime.IsZero() {
|
||||
record.Set("end_time", req.EndTime)
|
||||
}
|
||||
|
||||
if err := h.app.Save(record); err != nil {
|
||||
return e.InternalServerError("failed to update maintenance window", err)
|
||||
}
|
||||
|
||||
return e.JSON(http.StatusOK, h.recordToResponse(record))
|
||||
}
|
||||
|
||||
// deleteMaintenanceWindow deletes a maintenance window
|
||||
func (h *APIHandler) deleteMaintenanceWindow(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("maintenance_windows", id)
|
||||
if err != nil {
|
||||
return e.NotFoundError("maintenance window 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 maintenance window", err)
|
||||
}
|
||||
|
||||
return e.NoContent(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// cancelMaintenanceWindow cancels a maintenance window
|
||||
func (h *APIHandler) cancelMaintenanceWindow(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("maintenance_windows", id)
|
||||
if err != nil {
|
||||
return e.NotFoundError("maintenance window not found", err)
|
||||
}
|
||||
|
||||
if record.GetString("user") != authRecord.Id {
|
||||
return e.ForbiddenError("not authorized", nil)
|
||||
}
|
||||
|
||||
status := record.GetString("status")
|
||||
if status == "completed" || status == "cancelled" {
|
||||
return e.BadRequestError("cannot cancel completed or already cancelled maintenance window", nil)
|
||||
}
|
||||
|
||||
record.Set("status", "cancelled")
|
||||
if err := h.app.Save(record); err != nil {
|
||||
return e.InternalServerError("failed to cancel maintenance window", err)
|
||||
}
|
||||
|
||||
return e.JSON(http.StatusOK, h.recordToResponse(record))
|
||||
}
|
||||
|
||||
// recordToResponse converts a record to a response map
|
||||
func (h *APIHandler) recordToResponse(record *core.Record) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"id": record.Id,
|
||||
"name": record.GetString("name"),
|
||||
"description": record.GetString("description"),
|
||||
"monitor_id": record.GetString("monitor"),
|
||||
"domain_id": record.GetString("domain"),
|
||||
"start_time": record.GetDateTime("start_time").Time(),
|
||||
"end_time": record.GetDateTime("end_time").Time(),
|
||||
"status": record.GetString("status"),
|
||||
"recurring": record.GetBool("recurring"),
|
||||
"recurrence_pattern": record.GetString("recurrence_pattern"),
|
||||
"suppress_alerts": record.GetBool("suppress_alerts"),
|
||||
"created": record.GetDateTime("created").Time(),
|
||||
"updated": record.GetDateTime("updated").Time(),
|
||||
}
|
||||
}
|
||||
|
||||
// IsInMaintenanceWindow checks if a monitor or domain is currently in a maintenance window
|
||||
func (h *APIHandler) IsInMaintenanceWindow(monitorID, domainID string) bool {
|
||||
now := time.Now()
|
||||
|
||||
exp := dbx.NewExp("status = {:status} AND start_time <= {:now} AND end_time >= {:now}",
|
||||
dbx.Params{"status": "scheduled", "now": now})
|
||||
|
||||
if monitorID != "" {
|
||||
exp = dbx.NewExp("status = {:status} AND start_time <= {:now} AND end_time >= {:now} AND (monitor = {:monitor} OR monitor = '')",
|
||||
dbx.Params{"status": "scheduled", "now": now, "monitor": monitorID})
|
||||
}
|
||||
if domainID != "" {
|
||||
exp = dbx.NewExp("status = {:status} AND start_time <= {:now} AND end_time >= {:now} AND (domain = {:domain} OR domain = '')",
|
||||
dbx.Params{"status": "scheduled", "now": now, "domain": domainID})
|
||||
}
|
||||
|
||||
records, err := h.app.FindAllRecords("maintenance_windows", exp)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, record := range records {
|
||||
if record.GetBool("suppress_alerts") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user