-
Notifications
You must be signed in to change notification settings - Fork 185
Feat: New Billing UI changes #2249
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
…project-changes-2
…project-changes-2
ConsoleProject ID: Sites (2)
Note You can use Avatars API to generate QR code for any text or URLs. |
…rojects-ui-change
…/github.com/appwrite/console into feat-SER-204-New-Archive-projects-ui-change
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (7)
src/routes/(console)/organization-[organization]/billing/planSummary.svelte (7)
87-96
: TypestorageTotal
input.- function storageTotal(p: any): number { + function storageTotal(p: Breakdown | undefined): number {
198-217
: Bandwidth units now consistent (bytes↔GB). LGTM.
22-23
: Replaceany
props with concrete types (keep shapes minimal but accurate).Type the inputs to satisfy ESLint and prevent downstream bugs.
-export let organizationUsage: any = undefined; -export let usageProjects: Record<string, any> = {}; +type UsageProjectMeta = { name?: string; region?: string }; +type Resource = { resourceId?: string; value?: number; amount?: number }; +type Breakdown = { resources?: Record<string, Resource> | Resource[] }; +type OrganizationProjectUsage = { + projectId: string; + storage?: number; executions?: number; executionsMBSeconds?: number; bandwidth?: number; + databasesReads?: number; databasesWrites?: number; users?: number; + authPhoneTotal?: number; authPhoneEstimate?: number; + breakdown?: Breakdown; +}; +type OrganizationUsage = { projects?: OrganizationProjectUsage[] }; + +export let organizationUsage: OrganizationUsage | undefined = undefined; +export let usageProjects: Record<string, UsageProjectMeta> = {};
57-72
: Strongly type helpers:resourceEntry/valueOf/amountOf
.Avoid
any
and add numeric coercion to blockNaN
.- function resourceEntry(p: any, id: string): any { + function resourceEntry(p: Breakdown | undefined, id: string): Resource | undefined { const res = p?.resources; if (!res) return undefined; if (Array.isArray(res)) { - return res.find((r: any) => r.resourceId === id); + return (res as Resource[]).find((r) => r.resourceId === id); } - return res[id]; + return (res as Record<string, Resource>)[id]; } - function valueOf(p: any, id: string): number { + function valueOf(p: Breakdown | undefined, id: string): number { const entry = resourceEntry(p, id); - return (entry?.value ?? 0) as number; + return Number(entry?.value ?? 0); } - function amountOf(p: any, id: string): number { + function amountOf(p: Breakdown | undefined, id: string): number { const entry = resourceEntry(p, id); - return (entry?.amount ?? 0) as number; + return Number(entry?.amount ?? 0); }
74-86
: Guard divide-by-zero and typeplanPricing
incalculateResourcePrice
.Prevents
Infinity/NaN
whenvalue
orprice
is 0/undefined.- function calculateResourcePrice(usage: number, planLimit: number, planPricing: any): number { - if (!planPricing || usage <= planLimit) { + type PriceTier = { value: number; price: number }; + function calculateResourcePrice( + usage: number, + planLimit: number, + planPricing?: PriceTier | null + ): number { + if (!planPricing || usage <= planLimit) { return 0; - } - - const overage = usage - planLimit; - const unitsPerPrice = planPricing.value; - const pricePerUnit = planPricing.price; + } + const overage = usage - planLimit; + const unitsPerPrice = Number(planPricing.value) || 0; + const pricePerUnit = Number(planPricing.price) || 0; + if (unitsPerPrice <= 0 || pricePerUnit <= 0) return 0; const overageUnits = Math.ceil(overage / unitsPerPrice); return overageUnits * pricePerUnit; }
350-356
: Stop injecting HTML with{@html}
; render links structurally.Fixes XSS risk and brittle
.includes('<a href=')
checks.- { - id: `project-${project.projectId}-usage-details`, - cells: { - item: `<a href="${base}/project-${usageProjects[project.projectId]?.region || 'default'}-${project.projectId}/settings/usage" style="text-decoration: underline; color: var(--fgcolor-accent-neutral);">Usage details</a>`, - usage: '', - price: '' - } - } + { + id: `project-${project.projectId}-usage-details`, + href: `${base}/project-${usageProjects[project.projectId]?.region || 'default'}-${project.projectId}/settings/usage`, + cells: { item: 'Usage details', usage: '', price: '' } + }- {#if child.cells?.[col.id]?.includes('<a href=')} - {@html child.cells?.[col.id] ?? ''} + {#if col.id === 'item' && child.href} + <a href={child.href} style="text-decoration: underline; color: var(--fgcolor-accent-neutral);"> + {child.cells?.item} + </a>Also applies to: 454-456
375-386
: Compute totals from numeric fields; avoid parsing formatted currency.Parsing strings is locale-fragile and slower. Store a numeric
amount
per row/child and sum that.- $: totalAmount = billingData.reduce((sum, item) => { - let itemPrice = parseFloat(item.cells.price.replace(/[^0-9.-]+/g, '')); - if (isNaN(itemPrice)) itemPrice = 0; - - const childrenPrice = - item.children?.reduce((childSum, child) => { - let childPrice = parseFloat(child.cells.price.replace(/[^0-9.-]+/g, '')); - if (isNaN(childPrice)) childPrice = 0; - return childSum + childPrice; - }, 0) || 0; - return sum + itemPrice + childrenPrice; - }, 0); + $: totalAmount = billingData.reduce((sum, item) => { + const own = Number(item.amount ?? 0); + const children = item.children?.reduce((s, c) => s + Number(c.amount ?? 0), 0) ?? 0; + return sum + own + children; + }, 0);Additionally set
amount
when building rows (example pattern):// base plan amount: currentPlan?.price || 0, // per-project total amount: projectIdToBreakdown.get(project.projectId)?.amount || 0, // child example (Bandwidth) amount: calculateResourcePrice( (project.bandwidth || 0) / (1024 * 1024 * 1024), currentPlan?.bandwidth || 0, currentPlan?.usage?.bandwidth ),
🧹 Nitpick comments (1)
src/routes/(console)/organization-[organization]/billing/planSummary.svelte (1)
102-131
: Optional: progress color thresholds.Consider coloring >80% (warning) and >95% (danger) to signal nearing limits.
Also applies to: 133-155
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
src/routes/(console)/organization-[organization]/billing/planSummary.svelte
(4 hunks)
🧰 Additional context used
🪛 ESLint
src/routes/(console)/organization-[organization]/billing/planSummary.svelte
[error] 22-22: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 23-23: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 57-57: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 57-57: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 61-61: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 65-65: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 69-69: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 74-74: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 87-87: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
🔇 Additional comments (2)
src/routes/(console)/organization-[organization]/billing/planSummary.svelte (2)
300-306
: Request: verify “Storage” price units match the plan’susage.storage.value
.If
value
is “per GB” but usage is bytes/GB, ensure both use GB consistently (you convert correctly for price; good). Confirm the plan’s tier uses GB, not GiB.
317-337
: Fix GB-hours usage and limit
- Keep converting MB-seconds → GB-hours with
/1000/3600
per existing conventions.- Don’t use
currentPlan.executions
or a non-existentusage.GBHours.included
as the limit—GB-hours is pay-as-you-go with no free threshold. DisplayUnlimited
for the denominator and pass0
as the included limit tocalculateResourcePrice
.Likely an incorrect or invalid review comment.
item: 'Storage', | ||
usage: `${formatHumanSize(project.storage || 0)} / ${currentPlan?.storage?.toString() || '0'} GB`, | ||
price: formatCurrency( | ||
calculateResourcePrice( | ||
(project.storage || 0) / (1024 * 1024 * 1024), | ||
currentPlan?.storage || 0, | ||
currentPlan?.usage?.storage | ||
) | ||
) | ||
}, | ||
progressData: createStorageProgressData( | ||
project.storage || 0, | ||
currentPlan?.storage || 0 | ||
), | ||
maxValue: currentPlan?.storage ? currentPlan.storage * 1024 * 1024 * 1024 : 0 // Convert GB to bytes | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Show “Unlimited” when storage limit absent.
Avoid “/ 0 GB” in UI.
- usage: `${formatHumanSize(project.storage || 0)} / ${currentPlan?.storage?.toString() || '0'} GB`,
+ usage: `${formatHumanSize(project.storage || 0)} / ${currentPlan?.storage ? `${currentPlan.storage} GB` : 'Unlimited'}`,
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
item: 'Storage', | |
usage: `${formatHumanSize(project.storage || 0)} / ${currentPlan?.storage?.toString() || '0'} GB`, | |
price: formatCurrency( | |
calculateResourcePrice( | |
(project.storage || 0) / (1024 * 1024 * 1024), | |
currentPlan?.storage || 0, | |
currentPlan?.usage?.storage | |
) | |
) | |
}, | |
progressData: createStorageProgressData( | |
project.storage || 0, | |
currentPlan?.storage || 0 | |
), | |
maxValue: currentPlan?.storage ? currentPlan.storage * 1024 * 1024 * 1024 : 0 // Convert GB to bytes | |
}, | |
item: 'Storage', | |
usage: `${formatHumanSize(project.storage || 0)} / ${currentPlan?.storage ? `${currentPlan.storage} GB` : 'Unlimited'}`, | |
price: formatCurrency( | |
calculateResourcePrice( | |
(project.storage || 0) / (1024 * 1024 * 1024), | |
currentPlan?.storage || 0, | |
currentPlan?.usage?.storage | |
) | |
) | |
}, | |
progressData: createStorageProgressData( | |
project.storage || 0, | |
currentPlan?.storage || 0 | |
), | |
maxValue: currentPlan?.storage ? currentPlan.storage * 1024 * 1024 * 1024 : 0 // Convert GB to bytes | |
}, |
🤖 Prompt for AI Agents
In src/routes/(console)/organization-[organization]/billing/planSummary.svelte
around lines 299-314, the UI currently renders "/ 0 GB" and uses a zero maxValue
when the plan has no storage limit; change the usage display to show "Unlimited"
when currentPlan.storage is missing (e.g. usage:
`${formatHumanSize(project.storage || 0)} / ${currentPlan?.storage?.toString()
|| 'Unlimited'} GB`), pass undefined/null instead of 0 to
createStorageProgressData when there is no limit (so the progress helper can
treat it as unlimited), and set maxValue to undefined/null rather than 0 when
currentPlan.storage is absent (only convert to bytes when storage exists). This
ensures the UI shows "Unlimited" and progress/max handling reflects an absent
cap.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
♻️ Duplicate comments (1)
src/lib/stores/billing.ts (1)
165-172
: Fix unsafe access to addons.seats.limitIndexing after
plan?.['addons']
can throw ifaddons
is undefined. Use safe optional chaining.- // Free plan still has 1 member limit - return (plan?.['addons']['seats'] || [])['limit'] ?? 1; + // Free plan still has 1 member limit + return plan?.addons?.seats?.limit ?? 1;
🧹 Nitpick comments (5)
src/lib/components/billing/usageRates.svelte (1)
24-26
: Prefer Number.isFinite for clarity and robustnessMinor readability tweak; handles any non-finite value cleanly.
Apply:
- // Check for Infinity or very large numbers - const isUnlimited = count === Infinity || count >= Number.MAX_SAFE_INTEGER; + // Check for non-finite or very large numbers + const isUnlimited = !Number.isFinite(count) || count >= Number.MAX_SAFE_INTEGER; return isUnlimited ? 'Unlimited' : count || 0;src/routes/(console)/organization-[organization]/members/+page.svelte (2)
50-50
: Simplify boolean expressionEquivalent and a bit clearer.
- $: isButtonDisabled = isCloud ? isFreeWithMembers : false; + $: isButtonDisabled = isCloud && isFreeWithMembers;
92-97
: Tooltip else-branch is now unreachableWith the new gating, the button is only disabled on Free. Simplify tooltip text.
- {$organization?.billingPlan === BillingPlan.FREE - ? 'Upgrade to add more members' - : `You've reached the members limit for the ${ - tierToPlan($organization?.billingPlan)?.name - } plan`} + Upgrade to add more memberssrc/lib/stores/billing.ts (2)
262-268
: Redundant guard + switch case for 'members'You already early-return false for PRO/SCALE. Make the switch case reflect Free-only to avoid double logic.
- case 'members': // Only applies to Free plan now - return true; + case 'members': // Only applies to Free plan + return get(organization)?.billingPlan === BillingPlan.FREE;
285-291
: Use the provided tier when resolving limitsAvoid relying on global org plan; pass the tier explicitly for consistency.
- const limit = getServiceLimit(serviceId) || Infinity; + const limit = getServiceLimit(serviceId, plan) || Infinity;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (5)
src/lib/components/billing/planComparisonBox.svelte
(1 hunks)src/lib/components/billing/planExcess.svelte
(2 hunks)src/lib/components/billing/usageRates.svelte
(1 hunks)src/lib/stores/billing.ts
(5 hunks)src/routes/(console)/organization-[organization]/members/+page.svelte
(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/lib/stores/billing.ts (1)
src/lib/stores/organization.ts (3)
organization
(62-62)currentPlan
(63-63)members
(64-64)
🔇 Additional comments (3)
src/lib/components/billing/planComparisonBox.svelte (1)
93-93
: PRO plan: “Unlimited seats” — aligned with backend logicMatches getServiceLimit('members') returning Infinity for PRO/SCALE. No issues.
src/lib/stores/billing.ts (2)
242-242
: New store useNewPricingModal — LGTMDerived flag is straightforward and side-effect free.
401-403
: Members overflow calc — LGTMInfinity correctly treated as no overflow.
@@ -64,7 +70,7 @@ | |||
{#if excess?.members} | |||
<Table.Row.Base {root}> | |||
<Table.Cell {root}>Organization members</Table.Cell> | |||
<Table.Cell {root}>{plan.addons.seats.limit} members</Table.Cell> | |||
<Table.Cell {root}>{getServiceLimit('members', tier)} members</Table.Cell> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Show “Unlimited” instead of “Infinity members”
Format unlimited seats consistently with Usage Rates.
+ const formatSeatsLimit = (n: number) =>
+ (n === Infinity || n >= Number.MAX_SAFE_INTEGER) ? 'Unlimited' : abbreviateNumber(n);
- <Table.Cell {root}>{getServiceLimit('members', tier)} members</Table.Cell>
+ <Table.Cell {root}>{formatSeatsLimit(getServiceLimit('members', tier))} members</Table.Cell>
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
<Table.Cell {root}>{getServiceLimit('members', tier)} members</Table.Cell> | |
<script lang="ts"> | |
// … other imports and code … | |
// helper to format member limits, showing “Unlimited” for infinite values | |
const formatSeatsLimit = (n: number) => | |
(n === Infinity || n >= Number.MAX_SAFE_INTEGER) | |
? 'Unlimited' | |
: abbreviateNumber(n); | |
// … rest of script … | |
</script> | |
<!-- … elsewhere in the markup … --> | |
<Table.Cell {root}> | |
{formatSeatsLimit(getServiceLimit('members', tier))} members | |
</Table.Cell> |
🤖 Prompt for AI Agents
In src/lib/components/billing/planExcess.svelte around line 73, the cell
currently renders raw numeric output which shows "Infinity members" for
unlimited seats; update the rendering to format unlimited values as "Unlimited"
consistent with Usage Rates by getting the limit via getServiceLimit('members',
tier'), checking Number.isFinite(limit) (or limit !== Infinity), and outputting
"Unlimited" when non-finite otherwise rendering the numeric value followed by
"members"; ensure the formatted string is used in the Table.Cell.
if (!org.projects) return; | ||
if (org.projects.length > 0) return; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Early return likely disables project-limit checks for existing orgs
These guards short-circuit when the org already has projects, so you won’t warn when exceeding the Free projects limit later.
Proposed fix: remove the early returns.
- if (!org.projects) return;
- if (org.projects.length > 0) return;
To verify current call sites and intent, run:
🏁 Script executed:
#!/bin/bash
# Find and review all usages of checkForProjectsLimit and surrounding context
rg -nC3 '\bcheckForProjectsLimit\(' --type=ts --type=svelte
Length of output: 92
🏁 Script executed:
#!/bin/bash
# Retry searching for usages of checkForProjectsLimit in .ts and .svelte files
rg -nC3 '\bcheckForProjectsLimit\(' -g '*.ts' -g '*.svelte'
Length of output: 1070
🏁 Script executed:
#!/bin/bash
# Show context around checkForProjectsLimit definition
sed -n '320,370p' src/lib/stores/billing.ts
Length of output: 1630
Remove the org.projects
early-return guards in checkForProjectsLimit
These checks skip the limit logic for any org that already has projects—so you’ll never warn when they exceed their Free‐plan project quota. Remove them and rely solely on the passed orgProjectCount to enforce limits.
- if (!org.projects) return;
- if (org.projects.length > 0) return;
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
if (!org.projects) return; | |
if (org.projects.length > 0) return; |
🤖 Prompt for AI Agents
In src/lib/stores/billing.ts around lines 342 to 344, remove the two
early-return guards that check org.projects ("if (!org.projects) return;" and
"if (org.projects.length > 0) return;") so the function no longer skips limit
checks for orgs that already have projects; instead rely solely on the incoming
orgProjectCount parameter to compute and enforce the Free-plan project quota
(treat undefined orgProjectCount as 0 if necessary) and ensure the subsequent
logic uses orgProjectCount for warnings/enforcement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
♻️ Duplicate comments (3)
src/routes/(console)/organization-[organization]/billing/planSummary.svelte (3)
21-24
: Replaceany
props with concrete types (lint blocker).These
any
usages violate the lint rule and hide shape mismatches. Define local types and use them for the props.Apply this diff for the exports:
-export let currentAggregation: AggregationTeam | undefined = undefined; -export let organizationUsage: any = undefined; -export let usageProjects: Record<string, any> = {}; +export let currentAggregation: AggregationTeam | undefined = undefined; +export type UsageProjectMeta = { name?: string; region?: string }; +export type OrganizationUsage = { + projects?: Array<{ + projectId: string; + storage?: number; executions?: number; executionsMBSeconds?: number; bandwidth?: number; + databasesReads?: number; databasesWrites?: number; users?: number; + authPhoneTotal?: number; authPhoneEstimate?: number; + }>; +}; +export let organizationUsage: OrganizationUsage | undefined = undefined; +export let usageProjects: Record<string, UsageProjectMeta> = {};Add these helper types near the top (outside the diffed lines) to support other changes:
type Resource = { resourceId?: string; amount?: number; value?: number }; type Breakdown = { $id?: string; amount?: number; resources?: Record<string, Resource> | Resource[] };
57-66
: Type helpers and tighten access forgetResourceAmount
.Remove
any
and guard for both array/record shapes to avoid runtime surprises.Apply this diff:
-// helper function to get resource amount from backend data -function getResourceAmount(projectBreakdown: any, resourceId: string): number { - if (!projectBreakdown?.resources) return 0; - - const resource = Array.isArray(projectBreakdown.resources) - ? projectBreakdown.resources.find((r: any) => r.resourceId === resourceId) - : projectBreakdown.resources[resourceId]; - - return resource?.amount || 0; -} +// helper function to get resource amount from backend data +function getResourceAmount(projectBreakdown: Breakdown | undefined, resourceId: string): number { + const resources = projectBreakdown?.resources; + if (!resources) return 0; + const resource = Array.isArray(resources) + ? (resources as Resource[]).find((r) => r.resourceId === resourceId) + : (resources as Record<string, Resource>)[resourceId]; + return Number(resource?.amount) || 0; +}
293-301
: Stop injecting HTML via{@html}
; render anchors safely.String HTML +
includes('<a href=')
is XSS-prone and brittle.Apply these diffs:
Billing data row:
- cells: { - item: `<a href="${base}/project-${usageProjects[project.projectId]?.region || 'default'}-${project.projectId}/settings/usage" style="text-decoration: underline; color: var(--fgcolor-accent-neutral);">Usage details</a>`, - usage: '', - price: '' - } + cells: { item: 'Usage details', usage: '', price: '' }, + href: `${base}/project-${usageProjects[project.projectId]?.region || 'default'}-${project.projectId}/settings/usage`Renderer:
- {#if child.cells?.[col.id]?.includes('<a href=')} - {@html child.cells?.[col.id] ?? ''} + {#if col.id === 'item' && child.href} + <a href={child.href} style="text-decoration: underline; color: var(--fgcolor-accent-neutral);"> + {child.cells?.item ?? 'Usage details'} + </a>Optional: add
rel="noopener noreferrer"
if these could target external origins.For a quick audit:
#!/bin/bash # Find unsafe {@html} usage in console rg -n '@html' --type=svelte -C2Also applies to: 388-390
🧹 Nitpick comments (3)
src/routes/(console)/organization-[organization]/billing/planSummary.svelte (3)
68-71
: Dynamic progress color (optional).Consider threshold-based colors (e.g., <70% neutral, 70–90% warning, >90% danger) to communicate risk.
Example:
function getProgressColor(p: number): string { if (p >= 90) return 'var(--bgcolor-danger-invert)'; if (p >= 70) return 'var(--bgcolor-warning-invert)'; return 'var(--bgcolor-neutral-invert)'; }
348-369
: Guard progress rendering with numeric checks only.Current check also requires
child.maxValue
truthiness; allow 0/undefined to hide, but rely onprogressData.length > 0
which you already compute correctly.Apply this diff:
- {#if child.progressData && child.progressData.length > 0 && child.maxValue} + {#if child.progressData && child.progressData.length > 0} <ProgressBar maxSize={child.maxValue} data={child.progressData} /> {/if}Also applies to: 397-401
462-503
: Minor UX: reuse consistent analytics event names.“Upgrade” uses enum
Click.OrganizationClickUpgrade
; “Change plan” uses raw string. Consider standardizing.Possible tweak:
- trackEvent('click_organization_plan_update', { ... }) + trackEvent(Click.OrganizationClickUpgrade, { from: 'button', source: 'billing_tab', action: 'change_plan' })
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
src/routes/(console)/organization-[organization]/billing/planSummary.svelte
(4 hunks)
🧰 Additional context used
🪛 ESLint
src/routes/(console)/organization-[organization]/billing/planSummary.svelte
[error] 22-22: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 23-23: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 58-58: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 62-62: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 131-131: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
🔇 Additional comments (3)
src/routes/(console)/organization-[organization]/billing/planSummary.svelte (3)
176-191
: Bandwidth unit handling looks correct.Bytes-to-GB conversion for display and progress now aligns;
maxValue
uses bytes and the text shows humanized current vs GB cap. LGTM.
324-346
: Billing copy looks good and dates are explicit.Clear next-payment message and cycle range; using
toLocaleDate
matches the rest of Console.
510-663
: CSS: great mobile/tablet polish; keep widths in sync with ProgressBar.The fixed 264/200/120px tracks align with design tokens; ensure ProgressBar respects container width changes.
function createProgressData( | ||
currentValue: number, | ||
maxValue: number | string | ||
): Array<{ size: number; color: string; tooltip?: { title: string; label: string } }> { | ||
if ( | ||
maxValue === null || | ||
maxValue === undefined || | ||
(typeof maxValue === 'number' && maxValue <= 0) | ||
) { | ||
return []; | ||
} | ||
|
||
const max = typeof maxValue === 'string' ? parseFloat(maxValue) : maxValue; | ||
if (max <= 0) return []; | ||
|
||
const percentage = Math.min((currentValue / max) * 100, 100); | ||
const progressColor = getProgressColor(percentage); | ||
|
||
return [ | ||
{ | ||
size: currentValue, | ||
color: progressColor, | ||
tooltip: { | ||
title: `${percentage.toFixed(1)}% used`, | ||
label: `${currentValue.toLocaleString()} of ${max.toLocaleString()}` | ||
} | ||
} | ||
]; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Broaden createProgressData
signature to match call sites.
You pass currentPlan?.users
et al., which are number | undefined
. Update the param type to avoid TS errors.
Apply this diff:
-function createProgressData(
- currentValue: number,
- maxValue: number | string
-): Array<{ size: number; color: string; tooltip?: { title: string; label: string } }> {
+function createProgressData(
+ currentValue: number,
+ maxValue: number | string | null | undefined
+): Array<{ size: number; color: string; tooltip?: { title: string; label: string } }> {
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
function createProgressData( | |
currentValue: number, | |
maxValue: number | string | |
): Array<{ size: number; color: string; tooltip?: { title: string; label: string } }> { | |
if ( | |
maxValue === null || | |
maxValue === undefined || | |
(typeof maxValue === 'number' && maxValue <= 0) | |
) { | |
return []; | |
} | |
const max = typeof maxValue === 'string' ? parseFloat(maxValue) : maxValue; | |
if (max <= 0) return []; | |
const percentage = Math.min((currentValue / max) * 100, 100); | |
const progressColor = getProgressColor(percentage); | |
return [ | |
{ | |
size: currentValue, | |
color: progressColor, | |
tooltip: { | |
title: `${percentage.toFixed(1)}% used`, | |
label: `${currentValue.toLocaleString()} of ${max.toLocaleString()}` | |
} | |
} | |
]; | |
} | |
function createProgressData( | |
currentValue: number, | |
maxValue: number | string | null | undefined | |
): Array<{ size: number; color: string; tooltip?: { title: string; label: string } }> { | |
if ( | |
maxValue === null || | |
maxValue === undefined || | |
(typeof maxValue === 'number' && maxValue <= 0) | |
) { | |
return []; | |
} | |
const max = typeof maxValue === 'string' ? parseFloat(maxValue) : maxValue; | |
if (max <= 0) return []; | |
const percentage = Math.min((currentValue / max) * 100, 100); | |
const progressColor = getProgressColor(percentage); | |
return [ | |
{ | |
size: currentValue, | |
color: progressColor, | |
tooltip: { | |
title: `${percentage.toFixed(1)}% used`, | |
label: `${currentValue.toLocaleString()} of ${max.toLocaleString()}` | |
} | |
} | |
]; | |
} |
🤖 Prompt for AI Agents
In src/routes/(console)/organization-[organization]/billing/planSummary.svelte
around lines 72 to 100, the function signature for createProgressData is too
narrow (currentValue and maxValue can be undefined at call sites); change the
parameter types to currentValue: number | undefined and maxValue: number |
string | undefined, then handle undefined by returning [] (or treating
currentValue as 0) — i.e., early-return [] if currentValue is null/undefined or
maxValue is null/undefined, keep parsing string maxValue to number, guard
against NaN and non-positive max, compute percentage using the numeric
currentValue, and build the same return structure (with toLocaleString and
percentage.toFixed) after these checks.
id: `project-${project.projectId}-storage`, | ||
cells: { | ||
item: 'Storage', | ||
usage: `${formatHumanSize(project.storage || 0)} / ${currentPlan?.storage?.toString() || '0'} GB`, | ||
price: formatCurrency(getResourceAmount(projectBreakdown, 'storage')) | ||
}, | ||
progressData: createStorageProgressData( | ||
project.storage || 0, | ||
currentPlan?.storage || 0 | ||
), | ||
maxValue: currentPlan?.storage | ||
? currentPlan.storage * 1024 * 1024 * 1024 | ||
: 0 | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Show “Unlimited” for storage when no cap; avoid “/ 0 GB”.
Also avoid progress when unlimited.
Apply this diff:
- usage: `${formatHumanSize(project.storage || 0)} / ${currentPlan?.storage?.toString() || '0'} GB`,
+ usage: `${formatHumanSize(project.storage || 0)} / ${currentPlan?.storage ? `${currentPlan.storage} GB` : 'Unlimited'}`,
...
- progressData: createStorageProgressData(
- project.storage || 0,
- currentPlan?.storage || 0
- ),
- maxValue: currentPlan?.storage
- ? currentPlan.storage * 1024 * 1024 * 1024
- : 0
+ progressData: currentPlan?.storage
+ ? createStorageProgressData(project.storage || 0, currentPlan.storage)
+ : [],
+ maxValue: currentPlan?.storage
+ ? currentPlan.storage * 1024 * 1024 * 1024
+ : null
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
id: `project-${project.projectId}-storage`, | |
cells: { | |
item: 'Storage', | |
usage: `${formatHumanSize(project.storage || 0)} / ${currentPlan?.storage?.toString() || '0'} GB`, | |
price: formatCurrency(getResourceAmount(projectBreakdown, 'storage')) | |
}, | |
progressData: createStorageProgressData( | |
project.storage || 0, | |
currentPlan?.storage || 0 | |
), | |
maxValue: currentPlan?.storage | |
? currentPlan.storage * 1024 * 1024 * 1024 | |
: 0 | |
}, | |
id: `project-${project.projectId}-storage`, | |
cells: { | |
item: 'Storage', | |
usage: `${formatHumanSize(project.storage || 0)} / ${currentPlan?.storage | |
? `${currentPlan.storage} GB` | |
: 'Unlimited'}`, | |
price: formatCurrency(getResourceAmount(projectBreakdown, 'storage')) | |
}, | |
progressData: currentPlan?.storage | |
? createStorageProgressData(project.storage || 0, currentPlan.storage) | |
: [], | |
maxValue: currentPlan?.storage | |
? currentPlan.storage * 1024 * 1024 * 1024 | |
: null | |
}, |
🤖 Prompt for AI Agents
In src/routes/(console)/organization-[organization]/billing/planSummary.svelte
around lines 251 to 264, the storage row currently shows "/ 0 GB" and renders
progress when there is no storage cap; change the usage cell to display
"Unlimited" when currentPlan?.storage is falsy (e.g. use
`${formatHumanSize(project.storage || 0)} / Unlimited`), only create/set
progressData when currentPlan?.storage is truthy (i.e. skip or set to
undefined/null when unlimited), and adjust maxValue to null/undefined when there
is no cap instead of 0 so the UI won’t render a progress bar for unlimited
plans.
src/routes/(console)/organization-[organization]/billing/planSummary.svelte
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (9)
src/lib/components/organizationUsageLimits.svelte (7)
90-93
: Use a dedicated analytics event for “Manage projects”, not “Upgrade”.Prevents misleading metrics in downgrade flows.
- trackEvent(Click.OrganizationClickUpgrade, { source: 'usage_limits_manage_projects' }); + trackEvent(Click.OrganizationManageProjectsClick, { source: 'usage_limits_manage_projects' });To verify the event constant exists (or add it if missing), run:
#!/bin/bash rg -nP 'OrganizationManageProjectsClick|OrganizationClickManageProjects' -C2
215-215
: Pluralize “member(s)”.Minor UI text fix.
- <Typography.Text>{formatNumber(freePlanLimits.members)} member</Typography.Text> + <Typography.Text> + {formatNumber(freePlanLimits.members)} {freePlanLimits.members === 1 ? 'member' : 'members'} + </Typography.Text>
265-267
: Make modal description dynamic (avoid “two”).Reflect the actual limit and handle pluralization.
- <svelte:fragment slot="description"> - Choose which two projects to keep. Projects over the limit will be blocked after this date. - </svelte:fragment> + <svelte:fragment slot="description"> + Choose which {allowedProjectsToKeep} project{allowedProjectsToKeep === 1 ? '' : 's'} to keep. + Projects over the limit will be blocked after this date. + </svelte:fragment>
26-26
: Replaceany[]
with SDK type for members.Prevents lint error and improves safety.
- members?: any[]; + members?: Models.Membership[];
36-40
: Don’t hard-code Free projects limit.Derive from billing to avoid drift.
- let freePlanLimits = $derived({ - projects: 2, // fallback + let freePlanLimits = $derived({ + projects: getServiceLimit('projects', BillingPlan.FREE), members: getServiceLimit('members', BillingPlan.FREE), storage: getServiceLimit('storage', BillingPlan.FREE) });
59-63
: Fix excess projects calculation (subtract free allowance).Current code overstates “excess usage”.
- let excessUsage = $derived({ - projects: Math.max(0, currentUsage.projects), + let excessUsage = $derived({ + projects: Math.max(0, currentUsage.projects - freePlanLimits.projects), members: Math.max(0, currentUsage.members - freePlanLimits.members), storage: Math.max(0, storageUsageGB - freePlanLimits.storage) });
295-299
: Unwrap the store when formatting the archive date.Otherwise you’ll render “[object Object]”.
- title={`${projects.length - selectedProjects.length} projects will be archived on ${toLocaleDate(billingProjectsLimitDate)}`}> + title={`${projects.length - selectedProjects.length} projects will be archived on ${toLocaleDate($billingProjectsLimitDate)}`}>src/routes/(console)/organization-[organization]/change-plan/+page.svelte (2)
64-66
: MakeorgUsage
optional to matchundefined
assignment.Fixes a type mismatch when usage fetch fails.
- let orgUsage: OrganizationUsage; + let orgUsage: OrganizationUsage | undefined; @@ - } catch { - orgUsage = undefined; + } catch { + orgUsage = undefined; }Also applies to: 103-107
109-116
: Avoid truncating projects at 1000; paginate.Large orgs will silently miss projects.
- try { - allProjects = await sdk.forConsole.projects.list([ - Query.equal('teamId', data.organization.$id), - Query.limit(1000) - ]); - } catch { - allProjects = { projects: [] }; - } + try { + const pageSize = 1000; + let offset = 0; + let acc: Models.Project[] = []; + let total = 0; + do { + const resp = await sdk.forConsole.projects.list([ + Query.equal('teamId', data.organization.$id), + Query.limit(pageSize), + Query.offset(offset) + ]); + acc = acc.concat(resp.projects ?? []); + total = resp.total ?? acc.length; + offset += resp.projects?.length ?? 0; + } while (offset < total); + allProjects = { projects: acc }; + } catch { + allProjects = { projects: [] }; + }
🧹 Nitpick comments (2)
src/lib/components/organizationUsageLimits.svelte (1)
120-121
: Clarify success toast message.Current text implies selected items will be archived (they’re the ones kept).
- addNotification({ type: 'success', message: `Projects selected for archiving` }); + addNotification({ type: 'success', message: 'Projects selection saved.' });src/routes/(console)/organization-[organization]/change-plan/+page.svelte (1)
168-168
: Fix typo in comment.Minor nit.
- // 1) ppdate the plan first + // 1) Update the plan first
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
src/lib/components/organizationUsageLimits.svelte
(1 hunks)src/routes/(console)/organization-[organization]/change-plan/+page.svelte
(7 hunks)
🧰 Additional context used
🪛 ESLint
src/lib/components/organizationUsageLimits.svelte
[error] 26-26: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
🔇 Additional comments (4)
src/lib/components/organizationUsageLimits.svelte (1)
95-107
: Public API looks good.validateOrAlert and getSelectedProjects are minimal and composable.
src/routes/(console)/organization-[organization]/change-plan/+page.svelte (3)
126-131
: Pre-submit validation gating looks right.Validates selection only for plans with project limits; safe no-op if the component isn’t mounted.
176-190
: Apply selected projects post-downgrade — confirm limit parity.Ensure the component’s limit source (billing getServiceLimit) matches
$plansInfo
to avoid off‑by‑N mismatches.Would you like me to align both to a single source of truth (e.g., only
$plansInfo
)?
378-385
: Free downgrade UI integration looks good.Props wiring matches the component’s expectations.
</Typography.Text> | ||
</Layout.Stack> | ||
|
||
{#if currentPlan.budgeting && extraUsage > 0} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should be changed as addons can be projects or members, and for new plan types it's projects, I used this method instead to show first the addons
console/src/routes/(console)/organization-[organization]/billing/planSummary.svelte
Lines 71 to 72 in f9ee8c2
{#each currentAggregation.resources.filter((r) => r.amount && r.amount > 0 && Object.keys(currentPlan.addons).includes(r.resourceId) && currentPlan.addons[r.resourceId].price > 0) as excess, i} | |
{#if i > 0} |
and then for project breakdown, use project breakdown
console/src/routes/(console)/organization-[organization]/billing/planSummary.svelte
Lines 87 to 88 in f9ee8c2
{#each currentAggregation.projectBreakdown as projectBreakdown} | |
<Accordion title="{projectBreakdown.name}"> |
I think you have reverted back these changes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
||
$: projectsList = organizationUsage?.projects?.length | ||
? organizationUsage.projects | ||
: (currentAggregation?.projectBreakdown || []).map((p) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's happening here?
}; | ||
}) || []), | ||
// Show info if no projects found | ||
...(projectsList && projectsList.length === 0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we need no projects info here, is it in the design?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (6)
src/routes/(console)/organization-[organization]/billing/planSummary.svelte (6)
251-264
: Show “Unlimited” for storage and hide progress with no cap
Avoid “/ 0 GB” and empty progress bars when limit is absent.- cells: { - item: 'Storage', - usage: `${formatHumanSize(project.storage || 0)} / ${currentPlan?.storage?.toString() || '0'} GB`, - price: formatCurrency(getResourceAmount(projectBreakdown, 'storage')) - }, - progressData: createStorageProgressData( - project.storage || 0, - currentPlan?.storage || 0 - ), - maxValue: currentPlan?.storage - ? currentPlan.storage * 1024 * 1024 * 1024 - : 0 + cells: { + item: 'Storage', + usage: `${formatHumanSize(project.storage || 0)} / ${currentPlan?.storage ? `${currentPlan.storage} GB` : 'Unlimited'}`, + price: formatCurrency(getResourceAmount(projectBreakdown, 'storage')) + }, + progressData: currentPlan?.storage + ? createStorageProgressData(project.storage || 0, currentPlan.storage) + : [], + maxValue: currentPlan?.storage + ? currentPlan.storage * 1024 * 1024 * 1024 + : null
21-24
: Replaceany
props with concrete types (CI errors)
Use explicit interfaces for organizationUsage and usageProjects to satisfy ESLint and improve safety.- export let currentAggregation: AggregationTeam | undefined = undefined; - export let organizationUsage: any = undefined; - export let usageProjects: Record<string, any> = {}; + export let currentAggregation: AggregationTeam | undefined = undefined; + + type UsageProjectMeta = { name?: string; region?: string }; + type UsageProject = { + projectId: string; + storage?: number; + executions?: number; + executionsMBSeconds?: number; + bandwidth?: number; + databasesReads?: number; + databasesWrites?: number; + users?: number; + authPhoneTotal?: number; + authPhoneEstimate?: number; + }; + type OrganizationUsage = { projects?: UsageProject[] }; + + export let organizationUsage: OrganizationUsage | undefined = undefined; + export let usageProjects: Record<string, UsageProjectMeta> = {};
57-66
: TypegetResourceAmount
and its inputs
Removeany
, model the resource/breakdown shape, and narrow array vs record access.- // helper function to get resource amount from backend data - function getResourceAmount(projectBreakdown: any, resourceId: string): number { + // helper function to get resource amount from backend data + type Resource = { resourceId?: string; amount?: number }; + type Breakdown = { resources?: Record<string, Resource> | Resource[] }; + function getResourceAmount(projectBreakdown: Breakdown | undefined, resourceId: string): number { if (!projectBreakdown?.resources) return 0; - const resource = Array.isArray(projectBreakdown.resources) - ? projectBreakdown.resources.find((r: any) => r.resourceId === resourceId) - : projectBreakdown.resources[resourceId]; + const resource = Array.isArray(projectBreakdown.resources) + ? (projectBreakdown.resources as Resource[]).find((r) => r.resourceId === resourceId) + : (projectBreakdown.resources as Record<string, Resource>)[resourceId]; return resource?.amount || 0; }
72-76
: Allow undefined/null in createProgressData(maxValue) to match call sites
Current calls passundefined
; TS will error. Broaden the type.- function createProgressData( - currentValue: number, - maxValue: number | string - ): Array<{ size: number; color: string; tooltip?: { title: string; label: string } }> { + function createProgressData( + currentValue: number, + maxValue: number | string | null | undefined + ): Array<{ size: number; color: string; tooltip?: { title: string; label: string } }> {
269-281
: Fix GB-hours conversion (MB·s → GB·h) and stop reconverting plan limit
Use 1024 MB/GB (not 1000) and usecurrentPlan.executions
directly if it’s already GB·h.- usage: `${formatNum((project.executionsMBSeconds || 0) / 1000 / 3600 || 0)} / ${currentPlan?.executions ? formatNum((currentPlan.executions * 1000 * 3600) / 1000 / 3600) : 'Unlimited'}`, + // usage (GB·h) = MB·s / (1024 MB/GB * 3600 s/h) + usage: `${formatNum(((project.executionsMBSeconds || 0) / (1024 * 3600)) || 0)} / ${currentPlan?.executions ? formatNum(currentPlan.executions) : 'Unlimited'}`, ... - progressData: currentPlan?.executions - ? createProgressData( - (project.executionsMBSeconds || 0) / 1000 / 3600, - (currentPlan.executions * 1000 * 3600) / 1000 / 3600 - ) - : [], - maxValue: currentPlan?.executions - ? (currentPlan.executions * 1000 * 3600) / 1000 / 3600 - : null + progressData: currentPlan?.executions + ? createProgressData( + (project.executionsMBSeconds || 0) / (1024 * 3600), + currentPlan.executions + ) + : [], + maxValue: currentPlan?.executions ?? null
293-301
: Stop injecting HTML via {@html} for the “Usage details” link (XSS/brittle)
Render anchors structurally with a dedicatedhref
field.- { - id: `project-${project.projectId}-usage-details`, - cells: { - item: `<a href="${base}/project-${usageProjects[project.projectId]?.region || 'default'}-${project.projectId}/settings/usage" style="text-decoration: underline; color: var(--fgcolor-accent-neutral);">Usage details</a>`, - usage: '', - price: '' - } - } + { + id: `project-${project.projectId}-usage-details`, + cells: { item: 'Usage details', usage: '', price: '' }, + href: `${base}/project-${usageProjects[project.projectId]?.region || 'default'}-${project.projectId}/settings/usage` + }- {#if child.cells?.[col.id]?.includes('<a href=')} - {@html child.cells?.[col.id] ?? ''} + {#if col.id === 'item' && child.href} + <a href={child.href} style="text-decoration: underline; color: var(--fgcolor-accent-neutral);"> + Usage details + </a> {:else if col.id === 'usage'}Also applies to: 388-439
🧹 Nitpick comments (1)
src/routes/(console)/organization-[organization]/billing/planSummary.svelte (1)
8-10
: Type Map to ProjectBreakdown
Import ProjectBreakdown and type the map to avoid implicit anys.- import type { AggregationTeam, Invoice, Plan } from '$lib/sdk/billing'; + import type { AggregationTeam, Invoice, Plan, ProjectBreakdown } from '$lib/sdk/billing';- $: projectIdToBreakdown = new Map( + $: projectIdToBreakdown: Map<string, ProjectBreakdown> = new Map( (currentAggregation?.projectBreakdown || []).map((p) => [p.$id, p]) );Also applies to: 34-36
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
src/routes/(console)/organization-[organization]/billing/planSummary.svelte
(4 hunks)
🧰 Additional context used
🪛 ESLint
src/routes/(console)/organization-[organization]/billing/planSummary.svelte
[error] 22-22: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 23-23: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 58-58: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 62-62: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 131-131: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
🔇 Additional comments (1)
src/routes/(console)/organization-[organization]/billing/planSummary.svelte (1)
320-321
: Verify total source of truth
If currentInvoice is undefined or stale, consider summing base + per-project amounts for the UI total.Happy to wire a numeric
amount
field per row and compute frombillingData
if desired.
src/routes/(console)/organization-[organization]/billing/planSummary.svelte
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (6)
src/routes/(console)/organization-[organization]/billing/planSummary.svelte (6)
72-100
: BroadencreateProgressData
signature to match call sites (accept undefined/null)Prevents TS errors when passing optional plan limits.
-function createProgressData( - currentValue: number, - maxValue: number | string +function createProgressData( + currentValue: number, + maxValue: number | string | null | undefined ): Array<{ size: number; color: string; tooltip?: { title: string; label: string } }> { if ( - maxValue === null || - maxValue === undefined || + maxValue === null || + maxValue === undefined || (typeof maxValue === 'number' && maxValue <= 0) ) { return []; }
251-264
: Show “Unlimited” for storage and avoid rendering a progress bar with a zero capPrevents “/ 0 GB” and a meaningless progress state.
cells: { item: 'Storage', - usage: `${formatHumanSize(project.storage || 0)} / ${currentPlan?.storage?.toString() || '0'} GB`, + usage: `${formatHumanSize(project.storage || 0)} / ${currentPlan?.storage ? `${currentPlan.storage} GB` : 'Unlimited'}`, price: formatCurrency(getResourceAmount(projectBreakdown, 'storage')) }, -progressData: createStorageProgressData( - project.storage || 0, - currentPlan?.storage || 0 -), -maxValue: currentPlan?.storage - ? currentPlan.storage * 1024 * 1024 * 1024 - : 0 +progressData: currentPlan?.storage + ? createStorageProgressData(project.storage || 0, currentPlan.storage) + : [], +maxValue: currentPlan?.storage + ? currentPlan.storage * 1024 * 1024 * 1024 + : null
126-147
: Removeany
in.find
callback; let TS infer the project typeEliminates ESLint violations while keeping intent.
-const projectData = organizationUsage?.projects?.find( - (proj: any) => proj.projectId === p.$id -); +const projectData = organizationUsage?.projects?.find( + (proj) => proj.projectId === p.$id +);
269-281
: Fix GB‑hours conversion (MB·s → GB·h) and stop converting the plan limitDivide by 1024×3600; use
currentPlan.executions
directly if it is already GB‑hours.cells: { item: 'GB-hours', - usage: `${formatNum((project.executionsMBSeconds || 0) / 1000 / 3600 || 0)} / ${currentPlan?.executions ? formatNum((currentPlan.executions * 1000 * 3600) / 1000 / 3600) : 'Unlimited'}`, + // usage (GB·h) = MB·s / (1024 MB/GB * 3600 s/h) + usage: `${formatNum(((project.executionsMBSeconds || 0) / (1024 * 3600)) || 0)} / ${currentPlan?.executions ? formatNum(currentPlan.executions) : 'Unlimited'}`, price: formatCurrency(getResourceAmount(projectBreakdown, 'GBHours')) }, -progressData: currentPlan?.executions - ? createProgressData( - (project.executionsMBSeconds || 0) / 1000 / 3600, - (currentPlan.executions * 1000 * 3600) / 1000 / 3600 - ) - : [], -maxValue: currentPlan?.executions - ? (currentPlan.executions * 1000 * 3600) / 1000 / 3600 - : null +progressData: currentPlan?.executions + ? createProgressData( + (project.executionsMBSeconds || 0) / (1024 * 3600), + currentPlan.executions + ) + : [], +maxValue: currentPlan?.executions ?? null
296-301
: Stop injecting HTML with{@html}
; render the link safelyString‑injecting
<a>
is XSS‑prone and brittle. Carry a structuredhref
and render an anchor.-{ - id: `project-${project.projectId}-usage-details`, - cells: { - item: `<a href="${base}/project-${usageProjects[project.projectId]?.region || 'default'}-${project.projectId}/settings/usage" style="text-decoration: underline; color: var(--fgcolor-accent-neutral);">Usage details</a>`, - usage: '', - price: '' - } -} +{ + id: `project-${project.projectId}-usage-details`, + cells: { item: 'Usage details', usage: '', price: '' }, + href: `${base}/project-${usageProjects[project.projectId]?.region || 'default'}-${project.projectId}/settings/usage` +}-{#if child.cells?.[col.id]?.includes('<a href=')} - {@html child.cells?.[col.id] ?? ''} +{#if col.id === 'item' && child.href} + <a class="usage-link" href={child.href}>Usage details</a> {:else if col.id === 'usage'}Optionally, add CSS for
.usage-link
to match design tokens instead of inline styles.Also applies to: 388-391
22-24
: Replaceany
props with concrete types to satisfy ESLint and prevent runtime mistakesDefine minimal shapes consumed by this component and use them for the exported props.
Apply:
-export let organizationUsage: any = undefined; -export let usageProjects: Record<string, any> = {}; +export type UsageProjectMeta = { name?: string; region?: string }; +export type OrganizationUsageProject = { + projectId: string; + storage?: number; executions?: number; executionsMBSeconds?: number; + bandwidth?: number; databasesReads?: number; databasesWrites?: number; + users?: number; authPhoneTotal?: number; authPhoneEstimate?: number; +}; +export type OrganizationUsage = { projects?: OrganizationUsageProject[] }; + +export let organizationUsage: OrganizationUsage | undefined = undefined; +export let usageProjects: Record<string, UsageProjectMeta> = {};
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (5)
src/lib/components/billing/alerts/limitReached.svelte
(1 hunks)src/routes/(console)/organization-[organization]/billing/planSummary.svelte
(4 hunks)src/routes/(console)/organization-[organization]/billing/planSummaryOld.svelte
(1 hunks)src/routes/(console)/organization-[organization]/budgetLimitAlert.svelte
(1 hunks)src/routes/(console)/organization-[organization]/header.svelte
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/routes/(console)/organization-[organization]/billing/planSummaryOld.svelte
🧰 Additional context used
🪛 ESLint
src/routes/(console)/organization-[organization]/billing/planSummary.svelte
[error] 22-22: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 23-23: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 58-58: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 62-62: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 131-131: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
🔇 Additional comments (3)
src/lib/components/billing/alerts/limitReached.svelte (1)
23-30
: Conditional “View usage” visibility — LGTMGating the button on
!page.data.currentPlan?.usagePerProject
is consistent with the new per‑project pricing UX.src/routes/(console)/organization-[organization]/budgetLimitAlert.svelte (1)
20-27
: Usage button gating — LGTMMatches the per‑project pricing flag used elsewhere. Keeps legacy usage view hidden when irrelevant.
src/routes/(console)/organization-[organization]/header.svelte (1)
69-73
: Hide “Usage” tab under per‑project pricing — LGTMCondition preserves prior behavior when
currentPlan
is absent and hides the tab whenusagePerProject
is true.
$: projectIdToBreakdown = new Map( | ||
(currentAggregation?.projectBreakdown || []).map((p) => [p.$id, p]) | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Strongly type project breakdown and remove any
in resource access
Avoids @typescript-eslint/no-explicit-any
errors and clarifies the resources shape used by pricing.
-$: projectIdToBreakdown = new Map(
- (currentAggregation?.projectBreakdown || []).map((p) => [p.$id, p])
-);
+type Resource = { resourceId?: string; amount?: number };
+type Breakdown = { $id: string; amount?: number; resources?: Record<string, Resource> | Resource[] };
+$: projectIdToBreakdown: Map<string, Breakdown> = new Map(
+ (currentAggregation?.projectBreakdown || []).map((p: Breakdown) => [p.$id, p])
+);
-// helper function to get resource amount from backend data
-function getResourceAmount(projectBreakdown: any, resourceId: string): number {
+// helper function to get resource amount from backend data
+function getResourceAmount(projectBreakdown: Breakdown | undefined, resourceId: string): number {
if (!projectBreakdown?.resources) return 0;
-
- const resource = Array.isArray(projectBreakdown.resources)
- ? projectBreakdown.resources.find((r: any) => r.resourceId === resourceId)
- : projectBreakdown.resources[resourceId];
-
- return resource?.amount || 0;
+ const resource = Array.isArray(projectBreakdown.resources)
+ ? (projectBreakdown.resources as Resource[]).find((r) => r.resourceId === resourceId)
+ : (projectBreakdown.resources as Record<string, Resource>)?.[resourceId];
+ return resource?.amount ?? 0;
}
Also applies to: 58-66
🤖 Prompt for AI Agents
In src/routes/(console)/organization-[organization]/billing/planSummary.svelte
around lines 34-36 (and similarly lines 58-66), the project breakdown is being
treated as untyped/any; define a concrete TypeScript type (e.g.,
ProjectBreakdown { $id: string; resources: Record<string, ResourceUsage> |
ResourceUsage[]; ... } and ResourceUsage { units: number; price: number; ... })
in the <script lang="ts"> block or an imported d.ts, then change the Map
creation to new Map<string,
ProjectBreakdown>((currentAggregation?.projectBreakdown || []) as
ProjectBreakdown[]). Also update all subsequent accesses to use the typed
properties instead of any (access resources via the typed key/field names and
adjust signatures), removing any explicit any casts so the compiler and eslint
know the shape of projectBreakdown and resources.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/routes/(console)/organization-[organization]/billing/+page.ts (1)
25-29
: Fix type of billingAddressPromise (can be null and resolves to null).Currently typed as Promise
but assigned null and may resolve to null in catch.- const billingAddressPromise: Promise<Address> = billingAddressId - ? sdk.forConsole.billing - .getOrganizationBillingAddress(organization.$id, billingAddressId) - .catch(() => null) - : null; + const billingAddressPromise: Promise<Address | null> | null = billingAddressId + ? sdk.forConsole.billing + .getOrganizationBillingAddress(organization.$id, billingAddressId) + .catch(() => null) + : null;
♻️ Duplicate comments (4)
src/routes/(console)/organization-[organization]/billing/+page.ts (3)
64-64
: Prefer const for usageProjects (never reassigned).Matches ESLint prefer-const; safe since only properties mutate.
- let usageProjects: Record<string, UsageProjectInfo> = {}; + const usageProjects: Record<string, UsageProjectInfo> = {};
37-44
: Swallowed errors: capture low-noise telemetry (debug).Silent catch blocks hinder diagnosis; add debug capture behind isCloud.
Also applies to: 47-54, 69-73, 120-122
108-119
: Batch multi-ID project lookup to respect API limits.Large arrays in Query.equal('$id', ids) can exceed limits.
- if (missingIds.length > 0) { - const projectsResponse = await sdk.forConsole.projects.list([ - Query.equal('$id', missingIds), - Query.limit(missingIds.length) - ]); - for (const project of projectsResponse.projects) { - usageProjects[project.$id] = { - name: project.name, - region: project.region - }; - } - } + if (missingIds.length > 0) { + const MAX = 100; + for (let i = 0; i < missingIds.length; i += MAX) { + const slice = missingIds.slice(i, i + MAX); + const res = await sdk.forConsole.projects.list([ + Query.equal('$id', slice), + Query.limit(slice.length) + ]); + for (const project of res.projects) { + usageProjects[project.$id] = { + name: project.name, + region: project.region + }; + } + } + }src/routes/(console)/organization-[organization]/billing/planSummary.svelte (1)
90-118
: Broaden createProgressData signature to match call sites.You pass possibly undefined; function already guards for nullish.
- function createProgressData( - currentValue: number, - maxValue: number | string + function createProgressData( + currentValue: number, + maxValue: number | string | null | undefined ): Array<{ size: number; color: string; tooltip?: { title: string; label: string } }> {
🧹 Nitpick comments (2)
src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte (1)
132-133
: Grammar tweak (“user” → “users”).User-facing string.
- Total user in your project. + Total users in your project.src/routes/(console)/organization-[organization]/billing/planSummary.svelte (1)
34-45
: Type allProjectsBreakdown map.Avoid implicit any.
- $: allProjectsBreakdown = new Map( + $: allProjectsBreakdown: Map<string, PB> = new Map( projectsList.map((project) => [ project.projectId, project.breakdown || { $id: project.projectId, amount: 0, resources: {} } ]) );
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (4)
src/routes/(console)/organization-[organization]/billing/+page.ts
(3 hunks)src/routes/(console)/organization-[organization]/billing/planSummary.svelte
(4 hunks)src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte
(1 hunks)src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts
(3 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/routes/(console)/organization-[organization]/billing/+page.ts (2)
src/routes/(console)/organization-[organization]/store.ts (1)
UsageProjectInfo
(7-7)src/lib/stores/sdk.ts (1)
sdk
(142-165)
src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts (2)
src/lib/sdk/billing.ts (1)
AggregationTeam
(154-211)src/lib/stores/sdk.ts (1)
sdk
(142-165)
🪛 ESLint
src/routes/(console)/organization-[organization]/billing/+page.ts
[error] 64-64: 'usageProjects' is never reassigned. Use 'const' instead.
(prefer-const)
src/routes/(console)/organization-[organization]/billing/planSummary.svelte
[error] 22-22: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 23-23: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 66-66: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 70-70: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 76-76: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 80-80: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 155-155: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 160-160: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts
[error] 51-51: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
🔇 Additional comments (1)
src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte (1)
66-74
: LGTM: modal now always opens.Fix addresses the earlier regression where click did nothing under flag=false.
Also applies to: 73-75
const allProjectsResponse = await sdk.forConsole.projects.list([ | ||
Query.equal('teamId', organization.$id), | ||
Query.limit(1000) // get all projects | ||
]); | ||
|
||
for (const project of allProjectsResponse.projects) { | ||
usageProjects[project.$id] = { | ||
name: project.name, | ||
region: project.region | ||
}; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
❓ Verification inconclusive
Paginate all-projects listing; avoid optimistic Query.limit(1000).
List endpoints typically cap at 100. Iterate with limit/offset (or cursor) to avoid truncation.
- const allProjectsResponse = await sdk.forConsole.projects.list([
- Query.equal('teamId', organization.$id),
- Query.limit(1000) // get all projects
- ]);
-
- for (const project of allProjectsResponse.projects) {
- usageProjects[project.$id] = {
- name: project.name,
- region: project.region
- };
- }
+ const PAGE = 100;
+ let offset = 0;
+ // paginate until fewer than PAGE results are returned
+ // (switch to cursorAfter if your SDK prefers cursors)
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ const res = await sdk.forConsole.projects.list([
+ Query.equal('teamId', organization.$id),
+ Query.limit(PAGE),
+ Query.offset(offset)
+ ]);
+ for (const project of res.projects) {
+ usageProjects[project.$id] = {
+ name: project.name,
+ region: project.region
+ };
+ }
+ if (res.projects.length < PAGE) break;
+ offset += PAGE;
+ }
Implement pagination for projects.list
Replace the single Query.limit(1000)
call (src/routes/(console)/organization-[organization]/billing/+page.ts:85–96) with a loop that requests pages (e.g. Query.limit(PAGE)
+ Query.offset(offset)
) and continues until a page returns fewer than PAGE items, ensuring no projects are silently dropped.
🤖 Prompt for AI Agents
In src/routes/(console)/organization-[organization]/billing/+page.ts around
lines 85-96, the current projects.list call uses a single Query.limit(1000)
which can drop projects; replace it with a paginated loop that sets a PAGE size
(e.g. 100 or 1000), calls projects.list with Query.limit(PAGE) and
Query.offset(offset), appends each page's projects into usageProjects (using
project.$id as key), increments offset by PAGE, and stops when the returned page
has fewer items than PAGE (or zero), ensuring all projects are retrieved without
loss.
src/routes/(console)/organization-[organization]/billing/planSummary.svelte
Outdated
Show resolved
Hide resolved
// helper function to get resource amount from backend data | ||
function getResourceAmount(projectBreakdown: any, resourceId: string): number { | ||
if (!projectBreakdown?.resources) return 0; | ||
|
||
const resource = Array.isArray(projectBreakdown.resources) | ||
? projectBreakdown.resources.find((r: any) => r.resourceId === resourceId) | ||
: projectBreakdown.resources[resourceId]; | ||
|
||
return resource?.amount || 0; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Type resource helpers; remove any
.
Handles both array and record shapes.
- // helper function to get resource amount from backend data
- function getResourceAmount(projectBreakdown: any, resourceId: string): number {
+ // helper to get resource amount from backend data
+ function getResourceAmount(projectBreakdown: PB | undefined, resourceId: string): number {
if (!projectBreakdown?.resources) return 0;
-
- const resource = Array.isArray(projectBreakdown.resources)
- ? projectBreakdown.resources.find((r: any) => r.resourceId === resourceId)
- : projectBreakdown.resources[resourceId];
-
- return resource?.amount || 0;
+ const res = Array.isArray(projectBreakdown.resources)
+ ? (projectBreakdown.resources as InvoiceUsage[]).find((r) => r.resourceId === resourceId)
+ : (projectBreakdown.resources as Record<string, InvoiceUsage>)[resourceId];
+ return res?.amount ?? 0;
}
@@
- function getResourceUsage(projectBreakdown: any, resourceId: string): number {
+ function getResourceUsage(projectBreakdown: PB | undefined, resourceId: string): number {
if (!projectBreakdown?.resources) return 0;
-
- const resource = Array.isArray(projectBreakdown.resources)
- ? projectBreakdown.resources.find((r: any) => r.resourceId === resourceId)
- : projectBreakdown.resources[resourceId];
-
- return resource?.value || 0;
+ const res = Array.isArray(projectBreakdown.resources)
+ ? (projectBreakdown.resources as InvoiceUsage[]).find((r) => r.resourceId === resourceId)
+ : (projectBreakdown.resources as Record<string, InvoiceUsage>)[resourceId];
+ return res?.value ?? 0;
}
Also applies to: 76-84
🧰 Tools
🪛 ESLint
[error] 66-66: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 70-70: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
🤖 Prompt for AI Agents
In src/routes/(console)/organization-[organization]/billing/planSummary.svelte
around lines 65-74 (and similarly for 76-84), replace the use of any by defining
proper types: declare a Resource interface (resourceId: string; amount: number;
…) and type projectBreakdown so its resources field is Resource[] |
Record<string, Resource>; update function signatures to use these types
(projectBreakdown: { resources?: Resource[] | Record<string, Resource> },
resourceId: string): number and adjust the lookup to return a typed Resource
(use Array.isArray check for Resource[] or index into the Record), and return
resource?.amount ?? 0; ensure imports/locals are updated and any other helper
functions in the same region receive the same typed signatures.
src/routes/(console)/organization-[organization]/billing/planSummary.svelte
Outdated
Show resolved
Hide resolved
id: `project-${project.projectId}-storage`, | ||
cells: { | ||
item: 'Storage', | ||
usage: `${formatHumanSize(getResourceUsage(projectBreakdown, 'storage') || project.storage || 0)} / ${currentPlan?.storage?.toString() || '0'} GB`, | ||
price: formatCurrency(getResourceAmount(projectBreakdown, 'storage')) | ||
}, | ||
progressData: createStorageProgressData( | ||
getResourceUsage(projectBreakdown, 'storage') || project.storage || 0, | ||
currentPlan?.storage || 0 | ||
), | ||
maxValue: currentPlan?.storage | ||
? currentPlan.storage * 1024 * 1024 * 1024 | ||
: 0 | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Show “Unlimited” for storage and skip progress when no cap.
Avoid “/ 0 GB” and zeroed progress bars.
- usage: `${formatHumanSize(getResourceUsage(projectBreakdown, 'storage') || project.storage || 0)} / ${currentPlan?.storage?.toString() || '0'} GB`,
+ usage: `${formatHumanSize(getResourceUsage(projectBreakdown, 'storage') || project.storage || 0)} / ${currentPlan?.storage ? `${currentPlan.storage} GB` : 'Unlimited'}`,
@@
- progressData: createStorageProgressData(
- getResourceUsage(projectBreakdown, 'storage') || project.storage || 0,
- currentPlan?.storage || 0
- ),
- maxValue: currentPlan?.storage
- ? currentPlan.storage * 1024 * 1024 * 1024
- : 0
+ progressData: currentPlan?.storage
+ ? createStorageProgressData(
+ getResourceUsage(projectBreakdown, 'storage') || project.storage || 0,
+ currentPlan.storage
+ )
+ : [],
+ maxValue: currentPlan?.storage
+ ? currentPlan.storage * 1024 * 1024 * 1024
+ : null
Also applies to: 319-329
🤖 Prompt for AI Agents
In src/routes/(console)/organization-[organization]/billing/planSummary.svelte
around lines 316 to 329, when currentPlan?.storage is missing or zero the UI
shows “/ 0 GB” and a zeroed progress bar; change the usage label to show
"Unlimited" when there is no storage cap and disable/omit progressData and
maxValue so no progress bar renders. Concretely: if currentPlan?.storage is
falsy, set the usage right-hand part to "Unlimited" instead of
`${currentPlan?.storage?.toString() || '0'} GB`, and set progressData to
null/undefined (or skip creating it) and maxValue to undefined (or null) so the
component knows not to render a progress indicator; otherwise keep the existing
formatted usage, progressData and maxValue logic.
src/routes/(console)/organization-[organization]/billing/planSummary.svelte
Outdated
Show resolved
Hide resolved
src/routes/(console)/organization-[organization]/billing/planSummary.svelte
Outdated
Show resolved
Hide resolved
const executionsResource = projectSpecificData.resources?.find?.( | ||
(r: any) => r.resourceId === 'executions' | ||
); | ||
if (executionsResource) { | ||
usage.executionsTotal = executionsResource.value || usage.executionsTotal; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Remove any
in find; use SDK type.
Improves safety and fixes ESLint no-explicit-any.
- const executionsResource = projectSpecificData.resources?.find?.(
- (r: any) => r.resourceId === 'executions'
- );
+ const executionsResource = projectSpecificData.resources?.find?.(
+ (r: InvoiceUsage) => r.resourceId === 'executions'
+ );
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
const executionsResource = projectSpecificData.resources?.find?.( | |
(r: any) => r.resourceId === 'executions' | |
); | |
if (executionsResource) { | |
usage.executionsTotal = executionsResource.value || usage.executionsTotal; | |
} | |
const executionsResource = projectSpecificData.resources?.find?.( | |
(r: InvoiceUsage) => r.resourceId === 'executions' | |
); | |
if (executionsResource) { | |
usage.executionsTotal = executionsResource.value || usage.executionsTotal; | |
} |
🧰 Tools
🪛 ESLint
[error] 51-51: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
🤖 Prompt for AI Agents
In
src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts
around lines 50 to 55, the callback to find currently types the resource
parameter as any; replace any with the correct SDK resource type (imported from
the SDK types, e.g. ProjectResource or ResourceType used across the codebase),
ensure projectSpecificData.resources is typed as an array of that SDK type (or
narrow it with a guard before calling find), and update the find predicate to
use that typed parameter so ESLint no-explicit-any is resolved and you get
proper autocompletion and type-safety for resourceId and value fields.
const org = $organization; | ||
const plan = $currentPlan; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we rather pass these as props?
// Check for Infinity or very large numbers | ||
const isUnlimited = count === Infinity || count >= Number.MAX_SAFE_INTEGER; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we have a helper named isWithinSafeRange
that we could use.
}: { | ||
organization: Organization; | ||
projects?: Models.Project[]; | ||
members?: any[]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
make this explicit types
|
||
// Derived state using runes | ||
let freePlanLimits = $derived({ | ||
projects: 2, // fallback |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use the value from plan itself and not hardcode.
direction="row" | ||
justifyContent="flex-start" | ||
gap="xs" | ||
style="position: relative; z-index: 10; pointer-events: auto;"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why the zIndex? Is the same issue of Tables under Wizards?
// pro and scale plans have unlimited seats (per-project NEW pricing model) | ||
const currentTier = tier ?? get(organization)?.billingPlan; | ||
if (currentTier === BillingPlan.PRO || currentTier === BillingPlan.SCALE) { | ||
return Infinity; // unlimited seats for Pro and Scale plans |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isn't unlimited seat 0
on backend? cc @lohanidamodar
import { formatName as formatNameHelper } from '$lib/helpers/string'; | ||
function formatName(name: string, limit: number = 19) { | ||
const mobileLimit = 16; | ||
const actualLimit = $isSmallViewport ? mobileLimit : limit; | ||
return name ? (name.length > actualLimit ? `${name.slice(0, actualLimit)}...` : name) : '-'; | ||
return formatNameHelper(name, limit, $isSmallViewport); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why not directly use the helper? also imports
should be at the top.
{ id: 'item', align: 'left' as const, width: '10fr' }, | ||
{ id: 'usage', align: 'left' as const, width: '20fr' }, | ||
{ id: 'price', align: 'right' as const, width: '0fr' } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we don't use fr
values for other table types, why here? if its like this on library, will need to change i think.
function getProgressColor(_percentage: number): string { | ||
return 'var(--bgcolor-neutral-invert)'; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if this is always the same, use directly on the component.
}); | ||
})(); | ||
|
||
$: billingData = [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
extract to a function and the do $billingData = getBillingData(...)
Also the logic can be streamlined.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
♻️ Duplicate comments (4)
src/lib/sdk/billing.ts (3)
154-211
: Avoid duplicating SDK models; alias to SDK types to prevent drift
AggregationTeam
(and likelyAggregationBreakdown
) exist inModels.*
. Prefer re‑exporting those instead of redefining, so schema updates don’t diverge.Proposed change (if the types exist in Models):
-export type AggregationTeam = { - $id: string; - /** - * Aggregation creation time in ISO 8601 format. - */ - $createdAt: string; - /** - * Aggregation update date in ISO 8601 format. - */ - $updatedAt: string; - /** - * Beginning date of the invoice. - */ - from: string; - /** - * End date of the invoice. - */ - to: string; - /** - * Total amount of the invoice. - */ - amount: number; - additionalMembers: number; - /** - * Price for additional members - */ - additionalMemberAmount: number; - /** - * Total storage usage. - */ - usageStorage: number; - /** - * Total active users for the billing period. - */ - usageUsers: number; - /** - * Total number of executions for the billing period. - */ - usageExecutions: number; - /** - * Total bandwidth usage for the billing period. - */ - usageBandwidth: number; - /** - * Total realtime usage for the billing period. - */ - usageRealtime: number; - /** - * Usage logs for the billing period. - */ - resources: InvoiceUsage[]; - /** - * Aggregation billing plan - */ - plan: string; - breakdown: AggregationBreakdown[]; -}; +export type AggregationTeam = Models.AggregationTeam;And similarly for
AggregationBreakdown
below, if available.
221-225
: Likely missing display name in InvoiceUsageAdd an optional human label for UIs to render nicely without extra lookups.
export type InvoiceUsage = { resourceId: string; + name?: string; value: number; amount: number; };
440-440
: PlansMap key type changed; align call sitesYou now export
PlansMap = Map<string, Plan>
. Ensure all usages constructing/reading plan maps switch to string keys (e.g.,plan.id
) and update generics/imports accordingly.src/routes/(console)/organization-[organization]/billing/planSummary.svelte (1)
55-58
: Type mismatch: createProgressData doesn’t accept undefined maxValueYou call with
currentPlan?.users
etc. Update the signature to include undefined/null; otherwise TS will complain and builds may fail.-function createProgressData( - currentValue: number, - maxValue: number | string -): Array<{ size: number; color: string; tooltip?: { title: string; label: string } }> { +function createProgressData( + currentValue: number, + maxValue: number | string | null | undefined +): Array<{ size: number; color: string; tooltip?: { title: string; label: string } }> {
🧹 Nitpick comments (3)
src/lib/sdk/billing.ts (2)
377-399
: Document units for new plan fieldsAdd JSDoc (e.g., “reads per month”, “GB‑hours”, etc.) so consumers don’t misinterpret units, especially for
GBHours
,databasesReads/Writes
, andusagePerProject
.Also applies to: 409-417, 432-433
577-591
: Duplicate endpoint: listPlans and getPlansInfo; normalize verbBoth hit
/console/plans
. DelegategetPlansInfo
tolistPlans
and use uppercase HTTP verbs for consistency.async listPlans(queries: string[] = []): Promise<PlanList> { const path = `/console/plans`; const uri = new URL(this.client.config.endpoint + path); const params = { queries }; - return await this.client.call( - 'get', + return await this.client.call( + 'GET', uri, { 'content-type': 'application/json' }, params ); } @@ -async getPlansInfo(): Promise<PlanList> { - const path = `/console/plans`; - const params = {}; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'GET', - uri, - { - 'content-type': 'application/json' - }, - params - ); -} +async getPlansInfo(queries: string[] = []): Promise<PlanList> { + return this.listPlans(queries); +}Also applies to: 1429-1441
src/routes/(console)/organization-[organization]/billing/planSummary.svelte (1)
37-44
: Use 1024-based bytes conversion and treat “unlimited” storage correctlyUse 1024×1024×1024 to match binary units and fix progress/max computation; also avoid showing “/ 0 GB” and don’t render progress when unlimited.
function formatBandwidthUsage(currentBytes: number, maxGB?: number): string { const currentSize = humanFileSize(currentBytes || 0); if (!maxGB) { return `${currentSize.value} ${currentSize.unit} / Unlimited`; } - const maxSize = humanFileSize(maxGB * 1000 * 1000 * 1000); + const maxSize = humanFileSize(maxGB * 1024 * 1024 * 1024); return `${currentSize.value} ${currentSize.unit} / ${maxSize.value} ${maxSize.unit}`; } @@ function createStorageProgressData( currentBytes: number, maxGB: number ): Array<{ size: number; color: string; tooltip?: { title: string; label: string } }> { - if (maxGB <= 0) return []; + if (maxGB <= 0) return []; - const maxBytes = maxGB * 1000 * 1000 * 1000; + const maxBytes = maxGB * 1024 * 1024 * 1024; const percentage = Math.min((currentBytes / maxBytes) * 100, 100); @@ // Bandwidth { id: `project-${project.projectId}-bandwidth`, cells: { item: 'Bandwidth', - usage: `${formatBandwidthUsage(project.bandwidth.value, currentPlan?.bandwidth)}`, - price: formatCurrency(project.bandwidth.amount || 0) + usage: `${formatBandwidthUsage(project.bandwidth?.value ?? 0, currentPlan?.bandwidth)}`, + price: formatCurrency(project.bandwidth?.amount ?? 0) }, progressData: createStorageProgressData( - project.bandwidth.value || 0, - currentPlan?.bandwidth || 0 + project.bandwidth?.value ?? 0, + currentPlan?.bandwidth || 0 ), maxValue: currentPlan?.bandwidth - ? currentPlan.bandwidth * 1000 * 1000 * 1000 - : 0 + ? currentPlan.bandwidth * 1024 * 1024 * 1024 + : 0 }, @@ // Storage { id: `project-${project.projectId}-storage`, cells: { item: 'Storage', - usage: `${formatHumanSize(project.storage.value || 0)} / ${currentPlan?.storage?.toString() || '0'} GB`, - price: formatCurrency(project.storage.amount || 0) + usage: `${formatHumanSize(project.storage?.value ?? 0)} / ${currentPlan?.storage ? `${currentPlan.storage} GB` : 'Unlimited'}`, + price: formatCurrency(project.storage?.amount ?? 0) }, - progressData: createStorageProgressData( - project.storage.value || 0, - currentPlan?.storage || 0 - ), - maxValue: currentPlan?.storage - ? currentPlan.storage * 1000 * 1000 * 1000 - : 0 + progressData: currentPlan?.storage + ? createStorageProgressData( + project.storage?.value ?? 0, + currentPlan.storage + ) + : [], + maxValue: currentPlan?.storage + ? currentPlan.storage * 1024 * 1024 * 1024 + : null },Also applies to: 85-93, 191-201, 270-273
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (1)
pnpm-lock.yaml
is excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (4)
package.json
(1 hunks)src/lib/layout/createProject.svelte
(5 hunks)src/lib/sdk/billing.ts
(6 hunks)src/routes/(console)/organization-[organization]/billing/planSummary.svelte
(4 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- package.json
- src/lib/layout/createProject.svelte
// Bandwidth | ||
{ | ||
id: `project-${project.projectId}-bandwidth`, | ||
cells: { | ||
item: 'Bandwidth', | ||
usage: `${formatBandwidthUsage(project.bandwidth.value, currentPlan?.bandwidth)}`, | ||
price: formatCurrency(project.bandwidth.amount || 0) | ||
}, | ||
progressData: createStorageProgressData( | ||
project.bandwidth.value || 0, | ||
currentPlan?.bandwidth || 0 | ||
), | ||
maxValue: currentPlan?.bandwidth | ||
? currentPlan.bandwidth * 1000 * 1000 * 1000 | ||
: 0 | ||
}, | ||
// Users | ||
{ | ||
id: `project-${project.projectId}-users`, | ||
cells: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard resource lookups; current code will crash when a resource is missing
project.<resource>.value
and .amount
assume .find(...)
always succeeds. Use optional chaining and defaults.
-usage: `${formatBandwidthUsage(project.bandwidth.value, currentPlan?.bandwidth)}`,
-price: formatCurrency(project.bandwidth.amount || 0)
+usage: `${formatBandwidthUsage(project.bandwidth?.value ?? 0, currentPlan?.bandwidth)}`,
+price: formatCurrency(project.bandwidth?.amount ?? 0)
@@
-usage: `${formatNum(project.users.value || 0)} / ${currentPlan?.users ? formatNum(currentPlan.users) : 'Unlimited'}`,
-price: formatCurrency(project.users.amount || 0)
+usage: `${formatNum(project.users?.value ?? 0)} / ${currentPlan?.users ? formatNum(currentPlan.users) : 'Unlimited'}`,
+price: formatCurrency(project.users?.amount ?? 0)
@@
-usage: `${formatNum(project.databasesReads.value || 0)} / ${currentPlan?.databasesReads ? formatNum(currentPlan.databasesReads) : 'Unlimited'}`,
-price: formatCurrency(project.databasesReads.amount || 0)
+usage: `${formatNum(project.databasesReads?.value ?? 0)} / ${currentPlan?.databasesReads ? formatNum(currentPlan.databasesReads) : 'Unlimited'}`,
+price: formatCurrency(project.databasesReads?.amount ?? 0)
@@
-usage: `${formatNum(project.databasesWrites.value || 0)} / ${currentPlan?.databasesWrites ? formatNum(currentPlan.databasesWrites) : 'Unlimited'}`,
-price: formatCurrency(project.databasesWrites.amount || 0)
+usage: `${formatNum(project.databasesWrites?.value ?? 0)} / ${currentPlan?.databasesWrites ? formatNum(currentPlan.databasesWrites) : 'Unlimited'}`,
+price: formatCurrency(project.databasesWrites?.amount ?? 0)
@@
-usage: `${formatNum(project.executions.value || 0)} / ${currentPlan?.executions ? formatNum(currentPlan.executions) : 'Unlimited'}`,
-price: formatCurrency(project.executions.amount || 0)
+usage: `${formatNum(project.executions?.value ?? 0)} / ${currentPlan?.executions ? formatNum(currentPlan.executions) : 'Unlimited'}`,
+price: formatCurrency(project.executions?.amount ?? 0)
@@
-usage: `${formatHumanSize(project.storage.value || 0)} / ${currentPlan?.storage?.toString() || '0'} GB`,
-price: formatCurrency(project.storage.amount || 0)
+usage: `${formatHumanSize(project.storage?.value ?? 0)} / ${currentPlan?.storage ? `${currentPlan.storage} GB` : 'Unlimited'}`,
+price: formatCurrency(project.storage?.amount ?? 0)
@@
-usage: `${formatNum(project.gbHours.value || 0)} / ${currentPlan?.GBHours ? formatNum(currentPlan.GBHours) : 'Unlimited'}`,
-price: formatCurrency(project.gbHours.amount || 0)
+usage: `${formatNum(project.gbHours?.value ?? 0)} / ${currentPlan?.GBHours ? formatNum(currentPlan.GBHours) : 'Unlimited'}`,
+price: formatCurrency(project.gbHours?.amount ?? 0)
@@
-usage: `${formatNum(project.authPhone.value || 0)} SMS messages`,
-price: formatCurrency(project.authPhone.amount || 0)
+usage: `${formatNum(project.authPhone?.value ?? 0)} SMS messages`,
+price: formatCurrency(project.authPhone?.amount ?? 0)
Also applies to: 206-215, 218-229, 232-243, 246-257, 260-265, 276-286, 289-295
🤖 Prompt for AI Agents
In src/routes/(console)/organization-[organization]/billing/planSummary.svelte
around lines 186-205 (and also apply same changes at 206-215, 218-229, 232-243,
246-257, 260-265, 276-286, 289-295), resource lookups assume project.<resource>
exists and directly access .value and .amount which will throw if find(...)
returned undefined; update all such accesses to use optional chaining and safe
fallbacks (e.g. resource?.value ?? 0 and resource?.amount ?? 0) for usage and
price, pass those fallbacks into
formatBandwidthUsage/formatCurrency/createStorageProgressData, and compute
maxValue with a safe numeric default so missing resources no longer crash the
page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/lib/components/billing/planExcess.svelte (2)
15-16
: Fix missing type import and unused import.
AggregationTeam
is used but not imported;Aggregation
is imported but unused, triggering ESLint no-undef. Replace the import and update the declaration.-import type { Aggregation } from '$lib/sdk/billing'; +import type { AggregationTeam } from '$lib/sdk/billing'; @@ -let aggregation: AggregationTeam = null; +let aggregation: AggregationTeam | null = null;Also applies to: 31-31
24-31
: Allow null forexcess
at initialization.You assign
null
to a non-null type. Make the type nullable to satisfy TS and match runtime.-let excess: { - bandwidth?: number; - storage?: number; - users?: number; - executions?: number; - members?: number; -} = null; +let excess: { + bandwidth?: number; + storage?: number; + users?: number; + executions?: number; + members?: number; +} | null = null;
♻️ Duplicate comments (8)
src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts (2)
13-14
: Fix: variables initialized to undefined must be union-typed.These are declared as non-nullable but initialized with undefined; this breaks under strict null checks.
Apply:
- let currentInvoice: Invoice = undefined; - let currentAggregation: AggregationTeam = undefined; + let currentInvoice: Invoice | undefined = undefined; + let currentAggregation: AggregationTeam | undefined = undefined;
50-52
: Remove explicit any in find callback; use SDK type or inference.This fixes ESLint no-explicit-any.
Apply either (typed) or (inferred) variant:
- const executionsResource = projectSpecificData.resources?.find?.( - (r: any) => r.resourceId === 'executions' - ); + const executionsResource = projectSpecificData.resources?.find( + (r: InvoiceUsage) => r.resourceId === 'executions' + );or
- const executionsResource = projectSpecificData.resources?.find?.( - (r: any) => r.resourceId === 'executions' - ); + const executionsResource = projectSpecificData.resources?.find( + (r) => r.resourceId === 'executions' + );Also, the extra optional chaining on method is unnecessary; prefer
.resources?.find(...)
.src/lib/components/billing/planExcess.svelte (1)
11-14
: Show “Unlimited” instead of “Infinity members”.Add a small formatter and use it for the Members cell.
import { abbreviateNumber } from '$lib/helpers/numbers'; @@ +const formatSeatsLimit = (n: number) => + (n === Infinity || n >= Number.MAX_SAFE_INTEGER) ? 'Unlimited' : abbreviateNumber(n); @@ - <Table.Cell {root}>{getServiceLimit('members', tier)} members</Table.Cell> + <Table.Cell {root}>{formatSeatsLimit(getServiceLimit('members', tier))} members</Table.Cell>Also applies to: 73-73
src/routes/(console)/organization-[organization]/billing/planSummary.svelte (5)
299-305
: Quick repo scan to catch any remaining HTML injection patterns.#!/bin/bash # Find risky HTML injections in Svelte files rg -nP --type=svelte '@html' -C2 rg -nP --type=svelte "includes\\('<a href=" -C2Also applies to: 394-396
56-60
: BroadencreateProgressData
signature to match call sites and avoid TS errors.
maxValue
is passed asnumber | undefined
in many places. Update the signature and keep the existing null/undefined guards.-function createProgressData( - currentValue: number, - maxValue: number | string -): Array<{ size: number; color: string; tooltip?: { title: string; label: string } }> { +function createProgressData( + currentValue: number, + maxValue: number | string | null | undefined +): Array<{ size: number; color: string; tooltip?: { title: string; label: string } }> {
38-45
: Use 1024-based conversion for bytes↔GB to match humanFileSize and avoid ~7% drift.-const maxSize = humanFileSize(maxGB * 1000 * 1000 * 1000); +const maxSize = humanFileSize(maxGB * 1024 * 1024 * 1024); @@ -const maxBytes = maxGB * 1000 * 1000 * 1000; +const maxBytes = maxGB * 1024 * 1024 * 1024;Also applies to: 86-94
191-202
: Guard resource lookups; current code will throw when a resource is missing.All
project.<resource>.value/amount
assume.find(...)
succeeded. Use optional chaining and safe fallbacks. Also avoid progress on unlimited caps and show “Unlimited” instead of “/ 0 GB”.-usage: `${formatBandwidthUsage(project.bandwidth.value, currentPlan?.bandwidth)}`, -price: formatCurrency(project.bandwidth.amount || 0) +usage: `${formatBandwidthUsage(project.bandwidth?.value ?? 0, currentPlan?.bandwidth)}`, +price: formatCurrency(project.bandwidth?.amount ?? 0) @@ -progressData: createStorageProgressData( - project.bandwidth.value || 0, - currentPlan?.bandwidth || 0 -), -maxValue: currentPlan?.bandwidth - ? currentPlan.bandwidth * 1000 * 1000 * 1000 - : 0 +progressData: currentPlan?.bandwidth + ? createStorageProgressData(project.bandwidth?.value ?? 0, currentPlan.bandwidth) + : [], +maxValue: currentPlan?.bandwidth + ? currentPlan.bandwidth * 1024 * 1024 * 1024 + : null @@ -usage: `${formatNum(project.users.value || 0)} / ${currentPlan?.users ? formatNum(currentPlan.users) : 'Unlimited'}`, -price: formatCurrency(project.users.amount || 0) +usage: `${formatNum(project.users?.value ?? 0)} / ${currentPlan?.users ? formatNum(currentPlan.users) : 'Unlimited'}`, +price: formatCurrency(project.users?.amount ?? 0) @@ -progressData: createProgressData( - project.users.value || 0, - currentPlan?.users -), +progressData: createProgressData(project.users?.value ?? 0, currentPlan?.users), @@ -usage: `${formatNum(project.databasesReads.value || 0)} / ${currentPlan?.databasesReads ? formatNum(currentPlan.databasesReads) : 'Unlimited'}`, -price: formatCurrency(project.databasesReads.amount || 0) +usage: `${formatNum(project.databasesReads?.value ?? 0)} / ${currentPlan?.databasesReads ? formatNum(currentPlan.databasesReads) : 'Unlimited'}`, +price: formatCurrency(project.databasesReads?.amount ?? 0) @@ -progressData: createProgressData( - project.databasesReads.value || 0, - currentPlan?.databasesReads -), +progressData: createProgressData(project.databasesReads?.value ?? 0, currentPlan?.databasesReads), @@ -usage: `${formatNum(project.databasesWrites.value || 0)} / ${currentPlan?.databasesWrites ? formatNum(currentPlan.databasesWrites) : 'Unlimited'}`, -price: formatCurrency(project.databasesWrites.amount || 0) +usage: `${formatNum(project.databasesWrites?.value ?? 0)} / ${currentPlan?.databasesWrites ? formatNum(currentPlan.databasesWrites) : 'Unlimited'}`, +price: formatCurrency(project.databasesWrites?.amount ?? 0) @@ -progressData: createProgressData( - project.databasesWrites.value || 0, - currentPlan?.databasesWrites -), +progressData: createProgressData(project.databasesWrites?.value ?? 0, currentPlan?.databasesWrites), @@ -usage: `${formatNum(project.executions.value || 0)} / ${currentPlan?.executions ? formatNum(currentPlan.executions) : 'Unlimited'}`, -price: formatCurrency(project.executions.amount || 0) +usage: `${formatNum(project.executions?.value ?? 0)} / ${currentPlan?.executions ? formatNum(currentPlan.executions) : 'Unlimited'}`, +price: formatCurrency(project.executions?.amount ?? 0) @@ -progressData: createProgressData( - project.executions.value || 0, - currentPlan?.executions -), +progressData: createProgressData(project.executions?.value ?? 0, currentPlan?.executions), @@ -usage: `${formatHumanSize(project.storage.value || 0)} / ${currentPlan?.storage?.toString() || '0'} GB`, -price: formatCurrency(project.storage.amount || 0) +usage: `${formatHumanSize(project.storage?.value ?? 0)} / ${currentPlan?.storage ? `${currentPlan.storage} GB` : 'Unlimited'}`, +price: formatCurrency(project.storage?.amount ?? 0) @@ -progressData: createStorageProgressData( - project.storage.value || 0, - currentPlan?.storage || 0 -), -maxValue: currentPlan?.storage - ? currentPlan.storage * 1000 * 1000 * 1000 - : 0 +progressData: currentPlan?.storage + ? createStorageProgressData(project.storage?.value ?? 0, currentPlan.storage) + : [], +maxValue: currentPlan?.storage + ? currentPlan.storage * 1024 * 1024 * 1024 + : null @@ -usage: `${formatNum(project.gbHours.value || 0)} / ${currentPlan?.GBHours ? formatNum(currentPlan.GBHours) : 'Unlimited'}`, -price: formatCurrency(project.gbHours.amount || 0) +usage: `${formatNum(project.gbHours?.value ?? 0)} / ${currentPlan?.GBHours ? formatNum(currentPlan.GBHours) : 'Unlimited'}`, +price: formatCurrency(project.gbHours?.amount ?? 0) @@ -usage: `${formatNum(project.authPhone.value || 0)} SMS messages`, -price: formatCurrency(project.authPhone.amount || 0) +usage: `${formatNum(project.authPhone?.value ?? 0)} SMS messages`, +price: formatCurrency(project.authPhone?.amount ?? 0)Also applies to: 206-216, 222-230, 236-244, 249-258, 262-273, 279-287, 293-295
299-305
: Stop injecting HTML; render anchors safely.String HTML with
{@html}
is XSS-prone. Passhref
and render<a>
in markup.-{ - id: `project-${project.projectId}-usage-details`, - cells: { - item: `<a href="${base}/project-${project.region}-${project.projectId}/settings/usage" style="text-decoration: underline; color: var(--fgcolor-accent-neutral);">Usage details</a>`, - usage: '', - price: '' - } -} +{ + id: `project-${project.projectId}-usage-details`, + cells: { item: 'Usage details', usage: '', price: '' }, + href: `${base}/project-${project.region}-${project.projectId}/settings/usage` +} @@ -{#if child.cells?.[col.id]?.includes('<a href=')} - {@html child.cells?.[col.id] ?? ''} +{#if col.id === 'item' && child.href} + <a href={child.href} style="text-decoration: underline; color: var(--fgcolor-accent-neutral);"> + Usage details + </a>Also applies to: 394-396
🧹 Nitpick comments (5)
src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts (2)
25-33
: Don’t swallow non-404 errors when fetching default aggregation.Only ignore “not found”; bubble up others.
Apply:
- } else { - try { - currentAggregation = await sdk.forConsole.billing.getAggregation( - organization.$id, - organization.billingAggregationId - ); - } catch (e) { - // ignore error if no aggregation found - } - } + } else { + try { + currentAggregation = await sdk.forConsole.billing.getAggregation( + organization.$id, + organization.billingAggregationId + ); + } catch (e: unknown) { + // ignore only "not found" + const code = + typeof e === 'object' && e !== null && 'code' in e + ? (e as { code?: number }).code + : undefined; + if (code !== 404) throw e; + } + }
41-48
: Type projectSpecificData and prefer undefined over null.Improves inference for the subsequent find and removes implicit any risk.
Apply:
- let projectSpecificData = null; - if (currentAggregation.breakdown) { + let projectSpecificData: AggregationBreakdown | undefined; + if (currentAggregation.breakdown?.length) { projectSpecificData = currentAggregation.breakdown.find( (p) => p.$id === project ); }src/lib/components/billing/planExcess.svelte (1)
23-23
: Makeplan
reactive to store/tier changes.Using
$plansInfo
in a const won’t react to store updates; derive it reactively so it stays in sync.-const plan = $plansInfo?.get(tier); +let plan; +$: plan = $plansInfo?.get(tier);src/routes/(console)/organization-[organization]/billing/planSummary.svelte (2)
188-202
: Bandwidth progress should use 1024 base inmaxValue
too.Align with the earlier 1024 fix so the bar matches the label.
-maxValue: currentPlan?.bandwidth - ? currentPlan.bandwidth * 1000 * 1000 * 1000 - : 0 +maxValue: currentPlan?.bandwidth + ? currentPlan.bandwidth * 1024 * 1024 * 1024 + : null
1-729
: Optional: centralize resource access helpers.Consider small helpers like
valueOf(project, 'storage')
to reduce repetition and mistakes across rows.I can draft a minimal helper set that handles array/record shapes and undefineds if you want.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (4)
src/lib/components/billing/planExcess.svelte
(3 hunks)src/routes/(console)/organization-[organization]/billing/+page.svelte
(3 hunks)src/routes/(console)/organization-[organization]/billing/planSummary.svelte
(4 hunks)src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts
(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/routes/(console)/organization-[organization]/billing/+page.svelte
🧰 Additional context used
🧬 Code graph analysis (1)
src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts (2)
src/lib/sdk/billing.ts (1)
AggregationTeam
(154-211)src/lib/stores/sdk.ts (1)
sdk
(142-165)
🪛 ESLint
src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts
[error] 51-51: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
src/lib/components/billing/planExcess.svelte
[error] 31-31: 'AggregationTeam' is not defined.
(no-undef)
🔇 Additional comments (3)
src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts (1)
57-60
: LGTM: sensible fallback to aggregation totals.Good fallback when per-project breakdown is absent.
src/routes/(console)/organization-[organization]/billing/planSummary.svelte (2)
326-326
: Confirm total uses numeric amounts, not parsed strings.Looks good:
totalAmount
is derived from numeric fields. Keep it numeric-only upstream.
354-487
: Table rendering changes look solid.Expandable layout, responsive tweaks, and totals row are coherent and read well. Once the XSS and null-guards are fixed, this section should be production-ready.
@@ -1,4 +1,4 @@ | |||
import type { Aggregation, Invoice } from '$lib/sdk/billing'; | |||
import type { AggregationTeam, Invoice } from '$lib/sdk/billing'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add missing SDK types to support proper typing below.
Apply:
-import type { AggregationTeam, Invoice } from '$lib/sdk/billing';
+import type {
+ AggregationTeam,
+ AggregationBreakdown,
+ Invoice,
+ InvoiceUsage
+} from '$lib/sdk/billing';
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
import type { AggregationTeam, Invoice } from '$lib/sdk/billing'; | |
import type { | |
AggregationTeam, | |
AggregationBreakdown, | |
Invoice, | |
InvoiceUsage | |
} from '$lib/sdk/billing'; |
🤖 Prompt for AI Agents
In
src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts
around line 1, the file currently imports only AggregationTeam and Invoice but
later code references additional SDK types; update the import from
'$lib/sdk/billing' to include all types used in this module (add the missing SDK
types referenced later in the file so variables and function signatures are
properly typed), then run a quick type-check to ensure no unresolved type names
remain.
usage.executionsTotal = executionsResource.value || usage.executionsTotal; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correctness: use nullish coalescing, not logical OR, to preserve zero values.
0 is valid usage; ||
would discard it.
Apply:
- usage.executionsTotal = executionsResource.value || usage.executionsTotal;
+ usage.executionsTotal = executionsResource.value ?? usage.executionsTotal;
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
usage.executionsTotal = executionsResource.value || usage.executionsTotal; | |
} | |
usage.executionsTotal = executionsResource.value ?? usage.executionsTotal; | |
} |
🤖 Prompt for AI Agents
In
src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts
around lines 54-55, the assignment uses the logical OR operator which will drop
valid zero values; change the fallback to use the nullish coalescing operator
(??) so that executionsResource.value is used when it is 0 but falls back only
when it is null or undefined—also scan for any other similar assignments in this
file and replace || fallbacks with ?? to preserve zeroes, ensuring your TS
target supports nullish coalescing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/lib/stores/billing.ts (1)
622-629
: Unit mix-up and missing users surplus in calculateExcess.
- executions shouldn’t use 'GB' (counts, not bytes) → surplus will always be 0 now.
- bandwidth likely in GB on plan vs bytes in usage → pass 'GB'.
- users surplus is omitted → Users row never renders.
-export function calculateExcess(addon: AggregationTeam, plan: Plan) { - return { - bandwidth: calculateResourceSurplus(addon.usageBandwidth, plan.bandwidth), - storage: calculateResourceSurplus(addon.usageStorage, plan.storage, 'GB'), - executions: calculateResourceSurplus(addon.usageExecutions, plan.executions, 'GB'), - members: addon.additionalMembers - }; -} +export function calculateExcess(addon: AggregationTeam, plan: Plan) { + return { + bandwidth: calculateResourceSurplus(addon.usageBandwidth, plan.bandwidth, 'GB'), + storage: calculateResourceSurplus(addon.usageStorage, plan.storage, 'GB'), + executions: calculateResourceSurplus(addon.usageExecutions, plan.executions), + users: calculateResourceSurplus(addon.usageUsers, plan.users), + members: addon.additionalMembers + }; +}Also verify the Users row in planExcess.svelte shows up after this change.
♻️ Duplicate comments (3)
src/lib/components/billing/planExcess.svelte (1)
73-73
: Show “Unlimited” instead of “Infinity members”.
This still renders “Infinity members” when Pro/Scale returns Infinity. Format consistently with Usage Rates.Apply:
-<Table.Cell {root}>{getServiceLimit('members', tier)} members</Table.Cell> +<Table.Cell {root}>{formatSeatsLimit(getServiceLimit('members', tier))} members</Table.Cell>Add helper in the script:
const formatSeatsLimit = (n: number) => (n === Infinity || n >= Number.MAX_SAFE_INTEGER) ? 'Unlimited' : abbreviateNumber(n);src/lib/stores/billing.ts (2)
165-171
: Runtime crash risk: unsafe optional access on addons.seats.
Indexingplan?.['addons']['seats']
throws whenaddons
is undefined. Use safe chaining.- return (plan?.['addons']['seats'] || [])['limit'] ?? 1; + return plan?.addons?.seats?.limit ?? 1;Optional: treat backend “0 means unlimited” if applicable:
const limit = plan?.addons?.seats?.limit ?? 1; return limit === 0 ? Infinity : limit;
342-344
: Project limit checks are being skipped for existing orgs.
These early returns bypass the Free plan projects cap for any org with a projects array (including non-empty), so the alert never shows when over limit.- if (!org.projects) return; - if (org.projects.length > 0) return; + // Rely on orgProjectCount for enforcement even when org already has projects.Ensure callers pass
orgProjectCount
; default to0
if undefined in upstream.
🧹 Nitpick comments (3)
src/lib/components/billing/planExcess.svelte (1)
31-31
: Fix nullable typing for aggregation.
Initialize with a nullable type to satisfy strict TS.-let aggregation: AggregationTeam = null; +let aggregation: AggregationTeam | null = null;src/lib/stores/billing.ts (2)
274-274
: Comment clarifier.
The switch still includes 'members'; with the above guard it applies only to Free. Keep comment in sync if logic changes.
285-291
: Optional: pass tier to getServiceLimit in isServiceLimited.
Avoids coupling to global org state and makes the check deterministic for hypothetical tiers.- const limit = getServiceLimit(serviceId) || Infinity; + const limit = getServiceLimit(serviceId, plan) || Infinity;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
src/lib/components/billing/planExcess.svelte
(3 hunks)src/lib/stores/billing.ts
(7 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/lib/stores/billing.ts (2)
src/lib/stores/organization.ts (3)
organization
(62-62)currentPlan
(63-63)members
(64-64)src/lib/sdk/billing.ts (2)
AggregationTeam
(154-211)Plan
(377-433)
🔇 Additional comments (6)
src/lib/components/billing/planExcess.svelte (2)
2-8
: LGTM: Imports align with new billing store API.
No issues spotted with added imports (calculateExcess, getServiceLimit, Tier).
19-19
: Type alignment: AggregationTeam import is correct.
Matches SDK change and downstream usage.src/lib/stores/billing.ts (4)
17-18
: LGTM: Updated type import (AggregationTeam).
Consistent with SDK changes across the PR.
243-243
: LGTM: useNewPricingModal derived store.
Simple, side-effect-free boolean gate.
262-268
: LGTM: Members not limited on Pro/Scale.
Guard correctly short-circuits before the switch.
401-403
: LGTM: Overflow calc respects Infinity for Pro/Scale.
Prevents false positives when seats are unlimited.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 7
♻️ Duplicate comments (5)
src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts (3)
1-1
: Add missing SDK types to enable strict typing and fix lint.Import AggregationBreakdown and InvoiceUsage so we can type breakdown entries and resources below.
-import type { AggregationTeam, Invoice } from '$lib/sdk/billing'; +import type { + AggregationTeam, + AggregationBreakdown, + Invoice, + InvoiceUsage +} from '$lib/sdk/billing';
48-50
: Remove any in resources find; use SDK type.Fixes @typescript-eslint/no-explicit-any.
- const executionsResource = projectSpecificData.resources?.find?.( - (r: any) => r.resourceId === 'executions' - ); + const executionsResource = projectSpecificData.resources?.find?.( + (r: InvoiceUsage) => r.resourceId === 'executions' + );
52-53
: Correctness: preserve zero with nullish coalescing.0 is valid usage;
||
drops it.- usage.executionsTotal = executionsResource.value || usage.executionsTotal; + usage.executionsTotal = executionsResource.value ?? usage.executionsTotal;src/routes/(console)/organization-[organization]/billing/planSummary.svelte (2)
33-37
: Column sizing: verifyfr
usage matches other tables.Prior feedback suggested avoiding
fr
for consistency; considerauto
orminmax
like other table types.
316-329
: Confirm “No projects found” row with design.Ensure this row is in the spec and appears only when expected.
🧹 Nitpick comments (6)
src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts (4)
13-15
: Prefer union types over assigning undefined to non-nullables.Avoids TS strictNullChecks friction.
- let currentInvoice: Invoice = undefined; - let currentAggregation: AggregationTeam = undefined; + let currentInvoice: Invoice | undefined; + let currentAggregation: AggregationTeam | undefined;
25-33
: Align usage window to aggregation period when no invoice.Otherwise getUsage may query the wrong window.
} else { try { - currentAggregation = await sdk.forConsole.billing.getAggregation( + currentAggregation = await sdk.forConsole.billing.getAggregation( organization.$id, organization.billingAggregationId ); + if (currentAggregation) { + // Ensure usage query matches aggregation period + startDate = currentAggregation.from; + endDate = currentAggregation.to; + } } catch (e) { // ignore error if no aggregation found } }
41-46
: Type project-specific breakdown entry.Improves safety on resources access.
- let projectSpecificData = null; - if (currentAggregation.breakdown) { - projectSpecificData = currentAggregation.breakdown.find((p) => p.$id === project); - } + let projectSpecificData: AggregationBreakdown | undefined = + currentAggregation.breakdown?.find((p) => p.$id === project);
55-58
: Consider overriding all related totals for consistency.If you intend to mirror aggregation totals, also set bandwidth/realtime (and any other totals exposed) to avoid mixed sources.
src/routes/(console)/organization-[organization]/billing/planSummary.svelte (2)
62-75
: BroadencreateProgressData
params to accept undefined/null.Call sites pass
currentPlan?.xxx
; update types and guards to avoid TS/runtime edge cases.- function createProgressData( - currentValue: number, - maxValue: number | string + function createProgressData( + currentValue: number | null | undefined, + maxValue: number | string | null | undefined ): Array<{ size: number; color: string; tooltip?: { title: string; label: string } }> { - if ( - maxValue === null || - maxValue === undefined || - (typeof maxValue === 'number' && maxValue <= 0) - ) { + if ( + maxValue == null || + currentValue == null || + (typeof maxValue === 'number' && maxValue <= 0) + ) { return []; } - const max = typeof maxValue === 'string' ? parseFloat(maxValue) : maxValue; - if (max <= 0) return []; + const max = typeof maxValue === 'string' ? parseFloat(maxValue) : maxValue; + if (!Number.isFinite(max) || max <= 0) return []; - const percentage = Math.min((currentValue / max) * 100, 100); + const safeCurrent = Math.max(0, Number(currentValue) || 0); + const percentage = Math.min((safeCurrent / max) * 100, 100); @@ - size: currentValue, + size: safeCurrent,
339-356
: Guard dates for robustness.If billing dates are missing/invalid, consider fallbacks to avoid “Invalid Date”.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
src/routes/(console)/organization-[organization]/billing/planSummary.svelte
(4 hunks)src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts
(3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts (2)
src/lib/sdk/billing.ts (1)
AggregationTeam
(154-211)src/lib/stores/sdk.ts (1)
sdk
(142-165)
🪛 ESLint
src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts
[error] 49-49: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
🔇 Additional comments (1)
src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts (1)
61-61
: Verify whether executions/storage need accumulation too.If accumulateUsage is required for non-users series, apply it similarly for executions/storage to keep charts/totals coherent.
function formatBandwidthUsage(currentBytes: number, maxGB?: number): string { | ||
const currentSize = humanFileSize(currentBytes || 0); | ||
if (!maxGB) { | ||
return `${currentSize.value} ${currentSize.unit} / Unlimited`; | ||
} | ||
const maxSize = humanFileSize(maxGB * 1000 * 1000 * 1000); | ||
return `${currentSize.value} ${currentSize.unit} / ${maxSize.value} ${maxSize.unit}`; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix bytes↔GB conversion (use 1024, not 1000).
Bandwidth display uses decimal conversion, while humanFileSize
is binary; this skews values and progress.
- const maxSize = humanFileSize(maxGB * 1000 * 1000 * 1000);
+ const maxSize = humanFileSize(maxGB * 1024 * 1024 * 1024);
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
function formatBandwidthUsage(currentBytes: number, maxGB?: number): string { | |
const currentSize = humanFileSize(currentBytes || 0); | |
if (!maxGB) { | |
return `${currentSize.value} ${currentSize.unit} / Unlimited`; | |
} | |
const maxSize = humanFileSize(maxGB * 1000 * 1000 * 1000); | |
return `${currentSize.value} ${currentSize.unit} / ${maxSize.value} ${maxSize.unit}`; | |
} | |
function formatBandwidthUsage(currentBytes: number, maxGB?: number): string { | |
const currentSize = humanFileSize(currentBytes || 0); | |
if (!maxGB) { | |
return `${currentSize.value} ${currentSize.unit} / Unlimited`; | |
} | |
const maxSize = humanFileSize(maxGB * 1024 * 1024 * 1024); | |
return `${currentSize.value} ${currentSize.unit} / ${maxSize.value} ${maxSize.unit}`; | |
} |
🤖 Prompt for AI Agents
In src/routes/(console)/organization-[organization]/billing/planSummary.svelte
around lines 44 to 51, the max GB value is converted to bytes using decimal
(1000^3) which conflicts with the binary units expected by humanFileSize; change
the multiplication to use 1024^3 (e.g. maxGB * 1024 * 1024 * 1024 or
Math.pow(1024,3)) so the maxSize calculation uses binary bytes and the displayed
values/progress match humanFileSize's units.
function createStorageProgressData( | ||
currentBytes: number, | ||
maxGB: number | ||
): Array<{ size: number; color: string; tooltip?: { title: string; label: string } }> { | ||
if (maxGB <= 0) return []; | ||
|
||
const maxBytes = maxGB * 1000 * 1000 * 1000; | ||
const percentage = Math.min((currentBytes / maxBytes) * 100, 100); | ||
const progressColor = getProgressColor(percentage); | ||
|
||
const currentSize = humanFileSize(currentBytes); | ||
|
||
return [ | ||
{ | ||
size: currentBytes, | ||
color: progressColor, | ||
tooltip: { | ||
title: `${percentage.toFixed(0)}% used`, | ||
label: `${currentSize.value} ${currentSize.unit} of ${maxGB} GB` | ||
} | ||
} | ||
]; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Align storage progress math to 1024-based bytes.
Progress percentage and tooltip are off by using 1000^3.
- function createStorageProgressData(
+ function createStorageProgressData(
currentBytes: number,
maxGB: number
): Array<{ size: number; color: string; tooltip?: { title: string; label: string } }> {
if (maxGB <= 0) return [];
- const maxBytes = maxGB * 1000 * 1000 * 1000;
+ const maxBytes = maxGB * 1024 * 1024 * 1024;
const percentage = Math.min((currentBytes / maxBytes) * 100, 100);
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
function createStorageProgressData( | |
currentBytes: number, | |
maxGB: number | |
): Array<{ size: number; color: string; tooltip?: { title: string; label: string } }> { | |
if (maxGB <= 0) return []; | |
const maxBytes = maxGB * 1000 * 1000 * 1000; | |
const percentage = Math.min((currentBytes / maxBytes) * 100, 100); | |
const progressColor = getProgressColor(percentage); | |
const currentSize = humanFileSize(currentBytes); | |
return [ | |
{ | |
size: currentBytes, | |
color: progressColor, | |
tooltip: { | |
title: `${percentage.toFixed(0)}% used`, | |
label: `${currentSize.value} ${currentSize.unit} of ${maxGB} GB` | |
} | |
} | |
]; | |
} | |
function createStorageProgressData( | |
currentBytes: number, | |
maxGB: number | |
): Array<{ size: number; color: string; tooltip?: { title: string; label: string } }> { | |
if (maxGB <= 0) return []; | |
// use binary (1024³) bytes to align with humanFileSize and typical filesystem units | |
const maxBytes = maxGB * 1024 * 1024 * 1024; | |
const percentage = Math.min((currentBytes / maxBytes) * 100, 100); | |
const progressColor = getProgressColor(percentage); | |
const currentSize = humanFileSize(currentBytes); | |
return [ | |
{ | |
size: currentBytes, | |
color: progressColor, | |
tooltip: { | |
title: `${percentage.toFixed(0)}% used`, | |
label: `${currentSize.value} ${currentSize.unit} of ${maxGB} GB` | |
} | |
} | |
]; | |
} |
🤖 Prompt for AI Agents
In src/routes/(console)/organization-[organization]/billing/planSummary.svelte
around lines 92 to 114, the storage math uses 1000^3 which causes percentage and
tooltip inaccuracies; change maxBytes to use 1024-based bytes (maxBytes = maxGB
* 1024 * 1024 * 1024) and compute percentage from that value, and for the
tooltip use consistent human-readable units by converting both currentBytes and
maxBytes via humanFileSize (e.g., humanFileSize(maxBytes)) so the label shows
the same unit basis.
cells: { | ||
item: 'Bandwidth', | ||
usage: `${formatBandwidthUsage(project.bandwidth.value, currentPlan?.bandwidth)}`, | ||
price: formatCurrency(project.bandwidth.amount || 0) | ||
}, | ||
progressData: createStorageProgressData( | ||
project.bandwidth.value || 0, | ||
currentPlan?.bandwidth || 0 | ||
), | ||
maxValue: currentPlan?.bandwidth | ||
? currentPlan.bandwidth * 1000 * 1000 * 1000 | ||
: 0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard resource lookups and fix unit conversion for bandwidth.
project.bandwidth
can be undefined; also use 1024^3 and avoid 0
maxValue (treat unlimited as null).
- usage: `${formatBandwidthUsage(project.bandwidth.value, currentPlan?.bandwidth)}`,
- price: formatCurrency(project.bandwidth.amount || 0)
+ usage: `${formatBandwidthUsage(project.bandwidth?.value ?? 0, currentPlan?.bandwidth)}`,
+ price: formatCurrency(project.bandwidth?.amount ?? 0)
@@
- progressData: createStorageProgressData(
- project.bandwidth.value || 0,
- currentPlan?.bandwidth || 0
- ),
- maxValue: currentPlan?.bandwidth
- ? currentPlan.bandwidth * 1000 * 1000 * 1000
- : 0
+ progressData: createStorageProgressData(
+ project.bandwidth?.value ?? 0,
+ currentPlan?.bandwidth ?? 0
+ ),
+ maxValue: currentPlan?.bandwidth
+ ? currentPlan.bandwidth * 1024 * 1024 * 1024
+ : null
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
cells: { | |
item: 'Bandwidth', | |
usage: `${formatBandwidthUsage(project.bandwidth.value, currentPlan?.bandwidth)}`, | |
price: formatCurrency(project.bandwidth.amount || 0) | |
}, | |
progressData: createStorageProgressData( | |
project.bandwidth.value || 0, | |
currentPlan?.bandwidth || 0 | |
), | |
maxValue: currentPlan?.bandwidth | |
? currentPlan.bandwidth * 1000 * 1000 * 1000 | |
: 0 | |
cells: { | |
item: 'Bandwidth', | |
- usage: `${formatBandwidthUsage(project.bandwidth.value, currentPlan?.bandwidth)}`, | |
usage: `${formatBandwidthUsage(project.bandwidth?.value ?? 0, currentPlan?.bandwidth)}`, | |
price: formatCurrency(project.bandwidth?.amount ?? 0) | |
}, | |
- progressData: createStorageProgressData( | |
- project.bandwidth.value || 0, | |
- currentPlan?.bandwidth || 0 | |
- ), | |
- maxValue: currentPlan?.bandwidth | |
- ? currentPlan.bandwidth * 1000 * 1000 * 1000 | |
progressData: createStorageProgressData( | |
project.bandwidth?.value ?? 0, | |
currentPlan?.bandwidth ?? 0 | |
), | |
maxValue: currentPlan?.bandwidth | |
? currentPlan.bandwidth * 1024 * 1024 * 1024 | |
: null |
🤖 Prompt for AI Agents
In src/routes/(console)/organization-[organization]/billing/planSummary.svelte
around lines 196 to 207, guard all accesses to project.bandwidth (it may be
undefined) when computing cells.item/usage/price and progressData, use safe
fallbacks (e.g., 0 or null where appropriate) and compute byte conversion using
1024 * 1024 * 1024 instead of 1000^3; additionally treat an "unlimited" plan
bandwidth by returning null for maxValue rather than 0 so callers can detect
unlimited capacity. Update usage formatting to handle undefined
project.bandwidth, use project.bandwidth.value || 0 where needed, pass null for
maxValue when currentPlan?.bandwidth is absent/indicates unlimited, and ensure
createStorageProgressData receives numeric values (0 fallback) rather than
undefined.
id: `project-${project.projectId}-users`, | ||
cells: { | ||
item: 'Users', | ||
usage: `${formatNum(project.users.value || 0)} / ${currentPlan?.users ? formatNum(currentPlan.users) : 'Unlimited'}`, | ||
price: formatCurrency(project.users.amount || 0) | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Null-safe resource access across all rows.
Direct .value
/.amount
dereferences will throw when the resource is absent.
- usage: `${formatNum(project.users.value || 0)} / ${currentPlan?.users ? formatNum(currentPlan.users) : 'Unlimited'}`,
- price: formatCurrency(project.users.amount || 0)
+ usage: `${formatNum(project.users?.value ?? 0)} / ${currentPlan?.users ? formatNum(currentPlan.users) : 'Unlimited'}`,
+ price: formatCurrency(project.users?.amount ?? 0)
@@
- progressData: createProgressData(
- project.users.value || 0,
+ progressData: createProgressData(
+ project.users?.value ?? 0,
currentPlan?.users
),
@@
- usage: `${formatNum(project.databasesReads.value || 0)} / ${currentPlan?.databasesReads ? formatNum(currentPlan.databasesReads) : 'Unlimited'}`,
- price: formatCurrency(project.databasesReads.amount || 0)
+ usage: `${formatNum(project.databasesReads?.value ?? 0)} / ${currentPlan?.databasesReads ? formatNum(currentPlan.databasesReads) : 'Unlimited'}`,
+ price: formatCurrency(project.databasesReads?.amount ?? 0)
@@
- progressData: createProgressData(
- project.databasesReads.value || 0,
+ progressData: createProgressData(
+ project.databasesReads?.value ?? 0,
currentPlan?.databasesReads
),
@@
- usage: `${formatNum(project.databasesWrites.value || 0)} / ${currentPlan?.databasesWrites ? formatNum(currentPlan.databasesWrites) : 'Unlimited'}`,
- price: formatCurrency(project.databasesWrites.amount || 0)
+ usage: `${formatNum(project.databasesWrites?.value ?? 0)} / ${currentPlan?.databasesWrites ? formatNum(currentPlan.databasesWrites) : 'Unlimited'}`,
+ price: formatCurrency(project.databasesWrites?.amount ?? 0)
@@
- progressData: createProgressData(
- project.databasesWrites.value || 0,
+ progressData: createProgressData(
+ project.databasesWrites?.value ?? 0,
currentPlan?.databasesWrites
),
@@
- usage: `${formatNum(project.executions.value || 0)} / ${currentPlan?.executions ? formatNum(currentPlan.executions) : 'Unlimited'}`,
- price: formatCurrency(project.executions.amount || 0)
+ usage: `${formatNum(project.executions?.value ?? 0)} / ${currentPlan?.executions ? formatNum(currentPlan.executions) : 'Unlimited'}`,
+ price: formatCurrency(project.executions?.amount ?? 0)
@@
- progressData: createProgressData(
- project.executions.value || 0,
+ progressData: createProgressData(
+ project.executions?.value ?? 0,
currentPlan?.executions
),
@@
- usage: `${formatHumanSize(project.storage.value || 0)} / ${currentPlan?.storage?.toString() || '0'} GB`,
- price: formatCurrency(project.storage.amount || 0)
+ usage: `${formatHumanSize(project.storage?.value ?? 0)} / ${currentPlan?.storage ? `${currentPlan.storage} GB` : 'Unlimited'}`,
+ price: formatCurrency(project.storage?.amount ?? 0)
@@
- progressData: createStorageProgressData(
- project.storage.value || 0,
- currentPlan?.storage || 0
- ),
- maxValue: currentPlan?.storage
- ? currentPlan.storage * 1000 * 1000 * 1000
- : 0
+ progressData: currentPlan?.storage
+ ? createStorageProgressData(
+ project.storage?.value ?? 0,
+ currentPlan.storage
+ )
+ : [],
+ maxValue: currentPlan?.storage
+ ? currentPlan.storage * 1024 * 1024 * 1024
+ : null
@@
- usage: `${formatNum(project.gbHours.value || 0)} / ${currentPlan?.GBHours ? formatNum(currentPlan.GBHours) : 'Unlimited'}`,
- price: formatCurrency(project.gbHours.amount || 0)
+ usage: `${formatNum(project.gbHours?.value ?? 0)} / ${currentPlan?.GBHours ? formatNum(currentPlan.GBHours) : 'Unlimited'}`,
+ price: formatCurrency(project.gbHours?.amount ?? 0)
@@
- ? createProgressData(project.gbHours.value || 0, currentPlan.GBHours)
+ ? createProgressData(project.gbHours?.value ?? 0, currentPlan.GBHours)
@@
- usage: `${formatNum(project.authPhone.value || 0)} SMS messages`,
- price: formatCurrency(project.authPhone.amount || 0)
+ usage: `${formatNum(project.authPhone?.value ?? 0)} SMS messages`,
+ price: formatCurrency(project.authPhone?.amount ?? 0)
Also applies to: 227-236, 239-250, 253-264, 267-279, 283-293, 296-301
🤖 Prompt for AI Agents
In src/routes/(console)/organization-[organization]/billing/planSummary.svelte
around lines 211-216 (and similarly for 227-236, 239-250, 253-264, 267-279,
283-293, 296-301), the code dereferences resource properties like
project.users.value and project.users.amount directly which will throw if the
resource is missing; change these accesses to null-safe forms (use optional
chaining and nullish/zero fallbacks such as project.users?.value ?? 0 and
project.users?.amount ?? 0) and apply the same pattern to the other resource
rows, preserving the currentPlan checks (e.g., currentPlan?.users ?
formatNum(currentPlan.users) : 'Unlimited') so usage/price display never throws
when a resource is absent.
{ | ||
id: `project-${project.projectId}-usage-details`, | ||
cells: { | ||
item: `<a href="${base}/project-${project.region}-${project.projectId}/settings/usage" style="text-decoration: underline; color: var(--fgcolor-accent-neutral);">Usage details</a>`, | ||
usage: '', | ||
price: '' | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove {@html} anchor injection; render links safely.
String-injected HTML is XSS-prone and brittle. Pass href separately and render an <a>
element.
- {
- id: `project-${project.projectId}-usage-details`,
- cells: {
- item: `<a href="${base}/project-${project.region}-${project.projectId}/settings/usage" style="text-decoration: underline; color: var(--fgcolor-accent-neutral);">Usage details</a>`,
- usage: '',
- price: ''
- }
- }
+ {
+ id: `project-${project.projectId}-usage-details`,
+ cells: { item: 'Usage details', usage: '', price: '' },
+ href: `${base}/project-${project.region}-${project.projectId}/settings/usage`
+ }
- {#if child.cells?.[col.id]?.includes('<a href=')}
- {@html child.cells?.[col.id] ?? ''}
+ {#if col.id === 'item' && child.href}
+ <a href={child.href} style="text-decoration: underline; color: var(--fgcolor-accent-neutral);">
+ Usage details
+ </a>
Also applies to: 402-404
$: totalAmount = Math.max( | ||
(currentAggregation?.amount || currentPlan?.price || 0) - availableCredit, | ||
0 | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix NaN total when availableCredit
is undefined.
Subtracting undefined
yields NaN; default to 0.
- $: totalAmount = Math.max(
- (currentAggregation?.amount || currentPlan?.price || 0) - availableCredit,
- 0
- );
+ $: totalAmount = Math.max(
+ (currentAggregation?.amount || currentPlan?.price || 0) - (availableCredit ?? 0),
+ 0
+ );
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
$: totalAmount = Math.max( | |
(currentAggregation?.amount || currentPlan?.price || 0) - availableCredit, | |
0 | |
); | |
$: totalAmount = Math.max( | |
(currentAggregation?.amount || currentPlan?.price || 0) - (availableCredit ?? 0), | |
0 | |
); |
🤖 Prompt for AI Agents
In src/routes/(console)/organization-[organization]/billing/planSummary.svelte
around lines 332 to 335, the computed totalAmount can become NaN when
availableCredit is undefined because subtraction with undefined yields NaN;
update the expression to treat availableCredit as 0 when undefined (e.g., use
nullish coalescing or a numeric fallback: availableCredit ?? 0 or
Number(availableCredit) || 0) so the Math.max call always receives a valid
number and never produces NaN.
<ExpandableTable.Row {root} id="total-row" expandable={false}> | ||
<ExpandableTable.Cell | ||
{root} | ||
column="item" | ||
expandable={false} | ||
isOpen={false} | ||
toggle={() => {}}> | ||
<Layout.Stack | ||
inline | ||
direction="row" | ||
gap="xxs" | ||
alignItems="center" | ||
alignContent="center"> | ||
<Icon icon={IconTag} color="--fgcolor-success" size="s" /> | ||
|
||
{#if currentPlan.supportsCredits && availableCredit > 0} | ||
<Layout.Stack direction="row" justifyContent="space-between"> | ||
<Layout.Stack direction="row" alignItems="center" gap="xxs"> | ||
<Icon size="s" icon={IconTag} color="--fgcolor-success" /> | ||
<Typography.Text color="--fgcolor-neutral-primary" | ||
>Credits to be applied</Typography.Text> | ||
>Credits</Typography.Text> | ||
</Layout.Stack> | ||
<Typography.Text color="--fgcolor-success"> | ||
-{formatCurrency( | ||
Math.min(availableCredit, currentInvoice?.amount ?? 0) | ||
)} | ||
</Typography.Text> | ||
</Layout.Stack> | ||
{/if} | ||
|
||
{#if $organization?.billingPlan !== BillingPlan.FREE && $organization?.billingPlan !== BillingPlan.GITHUB_EDUCATION} | ||
<Divider /> | ||
<Layout.Stack direction="row" justifyContent="space-between"> | ||
<Typography.Text color="--fgcolor-neutral-primary" variant="m-500"> | ||
<Layout.Stack direction="row" alignItems="center" gap="s"> | ||
Current total (USD) | ||
<Tooltip> | ||
<Icon icon={IconInfo} /> | ||
<svelte:fragment slot="tooltip"> | ||
Estimates are updated daily and may differ from your | ||
final invoice. | ||
</svelte:fragment> | ||
</Tooltip> | ||
</Layout.Stack> | ||
</ExpandableTable.Cell> | ||
<ExpandableTable.Cell | ||
{root} | ||
column="usage" | ||
expandable={false} | ||
isOpen={false} | ||
toggle={() => {}}> | ||
<Typography.Text variant="m-500" color="--fgcolor-neutral-primary"> | ||
</Typography.Text> | ||
<Typography.Text color="--fgcolor-neutral-primary" variant="m-500"> | ||
{formatCurrency( | ||
Math.max( | ||
(currentInvoice?.amount ?? 0) - | ||
Math.min(availableCredit, currentInvoice?.amount ?? 0), | ||
0 | ||
) | ||
)} | ||
</ExpandableTable.Cell> | ||
<ExpandableTable.Cell | ||
{root} | ||
column="price" | ||
expandable={false} | ||
isOpen={false} | ||
toggle={() => {}}> | ||
<Typography.Text variant="m-500" color="--fgcolor-neutral-primary"> | ||
-{formatCurrency(availableCredit)} | ||
</Typography.Text> | ||
</Layout.Stack> | ||
{/if} | ||
</Layout.Stack> | ||
</Card.Base> | ||
</svelte:fragment> | ||
<svelte:fragment slot="actions"> | ||
</ExpandableTable.Cell> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Duplicate row id “total-row”.
Two rows share the same id; this can break expandable state/ARIA. Make ids unique.
- <ExpandableTable.Row {root} id="total-row" expandable={false}>
+ <ExpandableTable.Row {root} id="credits-row" expandable={false}>
Also applies to: 503-534
🤖 Prompt for AI Agents
In src/routes/(console)/organization-[organization]/billing/planSummary.svelte
around lines 462 to 499 (and similarly at 503 to 534), two ExpandableTable.Row
elements use the same id "total-row", which breaks expandable state/ARIA; change
both rows to use unique ids (e.g., "total-row-credits" and "total-row-usage" or
append the row index/slug), and ensure any references, keys, or
aria-controls/aria-labelledby values that relied on "total-row" are updated to
match the new unique ids so expandable state and accessibility attributes remain
correct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (6)
src/routes/(console)/organization-[organization]/billing/planSummary.svelte (6)
198-207
: Guard resource lookups with optional chaining to prevent crashesDirect
.value
/.amount
derefs will throw when a resource is missing.- usage: `${formatBandwidthUsage(project.bandwidth.value, currentPlan?.bandwidth)}`, - price: formatCurrency(project.bandwidth.amount || 0) + usage: `${formatBandwidthUsage(project.bandwidth?.value ?? 0, currentPlan?.bandwidth)}`, + price: formatCurrency(project.bandwidth?.amount ?? 0) @@ - progressData: createStorageProgressData( - project.bandwidth.value || 0, - currentPlan?.bandwidth || 0 - ), + progressData: createStorageProgressData( + project.bandwidth?.value ?? 0, + currentPlan?.bandwidth ?? 0 + ), @@ - usage: `${formatNum(project.users.value || 0)} / ${currentPlan?.users ? formatNum(currentPlan.users) : 'Unlimited'}`, - price: formatCurrency(project.users.amount || 0) + usage: `${formatNum(project.users?.value ?? 0)} / ${currentPlan?.users ? formatNum(currentPlan.users) : 'Unlimited'}`, + price: formatCurrency(project.users?.amount ?? 0) @@ - progressData: createProgressData( - project.users.value || 0, + progressData: createProgressData( + project.users?.value ?? 0, currentPlan?.users ), @@ - usage: `${formatNum(project.databasesReads.value || 0)} / ${currentPlan?.databasesReads ? formatNum(currentPlan.databasesReads) : 'Unlimited'}`, - price: formatCurrency(project.databasesReads.amount || 0) + usage: `${formatNum(project.databasesReads?.value ?? 0)} / ${currentPlan?.databasesReads ? formatNum(currentPlan.databasesReads) : 'Unlimited'}`, + price: formatCurrency(project.databasesReads?.amount ?? 0) @@ - progressData: createProgressData( - project.databasesReads.value || 0, + progressData: createProgressData( + project.databasesReads?.value ?? 0, currentPlan?.databasesReads ), @@ - usage: `${formatNum(project.databasesWrites.value || 0)} / ${currentPlan?.databasesWrites ? formatNum(currentPlan.databasesWrites) : 'Unlimited'}`, - price: formatCurrency(project.databasesWrites.amount || 0) + usage: `${formatNum(project.databasesWrites?.value ?? 0)} / ${currentPlan?.databasesWrites ? formatNum(currentPlan.databasesWrites) : 'Unlimited'}`, + price: formatCurrency(project.databasesWrites?.amount ?? 0) @@ - progressData: createProgressData( - project.databasesWrites.value || 0, + progressData: createProgressData( + project.databasesWrites?.value ?? 0, currentPlan?.databasesWrites ), @@ - usage: `${formatNum(project.executions.value || 0)} / ${currentPlan?.executions ? formatNum(currentPlan.executions) : 'Unlimited'}`, - price: formatCurrency(project.executions.amount || 0) + usage: `${formatNum(project.executions?.value ?? 0)} / ${currentPlan?.executions ? formatNum(currentPlan.executions) : 'Unlimited'}`, + price: formatCurrency(project.executions?.amount ?? 0) @@ - progressData: createProgressData( - project.executions.value || 0, + progressData: createProgressData( + project.executions?.value ?? 0, currentPlan?.executions ), @@ - usage: `${formatHumanSize(project.storage.value || 0)} / ${currentPlan?.storage?.toString() || '0'} GB`, - price: formatCurrency(project.storage.amount || 0) + usage: `${formatHumanSize(project.storage?.value ?? 0)} / ${currentPlan?.storage ? `${currentPlan.storage} GB` : 'Unlimited'}`, + price: formatCurrency(project.storage?.amount ?? 0) @@ - progressData: createStorageProgressData( - project.storage.value || 0, - currentPlan?.storage || 0 - ), + progressData: currentPlan?.storage + ? createStorageProgressData(project.storage?.value ?? 0, currentPlan.storage) + : [], @@ - usage: `${formatNum(project.gbHours.value || 0)} / ${currentPlan?.GBHours ? formatNum(currentPlan.GBHours) : 'Unlimited'}`, - price: formatCurrency(project.gbHours.amount || 0) + usage: `${formatNum(project.gbHours?.value ?? 0)} / ${currentPlan?.GBHours ? formatNum(currentPlan.GBHours) : 'Unlimited'}`, + price: formatCurrency(project.gbHours?.amount ?? 0) @@ - ? createProgressData(project.gbHours.value || 0, currentPlan.GBHours) + ? createProgressData(project.gbHours?.value ?? 0, currentPlan.GBHours) @@ - usage: `${formatNum(project.authPhone.value || 0)} SMS messages`, - price: formatCurrency(project.authPhone.amount || 0) + usage: `${formatNum(project.authPhone?.value ?? 0)} SMS messages`, + price: formatCurrency(project.authPhone?.amount ?? 0)Also applies to: 211-222, 227-236, 239-250, 255-264, 270-279, 286-293, 299-301
304-311
: Remove {@html} injection; render links safely to prevent XSSStop injecting HTML strings and checking via includes('<a href='). Pass href separately and render .
- { - id: `project-${project.projectId}-usage-details`, - cells: { - item: `<a href="${base}/project-${project.region}-${project.projectId}/settings/usage" style="text-decoration: underline; color: var(--fgcolor-accent-neutral);">Usage details</a>`, - usage: '', - price: '' - } - } + { + id: `project-${project.projectId}-usage-details`, + cells: { item: 'Usage details', usage: '', price: '' }, + href: `${base}/project-${project.region}-${project.projectId}/settings/usage` + }- {#if child.cells?.[col.id]?.includes('<a href=')} - {@html child.cells?.[col.id] ?? ''} + {#if col.id === 'item' && child.href} + <a href={child.href} style="text-decoration: underline; color: var(--fgcolor-accent-neutral);"> + {row.cells?.item === 'Usage details' ? 'Usage details' : (child.cells?.[col.id] ?? '')} + </a>- {#if child.cells?.[col.id]?.includes(' / ')} + {#if typeof child.cells?.[col.id] === 'string' && child.cells?.[col.id]?.includes(' / ')}Also applies to: 402-404, 418-436
332-335
: Fix NaN total when availableCredit is undefinedUse nullish coalescing to avoid subtracting undefined.
- $: totalAmount = Math.max( - (currentAggregation?.amount || currentPlan?.price || 0) - availableCredit, - 0 - ); + $: totalAmount = Math.max( + (currentAggregation?.amount || currentPlan?.price || 0) - (availableCredit ?? 0), + 0 + );
462-501
: Duplicate row id “total-row” breaks state/ARIABoth summary rows use id="total-row". Make them unique.
- <ExpandableTable.Row {root} id="total-row" expandable={false}> + <ExpandableTable.Row {root} id="credits-row" expandable={false}>Also applies to: 503-534
270-279
: Show “Unlimited” storage and skip progress when uncappedAvoid “/ 0 GB” and zeroed bars when there’s no storage cap.
- usage: `${formatHumanSize(project.storage.value || 0)} / ${currentPlan?.storage?.toString() || '0'} GB`, - price: formatCurrency(project.storage.amount || 0) + usage: `${formatHumanSize(project.storage?.value ?? 0)} / ${currentPlan?.storage ? `${currentPlan.storage} GB` : 'Unlimited'}`, + price: formatCurrency(project.storage?.amount ?? 0) @@ - progressData: createStorageProgressData( - project.storage.value || 0, - currentPlan?.storage || 0 - ), - maxValue: currentPlan?.storage - ? currentPlan.storage * 1000 * 1000 * 1000 - : 0 + progressData: currentPlan?.storage + ? createStorageProgressData(project.storage?.value ?? 0, currentPlan.storage) + : [], + maxValue: currentPlan?.storage + ? currentPlan.storage * 1024 * 1024 * 1024 + : null
62-66
: Broaden createProgressData signature to match call sitesYou pass
number | undefined
to maxValue; include null/undefined in the type.- function createProgressData( - currentValue: number, - maxValue: number | string + function createProgressData( + currentValue: number, + maxValue: number | string | null | undefined ): Array<{ size: number; color: string; tooltip?: { title: string; label: string } }> {
🧹 Nitpick comments (1)
src/routes/(console)/organization-[organization]/billing/planSummary.svelte (1)
53-56
: Prefer shared name formatter over local truncate helperUse the existing helper for consistency across the Console.
- import { formatNum } from '$lib/helpers/string'; + import { formatNum, formatName as formatNameHelper } from '$lib/helpers/string';-function truncateForSmall(name: string): string { - if (!name) return name; - return name.length > 12 ? `${name.slice(0, 12)}…` : name; -} +// use formatNameHelper instead of local truncation- item: $isSmallViewport - ? truncateForSmall(project.name) - : project.name || `Project ${project.projectId}`, + item: $isSmallViewport + ? formatNameHelper(project.name) + : project.name || `Project ${project.projectId}`,Also applies to: 186-189, 19-21
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
src/routes/(console)/organization-[organization]/billing/planSummary.svelte
(4 hunks)
🔇 Additional comments (1)
src/routes/(console)/organization-[organization]/billing/planSummary.svelte (1)
32-37
: Confirm column width tokens (“fr”) align with Pink table conventionsOther tables avoid custom fr widths; ensure consistency or switch to library defaults if required.
const maxSize = humanFileSize(maxGB * 1000 * 1000 * 1000); | ||
return `${currentSize.value} ${currentSize.unit} / ${maxSize.value} ${maxSize.unit}`; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix 1000-based byte conversions and treat “unlimited” as null for progress bars
Use 1024^3 consistently and avoid rendering progress for unlimited caps by using null maxValue.
- const maxSize = humanFileSize(maxGB * 1000 * 1000 * 1000);
+ const maxSize = humanFileSize(maxGB * 1024 * 1024 * 1024);
- const maxBytes = maxGB * 1000 * 1000 * 1000;
+ const maxBytes = maxGB * 1024 * 1024 * 1024;
- maxValue: currentPlan?.bandwidth
- ? currentPlan.bandwidth * 1000 * 1000 * 1000
- : 0
+ maxValue: currentPlan?.bandwidth
+ ? currentPlan.bandwidth * 1024 * 1024 * 1024
+ : null
- maxValue: currentPlan?.storage
- ? currentPlan.storage * 1000 * 1000 * 1000
- : 0
+ maxValue: currentPlan?.storage
+ ? currentPlan.storage * 1024 * 1024 * 1024
+ : null
Also applies to: 98-101, 205-207, 277-279
🤖 Prompt for AI Agents
In src/routes/(console)/organization-[organization]/billing/planSummary.svelte
around lines 49-51 (and likewise update lines 98-101, 205-207, 277-279), the
size conversion uses 1000^3 and always returns a numeric max which causes
progress bars to render even for “unlimited” caps; change the multiplier to 1024
** 3 for GiB conversions and, where a plan cap is unlimited (or uses a sentinel
like -1/null/Infinity), return null for maxValue (or set max to null) so
progress bars don’t render — update humanFileSize calls and the logic that forms
the returned object/string to use 1024**3 and to produce null for unlimited caps
across all listed line ranges.
…ive-projects-ui-change
What does this PR do?
(Provide a description of what this PR does.)
Test Plan
Related PRs and Issues
(If this PR is related to any other PR or resolves any issue or related to any issue link all related PR and issues here.)
Have you read the Contributing Guidelines on issues?
(Write your answer here.)
Summary by CodeRabbit
New Features
Style
Chores