package httpapi
import (
"html/template"
"net/http"
"net/url"
"strings"
"time"
)
type statusPageData struct {
GeneratedAt string
RefreshSeconds int
Filters statusPageFilters
QuickFilters []statusFilterLink
Overview screenStatusOverview
StatusAPIPath string
StatusPagePath string
}
type statusPageFilters struct {
ServerConnectivity string
Stale string
UpdatedSince string
Limit string
}
type statusFilterLink struct {
Label string
Href string
Class string
Active bool
}
var statusPageTemplate = template.Must(template.New("status-page").Funcs(template.FuncMap{
"connectivityLabel": connectivityLabel,
"screenDetailPath": screenDetailPath,
"statusClass": statusClass,
"timestampLabel": timestampLabel,
}).Parse(`
Screen Status
A compact browser view of the latest screen reports from the current in-memory status overview. Offline and degraded screens stay at the top for quick diagnostics.
{{.Overview.Summary.Total}}
Total known screens
{{.Overview.Summary.Offline}}
Offline
{{.Overview.Summary.Degraded}}
Degraded
{{.Overview.Summary.Online}}
Online
{{.Overview.Summary.Stale}}
Stale reports
Filters and refresh
This page refreshes every {{.RefreshSeconds}} seconds. Use the shortcut links or the form to narrow the existing connectivity and freshness filters without leaving the lightweight server-rendered flow.
JSON overview
Latest reports
Each row links directly to the existing per-screen JSON detail endpoint for a quick drill-down into the raw status payload.
{{if .Overview.Screens}}
| Screen |
Derived state |
Player status |
Server link |
Received |
Heartbeat |
{{range .Overview.Screens}}
|
{{.ScreenID}}
{{if .MQTTBroker}}{{.MQTTBroker}}{{else if .ServerURL}}{{.ServerURL}}{{else}}No endpoint details{{end}}
|
{{.DerivedState}} |
{{.Status}}
{{if .Stale}}Marked stale by server freshness check{{else}}Fresh within expected heartbeat window{{end}}
|
{{connectivityLabel .ServerConnectivity}}
{{if .ServerURL}}{{.ServerURL}}{{end}}
|
{{timestampLabel .ReceivedAt}}
{{if .LastHeartbeatAt}}Heartbeat {{timestampLabel .LastHeartbeatAt}}{{end}}
|
{{if gt .HeartbeatEverySeconds 0}}{{.HeartbeatEverySeconds}}s{{else}}-{{end}}
{{if .StartedAt}}Started {{timestampLabel .StartedAt}}{{end}}
|
{{end}}
{{else}}
No screen has reported status yet. Once a player posts to the existing status API, it will appear here automatically.
{{end}}
`))
func handleStatusPage(store playerStatusStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
overview, err := buildScreenStatusOverview(store, r.URL.Query())
if err != nil {
writeOverviewQueryError(w, err)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
data := buildStatusPageData(store, r.URL.Query(), overview)
if err := statusPageTemplate.Execute(w, data); err != nil {
http.Error(w, "failed to render status page", http.StatusInternalServerError)
}
}
}
func buildStatusPageData(store playerStatusStore, query url.Values, overview screenStatusOverview) statusPageData {
filters := statusPageFilters{
ServerConnectivity: strings.TrimSpace(query.Get("server_connectivity")),
Stale: strings.TrimSpace(query.Get("stale")),
UpdatedSince: strings.TrimSpace(query.Get("updated_since")),
Limit: strings.TrimSpace(query.Get("limit")),
}
return statusPageData{
GeneratedAt: store.Now().Format(time.RFC3339),
RefreshSeconds: 15,
Filters: filters,
QuickFilters: buildStatusQuickFilters(filters),
Overview: overview,
StatusAPIPath: buildOverviewPath("/api/v1/screens/status", filters),
StatusPagePath: "/status",
}
}
func buildStatusQuickFilters(filters statusPageFilters) []statusFilterLink {
return []statusFilterLink{
{
Label: "All screens",
Href: buildStatusPageHref(statusPageFilters{Limit: filters.Limit, UpdatedSince: filters.UpdatedSince}),
Class: "",
Active: filters.ServerConnectivity == "" && filters.Stale == "",
},
{
Label: "Connectivity offline",
Href: buildStatusPageHref(statusPageFilters{ServerConnectivity: "offline", Limit: filters.Limit, UpdatedSince: filters.UpdatedSince}),
Class: "offline",
Active: filters.ServerConnectivity == "offline" && filters.Stale == "",
},
{
Label: "Connectivity degraded",
Href: buildStatusPageHref(statusPageFilters{ServerConnectivity: "degraded", Limit: filters.Limit, UpdatedSince: filters.UpdatedSince}),
Class: "degraded",
Active: filters.ServerConnectivity == "degraded" && filters.Stale == "",
},
{
Label: "Stale reports",
Href: buildStatusPageHref(statusPageFilters{Stale: "true", Limit: filters.Limit, UpdatedSince: filters.UpdatedSince}),
Class: "",
Active: filters.ServerConnectivity == "" && filters.Stale == "true",
},
{
Label: "Fresh reports",
Href: buildStatusPageHref(statusPageFilters{Stale: "false", Limit: filters.Limit, UpdatedSince: filters.UpdatedSince}),
Class: "online",
Active: filters.ServerConnectivity == "" && filters.Stale == "false",
},
}
}
func buildStatusPageHref(filters statusPageFilters) string {
return buildOverviewPath("/status", filters)
}
func buildOverviewPath(basePath string, filters statusPageFilters) string {
query := url.Values{}
if filters.ServerConnectivity != "" {
query.Set("server_connectivity", filters.ServerConnectivity)
}
if filters.Stale != "" {
query.Set("stale", filters.Stale)
}
if filters.UpdatedSince != "" {
query.Set("updated_since", filters.UpdatedSince)
}
if filters.Limit != "" {
query.Set("limit", filters.Limit)
}
encoded := query.Encode()
if encoded == "" {
return basePath
}
return basePath + "?" + encoded
}
func statusClass(value string) string {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return "degraded"
}
return trimmed
}
func connectivityLabel(value string) string {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return "unknown"
}
return trimmed
}
func screenDetailPath(screenID string) string {
return "/api/v1/screens/" + url.PathEscape(strings.TrimSpace(screenID)) + "/status"
}
func timestampLabel(value string) string {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return "-"
}
return trimmed
}