Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions internal/handlers/capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,23 @@ func (h *CapabilitiesHandler) Get(c *fiber.Ctx) error {
})
}

// BUG-API-039 / BUG-API-311 (QA 2026-05-29): /api/v1/capabilities
// is dashboard-hit on every nav (sidebar tile counts, billing card,
// settings page render). The response only changes when
// api/plans.yaml is edited and the binary is redeployed — so the
// tier matrix is *immutable for the life of the running pod*.
// Without a Cache-Control hint each dashboard nav re-fetched the
// full ~4 KB matrix; sidebar fanout meant 4-6 redundant fetches per
// nav (BUG-DASH-016 noise). A `max-age=60` directive lets the
// browser fetch cache + intermediaries serve the response from the
// edge for a minute, cutting tile-render latency without hiding a
// real tier change for longer than one rule-23 deploy cycle (the
// build-SHA flip invalidates the proxy cache via /healthz polling).
// `public` because the tier matrix is the same for every caller
// (no per-user discrimination); `must-revalidate` so stale-while-
// revalidate proxies still re-fetch on expiry rather than serving
// indefinitely-stale rows after an extended offline period.
c.Set(fiber.HeaderCacheControl, "public, max-age=60, must-revalidate")
return c.JSON(fiber.Map{
"ok": true,
"tiers": out,
Expand Down
24 changes: 24 additions & 0 deletions internal/handlers/capabilities_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,30 @@ func TestCapabilities_TerminalTierUpgradeURLIsNull(t *testing.T) {
}
}

// TestCapabilities_CacheControlPublicMaxAge60 pins BUG-API-039 /
// BUG-API-311: /api/v1/capabilities is dashboard-hit on every nav
// (sidebar tiles, billing card, settings) and the tier matrix is
// immutable for the life of the running pod (only changes on a
// plans.yaml edit + redeploy). Without a Cache-Control hint each nav
// re-fetched the full ~4 KB matrix; sidebar fanout meant 4-6 redundant
// fetches per nav (BUG-DASH-016).
//
// Pin `public, max-age=60, must-revalidate` so the browser/edge cache
// serves the matrix for a minute while still re-validating on expiry.
// A future deletion of the c.Set call fails this assertion before merge.
func TestCapabilities_CacheControlPublicMaxAge60(t *testing.T) {
reg := plans.Default()
app := newCapabilitiesApp(t, reg)
req := httptest.NewRequest(http.MethodGet, "/api/v1/capabilities", nil)
resp, err := app.Test(req, -1)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode)
require.Equal(t, "public, max-age=60, must-revalidate",
resp.Header.Get("Cache-Control"),
"BUG-API-039/311: /api/v1/capabilities must stamp Cache-Control: public, max-age=60, must-revalidate so dashboard nav fanout doesn't re-fetch the same immutable matrix every navigation")
}

// TestCapabilities_AnnualDiscountFromYAML — when a {tier}_yearly variant
// exists in the registry, the canonical tier reports a non-zero
// annual_discount_percent computed from (1 - yearly/(monthly*12)).
Expand Down
Loading