console: Database tile shows size only; no box disk to tenants
Review fixes on d9bf4606: - Drop DiskFracMax/InstanceMetrics.DiskFrac. box.Disk is the shared volume's fill, and OwnerAppMetrics promises owners "unit-level rows only, no host totals"; a tenant could watch neighbours' disk use. - Put the Overview's status read under instanceStoreTimeout, so a slow status read costs the tile its number, never the page. - Tile copy (copy-reviewed): "on disk, including copies that are asleep" / "no size reported yet"; hidden for apps that run nothing. - Remove the stray .claude/settings.json (ledger hooks for every checkout). - Render test for both tile states.
Human
Prompt
Can we review, respond, merge and deploy the DB tile?
From this commit's own message. The prompt ledger has no entries for it — which is what a squashed, rebased or amended commit looks like, and what every commit looks like in a repo without a ledger.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 files changed,
+37
−75
.claude/settings.json+0 −24internal/console/metricsview.go+0 −8internal/console/overview.go+0 −11internal/console/overview_test.go+26 −29internal/console/server.go+5 −1internal/console/templates/app_overview.html+6 −2
CI passed — run details
diff --git a/.claude/settings.json b/.claude/settings.json| deleted file mode 100644 |
| index 7a1468fb5..000000000 |
| --- a/.claude/settings.json |
| +++ /dev/null |
| @@ -1,24 +0,0 @@ |
| -{ |
| - "hooks": { |
| - "Stop": [ |
| - { |
| - "hooks": [ |
| - { |
| - "command": "amadan ledger sync-transcript", |
| - "type": "command" |
| - } |
| - ] |
| - } |
| - ], |
| - "UserPromptSubmit": [ |
| - { |
| - "hooks": [ |
| - { |
| - "command": "amadan ledger add-prompt", |
| - "type": "command" |
| - } |
| - ] |
| - } |
| - ] |
| - } |
| -} |
diff --git a/internal/console/metricsview.go b/internal/console/metricsview.go| index 52a0857ea..7f1c70307 100644 |
| --- a/internal/console/metricsview.go |
| +++ b/internal/console/metricsview.go |
| @@ -674,13 +674,6 @@ type InstanceMetrics struct { |
| Box string |
| CPUFrac *float64 |
| MemBytes *int64 |
| - // DiskFrac is the used-space fraction of the BOX this instance currently |
| - // sits on — a host-level reading (every instance sharing the box reports |
| - // the same value), carried per-instance only so the Overview tile can |
| - // read it off InstanceMetrics like every other number there. It says |
| - // nothing about which box, so it is safe to render even though Box |
| - // itself is not (see Box's own doc comment). |
| - DiskFrac *float64 |
| SparkCPU, SparkMem string |
| @@ -780,7 +773,6 @@ func (s *Server) OwnerAppMetrics(ctx context.Context, hosts []string, unitByHost |
| Box: box.Box, |
| CPUFrac: u.CPUFrac, |
| MemBytes: u.MemBytes, |
| - DiskFrac: box.Disk, |
| SparkCPU: u.SparkCPU, |
| SparkMem: u.SparkMem, |
| } |
diff --git a/internal/console/overview.go b/internal/console/overview.go| index f193ec403..03db7eba9 100644 |
| --- a/internal/console/overview.go |
| +++ b/internal/console/overview.go |
| @@ -72,14 +72,6 @@ type OverviewStats struct { |
| CPUSum *float64 |
| MemSum *int64 |
| - // DiskFracMax is the fullest disk-used fraction among the boxes backing |
| - // this app's reporting instances — the worst case, not an average, |
| - // because a shared volume filling up is what crashes every tenant on it |
| - // at once (the 2026-10-01 bdn/ips outage: SQLITE_FULL on one instance's |
| - // writes, then every instance on the box failing to reopen its database |
| - // at all). Nil when nothing reports, same em-dash contract as CPUSum. |
| - DiskFracMax *float64 |
| - |
| // 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 — |
| @@ -130,9 +122,6 @@ func assembleOverview(view *AppView, acct string, metrics []InstanceMetrics, ver |
| } |
| *st.MemSum += *m.MemBytes |
| } |
| - if m.DiskFrac != nil && (st.DiskFracMax == nil || *m.DiskFrac > *st.DiskFracMax) { |
| - st.DiskFracMax = m.DiskFrac |
| - } |
| } |
| for _, rt := range view.Routes { |
| if rt.Account != acct || rt.Platform { |
diff --git a/internal/console/overview_test.go b/internal/console/overview_test.go| index 96dcbbe3e..fece75e74 100644 |
| --- a/internal/console/overview_test.go |
| +++ b/internal/console/overview_test.go |
| @@ -12,12 +12,14 @@ package console |
| // says so. |
| import ( |
| + "context" |
| "net/http" |
| "strings" |
| "testing" |
| "time" |
| "github.com/carlosframework/platform/internal/registry" |
| + "github.com/carlosframework/platform/internal/status" |
| ) |
| // f64 and i64 are the pointer literals InstanceMetrics carries — nil is a |
| @@ -665,35 +667,6 @@ func TestOverviewCountsSidecarsSeparately(t *testing.T) { |
| } |
| } |
| -// TestOverviewDiskFracMaxIsWorstBoxNotAverage: the Database tile's disk |
| -// figure must name the box closest to the SQLITE_FULL cliff, not blend it |
| -// away into an average — the 2026-10-01 bdn/ips outage was one box's volume |
| -// filling up and every instance on it failing together, a fact an average |
| -// across boxes would have hidden behind a calm-looking number. |
| -func TestOverviewDiskFracMaxIsWorstBoxNotAverage(t *testing.T) { |
| - view := &AppView{App: "ips", Routes: []RouteView{ |
| - {Host: "a", Account: "bab", Kind: "instance"}, |
| - {Host: "b", Account: "bab", Kind: "instance"}, |
| - {Host: "c", Account: "bab", Kind: "instance"}, |
| - }} |
| - metrics := []InstanceMetrics{ |
| - {Host: "a", Box: "box-1", DiskFrac: f64(0.40)}, |
| - {Host: "b", Box: "box-2", DiskFrac: f64(0.97)}, |
| - // Reporting, but this box's disk reading was unreadable: must not |
| - // win by default or be treated as the floor. |
| - {Host: "c", Box: "box-3"}, |
| - } |
| - st := assembleOverview(view, "bab", metrics, nil, boxJoin{}) |
| - if st.DiskFracMax == nil || *st.DiskFracMax != 0.97 { |
| - t.Fatalf("DiskFracMax = %v, want 0.97 (the fullest box, not an average)", st.DiskFracMax) |
| - } |
| - |
| - quiet := assembleOverview(view, "bab", []InstanceMetrics{{Host: "a"}, {Host: "b"}, {Host: "c"}}, nil, boxJoin{}) |
| - if quiet.DiskFracMax != nil { |
| - t.Fatalf("DiskFracMax = %v with no disk reading anywhere, want nil so the tile shows the em-dash", quiet.DiskFracMax) |
| - } |
| -} |
| - |
| // TestOverviewDBSizeSumsEveryRouteRegardlessOfReporting: a hibernating |
| // instance's database is still sitting on disk even though the metrics |
| // sweep hears nothing from it, so the Database tile's size must come from |
| @@ -761,3 +734,27 @@ func TestAppPageShowsPartialMismatchNotFullAlarm(t *testing.T) { |
| t.Errorf("the partial-mismatch banner does not link to diagnostics:\n%s", body) |
| } |
| } |
| + |
| +// TestAppOverviewDatabaseTileRenders: the tile shows the sized database from |
| +// a fresh box's status, and says so plainly when nothing has been sized. |
| +func TestAppOverviewDatabaseTileRenders(t *testing.T) { |
| + fx := newConsoleFixture(t) |
| + fx.addRoute(t, registry.Route{Host: "owned.example.com", Kind: "instance", |
| + Status: "ready", App: "kass", Backing: registry.BackingUnit}) |
| + c := signInPaul(t, fx) |
| + |
| + _, _, body := get(t, c, fx.u("/apps/kass")) |
| + if !strings.Contains(body, "<h2>Database</h2>") || !strings.Contains(body, "no size reported yet") { |
| + t.Fatalf("unsized app: want the Database tile saying no size yet in:\n%s", body) |
| + } |
| + |
| + box := status.CollectBox(nil, status.Probes{MachineID: "box1"}, time.Now()) |
| + box.Metrics = &status.BoxMetrics{RouteDBBytes: map[string]int64{"owned.example.com": 3 << 20}} |
| + if err := status.WriteBox(context.Background(), fx.rel, box); err != nil { |
| + t.Fatal(err) |
| + } |
| + _, _, body = get(t, c, fx.u("/apps/kass")) |
| + if !strings.Contains(body, "3.0 MB") || !strings.Contains(body, "on disk, including copies that are asleep") { |
| + t.Fatalf("sized app: want 3.0 MB on the Database tile in:\n%s", body) |
| + } |
| +} |
diff --git a/internal/console/server.go b/internal/console/server.go| index b579f4c8d..80838075e 100644 |
| --- a/internal/console/server.go |
| +++ b/internal/console/server.go |
| @@ -2815,7 +2815,11 @@ func (s *Server) handleApp(w http.ResponseWriter, r *http.Request) { |
| } |
| p.Versions = versions |
| metrics := s.appResourceMetrics(r.Context(), p.Detail, acct) |
| - dbJoin := loadBoxJoin(r.Context(), s.cfg.Releases, time.Now()) |
| + // The Database tile's sizes are best-effort: a slow status read costs the |
| + // tile its number (the em-dash), never the page. |
| + joinCtx, cancelJoin := context.WithTimeout(r.Context(), instanceStoreTimeout) |
| + dbJoin := loadBoxJoin(joinCtx, s.cfg.Releases, time.Now()) |
| + cancelJoin() |
| p.Overview = assembleOverview(p.Detail, acct.Sqid, metrics, versions, dbJoin) |
| // The diagram is drawn from the SAME tenant-filtered rows the Running tab |
| // renders, so the two can never disagree about which addresses exist or |
diff --git a/internal/console/templates/app_overview.html b/internal/console/templates/app_overview.html| index 72e576d4f..f029121bb 100644 |
| --- a/internal/console/templates/app_overview.html |
| +++ b/internal/console/templates/app_overview.html |
| @@ -37,11 +37,15 @@ |
| <p class="tile-value">{{pct .CPUSum}} · {{bytesOrDash .MemSum}}</p> |
| <p class="tile-detail">{{if .CPUSum}}CPU · memory, from the copies responding{{else if .MetricsUnavailable}}no data right now{{else}}no recent data{{end}}</p> |
| </a> |
| + {{/* Sizes come from the routes, not the metrics sweep: a sleeping copy's |
| + database is still on disk. Hidden for apps that run nothing. */}} |
| + {{if or .DBSizeSum .InstanceExpected}} |
| <a class="tile" href="{{$.Nav}}/apps/{{$.AppName}}/instances"> |
| <h2>Database</h2> |
| - <p class="tile-value">{{bytesOrDash .DBSizeSum}} · {{pct .DiskFracMax}}</p> |
| - <p class="tile-detail">{{if .DBSizeSum}}size · disk used, from the copies responding{{else if .MetricsUnavailable}}no data right now{{else}}no recent data{{end}}</p> |
| + <p class="tile-value">{{bytesOrDash .DBSizeSum}}</p> |
| + <p class="tile-detail">{{if .DBSizeSum}}on disk, including copies that are asleep{{else}}no size reported yet{{end}}</p> |
| </a> |
| + {{end}} |
| <a class="tile" href="{{$.Nav}}/apps/{{$.AppName}}/settings/domains"> |
| <h2>Web addresses</h2> |
| <p class="tile-value">{{.DomainTotal}}</p> |