Skip to content

Conversation

HarshMN2345
Copy link
Member

@HarshMN2345 HarshMN2345 commented Aug 25, 2025

What does this PR do?

(Provide a description of what this PR does.)

Test Plan

image

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

    • Archived projects section with unarchive/migrate actions, badges, region display, and confirmation flows.
    • Dynamic plan selection from server and optional disabling of Free plan per org.
    • Downgrade flow UI with usage-limits selector and archiving-date preview.
    • Per-project billing estimates with expandable breakdowns and usage links.
    • Feature-flagged billing UI for per-project pricing; new EstimatedCard and standardized name truncation.
  • Style

    • Conditional “View usage” buttons, Usage tab gating, executions text tweak, z-index fix, simplified Invite gating, PRO shows “Unlimited seats”.
  • Chores

    • Updated package sources for UI dependencies and added plan loading on organization pages.

Copy link

appwrite bot commented Aug 25, 2025

Console

Project ID: 688b7bf400350cbd60e9

Sites (2)
Site Status Logs Preview QR
 console-qa
688b7cf6003b1842c9dc
Ready Ready View Logs Preview URL QR Code
 console-cloud
688b7c18002b9b871a8f
Ready Ready View Logs Preview URL QR Code

Note

You can use Avatars API to generate QR code for any text or URLs.

@HarshMN2345 HarshMN2345 requested a review from ItzNotABug August 25, 2025 11:53
Copy link

@coderabbitai coderabbitai bot left a 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: Type storageTotal input.

