A started trial never showed as entitled. Two causes, both fixed here.
1. The live path never fetched entitlements at all. loadTenantForShell resolves
fixture tenants first and, for a REAL tenant, returned a shim with
`entitled: []` and `products: []` hard-coded — so `entitled.includes(p.id)`
was false for everything no matter what the registry said. It now fetches
/v1/catalog and /v1/entitlements (both best-effort: a failure degrades to an
empty grid rather than 404-ing the whole page) and filters entitlements to
enabled + not expired.
2. The ids could not match even in principle. The registry catalog offers
`certifai` and `compliance`; the portal's hard-coded list used
`compliance-scanner` — a different product entirely (it lives in
~/workspace/compliance-scanner and is not in this catalog). The findings
filter hard-coded the same wrong id.
The fix removes the class of bug rather than renaming one constant: products are
now derived from the registry catalog, and `entitled` is built from the SAME
registry keys, so the two sides cannot drift apart. The findings filter is
likewise derived from the tenant's own products.
Verified against the live dev registry: catalog returns
[certifai, compliance]; after starting a trial for acme, entitlements returns
`compliance` (enabled, expires 2026-09-16), and the mapping yields
entitled=[compliance] with entitled.includes("compliance")=true and
certifai=false. tsc --noEmit 0, next build 0.
Fixture tenants are unaffected — they still carry their own products, and the
derived filter uses those.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CNdLL9BdsWm7MCyui5ffPD