Commit 12c54d9
committed
fix(frontend/recs): enforce one-variant-per-cell radio selection (closes #224)
After PR #195 (issue #188) the recommendations refresh fans out across
2 terms × 3 payments per `(provider, account, service, region,
resource_type, engine)` cell — up to 6 alternative rec rows per
physical resource. They're alternatives, not additions, but the
checkboxes had no mutual-exclusion logic, producing wrong purchase
intent in two ways:
1. **Manual checking**: a user could check `1yr/all-upfront` AND
`3yr/no-upfront` for the same EC2 m5.large in the same account.
Both fed into the purchase plan → double commitment for one
resource.
2. **`select-all` was the destructive form**: every visible row got
added, so 3 cells × 6 variants = 18 commitments instead of the
3 the user expected when clicking "buy what's recommended". 6× the
intended spend per cell.
# What changes
- New `cellKey(rec)` helper returns the
`(provider, cloud_account_id, service, region, resource_type, engine)`
prefix (same as the scheduler ID encoding from #189, minus the
`(term, payment)` suffix). Recs sharing this key are alternatives
for the same physical resource.
- Per-row checkbox change handler now enforces radio behaviour: on
check, scan the visible-recs list for any sibling in the same cell
that's already selected, deselect it FIRST, then add the new rec.
Cells are independent — selecting in cell X doesn't touch cell Y's
selection.
- `select-all` handler rewritten: clear current selection, then call
new `pickBestVariantPerCell(recs)` which groups by cell and picks
the variant with the highest **effective monthly savings**:
`effective = savings - (upfront_cost / (term * 12))`. Amortizing
the upfront over the commitment term means a 3yr/all-upfront with
a huge lump-sum doesn't beat a 1yr/no-upfront just on raw
`savings`. Sibling issue #223 will replace this tiebreaker with
"matches resolved GlobalConfig.DefaultTerm + DefaultPayment" when
it lands; until then, amortized savings is the right deterministic
default.
# Out of scope (deliberate)
- **Native `<input type="radio" name="cell-X">` markup**. Per the
issue: "Designer call — if cell-grouping (sibling issue #226)
lands first, radios become visually correct." Stayed with
checkboxes-with-radio-behaviour for this PR; markup switch waits
for cell-grouping visual.
- **Default-select per cell from settings** (sibling #223).
# Tests
4 new tests in `frontend/src/__tests__/recommendations.test.ts`
inside the `'Issue #224: one-variant-per-cell radio selection'`
describe block:
(a) Manual toggle within a cell — checking variant B with variant
A already selected: A is removed, B is added. Sibling A was
not also added; B was not also removed.
(b) Cross-cell independence — selecting in cell X must NOT remove
cell Y's existing selection.
(c) `select-all` collapses 18 → 3: 3 cells × 6 variants. After
click, exactly 3 add calls; clearSelectedRecommendations was
called first to drop stale state.
(d) Tiebreaker pin — single cell with 3 variants whose
`(savings, upfront_cost, term)` produce known amortized values
($200, $300, $400). The middle variant wins ($400 effective)
despite the high-upfront variant having $1200 raw savings,
proving the amortization is actually computed.
`tsc --noEmit` clean. `npx jest --testPathPatterns="recommendations"`
exit 0. `npm run build` (webpack production) exit 0.1 parent c84fd02 commit 12c54d9
2 files changed
Lines changed: 236 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2023 | 2023 | | |
2024 | 2024 | | |
2025 | 2025 | | |
| 2026 | + | |
| 2027 | + | |
| 2028 | + | |
| 2029 | + | |
| 2030 | + | |
| 2031 | + | |
| 2032 | + | |
| 2033 | + | |
| 2034 | + | |
| 2035 | + | |
| 2036 | + | |
| 2037 | + | |
| 2038 | + | |
| 2039 | + | |
| 2040 | + | |
| 2041 | + | |
| 2042 | + | |
| 2043 | + | |
| 2044 | + | |
| 2045 | + | |
| 2046 | + | |
| 2047 | + | |
| 2048 | + | |
| 2049 | + | |
| 2050 | + | |
| 2051 | + | |
| 2052 | + | |
| 2053 | + | |
| 2054 | + | |
| 2055 | + | |
| 2056 | + | |
| 2057 | + | |
| 2058 | + | |
| 2059 | + | |
| 2060 | + | |
| 2061 | + | |
| 2062 | + | |
| 2063 | + | |
| 2064 | + | |
| 2065 | + | |
| 2066 | + | |
| 2067 | + | |
| 2068 | + | |
| 2069 | + | |
| 2070 | + | |
| 2071 | + | |
| 2072 | + | |
| 2073 | + | |
| 2074 | + | |
| 2075 | + | |
| 2076 | + | |
| 2077 | + | |
| 2078 | + | |
| 2079 | + | |
| 2080 | + | |
| 2081 | + | |
| 2082 | + | |
| 2083 | + | |
| 2084 | + | |
| 2085 | + | |
| 2086 | + | |
| 2087 | + | |
| 2088 | + | |
| 2089 | + | |
| 2090 | + | |
| 2091 | + | |
| 2092 | + | |
| 2093 | + | |
| 2094 | + | |
| 2095 | + | |
| 2096 | + | |
| 2097 | + | |
| 2098 | + | |
| 2099 | + | |
| 2100 | + | |
| 2101 | + | |
| 2102 | + | |
| 2103 | + | |
| 2104 | + | |
| 2105 | + | |
| 2106 | + | |
| 2107 | + | |
| 2108 | + | |
| 2109 | + | |
| 2110 | + | |
| 2111 | + | |
| 2112 | + | |
| 2113 | + | |
| 2114 | + | |
| 2115 | + | |
| 2116 | + | |
| 2117 | + | |
| 2118 | + | |
| 2119 | + | |
| 2120 | + | |
| 2121 | + | |
| 2122 | + | |
| 2123 | + | |
| 2124 | + | |
| 2125 | + | |
| 2126 | + | |
| 2127 | + | |
| 2128 | + | |
| 2129 | + | |
| 2130 | + | |
| 2131 | + | |
| 2132 | + | |
| 2133 | + | |
| 2134 | + | |
| 2135 | + | |
| 2136 | + | |
| 2137 | + | |
| 2138 | + | |
| 2139 | + | |
| 2140 | + | |
| 2141 | + | |
| 2142 | + | |
| 2143 | + | |
| 2144 | + | |
| 2145 | + | |
| 2146 | + | |
| 2147 | + | |
| 2148 | + | |
| 2149 | + | |
| 2150 | + | |
| 2151 | + | |
| 2152 | + | |
| 2153 | + | |
| 2154 | + | |
| 2155 | + | |
| 2156 | + | |
| 2157 | + | |
| 2158 | + | |
| 2159 | + | |
| 2160 | + | |
| 2161 | + | |
| 2162 | + | |
| 2163 | + | |
| 2164 | + | |
| 2165 | + | |
| 2166 | + | |
| 2167 | + | |
| 2168 | + | |
| 2169 | + | |
| 2170 | + | |
| 2171 | + | |
| 2172 | + | |
| 2173 | + | |
| 2174 | + | |
| 2175 | + | |
| 2176 | + | |
| 2177 | + | |
| 2178 | + | |
| 2179 | + | |
| 2180 | + | |
| 2181 | + | |
| 2182 | + | |
| 2183 | + | |
| 2184 | + | |
| 2185 | + | |
| 2186 | + | |
| 2187 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
142 | 142 | | |
143 | 143 | | |
144 | 144 | | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
145 | 194 | | |
146 | 195 | | |
147 | 196 | | |
| |||
1930 | 1979 | | |
1931 | 1980 | | |
1932 | 1981 | | |
1933 | | - | |
| 1982 | + | |
| 1983 | + | |
| 1984 | + | |
| 1985 | + | |
| 1986 | + | |
| 1987 | + | |
| 1988 | + | |
| 1989 | + | |
| 1990 | + | |
| 1991 | + | |
1934 | 1992 | | |
1935 | 1993 | | |
1936 | 1994 | | |
| |||
1942 | 2000 | | |
1943 | 2001 | | |
1944 | 2002 | | |
| 2003 | + | |
| 2004 | + | |
| 2005 | + | |
| 2006 | + | |
| 2007 | + | |
1945 | 2008 | | |
1946 | 2009 | | |
1947 | 2010 | | |
1948 | 2011 | | |
1949 | 2012 | | |
| 2013 | + | |
| 2014 | + | |
| 2015 | + | |
| 2016 | + | |
| 2017 | + | |
| 2018 | + | |
| 2019 | + | |
| 2020 | + | |
| 2021 | + | |
| 2022 | + | |
1950 | 2023 | | |
1951 | 2024 | | |
1952 | 2025 | | |
| |||
0 commit comments