From e8c837cf31cd424b90ba9ed7c98f6861616874be Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Thu, 7 May 2026 16:28:55 -0500 Subject: [PATCH] up --- pkg/models/statuspage.go | 151 ++++++++++++++++++ resources/views/statuspage.html | 179 ++++++++++++++++++++++ rest/controllers/statuspage_controller.go | 80 ++++++++++ rest/routes/publicas.go | 6 +- 4 files changed, 415 insertions(+), 1 deletion(-) create mode 100644 pkg/models/statuspage.go create mode 100644 resources/views/statuspage.html create mode 100644 rest/controllers/statuspage_controller.go diff --git a/pkg/models/statuspage.go b/pkg/models/statuspage.go new file mode 100644 index 0000000..184dbf1 --- /dev/null +++ b/pkg/models/statuspage.go @@ -0,0 +1,151 @@ +package models + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +// ─── Estructuras de la API pública de Atlassian Statuspage ──────────────────── + +type StatuspageStatus struct { + Indicator string `json:"indicator"` // none, minor, major, critical + Description string `json:"description"` // "All Systems Operational", etc. +} + +type StatuspagePage struct { + ID string `json:"id"` + Name string `json:"name"` + URL string `json:"url"` + TimeZone string `json:"time_zone"` + UpdatedAt time.Time `json:"updated_at"` +} + +type StatuspageComponent struct { + ID string `json:"id"` + Name string `json:"name"` + Status string `json:"status"` // operational, degraded_performance, partial_outage, major_outage, under_maintenance + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Position int `json:"position"` + Description string `json:"description"` + Showcase bool `json:"showcase"` + GroupID *string `json:"group_id"` + PageID string `json:"page_id"` + Group bool `json:"group"` + OnlyShowIfDegraded bool `json:"only_show_if_degraded"` +} + +type StatuspageIncidentUpdate struct { + ID string `json:"id"` + Status string `json:"status"` + Body string `json:"body"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type StatuspageIncident struct { + ID string `json:"id"` + Name string `json:"name"` + Status string `json:"status"` // investigating, identified, monitoring, resolved, postmortem + Impact string `json:"impact"` // none, minor, major, critical + PageID string `json:"page_id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + MonitoringAt *time.Time `json:"monitoring_at"` + ResolvedAt *time.Time `json:"resolved_at"` + ShortLink string `json:"shortlink"` + IncidentUpdates []StatuspageIncidentUpdate `json:"incident_updates"` + AffectedComponents []StatuspageComponent `json:"components"` +} + +type StatuspageScheduledMaintenance struct { + ID string `json:"id"` + Name string `json:"name"` + Status string `json:"status"` // scheduled, in_progress, verifying, completed + Impact string `json:"impact"` + PageID string `json:"page_id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + ScheduledFor time.Time `json:"scheduled_for"` + ScheduledUntil time.Time `json:"scheduled_until"` + ShortLink string `json:"shortlink"` + IncidentUpdates []StatuspageIncidentUpdate `json:"incident_updates"` + AffectedComponents []StatuspageComponent `json:"components"` +} + +// StatuspageSummary es la respuesta completa del endpoint /api/v2/summary.json +type StatuspageSummary struct { + Page StatuspagePage `json:"page"` + Status StatuspageStatus `json:"status"` + Components []StatuspageComponent `json:"components"` + Incidents []StatuspageIncident `json:"incidents"` + ScheduledMaintenances []StatuspageScheduledMaintenance `json:"scheduled_maintenances"` +} + +// FetchStatuspageSummary consulta la API pública de Atlassian Statuspage. +// pageID es el identificador de tu página (ej. "yh6f0r4529hb"). +func FetchStatuspageSummary(pageID string) (*StatuspageSummary, error) { + if pageID == "" { + return nil, fmt.Errorf("statuspage: pageID vacío") + } + url := fmt.Sprintf("https://%s.statuspage.io/api/v2/summary.json", pageID) + + client := &http.Client{Timeout: 8 * time.Second} + resp, err := client.Get(url) // #nosec G107 — URL construida desde config, no desde input de usuario + if err != nil { + return nil, fmt.Errorf("statuspage: error de red: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("statuspage: respuesta HTTP %d", resp.StatusCode) + } + + body, err := io.ReadAll(io.LimitReader(resp.Body, 512*1024)) // límite 512 KB + if err != nil { + return nil, fmt.Errorf("statuspage: error leyendo body: %w", err) + } + + var summary StatuspageSummary + if err := json.Unmarshal(body, &summary); err != nil { + return nil, fmt.Errorf("statuspage: error parseando JSON: %w", err) + } + return &summary, nil +} + +// ComponentStatusLabel devuelve una etiqueta legible en español. +func ComponentStatusLabel(status string) string { + switch status { + case "operational": + return "Operacional" + case "degraded_performance": + return "Rendimiento degradado" + case "partial_outage": + return "Interrupción parcial" + case "major_outage": + return "Interrupción mayor" + case "under_maintenance": + return "En mantenimiento" + default: + return status + } +} + +// IndicatorColor devuelve la clase de color Tailwind para el indicador global. +func IndicatorColor(indicator string) string { + switch indicator { + case "none": + return "green" + case "minor": + return "yellow" + case "major": + return "orange" + case "critical": + return "red" + default: + return "gray" + } +} diff --git a/resources/views/statuspage.html b/resources/views/statuspage.html new file mode 100644 index 0000000..70171b0 --- /dev/null +++ b/resources/views/statuspage.html @@ -0,0 +1,179 @@ +{{/* Página pública de estado del sistema — Atlassian Statuspage */}} +
+ + +
+

