up
This commit is contained in:
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
{{/* Página pública de estado del sistema — Atlassian Statuspage */}}
|
||||||
|
<div class="min-h-screen bg-slate-50 py-10 px-4" style="width:100vw; max-width:100%;">
|
||||||
|
|
||||||
|
<!-- ── Header ─────────────────────────────────────────────────────────── -->
|
||||||
|
<div class="max-w-3xl mx-auto mb-8 text-center">
|
||||||
|
<h1 class="text-3xl font-bold text-slate-800 mb-1">
|
||||||
|
{{if .summary}}{{.summary.Page.Name}}{{else}}Estado del sistema{{end}}
|
||||||
|
</h1>
|
||||||
|
<p class="text-slate-500 text-sm">Estado en tiempo real de nuestros servicios</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{if .error}}
|
||||||
|
<!-- ── Error ──────────────────────────────────────────────────────────── -->
|
||||||
|
<div class="max-w-3xl mx-auto mb-6">
|
||||||
|
<div class="bg-red-50 border border-red-200 rounded-xl p-5 flex items-start gap-3">
|
||||||
|
<svg class="w-5 h-5 text-red-500 mt-0.5 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"/>
|
||||||
|
</svg>
|
||||||
|
<p class="text-red-700 text-sm">{{.error}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .summary}}
|
||||||
|
|
||||||
|
<!-- ── Banner de estado global ────────────────────────────────────────── -->
|
||||||
|
{{$color := .indicatorColor}}
|
||||||
|
<div class="max-w-3xl mx-auto mb-8">
|
||||||
|
{{if eq $color "green"}}
|
||||||
|
<div class="bg-green-500 text-white rounded-2xl px-6 py-5 flex items-center gap-4 shadow-sm">
|
||||||
|
<svg class="w-8 h-8 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5"/>
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<p class="font-bold text-lg leading-tight">{{.summary.Status.Description}}</p>
|
||||||
|
<p class="text-green-100 text-sm">Todos los sistemas funcionan con normalidad</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{else if eq $color "yellow"}}
|
||||||
|
<div class="bg-yellow-400 text-yellow-900 rounded-2xl px-6 py-5 flex items-center gap-4 shadow-sm">
|
||||||
|
<svg class="w-8 h-8 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"/>
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<p class="font-bold text-lg leading-tight">{{.summary.Status.Description}}</p>
|
||||||
|
<p class="text-yellow-800 text-sm">Estamos investigando el problema</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{else if eq $color "orange"}}
|
||||||
|
<div class="bg-orange-500 text-white rounded-2xl px-6 py-5 flex items-center gap-4 shadow-sm">
|
||||||
|
<svg class="w-8 h-8 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"/>
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<p class="font-bold text-lg leading-tight">{{.summary.Status.Description}}</p>
|
||||||
|
<p class="text-orange-100 text-sm">Algunos servicios presentan interrupciones</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="bg-red-600 text-white rounded-2xl px-6 py-5 flex items-center gap-4 shadow-sm">
|
||||||
|
<svg class="w-8 h-8 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/>
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<p class="font-bold text-lg leading-tight">{{.summary.Status.Description}}</p>
|
||||||
|
<p class="text-red-200 text-sm">Se está trabajando para restablecer los servicios</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Incidencias activas ─────────────────────────────────────────────── -->
|
||||||
|
{{if .hasIncidents}}
|
||||||
|
<div class="max-w-3xl mx-auto mb-8">
|
||||||
|
<h2 class="text-base font-semibold text-slate-700 mb-3 uppercase tracking-wide">Incidencias activas</h2>
|
||||||
|
<div class="bg-white rounded-xl border border-slate-200 shadow-sm divide-y divide-slate-100">
|
||||||
|
{{range .summary.Incidents}}
|
||||||
|
<div class="px-5 py-4">
|
||||||
|
<div class="flex items-start justify-between gap-3">
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<p class="font-semibold text-slate-800 text-sm">{{.Name}}</p>
|
||||||
|
{{if .IncidentUpdates}}
|
||||||
|
<p class="text-slate-500 text-xs mt-1 line-clamp-2">{{(index .IncidentUpdates 0).Body}}</p>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<span class="shrink-0 text-xs font-medium px-2.5 py-1 rounded-full
|
||||||
|
{{if eq .Impact "critical"}}bg-red-100 text-red-700
|
||||||
|
{{else if eq .Impact "major"}}bg-orange-100 text-orange-700
|
||||||
|
{{else if eq .Impact "minor"}}bg-yellow-100 text-yellow-700
|
||||||
|
{{else}}bg-slate-100 text-slate-600{{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}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<!-- ── Mantenimientos programados ─────────────────────────────────────── -->
|
||||||
|
{{if .hasMaintenance}}
|
||||||
|
<div class="max-w-3xl mx-auto mb-8">
|
||||||
|
<h2 class="text-base font-semibold text-slate-700 mb-3 uppercase tracking-wide">Mantenimientos programados</h2>
|
||||||
|
<div class="bg-white rounded-xl border border-slate-200 shadow-sm divide-y divide-slate-100">
|
||||||
|
{{range .summary.ScheduledMaintenances}}
|
||||||
|
<div class="px-5 py-4 flex items-start justify-between gap-3">
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<p class="font-semibold text-slate-800 text-sm">{{.Name}}</p>
|
||||||
|
<p class="text-slate-400 text-xs mt-1">
|
||||||
|
Programado: {{.ScheduledFor.Format "02/01/2006 15:04"}} — {{.ScheduledUntil.Format "15:04"}} UTC
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span class="shrink-0 text-xs font-medium px-2.5 py-1 rounded-full bg-blue-100 text-blue-700">
|
||||||
|
{{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}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<!-- ── Componentes ────────────────────────────────────────────────────── -->
|
||||||
|
<div class="max-w-3xl mx-auto mb-6">
|
||||||
|
<h2 class="text-base font-semibold text-slate-700 mb-3 uppercase tracking-wide">Componentes</h2>
|
||||||
|
<div class="bg-white rounded-xl border border-slate-200 shadow-sm divide-y divide-slate-100">
|
||||||
|
{{range .components}}
|
||||||
|
{{if .Showcase}}
|
||||||
|
<div class="px-5 py-3.5 flex items-center justify-between gap-3">
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<p class="text-sm font-medium text-slate-700 truncate">{{.Name}}</p>
|
||||||
|
{{if .Description}}<p class="text-xs text-slate-400 truncate">{{.Description}}</p>{{end}}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
|
<span class="w-2 h-2 rounded-full
|
||||||
|
{{if eq .StatusColor "green"}}bg-green-500
|
||||||
|
{{else if eq .StatusColor "yellow"}}bg-yellow-400
|
||||||
|
{{else if eq .StatusColor "orange"}}bg-orange-500
|
||||||
|
{{else if eq .StatusColor "red"}}bg-red-600
|
||||||
|
{{else if eq .StatusColor "blue"}}bg-blue-500
|
||||||
|
{{else}}bg-slate-400{{end}}">
|
||||||
|
</span>
|
||||||
|
<span class="text-xs font-medium
|
||||||
|
{{if eq .StatusColor "green"}}text-green-700
|
||||||
|
{{else if eq .StatusColor "yellow"}}text-yellow-700
|
||||||
|
{{else if eq .StatusColor "orange"}}text-orange-700
|
||||||
|
{{else if eq .StatusColor "red"}}text-red-700
|
||||||
|
{{else if eq .StatusColor "blue"}}text-blue-700
|
||||||
|
{{else}}text-slate-500{{end}}">
|
||||||
|
{{.StatusLabel}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Footer ─────────────────────────────────────────────────────────── -->
|
||||||
|
<div class="max-w-3xl mx-auto text-center">
|
||||||
|
<p class="text-slate-400 text-xs">
|
||||||
|
Actualizado: {{.summary.Page.UpdatedAt.Format "02/01/2006 15:04"}} UTC
|
||||||
|
·
|
||||||
|
<a href="https://{{.pageID}}.statuspage.io" target="_blank" rel="noopener noreferrer" class="underline hover:text-slate-600">
|
||||||
|
Ver en Statuspage
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{end}}{{/* end if .summary */}}
|
||||||
|
|
||||||
|
</div>
|
||||||
@@ -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://<id>.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")
|
||||||
|
}
|
||||||
@@ -27,5 +27,9 @@ func RutasPublicas(web fiber.Router) {
|
|||||||
web.Get("/pago/estado", apiControllers.PagoEstadoAPI)
|
web.Get("/pago/estado", apiControllers.PagoEstadoAPI)
|
||||||
// ─── Documentación pública ────────────────────────────────────────────────
|
// ─── Documentación pública ────────────────────────────────────────────────
|
||||||
web.Get("/docs/:saas", controllers.DocsPublicoIndex)
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user