feat: cached public status snapshot with licence gate

This commit is contained in:
2026-08-24 14:24:07 +00:00
parent 6263c7e16f
commit 21a2d077d8
+165
View File
@@ -1,10 +1,15 @@
package services
import (
"encoding/json"
"log"
"sort"
"time"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/db"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/models"
"gitea.hostxtra.co.uk/mrhid6/vantage/shared/license"
"go.mongodb.org/mongo-driver/v2/bson"
)
// HistoryDays is the width of the public history bar. It is also the window the
@@ -368,6 +373,166 @@ func componentStatus(mon models.Monitor, known, inMaintenance bool, now time.Tim
}
}
const statusCacheTTL = 30 * time.Second
// PublicStatusSnapshot is the whole public read path.
//
// A missing page, an unpublished page and a page belonging to another instance
// all return ErrPageNotFound, identically. A distinct error for "exists but
// unpublished" would confirm it exists.
func PublicStatusSnapshot(instanceID, pageID string) (*StatusSnapshot, error) {
if err := ValidatePageID(pageID); err != nil {
return nil, ErrPageNotFound
}
if snap := cachedSnapshot(instanceID, pageID); snap != nil {
return snap, nil
}
page, err := GetStatusPage(instanceID, pageID)
if err != nil {
return nil, err
}
if !page.Published {
return nil, ErrPageNotFound
}
// The licence check answers 200 with available:false rather than 403,
// because the reader is a member of the public who can do nothing about it
// and deserves an explanation rather than a browser error.
st := GetLicenseState(instanceID)
if !st.Active() {
return &StatusSnapshot{Available: false, Reason: "licence_inactive", Title: page.Title}, nil
}
if !st.Feature(license.FeatureStatusPages) {
return &StatusSnapshot{Available: false, Reason: "feature_unavailable", Title: page.Title}, nil
}
in, err := loadSnapshotInput(instanceID, *page)
if err != nil {
return nil, err
}
snap := assembleSnapshot(in)
storeSnapshot(instanceID, pageID, snap)
return &snap, nil
}
func loadSnapshotInput(instanceID string, page models.StatusPage) (snapshotInput, error) {
in := snapshotInput{
Page: page,
Monitors: map[string]models.Monitor{},
Rollups: map[string][]models.Rollup{},
Now: time.Now().UTC(),
}
since := in.Now.AddDate(0, 0, -HistoryDays)
ids := []string{}
for _, sec := range page.Sections {
for _, e := range sec.Entries {
ids = append(ids, e.MonitorID)
}
}
if len(ids) == 0 {
// A page with no components still renders: title, banner and any
// authored incidents. Skipping the monitor queries avoids three
// unbounded $in lookups on an empty list.
authored, err := ListStatusIncidentsForPage(instanceID, page.PageID, since)
if err != nil {
return in, err
}
in.Authored = authored
return in, nil
}
ctx, cancel := spCtx()
defer cancel()
cur, err := db.Col("monitors").Find(ctx, bson.M{
"instance_id": instanceID,
"monitor_id": bson.M{"$in": ids},
})
if err != nil {
return in, err
}
mons := []models.Monitor{}
if err := cur.All(ctx, &mons); err != nil {
return in, err
}
for _, m := range mons {
in.Monitors[m.MonitorID] = m
}
rc, err := db.Col("monitor_rollups").Find(ctx, bson.M{
"instance_id": instanceID,
"monitor_id": bson.M{"$in": ids},
"period_start": bson.M{"$gte": since},
})
if err != nil {
return in, err
}
rollups := []models.Rollup{}
if err := rc.All(ctx, &rollups); err != nil {
return in, err
}
for _, r := range rollups {
in.Rollups[r.MonitorID] = append(in.Rollups[r.MonitorID], r)
}
ic, err := db.Col("incidents").Find(ctx, bson.M{
"instance_id": instanceID,
"monitor_id": bson.M{"$in": ids},
"started_at": bson.M{"$gte": since},
})
if err != nil {
return in, err
}
auto := []models.Incident{}
if err := ic.All(ctx, &auto); err != nil {
return in, err
}
in.AutoIncidents = auto
authored, err := ListStatusIncidentsForPage(instanceID, page.PageID, since)
if err != nil {
return in, err
}
in.Authored = authored
return in, nil
}
// A cache miss on Redis is a cache miss, never an error: the status page must
// survive the outage it exists to report.
func cachedSnapshot(instanceID, pageID string) *StatusSnapshot {
if statusRedis == nil {
return nil
}
ctx, cancel := spCtx()
defer cancel()
raw, err := statusRedis.Get(ctx, statusCacheKey(instanceID, pageID)).Bytes()
if err != nil || len(raw) == 0 {
return nil
}
var snap StatusSnapshot
if err := json.Unmarshal(raw, &snap); err != nil {
return nil
}
return &snap
}
func storeSnapshot(instanceID, pageID string, snap StatusSnapshot) {
if statusRedis == nil {
return
}
raw, err := json.Marshal(snap)
if err != nil {
return
}
ctx, cancel := spCtx()
defer cancel()
if err := statusRedis.Set(ctx, statusCacheKey(instanceID, pageID), raw, statusCacheTTL).Err(); err != nil {
log.Printf("status cache store %s/%s: %v", instanceID, pageID, err)
}
}
func overallState(sections []PublicSection) string {
worst := PublicUp
anyDown, anyMaint, anyOther := false, false, false