+ {{if .summary}}{{.summary.Page.Name}}{{else}}Estado del sistema{{end}} +

+

Estado en tiempo real de nuestros servicios

+
+ + {{if .error}} + +
+
+ + + +

{{.error}}

+
+
+ {{end}} + + {{if .summary}} + + + {{$color := .indicatorColor}} +
+ {{if eq $color "green"}} +
+ + + +
+

{{.summary.Status.Description}}

+

Todos los sistemas funcionan con normalidad

+
+
+ {{else if eq $color "yellow"}} +
+ + + +
+

{{.summary.Status.Description}}

+

Estamos investigando el problema

+
+
+ {{else if eq $color "orange"}} +
+ + + +
+

{{.summary.Status.Description}}

+

Algunos servicios presentan interrupciones

+
+
+ {{else}} +
+ + + +
+

{{.summary.Status.Description}}

+

Se está trabajando para restablecer los servicios

+
+
+ {{end}} +
+ + + {{if .hasIncidents}} +
+

Incidencias activas

+
+ {{range .summary.Incidents}} +
+
+
+

{{.Name}}

+ {{if .IncidentUpdates}} +

{{(index .IncidentUpdates 0).Body}}

+ {{end}} +
+ + {{if eq .Status "investigating"}}Investigando + {{else if eq .Status "identified"}}Identificado + {{else if eq .Status "monitoring"}}Monitoreando + {{else if eq .Status "resolved"}}Resuelto + {{else}}{{.Status}}{{end}} + +
+
+ {{end}} +
+
+ {{end}} + + + {{if .hasMaintenance}} +
+

Mantenimientos programados

+
+ {{range .summary.ScheduledMaintenances}} +
+
+

{{.Name}}

+

+ Programado: {{.ScheduledFor.Format "02/01/2006 15:04"}} — {{.ScheduledUntil.Format "15:04"}} UTC +

+
+ + {{if eq .Status "scheduled"}}Programado + {{else if eq .Status "in_progress"}}En progreso + {{else if eq .Status "verifying"}}Verificando + {{else if eq .Status "completed"}}Completado + {{else}}{{.Status}}{{end}} + +
+ {{end}} +
+
+ {{end}} + + +
+

