From 0c3462a4148435737ec68da90c134d621017679a Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Sun, 25 Oct 2026 13:59:42 +0100 Subject: [PATCH 1/2] fix(frontend): show the server's reason for each excluded RI on the exchange page Co-Authored-By: Claude Opus 5.5 --- frontend/src/__tests__/riexchange.test.ts | 30 ++++++++++++++++++++++- frontend/src/riexchange.ts | 11 +++++++-- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/frontend/src/__tests__/riexchange.test.ts b/frontend/src/__tests__/riexchange.test.ts index 671fe1af..bdf12877 100644 --- a/frontend/src/__tests__/riexchange.test.ts +++ b/frontend/src/__tests__/riexchange.test.ts @@ -1594,12 +1594,40 @@ describe('excluded RIs notice', () => { await loadReshapeRecommendations(); const notice = document.getElementById('ri-exchange-recommendations-excluded'); - expect(notice?.textContent).toContain('2 convertible RIs were left out because AWS returned no term length'); + expect(notice?.textContent).toContain('2 convertible RIs were left out because no term'); expect(notice?.textContent).toContain('ri-0'); expect(notice?.textContent).toContain(''); expect(notice?.querySelector('img')).toBeNull(); }); + it('shows each excluded RI\'s server reason, grouped by reason', () => { + renderExcludedRIsNotice([ + { ri_id: 'ri-dur', reason: 'non-positive duration' }, + { ri_id: 'ri-price', reason: 'no pricing available' }, + ]); + const text = document.getElementById('ri-exchange-recommendations-excluded')?.textContent ?? ''; + expect(text).toContain('1 convertible RI was left out because non-positive duration: ri-dur'); + expect(text).toContain('1 convertible RI was left out because no pricing available: ri-price'); + expect(text).not.toContain('no term length'); + }); + + it('falls back to the term-length text when reason is empty or missing', () => { + renderExcludedRIsNotice([ + { ri_id: 'ri-a', reason: '' }, + { ri_id: 'ri-b' } as unknown as { ri_id: string; reason: string }, + ]); + expect(document.getElementById('ri-exchange-recommendations-excluded')?.textContent).toContain( + '2 convertible RIs were left out because AWS returned no term length: ri-a, ri-b', + ); + }); + + it('renders a reason containing HTML special characters as text', () => { + renderExcludedRIsNotice([{ ri_id: 'ri-x', reason: 'bad & "quoted"' }]); + const notice = document.getElementById('ri-exchange-recommendations-excluded'); + expect(notice?.textContent).toContain('bad & "quoted"'); + expect(notice?.querySelector('b')).toBeNull(); + }); + it('shows no notice when excluded_ris is empty or absent, and clears a stale one', () => { renderExcludedRIsNotice([{ ri_id: 'ri-0', reason: 'x' }]); expect(document.getElementById('ri-exchange-recommendations-excluded')?.textContent).toContain('1 convertible RI was left out'); diff --git a/frontend/src/riexchange.ts b/frontend/src/riexchange.ts index 2d973389..b7b414b0 100644 --- a/frontend/src/riexchange.ts +++ b/frontend/src/riexchange.ts @@ -588,9 +588,16 @@ export function renderExcludedRIsNotice(excluded: ExcludedRI[] | undefined): voi notice.className = ''; return; } - const n = excluded.length; + const byReason = new Map(); + for (const e of excluded) { + const reason = e.reason || 'AWS returned no term length'; + byReason.set(reason, [...(byReason.get(reason) ?? []), e.ri_id]); + } notice.className = 'freshness-banner warning'; - notice.textContent = `${n} convertible RI${n === 1 ? ' was' : 's were'} left out because AWS returned no term length: ${excluded.map((e) => e.ri_id).join(', ')}`; + // textContent keeps server-provided reasons and ids inert. + notice.textContent = [...byReason] + .map(([reason, ids]) => `${ids.length} convertible RI${ids.length === 1 ? ' was' : 's were'} left out because ${reason}: ${ids.join(', ')}`) + .join('; '); } /** From 1246f2fd5c6642abbbcfa08d30fcfea9374ec51c Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Sun, 25 Oct 2026 15:33:57 +0100 Subject: [PATCH 2/2] perf(frontend): append excluded RI ids in place when grouping by reason The grouping copied the growing id list for every RI, which is quadratic when many RIs share one reason. Push to the existing list instead. Co-Authored-By: Claude Opus 5.5 --- frontend/src/riexchange.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/riexchange.ts b/frontend/src/riexchange.ts index b7b414b0..d9340edb 100644 --- a/frontend/src/riexchange.ts +++ b/frontend/src/riexchange.ts @@ -591,7 +591,9 @@ export function renderExcludedRIsNotice(excluded: ExcludedRI[] | undefined): voi const byReason = new Map(); for (const e of excluded) { const reason = e.reason || 'AWS returned no term length'; - byReason.set(reason, [...(byReason.get(reason) ?? []), e.ri_id]); + const ids = byReason.get(reason); + if (ids) ids.push(e.ri_id); + else byReason.set(reason, [e.ri_id]); } notice.className = 'freshness-banner warning'; // textContent keeps server-provided reasons and ids inert.