---
id: w5/m7/t003
title: "Plan-picker page: radio cards, current pre-selected, confirm + updateServicePlan, toasts"
worker: worker5
status: done
estimate: 45m
depends_on: [w5/m7/t001]
---

## Objective

Mirror Render's "Pick an Instance Type" page: radio cards for every tier, the current plan pre-selected, Save Changes disabled until the selection differs, a confirm step that names the rollout cost, then the m8 mutation and back to Settings.

## Context

- Render's captured layout (`.playwright-mcp/render-plan-picker.yml`): heading "Pick an Instance Type"; radiogroup of cards — Free visually separated from the paid ladder; each card: tier name, price, RAM, CPU; footer Cancel + Save Changes (disabled until changed). bex cards show **name / CPU / RAM only** (no prices — Metronome decision; the milestone README records this delta).
- Route: `dashboard/src/routes/services.$serviceId.plan.tsx` — mirrors Render's URL split (settings row links here). Cancel returns to Settings.
- Data: t001's `instanceTypes` for the cards; `server(id) { plan }` for pre-selection. Mutation: `updateServicePlan(id, plan)` (m8, already in the schema — add the operation to the dashboard's `.graphql` docs + codegen).
- Confirm dialog before saving (the repo's existing confirm-dialog idiom from suspend/restart in the services table): message must say the service resizes and rolls (one restart-shaped deploy) — same honesty as Render's flow. Success toast + navigate back to Settings; error toast on 400/failure (e.g. mid-flight plan removal).
- Free card: keep it selectable (parity), but bex's `free` is 0.1 CPU / 512 MB with *no* idle-sleep yet — no special copy needed beyond the specs themselves.
- i18n en+zh for all strings; follow dataviz/card idioms already in the feature.

## Steps

1. Route + page: radiogroup cards from `instanceTypes`, Free separated, current pre-checked, Save disabled until changed.
2. Confirm dialog → `updateServicePlan` → toast → navigate to Settings (plan row shows the new tier once the query refetches).
3. Error paths: mutation failure toast; plan-less App (nothing pre-selected, Save enables on first pick).

## Files

- `dashboard/src/routes/services.$serviceId.plan.tsx` (new)
- `dashboard/src/features/services/components/` (picker components), `locales/{en,zh}.ts`
- `dashboard/src/features/services/api/*.graphql` (+ regenerated definitions)

## Acceptance criteria

- [ ] Picker lists all 7 tiers with CPU/RAM, current plan pre-checked, Save disabled until a different card is picked.
- [ ] Saving confirms (rollout warning), fires `updateServicePlan`, toasts, and returns to Settings showing the new tier.
- [ ] No hardcoded tier data in the frontend (everything from `instanceTypes`).
- [ ] Lint + tests + build green.

## Out of scope

- Prices, custom-instance contact, per-tier availability gating.