Componentes

+
+ {{range .components}} + {{if .Showcase}} +
+
+

{{.Name}}

+ {{if .Description}}

{{.Description}}

{{end}} +
+
+ + + + {{.StatusLabel}} + +
+
+ {{end}} + {{end}} +
+
+ + +
+

+ Actualizado: {{.summary.Page.UpdatedAt.Format "02/01/2006 15:04"}} UTC +  ·  + + Ver en Statuspage + +

+
+ + {{end}}{{/* end if .summary */}} + +
diff --git a/rest/controllers/statuspage_controller.go b/rest/controllers/statuspage_controller.go new file mode 100644 index 0000000..2c8f849 --- /dev/null +++ b/rest/controllers/statuspage_controller.go @@ -0,0 +1,80 @@ +package controllers + +import ( + "os" + + "github.com/gofiber/fiber/v2" + "github.com/sujit-baniya/fiber-boilerplate/pkg/models" +) + +// StatusPage sirve la página pública /status que consulta la API de Atlassian Statuspage. +// Configura la variable de entorno STATUSPAGE_PAGE_ID con el ID de tu página +// (ej. "yh6f0r4529hb", visible en https://.statuspage.io). +func StatusPage(c *fiber.Ctx) error { + pageID := os.Getenv("STATUSPAGE_PAGE_ID") + + data := fiber.Map{ + "title": "Estado del sistema | ", + "pageID": pageID, + } + + if pageID == "" { + data["error"] = "STATUSPAGE_PAGE_ID no configurado" + return c.Render("statuspage", data, "layouts/landing") + } + + summary, err := models.FetchStatuspageSummary(pageID) + if err != nil { + data["error"] = "No se pudo obtener el estado en este momento. Intenta de nuevo más tarde." + return c.Render("statuspage", data, "layouts/landing") + } + + // Separar componentes de grupos y componentes normales + var groups []models.StatuspageComponent + var components []models.StatuspageComponent + for _, comp := range summary.Components { + if comp.Group { + groups = append(groups, comp) + } else { + components = append(components, comp) + } + } + + // Helpers de presentación + type CompVM struct { + models.StatuspageComponent + StatusLabel string + StatusColor string + } + toVM := func(comps []models.StatuspageComponent) []CompVM { + out := make([]CompVM, 0, len(comps)) + for _, c := range comps { + color := "green" + switch c.Status { + case "degraded_performance": + color = "yellow" + case "partial_outage": + color = "orange" + case "major_outage": + color = "red" + case "under_maintenance": + color = "blue" + } + out = append(out, CompVM{ + StatuspageComponent: c, + StatusLabel: models.ComponentStatusLabel(c.Status), + StatusColor: color, + }) + } + return out + } + + data["summary"] = summary + data["components"] = toVM(components) + data["groups"] = toVM(groups) + data["indicatorColor"] = models.IndicatorColor(summary.Status.Indicator) + data["hasIncidents"] = len(summary.Incidents) > 0 + data["hasMaintenance"] = len(summary.ScheduledMaintenances) > 0 + + return c.Render("statuspage", data, "layouts/landing") +} diff --git a/rest/routes/publicas.go b/rest/routes/publicas.go index 6afbe2b..6256fbb 100755 --- a/rest/routes/publicas.go +++ b/rest/routes/publicas.go @@ -27,5 +27,9 @@ func RutasPublicas(web fiber.Router) { web.Get("/pago/estado", apiControllers.PagoEstadoAPI) // ─── Documentación pública ──────────────────────────────────────────────── web.Get("/docs/:saas", controllers.DocsPublicoIndex) - web.Get("/docs/:saas/:slug", controllers.DocsPublicaPagina)} + web.Get("/docs/:saas/:slug", controllers.DocsPublicaPagina) + + // ─── Página de estado del sistema (Atlassian Statuspage) ──────────────── + web.Get("/status", controllers.StatusPage) +}