Instance types tab with browser
## Original requirement (verbatim, epic)
Move all product related configuration from Settings to Products section in the Operator.
Operator's ask, verbatim: "Create a task to merge these two concepts. User should be able to manage the list of supported instances - create UI with support of family, lookup from actual aws, showing params (cpu, ram, other) and price on-demand and current spot. user adds which we want to allow. on the hosting products section - when adding products only allow selecting from available, prefill the name from instance type, also allow easy navigation, by family, etc. show exclamation mark if the product has an instance type that is no longer allowed."
Itemized requirements (verbatim):
- 2. "**Browse live AWS data to add types.** Look up what the region offers through the operator's own cloud connection (EC2 `DescribeInstanceTypes`, on-demand price, current spot price). Show family, vCPU, RAM, architecture, network and other key specs, on-demand $/h and current spot $/h. The operator ticks which types to allow."
- 3. "**Navigate by family.** Group or filter by family (t3, m6i, m8i, c7g…), by architecture, and by vCPU/RAM range, with search. Usable on a large list."
Full PRD: Task 5632; full design: Task 5633 (read with `task_read` if you need more than this body).
## This child (C6): Products › Instance types tab + the shared `InstanceTypeBrowser`
Builds on: C2's `GET /api/instance-types/browse` + `POST /api/instance-types`, C1's `GET`/`DELETE /api/instance-types` (contracts below), C5's `apps/web/src/pages/Products.tsx` (append your tab to its `TABS` array, id `instance-types`, first position, gated on `system:view`), C3's IAM grants (for real dev prices). All merged before you start.
### Settled decisions (BA, verbatim)
- **D7 — Empty allow-list keeps today's meaning:** no cloud node, node group or Dedicated hosting can be created, and the Products page says so plainly.
- **D8 — Disallowing a type that is in use** (an active Dedicated hosting product, a node group, or running hostings) is permitted after a confirmation that names what uses it. Running nodes/hostings are untouched; affected products become stale (R7); node groups stop scaling out on it (existing reconcile behaviour).
### Pinned component contract (§4.5) — you OWN it; sibling C7 reuses it in `pick` mode
`apps/web/src/components/instanceTypes/InstanceTypeBrowser.tsx`:
`InstanceTypeBrowser({ rows, mode: 'allow' | 'manage' | 'pick', selected, onSelectedChange, showPrices, rowAction })`.
`allow` (browse) = multi-select with prices; `manage` (allowed list) = no selection, renders `rowAction(row)` (Disallow) + usage + "specs unknown"; `pick` (C7's hosting-product picker) = single-select, no prices. All three share grouping, filters, virtualisation and the 480px card layout.
```ts
export type InstanceTypeRow = {
instance_type: string; family: string
vcpu: number | null; memory_gib: number | null; architecture: 'x86_64' | 'arm64' | null
network_performance: string | null; current_generation: boolean | null; gpu_count: number | null
specs_known: boolean
on_demand_usd_hour?: number | null; spot_usd_hour?: number | null // browse only
allowed?: boolean // browse only
usage?: { products: {id: string; name: string; active: boolean}[]; node_groups: {id: string; name: string}[]; live_hostings: number } // allowed list only
}
// mapping: GET /api/instance-types types[] -> row as-is (no prices/allowed);
// GET /api/instance-types/browse types[] -> row with specs_known = true (drop `architectures`).
```
Export helpers `familyOf`, `instanceProductName` (AD22: `` `${type} · ${vcpu} vCPU · ${ram} GB` ``, ram integer when whole else 1 decimal — `m6i.2xlarge · 8 vCPU · 32 GB`, `t4g.nano · 2 vCPU · 0.5 GB`; unknown specs → type alone; unit-tested), `filterRows(rows, filters)`.
**AD25 unknown specs:** rows with `specs_known:false` match family filter and search; any vCPU/RAM range or architecture filter excludes them, and the count says so ("3 with unknown specs hidden").
### Quality bar (Architect §3 — these ARE acceptance, a naive build that skips them fails)
- NOT a flat unvirtualised table or a `<select>`: family groups (collapsible; header shows count + vCPU/RAM span), sticky filter row (family multi-select chips, arch toggle, vCPU and RAM min–max, search, using `ui/FilterRow.tsx` `FilterSelect`/`FilterSearch`), result count; virtualised with `@tanstack/react-virtual` (precedent `ui/TargetSelector.tsx:589`) over flattened group+row items; memoised filtering; search deferred with `useDeferredValue`.
- Type name NEVER truncated (`white-space: nowrap`, min column fits the longest name — `u7in-32tb.224xlarge`, 19 chars). At ≤600px each row is a 2-line card: line 1 type + allowed badge; line 2 `8 vCPU · 32 GiB · x86_64 · $0.4032/h · spot $0.1736/h`. No page-level horizontal scroll.
- Prices `$0.4032/h` (4 dp, USD). A denied/unavailable part shows "—" with a one-line reason in the header (e.g. "Spot prices unavailable: AccessDenied for ec2:DescribeSpotPriceHistory") — never `0`.
- Copy follows `docs/standards/ui-copy-standards.md` (labels a noun/verb, one-line help).
### Views
- **Allowed list** (`manage` mode over `GET /api/instance-types`): grouped, specs or "specs unknown", usage, Disallow → on 409 a confirmation dialog listing the usage (products, node groups, live hostings) then `DELETE ?confirm=true`.
- **Browse** (`allow` mode over `GET /api/instance-types/browse`): connection switcher (from `connections`), region shown, "as of" times from `fetched_at`, price-error lines, refresh, multi-select → "Allow N" → `POST /api/instance-types`; already-allowed rows marked. No connection → the `reason` text + link to Machines › Connections; the allowed list still works.
- **Empty state:** "No types allowed — node creation is off" (D7).
### Acceptance criteria (verbatim from PRD; your share)
- **AC2.4** … the Instance types page shows an explicit "No types allowed — node creation is off" state.
- **AC3.1** From the browse view the operator ticks one or many types and allows them in one action; from the allowed list they can disallow (D8 confirmation when in use).
- **AC4.1** Browse and allowed-list views can group by family and filter by family (multi-select), architecture (x86_64/arm64), vCPU range and RAM range, plus free-text search on type name; filters combine (AND) and the result count is shown.
- **AC4.2 (quality)** With the full region list (~800+ types) the view stays responsive (filter input → updated list without visible lag, no page-level horizontal scroll), type names are never truncated to an unreadable fragment, and spec/price columns stay aligned and legible at desktop width; at 480px the layout reflows (cards or stacked rows) with the type name, vCPU, RAM and both prices still readable — no horizontal page scroll.
- **AC5.2** Connection choice per D5; region displayed; switching connection reloads the list for that region.
- **AC5.4 (availability)** On dev, the operator's real cloud connection returns real types with real on-demand and spot prices. …
- **AC5.5** Responses may be cached briefly … but the view shows when prices were fetched.
Credentials & reach: Component tests — None — **verified**. AC4.2/AC5.4 on dev (browser) — `gmind/skynet/dev` (dev operator UI login), dev control plane `dev.jaah.ai` — credential **verified**; environment **deferred**: dev resolves to private 10.60.x addresses unreachable from node h2; run from a node with dev reach, otherwise report unverified with this reason. AC5.4 dev operator CloudConnection identity (the one `rental_connection_id` signs with) — **deferred**: visible only in the dev console; confirm via Machines › Connections + Browse showing prices or a price-error line naming the missing action; a third, hand-made identity is recorded as an unverified remainder. Live AWS reads (AD8–AD10 shapes) — `jaah/system/bootstrap-admin`, us-east-1 — **verified** (read-only probes succeeded); used for the AC5.4 live-AWS backstop below.
Out of scope: Do NOT change the browse/allow/list/delete API contracts (C1/C2) — consume them. Do NOT build the hosting-product picker dialog or stale icon (C7) — only the reusable component C7 will use. Do NOT add a reorder UI (AD3). Do NOT persist prices or derive product prices from them. Do NOT touch IAM/terraform. Do NOT add a feature flag. Epic: Shared tiers and storage sizes (unchanged), refunding the failed order, GCP/Azure.
Done means:
- AC4.1 — component test with a stubbed list of ≥ 600 types across ≥ 40 families: filter `m8i` + arm64 → 0; `m8i` + x86_64 → only m8i rows; vCPU 8–16 narrows correctly; search "c7g.2" finds `c7g.2xlarge`; result count shown; unknown-spec rows hidden by a range filter and the count says so; same filters work in `manage` mode.
- AC3.1 — component test: tick 3 → "Allow 3" → POST body has the 3 keys → allowed list shows them; Disallow an in-use type → 409 → dialog lists usage → confirm → `DELETE ?confirm=true`.
- AC2.4 — component test for the empty state text.
- AC5.2 — component test: switching connection refetches with the new `connection_id` and shows its region; no-connection reason + link rendered.
- AC5.5 — component test shows an "as of" time; a `price_errors.spot` value renders "—" + the one-line reason.
- `instanceProductName` unit test (AD22 cases incl. 0.5 GB and unknown specs).
- AC4.2 — component test asserts the full type name text present (`u7in-32tb.224xlarge`); Playwright at 1440px and 480px with a real region on dev, screenshots in the report (deferred — dev.jaah.ai unreachable from node h2). Fallback when dev is unreachable: Playwright at 1440px and 480px against the local `./dc` stack with ~1,400 fixture rows (stubbed browse response), asserting no horizontal page scroll and untruncated names; screenshots in the report.
- AC5.4 — dev browser flow: Products › Instance types › Browse → `m8i.2xlarge` row shows 8 vCPU / 32 GiB and non-empty on-demand and spot prices; screenshot. (deferred — dev.jaah.ai unreachable from node h2 and the dev connection identity is console-only; report unverified with reason if your node also cannot reach it)
- AC5.4 live-AWS backstop (required even when dev is unreachable) — run the merged browse merge path (`instance_types` browse with C2's clients) against real us-east-1, signed with `jaah/system/bootstrap-admin` (read-only), and cite: `m8i.2xlarge` → 8 vCPU / 32 GiB with non-null on-demand and spot $/h, total type count, no price error. Never write the credential value anywhere.
- `./dc check quick` green; Jaah Deploy for the merge succeeded.
Outcome
Instance types tab + shared InstanceTypeBrowser shipped. Also fixed a C2 parser bug where live AWS showed 0 types. PR #4966 merged, CI and Jaah Deploy green. Dev browser check unverified: dev unreachable. UI follow-ups in report (task creation refused).
Artifacts
Activity
- Draft
- Ready
- Runner started
- Runner ended (done)