feat: status page schema, licence feature and indexes

This commit is contained in:
2026-08-24 14:00:00 +00:00
parent 383b763a66
commit 1c15961309
6 changed files with 190 additions and 0 deletions
+4
View File
@@ -174,6 +174,10 @@ func runSchemaSetup() {
log.Printf("warning: failed to ensure workload indexes: %v", err)
}
if err := services.EnsureStatusPageIndexes(); err != nil {
log.Printf("status page indexes: %v", err)
}
if err := services.EnsureAuditIndexes(); err != nil {
log.Printf("warning: failed to ensure audit indexes: %v", err)
}
+110
View File
@@ -0,0 +1,110 @@
package models
import (
"time"
"go.mongodb.org/mongo-driver/v2/bson"
)
// A status page entry kind. Incidents and maintenance share one document
// because they share a timeline, an impact and a set of affected components.
const (
StatusKindIncident = "incident"
StatusKindMaintenance = "maintenance"
)
const (
ImpactNone = "none"
ImpactMinor = "minor"
ImpactMajor = "major"
ImpactCritical = "critical"
)
// Incident statuses.
const (
IncidentInvestigating = "investigating"
IncidentIdentified = "identified"
IncidentMonitoring = "monitoring"
IncidentResolved = "resolved"
)
// Maintenance statuses.
const (
MaintenanceScheduled = "scheduled"
MaintenanceInProgress = "in_progress"
MaintenanceCompleted = "completed"
)
// StatusPageEntry names one monitor on one page.
//
// DisplayName overrides the monitor's own name for this page only. A monitor's
// internal name is frequently not a name anybody wants published, and the same
// monitor may need different words on a customer page and a partner page.
type StatusPageEntry struct {
MonitorID string `bson:"monitor_id" json:"monitor_id"`
DisplayName string `bson:"display_name,omitempty" json:"display_name,omitempty"`
}
// StatusPageSection is page-local and unrelated to Monitor.Group, which labels
// rows on the authenticated monitors list.
type StatusPageSection struct {
Name string `bson:"name" json:"name"`
Entries []StatusPageEntry `bson:"entries" json:"entries"`
}
// StatusPageBanner is three fields on the page rather than a collection,
// because it is one string with no lifecycle.
type StatusPageBanner struct {
Enabled bool `bson:"enabled" json:"enabled"`
Level string `bson:"level,omitempty" json:"level,omitempty"`
Text string `bson:"text,omitempty" json:"text,omitempty"`
}
// StatusPage is read whole, always, which is why its structure is embedded
// rather than joined: one page is one read is one cache fill.
type StatusPage struct {
ID bson.ObjectID `bson:"_id,omitempty" json:"_id,omitempty"`
InstanceID string `bson:"instance_id" json:"instance_id"`
PageID string `bson:"page_id" json:"page_id"`
Title string `bson:"title" json:"title"`
Description string `bson:"description,omitempty" json:"description,omitempty"`
LogoURL string `bson:"logo_url,omitempty" json:"logo_url,omitempty"`
Published bool `bson:"published" json:"published"`
Banner StatusPageBanner `bson:"banner" json:"banner"`
Sections []StatusPageSection `bson:"sections" json:"sections"`
CreatedAt time.Time `bson:"created_at" json:"created_at"`
UpdatedAt time.Time `bson:"updated_at" json:"updated_at"`
}
type StatusIncidentUpdate struct {
At time.Time `bson:"at" json:"at"`
Status string `bson:"status" json:"status"`
Body string `bson:"body" json:"body"`
Author string `bson:"author" json:"author"`
}
// StatusIncident is operator-authored. Monitor-detected outages stay in the
// incidents collection and are derived at assembly time; copying them here
// would be a second writer for the same fact.
//
// PageIDs is explicit rather than derived from AffectedMonitors: deriving it
// would mean adding a monitor to a page retroactively republishes old
// incidents to a new audience.
type StatusIncident struct {
ID bson.ObjectID `bson:"_id,omitempty" json:"_id,omitempty"`
InstanceID string `bson:"instance_id" json:"instance_id"`
IncidentID string `bson:"incident_id" json:"incident_id"`
PageIDs []string `bson:"page_ids" json:"page_ids"`
Kind string `bson:"kind" json:"kind"`
Title string `bson:"title" json:"title"`
Impact string `bson:"impact" json:"impact"`
AffectedMonitors []string `bson:"affected_monitors,omitempty" json:"affected_monitors,omitempty"`
Status string `bson:"status" json:"status"`
ScheduledStart *time.Time `bson:"scheduled_start,omitempty" json:"scheduled_start,omitempty"`
ScheduledEnd *time.Time `bson:"scheduled_end,omitempty" json:"scheduled_end,omitempty"`
Updates []StatusIncidentUpdate `bson:"updates" json:"updates"`
StartedAt time.Time `bson:"started_at" json:"started_at"`
ResolvedAt *time.Time `bson:"resolved_at,omitempty" json:"resolved_at,omitempty"`
CreatedAt time.Time `bson:"created_at" json:"created_at"`
UpdatedAt time.Time `bson:"updated_at" json:"updated_at"`
}
@@ -46,6 +46,8 @@ var ScopedCollections = []string{
"vuln_alert_rules",
"api_tokens",
"server_workloads",
"status_pages",
"status_incidents",
}
// collectionRenames maps the two collections whose names change. Ordered so the
+47
View File
@@ -0,0 +1,47 @@
package services
import (
"context"
"time"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/db"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
"go.mongodb.org/mongo-driver/v2/mongo/options"
)
func spCtx() (context.Context, context.CancelFunc) {
return context.WithTimeout(context.Background(), 5*time.Second)
}
// EnsureStatusPageIndexes follows EnsureWorkflowIndexes rather than
// EnsureAuthIndexes: the unique page_id index is a correctness property, but a
// missing secondary index on a small collection degrades to a scan, which is no
// reason to refuse to serve the fleet. main.go warns rather than exiting.
func EnsureStatusPageIndexes() error {
ctx, cancel := spCtx()
defer cancel()
if _, err := db.Col("status_pages").Indexes().CreateOne(ctx, mongo.IndexModel{
Keys: bson.D{{Key: "instance_id", Value: 1}, {Key: "page_id", Value: 1}},
Options: options.Index().SetUnique(true),
}); err != nil {
return err
}
if _, err := db.Col("status_incidents").Indexes().CreateOne(ctx, mongo.IndexModel{
Keys: bson.D{{Key: "instance_id", Value: 1}, {Key: "incident_id", Value: 1}},
Options: options.Index().SetUnique(true),
}); err != nil {
return err
}
// The public read filters by page and orders by recency, and it is the
// only query on this collection that runs on every visit.
if _, err := db.Col("status_incidents").Indexes().CreateOne(ctx, mongo.IndexModel{
Keys: bson.D{{Key: "instance_id", Value: 1}, {Key: "page_ids", Value: 1}, {Key: "started_at", Value: -1}},
}); err != nil {
return err
}
return nil
}
@@ -0,0 +1,22 @@
package services
import "testing"
// A new instance-scoped collection that is not in ScopedCollections leaves its
// rows behind when the instance is deleted. This is the cheapest possible
// guard against the omission.
func TestStatusCollectionsAreScoped(t *testing.T) {
want := []string{"status_pages", "status_incidents"}
for _, name := range want {
found := false
for _, got := range ScopedCollections {
if got == name {
found = true
break
}
}
if !found {
t.Errorf("ScopedCollections is missing %q", name)
}
}
}