// The app Overview dashboard's numbers, and the Instances tab's one table. // Both are joins over reads the app pages already do — AppView (routes + // channels), InstanceMetrics (the shared metrics sweep) and VersionView — // assembled here in Go so the templates render facts, never compute them. package console import ( "strings" "github.com/carlosframework/platform/internal/registry" ) // OverviewStats is the Overview dashboard: the handful of numbers an owner // checks before anything else. Every field is derived from platform state the // page was already reading — this struct adds no bucket or registry I/O. type OverviewStats struct { // Deployment is what this account's routes report serving. Channel // pointers are desired targets, not proof that a route adopted them. Deployment DeploymentStatus // The app's instances (routes owned by the viewed account), and how many // of them the last metrics sweep actually saw. The TOTAL comes from the // routes, never the sweep: a failed sweep costs the readings, and must // not make the dashboard claim a live fleet has no hosts. Asleep counts // hibernating non-reporters — expected quiet, not missing. InstanceTotal int InstanceReporting int InstanceAsleep int // InstanceNoReadings counts instance-kind routes nobody could sample: not // provisioned, no carlos-app@ backing, no stamped unit. They are honest // unknowns — the tile names them separately and the verdict must not // treat them as down (the 2026-08-10 tito false alarm). InstanceNoReadings int // InstanceCounted is the Running tile's denominator: InstanceExpected // minus the unknowable. Templates render facts, never compute them. InstanceCounted int // SidecarTotal is how many of this account's routes are standalone // workers (Kind "sidecar") — counted apart from InstanceTotal on // purpose. A sidecar never serves a request and is never static either // (registry.Route.IsSidecar's doc), so folding it into InstanceTotal // would make an app with only sidecars read as "static — nothing to // run" once InstanceExpected (instance-kind only) stays 0, and would // make a mixed app's tile count a worker as if it were a copy of the // app process. It has no health rollup here, mirroring InstanceTotal's // role before InstanceExpected existed — the Instances tab's per-row // healthWord is where a sidecar's own state (Running/Asleep/Waking/ // Crashed/Never started) is read. SidecarTotal int // MetricsUnavailable: the sweep itself failed (nil metrics), which is // "readings unavailable", not "zero instances reporting". MetricsUnavailable bool // InstanceExpected is how many of this account's routes are lifecycle- // managed processes (kind "instance") — the only kind the metrics sweep // can ever hear from. Static and service routes are served without a // tenant process, so they are never "silent": expecting a reading from // them would flag every static site as unhealthy forever. InstanceExpected int // Verdict is the dashboard's one-word answer to "is this app healthy", // computed here so templates render it as a fact. Verdict is the CSS // suffix ("ok", "attention", "notdeployed", "unknown"); VerdictLabel the // words. The rule errs toward "can't tell" over a guessed "all good" — // same honesty contract as the em-dash. Verdict string VerdictLabel string // Current resource use summed across every reporting instance. Nil when // nothing reports — the tiles render the em-dash, never zeros. CPUSum *float64 MemSum *int64 // DBSizeSum is this app's total on-disk SQLite footprint (each route's // file plus its -wal, status.BoxMetrics.RouteDBBytes via dbJoin), summed // across every one of the account's own routes with a sized database — // not gated on the metrics sweep reporting, since a hibernating // instance's database is still on disk and still worth showing. Nil // when no route could be sized. DBSizeSum *int64 // Domains, and how many need someone's attention: a custom domain not // pointing here yet, or a cert that is not ready. DomainTotal int DomainAttention int // Shipped releases. The versions list is capped (versionsCap), so // ReleasesCapped says "20+" rather than lying with "20". ReleaseCount int ReleasesCapped bool LatestVersion string } // assembleOverview derives the dashboard from what handleApp already loaded. // metrics may be nil (a failed sweep costs the readings and says so, never // the page or the instance count); versions may be nil the same way. // // The tenant boundary is drawn from the ROUTES, on every number here: routes // are grouped by app NAME across accounts (Assemble's byApp), so both the // instance count and the domains count must skip rows stamped with another // account — a same-named app elsewhere must not inflate this dashboard or // wave its failed cert on it. func assembleOverview(view *AppView, acct string, metrics []InstanceMetrics, versions []VersionView, dbJoin boxJoin) OverviewStats { st := OverviewStats{MetricsUnavailable: metrics == nil} st.Deployment = observedDeployment(view.Routes, acct) reporting := make(map[string]bool, len(metrics)) for _, m := range metrics { if !m.Reporting() { continue } reporting[m.Host] = true if m.CPUFrac != nil { if st.CPUSum == nil { st.CPUSum = new(float64) } *st.CPUSum += *m.CPUFrac } if m.MemBytes != nil { if st.MemSum == nil { st.MemSum = new(int64) } *st.MemSum += *m.MemBytes } } for _, rt := range view.Routes { if rt.Account != acct || rt.Platform { continue } if n := dbJoin.dbSize(rt.Host); n != nil { if st.DBSizeSum == nil { st.DBSizeSum = new(int64) } *st.DBSizeSum += *n } if rt.Kind == "sidecar" { st.SidecarTotal++ continue } st.InstanceTotal++ if rt.Kind == "instance" { st.InstanceExpected++ if reporting[rt.Host] { st.InstanceReporting++ } else if rt.Hibernate { st.InstanceAsleep++ } else if !expectsReadings(rt.Provisioned, rt.Unit, rt.Backing) { st.InstanceNoReadings++ } } } st.InstanceCounted = st.InstanceExpected - st.InstanceNoReadings // Routes and Domains are parallel slices — Assemble appends one of each // per registry route, in the same loop — which is what lets the domain // rows be account-filtered here at all (DomainView carries no account of // its own). Guarded rather than assumed, and the guard fails toward // counting nothing: a miscount that says 0 invites a look, one that // includes another tenant's failure does not. if len(view.Domains) == len(view.Routes) { for i, rt := range view.Routes { if rt.Account != acct || rt.Platform { continue } st.DomainTotal++ if domainNeedsAttention(view.Domains[i]) { st.DomainAttention++ } } } st.ReleaseCount = len(versions) st.ReleasesCapped = len(versions) == versionsCap if len(versions) > 0 { st.LatestVersion = versions[0].Version } st.Verdict, st.VerdictLabel = healthVerdict(st) return st } // healthVerdict folds the dashboard's numbers into one word. Order matters: // no owned routes means not deployed; a route with no observed version is // unknown, never "all good"; and a failed sweep with real instances is // "can't tell" too. Channel pointer failures do not enter this verdict: // they obscure a target, not what the routes already report serving. func healthVerdict(st OverviewStats) (string, string) { switch { case !st.Deployment.HasRoutes(): return "notdeployed", "Not deployed yet" case st.DomainAttention > 0: return "attention", "Needs attention" case st.InstanceReporting+st.InstanceAsleep < st.InstanceCounted: return "attention", "Needs attention" case st.Deployment.ReportableCount == 0: return "ok", "All good" case !st.Deployment.HasReportedVersion(): return "unknown", "Can't tell right now" case st.Deployment.UnknownCount > 0: return "unknown", "Can't tell right now" case st.MetricsUnavailable && st.InstanceExpected > 0: return "unknown", "Can't tell right now" default: return "ok", "All good" } } // domainNeedsAttention is the dashboard's one-bit read of a Domains row: // guidance text means DNS isn't pointing here, and a TLS status other than // ready/issuing means issuance failed. Issuing is patience, not attention. func domainNeedsAttention(d DomainView) bool { if d.Guidance != "" { return true } switch { case d.TLS == "", strings.HasPrefix(d.TLS, "ready"), strings.HasPrefix(d.TLS, "issuing"): return false } return true } // InstanceRow is one line of the Instances tab's table: a route joined with // its readings from the shared sweep. The join key is the host — the one name // owners have for an instance. type InstanceRow struct { Host string Channel string Version string // what the route reports it is running, "" unknown Kind string Status string // registry status; "" means ready Hibernate bool Metrics InstanceMetrics // zero-valued (Reporting false) when unswept // HealthWord is the Status enum + hibernate + reporting folded into one // plain word — the column's whole job. HealthWord string Restartable bool } // assembleInstanceRows joins the app's routes with the sweep's readings, in // host order (the routes' own order). Only routes stamped with the viewed // account join — the same tenant boundary appResourceMetrics draws, kept // byte-for-byte: rows here and metrics there must cover the same hosts. func assembleInstanceRows(view *AppView, acct *Account, metrics []InstanceMetrics) []InstanceRow { byHost := make(map[string]InstanceMetrics, len(metrics)) for _, m := range metrics { byHost[m.Host] = m } rows := make([]InstanceRow, 0, len(view.Routes)) for _, rt := range view.Routes { if rt.Account != acct.Sqid || rt.Platform { continue } status := rt.Status if status == "" { status = "ready" // registry contract: empty status means ready } rows = append(rows, InstanceRow{ Host: rt.Host, Channel: rt.Channel, Version: rt.Version, Kind: rt.Kind, Status: status, Hibernate: rt.Hibernate, Metrics: byHost[rt.Host], Restartable: rt.Restartable, HealthWord: healthWord(rt.Kind, status, rt.Hibernate, byHost[rt.Host].Reporting(), expectsReadings(rt.Provisioned, rt.Unit, rt.Backing)), }) } return rows } // expectsReadings is the one rule for whether silence from a route's process // is meaningful: the platform runs the process (a provisioned instance or a // carlos-app@ tenant) or a stamped unit tells the sweep where to look. // Everything else — a manually-added route whose process some bespoke, // unstamped unit owns — is honest ignorance, not an outage, and must not // render red (the 2026-08-10 tito false alarm: 18 serving routes flagged // "Not responding" because their titogo@* units matched no sweep key). func expectsReadings(provisioned bool, unit, backing string) bool { return provisioned || unit != "" || backing == registry.BackingUnit } // healthWord folds provisioning state, hibernation, liveness and whether a // reading was ever possible into the one word the table shows. Provisioning // words win while provisioning is real (a cert mid-issue IS the situation); // once the platform says ready, the question becomes "is the process alive", // which only instance-kind routes can answer — static and service routes are // served without a tenant process, so ready means Serving, full stop. Among // silent instances, expectReadings decides whether the silence is an outage // ("Not responding") or honest ignorance about a route the sweep was never // told how to sample ("No readings") — see expectsReadings. func healthWord(kind, status string, hibernate, reporting, expectReadings bool) string { if kind == "sidecar" { return sidecarHealthWord(status, hibernate, reporting, expectReadings) } switch status { case "pending", "creating", "starting", "issuing": return "Setting up" case "failed": return "Failed" } if kind != "instance" { return "Serving" } switch { case reporting: return "Running" case hibernate: return "Asleep" case expectReadings: return "Not responding" default: return "No readings" } } // sidecarHealthWord is healthWord's vocabulary for Kind=="sidecar" rows, // split out because "Serving" — the word every other non-instance kind gets // — is simply false for a sidecar: it is a standalone worker that never // binds a socket or answers a request (registry.Route.IsSidecar's doc), so // nothing it does is ever "serving". Its five words: Running (reporting a // version over the agent socket), Asleep (hibernating, the app manifest's // choice), Waking (mid-provisioning, mirrors "Setting up"), Crashed (the // platform runs its process and it has gone silent — mirrors "failed" and // the instance family's "Not responding", but a sidecar's silence reads as // a crash rather than a request timing out, since nothing dials it) and // Never started (nobody has ever run it: not reporting, not hibernating, // and not yet provisioned to run at all). Every word here is also in // healthWords (listquery.go), which is what makes it filterable on the // Running tab. func sidecarHealthWord(status string, hibernate, reporting, expectReadings bool) string { switch status { case "pending", "creating", "starting", "issuing": return "Waking" case "failed": return "Crashed" } switch { case reporting: return "Running" case hibernate: return "Asleep" case expectReadings: return "Crashed" default: return "Never started" } }