package server import ( "context" "log/slog" "strconv" "strings" ) // This file backs the "At a glance" and "System" regions of the operator // landing surface (operator.html, the branch taken when BodyTemplate is // empty). The landing surface answers one question — "what is this // deployment doing right now?" — in three passes, coarsest first: // // 1. At a glance counts, one per capability, each a link into its section // 2. System which integrations are registered and whether the // delivery queue is draining // 3. Recent activity the unified event timeline (loadRecentActivity, in // operator.go) // // Every number here is a live read. Nothing is cached and nothing is // precomputed at boot: an operator looking at this page is looking at the // database as of this request. // OverviewStat is one tile in the landing surface's "At a glance" row. // // Value is pre-formatted in Go rather than in the template because the // grouping separator is a presentation decision and html/template has no // number formatter; the template's job is layout only. // // Available distinguishes "counted zero" from "could not count". A failed // count must never render as 0 — an operator reading 0 organizations off a // broken query would draw exactly the wrong conclusion — so the template // renders an em dash for unavailable tiles instead. type OverviewStat struct { Label string Value string Caption string // one line stating precisely what was counted Href string // operator section this tile drills into; "" renders a static tile Available bool } // OverviewProvider is one registered integration in the System panel, // projected from the provider registry (core.providers) rather than // hardcoded, so installing a conforming integration surfaces it here with // no template edit. type OverviewProvider struct { Slug string DisplayName string Kind string Status string SurfacePath string // "" when the provider declares no operator surface Healthy bool // status == "active"; drives the registry-status badge color, not a health check // Configured and MissingKeysText carry the SAME configuration-readiness // signal the Integrations list shows (configurationReadiness) — the // required-key resolution the settings page performs. Configured is // vacuously true for a provider with no required configuration. Configured bool MissingKeysText string } // OverviewQueue is the integration outbox's health, split by what an // operator would do about each bucket. Retrying rows are in-flight and need // no action; DeadLetter rows have exhausted their retries and will not move // without intervention, so the template gives that count the alarm styling. type OverviewQueue struct { Pending int64 Retrying int64 DeadLetter int64 Available bool } // NeedsAttention reports whether the delivery queue holds work that will not // resolve on its own. Exposed as a method so the template asks a question // instead of re-deriving the threshold. func (q OverviewQueue) NeedsAttention() bool { return q.DeadLetter > 0 } // OverviewData is everything the landing surface renders above the activity // timeline. type OverviewData struct { Stats []OverviewStat Providers []OverviewProvider Queue OverviewQueue } // loadOverview assembles the landing surface's counts and system signals. // // Like loadRecentActivity, every source degrades independently: a failing // query costs its own tile (rendered unavailable) and is logged, but never // 500s the operator's entry point. The landing page is the surface an // operator reaches for when something is already wrong, so it has to render // under partial failure. func (h *OperatorHandler) loadOverview(ctx context.Context) OverviewData { data := OverviewData{} persons, personsErr := h.IdentityQ.CountActivePersons(ctx) h.logCountErr("active persons", personsErr) joined, joinedErr := h.IdentityQ.CountPersonsJoinedLast30Days(ctx) h.logCountErr("persons joined in 30 days", joinedErr) teams, teamsErr := h.OrgQ.CountTeamOrganizations(ctx) h.logCountErr("team organizations", teamsErr) subCounts, subCountsErr := h.BillingQ.CountLiveSubscriptionsByStatus(ctx) h.logCountErr("live subscriptions", subCountsErr) recurringRows, recurringErr := h.BillingQ.SumMonthlyRecurringByCurrency(ctx) h.logCountErr("monthly recurring", recurringErr) recurring := make([]moneyBucket, 0, len(recurringRows)) for _, r := range recurringRows { recurring = append(recurring, moneyBucket{currency: r.Currency, cents: r.MonthlyCents}) } openInvoices, openInvoicesErr := h.BillingQ.CountOpenInvoices(ctx) h.logCountErr("open invoices", openInvoicesErr) outstandingRows, outstandingErr := h.BillingQ.SumOpenInvoiceBalanceByCurrency(ctx) h.logCountErr("open invoice balance", outstandingErr) outstanding := make([]moneyBucket, 0, len(outstandingRows)) for _, r := range outstandingRows { outstanding = append(outstanding, moneyBucket{currency: r.Currency, cents: r.OutstandingCents}) } grants, grantsErr := h.EntitlementsQ.CountDeliveringOperatorGrants(ctx) h.logCountErr("delivering operator grants", grantsErr) defaults, defaultsErr := h.EntitlementsQ.CountDeliveringDefaultGrants(ctx) h.logCountErr("delivering default grants", defaultsErr) products, productsErr := h.BillingQ.CountPublishedProducts(ctx) h.logCountErr("published products", productsErr) data.Stats = []OverviewStat{ // Links to the People directory (operator-people-directory: "the // landing surface's People metric tile SHALL link to the // directory") so the count it advertises has a one-click answer. newOverviewStat("People", personsErr, persons, peopleCaption(joined, joinedErr), "/operator/persons"), // Personal orgs exist one-per-member by structural convention; a // count including them tracks the People tile and reads as an error. newOverviewStat("Team organizations", teamsErr, teams, "Personal orgs excluded", "/operator/organizations"), // The billing pair reads together: what recurs (commitment), then // what is currently outstanding (receivables). newMoneyStat("Monthly recurring", recurringErr, recurring, recurringCaption(subCounts.ActiveCount, subCounts.TrialingCount, subCountsErr, len(recurring)), "/operator/billing/subscriptions"), newOverviewStat("Open invoices", openInvoicesErr, openInvoices, openInvoicesCaption(outstanding, outstandingErr), "/operator/billing/invoices"), newOverviewStat("Delivering grants", grantsErr, grants, deliveringGrantsCaption(defaults, defaultsErr), "/operator/grants"), newOverviewStat("Catalog products", productsErr, products, "Published; drafts and retired products excluded", "/operator/products"), } data.Providers = h.loadOverviewProviders(ctx) data.Queue = h.loadOverviewQueue(ctx) return data } // deliveringGrantsCaption completes the headline: the tile counts the // grants an operator deliberately issued (the actionable set), and the // caption carries the signup-default mass conferral mints one-per-org — // excluded above because it would drown the number. "Plus" makes the // exclusion arithmetic explicit. The ledger-vs-delivery definition of // "delivering" lives with the queries (CountDeliveringOperatorGrants). func deliveringGrantsCaption(defaults int64, err error) string { if err != nil { return "Signup defaults excluded" } return "Plus " + formatCount(defaults) + " signup " + pluralize(defaults, "default", "defaults") } // peopleCaption prefers velocity over a restatement of what a person record // is; the census is already the headline above it. func peopleCaption(joined int64, err error) string { if err != nil { return "Active person records; find one with the search above" } return formatCount(joined) + " joined in the last 30 days" } // recurringCaption pairs the money headline with the subscriptions behind // it. Trialing subscriptions contribute no money (they are not paying yet) // but stay visible here so the pipeline is never invisible. otherBuckets // counts the currency buckets the headline could not show; conversion is // out of scope (issues.md). func recurringCaption(active, trialing int64, err error, buckets int) string { if err != nil { return "Active subscriptions" } c := formatCount(active) + " active " + pluralize(active, "subscription", "subscriptions") if trialing > 0 { c += "; " + formatCount(trialing) + " trialing" } if buckets > 1 { c += "; plus " + formatCount(int64(buckets-1)) + " more " + pluralize(int64(buckets-1), "currency", "currencies") } return c } // openInvoicesCaption leads with the outstanding balance when it is // available; with no open invoices (or a failed sum) it falls back to // stating the filter. func openInvoicesCaption(outstanding []moneyBucket, err error) string { const filter = "issued and awaiting payment" if err != nil || len(outstanding) == 0 { return "Issued and awaiting payment" } top, others := largestBucket(outstanding) c := formatMoney(top.cents, top.currency) + " outstanding; " + filter if others > 0 { c += "; more in other currencies" } return c } // moneyBucket is one currency's summed amount. Money never crosses // currencies on this surface: the largest bucket is displayed and the rest // are acknowledged, because converting needs a rate source this deployment // does not have (issues.md). type moneyBucket struct { currency string cents int64 } // largestBucket returns the biggest bucket and how many others exist. The // input is ORDER BY-ed descending by both backing queries, but sorting here // too keeps the helper safe to reuse. func largestBucket(buckets []moneyBucket) (moneyBucket, int) { top := buckets[0] for _, b := range buckets[1:] { if b.cents > top.cents { top = b } } return top, len(buckets) - 1 } // newMoneyStat builds the recurring-money tile. An empty bucket list with a // nil error is a real zero (no active subscriptions), rendered as "$0" only // when a currency is unknowable — there is nothing to prefer — so it falls // back to the bare digit. func newMoneyStat(label string, err error, buckets []moneyBucket, caption, href string) OverviewStat { stat := OverviewStat{Label: label, Caption: caption, Href: href, Available: err == nil} if err != nil { return stat } if len(buckets) == 0 { stat.Value = "0" return stat } // formatMoney is the package's shared renderer (member_products.go), so // the operator headline and member-facing prices read identically. top, _ := largestBucket(buckets) stat.Value = formatMoney(top.cents, top.currency) return stat } // newOverviewStat builds a tile, folding the load error into the tile's own // availability so a caller never has to branch on err at every call site. func newOverviewStat(label string, err error, value int64, caption, href string) OverviewStat { stat := OverviewStat{Label: label, Caption: caption, Href: href, Available: err == nil} if err == nil { stat.Value = formatCount(value) } return stat } func (h *OperatorHandler) logCountErr(what string, err error) { if err != nil { h.Logger.Warn("operator overview: count failed", slog.String("metric", what), slog.Any("error", err)) } } // formatCount renders n with thin comma grouping ("12,480"). Written out // rather than pulled from golang.org/x/text because the operator surface is // English-only and a locale-aware formatter would be the only thing that // dependency was used for. func formatCount(n int64) string { digits := strconv.FormatInt(n, 10) sign := "" if strings.HasPrefix(digits, "-") { sign, digits = "-", digits[1:] } if len(digits) <= 3 { return sign + digits } // Walk from the least-significant end, inserting a separator every third // digit, then reverse once at the end. var reversed strings.Builder for i := 0; i < len(digits); i++ { if i > 0 && i%3 == 0 { reversed.WriteByte(',') } reversed.WriteByte(digits[len(digits)-1-i]) } out := []byte(reversed.String()) for i, j := 0, len(out)-1; i < j; i, j = i+1, j-1 { out[i], out[j] = out[j], out[i] } return sign + string(out) } // pluralize picks a noun form for n. Only the two irregular-free cases the // overview needs; not a general inflector. func pluralize(n int64, singular, plural string) string { if n == 1 { return singular } return plural } // loadOverviewProviders lists every registered integration — not just the // provisioning ones the sidebar nav shows — because the System panel reports // what is installed, and a billing or identity provider being down matters // just as much as a provisioning one. func (h *OperatorHandler) loadOverviewProviders(ctx context.Context) []OverviewProvider { if h.IntegrationQ == nil { return nil } providers, err := h.IntegrationQ.ListProviders(ctx) if err != nil { h.Logger.Warn("operator overview: list providers failed", slog.Any("error", err)) return nil } rows := make([]OverviewProvider, 0, len(providers)) for _, p := range providers { surface := "" if p.OperatorSurfacePath.Valid { surface = p.OperatorSurfacePath.String } configured, missing := configurationReadiness(h.IntegrationConfigs, p.Slug) rows = append(rows, OverviewProvider{ Slug: p.Slug, DisplayName: p.DisplayName, Kind: p.ProviderKind, Status: p.Status, SurfacePath: surface, Healthy: p.Status == "active", Configured: configured, MissingKeysText: strings.Join(missing, ", "), }) } return rows } // loadOverviewQueue probes the integration outbox. An unavailable result // leaves Available false and the template omits the panel rather than // printing three zeroes it cannot stand behind. func (h *OperatorHandler) loadOverviewQueue(ctx context.Context) OverviewQueue { if h.IntegrationQ == nil { return OverviewQueue{} } counts, err := h.IntegrationQ.CountOutboxByStatus(ctx) if err != nil { h.Logger.Warn("operator overview: outbox health probe failed", slog.Any("error", err)) return OverviewQueue{} } return OverviewQueue{ Pending: counts.PendingCount, Retrying: counts.FailedCount, DeadLetter: counts.DeadLetterCount, Available: true, } }