From d3b5416c0d50d362d886c661e2aadce193e07090 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Apr 2026 12:09:07 +0000 Subject: [PATCH 1/5] feat: combine bandwidth and realtime bandwidth into a single row with breakdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merges the standalone "Realtime bandwidth" row into the "Bandwidth" row. The progress bar now renders two color-coded segments — regular bandwidth and realtime bandwidth — with hover tooltips showing the realtime value. --- .../billing/planSummary.svelte | 76 +++++++++++++++---- 1 file changed, 62 insertions(+), 14 deletions(-) diff --git a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte index 84683f8c6..5f4e08b1d 100644 --- a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte +++ b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte @@ -148,6 +148,50 @@ ]; } + function createBandwidthProgressData( + regularBytes: number, + realtimeBytes: number, + maxGB: number + ): Array<{ size: number; color: string; tooltip?: { title: string; label: string } }> { + if (maxGB <= 0) return []; + + const maxBytes = maxGB * 1000 * 1000 * 1000; + const total = regularBytes + realtimeBytes; + const percentage = Math.min((total / maxBytes) * 100, 100); + const regularSize = humanFileSize(regularBytes); + const realtimeSize = humanFileSize(realtimeBytes); + + const segments: Array<{ + size: number; + color: string; + tooltip?: { title: string; label: string }; + }> = []; + + if (regularBytes > 0) { + segments.push({ + size: regularBytes, + color: 'var(--bgcolor-neutral-invert)', + tooltip: { + title: `${percentage.toFixed(0)}% used`, + label: `Bandwidth: ${regularSize.value} ${regularSize.unit}` + } + }); + } + + if (realtimeBytes > 0) { + segments.push({ + size: realtimeBytes, + color: 'var(--bgcolor-neutral-invert-weak)', + tooltip: { + title: 'Realtime bandwidth', + label: `${realtimeSize.value} ${realtimeSize.unit}` + } + }); + } + + return segments; + } + function getResource(resources: Array | undefined, resourceId: string) { return resources?.find((resource) => resource.resourceId === resourceId); } @@ -282,9 +326,16 @@ const projects = (currentAggregation?.breakdown || []).map((projectData) => { const resources = projectData.resources || []; const bandwidth = getResource(resources, 'bandwidth'); + const realtimeBandwidth = getResource(resources, 'realtimeBandwidth'); const storage = getResource(resources, 'storage'); const authPhone = getResource(resources, 'authPhone'); + const bandwidthValue = bandwidth?.value || 0; + const realtimeBandwidthValue = realtimeBandwidth?.value || 0; + const combinedBandwidthValue = bandwidthValue + realtimeBandwidthValue; + const combinedBandwidthAmount = + (bandwidth?.amount || 0) + (realtimeBandwidth?.amount || 0); + return { id: `project-${projectData.$id}`, expandable: true, @@ -302,14 +353,20 @@ label: 'Bandwidth', resource: bandwidth, planLimit: currentPlan?.bandwidth, - usageFormatter: ({ value, planLimit, hasLimit }) => + usageFormatter: ({ planLimit, hasLimit }) => formatBandwidthUsage( - value, + combinedBandwidthValue, hasLimit ? (planLimit ?? undefined) : undefined ), - priceFormatter: ({ amount }) => formatCurrency(amount), - progressFactory: ({ value, planLimit, hasLimit }) => - hasLimit ? createStorageProgressData(value, planLimit || 0) : [], + priceFormatter: () => formatCurrency(combinedBandwidthAmount), + progressFactory: ({ planLimit, hasLimit }) => + hasLimit + ? createBandwidthProgressData( + bandwidthValue, + realtimeBandwidthValue, + planLimit || 0 + ) + : [], maxFactory: ({ planLimit, hasLimit }) => hasLimit ? (planLimit || 0) * 1000 * 1000 * 1000 : null }), @@ -383,15 +440,6 @@ getResource(resources, 'realtimeMessages'), currentPlan?.realtimeMessages ), - createRow({ - id: 'realtime-bandwidth', - label: 'Realtime bandwidth', - resource: getResource(resources, 'realtimeBandwidth'), - usageFormatter: ({ value }) => - humanFileSize(value).value + humanFileSize(value).unit, - priceFormatter: ({ amount }) => formatCurrency(amount), - includeProgress: false - }), createRow({ id: 'sms', label: 'Phone OTP', From b7e558545e3f140aa6f863618ca16e88dbbe2e9c Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 3 May 2026 07:03:27 +0000 Subject: [PATCH 2/5] fix(billing): stop double-counting realtime bandwidth into bandwidth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend already rolls realtimeBandwidth value + amount into the bandwidth resource, so the previous commit was summing them again (value + amount), inflating both the displayed total and the priced amount, and over-driving the progress bar. Use bandwidth?.value / bandwidth?.amount directly (already include realtime). For the multi-segment progress bar, carve the realtime slice out of the total instead of summing — regular = total - realtime, realtime = realtime — so the segments add to the actual bandwidth total. Same neutral colour family as before, no multi-colour added. Clamps realtime to [0, total] in case of dirty data. --- .../billing/planSummary.svelte | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte index 5f4e08b1d..11e25948f 100644 --- a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte +++ b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte @@ -149,17 +149,20 @@ } function createBandwidthProgressData( - regularBytes: number, + totalBytes: number, realtimeBytes: number, maxGB: number ): Array<{ size: number; color: string; tooltip?: { title: string; label: string } }> { if (maxGB <= 0) return []; const maxBytes = maxGB * 1000 * 1000 * 1000; - const total = regularBytes + realtimeBytes; - const percentage = Math.min((total / maxBytes) * 100, 100); - const regularSize = humanFileSize(regularBytes); - const realtimeSize = humanFileSize(realtimeBytes); + const percentage = Math.min((totalBytes / maxBytes) * 100, 100); + // Backend already rolls realtimeBytes into totalBytes; carve the realtime + // slice out of the total instead of summing them. Clamp in case of dirty data. + const realtimePortion = Math.max(0, Math.min(realtimeBytes, totalBytes)); + const regularPortion = Math.max(0, totalBytes - realtimePortion); + const regularSize = humanFileSize(regularPortion); + const realtimeSize = humanFileSize(realtimePortion); const segments: Array<{ size: number; @@ -167,9 +170,9 @@ tooltip?: { title: string; label: string }; }> = []; - if (regularBytes > 0) { + if (regularPortion > 0) { segments.push({ - size: regularBytes, + size: regularPortion, color: 'var(--bgcolor-neutral-invert)', tooltip: { title: `${percentage.toFixed(0)}% used`, @@ -178,9 +181,9 @@ }); } - if (realtimeBytes > 0) { + if (realtimePortion > 0) { segments.push({ - size: realtimeBytes, + size: realtimePortion, color: 'var(--bgcolor-neutral-invert-weak)', tooltip: { title: 'Realtime bandwidth', @@ -330,11 +333,11 @@ const storage = getResource(resources, 'storage'); const authPhone = getResource(resources, 'authPhone'); + // Backend already rolls realtimeBandwidth value + amount into bandwidth; + // use the bandwidth resource directly and pass realtime only as a slice + // for the progress segment, never as an additive total. const bandwidthValue = bandwidth?.value || 0; const realtimeBandwidthValue = realtimeBandwidth?.value || 0; - const combinedBandwidthValue = bandwidthValue + realtimeBandwidthValue; - const combinedBandwidthAmount = - (bandwidth?.amount || 0) + (realtimeBandwidth?.amount || 0); return { id: `project-${projectData.$id}`, @@ -353,16 +356,16 @@ label: 'Bandwidth', resource: bandwidth, planLimit: currentPlan?.bandwidth, - usageFormatter: ({ planLimit, hasLimit }) => + usageFormatter: ({ value, planLimit, hasLimit }) => formatBandwidthUsage( - combinedBandwidthValue, + value, hasLimit ? (planLimit ?? undefined) : undefined ), - priceFormatter: () => formatCurrency(combinedBandwidthAmount), - progressFactory: ({ planLimit, hasLimit }) => + priceFormatter: ({ amount }) => formatCurrency(amount), + progressFactory: ({ value, planLimit, hasLimit }) => hasLimit ? createBandwidthProgressData( - bandwidthValue, + value, realtimeBandwidthValue, planLimit || 0 ) From 0de30dad10ecdfdc37e322cf214530ec828150c0 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 3 May 2026 07:04:34 +0000 Subject: [PATCH 3/5] fix(billing): skip realtime segment when bandwidth < realtimeBandwidth A handful of legacy orgs have realtime bandwidth tracked separately (not yet billed and not added into the bandwidth resource). For them realtimeBytes > totalBytes, which would otherwise overflow the bar or collapse the regular segment to zero. Detect that case and render bandwidth as a single segment instead, preserving today's behaviour for those orgs. --- .../billing/planSummary.svelte | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte index 11e25948f..e611e8fe9 100644 --- a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte +++ b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte @@ -157,9 +157,12 @@ const maxBytes = maxGB * 1000 * 1000 * 1000; const percentage = Math.min((totalBytes / maxBytes) * 100, 100); - // Backend already rolls realtimeBytes into totalBytes; carve the realtime - // slice out of the total instead of summing them. Clamp in case of dirty data. - const realtimePortion = Math.max(0, Math.min(realtimeBytes, totalBytes)); + // Backend rolls realtimeBytes into totalBytes for billed orgs, so realtime + // is a slice of the total. A handful of legacy orgs still have realtime + // tracked separately (not billed yet) — for them realtime > total. In that + // case skip the breakdown and render the bandwidth as a single segment. + const showRealtimeSegment = realtimeBytes > 0 && realtimeBytes <= totalBytes; + const realtimePortion = showRealtimeSegment ? realtimeBytes : 0; const regularPortion = Math.max(0, totalBytes - realtimePortion); const regularSize = humanFileSize(regularPortion); const realtimeSize = humanFileSize(realtimePortion); From f4a67818447ac275f61a86f4871f9571b47c1bcc Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 3 May 2026 07:07:54 +0000 Subject: [PATCH 4/5] fix(billing): surface separate Realtime bandwidth row for legacy orgs A handful of orgs still have realtime bandwidth tracked outside the bandwidth resource (not billed/rolled in yet). Earlier commit hid their realtime value entirely once the breakdown bar suppressed it. Render a standalone informational Realtime bandwidth row only when realtimeBandwidthValue > bandwidthValue, matching today's pre-merge behaviour for those orgs while keeping the modern path one row. --- .../billing/planSummary.svelte | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte index e611e8fe9..d48b95100 100644 --- a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte +++ b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte @@ -376,6 +376,25 @@ maxFactory: ({ planLimit, hasLimit }) => hasLimit ? (planLimit || 0) * 1000 * 1000 * 1000 : null }), + // Legacy orgs still track realtime bandwidth separately (not yet + // rolled into the bandwidth resource). Surface it as its own row + // so the value is visible even though it can't fit as a slice + // of the bandwidth bar. + ...(realtimeBandwidthValue > bandwidthValue + ? [ + createRow({ + id: 'realtime-bandwidth', + label: 'Realtime bandwidth', + resource: realtimeBandwidth, + usageFormatter: ({ value }) => { + const size = humanFileSize(value); + return `${size.value} ${size.unit}`; + }, + priceFormatter: ({ amount }) => formatCurrency(amount), + includeProgress: false + }) + ] + : []), // standard resources (numeric) createResourceRow( 'users', From 3d6844b3563264450ac397fe127a23aec7c7ff0e Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 3 May 2026 07:15:36 +0000 Subject: [PATCH 5/5] chore: drop explanatory comments per repo convention --- .../billing/planSummary.svelte | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte index d48b95100..cc56a826c 100644 --- a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte +++ b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte @@ -157,10 +157,6 @@ const maxBytes = maxGB * 1000 * 1000 * 1000; const percentage = Math.min((totalBytes / maxBytes) * 100, 100); - // Backend rolls realtimeBytes into totalBytes for billed orgs, so realtime - // is a slice of the total. A handful of legacy orgs still have realtime - // tracked separately (not billed yet) — for them realtime > total. In that - // case skip the breakdown and render the bandwidth as a single segment. const showRealtimeSegment = realtimeBytes > 0 && realtimeBytes <= totalBytes; const realtimePortion = showRealtimeSegment ? realtimeBytes : 0; const regularPortion = Math.max(0, totalBytes - realtimePortion); @@ -336,9 +332,6 @@ const storage = getResource(resources, 'storage'); const authPhone = getResource(resources, 'authPhone'); - // Backend already rolls realtimeBandwidth value + amount into bandwidth; - // use the bandwidth resource directly and pass realtime only as a slice - // for the progress segment, never as an additive total. const bandwidthValue = bandwidth?.value || 0; const realtimeBandwidthValue = realtimeBandwidth?.value || 0; @@ -376,10 +369,6 @@ maxFactory: ({ planLimit, hasLimit }) => hasLimit ? (planLimit || 0) * 1000 * 1000 * 1000 : null }), - // Legacy orgs still track realtime bandwidth separately (not yet - // rolled into the bandwidth resource). Surface it as its own row - // so the value is visible even though it can't fit as a slice - // of the bandwidth bar. ...(realtimeBandwidthValue > bandwidthValue ? [ createRow({