From f3d195d4886d3f540a95e9e321c160f8a522963d Mon Sep 17 00:00:00 2001 From: Jean du Plessis Date: Mon, 20 Jul 2026 20:42:01 +0200 Subject: [PATCH 1/3] fix(cost-insights): show spend in partial chart periods --- .../overview/SpendEvidenceCard.tsx | 31 ++++++++++++------- .../web/src/components/cost-insights/types.ts | 4 +-- .../src/lib/cost-insights/presenter.test.ts | 6 ++-- apps/web/src/lib/cost-insights/presenter.ts | 6 +--- 4 files changed, 26 insertions(+), 21 deletions(-) diff --git a/apps/web/src/components/cost-insights/overview/SpendEvidenceCard.tsx b/apps/web/src/components/cost-insights/overview/SpendEvidenceCard.tsx index 5a4f9f3d94..322e20443d 100644 --- a/apps/web/src/components/cost-insights/overview/SpendEvidenceCard.tsx +++ b/apps/web/src/components/cost-insights/overview/SpendEvidenceCard.tsx @@ -46,7 +46,7 @@ export function SpendEvidenceCard({ '90d': 'Last 90 days', }[range]; const highest = evidence - .filter(point => point.coverage === 'complete') + .filter(point => point.coverage !== 'unavailable') .reduce<(typeof evidence)[number] | undefined>((currentHighest, point) => { if (!currentHighest) return point; const currentTotal = currentHighest.variableUsd + currentHighest.scheduledUsd; @@ -105,7 +105,7 @@ export function SpendEvidenceCard({ ? `${rangeLabel}: spend total unavailable because one or more periods have incomplete coverage.` : `${rangeLabel}: ${money(completeTotal)} total.`}{' '} {highest - ? `Highest complete period was ${highest.label} at ${money(highest.variableUsd + highest.scheduledUsd)}.` + ? `Highest period with spend evidence was ${highest.label} at ${money(highest.variableUsd + highest.scheduledUsd)}.` : ''}

@@ -135,9 +135,9 @@ export function SpendEvidenceCard({ const isPeak = highest !== undefined && point.periodStart === highest.periodStart; const showTick = index % tickStride === 0 || index === evidence.length - 1; const accessibilityLabel = - point.coverage === 'complete' - ? `${point.label}: ${money(pointTotal)} total, ${money(point.variableUsd)} usage-based, ${money(point.scheduledUsd)} scheduled` - : `${point.label}: spend data unavailable, ${point.coveredHours} of ${point.totalHours} hours covered`; + point.coverage === 'unavailable' + ? `${point.label}: spend data unavailable, ${point.coveredHours} of ${point.totalHours} hours covered` + : `${point.label}: ${point.coverage === 'partial' ? 'at least ' : ''}${money(pointTotal)} total, ${money(point.variableUsd)} usage-based, ${money(point.scheduledUsd)} scheduled${point.coverage === 'partial' ? `, ${point.coveredHours} of ${point.totalHours} hours covered` : ''}`; return ( @@ -153,7 +153,7 @@ export function SpendEvidenceCard({ onKeyDown={event => handleBarKeyDown(event, index)} > - {isPeak && point.coverage === 'complete' && ( + {isPeak && point.coverage !== 'unavailable' && ( {money(pointTotal)} @@ -163,16 +163,18 @@ export function SpendEvidenceCard({ - {point.coverage === 'complete' && ( + {point.coverage !== 'unavailable' && ( <>

{point.label}
- {point.coverage === 'complete' ? ( + {point.coverage !== 'unavailable' ? (
-
Total
+
+ {point.coverage === 'partial' ? 'Known spend' : 'Total'} +
{money(pointTotal)}
@@ -218,6 +222,11 @@ export function SpendEvidenceCard({
{money(point.scheduledUsd)}
+ {point.coverage === 'partial' && ( +
+ Partial coverage: {point.coveredHours} of {point.totalHours} hours. +
+ )}
) : (

diff --git a/apps/web/src/components/cost-insights/types.ts b/apps/web/src/components/cost-insights/types.ts index 86ddc192a0..bec83991b5 100644 --- a/apps/web/src/components/cost-insights/types.ts +++ b/apps/web/src/components/cost-insights/types.ts @@ -30,12 +30,12 @@ type SpendEvidencePointBase = { export type SpendEvidencePoint = | (SpendEvidencePointBase & { - coverage: 'complete'; + coverage: 'complete' | 'partial'; variableUsd: number; scheduledUsd: number; }) | (SpendEvidencePointBase & { - coverage: 'partial' | 'unavailable'; + coverage: 'unavailable'; variableUsd: null; scheduledUsd: null; }); diff --git a/apps/web/src/lib/cost-insights/presenter.test.ts b/apps/web/src/lib/cost-insights/presenter.test.ts index 75bbbf0033..ae94b38142 100644 --- a/apps/web/src/lib/cost-insights/presenter.test.ts +++ b/apps/web/src/lib/cost-insights/presenter.test.ts @@ -555,7 +555,7 @@ describe('Cost Insights presenter', () => { ).toThrow('Covered Cost Insights evidence must include both spend categories.'); }); - it('marks aggregate evidence partial without exposing an understated covered subtotal', () => { + it('shows known spend in partial aggregate evidence without presenting it as a complete total', () => { const points = [ { hourStart: '2026-06-25T22:00:00.000Z', @@ -594,8 +594,8 @@ describe('Cost Insights presenter', () => { coverage: 'partial', coveredHours: 1, totalHours: 2, - variableUsd: null, - scheduledUsd: null, + variableUsd: 2, + scheduledUsd: 1, }, { label: 'Jun 26', diff --git a/apps/web/src/lib/cost-insights/presenter.ts b/apps/web/src/lib/cost-insights/presenter.ts index da46ab7cf5..783dcbc2fc 100644 --- a/apps/web/src/lib/cost-insights/presenter.ts +++ b/apps/web/src/lib/cost-insights/presenter.ts @@ -225,10 +225,6 @@ function presentEvidenceBucket(points: OwnerHourlySpend[]): SpendEvidencePoint { if (covered.length === 0) { return { ...common, coverage: 'unavailable', variableUsd: null, scheduledUsd: null }; } - if (covered.length !== points.length) { - return { ...common, coverage: 'partial', variableUsd: null, scheduledUsd: null }; - } - let variableMicrodollars = 0; let scheduledMicrodollars = 0; for (const point of covered) { @@ -238,7 +234,7 @@ function presentEvidenceBucket(points: OwnerHourlySpend[]): SpendEvidencePoint { } return { ...common, - coverage: 'complete', + coverage: covered.length === points.length ? 'complete' : 'partial', variableUsd: microdollarsToUsd(variableMicrodollars), scheduledUsd: microdollarsToUsd(scheduledMicrodollars), }; From b856ecaf6c95daab3226f61fb028c0e069e8ed33 Mon Sep 17 00:00:00 2001 From: Jean du Plessis Date: Mon, 20 Jul 2026 20:46:18 +0200 Subject: [PATCH 2/3] fix(cost-insights): clarify anomaly spend status --- apps/web/src/lib/cost-insights/presenter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/lib/cost-insights/presenter.ts b/apps/web/src/lib/cost-insights/presenter.ts index 783dcbc2fc..442a2723b3 100644 --- a/apps/web/src/lib/cost-insights/presenter.ts +++ b/apps/web/src/lib/cost-insights/presenter.ts @@ -468,7 +468,7 @@ function buildMetrics(params: { params.currentHourVariableMicrodollars === null ? 'Current-hour spend evidence is unavailable' : params.currentHourVariableMicrodollars >= params.anomalyThresholdMicrodollars - ? 'Above current alert level' + ? 'Unusually high for this account' : `Typical hour: ${money(params.anomalyBaselineMicrodollars)}`, tone: params.currentHourVariableMicrodollars !== null && From 17e9bf2673a41bd7708e762b8fd6a4209bbc7657 Mon Sep 17 00:00:00 2001 From: Jean du Plessis Date: Mon, 20 Jul 2026 20:53:46 +0200 Subject: [PATCH 3/3] fix(cost-insights): preserve evidence discriminants --- apps/web/src/components/cost-insights/types.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/cost-insights/types.ts b/apps/web/src/components/cost-insights/types.ts index bec83991b5..05646a675d 100644 --- a/apps/web/src/components/cost-insights/types.ts +++ b/apps/web/src/components/cost-insights/types.ts @@ -30,7 +30,12 @@ type SpendEvidencePointBase = { export type SpendEvidencePoint = | (SpendEvidencePointBase & { - coverage: 'complete' | 'partial'; + coverage: 'complete'; + variableUsd: number; + scheduledUsd: number; + }) + | (SpendEvidencePointBase & { + coverage: 'partial'; variableUsd: number; scheduledUsd: number; })