-    function storageTotal(p: any): number {
+    function storageTotal(p: Breakdown | undefined): number {

198-217: Bandwidth units now consistent (bytes↔GB). LGTM.


22-23: Replace any 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 block NaN.

-    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 type planPricing in calculateResourcePrice.

Prevents Infinity/NaN when value or price 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 5e134e2 and a8d3f02.

📒 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’s usage.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-existent usage.GBHours.included as the limit—GB-hours is pay-as-you-go with no free threshold. Display Unlimited for the denominator and pass 0 as the included limit to calculateResourcePrice.

Likely an incorrect or invalid review comment.

Comment on lines 299 to 314
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
},
Copy link

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.

Suggested change
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.

Copy link

@coderabbitai coderabbitai bot left a 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.limit

Indexing after plan?.['addons'] can throw if addons 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 robustness

Minor 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 expression

Equivalent and a bit clearer.

-    $: isButtonDisabled = isCloud ? isFreeWithMembers : false;
+    $: isButtonDisabled = isCloud && isFreeWithMembers;

92-97: Tooltip else-branch is now unreachable

With 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 members
src/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 limits

Avoid 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.

📥 Commits

Reviewing files that changed from the base of the PR and between a8d3f02 and 49ad425.

📒 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 logic

Matches getServiceLimit('members') returning Infinity for PRO/SCALE. No issues.

src/lib/stores/billing.ts (2)

242-242: New store useNewPricingModal — LGTM

Derived flag is straightforward and side-effect free.


401-403: Members overflow calc — LGTM

Infinity 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>
Copy link

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.

Suggested change
<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.

Comment on lines +342 to 344
if (!org.projects) return;
if (org.projects.length > 0) return;

Copy link

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.

Suggested change
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.

Copy link

@coderabbitai coderabbitai bot left a 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: Replace any 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 for getResourceAmount.

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 -C2

Also 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 on progressData.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.

📥 Commits

Reviewing files that changed from the base of the PR and between 49ad425 and ceed8f2.

📒 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.

Comment on lines +72 to +100
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()}`
}
}
];
}
Copy link

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.

Suggested change
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.

Comment on lines 251 to 264
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
},
Copy link

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.

Suggested change
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.

Copy link

@coderabbitai coderabbitai bot left a 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: Replace any[] 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: Make orgUsage optional to match undefined 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.

📥 Commits

Reviewing files that changed from the base of the PR and between ceed8f2 and 1228ee2.

📒 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}
Copy link
Member

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

{#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
{#each currentAggregation.projectBreakdown as projectBreakdown}
<Accordion title="{projectBreakdown.name}">

I think you have reverted back these changes

Copy link
Member

@lohanidamodar lohanidamodar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image I don't think we need to so no-projects found here, if no projects found we can skip, as it might be 1. user has no projects 2. no projects have any overages


$: projectsList = organizationUsage?.projects?.length
? organizationUsage.projects
: (currentAggregation?.projectBreakdown || []).map((p) => {
Copy link
Member

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
Copy link
Member

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?

Copy link

@coderabbitai coderabbitai bot left a 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: Replace any 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: Type getResourceAmount and its inputs
Remove any, 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 pass undefined; 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 use currentPlan.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 dedicated href 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 1228ee2 and 2530009.

📒 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 from billingData if desired.

Copy link

@coderabbitai coderabbitai bot left a 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: Broaden createProgressData 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 cap

Prevents “/ 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: Remove any in .find callback; let TS infer the project type

Eliminates 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 limit

Divide 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 safely

String‑injecting <a> is XSS‑prone and brittle. Carry a structured href 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: Replace any props with concrete types to satisfy ESLint and prevent runtime mistakes

Define 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 2530009 and 637de1c.

📒 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 — LGTM

Gating 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 — LGTM

Matches 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 — LGTM

Condition preserves prior behavior when currentPlan is absent and hides the tab when usagePerProject is true.

Comment on lines 34 to 36
$: projectIdToBreakdown = new Map(
(currentAggregation?.projectBreakdown || []).map((p) => [p.$id, p])
);
Copy link

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.

Copy link

@coderabbitai coderabbitai bot left a 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 637de1c and 7b68969.

📒 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

Comment on lines 85 to 96
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
};
}

Copy link

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.

Comment on lines 65 to 74
// 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;
}
Copy link

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.

Comment on lines 316 to 329
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
},
Copy link

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.

Comment on lines +50 to +55
const executionsResource = projectSpecificData.resources?.find?.(
(r: any) => r.resourceId === 'executions'
);
if (executionsResource) {
usage.executionsTotal = executionsResource.value || usage.executionsTotal;
}
Copy link

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.

Suggested change
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.

Comment on lines +80 to +81
const org = $organization;
const plan = $currentPlan;
Copy link
Member

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?

Comment on lines +24 to +25
// Check for Infinity or very large numbers
const isUnlimited = count === Infinity || count >= Number.MAX_SAFE_INTEGER;
Copy link
Member

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[];
Copy link
Member

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
Copy link
Member

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;">
Copy link
Member

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
Copy link
Member

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

Comment on lines +132 to +134
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);
Copy link
Member

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.

Comment on lines +29 to +31
{ id: 'item', align: 'left' as const, width: '10fr' },
{ id: 'usage', align: 'left' as const, width: '20fr' },
{ id: 'price', align: 'right' as const, width: '0fr' }
Copy link
Member

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.

Comment on lines +86 to +88
function getProgressColor(_percentage: number): string {
return 'var(--bgcolor-neutral-invert)';
}
Copy link
Member

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 = [
Copy link
Member

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.

Copy link

@coderabbitai coderabbitai bot left a 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 likely AggregationBreakdown) exist in Models.*. 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 InvoiceUsage

Add 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 sites

You 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 maxValue

You 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 fields

Add JSDoc (e.g., “reads per month”, “GB‑hours”, etc.) so consumers don’t misinterpret units, especially for GBHours, databasesReads/Writes, and usagePerProject.

Also applies to: 409-417, 432-433


577-591: Duplicate endpoint: listPlans and getPlansInfo; normalize verb

Both hit /console/plans. Delegate getPlansInfo to listPlans 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 correctly

Use 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 7b68969 and f3c4246.

⛔ 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

Comment on lines +186 to +205
// 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: {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Copy link

@coderabbitai coderabbitai bot left a 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 for excess 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=" -C2

Also applies to: 394-396


56-60: Broaden createProgressData signature to match call sites and avoid TS errors.

maxValue is passed as number | 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. Pass href 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: Make plan 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 in maxValue 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.

📥 Commits

Reviewing files that changed from the base of the PR and between f3c4246 and 7b5e65d.

📒 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';
Copy link

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.

Suggested change
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.

Comment on lines +54 to +55
usage.executionsTotal = executionsResource.value || usage.executionsTotal;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
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.

Copy link

@coderabbitai coderabbitai bot left a 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.
Indexing plan?.['addons']['seats'] throws when addons 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 to 0 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 7b5e65d and 6e23bb5.

📒 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.

Copy link

@coderabbitai coderabbitai bot left a 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: verify fr usage matches other tables.

Prior feedback suggested avoiding fr for consistency; consider auto or minmax 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: Broaden createProgressData 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 6e23bb5 and f03aa84.

📒 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.

Comment on lines +44 to +51
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}`;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines +92 to +114
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`
}
}
];
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines +196 to +207
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines +211 to +216
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)
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Comment on lines +304 to +311
{
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: ''
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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

Comment on lines +332 to +335
$: totalAmount = Math.max(
(currentAggregation?.amount || currentPlan?.price || 0) - availableCredit,
0
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
$: 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.

Comment on lines +462 to +499
<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>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Copy link

@coderabbitai coderabbitai bot left a 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 crashes

Direct .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 XSS

Stop 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 undefined

Use 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/ARIA

Both 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 uncapped

Avoid “/ 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 sites

You 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 helper

Use 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.

📥 Commits

Reviewing files that changed from the base of the PR and between f03aa84 and 333ad68.

📒 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 conventions

Other tables avoid custom fr widths; ensure consistency or switch to library defaults if required.

Comment on lines +49 to +51
const maxSize = humanFileSize(maxGB * 1000 * 1000 * 1000);
return `${currentSize.value} ${currentSize.unit} / ${maxSize.value} ${maxSize.unit}`;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants