Add Easy Mode plan switch and caps

Task 13442 · Done · Coder · created · updated

Part of epic "Build sponsored Easy Mode app hosting" (PRD task 13412, architecture task 13413). Architecture child **C2 Plan switch + caps**. ## Original requirement (verbatim) > One plan switch, Easy Mode, default OFF, with per-org override and caps. Turning it on for an org also turns on app hosting (no separate hosting switch). > Caps (proposed defaults, plan terms with per-org overrides): 2 apps per org, 100k back-end requests/month, 10 GB egress/month, 500 MB database per app, Lambda reserved concurrency 2, optional sponsorship end date. Over cap: app paused to an "over limit" page, org notified. > 2. Plan switch + caps: `easy_mode` switch (default OFF) and the cap terms with per-org overrides, in the existing plan-terms pattern; operator UI on the org's plan. PRD A9: Easy Mode is visible to tenants only when on (plan-switch rule `apps/api/app/plan_terms.py:14-17`: off renders nothing, no upsell); every API door to Easy Mode/hosting returns 403 "Easy Mode is not on your plan…" when off. ## Approach (settled by the Architect) - Switch: `SwitchSpec("easy_mode", "Easy Mode")` in `apps/api/app/plan_terms.py:209-254` (default OFF); columns `billing_plans.easy_mode` + `orgs.easy_mode_override` (pattern `production_qa`, migration `202611012000_production_qa.py`). Doors use `require_switch(session, org_id, "easy_mode")` (`plan_terms.py:962-970`) so every Easy Mode / hosted-app route 403s with the plan sentence. - Six terms, nullable on `billing_plans`, `*_override` on `orgs`, resolved by ONE new resolver (pinned): ```python @dataclass(frozen=True) class EasyModeTerms: apps: int # default 2 requests_month: int # default 100_000 egress_gb_month: int # default 10 db_mb: int # default 500 concurrency: int # default 2 sponsor_end: date | None # default None def easy_mode_terms(session, org_id) -> EasyModeTerms # override → plan chain → (default) → constant ``` A dedicated resolver, not `CAP_KINDS` (these are not slot/agent caps; one is a date; `plan_end`, `plan_terms.py:753-768`, is the precedent). - Web: `PLAN_SWITCH_DEFAULTS` (`apps/web/src/pages/settings/billing/api.ts:42-76`), `FEATURE_NAME` (`apps/web/src/lib/planSwitches.tsx:105-128`), `FEATURE_SWITCHES` (`apps/web/src/pages/products/plan/PlanFields.tsx:152-171`); an "Easy Mode" field group in PlanFields and the org Overrides tab (`apps/web/src/pages/orgs/overridesForm.ts`), shown with units ("requests / month", "GB / month", "MB per app", "at once", date). Copy per `docs/standards/ui-copy-standards.md`; layout per `docs/standards/ui-style-guide.md`. - This child owns ONE migration (the switch + six term columns on both tables). It does not add `projects.easy_mode` or `org_comps.origin` (enablement child C4 owns those). ## Acceptance criteria (verbatim, BA) - **AC-2 Caps editable.** The operator sees `Easy Mode` among a plan's switches and the six cap terms (apps per org, requests/month, egress GB/month, DB MB/app, concurrency, sponsorship end) on the plan editor and on the org's Overrides tab, with the defaults 2 / 100,000 / 10 / 500 / 2 / none; an org override wins over the plan. → verify: dev, `/products/plans/<key>` and `/customers/<org>` Overrides tab; set requests to 50,000 for the test org, reload, observe the org's effective term 50,000 while the plan still reads 100,000. - **AC-1 Off is invisible** (this child's part: the registry). "… Also `./dc test` of the switch registry test shows `easy_mode` default False." All UI oracles run on the dev console (`app.dev.jaah.ai`) in a browser; all copy follows `docs/standards/ui-copy-standards.md` and each page passes at phone width. Every criterion also fails if text is truncated to a few characters, overlaps, or a page shows a raw slug/id/enum instead of a readable label. Credentials & reach: - AC-1…AC-9, AC-14…AC-20, AC-22, AC-23, AC-3b (dev UI) — `me/jaah/debug/main/operator-login` (operator; switches into the test tenant, covers member role) on `app.dev.jaah.ai`; member view also via `org/console/dev-tpl-member-login` — verified — both on roster, `credential_get` ok; operator login `POST /api/auth/login` 200 from this node. Out of scope: a separate GitHub org; a separate app apex domain; custom domains, registrar purchase and paid hosting after sponsorship (draft task 10233 stays as that later work); the always-on shared-box hosting tier. Do NOT add app-hosting price rows or billing; do NOT make Easy Mode an org setting a tenant can flip (plan/override, operator only); do NOT build the enablement action, credit or node (C4) or any hosted-app route (C6). Done means: - AC-2 — on dev after deploy: plan editor and the test org's Overrides tab show the Easy Mode switch and the six terms with readable labels, units and the defaults 2 / 100,000 / 10 / 500 / 2 / none; set the org's requests override to 50,000, reload, the org's effective term reads 50,000 while the plan reads 100,000 (screenshots at desktop and phone width; nothing truncated, no raw keys). - AC-1 (registry) — `./dc test` (api) includes a test asserting `easy_mode` is in the switch registry with default False on every plan, and that `easy_mode_terms` resolves override → plan → constant for each term; cite the passing output. - `require_switch(session, org_id, "easy_mode")` returns 403 with the plan sentence for an org with the switch off (unit test).

Outcome

Added Easy Mode plan switch (off on every plan, 403 plan sentence) + six caps with org overrides via plan_terms.easy_mode_terms; plan editor + Overrides tab edit them with units. Quick gate + 1535 API/web tests green; visual check done. PR #7901 queued. Memory: used mem-25288, mem-31277; proposed none.

Pull request

Artifacts

Activity

  • Draft
  • Ready
  • Runner started
  • Runner ended (done)